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)]