use crate::prelude::*;
use std::time::SystemTime;
use kanidm_proto::internal::IdentifyUserResponse;
use openssl::ec::EcKey;
use openssl::pkey::{PKey, Private, Public};
use openssl::pkey_ctx::PkeyCtx;
use sketching::admin_error;
use uuid::Uuid;
use crate::credential::totp::{Totp, TotpAlgo, TotpDigits};
use crate::server::QueryServerTransaction;
use crate::{event::SearchEvent, server::identity::Identity};
use crate::idm::server::IdmServerProxyReadTransaction;
static TOTP_STEP: u64 = 30;
#[derive(Debug)]
pub struct IdentifyUserStartEvent {
pub target: Uuid,
pub ident: Identity,
}
impl IdentifyUserStartEvent {
pub fn new(target: Uuid, ident: Identity) -> Self {
IdentifyUserStartEvent { target, ident }
}
}
pub struct IdentifyUserDisplayCodeEvent {
pub target: Uuid,
pub ident: Identity,
}
impl IdentifyUserDisplayCodeEvent {
pub fn new(target: Uuid, ident: Identity) -> Self {
IdentifyUserDisplayCodeEvent { target, ident }
}
}
pub struct IdentifyUserSubmitCodeEvent {
pub code: u32,
pub target: Uuid,
pub ident: Identity,
}
impl IdentifyUserSubmitCodeEvent {
pub fn new(target: Uuid, ident: Identity, code: u32) -> Self {
IdentifyUserSubmitCodeEvent {
target,
ident,
code,
}
}
}
impl IdmServerProxyReadTransaction<'_> {
pub fn handle_identify_user_start(
&mut self,
IdentifyUserStartEvent { target, ident }: &IdentifyUserStartEvent,
) -> Result<IdentifyUserResponse, OperationError> {
if let Some(early_response) = self.check_for_early_return_conditions(ident, target)? {
return Ok(early_response);
}
let response = if ident.get_uuid() < Some(*target) {
IdentifyUserResponse::WaitForCode
} else {
let totp_secret = self.get_self_totp_secret(target, ident)?;
let totp = self.compute_totp(totp_secret)?;
IdentifyUserResponse::ProvideCode {
step: TOTP_STEP as u32,
totp,
}
};
Ok(response)
}
pub fn handle_identify_user_display_code(
&mut self,
IdentifyUserDisplayCodeEvent { target, ident }: &IdentifyUserDisplayCodeEvent,
) -> Result<IdentifyUserResponse, OperationError> {
if let Some(early_response) = self.check_for_early_return_conditions(ident, target)? {
return Ok(early_response);
}
let totp_secret = self.get_self_totp_secret(target, ident)?;
let totp = self.compute_totp(totp_secret)?;
Ok(IdentifyUserResponse::ProvideCode {
step: TOTP_STEP as u32,
totp,
})
}
pub fn handle_identify_user_submit_code(
&mut self,
IdentifyUserSubmitCodeEvent {
target,
ident,
code,
}: &IdentifyUserSubmitCodeEvent,
) -> Result<IdentifyUserResponse, OperationError> {
if let Some(early_response) = self.check_for_early_return_conditions(ident, target)? {
return Ok(early_response);
}
let totp_secret = self.get_other_user_totp_secret(target, ident)?;
let other_user_totp = self.compute_totp(totp_secret)?;
if other_user_totp != *code {
return Ok(IdentifyUserResponse::CodeFailure);
}
let res = if ident.get_uuid() < Some(*target) {
let shared_secret = self.get_self_totp_secret(target, ident)?;
let totp = self.compute_totp(shared_secret)?;
IdentifyUserResponse::ProvideCode {
step: TOTP_STEP as u32,
totp,
}
} else {
IdentifyUserResponse::Success
};
Ok(res)
}
fn check_for_early_return_conditions(
&mut self,
ident: &Identity,
target: &Uuid,
) -> Result<Option<IdentifyUserResponse>, OperationError> {
if !self.check_if_identify_feature_available(ident)? {
return Ok(Some(IdentifyUserResponse::IdentityVerificationUnavailable));
};
if !self.is_valid_user_uuid(ident, target)? {
return Ok(Some(IdentifyUserResponse::InvalidUserId));
};
if ident.get_uuid().eq(&Some(*target)) {
return Ok(Some(IdentifyUserResponse::IdentityVerificationAvailable));
};
Ok(None)
}
fn check_if_identify_feature_available(
&mut self,
ident: &Identity,
) -> Result<bool, OperationError> {
let search = match SearchEvent::from_whoami_request(ident.clone(), &self.qs_read) {
Ok(s) => s,
Err(e) => {
admin_error!("Failed to generate whoami search event: {:?}", e);
return Err(e);
}
};
self.qs_read
.search(&search)
.and_then(|mut entries| entries.pop().ok_or(OperationError::NoMatchingEntries))
.map(
|entry| match entry.get_ava_single_eckey_private(Attribute::IdVerificationEcKey) {
Some(key) => key.check_key().is_ok(),
None => false,
},
)
}
fn is_valid_user_uuid(
&mut self,
ident: &Identity,
target: &Uuid,
) -> Result<bool, OperationError> {
let search =
match SearchEvent::from_target_uuid_request(ident.clone(), *target, &self.qs_read) {
Ok(s) => s,
Err(e) => {
admin_error!("Failed to retrieve user with the given UUID: {:?}", e);
return Err(e);
}
};
let user_entry = self
.qs_read
.search(&search)
.and_then(|mut entries| entries.pop().ok_or(OperationError::NoMatchingEntries))?;
match user_entry.get_ava_single_eckey_public(Attribute::IdVerificationEcKey) {
Some(key) => Ok(key.check_key().is_ok()),
None => Ok(false),
}
}
fn get_user_own_key(&mut self, ident: &Identity) -> Result<EcKey<Private>, OperationError> {
let search = match SearchEvent::from_whoami_request(ident.clone(), &self.qs_read) {
Ok(s) => s,
Err(e) => {
admin_error!(
"Failed to retrieve user with the given UUID: {}. \n{:?}",
ident.get_uuid().unwrap_or_default(),
e
);
return Err(e);
}
};
self.qs_read
.search(&search)
.and_then(|mut entries| entries.pop().ok_or(OperationError::NoMatchingEntries))
.and_then(|entry| {
match entry.get_ava_single_eckey_private(Attribute::IdVerificationEcKey) {
Some(key) => Ok(key.clone()),
None => Err(OperationError::InvalidAccountState(format!(
"{}'s private key is missing!",
ident.get_uuid().unwrap_or_default()
))),
}
})
}
fn get_other_user_public_key(
&mut self,
target: &Uuid,
ident: &Identity,
) -> Result<EcKey<Public>, OperationError> {
let search =
match SearchEvent::from_target_uuid_request(ident.clone(), *target, &self.qs_read) {
Ok(s) => s,
Err(e) => {
admin_error!(
"Failed to retrieve user with the given UUID: {}. \n{:?}",
ident.get_uuid().unwrap_or_default(),
e
);
return Err(e);
}
};
self.qs_read
.search(&search)
.and_then(|mut entries| entries.pop().ok_or(OperationError::NoMatchingEntries))
.and_then(|entry| {
match entry.get_ava_single_eckey_public(Attribute::IdVerificationEcKey) {
Some(key) => Ok(key.clone()),
None => Err(OperationError::InvalidAccountState(format!(
"{target}'s public key is missing!",
))),
}
})
}
fn compute_totp(&mut self, totp_secret: Vec<u8>) -> Result<u32, OperationError> {
let totp = Totp::new(totp_secret, TOTP_STEP, TotpAlgo::Sha256, TotpDigits::Six);
let current_time = SystemTime::now();
totp.do_totp(¤t_time)
.map_err(|_| OperationError::CryptographyError)
}
fn get_self_totp_secret(
&mut self,
target: &Uuid,
ident: &Identity,
) -> Result<Vec<u8>, OperationError> {
let self_private = self.get_user_own_key(ident)?;
let other_user_public_key = self.get_other_user_public_key(target, ident)?;
let mut shared_key = self.derive_shared_key(self_private, other_user_public_key)?;
let Some(self_uuid) = ident.get_uuid() else {
return Err(OperationError::NotAuthenticated);
};
shared_key.extend_from_slice(self_uuid.as_bytes());
Ok(shared_key)
}
fn get_other_user_totp_secret(
&mut self,
target: &Uuid,
ident: &Identity,
) -> Result<Vec<u8>, OperationError> {
let self_private = self.get_user_own_key(ident)?;
let other_user_public_key = self.get_other_user_public_key(target, ident)?;
let mut shared_key = self.derive_shared_key(self_private, other_user_public_key)?;
shared_key.extend_from_slice(target.as_bytes());
Ok(shared_key)
}
fn derive_shared_key(
&self,
private: EcKey<Private>,
public: EcKey<Public>,
) -> Result<Vec<u8>, OperationError> {
let cryptography_error = |_| OperationError::CryptographyError;
let pkey_private = PKey::from_ec_key(private).map_err(cryptography_error)?;
let pkey_public = PKey::from_ec_key(public).map_err(cryptography_error)?;
let mut private_key_ctx: PkeyCtx<Private> =
PkeyCtx::new(&pkey_private).map_err(cryptography_error)?;
private_key_ctx.derive_init().map_err(cryptography_error)?;
private_key_ctx
.derive_set_peer(&pkey_public)
.map_err(cryptography_error)?;
let keylen = private_key_ctx.derive(None).map_err(cryptography_error)?;
let mut tmp_vec = vec![0; keylen];
let buffer = tmp_vec.as_mut_slice();
private_key_ctx
.derive(Some(buffer))
.map_err(cryptography_error)?;
Ok(buffer.to_vec())
}
}
#[cfg(test)]
mod test {
use kanidm_proto::internal::IdentifyUserResponse;
use crate::idm::identityverification::{
IdentifyUserDisplayCodeEvent, IdentifyUserStartEvent, IdentifyUserSubmitCodeEvent,
};
use crate::prelude::*;
#[idm_test]
async fn test_identity_verification_unavailable(
idms: &IdmServer,
_idms_delayed: &IdmServerDelayed,
) {
let ct = duration_from_epoch_now();
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let self_uuid = Uuid::new_v4();
let valid_user_uuid = Uuid::new_v4();
let e1 = create_invalid_user_account(self_uuid);
let e2 = create_valid_user_account(valid_user_uuid);
let ce = CreateEvent::new_internal(vec![e1, e2]);
assert!(idms_prox_write.qs_write.create(&ce).is_ok());
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let ident = idms_prox_read
.qs_read
.internal_search_uuid(self_uuid)
.map(Identity::from_impersonate_entry_readonly)
.expect("Failed to impersonate identity");
let res = idms_prox_read
.handle_identify_user_start(&IdentifyUserStartEvent::new(self_uuid, ident.clone()));
assert!(matches!(
res,
Ok(IdentifyUserResponse::IdentityVerificationUnavailable)
));
let res = idms_prox_read.handle_identify_user_start(&IdentifyUserStartEvent::new(
valid_user_uuid,
ident.clone(),
));
assert!(matches!(
res,
Ok(IdentifyUserResponse::IdentityVerificationUnavailable)
));
let res = idms_prox_read.handle_identify_user_display_code(
&IdentifyUserDisplayCodeEvent::new(valid_user_uuid, ident.clone()),
);
assert!(matches!(
res,
Ok(IdentifyUserResponse::IdentityVerificationUnavailable)
));
let res = idms_prox_read.handle_identify_user_submit_code(
&IdentifyUserSubmitCodeEvent::new(valid_user_uuid, ident, 123456),
);
assert!(matches!(
res,
Ok(IdentifyUserResponse::IdentityVerificationUnavailable)
));
}
#[idm_test]
async fn test_invalid_user_id(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
let ct = duration_from_epoch_now();
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let invalid_user_uuid = Uuid::new_v4();
let valid_user_a_uuid = Uuid::new_v4();
let valid_user_b_uuid = Uuid::new_v4();
let e1 = create_invalid_user_account(invalid_user_uuid);
let e2 = create_valid_user_account(valid_user_a_uuid);
let e3 = create_valid_user_account(valid_user_b_uuid);
let ce = CreateEvent::new_internal(vec![e1, e2, e3]);
assert!(idms_prox_write.qs_write.create(&ce).is_ok());
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let ident = idms_prox_read
.qs_read
.internal_search_uuid(valid_user_a_uuid)
.map(Identity::from_impersonate_entry_readonly)
.expect("Failed to impersonate identity");
let res = idms_prox_read.handle_identify_user_start(&IdentifyUserStartEvent::new(
invalid_user_uuid,
ident.clone(),
));
assert!(matches!(res, Ok(IdentifyUserResponse::InvalidUserId)));
let res = idms_prox_read.handle_identify_user_start(&IdentifyUserStartEvent::new(
invalid_user_uuid,
ident.clone(),
));
assert!(matches!(res, Ok(IdentifyUserResponse::InvalidUserId)));
let res = idms_prox_read.handle_identify_user_display_code(
&IdentifyUserDisplayCodeEvent::new(invalid_user_uuid, ident.clone()),
);
assert!(matches!(res, Ok(IdentifyUserResponse::InvalidUserId)));
let res = idms_prox_read.handle_identify_user_submit_code(
&IdentifyUserSubmitCodeEvent::new(invalid_user_uuid, ident, 123456),
);
assert!(matches!(res, Ok(IdentifyUserResponse::InvalidUserId)));
}
#[idm_test]
async fn test_start_event(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
let ct = duration_from_epoch_now();
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let valid_user_a_uuid = Uuid::new_v4();
let e = create_valid_user_account(valid_user_a_uuid);
let ce = CreateEvent::new_internal(vec![e]);
assert!(idms_prox_write.qs_write.create(&ce).is_ok());
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let ident = idms_prox_read
.qs_read
.internal_search_uuid(valid_user_a_uuid)
.map(Identity::from_impersonate_entry_readonly)
.expect("Failed to impersonate identity");
let res = idms_prox_read.handle_identify_user_start(&IdentifyUserStartEvent::new(
valid_user_a_uuid,
ident.clone(),
));
assert!(matches!(
res,
Ok(IdentifyUserResponse::IdentityVerificationAvailable)
));
}
#[idm_test] async fn test_code_correctness(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
let ct = duration_from_epoch_now();
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let user_a_uuid = Uuid::new_v4();
let user_b_uuid = Uuid::new_v4();
let e1 = create_valid_user_account(user_a_uuid);
let e2 = create_valid_user_account(user_b_uuid);
let ce = CreateEvent::new_internal(vec![e1, e2]);
assert!(idms_prox_write.qs_write.create(&ce).is_ok());
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let ident_a = idms_prox_read
.qs_read
.internal_search_uuid(user_a_uuid)
.map(Identity::from_impersonate_entry_readonly)
.expect("Failed to impersonate identity");
let ident_b = idms_prox_read
.qs_read
.internal_search_uuid(user_b_uuid)
.map(Identity::from_impersonate_entry_readonly)
.expect("Failed to impersonate identity");
let (lower_user, lower_user_uuid, higher_user, higher_user_uuid) =
if user_a_uuid < user_b_uuid {
(ident_a, user_a_uuid, ident_b, user_b_uuid)
} else {
(ident_b, user_b_uuid, ident_a, user_a_uuid)
};
let res_higher_user = idms_prox_read.handle_identify_user_start(
&IdentifyUserStartEvent::new(lower_user_uuid, higher_user.clone()),
);
let Ok(IdentifyUserResponse::ProvideCode { totp, .. }) = res_higher_user else {
panic!();
};
let res_lower_user_wrong = idms_prox_read.handle_identify_user_submit_code(
&IdentifyUserSubmitCodeEvent::new(higher_user_uuid, lower_user.clone(), totp + 1),
);
assert!(matches!(
res_lower_user_wrong,
Ok(IdentifyUserResponse::CodeFailure)
));
let res_lower_user_correct = idms_prox_read.handle_identify_user_submit_code(
&IdentifyUserSubmitCodeEvent::new(higher_user_uuid, lower_user.clone(), totp),
);
assert!(matches!(
res_lower_user_correct,
Ok(IdentifyUserResponse::ProvideCode { .. })
));
let Ok(IdentifyUserResponse::ProvideCode { totp, .. }) = res_lower_user_correct else {
panic!("Invalid");
};
let res_higher_user_2_wrong = idms_prox_read.handle_identify_user_submit_code(
&IdentifyUserSubmitCodeEvent::new(lower_user_uuid, higher_user.clone(), totp + 1),
);
assert!(matches!(
res_higher_user_2_wrong,
Ok(IdentifyUserResponse::CodeFailure)
));
let res_higher_user_2_correct = idms_prox_read.handle_identify_user_submit_code(
&IdentifyUserSubmitCodeEvent::new(lower_user_uuid, higher_user.clone(), totp),
);
assert!(matches!(
res_higher_user_2_correct,
Ok(IdentifyUserResponse::Success)
));
}
#[idm_test]
async fn test_totps_differ(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
let ct = duration_from_epoch_now();
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let user_a_uuid = Uuid::new_v4();
let user_b_uuid = Uuid::new_v4();
let e1 = create_valid_user_account(user_a_uuid);
let e2 = create_valid_user_account(user_b_uuid);
let ce = CreateEvent::new_internal(vec![e1, e2]);
assert!(idms_prox_write.qs_write.create(&ce).is_ok());
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let ident_a = idms_prox_read
.qs_read
.internal_search_uuid(user_a_uuid)
.map(Identity::from_impersonate_entry_readonly)
.expect("Failed to impersonate identity");
let ident_b = idms_prox_read
.qs_read
.internal_search_uuid(user_b_uuid)
.map(Identity::from_impersonate_entry_readonly)
.expect("Failed to impersonate identity");
let (lower_user, lower_user_uuid, higher_user, higher_user_uuid) =
if user_a_uuid < user_b_uuid {
(ident_a, user_a_uuid, ident_b, user_b_uuid)
} else {
(ident_b, user_b_uuid, ident_a, user_a_uuid)
};
let res_higher_user = idms_prox_read.handle_identify_user_start(
&IdentifyUserStartEvent::new(lower_user_uuid, higher_user.clone()),
);
let Ok(IdentifyUserResponse::ProvideCode {
totp: higher_user_totp,
..
}) = res_higher_user
else {
panic!();
};
let res_lower_user_correct =
idms_prox_read.handle_identify_user_submit_code(&IdentifyUserSubmitCodeEvent::new(
higher_user_uuid,
lower_user.clone(),
higher_user_totp,
));
if let Ok(IdentifyUserResponse::ProvideCode {
totp: lower_user_totp,
..
}) = res_lower_user_correct
{
assert_ne!(higher_user_totp, lower_user_totp);
} else {
debug_assert!(false);
}
}
fn create_valid_user_account(uuid: Uuid) -> EntryInitNew {
let mut name = String::from("valid_user");
name.push_str(&uuid.to_string());
name.truncate(14);
entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname(&name)),
(Attribute::Uuid, Value::Uuid(uuid)),
(Attribute::Description, Value::new_utf8s("some valid user")),
(Attribute::DisplayName, Value::new_utf8s("Some valid user"))
)
}
fn create_invalid_user_account(uuid: Uuid) -> EntryInitNew {
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("invalid_user")),
(Attribute::Uuid, Value::Uuid(uuid)),
(Attribute::Description, Value::new_utf8s("invalid_user")),
(Attribute::DisplayName, Value::new_utf8s("Invalid user"))
)
}
}