Skip to content

Commit 11df3db

Browse files
authored
Merge pull request #305 from himmelblau-idm/dmulder/fido
Add Fido MFA
2 parents 2fb7ca5 + efda7ba commit 11df3db

File tree

10 files changed

+386
-24
lines changed

10 files changed

+386
-24
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ sudo zypper ref && sudo zypper in himmelblau nss-himmelblau pam-himmelblau
6767

6868
The following packages are required on openSUSE to build and test this package.
6969

70-
sudo zypper in make cargo git gcc sqlite3-devel libopenssl-3-devel pam-devel libcap-devel libtalloc-devel libtevent-devel libldb-devel libdhash-devel krb5-devel pcre2-devel libclang13 autoconf make automake gettext-tools clang dbus-1-devel utf8proc-devel gobject-introspection-devel cairo-devel gdk-pixbuf-devel libsoup-devel pango-devel atk-devel gtk3-devel webkit2gtk3-devel
70+
sudo zypper in make cargo git gcc sqlite3-devel libopenssl-3-devel pam-devel libcap-devel libtalloc-devel libtevent-devel libldb-devel libdhash-devel krb5-devel pcre2-devel libclang13 autoconf make automake gettext-tools clang dbus-1-devel utf8proc-devel gobject-introspection-devel cairo-devel gdk-pixbuf-devel libsoup-devel pango-devel atk-devel gtk3-devel webkit2gtk3-devel libudev-devel mercurial python311-gyp
7171

7272

7373
Or on Debian based systems:

src/cli/src/main.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,8 @@ async fn main() -> ExitCode {
179179
};
180180
let pin_min_len = cfg.get_hello_pin_min_length();
181181

