use core::ops::Deref;
use std::collections::BTreeMap;
use std::fmt::{self, Display};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use sshkey_attest::proto::PublicKey as SshPublicKey;
use hashbrown::HashSet;
use kanidm_proto::internal::{
CUCredState, CUExtPortal, CURegState, CURegWarning, CUStatus, CredentialDetail, PasskeyDetail,
PasswordFeedback, TotpSecret,
};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use webauthn_rs::prelude::{
AttestedPasskey as AttestedPasskeyV4, AttestedPasskeyRegistration, CreationChallengeResponse,
Passkey as PasskeyV4, PasskeyRegistration, RegisterPublicKeyCredential, WebauthnError,
};
use crate::credential::totp::{Totp, TOTP_DEFAULT_STEP};
use crate::credential::{BackupCodes, Credential};
use crate::idm::account::Account;
use crate::idm::server::{IdmServerCredUpdateTransaction, IdmServerProxyWriteTransaction};
use crate::prelude::*;
use crate::server::access::Access;
use crate::utils::{backup_code_from_random, readable_password_from_random, uuid_from_duration};
use crate::value::{CredUpdateSessionPerms, CredentialType, IntentTokenState, LABEL_RE};
use compact_jwt::compact::JweCompact;
use compact_jwt::jwe::JweBuilder;
use super::accountpolicy::ResolvedAccountPolicy;
const MAXIMUM_CRED_UPDATE_TTL: Duration = Duration::from_secs(900);
const MINIMUM_INTENT_TTL: Duration = Duration::from_secs(300);
const DEFAULT_INTENT_TTL: Duration = Duration::from_secs(3600);
const MAXIMUM_INTENT_TTL: Duration = Duration::from_secs(86400);
#[derive(Debug)]
pub enum PasswordQuality {
TooShort(u32),
BadListed,
DontReusePasswords,
Feedback(Vec<PasswordFeedback>),
}
#[derive(Clone, Debug)]
pub struct CredentialUpdateIntentToken {
pub intent_id: String,
pub expiry_time: OffsetDateTime,
}
#[derive(Clone, Debug)]
pub struct CredentialUpdateIntentTokenExchange {
pub intent_id: String,
}
impl From<CredentialUpdateIntentToken> for CredentialUpdateIntentTokenExchange {
fn from(tok: CredentialUpdateIntentToken) -> Self {
CredentialUpdateIntentTokenExchange {
intent_id: tok.intent_id,
}
}
}
#[derive(Serialize, Deserialize, Debug)]
struct CredentialUpdateSessionTokenInner {
pub sessionid: Uuid,
pub max_ttl: Duration,
}
#[derive(Debug)]
pub struct CredentialUpdateSessionToken {
pub token_enc: JweCompact,
}
#[derive(Clone)]
enum MfaRegState {
None,
TotpInit(Totp),
TotpTryAgain(Totp),
TotpInvalidSha1(Totp, Totp, String),
Passkey(Box<CreationChallengeResponse>, PasskeyRegistration),
#[allow(dead_code)]
AttestedPasskey(Box<CreationChallengeResponse>, AttestedPasskeyRegistration),
}
impl fmt::Debug for MfaRegState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let t = match self {
MfaRegState::None => "MfaRegState::None",
MfaRegState::TotpInit(_) => "MfaRegState::TotpInit",
MfaRegState::TotpTryAgain(_) => "MfaRegState::TotpTryAgain",
MfaRegState::TotpInvalidSha1(_, _, _) => "MfaRegState::TotpInvalidSha1",
MfaRegState::Passkey(_, _) => "MfaRegState::Passkey",
MfaRegState::AttestedPasskey(_, _) => "MfaRegState::AttestedPasskey",
};
write!(f, "{t}")
}
}
#[derive(Debug, Clone, Copy)]
enum CredentialState {
Modifiable,
DeleteOnly,
AccessDeny,
PolicyDeny,
}
impl From<CredentialState> for CUCredState {
fn from(val: CredentialState) -> CUCredState {
match val {
CredentialState::Modifiable => CUCredState::Modifiable,
CredentialState::DeleteOnly => CUCredState::DeleteOnly,
CredentialState::AccessDeny => CUCredState::AccessDeny,
CredentialState::PolicyDeny => CUCredState::PolicyDeny,
}
}
}
#[derive(Clone)]
pub(crate) struct CredentialUpdateSession {
issuer: String,
account: Account,
resolved_account_policy: ResolvedAccountPolicy,
intent_token_id: Option<String>,
ext_cred_portal: CUExtPortal,
primary_state: CredentialState,
primary: Option<Credential>,
unixcred: Option<Credential>,
unixcred_state: CredentialState,
sshkeys: BTreeMap<String, SshPublicKey>,
sshkeys_state: CredentialState,
passkeys: BTreeMap<Uuid, (String, PasskeyV4)>,
passkeys_state: CredentialState,
attested_passkeys: BTreeMap<Uuid, (String, AttestedPasskeyV4)>,
attested_passkeys_state: CredentialState,
mfaregstate: MfaRegState,
}
impl fmt::Debug for CredentialUpdateSession {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let primary: Option<CredentialDetail> = self.primary.as_ref().map(|c| c.into());
let passkeys: Vec<PasskeyDetail> = self
.passkeys
.iter()
.map(|(uuid, (tag, _pk))| PasskeyDetail {
tag: tag.clone(),
uuid: *uuid,
})
.collect();
let attested_passkeys: Vec<PasskeyDetail> = self
.attested_passkeys
.iter()
.map(|(uuid, (tag, _pk))| PasskeyDetail {
tag: tag.clone(),
uuid: *uuid,
})
.collect();
f.debug_struct("CredentialUpdateSession")
.field("account.spn", &self.account.spn)
.field("account.unix", &self.account.unix_extn().is_some())
.field("resolved_account_policy", &self.resolved_account_policy)
.field("intent_token_id", &self.intent_token_id)
.field("primary.detail()", &primary)
.field("primary.state", &self.primary_state)
.field("passkeys.list()", &passkeys)
.field("passkeys.state", &self.passkeys_state)
.field("attested_passkeys.list()", &attested_passkeys)
.field("attested_passkeys.state", &self.attested_passkeys_state)
.field("mfaregstate", &self.mfaregstate)
.finish()
}
}
impl CredentialUpdateSession {
fn can_commit(&self) -> (bool, Vec<CredentialUpdateSessionStatusWarnings>) {
let mut warnings = Vec::with_capacity(0);
let cred_type_min = self.resolved_account_policy.credential_policy();
debug!(?cred_type_min);
match cred_type_min {
CredentialType::Any => {}
CredentialType::Mfa => {
if self
.primary
.as_ref()
.map(|cred| !cred.is_mfa())
.unwrap_or(false)
{
warnings.push(CredentialUpdateSessionStatusWarnings::MfaRequired);
}
}
CredentialType::Passkey => {
if self.primary.is_some() {
warnings.push(CredentialUpdateSessionStatusWarnings::PasskeyRequired);
}
}
CredentialType::AttestedPasskey => {
if !self.passkeys.is_empty() || self.primary.is_some() {
warnings.push(CredentialUpdateSessionStatusWarnings::AttestedPasskeyRequired);
}
}
CredentialType::AttestedResidentkey => {
if !self.attested_passkeys.is_empty()
|| !self.passkeys.is_empty()
|| self.primary.is_some()
{
warnings
.push(CredentialUpdateSessionStatusWarnings::AttestedResidentKeyRequired);
}
}
CredentialType::Invalid => {
warnings.push(CredentialUpdateSessionStatusWarnings::Unsatisfiable)
}
}
if let Some(att_ca_list) = self.resolved_account_policy.webauthn_attestation_ca_list() {
if att_ca_list.is_empty() {
warnings
.push(CredentialUpdateSessionStatusWarnings::WebauthnAttestationUnsatisfiable)
}
}
(warnings.is_empty(), warnings)
}
}
pub enum MfaRegStateStatus {
None,
TotpCheck(TotpSecret),
TotpTryAgain,
TotpInvalidSha1,
BackupCodes(HashSet<String>),
Passkey(CreationChallengeResponse),
AttestedPasskey(CreationChallengeResponse),
}
impl fmt::Debug for MfaRegStateStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let t = match self {
MfaRegStateStatus::None => "MfaRegStateStatus::None",
MfaRegStateStatus::TotpCheck(_) => "MfaRegStateStatus::TotpCheck(_)",
MfaRegStateStatus::TotpTryAgain => "MfaRegStateStatus::TotpTryAgain",
MfaRegStateStatus::TotpInvalidSha1 => "MfaRegStateStatus::TotpInvalidSha1",
MfaRegStateStatus::BackupCodes(_) => "MfaRegStateStatus::BackupCodes",
MfaRegStateStatus::Passkey(_) => "MfaRegStateStatus::Passkey",
MfaRegStateStatus::AttestedPasskey(_) => "MfaRegStateStatus::AttestedPasskey",
};
write!(f, "{t}")
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum CredentialUpdateSessionStatusWarnings {
MfaRequired,
PasskeyRequired,
AttestedPasskeyRequired,
AttestedResidentKeyRequired,
Unsatisfiable,
WebauthnAttestationUnsatisfiable,
}
impl Display for CredentialUpdateSessionStatusWarnings {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "{:?}", self)
}
}
impl From<CredentialUpdateSessionStatusWarnings> for CURegWarning {
fn from(val: CredentialUpdateSessionStatusWarnings) -> CURegWarning {
match val {
CredentialUpdateSessionStatusWarnings::MfaRequired => CURegWarning::MfaRequired,
CredentialUpdateSessionStatusWarnings::PasskeyRequired => CURegWarning::PasskeyRequired,
CredentialUpdateSessionStatusWarnings::AttestedPasskeyRequired => {
CURegWarning::AttestedPasskeyRequired
}
CredentialUpdateSessionStatusWarnings::AttestedResidentKeyRequired => {
CURegWarning::AttestedResidentKeyRequired
}
CredentialUpdateSessionStatusWarnings::Unsatisfiable => CURegWarning::Unsatisfiable,
CredentialUpdateSessionStatusWarnings::WebauthnAttestationUnsatisfiable => {
CURegWarning::WebauthnAttestationUnsatisfiable
}
}
}
}
#[derive(Debug)]
pub struct CredentialUpdateSessionStatus {
spn: String,
displayname: String,
ext_cred_portal: CUExtPortal,
mfaregstate: MfaRegStateStatus,
can_commit: bool,
warnings: Vec<CredentialUpdateSessionStatusWarnings>,
primary: Option<CredentialDetail>,
primary_state: CredentialState,
passkeys: Vec<PasskeyDetail>,
passkeys_state: CredentialState,
attested_passkeys: Vec<PasskeyDetail>,
attested_passkeys_state: CredentialState,
attested_passkeys_allowed_devices: Vec<String>,
unixcred: Option<CredentialDetail>,
unixcred_state: CredentialState,
sshkeys: BTreeMap<String, SshPublicKey>,
sshkeys_state: CredentialState,
}
impl CredentialUpdateSessionStatus {
pub fn can_commit(&self) -> bool {
self.can_commit
}
pub fn mfaregstate(&self) -> &MfaRegStateStatus {
&self.mfaregstate
}
}
#[allow(clippy::from_over_into)]
impl Into<CUStatus> for CredentialUpdateSessionStatus {
fn into(self) -> CUStatus {
CUStatus {
spn: self.spn,
displayname: self.displayname,
ext_cred_portal: self.ext_cred_portal,
mfaregstate: match self.mfaregstate {
MfaRegStateStatus::None => CURegState::None,
MfaRegStateStatus::TotpCheck(c) => CURegState::TotpCheck(c),
MfaRegStateStatus::TotpTryAgain => CURegState::TotpTryAgain,
MfaRegStateStatus::TotpInvalidSha1 => CURegState::TotpInvalidSha1,
MfaRegStateStatus::BackupCodes(s) => {
CURegState::BackupCodes(s.into_iter().collect())
}
MfaRegStateStatus::Passkey(r) => CURegState::Passkey(r),
MfaRegStateStatus::AttestedPasskey(r) => CURegState::AttestedPasskey(r),
},
can_commit: self.can_commit,
warnings: self.warnings.into_iter().map(|w| w.into()).collect(),
primary: self.primary,
primary_state: self.primary_state.into(),
passkeys: self.passkeys,
passkeys_state: self.passkeys_state.into(),
attested_passkeys: self.attested_passkeys,
attested_passkeys_state: self.attested_passkeys_state.into(),
attested_passkeys_allowed_devices: self.attested_passkeys_allowed_devices,
unixcred: self.unixcred,
unixcred_state: self.unixcred_state.into(),
sshkeys: self.sshkeys,
sshkeys_state: self.sshkeys_state.into(),
}
}
}
impl From<&CredentialUpdateSession> for CredentialUpdateSessionStatus {
fn from(session: &CredentialUpdateSession) -> Self {
let (can_commit, warnings) = session.can_commit();
let attested_passkeys_allowed_devices: Vec<String> = session
.resolved_account_policy
.webauthn_attestation_ca_list()
.iter()
.flat_map(|att_ca_list: &&webauthn_rs::prelude::AttestationCaList| {
att_ca_list.cas().values().flat_map(|ca| {
ca.aaguids()
.values()
.map(|device| device.description_en().to_string())
})
})
.collect();
CredentialUpdateSessionStatus {
spn: session.account.spn.clone(),
displayname: session.account.displayname.clone(),
ext_cred_portal: session.ext_cred_portal.clone(),
can_commit,
warnings,
primary: session.primary.as_ref().map(|c| c.into()),
primary_state: session.primary_state,
passkeys: session
.passkeys
.iter()
.map(|(uuid, (tag, _pk))| PasskeyDetail {
tag: tag.clone(),
uuid: *uuid,
})
.collect(),
passkeys_state: session.passkeys_state,
attested_passkeys: session
.attested_passkeys
.iter()
.map(|(uuid, (tag, _pk))| PasskeyDetail {
tag: tag.clone(),
uuid: *uuid,
})
.collect(),
attested_passkeys_state: session.attested_passkeys_state,
attested_passkeys_allowed_devices,
unixcred: session.unixcred.as_ref().map(|c| c.into()),
unixcred_state: session.unixcred_state,
sshkeys: session.sshkeys.clone(),
sshkeys_state: session.sshkeys_state,
mfaregstate: match &session.mfaregstate {
MfaRegState::None => MfaRegStateStatus::None,
MfaRegState::TotpInit(token) => MfaRegStateStatus::TotpCheck(
token.to_proto(session.account.name.as_str(), session.issuer.as_str()),
),
MfaRegState::TotpTryAgain(_) => MfaRegStateStatus::TotpTryAgain,
MfaRegState::TotpInvalidSha1(_, _, _) => MfaRegStateStatus::TotpInvalidSha1,
MfaRegState::Passkey(r, _) => MfaRegStateStatus::Passkey(r.as_ref().clone()),
MfaRegState::AttestedPasskey(r, _) => {
MfaRegStateStatus::AttestedPasskey(r.as_ref().clone())
}
},
}
}
}
pub(crate) type CredentialUpdateSessionMutex = Arc<Mutex<CredentialUpdateSession>>;
pub struct InitCredentialUpdateIntentEvent {
pub ident: Identity,
pub target: Uuid,
pub max_ttl: Option<Duration>,
}
impl InitCredentialUpdateIntentEvent {
pub fn new(ident: Identity, target: Uuid, max_ttl: Option<Duration>) -> Self {
InitCredentialUpdateIntentEvent {
ident,
target,
max_ttl,
}
}
#[cfg(test)]
pub fn new_impersonate_entry(
e: std::sync::Arc<Entry<EntrySealed, EntryCommitted>>,
target: Uuid,
max_ttl: Duration,
) -> Self {
let ident = Identity::from_impersonate_entry_readwrite(e);
InitCredentialUpdateIntentEvent {
ident,
target,
max_ttl: Some(max_ttl),
}
}
}
pub struct InitCredentialUpdateEvent {
pub ident: Identity,
pub target: Uuid,
}
impl InitCredentialUpdateEvent {
pub fn new(ident: Identity, target: Uuid) -> Self {
InitCredentialUpdateEvent { ident, target }
}
#[cfg(test)]
pub fn new_impersonate_entry(e: std::sync::Arc<Entry<EntrySealed, EntryCommitted>>) -> Self {
let ident = Identity::from_impersonate_entry_readwrite(e);
let target = ident
.get_uuid()
.ok_or(OperationError::InvalidState)
.expect("Identity has no uuid associated");
InitCredentialUpdateEvent { ident, target }
}
}
impl<'a> IdmServerProxyWriteTransaction<'a> {
fn validate_init_credential_update(
&mut self,
target: Uuid,
ident: &Identity,
) -> Result<(Account, ResolvedAccountPolicy, CredUpdateSessionPerms), OperationError> {
let entry = self.qs_write.internal_search_uuid(target)?;
security_info!(
%target,
"Initiating Credential Update Session",
);
if ident.access_scope() != AccessScope::ReadWrite {
security_access!("identity access scope is not permitted to modify");
security_access!("denied ❌");
return Err(OperationError::AccessDenied);
}
let (account, resolved_account_policy) =
Account::try_from_entry_with_policy(entry.as_ref(), &mut self.qs_write)?;
let effective_perms = self
.qs_write
.get_accesscontrols()
.effective_permission_check(
ident,
Some(btreeset![
Attribute::PrimaryCredential,
Attribute::PassKeys,
Attribute::AttestedPasskeys,
Attribute::UnixPassword,
Attribute::SshPublicKey
]),
&[entry],
)?;
let eperm = effective_perms.first().ok_or_else(|| {
error!("Effective Permission check returned no results");
OperationError::InvalidState
})?;
if eperm.target != account.uuid {
error!("Effective Permission check target differs from requested entry uuid");
return Err(OperationError::InvalidEntryState);
}
let eperm_search_primary_cred = match &eperm.search {
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::PrimaryCredential),
};
let eperm_mod_primary_cred = match &eperm.modify_pres {
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::PrimaryCredential),
};
let eperm_rem_primary_cred = match &eperm.modify_rem {
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::PrimaryCredential),
};
let primary_can_edit =
eperm_search_primary_cred && eperm_mod_primary_cred && eperm_rem_primary_cred;
let eperm_search_passkeys = match &eperm.search {
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::PassKeys),
};
let eperm_mod_passkeys = match &eperm.modify_pres {
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::PassKeys),
};
let eperm_rem_passkeys = match &eperm.modify_rem {
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::PassKeys),
};
let passkeys_can_edit = eperm_search_passkeys && eperm_mod_passkeys && eperm_rem_passkeys;
let eperm_search_attested_passkeys = match &eperm.search {
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::AttestedPasskeys),
};
let eperm_mod_attested_passkeys = match &eperm.modify_pres {
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::AttestedPasskeys),
};
let eperm_rem_attested_passkeys = match &eperm.modify_rem {
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::AttestedPasskeys),
};
let attested_passkeys_can_edit = eperm_search_attested_passkeys
&& eperm_mod_attested_passkeys
&& eperm_rem_attested_passkeys;
let eperm_search_unixcred = match &eperm.search {
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::UnixPassword),
};
let eperm_mod_unixcred = match &eperm.modify_pres {
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::UnixPassword),
};
let eperm_rem_unixcred = match &eperm.modify_rem {
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::UnixPassword),
};
let unixcred_can_edit = account.unix_extn().is_some()
&& eperm_search_unixcred
&& eperm_mod_unixcred
&& eperm_rem_unixcred;
let eperm_search_sshpubkey = match &eperm.search {
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::SshPublicKey),
};
let eperm_mod_sshpubkey = match &eperm.modify_pres {
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::SshPublicKey),
};
let eperm_rem_sshpubkey = match &eperm.modify_rem {
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::SshPublicKey),
};
let sshpubkey_can_edit = account.unix_extn().is_some()
&& eperm_search_sshpubkey
&& eperm_mod_sshpubkey
&& eperm_rem_sshpubkey;
let ext_cred_portal_can_view = if let Some(sync_parent_uuid) = account.sync_parent_uuid {
let entry = self.qs_write.internal_search_uuid(sync_parent_uuid)?;
let effective_perms = self
.qs_write
.get_accesscontrols()
.effective_permission_check(
ident,
Some(btreeset![Attribute::SyncCredentialPortal]),
&[entry],
)?;
let eperm = effective_perms.first().ok_or_else(|| {
admin_error!("Effective Permission check returned no results");
OperationError::InvalidState
})?;
match &eperm.search {
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::SyncCredentialPortal),
}
} else {
false
};
if !(primary_can_edit
|| passkeys_can_edit
|| attested_passkeys_can_edit
|| ext_cred_portal_can_view
|| sshpubkey_can_edit
|| unixcred_can_edit)
{
error!("Unable to proceed with credential update intent - at least one type of credential must be modifiable or visible.");
Err(OperationError::NotAuthorised)
} else {
security_info!(%primary_can_edit, %passkeys_can_edit, %unixcred_can_edit, %sshpubkey_can_edit, %ext_cred_portal_can_view, "Proceeding");
Ok((
account,
resolved_account_policy,
CredUpdateSessionPerms {
ext_cred_portal_can_view,
passkeys_can_edit,
attested_passkeys_can_edit,
primary_can_edit,
unixcred_can_edit,
sshpubkey_can_edit,
},
))
}
}
fn create_credupdate_session(
&mut self,
sessionid: Uuid,
intent_token_id: Option<String>,
account: Account,
resolved_account_policy: ResolvedAccountPolicy,
perms: CredUpdateSessionPerms,
ct: Duration,
) -> Result<(CredentialUpdateSessionToken, CredentialUpdateSessionStatus), OperationError> {
let ext_cred_portal_can_view = perms.ext_cred_portal_can_view;
let cred_type_min = resolved_account_policy.credential_policy();
let passkey_attestation_required = resolved_account_policy
.webauthn_attestation_ca_list()
.is_some();
let primary_state = if cred_type_min > CredentialType::Mfa {
CredentialState::PolicyDeny
} else if perms.primary_can_edit {
CredentialState::Modifiable
} else {
CredentialState::AccessDeny
};
let passkeys_state =
if cred_type_min > CredentialType::Passkey || passkey_attestation_required {
CredentialState::PolicyDeny
} else if perms.passkeys_can_edit {
CredentialState::Modifiable
} else {
CredentialState::AccessDeny
};
let attested_passkeys_state = if cred_type_min > CredentialType::AttestedPasskey {
CredentialState::PolicyDeny
} else if perms.attested_passkeys_can_edit {
if passkey_attestation_required {
CredentialState::Modifiable
} else {
CredentialState::DeleteOnly
}
} else {
CredentialState::AccessDeny
};
let unixcred_state = if account.unix_extn().is_none() {
CredentialState::PolicyDeny
} else if perms.unixcred_can_edit {
CredentialState::Modifiable
} else {
CredentialState::AccessDeny
};
let sshkeys_state = if perms.sshpubkey_can_edit {
CredentialState::Modifiable
} else {
CredentialState::AccessDeny
};
let primary = if matches!(primary_state, CredentialState::Modifiable) {
account.primary.clone()
} else {
None
};
let passkeys = if matches!(passkeys_state, CredentialState::Modifiable) {
account.passkeys.clone()
} else {
BTreeMap::default()
};
let unixcred: Option<Credential> = if matches!(unixcred_state, CredentialState::Modifiable)
{
account.unix_extn().and_then(|uext| uext.ucred()).cloned()
} else {
None
};
let sshkeys = if matches!(sshkeys_state, CredentialState::Modifiable) {
account.sshkeys().clone()
} else {
BTreeMap::default()
};
let attested_passkeys = if matches!(attested_passkeys_state, CredentialState::Modifiable)
|| matches!(attested_passkeys_state, CredentialState::DeleteOnly)
{
if let Some(att_ca_list) = resolved_account_policy.webauthn_attestation_ca_list() {
let mut attested_passkeys = BTreeMap::default();
for (uuid, (label, apk)) in account.attested_passkeys.iter() {
match apk.verify_attestation(att_ca_list) {
Ok(_) => {
attested_passkeys.insert(*uuid, (label.clone(), apk.clone()));
}
Err(e) => {
warn!(eclass=?e, emsg=%e, "credential no longer meets attestation criteria");
}
}
}
attested_passkeys
} else {
account.attested_passkeys.clone()
}
} else {
BTreeMap::default()
};
let ext_cred_portal = match (account.sync_parent_uuid, ext_cred_portal_can_view) {
(Some(sync_parent_uuid), true) => {
let sync_entry = self.qs_write.internal_search_uuid(sync_parent_uuid)?;
sync_entry
.get_ava_single_url(Attribute::SyncCredentialPortal)
.cloned()
.map(CUExtPortal::Some)
.unwrap_or(CUExtPortal::Hidden)
}
(Some(_), false) => CUExtPortal::Hidden,
(None, _) => CUExtPortal::None,
};
let issuer = self.qs_write.get_domain_display_name().to_string();
let session = CredentialUpdateSession {
account,
resolved_account_policy,
issuer,
intent_token_id,
ext_cred_portal,
primary,
primary_state,
unixcred,
unixcred_state,
sshkeys,
sshkeys_state,
passkeys,
passkeys_state,
attested_passkeys,
attested_passkeys_state,
mfaregstate: MfaRegState::None,
};
let max_ttl = ct + MAXIMUM_CRED_UPDATE_TTL;
let token = CredentialUpdateSessionTokenInner { sessionid, max_ttl };
let token_data = serde_json::to_vec(&token).map_err(|e| {
admin_error!(err = ?e, "Unable to encode token data");
OperationError::SerdeJsonError
})?;
let token_jwe = JweBuilder::from(token_data).build();
let token_enc = self
.qs_write
.get_domain_key_object_handle()?
.jwe_a128gcm_encrypt(&token_jwe, ct)?;
let status: CredentialUpdateSessionStatus = (&session).into();
let session = Arc::new(Mutex::new(session));
self.expire_credential_update_sessions(ct);
self.cred_update_sessions.insert(sessionid, session);
trace!("cred_update_sessions.insert - {}", sessionid);
Ok((CredentialUpdateSessionToken { token_enc }, status))
}
#[instrument(level = "debug", skip_all)]
pub fn init_credential_update_intent(
&mut self,
event: &InitCredentialUpdateIntentEvent,
ct: Duration,
) -> Result<CredentialUpdateIntentToken, OperationError> {
let (account, _resolved_account_policy, perms) =
self.validate_init_credential_update(event.target, &event.ident)?;
let mttl = event.max_ttl.unwrap_or(DEFAULT_INTENT_TTL);
let clamped_mttl = mttl.clamp(MINIMUM_INTENT_TTL, MAXIMUM_INTENT_TTL);
debug!(?clamped_mttl, "clamped update intent validity");
let max_ttl = ct + clamped_mttl;
let expiry_time = OffsetDateTime::UNIX_EPOCH + max_ttl;
let intent_id = readable_password_from_random();
let mut modlist = ModifyList::new_append(
Attribute::CredentialUpdateIntentToken,
Value::IntentToken(
intent_id.clone(),
IntentTokenState::Valid { max_ttl, perms },
),
);
account
.credential_update_intent_tokens
.iter()
.for_each(|(existing_intent_id, state)| {
let max_ttl = match state {
IntentTokenState::Valid { max_ttl, perms: _ }
| IntentTokenState::InProgress {
max_ttl,
perms: _,
session_id: _,
session_ttl: _,
}
| IntentTokenState::Consumed { max_ttl } => *max_ttl,
};
if ct >= max_ttl {
modlist.push_mod(Modify::Removed(
Attribute::CredentialUpdateIntentToken,
PartialValue::IntentToken(existing_intent_id.clone()),
));
}
});
self.qs_write
.internal_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(account.uuid))),
&modlist,
)
.map_err(|e| {
request_error!(error = ?e);
e
})?;
Ok(CredentialUpdateIntentToken {
intent_id,
expiry_time,
})
}
pub fn exchange_intent_credential_update(
&mut self,
token: CredentialUpdateIntentTokenExchange,
current_time: Duration,
) -> Result<(CredentialUpdateSessionToken, CredentialUpdateSessionStatus), OperationError> {
let CredentialUpdateIntentTokenExchange { intent_id } = token;
let mut vs = self.qs_write.internal_search(filter!(f_eq(
Attribute::CredentialUpdateIntentToken,
PartialValue::IntentToken(intent_id.clone())
)))?;
let entry = match vs.pop() {
Some(entry) => {
if vs.is_empty() {
entry
} else {
let matched_uuids = std::iter::once(entry.get_uuid())
.chain(vs.iter().map(|e| e.get_uuid()))
.collect::<Vec<_>>();
security_error!("Multiple entries had identical intent_id - for safety, rejecting the use of this intent_id! {:?}", matched_uuids);
return Err(OperationError::InvalidState);
}
}
None => {
security_info!(
"Rejecting Update Session - Intent Token does not exist (replication delay?)",
);
return Err(OperationError::Wait(
OffsetDateTime::UNIX_EPOCH + (current_time + Duration::from_secs(150)),
));
}
};
let (account, resolved_account_policy) =
Account::try_from_entry_with_policy(entry.as_ref(), &mut self.qs_write)?;
let (max_ttl, perms) = match account.credential_update_intent_tokens.get(&intent_id) {
Some(IntentTokenState::Consumed { max_ttl: _ }) => {
security_info!(
%entry,
%account.uuid,
"Rejecting Update Session - Intent Token has already been exchanged",
);
return Err(OperationError::SessionExpired);
}
Some(IntentTokenState::InProgress {
max_ttl,
perms,
session_id,
session_ttl,
}) => {
if current_time > *session_ttl {
security_info!(
%entry,
%account.uuid,
"Initiating Credential Update Session - Previous session {} has expired", session_id
);
} else {
security_info!(
%entry,
%account.uuid,
"Initiating Update Session - Intent Token was in use {} - this will be invalidated.", session_id
);
};
(*max_ttl, *perms)
}
Some(IntentTokenState::Valid { max_ttl, perms }) => (*max_ttl, *perms),
None => {
admin_error!("Corruption may have occurred - index yielded an entry for intent_id, but the entry does not contain that intent_id");
return Err(OperationError::InvalidState);
}
};
if current_time >= max_ttl {
security_info!(?current_time, ?max_ttl, %account.uuid, "intent has expired");
return Err(OperationError::SessionExpired);
}
security_info!(
%entry,
%account.uuid,
"Initiating Credential Update Session",
);
let session_id = uuid_from_duration(current_time + MAXIMUM_CRED_UPDATE_TTL, self.sid);
let mut modlist = ModifyList::new();
modlist.push_mod(Modify::Removed(
Attribute::CredentialUpdateIntentToken,
PartialValue::IntentToken(intent_id.clone()),
));
modlist.push_mod(Modify::Present(
Attribute::CredentialUpdateIntentToken,
Value::IntentToken(
intent_id.clone(),
IntentTokenState::InProgress {
max_ttl,
perms,
session_id,
session_ttl: current_time + MAXIMUM_CRED_UPDATE_TTL,
},
),
));
self.qs_write
.internal_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(account.uuid))),
&modlist,
)
.map_err(|e| {
request_error!(error = ?e);
e
})?;
self.create_credupdate_session(
session_id,
Some(intent_id),
account,
resolved_account_policy,
perms,
current_time,
)
}
#[instrument(level = "debug", skip_all)]
pub fn init_credential_update(
&mut self,
event: &InitCredentialUpdateEvent,
current_time: Duration,
) -> Result<(CredentialUpdateSessionToken, CredentialUpdateSessionStatus), OperationError> {
let (account, resolved_account_policy, perms) =
self.validate_init_credential_update(event.target, &event.ident)?;
let sessionid = uuid_from_duration(current_time + MAXIMUM_CRED_UPDATE_TTL, self.sid);
self.create_credupdate_session(
sessionid,
None,
account,
resolved_account_policy,
perms,
current_time,
)
}
#[instrument(level = "trace", skip(self))]
pub fn expire_credential_update_sessions(&mut self, ct: Duration) {
let before = self.cred_update_sessions.len();
let split_at = uuid_from_duration(ct, self.sid);
trace!(?split_at, "expiring less than");
self.cred_update_sessions.split_off_lt(&split_at);
let removed = before - self.cred_update_sessions.len();
trace!(?removed);
}
fn credential_update_commit_common(
&mut self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
) -> Result<
(
ModifyList<ModifyInvalid>,
CredentialUpdateSession,
CredentialUpdateSessionTokenInner,
),
OperationError,
> {
let session_token: CredentialUpdateSessionTokenInner = self
.qs_write
.get_domain_key_object_handle()?
.jwe_decrypt(&cust.token_enc)
.map_err(|e| {
admin_error!(?e, "Failed to decrypt credential update session request");
OperationError::SessionExpired
})
.and_then(|data| {
data.from_json().map_err(|e| {
admin_error!(err = ?e, "Failed to deserialise credential update session request");
OperationError::SerdeJsonError
})
})?;
if ct >= session_token.max_ttl {
trace!(?ct, ?session_token.max_ttl);
security_info!(%session_token.sessionid, "session expired");
return Err(OperationError::SessionExpired);
}
let session_handle = self.cred_update_sessions.remove(&session_token.sessionid)
.ok_or_else(|| {
admin_error!("No such sessionid exists on this server - may be due to a load balancer failover or replay? {:?}", session_token.sessionid);
OperationError::InvalidState
})?;
let session = session_handle
.try_lock()
.map(|guard| (*guard).clone())
.map_err(|_| {
admin_error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
let modlist = ModifyList::new();
Ok((modlist, session, session_token))
}
pub fn commit_credential_update(
&mut self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
) -> Result<(), OperationError> {
let (mut modlist, session, session_token) =
self.credential_update_commit_common(cust, ct)?;
let can_commit = session.can_commit();
if !can_commit.0 {
let commit_failure_reasons = can_commit
.1
.iter()
.map(|e| e.to_string())
.collect::<Vec<String>>()
.join(", ");
admin_error!(
"Session is unable to commit due to: {}",
commit_failure_reasons
);
return Err(OperationError::InvalidState);
}
if let Some(intent_token_id) = &session.intent_token_id {
let entry = self.qs_write.internal_search_uuid(session.account.uuid)?;
let account = Account::try_from_entry_rw(entry.as_ref(), &mut self.qs_write)?;
let max_ttl = match account.credential_update_intent_tokens.get(intent_token_id) {
Some(IntentTokenState::InProgress {
max_ttl,
perms: _,
session_id,
session_ttl: _,
}) => {
if *session_id != session_token.sessionid {
security_info!("Session originated from an intent token, but the intent token has initiated a conflicting second update session. Refusing to commit changes.");
return Err(OperationError::InvalidState);
} else {
*max_ttl
}
}
Some(IntentTokenState::Consumed { max_ttl: _ })
| Some(IntentTokenState::Valid {
max_ttl: _,
perms: _,
})
| None => {
security_info!("Session originated from an intent token, but the intent token has transitioned to an invalid state. Refusing to commit changes.");
return Err(OperationError::InvalidState);
}
};
modlist.push_mod(Modify::Removed(
Attribute::CredentialUpdateIntentToken,
PartialValue::IntentToken(intent_token_id.clone()),
));
modlist.push_mod(Modify::Present(
Attribute::CredentialUpdateIntentToken,
Value::IntentToken(
intent_token_id.clone(),
IntentTokenState::Consumed { max_ttl },
),
));
};
match session.primary_state {
CredentialState::Modifiable => {
modlist.push_mod(Modify::Purged(Attribute::PrimaryCredential));
if let Some(ncred) = &session.primary {
let vcred = Value::new_credential("primary", ncred.clone());
modlist.push_mod(Modify::Present(Attribute::PrimaryCredential, vcred));
};
}
CredentialState::DeleteOnly | CredentialState::PolicyDeny => {
modlist.push_mod(Modify::Purged(Attribute::PrimaryCredential));
}
CredentialState::AccessDeny => {}
};
match session.passkeys_state {
CredentialState::DeleteOnly | CredentialState::Modifiable => {
modlist.push_mod(Modify::Purged(Attribute::PassKeys));
session.passkeys.iter().for_each(|(uuid, (tag, pk))| {
let v_pk = Value::Passkey(*uuid, tag.clone(), pk.clone());
modlist.push_mod(Modify::Present(Attribute::PassKeys, v_pk));
});
}
CredentialState::PolicyDeny => {
modlist.push_mod(Modify::Purged(Attribute::PassKeys));
}
CredentialState::AccessDeny => {}
};
match session.attested_passkeys_state {
CredentialState::DeleteOnly | CredentialState::Modifiable => {
modlist.push_mod(Modify::Purged(Attribute::AttestedPasskeys));
session
.attested_passkeys
.iter()
.for_each(|(uuid, (tag, pk))| {
let v_pk = Value::AttestedPasskey(*uuid, tag.clone(), pk.clone());
modlist.push_mod(Modify::Present(Attribute::AttestedPasskeys, v_pk));
});
}
CredentialState::PolicyDeny => {
modlist.push_mod(Modify::Purged(Attribute::AttestedPasskeys));
}
CredentialState::AccessDeny => {}
};
match session.unixcred_state {
CredentialState::DeleteOnly | CredentialState::Modifiable => {
modlist.push_mod(Modify::Purged(Attribute::UnixPassword));
if let Some(ncred) = &session.unixcred {
let vcred = Value::new_credential("unix", ncred.clone());
modlist.push_mod(Modify::Present(Attribute::UnixPassword, vcred));
}
}
CredentialState::PolicyDeny => {
modlist.push_mod(Modify::Purged(Attribute::UnixPassword));
}
CredentialState::AccessDeny => {}
};
match session.sshkeys_state {
CredentialState::DeleteOnly | CredentialState::Modifiable => {
modlist.push_mod(Modify::Purged(Attribute::SshPublicKey));
for (tag, pk) in &session.sshkeys {
let v_sk = Value::SshKey(tag.clone(), pk.clone());
modlist.push_mod(Modify::Present(Attribute::SshPublicKey, v_sk));
}
}
CredentialState::PolicyDeny => {
modlist.push_mod(Modify::Purged(Attribute::SshPublicKey));
}
CredentialState::AccessDeny => {}
};
trace!(?modlist, "processing change");
if modlist.is_empty() {
trace!("no changes to apply");
Ok(())
} else {
self.qs_write
.internal_modify(
&filter!(f_eq(
Attribute::Uuid,
PartialValue::Uuid(session.account.uuid)
)),
&modlist,
)
.map_err(|e| {
request_error!(error = ?e);
e
})
}
}
pub fn cancel_credential_update(
&mut self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
) -> Result<(), OperationError> {
let (mut modlist, session, session_token) =
self.credential_update_commit_common(cust, ct)?;
if let Some(intent_token_id) = &session.intent_token_id {
let entry = self.qs_write.internal_search_uuid(session.account.uuid)?;
let account = Account::try_from_entry_rw(entry.as_ref(), &mut self.qs_write)?;
let (max_ttl, perms) = match account
.credential_update_intent_tokens
.get(intent_token_id)
{
Some(IntentTokenState::InProgress {
max_ttl,
perms,
session_id,
session_ttl: _,
}) => {
if *session_id != session_token.sessionid {
security_info!("Session originated from an intent token, but the intent token has initiated a conflicting second update session. Refusing to commit changes.");
return Err(OperationError::InvalidState);
} else {
(*max_ttl, *perms)
}
}
Some(IntentTokenState::Consumed { max_ttl: _ })
| Some(IntentTokenState::Valid {
max_ttl: _,
perms: _,
})
| None => {
security_info!("Session originated from an intent token, but the intent token has transitioned to an invalid state. Refusing to commit changes.");
return Err(OperationError::InvalidState);
}
};
modlist.push_mod(Modify::Removed(
Attribute::CredentialUpdateIntentToken,
PartialValue::IntentToken(intent_token_id.clone()),
));
modlist.push_mod(Modify::Present(
Attribute::CredentialUpdateIntentToken,
Value::IntentToken(
intent_token_id.clone(),
IntentTokenState::Valid { max_ttl, perms },
),
));
};
if !modlist.is_empty() {
trace!(?modlist, "processing change");
self.qs_write
.internal_modify(
&filter!(f_eq(
Attribute::Uuid,
PartialValue::Uuid(session.account.uuid)
)),
&modlist,
)
.map_err(|e| {
request_error!(error = ?e);
e
})
} else {
Ok(())
}
}
}
impl<'a> IdmServerCredUpdateTransaction<'a> {
#[cfg(test)]
pub fn get_origin(&self) -> &Url {
&self.webauthn.get_allowed_origins()[0]
}
fn get_current_session(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
) -> Result<CredentialUpdateSessionMutex, OperationError> {
let session_token: CredentialUpdateSessionTokenInner = self
.qs_read
.get_domain_key_object_handle()?
.jwe_decrypt(&cust.token_enc)
.map_err(|e| {
admin_error!(?e, "Failed to decrypt credential update session request");
OperationError::SessionExpired
})
.and_then(|data| {
data.from_json().map_err(|e| {
admin_error!(err = ?e, "Failed to deserialise credential update session request");
OperationError::SerdeJsonError
})
})?;
if ct >= session_token.max_ttl {
trace!(?ct, ?session_token.max_ttl);
security_info!(%session_token.sessionid, "session expired");
return Err(OperationError::SessionExpired);
}
self.cred_update_sessions.get(&session_token.sessionid)
.ok_or_else(|| {
admin_error!("No such sessionid exists on this server - may be due to a load balancer failover or token replay? {}", session_token.sessionid);
OperationError::InvalidState
})
.cloned()
}
pub fn credential_update_status(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let session = session_handle.try_lock().map_err(|_| {
admin_error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
let status: CredentialUpdateSessionStatus = session.deref().into();
Ok(status)
}
#[instrument(level = "trace", skip(self))]
fn check_password_quality(
&self,
cleartext: &str,
resolved_account_policy: &ResolvedAccountPolicy,
related_inputs: &[&str],
radius_secret: Option<&str>,
) -> Result<(), PasswordQuality> {
let pw_min_length = resolved_account_policy.pw_min_length();
if cleartext.len() < pw_min_length as usize {
return Err(PasswordQuality::TooShort(pw_min_length));
}
if let Some(some_radius_secret) = radius_secret {
if cleartext.contains(some_radius_secret) {
return Err(PasswordQuality::DontReusePasswords);
}
}
for related in related_inputs {
if cleartext.contains(related) {
return Err(PasswordQuality::Feedback(vec![
PasswordFeedback::NamesAndSurnamesByThemselvesAreEasyToGuess,
PasswordFeedback::AvoidDatesAndYearsThatAreAssociatedWithYou,
]));
}
}
let entropy = zxcvbn::zxcvbn(cleartext, related_inputs).map_err(|e| {
admin_error!("zxcvbn check failure (password empty?) {:?}", e);
PasswordQuality::Feedback(vec![
PasswordFeedback::UseAFewWordsAvoidCommonPhrases,
PasswordFeedback::AddAnotherWordOrTwo,
PasswordFeedback::NoNeedForSymbolsDigitsOrUppercaseLetters,
])
})?;
if entropy.score() < 4 {
let feedback: zxcvbn::feedback::Feedback = entropy
.feedback()
.as_ref()
.ok_or(OperationError::InvalidState)
.cloned()
.map_err(|e| {
security_info!("zxcvbn returned no feedback when score < 3 -> {:?}", e);
PasswordQuality::Feedback(vec![
PasswordFeedback::UseAFewWordsAvoidCommonPhrases,
PasswordFeedback::AddAnotherWordOrTwo,
PasswordFeedback::NoNeedForSymbolsDigitsOrUppercaseLetters,
])
})?;
security_info!(?feedback, "pw quality feedback");
let feedback: Vec<_> = feedback
.suggestions()
.iter()
.map(|s| {
match s {
zxcvbn::feedback::Suggestion::UseAFewWordsAvoidCommonPhrases => {
PasswordFeedback::UseAFewWordsAvoidCommonPhrases
}
zxcvbn::feedback::Suggestion::NoNeedForSymbolsDigitsOrUppercaseLetters => {
PasswordFeedback::NoNeedForSymbolsDigitsOrUppercaseLetters
}
zxcvbn::feedback::Suggestion::AddAnotherWordOrTwo => {
PasswordFeedback::AddAnotherWordOrTwo
}
zxcvbn::feedback::Suggestion::CapitalizationDoesntHelpVeryMuch => {
PasswordFeedback::CapitalizationDoesntHelpVeryMuch
}
zxcvbn::feedback::Suggestion::AllUppercaseIsAlmostAsEasyToGuessAsAllLowercase => {
PasswordFeedback::AllUppercaseIsAlmostAsEasyToGuessAsAllLowercase
}
zxcvbn::feedback::Suggestion::ReversedWordsArentMuchHarderToGuess => {
PasswordFeedback::ReversedWordsArentMuchHarderToGuess
}
zxcvbn::feedback::Suggestion::PredictableSubstitutionsDontHelpVeryMuch => {
PasswordFeedback::PredictableSubstitutionsDontHelpVeryMuch
}
zxcvbn::feedback::Suggestion::UseALongerKeyboardPatternWithMoreTurns => {
PasswordFeedback::UseALongerKeyboardPatternWithMoreTurns
}
zxcvbn::feedback::Suggestion::AvoidRepeatedWordsAndCharacters => {
PasswordFeedback::AvoidRepeatedWordsAndCharacters
}
zxcvbn::feedback::Suggestion::AvoidSequences => {
PasswordFeedback::AvoidSequences
}
zxcvbn::feedback::Suggestion::AvoidRecentYears => {
PasswordFeedback::AvoidRecentYears
}
zxcvbn::feedback::Suggestion::AvoidYearsThatAreAssociatedWithYou => {
PasswordFeedback::AvoidYearsThatAreAssociatedWithYou
}
zxcvbn::feedback::Suggestion::AvoidDatesAndYearsThatAreAssociatedWithYou => {
PasswordFeedback::AvoidDatesAndYearsThatAreAssociatedWithYou
}
}
})
.chain(feedback.warning().map(|w| match w {
zxcvbn::feedback::Warning::StraightRowsOfKeysAreEasyToGuess => {
PasswordFeedback::StraightRowsOfKeysAreEasyToGuess
}
zxcvbn::feedback::Warning::ShortKeyboardPatternsAreEasyToGuess => {
PasswordFeedback::ShortKeyboardPatternsAreEasyToGuess
}
zxcvbn::feedback::Warning::RepeatsLikeAaaAreEasyToGuess => {
PasswordFeedback::RepeatsLikeAaaAreEasyToGuess
}
zxcvbn::feedback::Warning::RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess => {
PasswordFeedback::RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess
}
zxcvbn::feedback::Warning::ThisIsATop10Password => {
PasswordFeedback::ThisIsATop10Password
}
zxcvbn::feedback::Warning::ThisIsATop100Password => {
PasswordFeedback::ThisIsATop100Password
}
zxcvbn::feedback::Warning::ThisIsACommonPassword => {
PasswordFeedback::ThisIsACommonPassword
}
zxcvbn::feedback::Warning::ThisIsSimilarToACommonlyUsedPassword => {
PasswordFeedback::ThisIsSimilarToACommonlyUsedPassword
}
zxcvbn::feedback::Warning::SequencesLikeAbcAreEasyToGuess => {
PasswordFeedback::SequencesLikeAbcAreEasyToGuess
}
zxcvbn::feedback::Warning::RecentYearsAreEasyToGuess => {
PasswordFeedback::RecentYearsAreEasyToGuess
}
zxcvbn::feedback::Warning::AWordByItselfIsEasyToGuess => {
PasswordFeedback::AWordByItselfIsEasyToGuess
}
zxcvbn::feedback::Warning::DatesAreOftenEasyToGuess => {
PasswordFeedback::DatesAreOftenEasyToGuess
}
zxcvbn::feedback::Warning::NamesAndSurnamesByThemselvesAreEasyToGuess => {
PasswordFeedback::NamesAndSurnamesByThemselvesAreEasyToGuess
}
zxcvbn::feedback::Warning::CommonNamesAndSurnamesAreEasyToGuess => {
PasswordFeedback::CommonNamesAndSurnamesAreEasyToGuess
}
}))
.collect();
return Err(PasswordQuality::Feedback(feedback));
}
if self
.qs_read
.pw_badlist()
.contains(&cleartext.to_lowercase())
{
security_info!("Password found in badlist, rejecting");
Err(PasswordQuality::BadListed)
} else {
Ok(())
}
}
#[instrument(level = "trace", skip(cust, self))]
pub fn credential_primary_set_password(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
pw: &str,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
admin_error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
if !matches!(session.primary_state, CredentialState::Modifiable) {
error!("Session does not have permission to modify primary credential");
return Err(OperationError::AccessDenied);
};
self.check_password_quality(
pw,
&session.resolved_account_policy,
session.account.related_inputs().as_slice(),
session.account.radius_secret.as_deref(),
)
.map_err(|e| match e {
PasswordQuality::TooShort(sz) => {
OperationError::PasswordQuality(vec![PasswordFeedback::TooShort(sz)])
}
PasswordQuality::BadListed => {
OperationError::PasswordQuality(vec![PasswordFeedback::BadListed])
}
PasswordQuality::DontReusePasswords => {
OperationError::PasswordQuality(vec![PasswordFeedback::DontReusePasswords])
}
PasswordQuality::Feedback(feedback) => OperationError::PasswordQuality(feedback),
})?;
let ncred = match &session.primary {
Some(primary) => {
primary.set_password(self.crypto_policy, pw)?
}
None => Credential::new_password_only(self.crypto_policy, pw)?,
};
session.primary = Some(ncred);
Ok(session.deref().into())
}
pub fn credential_primary_init_totp(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
admin_error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
if !matches!(session.primary_state, CredentialState::Modifiable) {
error!("Session does not have permission to modify primary credential");
return Err(OperationError::AccessDenied);
};
if !matches!(session.mfaregstate, MfaRegState::None) {
debug!("Clearing incomplete mfareg");
}
let totp_token = Totp::generate_secure(TOTP_DEFAULT_STEP);
session.mfaregstate = MfaRegState::TotpInit(totp_token);
Ok(session.deref().into())
}
pub fn credential_primary_check_totp(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
totp_chal: u32,
label: &str,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
admin_error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
if !matches!(session.primary_state, CredentialState::Modifiable) {
error!("Session does not have permission to modify primary credential");
return Err(OperationError::AccessDenied);
};
match &session.mfaregstate {
MfaRegState::TotpInit(totp_token)
| MfaRegState::TotpTryAgain(totp_token)
| MfaRegState::TotpInvalidSha1(totp_token, _, _) => {
if totp_token.verify(totp_chal, ct) {
let ncred = session
.primary
.as_ref()
.map(|cred| cred.append_totp(label.to_string(), totp_token.clone()))
.ok_or_else(|| {
admin_error!("A TOTP was added, but no primary credential stub exists");
OperationError::InvalidState
})?;
session.primary = Some(ncred);
session.mfaregstate = MfaRegState::None;
Ok(session.deref().into())
} else {
let token_sha1 = totp_token.clone().downgrade_to_legacy();
if token_sha1.verify(totp_chal, ct) {
session.mfaregstate = MfaRegState::TotpInvalidSha1(
totp_token.clone(),
token_sha1,
label.to_string(),
);
Ok(session.deref().into())
} else {
session.mfaregstate = MfaRegState::TotpTryAgain(totp_token.clone());
Ok(session.deref().into())
}
}
}
_ => Err(OperationError::InvalidRequestState),
}
}
pub fn credential_primary_accept_sha1_totp(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
admin_error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
if !matches!(session.primary_state, CredentialState::Modifiable) {
error!("Session does not have permission to modify primary credential");
return Err(OperationError::AccessDenied);
};
match &session.mfaregstate {
MfaRegState::TotpInvalidSha1(_, token_sha1, label) => {
let ncred = session
.primary
.as_ref()
.map(|cred| cred.append_totp(label.to_string(), token_sha1.clone()))
.ok_or_else(|| {
admin_error!("A TOTP was added, but no primary credential stub exists");
OperationError::InvalidState
})?;
security_info!("A SHA1 TOTP credential was accepted");
session.primary = Some(ncred);
session.mfaregstate = MfaRegState::None;
Ok(session.deref().into())
}
_ => Err(OperationError::InvalidRequestState),
}
}
pub fn credential_primary_remove_totp(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
label: &str,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
admin_error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
if !matches!(session.primary_state, CredentialState::Modifiable) {
error!("Session does not have permission to modify primary credential");
return Err(OperationError::AccessDenied);
};
if !matches!(session.mfaregstate, MfaRegState::None) {
admin_info!("Invalid TOTP state, another update is in progress");
return Err(OperationError::InvalidState);
}
let ncred = session
.primary
.as_ref()
.map(|cred| cred.remove_totp(label))
.ok_or_else(|| {
admin_error!("Try to remove TOTP, but no primary credential stub exists");
OperationError::InvalidState
})?;
session.primary = Some(ncred);
session.mfaregstate = MfaRegState::None;
Ok(session.deref().into())
}
pub fn credential_primary_init_backup_codes(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
if !matches!(session.primary_state, CredentialState::Modifiable) {
error!("Session does not have permission to modify primary credential");
return Err(OperationError::AccessDenied);
};
let codes = backup_code_from_random();
let ncred = session
.primary
.as_ref()
.ok_or_else(|| {
error!("Tried to add backup codes, but no primary credential stub exists");
OperationError::InvalidState
})
.and_then(|cred|
cred.update_backup_code(BackupCodes::new(codes.clone()))
.map_err(|_| {
error!("Tried to add backup codes, but MFA is not enabled on this credential yet");
OperationError::InvalidState
})
)
?;
session.primary = Some(ncred);
Ok(session.deref().into()).map(|mut status: CredentialUpdateSessionStatus| {
status.mfaregstate = MfaRegStateStatus::BackupCodes(codes);
status
})
}
pub fn credential_primary_remove_backup_codes(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
admin_error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
if !matches!(session.primary_state, CredentialState::Modifiable) {
error!("Session does not have permission to modify primary credential");
return Err(OperationError::AccessDenied);
};
let ncred = session
.primary
.as_ref()
.ok_or_else(|| {
admin_error!("Tried to add backup codes, but no primary credential stub exists");
OperationError::InvalidState
})
.and_then(|cred|
cred.remove_backup_code()
.map_err(|_| {
admin_error!("Tried to remove backup codes, but MFA is not enabled on this credential yet");
OperationError::InvalidState
})
)
?;
session.primary = Some(ncred);
Ok(session.deref().into())
}
pub fn credential_primary_delete(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
admin_error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
if !(matches!(session.primary_state, CredentialState::Modifiable)
|| matches!(session.primary_state, CredentialState::DeleteOnly))
{
error!("Session does not have permission to modify primary credential");
return Err(OperationError::AccessDenied);
};
session.primary = None;
Ok(session.deref().into())
}
pub fn credential_passkey_init(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
admin_error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
if !matches!(session.passkeys_state, CredentialState::Modifiable) {
error!("Session does not have permission to modify passkeys");
return Err(OperationError::AccessDenied);
};
if !matches!(session.mfaregstate, MfaRegState::None) {
debug!("Clearing incomplete mfareg");
}
let (ccr, pk_reg) = self
.webauthn
.start_passkey_registration(
session.account.uuid,
&session.account.spn,
&session.account.displayname,
session.account.existing_credential_id_list(),
)
.map_err(|e| {
error!(eclass=?e, emsg=%e, "Unable to start passkey registration");
OperationError::Webauthn
})?;
session.mfaregstate = MfaRegState::Passkey(Box::new(ccr), pk_reg);
Ok(session.deref().into())
}
pub fn credential_passkey_finish(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
label: String,
reg: &RegisterPublicKeyCredential,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
admin_error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
if !matches!(session.passkeys_state, CredentialState::Modifiable) {
error!("Session does not have permission to modify passkeys");
return Err(OperationError::AccessDenied);
};
match &session.mfaregstate {
MfaRegState::Passkey(_ccr, pk_reg) => {
let result = self
.webauthn
.finish_passkey_registration(reg, pk_reg)
.map_err(|e| {
error!(eclass=?e, emsg=%e, "Unable to complete passkey registration");
match e {
WebauthnError::UserNotVerified => {
OperationError::CU0003WebauthnUserNotVerified
}
_ => OperationError::CU0002WebauthnRegistrationError,
}
});
session.mfaregstate = MfaRegState::None;
let passkey = result?;
let pk_id = Uuid::new_v4();
session.passkeys.insert(pk_id, (label, passkey));
Ok(session.deref().into())
}
_ => Err(OperationError::InvalidRequestState),
}
}
pub fn credential_passkey_remove(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
uuid: Uuid,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
admin_error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
if !(matches!(session.passkeys_state, CredentialState::Modifiable)
|| matches!(session.passkeys_state, CredentialState::DeleteOnly))
{
error!("Session does not have permission to modify passkeys");
return Err(OperationError::AccessDenied);
};
session.passkeys.remove(&uuid);
Ok(session.deref().into())
}
pub fn credential_attested_passkey_init(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
if !matches!(session.attested_passkeys_state, CredentialState::Modifiable) {
error!("Session does not have permission to modify attested passkeys");
return Err(OperationError::AccessDenied);
};
if !matches!(session.mfaregstate, MfaRegState::None) {
debug!("Cancelling abandoned mfareg");
}
let att_ca_list = session
.resolved_account_policy
.webauthn_attestation_ca_list()
.cloned()
.ok_or_else(|| {
error!(
"No attestation CA list is available, can not proceed with attested passkeys."
);
OperationError::AccessDenied
})?;
let (ccr, pk_reg) = self
.webauthn
.start_attested_passkey_registration(
session.account.uuid,
&session.account.spn,
&session.account.displayname,
session.account.existing_credential_id_list(),
att_ca_list,
None,
)
.map_err(|e| {
error!(eclass=?e, emsg=%e, "Unable to start passkey registration");
OperationError::Webauthn
})?;
session.mfaregstate = MfaRegState::AttestedPasskey(Box::new(ccr), pk_reg);
Ok(session.deref().into())
}
pub fn credential_attested_passkey_finish(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
label: String,
reg: &RegisterPublicKeyCredential,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
admin_error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
if !matches!(session.attested_passkeys_state, CredentialState::Modifiable) {
error!("Session does not have permission to modify attested passkeys");
return Err(OperationError::AccessDenied);
};
match &session.mfaregstate {
MfaRegState::AttestedPasskey(_ccr, pk_reg) => {
let result = self
.webauthn
.finish_attested_passkey_registration(reg, pk_reg)
.map_err(|e| {
error!(eclass=?e, emsg=%e, "Unable to complete attested passkey registration");
match e {
WebauthnError::AttestationChainNotTrusted(_)
| WebauthnError::AttestationNotVerifiable => {
OperationError::CU0001WebauthnAttestationNotTrusted
},
WebauthnError::UserNotVerified => {
OperationError::CU0003WebauthnUserNotVerified
},
_ => OperationError::CU0002WebauthnRegistrationError,
}
});
session.mfaregstate = MfaRegState::None;
let passkey = result?;
trace!(?passkey);
let pk_id = Uuid::new_v4();
session.attested_passkeys.insert(pk_id, (label, passkey));
trace!(?session.attested_passkeys);
Ok(session.deref().into())
}
_ => Err(OperationError::InvalidRequestState),
}
}
pub fn credential_attested_passkey_remove(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
uuid: Uuid,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
admin_error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
if !(matches!(session.attested_passkeys_state, CredentialState::Modifiable)
|| matches!(session.attested_passkeys_state, CredentialState::DeleteOnly))
{
error!("Session does not have permission to modify attested passkeys");
return Err(OperationError::AccessDenied);
};
session.attested_passkeys.remove(&uuid);
Ok(session.deref().into())
}
#[instrument(level = "trace", skip(cust, self))]
pub fn credential_unix_set_password(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
pw: &str,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
admin_error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
if !matches!(session.unixcred_state, CredentialState::Modifiable) {
error!("Session does not have permission to modify unix credential");
return Err(OperationError::AccessDenied);
};
self.check_password_quality(
pw,
&session.resolved_account_policy,
session.account.related_inputs().as_slice(),
session.account.radius_secret.as_deref(),
)
.map_err(|e| match e {
PasswordQuality::TooShort(sz) => {
OperationError::PasswordQuality(vec![PasswordFeedback::TooShort(sz)])
}
PasswordQuality::BadListed => {
OperationError::PasswordQuality(vec![PasswordFeedback::BadListed])
}
PasswordQuality::DontReusePasswords => {
OperationError::PasswordQuality(vec![PasswordFeedback::DontReusePasswords])
}
PasswordQuality::Feedback(feedback) => OperationError::PasswordQuality(feedback),
})?;
let ncred = match &session.unixcred {
Some(unixcred) => {
unixcred.set_password(self.crypto_policy, pw)?
}
None => Credential::new_password_only(self.crypto_policy, pw)?,
};
session.unixcred = Some(ncred);
Ok(session.deref().into())
}
pub fn credential_unix_delete(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
admin_error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
if !(matches!(session.unixcred_state, CredentialState::Modifiable)
|| matches!(session.unixcred_state, CredentialState::DeleteOnly))
{
error!("Session does not have permission to modify unix credential");
return Err(OperationError::AccessDenied);
};
session.unixcred = None;
Ok(session.deref().into())
}
#[instrument(level = "trace", skip(cust, self))]
pub fn credential_sshkey_add(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
label: String,
sshpubkey: SshPublicKey,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
admin_error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
if !matches!(session.unixcred_state, CredentialState::Modifiable) {
error!("Session does not have permission to modify unix credential");
return Err(OperationError::AccessDenied);
};
if !LABEL_RE.is_match(&label) {
error!("SSH Public Key label invalid");
return Err(OperationError::InvalidLabel);
}
if session.sshkeys.contains_key(&label) {
error!("SSH Public Key label duplicate");
return Err(OperationError::DuplicateLabel);
}
if session.sshkeys.values().any(|sk| *sk == sshpubkey) {
error!("SSH Public Key duplicate");
return Err(OperationError::DuplicateKey);
}
session.sshkeys.insert(label, sshpubkey);
Ok(session.deref().into())
}
pub fn credential_sshkey_remove(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
label: &str,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
admin_error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
if !(matches!(session.sshkeys_state, CredentialState::Modifiable)
|| matches!(session.sshkeys_state, CredentialState::DeleteOnly))
{
error!("Session does not have permission to modify sshkeys");
return Err(OperationError::AccessDenied);
};
session.sshkeys.remove(label).ok_or_else(|| {
error!("No such key for label");
OperationError::NoMatchingEntries
})?;
Ok(session.deref().into())
}
pub fn credential_update_cancel_mfareg(
&self,
cust: &CredentialUpdateSessionToken,
ct: Duration,
) -> Result<CredentialUpdateSessionStatus, OperationError> {
let session_handle = self.get_current_session(cust, ct)?;
let mut session = session_handle.try_lock().map_err(|_| {
admin_error!("Session already locked, unable to proceed.");
OperationError::InvalidState
})?;
trace!(?session);
session.mfaregstate = MfaRegState::None;
Ok(session.deref().into())
}
}
#[cfg(test)]
mod tests {
use compact_jwt::JwsCompact;
use std::time::Duration;
use kanidm_proto::internal::{CUExtPortal, CredentialDetailType, PasswordFeedback};
use kanidm_proto::v1::{AuthAllowed, AuthIssueSession, AuthMech, UnixUserToken};
use uuid::uuid;
use webauthn_authenticator_rs::softpasskey::SoftPasskey;
use webauthn_authenticator_rs::softtoken::{self, SoftToken};
use webauthn_authenticator_rs::{AuthenticatorBackend, WebauthnAuthenticator};
use webauthn_rs::prelude::AttestationCaListBuilder;
use super::{
CredentialState, CredentialUpdateSessionStatus, CredentialUpdateSessionStatusWarnings,
CredentialUpdateSessionToken, InitCredentialUpdateEvent, InitCredentialUpdateIntentEvent,
MfaRegStateStatus, MAXIMUM_CRED_UPDATE_TTL, MAXIMUM_INTENT_TTL, MINIMUM_INTENT_TTL,
};
use crate::credential::totp::Totp;
use crate::event::CreateEvent;
use crate::idm::audit::AuditEvent;
use crate::idm::delayed::DelayedAction;
use crate::idm::event::{
AuthEvent, AuthResult, RegenerateRadiusSecretEvent, UnixUserAuthEvent,
};
use crate::idm::server::{IdmServer, IdmServerCredUpdateTransaction, IdmServerDelayed};
use crate::idm::AuthState;
use crate::prelude::*;
use crate::utils::password_from_random_len;
use crate::value::CredentialType;
use sshkey_attest::proto::PublicKey as SshPublicKey;
const TEST_CURRENT_TIME: u64 = 6000;
const TESTPERSON_UUID: Uuid = uuid!("cf231fea-1a8f-4410-a520-fd9b1a379c86");
const SSHKEY_VALID_1: &str = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey";
const SSHKEY_VALID_2: &str = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBIbkSsdGCRoW6v0nO/3vNYPhG20YhWU0wQPY7x52EOb4dmYhC4IJfzVDpEPg313BxWRKQglb5RQ1PPkou7JFyCUAAAAEc3NoOg== testuser@fidokey";
const SSHKEY_INVALID: &str = "sk-ecrsa-sha9000-nistp@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBIbkSsdGCRoW6v0nO/3vNYPhG20YhWU0wQPY7x52EOb4dmYhC4IJfzVDpEPg313BxWRKQglb5RQ1PPkou7JFyCUAAAAEc3NoOg== badkey@rejectme";
#[idm_test]
async fn credential_update_session_init(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let testaccount_uuid = Uuid::new_v4();
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
(Attribute::Name, Value::new_iname("user_account_only")),
(Attribute::Uuid, Value::Uuid(testaccount_uuid)),
(Attribute::Description, Value::new_utf8s("testaccount")),
(Attribute::DisplayName, Value::new_utf8s("testaccount"))
);
let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::PosixAccount.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(Attribute::Uuid, Value::Uuid(TESTPERSON_UUID)),
(Attribute::Description, Value::new_utf8s("testperson")),
(Attribute::DisplayName, Value::new_utf8s("testperson"))
);
let ce = CreateEvent::new_internal(vec![e1, e2]);
let cr = idms_prox_write.qs_write.create(&ce);
assert!(cr.is_ok());
let testaccount = idms_prox_write
.qs_write
.internal_search_uuid(testaccount_uuid)
.expect("failed");
let testperson = idms_prox_write
.qs_write
.internal_search_uuid(TESTPERSON_UUID)
.expect("failed");
let idm_admin = idms_prox_write
.qs_write
.internal_search_uuid(UUID_IDM_ADMIN)
.expect("failed");
let cur = idms_prox_write.init_credential_update(
&InitCredentialUpdateEvent::new_impersonate_entry(testaccount),
ct,
);
assert!(matches!(cur, Err(OperationError::NotAuthorised)));
let cur = idms_prox_write.init_credential_update(
&InitCredentialUpdateEvent::new_impersonate_entry(testperson),
ct,
);
assert!(cur.is_ok());
let cur = idms_prox_write.init_credential_update_intent(
&InitCredentialUpdateIntentEvent::new_impersonate_entry(
idm_admin,
TESTPERSON_UUID,
MINIMUM_INTENT_TTL,
),
ct,
);
assert!(cur.is_ok());
let intent_tok = cur.expect("Failed to create intent token!");
let cur = idms_prox_write
.exchange_intent_credential_update(intent_tok.clone().into(), ct + MINIMUM_INTENT_TTL);
assert!(matches!(cur, Err(OperationError::SessionExpired)));
let cur = idms_prox_write
.exchange_intent_credential_update(intent_tok.clone().into(), ct + MAXIMUM_INTENT_TTL);
assert!(matches!(cur, Err(OperationError::SessionExpired)));
let (cust_a, _c_status) = idms_prox_write
.exchange_intent_credential_update(intent_tok.clone().into(), ct)
.unwrap();
let (cust_b, _c_status) = idms_prox_write
.exchange_intent_credential_update(intent_tok.into(), ct + Duration::from_secs(1))
.unwrap();
let cur = idms_prox_write.commit_credential_update(&cust_a, ct);
trace!(?cur);
assert!(cur.is_err());
let _ = idms_prox_write.commit_credential_update(&cust_b, ct);
idms_prox_write.commit().expect("Failed to commit txn");
}
async fn setup_test_session(
idms: &IdmServer,
ct: Duration,
) -> (CredentialUpdateSessionToken, CredentialUpdateSessionStatus) {
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let modlist = ModifyList::new_purge(Attribute::CredentialTypeMinimum);
idms_prox_write
.qs_write
.internal_modify_uuid(UUID_IDM_ALL_PERSONS, &modlist)
.expect("Unable to change default session exp");
let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::PosixAccount.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(Attribute::Uuid, Value::Uuid(TESTPERSON_UUID)),
(Attribute::Description, Value::new_utf8s("testperson")),
(Attribute::DisplayName, Value::new_utf8s("testperson"))
);
let ce = CreateEvent::new_internal(vec![e2]);
let cr = idms_prox_write.qs_write.create(&ce);
assert!(cr.is_ok());
let testperson = idms_prox_write
.qs_write
.internal_search_uuid(TESTPERSON_UUID)
.expect("failed");
let rrse = RegenerateRadiusSecretEvent::new_internal(TESTPERSON_UUID);
let _ = idms_prox_write
.regenerate_radius_secret(&rrse)
.expect("Failed to reset radius credential 1");
let cur = idms_prox_write.init_credential_update(
&InitCredentialUpdateEvent::new_impersonate_entry(testperson),
ct,
);
idms_prox_write.commit().expect("Failed to commit txn");
cur.expect("Failed to start update")
}
async fn renew_test_session(
idms: &IdmServer,
ct: Duration,
) -> (CredentialUpdateSessionToken, CredentialUpdateSessionStatus) {
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let testperson = idms_prox_write
.qs_write
.internal_search_uuid(TESTPERSON_UUID)
.expect("failed");
let cur = idms_prox_write.init_credential_update(
&InitCredentialUpdateEvent::new_impersonate_entry(testperson),
ct,
);
trace!(renew_test_session_result = ?cur);
idms_prox_write.commit().expect("Failed to commit txn");
cur.expect("Failed to start update")
}
async fn commit_session(idms: &IdmServer, ct: Duration, cust: CredentialUpdateSessionToken) {
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
idms_prox_write
.commit_credential_update(&cust, ct)
.expect("Failed to commit credential update.");
idms_prox_write.commit().expect("Failed to commit txn");
}
async fn check_testperson_password(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
pw: &str,
ct: Duration,
) -> Option<JwsCompact> {
let mut idms_auth = idms.auth().await.unwrap();
let auth_init = AuthEvent::named_init("testperson");
let r1 = idms_auth
.auth(&auth_init, ct, Source::Internal.into())
.await;
let ar = r1.unwrap();
let AuthResult { sessionid, state } = ar;
if !matches!(state, AuthState::Choose(_)) {
debug!("Can't proceed - {:?}", state);
return None;
};
let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::Password);
let r2 = idms_auth
.auth(&auth_begin, ct, Source::Internal.into())
.await;
let ar = r2.unwrap();
let AuthResult { sessionid, state } = ar;
assert!(matches!(state, AuthState::Continue(_)));
let pw_step = AuthEvent::cred_step_password(sessionid, pw);
let r2 = idms_auth.auth(&pw_step, ct, Source::Internal.into()).await;
debug!("r2 ==> {:?}", r2);
idms_auth.commit().expect("Must not fail");
match r2 {
Ok(AuthResult {
sessionid: _,
state: AuthState::Success(token, AuthIssueSession::Token),
}) => {
let da = idms_delayed.try_recv().expect("invalid");
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
Some(*token)
}
_ => None,
}
}
async fn check_testperson_unix_password(
idms: &IdmServer,
pw: &str,
ct: Duration,
) -> Option<UnixUserToken> {
let mut idms_auth = idms.auth().await.unwrap();
let auth_event = UnixUserAuthEvent::new_internal(TESTPERSON_UUID, pw);
idms_auth
.auth_unix(&auth_event, ct)
.await
.expect("Unable to perform unix authentication")
}
async fn check_testperson_password_totp(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
pw: &str,
token: &Totp,
ct: Duration,
) -> Option<JwsCompact> {
let mut idms_auth = idms.auth().await.unwrap();
let auth_init = AuthEvent::named_init("testperson");
let r1 = idms_auth
.auth(&auth_init, ct, Source::Internal.into())
.await;
let ar = r1.unwrap();
let AuthResult { sessionid, state } = ar;
if !matches!(state, AuthState::Choose(_)) {
debug!("Can't proceed - {:?}", state);
return None;
};
let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::PasswordTotp);
let r2 = idms_auth
.auth(&auth_begin, ct, Source::Internal.into())
.await;
let ar = r2.unwrap();
let AuthResult { sessionid, state } = ar;
assert!(matches!(state, AuthState::Continue(_)));
let totp = token
.do_totp_duration_from_epoch(&ct)
.expect("Failed to perform totp step");
let totp_step = AuthEvent::cred_step_totp(sessionid, totp);
let r2 = idms_auth
.auth(&totp_step, ct, Source::Internal.into())
.await;
let ar = r2.unwrap();
let AuthResult { sessionid, state } = ar;
assert!(matches!(state, AuthState::Continue(_)));
let pw_step = AuthEvent::cred_step_password(sessionid, pw);
let r3 = idms_auth.auth(&pw_step, ct, Source::Internal.into()).await;
debug!("r3 ==> {:?}", r3);
idms_auth.commit().expect("Must not fail");
match r3 {
Ok(AuthResult {
sessionid: _,
state: AuthState::Success(token, AuthIssueSession::Token),
}) => {
let da = idms_delayed.try_recv().expect("invalid");
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
Some(*token)
}
_ => None,
}
}
async fn check_testperson_password_backup_code(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
pw: &str,
code: &str,
ct: Duration,
) -> Option<JwsCompact> {
let mut idms_auth = idms.auth().await.unwrap();
let auth_init = AuthEvent::named_init("testperson");
let r1 = idms_auth
.auth(&auth_init, ct, Source::Internal.into())
.await;
let ar = r1.unwrap();
let AuthResult { sessionid, state } = ar;
if !matches!(state, AuthState::Choose(_)) {
debug!("Can't proceed - {:?}", state);
return None;
};
let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::PasswordBackupCode);
let r2 = idms_auth
.auth(&auth_begin, ct, Source::Internal.into())
.await;
let ar = r2.unwrap();
let AuthResult { sessionid, state } = ar;
assert!(matches!(state, AuthState::Continue(_)));
let code_step = AuthEvent::cred_step_backup_code(sessionid, code);
let r2 = idms_auth
.auth(&code_step, ct, Source::Internal.into())
.await;
let ar = r2.unwrap();
let AuthResult { sessionid, state } = ar;
assert!(matches!(state, AuthState::Continue(_)));
let pw_step = AuthEvent::cred_step_password(sessionid, pw);
let r3 = idms_auth.auth(&pw_step, ct, Source::Internal.into()).await;
debug!("r3 ==> {:?}", r3);
idms_auth.commit().expect("Must not fail");
match r3 {
Ok(AuthResult {
sessionid: _,
state: AuthState::Success(token, AuthIssueSession::Token),
}) => {
let da = idms_delayed.try_recv().expect("invalid");
assert!(matches!(da, DelayedAction::BackupCodeRemoval(_)));
let r = idms.delayed_action(ct, da).await;
assert!(r.is_ok());
let da = idms_delayed.try_recv().expect("invalid");
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
Some(*token)
}
_ => None,
}
}
async fn check_testperson_passkey<T: AuthenticatorBackend>(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
wa: &mut WebauthnAuthenticator<T>,
origin: Url,
ct: Duration,
) -> Option<JwsCompact> {
let mut idms_auth = idms.auth().await.unwrap();
let auth_init = AuthEvent::named_init("testperson");
let r1 = idms_auth
.auth(&auth_init, ct, Source::Internal.into())
.await;
let ar = r1.unwrap();
let AuthResult { sessionid, state } = ar;
if !matches!(state, AuthState::Choose(_)) {
debug!("Can't proceed - {:?}", state);
return None;
};
let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::Passkey);
let r2 = idms_auth
.auth(&auth_begin, ct, Source::Internal.into())
.await;
let ar = r2.unwrap();
let AuthResult { sessionid, state } = ar;
trace!(?state);
let rcr = match state {
AuthState::Continue(mut allowed) => match allowed.pop() {
Some(AuthAllowed::Passkey(rcr)) => rcr,
_ => unreachable!(),
},
_ => unreachable!(),
};
trace!(?rcr);
let resp = wa
.do_authentication(origin, rcr)
.expect("failed to use softtoken to authenticate");
let passkey_step = AuthEvent::cred_step_passkey(sessionid, resp);
let r3 = idms_auth
.auth(&passkey_step, ct, Source::Internal.into())
.await;
debug!("r3 ==> {:?}", r3);
idms_auth.commit().expect("Must not fail");
match r3 {
Ok(AuthResult {
sessionid: _,
state: AuthState::Success(token, AuthIssueSession::Token),
}) => {
let da = idms_delayed.try_recv().expect("invalid");
assert!(matches!(da, DelayedAction::WebauthnCounterIncrement(_)));
let r = idms.delayed_action(ct, da).await;
assert!(r.is_ok());
let da = idms_delayed.try_recv().expect("invalid");
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
Some(*token)
}
_ => None,
}
}
#[idm_test]
async fn credential_update_session_cleanup(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (cust, _) = setup_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
let c_status = cutxn.credential_update_status(&cust, ct);
assert!(c_status.is_ok());
drop(cutxn);
let (_cust, _) =
renew_test_session(idms, ct + MAXIMUM_CRED_UPDATE_TTL + Duration::from_secs(1)).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
let c_status = cutxn
.credential_update_status(&cust, ct)
.expect_err("Session is still valid!");
assert!(matches!(c_status, OperationError::InvalidState));
}
#[idm_test]
async fn credential_update_onboarding_create_new_pw(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
) {
let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (cust, _) = setup_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
let c_status = cutxn
.credential_update_status(&cust, ct)
.expect("Failed to get the current session status.");
trace!(?c_status);
assert!(c_status.primary.is_none());
let c_status = cutxn
.credential_primary_set_password(&cust, ct, test_pw)
.expect("Failed to update the primary cred password");
assert!(c_status.can_commit);
drop(cutxn);
commit_session(idms, ct, cust).await;
assert!(check_testperson_password(idms, idms_delayed, test_pw, ct)
.await
.is_some());
let (cust, _) = renew_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
let c_status = cutxn
.credential_update_status(&cust, ct)
.expect("Failed to get the current session status.");
trace!(?c_status);
assert!(c_status.primary.is_some());
let c_status = cutxn
.credential_primary_delete(&cust, ct)
.expect("Failed to delete the primary cred");
trace!(?c_status);
assert!(c_status.primary.is_none());
drop(cutxn);
commit_session(idms, ct, cust).await;
assert!(check_testperson_password(idms, idms_delayed, test_pw, ct)
.await
.is_none());
}
#[idm_test]
async fn credential_update_password_quality_checks(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (cust, _) = setup_test_session(idms, ct).await;
let mut r_txn = idms.proxy_read().await.unwrap();
let radius_secret = r_txn
.qs_read
.internal_search_uuid(TESTPERSON_UUID)
.expect("No such entry")
.get_ava_single_secret(Attribute::RadiusSecret)
.expect("No radius secret found")
.to_string();
drop(r_txn);
let cutxn = idms.cred_update_transaction().await.unwrap();
let c_status = cutxn
.credential_update_status(&cust, ct)
.expect("Failed to get the current session status.");
trace!(?c_status);
assert!(c_status.primary.is_none());
let err = cutxn
.credential_primary_set_password(&cust, ct, "password")
.unwrap_err();
trace!(?err);
assert!(
matches!(err, OperationError::PasswordQuality(details) if details == vec!(PasswordFeedback::TooShort(PW_MIN_LENGTH),))
);
let err = cutxn
.credential_primary_set_password(&cust, ct, "password1234")
.unwrap_err();
trace!(?err);
assert!(
matches!(err, OperationError::PasswordQuality(details) if details
== vec!(
PasswordFeedback::AddAnotherWordOrTwo,
PasswordFeedback::ThisIsACommonPassword,
))
);
let err = cutxn
.credential_primary_set_password(&cust, ct, &radius_secret)
.unwrap_err();
trace!(?err);
assert!(
matches!(err, OperationError::PasswordQuality(details) if details == vec!(PasswordFeedback::DontReusePasswords,))
);
let err = cutxn
.credential_primary_set_password(&cust, ct, "testperson2023")
.unwrap_err();
trace!(?err);
assert!(
matches!(err, OperationError::PasswordQuality(details) if details == vec!(
PasswordFeedback::NamesAndSurnamesByThemselvesAreEasyToGuess,
PasswordFeedback::AvoidDatesAndYearsThatAreAssociatedWithYou,
))
);
let err = cutxn
.credential_primary_set_password(
&cust,
ct,
"demo_badlist_shohfie3aeci2oobur0aru9uushah6EiPi2woh4hohngoighaiRuepieN3ongoo1",
)
.unwrap_err();
trace!(?err);
assert!(
matches!(err, OperationError::PasswordQuality(details) if details == vec!(PasswordFeedback::BadListed))
);
assert!(c_status.can_commit);
drop(cutxn);
}
#[idm_test]
async fn credential_update_password_min_length_account_policy(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let test_pw_min_length = PW_MIN_LENGTH * 2;
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let modlist = ModifyList::new_purge_and_set(
Attribute::AuthPasswordMinimumLength,
Value::Uint32(test_pw_min_length),
);
idms_prox_write
.qs_write
.internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
.expect("Unable to change default session exp");
assert!(idms_prox_write.commit().is_ok());
let (cust, _) = setup_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
let c_status = cutxn
.credential_update_status(&cust, ct)
.expect("Failed to get the current session status.");
trace!(?c_status);
assert!(c_status.primary.is_none());
let pw = password_from_random_len(8);
let err = cutxn
.credential_primary_set_password(&cust, ct, &pw)
.unwrap_err();
trace!(?err);
assert!(
matches!(err, OperationError::PasswordQuality(details) if details == vec!(PasswordFeedback::TooShort(test_pw_min_length),))
);
let pw = password_from_random_len(test_pw_min_length - 1);
let err = cutxn
.credential_primary_set_password(&cust, ct, &pw)
.unwrap_err();
trace!(?err);
assert!(matches!(err,OperationError::PasswordQuality(details)
if details == vec!(PasswordFeedback::TooShort(test_pw_min_length),)));
let pw = password_from_random_len(test_pw_min_length);
let c_status = cutxn
.credential_primary_set_password(&cust, ct, &pw)
.expect("Failed to update the primary cred password");
assert!(c_status.can_commit);
drop(cutxn);
commit_session(idms, ct, cust).await;
}
#[idm_test]
async fn credential_update_onboarding_create_new_mfa_totp_basic(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
) {
let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (cust, _) = setup_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
let c_status = cutxn
.credential_primary_set_password(&cust, ct, test_pw)
.expect("Failed to update the primary cred password");
assert!(c_status.can_commit);
let c_status = cutxn
.credential_primary_init_totp(&cust, ct)
.expect("Failed to update the primary cred password");
let totp_token: Totp = match c_status.mfaregstate {
MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
_ => None,
}
.expect("Unable to retrieve totp token, invalid state.");
trace!(?totp_token);
let chal = totp_token
.do_totp_duration_from_epoch(&ct)
.expect("Failed to perform totp step");
let c_status = cutxn
.credential_primary_check_totp(&cust, ct, chal + 1, "totp")
.expect("Failed to update the primary cred totp");
assert!(matches!(
c_status.mfaregstate,
MfaRegStateStatus::TotpTryAgain
));
let c_status = cutxn
.credential_primary_check_totp(&cust, ct, chal, "totp")
.expect("Failed to update the primary cred totp");
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
_ => false,
});
drop(cutxn);
commit_session(idms, ct, cust).await;
assert!(
check_testperson_password_totp(idms, idms_delayed, test_pw, &totp_token, ct)
.await
.is_some()
);
let (cust, _) = renew_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
let c_status = cutxn
.credential_primary_remove_totp(&cust, ct, "totp")
.expect("Failed to update the primary cred password");
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
assert!(matches!(
c_status.primary.as_ref().map(|c| &c.type_),
Some(CredentialDetailType::Password)
));
drop(cutxn);
commit_session(idms, ct, cust).await;
assert!(check_testperson_password(idms, idms_delayed, test_pw, ct)
.await
.is_some());
}
#[idm_test]
async fn credential_update_onboarding_create_new_mfa_totp_sha1(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
) {
let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (cust, _) = setup_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
let c_status = cutxn
.credential_primary_set_password(&cust, ct, test_pw)
.expect("Failed to update the primary cred password");
assert!(c_status.can_commit);
let c_status = cutxn
.credential_primary_init_totp(&cust, ct)
.expect("Failed to update the primary cred password");
let totp_token: Totp = match c_status.mfaregstate {
MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
_ => None,
}
.expect("Unable to retrieve totp token, invalid state.");
let totp_token = totp_token.downgrade_to_legacy();
trace!(?totp_token);
let chal = totp_token
.do_totp_duration_from_epoch(&ct)
.expect("Failed to perform totp step");
let c_status = cutxn
.credential_primary_check_totp(&cust, ct, chal, "totp")
.expect("Failed to update the primary cred password");
assert!(matches!(
c_status.mfaregstate,
MfaRegStateStatus::TotpInvalidSha1
));
let c_status = cutxn
.credential_primary_accept_sha1_totp(&cust, ct)
.expect("Failed to update the primary cred password");
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
_ => false,
});
drop(cutxn);
commit_session(idms, ct, cust).await;
assert!(
check_testperson_password_totp(idms, idms_delayed, test_pw, &totp_token, ct)
.await
.is_some()
);
}
#[idm_test]
async fn credential_update_onboarding_create_new_mfa_totp_backup_codes(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
) {
let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (cust, _) = setup_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
let _c_status = cutxn
.credential_primary_set_password(&cust, ct, test_pw)
.expect("Failed to update the primary cred password");
assert!(matches!(
cutxn.credential_primary_init_backup_codes(&cust, ct),
Err(OperationError::InvalidState)
));
let c_status = cutxn
.credential_primary_init_totp(&cust, ct)
.expect("Failed to update the primary cred password");
let totp_token: Totp = match c_status.mfaregstate {
MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
_ => None,
}
.expect("Unable to retrieve totp token, invalid state.");
trace!(?totp_token);
let chal = totp_token
.do_totp_duration_from_epoch(&ct)
.expect("Failed to perform totp step");
let c_status = cutxn
.credential_primary_check_totp(&cust, ct, chal, "totp")
.expect("Failed to update the primary cred totp");
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
_ => false,
});
let c_status = cutxn
.credential_primary_init_backup_codes(&cust, ct)
.expect("Failed to update the primary cred password");
let codes = match c_status.mfaregstate {
MfaRegStateStatus::BackupCodes(codes) => Some(codes),
_ => None,
}
.expect("Unable to retrieve backupcodes, invalid state.");
debug!("{:?}", c_status.primary.as_ref().map(|c| &c.type_));
assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
Some(CredentialDetailType::PasswordMfa(totp, _, 8)) => !totp.is_empty(),
_ => false,
});
drop(cutxn);
commit_session(idms, ct, cust).await;
let backup_code = codes.iter().next().expect("No codes available");
assert!(check_testperson_password_backup_code(
idms,
idms_delayed,
test_pw,
backup_code,
ct
)
.await
.is_some());
let (cust, _) = renew_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
let c_status = cutxn
.credential_update_status(&cust, ct)
.expect("Failed to get the current session status.");
assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
Some(CredentialDetailType::PasswordMfa(totp, _, 7)) => !totp.is_empty(),
_ => false,
});
let c_status = cutxn
.credential_primary_remove_backup_codes(&cust, ct)
.expect("Failed to update the primary cred password");
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
_ => false,
});
let c_status = cutxn
.credential_primary_init_backup_codes(&cust, ct)
.expect("Failed to update the primary cred password");
assert!(matches!(
c_status.mfaregstate,
MfaRegStateStatus::BackupCodes(_)
));
assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
Some(CredentialDetailType::PasswordMfa(totp, _, 8)) => !totp.is_empty(),
_ => false,
});
let c_status = cutxn
.credential_primary_remove_totp(&cust, ct, "totp")
.expect("Failed to update the primary cred password");
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
assert!(matches!(
c_status.primary.as_ref().map(|c| &c.type_),
Some(CredentialDetailType::Password)
));
drop(cutxn);
commit_session(idms, ct, cust).await;
}
#[idm_test]
async fn credential_update_onboarding_cancel_inprogress_totp(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
) {
let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (cust, _) = setup_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
let c_status = cutxn
.credential_primary_set_password(&cust, ct, test_pw)
.expect("Failed to update the primary cred password");
assert!(c_status.can_commit);
let c_status = cutxn
.credential_primary_init_totp(&cust, ct)
.expect("Failed to update the primary cred totp");
assert!(c_status.can_commit);
assert!(matches!(
c_status.mfaregstate,
MfaRegStateStatus::TotpCheck(_)
));
let c_status = cutxn
.credential_update_cancel_mfareg(&cust, ct)
.expect("Failed to cancel in-flight totp change");
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
assert!(c_status.can_commit);
drop(cutxn);
commit_session(idms, ct, cust).await;
assert!(check_testperson_password(idms, idms_delayed, test_pw, ct)
.await
.is_some());
}
async fn create_new_passkey(
ct: Duration,
origin: &Url,
cutxn: &IdmServerCredUpdateTransaction<'_>,
cust: &CredentialUpdateSessionToken,
wa: &mut WebauthnAuthenticator<SoftPasskey>,
) -> CredentialUpdateSessionStatus {
let c_status = cutxn
.credential_passkey_init(cust, ct)
.expect("Failed to initiate passkey registration");
assert!(c_status.passkeys.is_empty());
let passkey_chal = match c_status.mfaregstate {
MfaRegStateStatus::Passkey(c) => Some(c),
_ => None,
}
.expect("Unable to access passkey challenge, invalid state");
let passkey_resp = wa
.do_registration(origin.clone(), passkey_chal)
.expect("Failed to create soft passkey");
let label = "softtoken".to_string();
let c_status = cutxn
.credential_passkey_finish(cust, ct, label, &passkey_resp)
.expect("Failed to initiate passkey registration");
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
assert!(c_status.primary.as_ref().is_none());
trace!(?c_status);
assert_eq!(c_status.passkeys.len(), 1);
c_status
}
#[idm_test]
async fn credential_update_onboarding_create_new_passkey(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (cust, _) = setup_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
let origin = cutxn.get_origin().clone();
let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
let c_status = create_new_passkey(ct, &origin, &cutxn, &cust, &mut wa).await;
let pk_uuid = c_status.passkeys.first().map(|pkd| pkd.uuid).unwrap();
drop(cutxn);
commit_session(idms, ct, cust).await;
assert!(
check_testperson_passkey(idms, idms_delayed, &mut wa, origin.clone(), ct)
.await
.is_some()
);
let (cust, _) = renew_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
trace!(?c_status);
assert!(c_status.primary.is_none());
assert_eq!(c_status.passkeys.len(), 1);
let c_status = cutxn
.credential_passkey_remove(&cust, ct, pk_uuid)
.expect("Failed to delete the passkey");
trace!(?c_status);
assert!(c_status.primary.is_none());
assert!(c_status.passkeys.is_empty());
drop(cutxn);
commit_session(idms, ct, cust).await;
assert!(
check_testperson_passkey(idms, idms_delayed, &mut wa, origin, ct)
.await
.is_none()
);
}
#[idm_test]
async fn credential_update_access_denied(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let sync_uuid = Uuid::new_v4();
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::SyncAccount.to_value()),
(Attribute::Name, Value::new_iname("test_scim_sync")),
(Attribute::Uuid, Value::Uuid(sync_uuid)),
(
Attribute::Description,
Value::new_utf8s("A test sync agreement")
)
);
let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::SyncObject.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::PosixAccount.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::SyncParentUuid, Value::Refer(sync_uuid)),
(Attribute::Name, Value::new_iname("testperson")),
(Attribute::Uuid, Value::Uuid(TESTPERSON_UUID)),
(Attribute::Description, Value::new_utf8s("testperson")),
(Attribute::DisplayName, Value::new_utf8s("testperson"))
);
let ce = CreateEvent::new_internal(vec![e1, e2]);
let cr = idms_prox_write.qs_write.create(&ce);
assert!(cr.is_ok());
let testperson = idms_prox_write
.qs_write
.internal_search_uuid(TESTPERSON_UUID)
.expect("failed");
let cur = idms_prox_write.init_credential_update(
&InitCredentialUpdateEvent::new_impersonate_entry(testperson),
ct,
);
idms_prox_write.commit().expect("Failed to commit txn");
let (cust, custatus) = cur.expect("Failed to start update");
trace!(?custatus);
let CredentialUpdateSessionStatus {
spn: _,
displayname: _,
ext_cred_portal,
mfaregstate: _,
can_commit: _,
warnings: _,
primary: _,
primary_state,
passkeys: _,
passkeys_state,
attested_passkeys: _,
attested_passkeys_state,
attested_passkeys_allowed_devices: _,
unixcred_state,
unixcred: _,
sshkeys: _,
sshkeys_state,
} = custatus;
assert!(matches!(ext_cred_portal, CUExtPortal::Hidden));
assert!(matches!(primary_state, CredentialState::AccessDeny));
assert!(matches!(passkeys_state, CredentialState::AccessDeny));
assert!(matches!(
attested_passkeys_state,
CredentialState::AccessDeny
));
assert!(matches!(unixcred_state, CredentialState::AccessDeny));
assert!(matches!(sshkeys_state, CredentialState::AccessDeny));
let cutxn = idms.cred_update_transaction().await.unwrap();
let err = cutxn
.credential_primary_set_password(&cust, ct, "password")
.unwrap_err();
assert!(matches!(err, OperationError::AccessDenied));
let err = cutxn
.credential_unix_set_password(&cust, ct, "password")
.unwrap_err();
assert!(matches!(err, OperationError::AccessDenied));
let sshkey = SshPublicKey::from_string(SSHKEY_VALID_1).expect("Invalid SSHKEY_VALID_1");
let err = cutxn
.credential_sshkey_add(&cust, ct, "label".to_string(), sshkey)
.unwrap_err();
assert!(matches!(err, OperationError::AccessDenied));
let err = cutxn.credential_primary_init_totp(&cust, ct).unwrap_err();
assert!(matches!(err, OperationError::AccessDenied));
let err = cutxn
.credential_primary_check_totp(&cust, ct, 0, "totp")
.unwrap_err();
assert!(matches!(err, OperationError::AccessDenied));
let err = cutxn
.credential_primary_accept_sha1_totp(&cust, ct)
.unwrap_err();
assert!(matches!(err, OperationError::AccessDenied));
let err = cutxn
.credential_primary_remove_totp(&cust, ct, "totp")
.unwrap_err();
assert!(matches!(err, OperationError::AccessDenied));
let err = cutxn
.credential_primary_init_backup_codes(&cust, ct)
.unwrap_err();
assert!(matches!(err, OperationError::AccessDenied));
let err = cutxn
.credential_primary_remove_backup_codes(&cust, ct)
.unwrap_err();
assert!(matches!(err, OperationError::AccessDenied));
let err = cutxn.credential_primary_delete(&cust, ct).unwrap_err();
assert!(matches!(err, OperationError::AccessDenied));
let err = cutxn.credential_passkey_init(&cust, ct).unwrap_err();
assert!(matches!(err, OperationError::AccessDenied));
let err = cutxn
.credential_passkey_remove(&cust, ct, Uuid::new_v4())
.unwrap_err();
assert!(matches!(err, OperationError::AccessDenied));
let c_status = cutxn
.credential_update_status(&cust, ct)
.expect("Failed to get the current session status.");
trace!(?c_status);
assert!(c_status.primary.is_none());
assert!(c_status.passkeys.is_empty());
drop(cutxn);
commit_session(idms, ct, cust).await;
}
#[idm_test]
async fn credential_update_account_policy_mfa_required(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let modlist = ModifyList::new_purge_and_set(
Attribute::CredentialTypeMinimum,
CredentialType::Mfa.into(),
);
idms_prox_write
.qs_write
.internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
.expect("Unable to change default session exp");
assert!(idms_prox_write.commit().is_ok());
let (cust, _) = setup_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
let c_status = cutxn
.credential_update_status(&cust, ct)
.expect("Failed to get the current session status.");
trace!(?c_status);
assert!(c_status.primary.is_none());
let c_status = cutxn
.credential_primary_set_password(&cust, ct, test_pw)
.expect("Failed to update the primary cred password");
assert!(!c_status.can_commit);
assert!(c_status
.warnings
.contains(&CredentialUpdateSessionStatusWarnings::MfaRequired));
let c_status = cutxn
.credential_primary_init_totp(&cust, ct)
.expect("Failed to update the primary cred password");
let totp_token: Totp = match c_status.mfaregstate {
MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
_ => None,
}
.expect("Unable to retrieve totp token, invalid state.");
trace!(?totp_token);
let chal = totp_token
.do_totp_duration_from_epoch(&ct)
.expect("Failed to perform totp step");
let c_status = cutxn
.credential_primary_check_totp(&cust, ct, chal, "totp")
.expect("Failed to update the primary cred totp");
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
_ => false,
});
assert!(c_status.can_commit);
assert!(c_status.warnings.is_empty());
drop(cutxn);
commit_session(idms, ct, cust).await;
let (cust, _) = renew_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
let c_status = cutxn
.credential_primary_remove_totp(&cust, ct, "totp")
.expect("Failed to update the primary cred totp");
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
assert!(matches!(
c_status.primary.as_ref().map(|c| &c.type_),
Some(CredentialDetailType::Password)
));
assert!(!c_status.can_commit);
assert!(c_status
.warnings
.contains(&CredentialUpdateSessionStatusWarnings::MfaRequired));
let c_status = cutxn
.credential_primary_delete(&cust, ct)
.expect("Failed to delete the primary credential");
assert!(c_status.primary.is_none());
let origin = cutxn.get_origin().clone();
let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
let c_status = create_new_passkey(ct, &origin, &cutxn, &cust, &mut wa).await;
assert!(c_status.can_commit);
assert!(c_status.warnings.is_empty());
assert_eq!(c_status.passkeys.len(), 1);
drop(cutxn);
commit_session(idms, ct, cust).await;
}
#[idm_test]
async fn credential_update_account_policy_passkey_required(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let modlist = ModifyList::new_purge_and_set(
Attribute::CredentialTypeMinimum,
CredentialType::Passkey.into(),
);
idms_prox_write
.qs_write
.internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
.expect("Unable to change default session exp");
assert!(idms_prox_write.commit().is_ok());
let (cust, _) = setup_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
let c_status = cutxn
.credential_update_status(&cust, ct)
.expect("Failed to get the current session status.");
trace!(?c_status);
assert!(c_status.primary.is_none());
assert!(matches!(
c_status.primary_state,
CredentialState::PolicyDeny
));
let err = cutxn
.credential_primary_set_password(&cust, ct, test_pw)
.unwrap_err();
assert!(matches!(err, OperationError::AccessDenied));
let origin = cutxn.get_origin().clone();
let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
let c_status = create_new_passkey(ct, &origin, &cutxn, &cust, &mut wa).await;
assert!(c_status.can_commit);
assert!(c_status.warnings.is_empty());
assert_eq!(c_status.passkeys.len(), 1);
drop(cutxn);
commit_session(idms, ct, cust).await;
}
#[idm_test]
async fn credential_update_account_policy_attested_passkey_required(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (soft_token_valid, ca_root) = SoftToken::new(true).unwrap();
let mut wa_token_valid = WebauthnAuthenticator::new(soft_token_valid);
let mut att_ca_builder = AttestationCaListBuilder::new();
att_ca_builder
.insert_device_x509(
ca_root,
softtoken::AAGUID,
"softtoken".to_string(),
Default::default(),
)
.unwrap();
let att_ca_list = att_ca_builder.build();
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let modlist = ModifyList::new_purge_and_set(
Attribute::WebauthnAttestationCaList,
Value::WebauthnAttestationCaList(att_ca_list),
);
idms_prox_write
.qs_write
.internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
.expect("Unable to change webauthn attestation policy");
assert!(idms_prox_write.commit().is_ok());
let (soft_token_invalid, _) = SoftToken::new(true).unwrap();
let mut wa_token_invalid = WebauthnAuthenticator::new(soft_token_invalid);
let mut wa_passkey_invalid = WebauthnAuthenticator::new(SoftPasskey::new(true));
let (cust, _) = setup_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
let origin = cutxn.get_origin().clone();
let c_status = cutxn
.credential_update_status(&cust, ct)
.expect("Failed to get the current session status.");
trace!(?c_status);
assert!(c_status.attested_passkeys.is_empty());
assert_eq!(
c_status.attested_passkeys_allowed_devices,
vec!["softtoken".to_string()]
);
let err = cutxn.credential_passkey_init(&cust, ct).unwrap_err();
assert!(matches!(err, OperationError::AccessDenied));
let c_status = cutxn
.credential_attested_passkey_init(&cust, ct)
.expect("Failed to initiate attested passkey registration");
let passkey_chal = match c_status.mfaregstate {
MfaRegStateStatus::AttestedPasskey(c) => Some(c),
_ => None,
}
.expect("Unable to access passkey challenge, invalid state");
let passkey_resp = wa_passkey_invalid
.do_registration(origin.clone(), passkey_chal)
.expect("Failed to create soft passkey");
let label = "softtoken".to_string();
let err = cutxn
.credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
.unwrap_err();
assert!(matches!(
err,
OperationError::CU0001WebauthnAttestationNotTrusted
));
let c_status = cutxn
.credential_attested_passkey_init(&cust, ct)
.expect("Failed to initiate attested passkey registration");
let passkey_chal = match c_status.mfaregstate {
MfaRegStateStatus::AttestedPasskey(c) => Some(c),
_ => None,
}
.expect("Unable to access passkey challenge, invalid state");
let passkey_resp = wa_token_invalid
.do_registration(origin.clone(), passkey_chal)
.expect("Failed to create soft passkey");
let label = "softtoken".to_string();
let err = cutxn
.credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
.unwrap_err();
assert!(matches!(
err,
OperationError::CU0001WebauthnAttestationNotTrusted
));
let c_status = cutxn
.credential_attested_passkey_init(&cust, ct)
.expect("Failed to initiate attested passkey registration");
let passkey_chal = match c_status.mfaregstate {
MfaRegStateStatus::AttestedPasskey(c) => Some(c),
_ => None,
}
.expect("Unable to access passkey challenge, invalid state");
let passkey_resp = wa_token_valid
.do_registration(origin.clone(), passkey_chal)
.expect("Failed to create soft passkey");
let label = "softtoken".to_string();
let c_status = cutxn
.credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
.expect("Failed to initiate passkey registration");
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
trace!(?c_status);
assert_eq!(c_status.attested_passkeys.len(), 1);
let pk_uuid = c_status
.attested_passkeys
.first()
.map(|pkd| pkd.uuid)
.unwrap();
drop(cutxn);
commit_session(idms, ct, cust).await;
assert!(check_testperson_passkey(
idms,
idms_delayed,
&mut wa_token_valid,
origin.clone(),
ct
)
.await
.is_some());
let (cust, _) = renew_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
trace!(?c_status);
assert!(c_status.primary.is_none());
assert!(c_status.passkeys.is_empty());
assert_eq!(c_status.attested_passkeys.len(), 1);
let c_status = cutxn
.credential_attested_passkey_remove(&cust, ct, pk_uuid)
.expect("Failed to delete the attested passkey");
trace!(?c_status);
assert!(c_status.primary.is_none());
assert!(c_status.passkeys.is_empty());
assert!(c_status.attested_passkeys.is_empty());
drop(cutxn);
commit_session(idms, ct, cust).await;
assert!(
check_testperson_passkey(idms, idms_delayed, &mut wa_token_valid, origin, ct)
.await
.is_none()
);
}
#[idm_test(audit = 1)]
async fn credential_update_account_policy_attested_passkey_changed(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
idms_audit: &mut IdmServerAudit,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (soft_token_1, ca_root_1) = SoftToken::new(true).unwrap();
let mut wa_token_1 = WebauthnAuthenticator::new(soft_token_1);
let (_soft_token_2, ca_root_2) = SoftToken::new(true).unwrap();
let mut att_ca_builder = AttestationCaListBuilder::new();
att_ca_builder
.insert_device_x509(
ca_root_1.clone(),
softtoken::AAGUID,
"softtoken_1".to_string(),
Default::default(),
)
.unwrap();
let att_ca_list = att_ca_builder.build();
trace!(?att_ca_list);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let modlist = ModifyList::new_purge_and_set(
Attribute::WebauthnAttestationCaList,
Value::WebauthnAttestationCaList(att_ca_list),
);
idms_prox_write
.qs_write
.internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
.expect("Unable to change webauthn attestation policy");
assert!(idms_prox_write.commit().is_ok());
let mut att_ca_builder = AttestationCaListBuilder::new();
att_ca_builder
.insert_device_x509(
ca_root_2,
softtoken::AAGUID,
"softtoken_2".to_string(),
Default::default(),
)
.unwrap();
let att_ca_list_post = att_ca_builder.build();
let (cust, _) = setup_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
let origin = cutxn.get_origin().clone();
let c_status = cutxn
.credential_attested_passkey_init(&cust, ct)
.expect("Failed to initiate attested passkey registration");
let passkey_chal = match c_status.mfaregstate {
MfaRegStateStatus::AttestedPasskey(c) => Some(c),
_ => None,
}
.expect("Unable to access passkey challenge, invalid state");
let passkey_resp = wa_token_1
.do_registration(origin.clone(), passkey_chal)
.expect("Failed to create soft passkey");
let label = "softtoken".to_string();
let c_status = cutxn
.credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
.expect("Failed to initiate passkey registration");
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
trace!(?c_status);
assert_eq!(c_status.attested_passkeys.len(), 1);
drop(cutxn);
commit_session(idms, ct, cust).await;
assert!(
check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
.await
.is_some()
);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let modlist = ModifyList::new_purge_and_set(
Attribute::WebauthnAttestationCaList,
Value::WebauthnAttestationCaList(att_ca_list_post),
);
idms_prox_write
.qs_write
.internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
.expect("Unable to change webauthn attestation policy");
assert!(idms_prox_write.commit().is_ok());
assert!(
check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
.await
.is_none()
);
match idms_audit.audit_rx().try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => panic!("Oh no"),
}
let (cust, _) = renew_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
let c_status = cutxn
.credential_update_status(&cust, ct)
.expect("Failed to get the current session status.");
trace!(?c_status);
assert!(c_status.attested_passkeys.is_empty());
drop(cutxn);
commit_session(idms, ct, cust).await;
assert!(
check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
.await
.is_none()
);
}
#[idm_test]
async fn credential_update_account_policy_attested_passkey_downgrade(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (soft_token_1, ca_root_1) = SoftToken::new(true).unwrap();
let mut wa_token_1 = WebauthnAuthenticator::new(soft_token_1);
let mut att_ca_builder = AttestationCaListBuilder::new();
att_ca_builder
.insert_device_x509(
ca_root_1.clone(),
softtoken::AAGUID,
"softtoken_1".to_string(),
Default::default(),
)
.unwrap();
let att_ca_list = att_ca_builder.build();
trace!(?att_ca_list);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let modlist = ModifyList::new_purge_and_set(
Attribute::WebauthnAttestationCaList,
Value::WebauthnAttestationCaList(att_ca_list),
);
idms_prox_write
.qs_write
.internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
.expect("Unable to change webauthn attestation policy");
assert!(idms_prox_write.commit().is_ok());
let (cust, _) = setup_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
let origin = cutxn.get_origin().clone();
let c_status = cutxn
.credential_attested_passkey_init(&cust, ct)
.expect("Failed to initiate attested passkey registration");
let passkey_chal = match c_status.mfaregstate {
MfaRegStateStatus::AttestedPasskey(c) => Some(c),
_ => None,
}
.expect("Unable to access passkey challenge, invalid state");
let passkey_resp = wa_token_1
.do_registration(origin.clone(), passkey_chal)
.expect("Failed to create soft passkey");
let label = "softtoken".to_string();
let c_status = cutxn
.credential_attested_passkey_finish(&cust, ct, label, &passkey_resp)
.expect("Failed to initiate passkey registration");
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
trace!(?c_status);
assert_eq!(c_status.attested_passkeys.len(), 1);
drop(cutxn);
commit_session(idms, ct, cust).await;
assert!(
check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
.await
.is_some()
);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let modlist = ModifyList::new_purge(Attribute::WebauthnAttestationCaList);
idms_prox_write
.qs_write
.internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
.expect("Unable to change webauthn attestation policy");
assert!(idms_prox_write.commit().is_ok());
assert!(
check_testperson_passkey(idms, idms_delayed, &mut wa_token_1, origin.clone(), ct)
.await
.is_some()
);
let (cust, _) = renew_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
let c_status = cutxn
.credential_update_status(&cust, ct)
.expect("Failed to get the current session status.");
trace!(?c_status);
assert_eq!(c_status.attested_passkeys.len(), 1);
assert!(matches!(
c_status.attested_passkeys_state,
CredentialState::DeleteOnly
));
drop(cutxn);
commit_session(idms, ct, cust).await;
}
#[idm_test]
async fn credential_update_unix_password(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (cust, _) = setup_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
let c_status = cutxn
.credential_update_status(&cust, ct)
.expect("Failed to get the current session status.");
trace!(?c_status);
assert!(c_status.unixcred.is_none());
let c_status = cutxn
.credential_unix_set_password(&cust, ct, test_pw)
.expect("Failed to update the unix cred password");
assert!(c_status.can_commit);
drop(cutxn);
commit_session(idms, ct, cust).await;
assert!(check_testperson_unix_password(idms, test_pw, ct)
.await
.is_some());
let (cust, _) = renew_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
let c_status = cutxn
.credential_update_status(&cust, ct)
.expect("Failed to get the current session status.");
trace!(?c_status);
assert!(c_status.unixcred.is_some());
let c_status = cutxn
.credential_unix_delete(&cust, ct)
.expect("Failed to delete the unix cred");
trace!(?c_status);
assert!(c_status.unixcred.is_none());
drop(cutxn);
commit_session(idms, ct, cust).await;
assert!(check_testperson_unix_password(idms, test_pw, ct)
.await
.is_none());
}
#[idm_test]
async fn credential_update_sshkeys(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
let sshkey_valid_1 =
SshPublicKey::from_string(SSHKEY_VALID_1).expect("Invalid SSHKEY_VALID_1");
let sshkey_valid_2 =
SshPublicKey::from_string(SSHKEY_VALID_2).expect("Invalid SSHKEY_VALID_2");
assert!(SshPublicKey::from_string(SSHKEY_INVALID).is_err());
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let (cust, _) = setup_test_session(idms, ct).await;
let cutxn = idms.cred_update_transaction().await.unwrap();
let c_status = cutxn
.credential_update_status(&cust, ct)
.expect("Failed to get the current session status.");
trace!(?c_status);
assert!(c_status.sshkeys.is_empty());
let result = cutxn.credential_sshkey_add(&cust, ct, "".to_string(), sshkey_valid_1.clone());
assert!(matches!(result, Err(OperationError::InvalidLabel)));
let result =
cutxn.credential_sshkey_add(&cust, ct, "🚛".to_string(), sshkey_valid_1.clone());
assert!(matches!(result, Err(OperationError::InvalidLabel)));
let result = cutxn.credential_sshkey_remove(&cust, ct, "key1");
assert!(matches!(result, Err(OperationError::NoMatchingEntries)));
let c_status = cutxn
.credential_sshkey_add(&cust, ct, "key1".to_string(), sshkey_valid_1.clone())
.expect("Failed to add sshkey_valid_1");
trace!(?c_status);
assert_eq!(c_status.sshkeys.len(), 1);
assert!(c_status.sshkeys.contains_key("key1"));
let c_status = cutxn
.credential_sshkey_add(&cust, ct, "key2".to_string(), sshkey_valid_2.clone())
.expect("Failed to add sshkey_valid_2");
trace!(?c_status);
assert_eq!(c_status.sshkeys.len(), 2);
assert!(c_status.sshkeys.contains_key("key1"));
assert!(c_status.sshkeys.contains_key("key2"));
let c_status = cutxn
.credential_sshkey_remove(&cust, ct, "key2")
.expect("Failed to remove sshkey_valid_2");
trace!(?c_status);
assert_eq!(c_status.sshkeys.len(), 1);
assert!(c_status.sshkeys.contains_key("key1"));
let result =
cutxn.credential_sshkey_add(&cust, ct, "key1".to_string(), sshkey_valid_2.clone());
assert!(matches!(result, Err(OperationError::DuplicateLabel)));
let result =
cutxn.credential_sshkey_add(&cust, ct, "key2".to_string(), sshkey_valid_1.clone());
assert!(matches!(result, Err(OperationError::DuplicateKey)));
drop(cutxn);
commit_session(idms, ct, cust).await;
}
}