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 <dmulder@samba.org>

* 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 <dmulder@samba.org>

* Add Pin authentication

After setting up a Windows Hello pin, users can
authentication using this pin.

Signed-off-by: David Mulder <dmulder@samba.org>
This commit is contained in:
David Mulder 2024-04-04 16:50:37 -06:00 committed by GitHub
parent 30179e900c
commit bec8c9058c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 186 additions and 27 deletions

View file

@ -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. } // while true, continue calling PamAuthenticateStep until we get a decision.

View file

@ -76,6 +76,8 @@ pub enum AuthCredHandler {
MFA { MFA {
data: Vec<String>, data: Vec<String>,
}, },
SetupPin,
Pin,
} }
pub enum AuthRequest { pub enum AuthRequest {
@ -93,6 +95,11 @@ pub enum AuthRequest {
polling_interval: u32, polling_interval: u32,
}, },
MFAPollWait, MFAPollWait,
SetupPin {
/// Message to display to the user.
msg: String,
},
Pin,
} }
#[allow(clippy::from_over_into)] #[allow(clippy::from_over_into)]
@ -112,6 +119,8 @@ impl Into<PamAuthResponse> for AuthRequest {
polling_interval, polling_interval,
}, },
AuthRequest::MFAPollWait => PamAuthResponse::MFAPollWait, 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, _machine_key: &tpm::MachineKey,
) -> Result<UserToken, IdpError>; ) -> Result<UserToken, IdpError>;
async fn unix_user_online_auth_init( async fn unix_user_online_auth_init<D: KeyStoreTxn + Send>(
&self, &self,
_account_id: &str, _account_id: &str,
_token: Option<&UserToken>, _token: Option<&UserToken>,
_keystore: &mut D,
_tpm: &mut tpm::BoxedDynTpm, _tpm: &mut tpm::BoxedDynTpm,
_machine_key: &tpm::MachineKey, _machine_key: &tpm::MachineKey,
_shutdown_rx: &broadcast::Receiver<()>, _shutdown_rx: &broadcast::Receiver<()>,
@ -181,13 +191,13 @@ pub trait IdProvider {
_shutdown_rx: &broadcast::Receiver<()>, _shutdown_rx: &broadcast::Receiver<()>,
) -> Result<(AuthResult, AuthCacheAction), IdpError>; ) -> Result<(AuthResult, AuthCacheAction), IdpError>;
async fn unix_user_offline_auth_init( async fn unix_user_offline_auth_init<D: KeyStoreTxn + Send>(
&self, &self,
_account_id: &str, _account_id: &str,
_token: Option<&UserToken>, _token: Option<&UserToken>,
_keystore: &mut D,
) -> Result<(AuthRequest, AuthCredHandler), IdpError>; ) -> Result<(AuthRequest, AuthCredHandler), IdpError>;
/*
// I thought about this part of the interface a lot. we could have the // 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 // 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 // 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 // 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 // for now I think making it generic is a good first step and we can change
// it later. // 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<D: KeyStoreTxn + Send>(
&self, &self,
_account_id: &str, _account_id: &str,
_token: &UserToken,
_cred_handler: &mut AuthCredHandler, _cred_handler: &mut AuthCredHandler,
_pam_next_req: PamAuthRequest, _pam_next_req: PamAuthRequest,
_keystore: &mut D,
_tpm: &mut tpm::BoxedDynTpm,
_machine_key: &tpm::MachineKey,
_online_at_init: bool, _online_at_init: bool,
) -> Result<AuthResult, IdpError>; ) -> Result<AuthResult, IdpError>;
*/
async fn unix_group_get( async fn unix_group_get(
&self, &self,

View file

@ -191,10 +191,11 @@ impl IdProvider for KanidmProvider {
} }
} }
async fn unix_user_online_auth_init( async fn unix_user_online_auth_init<D: KeyStoreTxn + Send>(
&self, &self,
_account_id: &str, _account_id: &str,
_token: Option<&UserToken>, _token: Option<&UserToken>,
_keystore: &mut D,
_tpm: &mut tpm::BoxedDynTpm, _tpm: &mut tpm::BoxedDynTpm,
_machine_key: &tpm::MachineKey, _machine_key: &tpm::MachineKey,
_shutdown_rx: &broadcast::Receiver<()>, _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<D: KeyStoreTxn + Send>(
&self, &self,
_account_id: &str, _account_id: &str,
_token: Option<&UserToken>, _token: Option<&UserToken>,
_keystore: &mut D,
) -> Result<(AuthRequest, AuthCredHandler), IdpError> { ) -> Result<(AuthRequest, AuthCredHandler), IdpError> {
// Not sure that I need to do much here? // Not sure that I need to do much here?
Ok((AuthRequest::Password, AuthCredHandler::Password)) Ok((AuthRequest::Password, AuthCredHandler::Password))
} }
/* async fn unix_user_offline_auth_step<D: KeyStoreTxn + Send>(
async fn unix_user_offline_auth_step(
&self, &self,
_account_id: &str, _account_id: &str,
_token: &UserToken,
_cred_handler: &mut AuthCredHandler, _cred_handler: &mut AuthCredHandler,
_pam_next_req: PamAuthRequest, _pam_next_req: PamAuthRequest,
_keystore: &mut D,
_tpm: &mut tpm::BoxedDynTpm,
_machine_key: &tpm::MachineKey,
_online_at_init: bool, _online_at_init: bool,
) -> Result<AuthResult, IdpError> { ) -> Result<AuthResult, IdpError> {
// We need any cached credentials here. // We need any cached credentials here.
todo!(); Err(IdpError::BadRequest)
} }
*/
async fn unix_group_get( async fn unix_group_get(
&self, &self,

View file

@ -893,19 +893,24 @@ where
let maybe_err = if online_at_init { let maybe_err = if online_at_init {
let mut hsm_lock = self.hsm.lock().await; let mut hsm_lock = self.hsm.lock().await;
let mut dbtxn = self.db.write().await;
self.client self.client
.unix_user_online_auth_init( .unix_user_online_auth_init(
account_id, account_id,
token.as_ref(), token.as_ref(),
&mut dbtxn,
hsm_lock.deref_mut(), hsm_lock.deref_mut(),
&self.machine_key, &self.machine_key,
&shutdown_rx, &shutdown_rx,
) )
.await .await
} else { } else {
let mut dbtxn = self.db.write().await;
// Can the auth proceed offline? // Can the auth proceed offline?
self.client 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 .await
}; };
@ -1013,10 +1018,10 @@ where
*/ */
( (
&mut AuthSession::InProgress { &mut AuthSession::InProgress {
account_id: _, ref account_id,
id: _, id: _,
token: Some(ref token), token: Some(ref token),
online_at_init: _, online_at_init,
ref mut cred_handler, ref mut cred_handler,
// Only need in online auth. // Only need in online auth.
shutdown_rx: _, shutdown_rx: _,
@ -1029,9 +1034,9 @@ where
// Rather than calling client, should this actually be self // Rather than calling client, should this actually be self
// contained to the resolver so that it has generic offline-paths // contained to the resolver so that it has generic offline-paths
// that are possible? // that are possible?
match (cred_handler, pam_next_req) { match (&cred_handler, &pam_next_req) {
(AuthCredHandler::Password, PamAuthRequest::Password { cred }) => { (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 { Ok(true) => Ok(AuthResult::Success {
token: *token.clone(), token: *token.clone(),
}), }),
@ -1054,18 +1059,40 @@ where
// AuthCredHandler::MFA is invalid for offline auth // AuthCredHandler::MFA is invalid for offline auth
return Err(()); 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;
/* let auth_result = self
self.client .client
.unix_user_offline_auth_step( .unix_user_offline_auth_step(
&account_id, account_id,
token,
cred_handler, cred_handler,
pam_next_req, pam_next_req,
&mut dbtxn,
hsm_lock.deref_mut(),
&self.machine_key,
online_at_init, online_at_init,
) )
.await .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, .. }, _) => { (&mut AuthSession::InProgress { token: None, .. }, _) => {
// Can't do much with offline auth when there is no token ... // Can't do much with offline auth when there is no token ...
@ -1144,6 +1171,14 @@ where
// Can continue! // Can continue!
auth_session auth_session
} }
(auth_session, PamAuthResponse::SetupPin { .. }) => {
// Can continue!
auth_session
}
(auth_session, PamAuthResponse::Pin) => {
// Can continue!
auth_session
}
(_, PamAuthResponse::Unknown) => return Ok(None), (_, PamAuthResponse::Unknown) => return Ok(None),
(_, PamAuthResponse::Denied) => return Ok(Some(false)), (_, PamAuthResponse::Denied) => return Ok(Some(false)),
(_, PamAuthResponse::Success) => { (_, PamAuthResponse::Success) => {

View file

@ -51,6 +51,11 @@ pub enum PamAuthResponse {
polling_interval: u32, polling_interval: u32,
}, },
MFAPollWait, MFAPollWait,
/// PAM must prompt for a new PIN and confirm that PIN input
SetupPin {
msg: String,
},
Pin,
// CTAP2 // CTAP2
} }
@ -60,6 +65,8 @@ pub enum PamAuthRequest {
DeviceAuthorizationGrant { data: DeviceAuthorizationResponse }, DeviceAuthorizationGrant { data: DeviceAuthorizationResponse },
MFACode { cred: String }, MFACode { cred: String },
MFAPoll, MFAPoll,
SetupPin { pin: String },
Pin { cred: String },
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]