182-
let mut req = ClientRequest::PamAuthenticateInit(account_id.clone());
182+
let mut req =
183+
ClientRequest::PamAuthenticateInit(account_id.clone(), "aad-tool".to_string());
183184
loop {
184185
match_sm_auth_client_response!(daemon_client.call_and_wait(&req, timeout), req, pin_min_len,
185186
ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Password) => {

src/common/src/idprovider/himmelblau.rs

+70-4
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ use himmelblau::auth::{BrokerClientApplication, UserToken as UnixUserToken};
3131
use himmelblau::discovery::EnrollAttrs;
3232
use himmelblau::error::{MsalError, DEVICE_AUTH_FAIL};
3333
use himmelblau::graph::{DirectoryObject, Graph};
34-
use himmelblau::MFAAuthContinue;
34+
use himmelblau::{AuthOption, MFAAuthContinue};
3535
use idmap::Idmap;
3636
use kanidm_hsm_crypto::{LoadableIdentityKey, LoadableMsOapxbcRsaKey, PinValue, SealedData, Tpm};
3737
use reqwest;
@@ -362,6 +362,7 @@ impl IdProvider for HimmelblauMultiProvider {
362362
async fn unix_user_online_auth_step<D: KeyStoreTxn + Send>(
363363
&self,
364364
account_id: &str,
365+
service: &str,
365366
cred_handler: &mut AuthCredHandler,
366367
pam_next_req: PamAuthRequest,
367368
keystore: &mut D,
@@ -377,6 +378,7 @@ impl IdProvider for HimmelblauMultiProvider {
377378
provider
378379
.unix_user_online_auth_step(
379380
account_id,
381+
service,
380382
cred_handler,
381383
pam_next_req,
382384
keystore,
@@ -835,6 +837,7 @@ impl IdProvider for HimmelblauProvider {
835837
async fn unix_user_online_auth_step<D: KeyStoreTxn + Send>(
836838
&self,
837839
account_id: &str,
840+
service: &str,
838841
cred_handler: &mut AuthCredHandler,
839842
pam_next_req: PamAuthRequest,
840843
keystore: &mut D,
@@ -1000,14 +1003,17 @@ impl IdProvider for HimmelblauProvider {
10001003
// enrolled but creating a new Hello Pin, we follow the same process,
10011004
// since only an enrollment token can be exchanged for a PRT (which
10021005
// will be needed to enroll the Hello Pin).
1006+
let mut opts = vec![];
1007+
// Prohibit Fido over ssh (since it can't work)
1008+
if service != "ssh" {
1009+
opts.push(AuthOption::Fido);
1010+
}
10031011
let mresp = self
10041012
.client
10051013
.write()
10061014
.await
10071015
.initiate_acquire_token_by_mfa_flow_for_device_enrollment(
1008-
account_id,
1009-
&cred,
1010-
vec![],
1016+
account_id, &cred, opts,
10111017
)
10121018
.await;
10131019
// We need to wait to handle the response until after we've released
@@ -1064,6 +1070,25 @@ impl IdProvider for HimmelblauProvider {
10641070
}
10651071
};
10661072
match resp.mfa_method.as_str() {
1073+
"FidoKey" => {
1074+
let fido_challenge =
1075+
resp.fido_challenge.clone().ok_or(IdpError::BadRequest)?;
1076+
1077+
let fido_allow_list =
1078+
resp.fido_allow_list.clone().ok_or(IdpError::BadRequest)?;
1079+
*cred_handler = AuthCredHandler::MFA { flow: resp };
1080+
return Ok((
1081+
AuthResult::Next(AuthRequest::Fido {
1082+
fido_allow_list,
1083+
fido_challenge,
1084+
}),
1085+
/* An MFA auth cannot cache the password. This would
1086+
* lead to a potential downgrade to SFA attack (where
1087+
* the attacker auths with a stolen password, then
1088+
* disconnects the network to complete the auth). */
1089+
AuthCacheAction::None,
1090+
));
1091+
}
10671092
"PhoneAppOTP" | "OneWaySMS" | "ConsolidatedTelephony" => {
10681093
let msg = resp.msg.clone();
10691094
*cred_handler = AuthCredHandler::MFA { flow: resp };
@@ -1210,6 +1235,47 @@ impl IdProvider for HimmelblauProvider {
12101235
Err(e) => Err(e),
12111236
}
12121237
}
1238+
(AuthCredHandler::MFA { ref mut flow }, PamAuthRequest::Fido { assertion }) => {
1239+
let token = self
1240+
.client
1241+
.write()
1242+
.await
1243+
.acquire_token_by_mfa_flow(account_id, Some(&assertion), None, flow)
1244+
.await
1245+
.map_err(|e| {
1246+
error!("{:?}", e);
1247+
IdpError::NotFound
1248+
})?;
1249+
let token2 = enroll_and_obtain_enrolled_token!(token);
1250+
match self.token_validate(account_id, &token2).await {
1251+
Ok(AuthResult::Success { token: token3 }) => {
1252+
// Skip Hello enrollment if it is disabled by config
1253+
let hello_enabled = self.config.read().await.get_enable_hello();
1254+
if !hello_enabled {
1255+
info!("Skipping Hello enrollment because it is disabled");
1256+
return Ok((
1257+
AuthResult::Success { token: token3 },
1258+
AuthCacheAction::None,
1259+
));
1260+
}
1261+
1262+
// Setup Windows Hello
1263+
*cred_handler = AuthCredHandler::SetupPin { token };
1264+
return Ok((
1265+
AuthResult::Next(AuthRequest::SetupPin {
1266+
msg: format!(
1267+
"Set up a PIN\n {}{}",
1268+
"A Hello PIN is a fast, secure way to sign",
1269+
"in to your device, apps, and services."
1270+
),
1271+
}),
1272+
AuthCacheAction::None,
1273+
));
1274+
}
1275+
Ok(auth_result) => Ok((auth_result, AuthCacheAction::None)),
1276+
Err(e) => Err(e),
1277+
}
1278+
}
12131279
_ => {
12141280
error!("Unexpected AuthCredHandler and PamAuthRequest pairing");
12151281
Err(IdpError::NotFound)

src/common/src/idprovider/interface.rs

+12
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ pub enum AuthRequest {
104104
msg: String,
105105
},
106106
Pin,
107+
Fido {
108+
fido_challenge: String,
109+
fido_allow_list: Vec<String>,
110+
},
107111
}
108112

109113
#[allow(clippy::from_over_into)]
@@ -122,6 +126,13 @@ impl Into<PamAuthResponse> for AuthRequest {
122126
AuthRequest::MFAPollWait => PamAuthResponse::MFAPollWait,
123127
AuthRequest::SetupPin { msg } => PamAuthResponse::SetupPin { msg },
124128
AuthRequest::Pin => PamAuthResponse::Pin,
129+
AuthRequest::Fido {
130+
fido_challenge,
131+
fido_allow_list,
132+
} => PamAuthResponse::Fido {
133+
fido_challenge,
134+
fido_allow_list,
135+
},
125136
}
126137
}
127138
}
@@ -219,6 +230,7 @@ pub trait IdProvider {
219230
async fn unix_user_online_auth_step<D: KeyStoreTxn + Send>(
220231
&self,
221232
_account_id: &str,
233+
_service: &str,
222234
_cred_handler: &mut AuthCredHandler,
223235
_pam_next_req: PamAuthRequest,
224236
_keystore: &mut D,

src/common/src/resolver.rs

+16-1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ enum CacheState {
5757
pub enum AuthSession {
5858
InProgress {
5959
account_id: String,
60+
service: String,
6061
id: Id,
6162
token: Option<Box<UserToken>>,
6263
online_at_init: bool,
@@ -996,6 +997,7 @@ where
996997
pub async fn pam_account_authenticate_init(
997998
&self,
998999
account_id: &str,
1000+
service: &str,
9991001
shutdown_rx: broadcast::Receiver<()>,
10001002
) -> Result<(AuthSession, PamAuthResponse), ()> {
10011003
// Setup an auth session. If possible bring the resolver online.
@@ -1042,6 +1044,7 @@ where
10421044
Ok((next_req, cred_handler)) => {
10431045
let auth_session = AuthSession::InProgress {
10441046
account_id: account_id.to_string(),
1047+
service: service.to_string(),
10451048
id,
10461049
token: token.map(Box::new),
10471050
online_at_init,
@@ -1078,6 +1081,7 @@ where
10781081
(
10791082
&mut AuthSession::InProgress {
10801083
ref account_id,
1084+
ref service,
10811085
id: _,
10821086
token: _,
10831087
online_at_init: true,
@@ -1093,6 +1097,7 @@ where
10931097
.client
10941098
.unix_user_online_auth_step(
10951099
account_id,
1100+
service,
10961101
cred_handler,
10971102
pam_next_req,
10981103
&mut dbtxn,
@@ -1143,6 +1148,7 @@ where
11431148
(
11441149
&mut AuthSession::InProgress {
11451150
ref account_id,
1151+
service: _,
11461152
id: _,
11471153
token: Some(ref token),
11481154
online_at_init,
@@ -1216,6 +1222,10 @@ where
12161222
// AuthCredHandler::None is invalid with SetupPin
12171223
return Err(());
12181224
}
1225+
(AuthCredHandler::None, PamAuthRequest::Fido { .. }) => {
1226+
// AuthCredHandler::None is invalid with Fido
1227+
return Err(());
1228+
}
12191229
}
12201230
}
12211231
(&mut AuthSession::InProgress { token: None, .. }, _) => {
@@ -1267,12 +1277,13 @@ where
12671277
pub async fn pam_account_authenticate(
12681278
&self,
12691279
account_id: &str,
1280+
service: &str,
12701281
password: &str,
12711282
) -> Result<Option<bool>, ()> {
12721283
let (_shutdown_tx, shutdown_rx) = broadcast::channel(1);
12731284

12741285
let mut auth_session = match self
1275-
.pam_account_authenticate_init(account_id, shutdown_rx)
1286+
.pam_account_authenticate_init(account_id, service, shutdown_rx)
12761287
.await?
12771288
{
12781289
(auth_session, PamAuthResponse::Password) => {
@@ -1299,6 +1310,10 @@ where
12991310
// Can continue!
13001311
auth_session
13011312
}
1313+
(auth_session, PamAuthResponse::Fido { .. }) => {
1314+
// Can continue!
1315+
auth_session
1316+
}
13021317
(_, PamAuthResponse::Unknown) => return Ok(None),
13031318
(_, PamAuthResponse::Denied) => return Ok(Some(false)),
13041319
(_, PamAuthResponse::Success) => {

src/common/src/unix_proto.rs

+10-3
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@ pub enum PamAuthResponse {
5050
msg: String,
5151
},
5252
Pin,
53-
// CTAP2
53+
/// PAM must generate a Fido assertion
54+
Fido {
55+
fido_challenge: String,
56+
fido_allow_list: Vec<String>,
57+
},
5458
}
5559

5660
#[derive(Serialize, Deserialize, Debug)]
@@ -60,6 +64,7 @@ pub enum PamAuthRequest {
6064
MFAPoll { poll_attempt: u32 },
6165
SetupPin { pin: String },
6266
Pin { cred: String },
67+
Fido { assertion: String },
6368
}
6469

6570
#[derive(Serialize, Deserialize, Debug)]
@@ -70,7 +75,7 @@ pub enum ClientRequest {
7075
NssGroups,
7176
NssGroupByGid(u32),
7277
NssGroupByName(String),
73-
PamAuthenticateInit(String),
78+
PamAuthenticateInit(String, String),
7479
PamAuthenticateStep(PamAuthRequest),
7580
PamAccountAllowed(String),
7681
PamAccountBeginSession(String),
@@ -90,7 +95,9 @@ impl ClientRequest {
9095
ClientRequest::NssGroups => "NssGroups".to_string(),
9196
ClientRequest::NssGroupByGid(id) => format!("NssGroupByGid({})", id),
9297
ClientRequest::NssGroupByName(id) => format!("NssGroupByName({})", id),
93-
ClientRequest::PamAuthenticateInit(id) => format!("PamAuthenticateInit({})", id),
98+
ClientRequest::PamAuthenticateInit(id, service) => {
99+
format!("PamAuthenticateInit({}, {})", id, service)
100+
}
94101
ClientRequest::PamAuthenticateStep(_) => "PamAuthenticateStep".to_string(),
95102
ClientRequest::PamAccountAllowed(id) => {
96103
format!("PamAccountAllowed({})", id)

src/daemon/src/daemon.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ async fn handle_client(
291291
ClientResponse::NssGroup(None)
292292
})
293293
}
294-
ClientRequest::PamAuthenticateInit(account_id) => {
294+
ClientRequest::PamAuthenticateInit(account_id, service) => {
295295
debug!("pam authenticate init");
296296

297297
match &pam_auth_session_state {
@@ -306,6 +306,7 @@ async fn handle_client(
306306
match cachelayer
307307
.pam_account_authenticate_init(
308308
account_id.as_str(),
309+
service.as_str(),
309310
shutdown_tx.subscribe(),
310311
)
311312
.await

src/pam/Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ tracing = { workspace = true }
2626
himmelblau_unix_common.workspace = true
2727
tokio.workspace = true
2828
libhimmelblau.workspace = true
29+
authenticator = { version = "0.4.1", default-features = false, features = ["crypto_openssl"] }
30+
base64.workspace = true
31+
serde_json.workspace = true
32+
sha2 = "0.10.8"
2933

3034
[build-dependencies]
3135
pkg-config.workspace = true

src/pam/src/pam/conv.rs

+6
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ struct PamResponse {
5151
/// Communication is mediated by the pam client (the application that invoked
5252
/// pam). Messages sent will be relayed to the user by the client, and response
5353
/// will be relayed back.
54+
#[derive(Clone)]
5455
#[repr(C)]
5556
pub struct PamConv {
5657
conv: extern "C" fn(
@@ -108,3 +109,8 @@ impl PamItem for PamConv {
108109
PAM_CONV
109110
}
110111
}
112+
113+
// PamConv isn't really thread safe, but we mark it as such so that we can
114+
// wrap it in a Arc<Mutex> later for passing between threads.
115+
unsafe impl Send for PamConv {}
116+
unsafe impl Sync for PamConv {}

0 commit comments

Comments
 (0)