From bec8c9058c5d903954eb9c91e28b6caed5c66d0b Mon Sep 17 00:00:00 2001 From: David Mulder Date: Thu, 4 Apr 2024 16:50:37 -0600 Subject: [PATCH] Windows Hello Authentication requirements (#2688) * Add keystore to unix_user_online_auth_init Himmelblau needs this to check whether the device is enrolled in the domain (via the presence of TPM keys), to know whether to attempt Windows Hello PIN auth, or to enroll first in the domain. Signed-off-by: David Mulder * Implement PIN setup After enrolling in a domain, Himmelblau will prompt the user to choose a pin, which will be the auth value for an associated Windows Hello TPM key. We loop here until the values match. Otherwise no validation is performed. Validation can be done by the id provider, and can send an additional request to PAM if the PIN is invalid. Signed-off-by: David Mulder * Add Pin authentication After setting up a Windows Hello pin, users can authentication using this pin. Signed-off-by: David Mulder --- unix_integration/pam_kanidm/src/pam/mod.rs | 95 ++++++++++++++++++++ unix_integration/src/idprovider/interface.rs | 28 ++++-- unix_integration/src/idprovider/kanidm.rs | 16 ++-- unix_integration/src/resolver.rs | 67 ++++++++++---- unix_integration/src/unix_proto.rs | 7 ++ 5 files changed, 186 insertions(+), 27 deletions(-) diff --git a/unix_integration/pam_kanidm/src/pam/mod.rs b/unix_integration/pam_kanidm/src/pam/mod.rs index 8dc552598..7cddb7fdd 100755 --- a/unix_integration/pam_kanidm/src/pam/mod.rs +++ b/unix_integration/pam_kanidm/src/pam/mod.rs @@ -397,6 +397,101 @@ impl PamHooks for PamKanidm { ); } + }, + ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::SetupPin { + msg, + }) => { + match conv.send(PAM_TEXT_INFO, &msg) { + Ok(_) => {} + Err(err) => { + if opts.debug { + println!("Message prompt failed"); + } + return err; + } + } + + let mut pin; + let mut confirm; + loop { + pin = match conv.send(PAM_PROMPT_ECHO_OFF, "New PIN") { + Ok(password) => match password { + Some(cred) => cred, + None => { + debug!("no pin"); + return PamResultCode::PAM_CRED_INSUFFICIENT; + } + }, + Err(err) => { + debug!("unable to get pin"); + return err; + } + }; + + confirm = match conv.send(PAM_PROMPT_ECHO_OFF, "Confirm PIN") { + Ok(password) => match password { + Some(cred) => cred, + None => { + debug!("no confirmation pin"); + return PamResultCode::PAM_CRED_INSUFFICIENT; + } + }, + Err(err) => { + debug!("unable to get confirmation pin"); + return err; + } + }; + + if pin == confirm { + break; + } else { + match conv.send(PAM_TEXT_INFO, "Inputs did not match. Try again.") { + Ok(_) => {} + Err(err) => { + if opts.debug { + println!("Message prompt failed"); + } + return err; + } + } + } + } + + // Now setup the request for the next loop. + timeout = cfg.unix_sock_timeout; + req = ClientRequest::PamAuthenticateStep(PamAuthRequest::SetupPin { + pin, + }); + continue; + }, + ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Pin) => { + let mut consume_authtok = None; + // Swap the authtok out with a None, so it can only be consumed once. + // If it's already been swapped, we are just swapping two null pointers + // here effectively. + std::mem::swap(&mut authtok, &mut consume_authtok); + let cred = if let Some(cred) = consume_authtok { + cred + } else { + match conv.send(PAM_PROMPT_ECHO_OFF, "PIN") { + Ok(password) => match password { + Some(cred) => cred, + None => { + debug!("no pin"); + return PamResultCode::PAM_CRED_INSUFFICIENT; + } + }, + Err(err) => { + debug!("unable to get pin"); + return err; + } + } + }; + + // Now setup the request for the next loop. + timeout = cfg.unix_sock_timeout; + req = ClientRequest::PamAuthenticateStep(PamAuthRequest::Pin { cred }); + continue; } ); } // while true, continue calling PamAuthenticateStep until we get a decision. diff --git a/unix_integration/src/idprovider/interface.rs b/unix_integration/src/idprovider/interface.rs index 22a79e9dc..030de4234 100644 --- a/unix_integration/src/idprovider/interface.rs +++ b/unix_integration/src/idprovider/interface.rs @@ -76,6 +76,8 @@ pub enum AuthCredHandler { MFA { data: Vec, }, + SetupPin, + Pin, } pub enum AuthRequest { @@ -93,6 +95,11 @@ pub enum AuthRequest { polling_interval: u32, }, MFAPollWait, + SetupPin { + /// Message to display to the user. + msg: String, + }, + Pin, } #[allow(clippy::from_over_into)] @@ -112,6 +119,8 @@ impl Into for AuthRequest { polling_interval, }, AuthRequest::MFAPollWait => PamAuthResponse::MFAPollWait, + AuthRequest::SetupPin { msg } => PamAuthResponse::SetupPin { msg }, + AuthRequest::Pin => PamAuthResponse::Pin, } } } @@ -161,10 +170,11 @@ pub trait IdProvider { _machine_key: &tpm::MachineKey, ) -> Result; - async fn unix_user_online_auth_init( + async fn unix_user_online_auth_init( &self, _account_id: &str, _token: Option<&UserToken>, + _keystore: &mut D, _tpm: &mut tpm::BoxedDynTpm, _machine_key: &tpm::MachineKey, _shutdown_rx: &broadcast::Receiver<()>, @@ -181,13 +191,13 @@ pub trait IdProvider { _shutdown_rx: &broadcast::Receiver<()>, ) -> Result<(AuthResult, AuthCacheAction), IdpError>; - async fn unix_user_offline_auth_init( + async fn unix_user_offline_auth_init( &self, _account_id: &str, _token: Option<&UserToken>, + _keystore: &mut D, ) -> Result<(AuthRequest, AuthCredHandler), IdpError>; - /* // I thought about this part of the interface a lot. we could have the // provider actually need to check the password or credentials, but then // we need to rework the tpm/crypto engine to be an argument to pass here @@ -202,14 +212,22 @@ pub trait IdProvider { // involved if there is some "custom logic" or similar that is needed but // for now I think making it generic is a good first step and we can change // it later. - async fn unix_user_offline_auth_step( + // + // EDIT 04042024: When we're performing an offline PIN auth, the PIN can + // unlock the associated TPM key. While we can't perform a full request + // for an auth token, we can verify that the PIN successfully unlocks the + // TPM key. + async fn unix_user_offline_auth_step( &self, _account_id: &str, + _token: &UserToken, _cred_handler: &mut AuthCredHandler, _pam_next_req: PamAuthRequest, + _keystore: &mut D, + _tpm: &mut tpm::BoxedDynTpm, + _machine_key: &tpm::MachineKey, _online_at_init: bool, ) -> Result; - */ async fn unix_group_get( &self, diff --git a/unix_integration/src/idprovider/kanidm.rs b/unix_integration/src/idprovider/kanidm.rs index 4ed0aedf5..6fc015756 100644 --- a/unix_integration/src/idprovider/kanidm.rs +++ b/unix_integration/src/idprovider/kanidm.rs @@ -191,10 +191,11 @@ impl IdProvider for KanidmProvider { } } - async fn unix_user_online_auth_init( + async fn unix_user_online_auth_init( &self, _account_id: &str, _token: Option<&UserToken>, + _keystore: &mut D, _tpm: &mut tpm::BoxedDynTpm, _machine_key: &tpm::MachineKey, _shutdown_rx: &broadcast::Receiver<()>, @@ -292,27 +293,30 @@ impl IdProvider for KanidmProvider { } } - async fn unix_user_offline_auth_init( + async fn unix_user_offline_auth_init( &self, _account_id: &str, _token: Option<&UserToken>, + _keystore: &mut D, ) -> Result<(AuthRequest, AuthCredHandler), IdpError> { // Not sure that I need to do much here? Ok((AuthRequest::Password, AuthCredHandler::Password)) } - /* - async fn unix_user_offline_auth_step( + async fn unix_user_offline_auth_step( &self, _account_id: &str, + _token: &UserToken, _cred_handler: &mut AuthCredHandler, _pam_next_req: PamAuthRequest, + _keystore: &mut D, + _tpm: &mut tpm::BoxedDynTpm, + _machine_key: &tpm::MachineKey, _online_at_init: bool, ) -> Result { // We need any cached credentials here. - todo!(); + Err(IdpError::BadRequest) } - */ async fn unix_group_get( &self, diff --git a/unix_integration/src/resolver.rs b/unix_integration/src/resolver.rs index 271087014..a4d214e26 100644 --- a/unix_integration/src/resolver.rs +++ b/unix_integration/src/resolver.rs @@ -893,19 +893,24 @@ where let maybe_err = if online_at_init { let mut hsm_lock = self.hsm.lock().await; + let mut dbtxn = self.db.write().await; + self.client .unix_user_online_auth_init( account_id, token.as_ref(), + &mut dbtxn, hsm_lock.deref_mut(), &self.machine_key, &shutdown_rx, ) .await } else { + let mut dbtxn = self.db.write().await; + // Can the auth proceed offline? self.client - .unix_user_offline_auth_init(account_id, token.as_ref()) + .unix_user_offline_auth_init(account_id, token.as_ref(), &mut dbtxn) .await }; @@ -1013,10 +1018,10 @@ where */ ( &mut AuthSession::InProgress { - account_id: _, + ref account_id, id: _, token: Some(ref token), - online_at_init: _, + online_at_init, ref mut cred_handler, // Only need in online auth. shutdown_rx: _, @@ -1029,9 +1034,9 @@ where // Rather than calling client, should this actually be self // contained to the resolver so that it has generic offline-paths // that are possible? - match (cred_handler, pam_next_req) { + match (&cred_handler, &pam_next_req) { (AuthCredHandler::Password, PamAuthRequest::Password { cred }) => { - match self.check_cache_userpassword(token.uuid, &cred).await { + match self.check_cache_userpassword(token.uuid, cred).await { Ok(true) => Ok(AuthResult::Success { token: *token.clone(), }), @@ -1054,18 +1059,40 @@ where // AuthCredHandler::MFA is invalid for offline auth return Err(()); } - } + (AuthCredHandler::SetupPin, _) => { + // AuthCredHandler::SetupPin is invalid for offline auth + return Err(()); + } + (AuthCredHandler::Pin, PamAuthRequest::Pin { .. }) => { + // The Pin acts as a single device password, and can be + // used to unlock the TPM to validate the authentication. + let mut hsm_lock = self.hsm.lock().await; + let mut dbtxn = self.db.write().await; - /* - self.client - .unix_user_offline_auth_step( - &account_id, - cred_handler, - pam_next_req, - online_at_init, - ) - .await - */ + let auth_result = self + .client + .unix_user_offline_auth_step( + account_id, + token, + cred_handler, + pam_next_req, + &mut dbtxn, + hsm_lock.deref_mut(), + &self.machine_key, + online_at_init, + ) + .await; + + drop(hsm_lock); + dbtxn.commit().map_err(|_| ())?; + + auth_result + } + (AuthCredHandler::Pin, _) => { + // AuthCredHandler::Pin is only valid with a cred provided + return Err(()); + } + } } (&mut AuthSession::InProgress { token: None, .. }, _) => { // Can't do much with offline auth when there is no token ... @@ -1144,6 +1171,14 @@ where // Can continue! auth_session } + (auth_session, PamAuthResponse::SetupPin { .. }) => { + // Can continue! + auth_session + } + (auth_session, PamAuthResponse::Pin) => { + // Can continue! + auth_session + } (_, PamAuthResponse::Unknown) => return Ok(None), (_, PamAuthResponse::Denied) => return Ok(Some(false)), (_, PamAuthResponse::Success) => { diff --git a/unix_integration/src/unix_proto.rs b/unix_integration/src/unix_proto.rs index d0fbf3c76..9da232a7a 100644 --- a/unix_integration/src/unix_proto.rs +++ b/unix_integration/src/unix_proto.rs @@ -51,6 +51,11 @@ pub enum PamAuthResponse { polling_interval: u32, }, MFAPollWait, + /// PAM must prompt for a new PIN and confirm that PIN input + SetupPin { + msg: String, + }, + Pin, // CTAP2 } @@ -60,6 +65,8 @@ pub enum PamAuthRequest { DeviceAuthorizationGrant { data: DeviceAuthorizationResponse }, MFACode { cred: String }, MFAPoll, + SetupPin { pin: String }, + Pin { cred: String }, } #[derive(Serialize, Deserialize, Debug)]