Skip to content

Commit

Permalink
Merge pull request #305 from himmelblau-idm/dmulder/fido
Browse files Browse the repository at this point in the history
Add Fido MFA
  • Loading branch information
dmulder authored Dec 11, 2024
2 parents 2fb7ca5 + efda7ba commit 11df3db
Show file tree
Hide file tree
Showing 10 changed files with 386 additions and 24 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ sudo zypper ref && sudo zypper in himmelblau nss-himmelblau pam-himmelblau

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

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


Or on Debian based systems:
Expand Down
3 changes: 2 additions & 1 deletion src/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@ async fn main() -> ExitCode {
};
let pin_min_len = cfg.get_hello_pin_min_length();

let mut req = ClientRequest::PamAuthenticateInit(account_id.clone());
let mut req =
ClientRequest::PamAuthenticateInit(account_id.clone(), "aad-tool".to_string());
loop {
match_sm_auth_client_response!(daemon_client.call_and_wait(&req, timeout), req, pin_min_len,
ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Password) => {
Expand Down
74 changes: 70 additions & 4 deletions src/common/src/idprovider/himmelblau.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use himmelblau::auth::{BrokerClientApplication, UserToken as UnixUserToken};
use himmelblau::discovery::EnrollAttrs;
use himmelblau::error::{MsalError, DEVICE_AUTH_FAIL};
use himmelblau::graph::{DirectoryObject, Graph};
use himmelblau::MFAAuthContinue;
use himmelblau::{AuthOption, MFAAuthContinue};
use idmap::Idmap;
use kanidm_hsm_crypto::{LoadableIdentityKey, LoadableMsOapxbcRsaKey, PinValue, SealedData, Tpm};
use reqwest;
Expand Down Expand Up @@ -362,6 +362,7 @@ impl IdProvider for HimmelblauMultiProvider {
async fn unix_user_online_auth_step<D: KeyStoreTxn + Send>(
&self,
account_id: &str,
service: &str,
cred_handler: &mut AuthCredHandler,
pam_next_req: PamAuthRequest,
keystore: &mut D,
Expand All @@ -377,6 +378,7 @@ impl IdProvider for HimmelblauMultiProvider {
provider
.unix_user_online_auth_step(
account_id,
service,
cred_handler,
pam_next_req,
keystore,
Expand Down Expand Up @@ -835,6 +837,7 @@ impl IdProvider for HimmelblauProvider {
async fn unix_user_online_auth_step<D: KeyStoreTxn + Send>(
&self,
account_id: &str,
service: &str,
cred_handler: &mut AuthCredHandler,
pam_next_req: PamAuthRequest,
keystore: &mut D,
Expand Down Expand Up @@ -1000,14 +1003,17 @@ impl IdProvider for HimmelblauProvider {
// enrolled but creating a new Hello Pin, we follow the same process,
// since only an enrollment token can be exchanged for a PRT (which
// will be needed to enroll the Hello Pin).
let mut opts = vec![];
// Prohibit Fido over ssh (since it can't work)
if service != "ssh" {
opts.push(AuthOption::Fido);
}
let mresp = self
.client
.write()
.await
.initiate_acquire_token_by_mfa_flow_for_device_enrollment(
account_id,
&cred,
vec![],
account_id, &cred, opts,
)
.await;
// We need to wait to handle the response until after we've released
Expand Down Expand Up @@ -1064,6 +1070,25 @@ impl IdProvider for HimmelblauProvider {
}
};
match resp.mfa_method.as_str() {
"FidoKey" => {
let fido_challenge =
resp.fido_challenge.clone().ok_or(IdpError::BadRequest)?;

let fido_allow_list =
resp.fido_allow_list.clone().ok_or(IdpError::BadRequest)?;
*cred_handler = AuthCredHandler::MFA { flow: resp };
return Ok((
AuthResult::Next(AuthRequest::Fido {
fido_allow_list,
fido_challenge,
}),
/* An MFA auth cannot cache the password. This would
* lead to a potential downgrade to SFA attack (where
* the attacker auths with a stolen password, then
* disconnects the network to complete the auth). */
AuthCacheAction::None,
));
}
"PhoneAppOTP" | "OneWaySMS" | "ConsolidatedTelephony" => {
let msg = resp.msg.clone();
*cred_handler = AuthCredHandler::MFA { flow: resp };
Expand Down Expand Up @@ -1210,6 +1235,47 @@ impl IdProvider for HimmelblauProvider {
Err(e) => Err(e),
}
}
(AuthCredHandler::MFA { ref mut flow }, PamAuthRequest::Fido { assertion }) => {
let token = self
.client
.write()
.await
.acquire_token_by_mfa_flow(account_id, Some(&assertion), None, flow)
.await
.map_err(|e| {
error!("{:?}", e);
IdpError::NotFound
})?;
let token2 = enroll_and_obtain_enrolled_token!(token);
match self.token_validate(account_id, &token2).await {
Ok(AuthResult::Success { token: token3 }) => {
// Skip Hello enrollment if it is disabled by config
let hello_enabled = self.config.read().await.get_enable_hello();
if !hello_enabled {
info!("Skipping Hello enrollment because it is disabled");
return Ok((
AuthResult::Success { token: token3 },
AuthCacheAction::None,
));
}

// Setup Windows Hello
*cred_handler = AuthCredHandler::SetupPin { token };
return Ok((
AuthResult::Next(AuthRequest::SetupPin {
msg: format!(
"Set up a PIN\n {}{}",
"A Hello PIN is a fast, secure way to sign",
"in to your device, apps, and services."
),
}),
AuthCacheAction::None,
));
}
Ok(auth_result) => Ok((auth_result, AuthCacheAction::None)),
Err(e) => Err(e),
}
}
_ => {
error!("Unexpected AuthCredHandler and PamAuthRequest pairing");
Err(IdpError::NotFound)
Expand Down
12 changes: 12 additions & 0 deletions src/common/src/idprovider/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ pub enum AuthRequest {
msg: String,
},
Pin,
Fido {
fido_challenge: String,
fido_allow_list: Vec<String>,
},
}

#[allow(clippy::from_over_into)]
Expand All @@ -122,6 +126,13 @@ impl Into<PamAuthResponse> for AuthRequest {
AuthRequest::MFAPollWait => PamAuthResponse::MFAPollWait,
AuthRequest::SetupPin { msg } => PamAuthResponse::SetupPin { msg },
AuthRequest::Pin => PamAuthResponse::Pin,
AuthRequest::Fido {
fido_challenge,
fido_allow_list,
} => PamAuthResponse::Fido {
fido_challenge,
fido_allow_list,
},
}
}
}
Expand Down Expand Up @@ -219,6 +230,7 @@ pub trait IdProvider {
async fn unix_user_online_auth_step<D: KeyStoreTxn + Send>(
&self,
_account_id: &str,
_service: &str,
_cred_handler: &mut AuthCredHandler,
_pam_next_req: PamAuthRequest,
_keystore: &mut D,
Expand Down
17 changes: 16 additions & 1 deletion src/common/src/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ enum CacheState {
pub enum AuthSession {
InProgress {
account_id: String,
service: String,
id: Id,
token: Option<Box<UserToken>>,
online_at_init: bool,
Expand Down Expand Up @@ -996,6 +997,7 @@ where
pub async fn pam_account_authenticate_init(
&self,
account_id: &str,
service: &str,
shutdown_rx: broadcast::Receiver<()>,
) -> Result<(AuthSession, PamAuthResponse), ()> {
// Setup an auth session. If possible bring the resolver online.
Expand Down Expand Up @@ -1042,6 +1044,7 @@ where
Ok((next_req, cred_handler)) => {
let auth_session = AuthSession::InProgress {
account_id: account_id.to_string(),
service: service.to_string(),
id,
token: token.map(Box::new),
online_at_init,
Expand Down Expand Up @@ -1078,6 +1081,7 @@ where
(
&mut AuthSession::InProgress {
ref account_id,
ref service,
id: _,
token: _,
online_at_init: true,
Expand All @@ -1093,6 +1097,7 @@ where
.client
.unix_user_online_auth_step(
account_id,
service,
cred_handler,
pam_next_req,
&mut dbtxn,
Expand Down Expand Up @@ -1143,6 +1148,7 @@ where
(
&mut AuthSession::InProgress {
ref account_id,
service: _,
id: _,
token: Some(ref token),
online_at_init,
Expand Down Expand Up @@ -1216,6 +1222,10 @@ where
// AuthCredHandler::None is invalid with SetupPin
return Err(());
}
(AuthCredHandler::None, PamAuthRequest::Fido { .. }) => {
// AuthCredHandler::None is invalid with Fido
return Err(());
}
}
}
(&mut AuthSession::InProgress { token: None, .. }, _) => {
Expand Down Expand Up @@ -1267,12 +1277,13 @@ where
pub async fn pam_account_authenticate(
&self,
account_id: &str,
service: &str,
password: &str,
) -> Result<Option<bool>, ()> {
let (_shutdown_tx, shutdown_rx) = broadcast::channel(1);

let mut auth_session = match self
.pam_account_authenticate_init(account_id, shutdown_rx)
.pam_account_authenticate_init(account_id, service, shutdown_rx)
.await?
{
(auth_session, PamAuthResponse::Password) => {
Expand All @@ -1299,6 +1310,10 @@ where
// Can continue!
auth_session
}
(auth_session, PamAuthResponse::Fido { .. }) => {
// Can continue!
auth_session
}
(_, PamAuthResponse::Unknown) => return Ok(None),
(_, PamAuthResponse::Denied) => return Ok(Some(false)),
(_, PamAuthResponse::Success) => {
Expand Down
13 changes: 10 additions & 3 deletions src/common/src/unix_proto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ pub enum PamAuthResponse {
msg: String,
},
Pin,
// CTAP2
/// PAM must generate a Fido assertion
Fido {
fido_challenge: String,
fido_allow_list: Vec<String>,
},
}

#[derive(Serialize, Deserialize, Debug)]
Expand All @@ -60,6 +64,7 @@ pub enum PamAuthRequest {
MFAPoll { poll_attempt: u32 },
SetupPin { pin: String },
Pin { cred: String },
Fido { assertion: String },
}

#[derive(Serialize, Deserialize, Debug)]
Expand All @@ -70,7 +75,7 @@ pub enum ClientRequest {
NssGroups,
NssGroupByGid(u32),
NssGroupByName(String),
PamAuthenticateInit(String),
PamAuthenticateInit(String, String),
PamAuthenticateStep(PamAuthRequest),
PamAccountAllowed(String),
PamAccountBeginSession(String),
Expand All @@ -90,7 +95,9 @@ impl ClientRequest {
ClientRequest::NssGroups => "NssGroups".to_string(),
ClientRequest::NssGroupByGid(id) => format!("NssGroupByGid({})", id),
ClientRequest::NssGroupByName(id) => format!("NssGroupByName({})", id),
ClientRequest::PamAuthenticateInit(id) => format!("PamAuthenticateInit({})", id),
ClientRequest::PamAuthenticateInit(id, service) => {
format!("PamAuthenticateInit({}, {})", id, service)
}
ClientRequest::PamAuthenticateStep(_) => "PamAuthenticateStep".to_string(),
ClientRequest::PamAccountAllowed(id) => {
format!("PamAccountAllowed({})", id)
Expand Down
3 changes: 2 additions & 1 deletion src/daemon/src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ async fn handle_client(
ClientResponse::NssGroup(None)
})
}
ClientRequest::PamAuthenticateInit(account_id) => {
ClientRequest::PamAuthenticateInit(account_id, service) => {
debug!("pam authenticate init");

match &pam_auth_session_state {
Expand All @@ -306,6 +306,7 @@ async fn handle_client(
match cachelayer
.pam_account_authenticate_init(
account_id.as_str(),
service.as_str(),
shutdown_tx.subscribe(),
)
.await
Expand Down
4 changes: 4 additions & 0 deletions src/pam/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ tracing = { workspace = true }
himmelblau_unix_common.workspace = true
tokio.workspace = true
libhimmelblau.workspace = true
authenticator = { version = "0.4.1", default-features = false, features = ["crypto_openssl"] }
base64.workspace = true
serde_json.workspace = true
sha2 = "0.10.8"

[build-dependencies]
pkg-config.workspace = true
Expand Down
6 changes: 6 additions & 0 deletions src/pam/src/pam/conv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ struct PamResponse {
/// Communication is mediated by the pam client (the application that invoked
/// pam). Messages sent will be relayed to the user by the client, and response
/// will be relayed back.
#[derive(Clone)]
#[repr(C)]
pub struct PamConv {
conv: extern "C" fn(
Expand Down Expand Up @@ -108,3 +109,8 @@ impl PamItem for PamConv {
PAM_CONV
}
}

// PamConv isn't really thread safe, but we mark it as such so that we can
// wrap it in a Arc<Mutex> later for passing between threads.
unsafe impl Send for PamConv {}
unsafe impl Sync for PamConv {}
Loading

0 comments on commit 11df3db

Please sign in to comment.