Skip to content

Commit

Permalink
Implement Hello Pin changes via PAM
Browse files Browse the repository at this point in the history
This implements Pin changes via pam (a user will
call the terminal `passwd` command). This will
require more interaction than a typical call to
`passwd`, since we need to authenitcate to Entra
Id to register the new Pin. We could use either
an existing Hello Key or the PRT to register the
key, but not if we authenticated using a DAG (or
SFA, for that matter). This extra authentication
step forces the token to have an ngcmfa amr (if
using the interactive flow).

Signed-off-by: David Mulder <[email protected]>
  • Loading branch information
dmulder committed Dec 9, 2024
1 parent 0eece87 commit 3bbf663
Show file tree
Hide file tree
Showing 11 changed files with 483 additions and 6 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ tracing-subscriber = "^0.3.17"
tracing = "^0.1.37"
himmelblau_unix_common = { path = "src/common" }
kanidm_unix_common = { path = "src/glue" }
libhimmelblau = { version = "0.4.2" }
libhimmelblau = { version = "0.4.4", features = ["interactive"] }
clap = { version = "^4.5", features = ["derive", "env"] }
clap_complete = "^4.4.1"
reqwest = { version = "^0.12.2", features = ["json"] }
Expand Down
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
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


Or on Debian based systems:
Expand Down
143 changes: 143 additions & 0 deletions src/common/src/idprovider/himmelblau.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,57 @@ impl IdProvider for HimmelblauMultiProvider {
}
}

async fn check_auth_token_set<D: KeyStoreTxn + Send>(
&self,
account_id: &str,
keystore: &mut D,
) -> Result<bool, IdpError> {
match split_username(account_id) {
Some((_sam, domain)) => {
let providers = self.providers.read().await;
match providers.get(domain) {
Some(provider) => provider.check_auth_token_set(account_id, keystore).await,
None => Err(IdpError::NotFound),
}
}
None => Err(IdpError::NotFound),
}
}

async fn change_auth_token<D: KeyStoreTxn + Send>(
&self,
account_id: &str,
token: &UnixUserToken,
old_tok: Option<&str>,
new_tok: &str,
keystore: &mut D,
tpm: &mut tpm::BoxedDynTpm,
machine_key: &tpm::MachineKey,
) -> Result<bool, IdpError> {
match split_username(account_id) {
Some((_sam, domain)) => {
let providers = self.providers.read().await;
match providers.get(domain) {
Some(provider) => {
provider
.change_auth_token(
account_id,
token,
old_tok,
new_tok,
keystore,
tpm,
machine_key,
)
.await
}
None => Err(IdpError::NotFound),
}
}
None => Err(IdpError::NotFound),
}
}

async fn unix_user_get(
&self,
id: &Id,
Expand Down Expand Up @@ -575,6 +626,98 @@ impl IdProvider for HimmelblauProvider {
})
}

async fn check_auth_token_set<D: KeyStoreTxn + Send>(
&self,
account_id: &str,
keystore: &mut D,
) -> Result<bool, IdpError> {
let hello_tag = self.fetch_hello_key_tag(account_id);
let hello_key: Option<LoadableIdentityKey> =
keystore.get_tagged_hsm_key(&hello_tag).unwrap_or(None);
Ok(hello_key.is_some())
}

async fn change_auth_token<D: KeyStoreTxn + Send>(
&self,
account_id: &str,
token: &UnixUserToken,
old_tok: Option<&str>,
new_tok: &str,
keystore: &mut D,
tpm: &mut tpm::BoxedDynTpm,
machine_key: &tpm::MachineKey,
) -> Result<bool, IdpError> {
let hello_tag = self.fetch_hello_key_tag(account_id);

// Ensure the user is setting the token for the account it has authenticated to
if account_id.to_string().to_lowercase()
!= token
.spn()
.map_err(|e| {
error!("Failed checking the spn on the user token: {:?}", e);
IdpError::BadRequest
})?
.to_lowercase()
{
error!("A hello key may only be set by the authenticated user!");
return Err(IdpError::BadRequest);
}

// Ensure the old pin is valid
if let Some(old_tok) = old_tok {
let hello_key: LoadableIdentityKey = keystore
.get_tagged_hsm_key(&hello_tag)
.map_err(|e| {
error!("Failed fetching hello key from keystore: {:?}", e);
IdpError::BadRequest
})?
.ok_or_else(|| {
error!("Authentication failed. Hello key missing.");
IdpError::BadRequest
})?;

let pin = PinValue::new(old_tok).map_err(|e| {
error!("Failed setting pin value: {:?}", e);
IdpError::Tpm
})?;
tpm.identity_key_load(machine_key, Some(&pin), &hello_key)
.map_err(|e| {
error!("{:?}", e);
IdpError::BadRequest
})?;
} else {
// If no old pin was provided, make sure one isn't already set
let hello_key: Option<LoadableIdentityKey> =
keystore.get_tagged_hsm_key(&hello_tag).unwrap_or(None);
if hello_key.is_some() {
error!("Failed to set Hello pin. An existing key is already set!");
return Ok(false);
}
}

// Set the hello pin
let hello_key = match self
.client
.write()
.await
.provision_hello_for_business_key(token, tpm, machine_key, new_tok)
.await
{
Ok(hello_key) => hello_key,
Err(e) => {
error!("Failed to provision hello key: {:?}", e);
return Ok(false);
}
};
keystore
.insert_tagged_hsm_key(&hello_tag, &hello_key)
.map_err(|e| {
error!("Failed to provision hello key: {:?}", e);
IdpError::Tpm
})?;
Ok(true)
}

async fn unix_user_get(
&self,
id: &Id,
Expand Down
17 changes: 17 additions & 0 deletions src/common/src/idprovider/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,23 @@ pub trait IdProvider {
_machine_key: &tpm::MachineKey,
) -> Result<String, IdpError>;

async fn check_auth_token_set<D: KeyStoreTxn + Send>(
&self,
_account_id: &str,
_keystore: &mut D,
) -> Result<bool, IdpError>;

async fn change_auth_token<D: KeyStoreTxn + Send>(
&self,
_account_id: &str,
_token: &UnixUserToken,
_old_tok: Option<&str>,
_new_tok: &str,
_keystore: &mut D,
_tpm: &mut tpm::BoxedDynTpm,
_machine_key: &tpm::MachineKey,
) -> Result<bool, IdpError>;

async fn unix_user_online_auth_init<D: KeyStoreTxn + Send>(
&self,
_account_id: &str,
Expand Down
46 changes: 46 additions & 0 deletions src/common/src/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,52 @@ where
}
}

pub async fn check_auth_token_set(&self, account_id: &str) -> Result<bool, ()> {
let mut dbtxn = self.db.write().await;

let res = self
.client
.check_auth_token_set(account_id, &mut dbtxn)
.await;

dbtxn.commit().map_err(|_| ())?;

res.map_err(|e| {
debug!("check_auth_token_set error -> {:?}", e);
})
}

pub async fn change_auth_token(
&self,
account_id: &str,
token: &UnixUserToken,
old_tok: Option<&str>,
new_tok: &str,
) -> Result<bool, ()> {
let mut hsm_lock = self.hsm.lock().await;
let mut dbtxn = self.db.write().await;

let res = self
.client
.change_auth_token(
account_id,
token,
old_tok,
new_tok,
&mut dbtxn,
hsm_lock.deref_mut(),
&self.machine_key,
)
.await;

drop(hsm_lock);
dbtxn.commit().map_err(|_| ())?;

res.map_err(|e| {
debug!("change_auth_token error -> {:?}", e);
})
}

pub async fn get_usertoken(&self, account_id: Id) -> Result<Option<UserToken>, ()> {
debug!("get_usertoken");
// get the item from the cache
Expand Down
6 changes: 6 additions & 0 deletions src/common/src/unix_proto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ pub enum ClientRequest {
PamAuthenticateStep(PamAuthRequest),
PamAccountAllowed(String),
PamAccountBeginSession(String),
PamAuthTokenIsSet(String),
PamChangeAuthToken(String, String, String, Option<String>, String),
InvalidateCache,
ClearCache,
Status,
Expand All @@ -95,6 +97,10 @@ impl ClientRequest {
format!("PamAccountAllowed({})", id)
}
ClientRequest::PamAccountBeginSession(_) => "PamAccountBeginSession".to_string(),
ClientRequest::PamAuthTokenIsSet(id) => format!("PamAuthTokenIsSet({})", id),
ClientRequest::PamChangeAuthToken(id, _, _, _, _) => {
format!("PamChangeAuthToken({}, ...)", id)
}
ClientRequest::InvalidateCache => "InvalidateCache".to_string(),
ClientRequest::ClearCache => "ClearCache".to_string(),
ClientRequest::Status => "Status".to_string(),
Expand Down
1 change: 1 addition & 0 deletions src/daemon/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ kanidm_lib_file_permissions.workspace = true
identity_dbus_broker.workspace = true
base64.workspace = true
async-trait = "0.1.83"
libhimmelblau.workspace = true

[package.metadata.deb]
name = "himmelblau"
Expand Down
34 changes: 34 additions & 0 deletions src/daemon/src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ use std::time::Duration;
use bytes::{BufMut, BytesMut};
use clap::{Arg, ArgAction, Command};
use futures::{SinkExt, StreamExt};
use himmelblau::{ClientInfo, IdToken, UserToken as UnixUserToken};
use himmelblau_unix_common::config::HimmelblauConfig;
use himmelblau_unix_common::constants::DEFAULT_CONFIG_PATH;
use himmelblau_unix_common::db::{Cache, CacheTxn, Db};
Expand Down Expand Up @@ -549,6 +550,39 @@ async fn handle_client(
_ => ClientResponse::Error,
}
}
ClientRequest::PamAuthTokenIsSet(account_id) => {
debug!("sm_chauthtok check set");
cachelayer
.check_auth_token_set(&account_id)
.await
.map(|is_set| ClientResponse::PamStatus(Some(is_set)))
.unwrap_or(ClientResponse::Error)
}
ClientRequest::PamChangeAuthToken(
account_id,
access_token,
refresh_token,
old_pin,
new_pin,
) => {
debug!("sm_chauthtok req");
let token = UnixUserToken {
token_type: "Bearer".to_string(),
scope: None,
expires_in: 0,
ext_expires_in: 0,
access_token: Some(access_token),
refresh_token,
id_token: IdToken::default(),
client_info: ClientInfo::default(),
prt: None,
};
cachelayer
.change_auth_token(&account_id, &token, old_pin.as_deref(), &new_pin)
.await
.map(|_| ClientResponse::Ok)
.unwrap_or(ClientResponse::Error)
}
ClientRequest::InvalidateCache => {
debug!("invalidate cache");
cachelayer
Expand Down
2 changes: 2 additions & 0 deletions src/pam/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ kanidm_unix_common = { workspace = true }
tracing-subscriber = { workspace = true }
tracing = { workspace = true }
himmelblau_unix_common.workspace = true
tokio.workspace = true
libhimmelblau.workspace = true

[build-dependencies]
pkg-config.workspace = true
Expand Down
Loading

0 comments on commit 3bbf663

Please sign in to comment.