#[cfg(feature = "std")]
use crate::error::Error;
use crate::error::Result;
use crate::types::{Credential, CredentialBackupState, CredentialRef};
pub use soft_fido2_ctap::authenticator::BuiltInUvState;
use soft_fido2_ctap::authenticator::{
Authenticator as CtapAuthenticator, AuthenticatorConfig as CtapConfig,
};
use soft_fido2_ctap::callbacks::{
CredentialStorageCallbacks, PinStorageCallbacks, UpResult as CtapUpResult,
UserInteractionCallbacks, UvResult as CtapUvResult,
};
use soft_fido2_ctap::cbor::MAX_CTAP_MESSAGE_SIZE;
use soft_fido2_ctap::key_provider::{CredentialKeyProvider, SoftwareCredentialKeyProvider};
use soft_fido2_ctap::types::{Credential as CtapCredential, PinState};
use soft_fido2_ctap::{CommandDispatcher, StatusCode};
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec;
use alloc::vec::Vec;
#[cfg(feature = "std")]
use std::sync::{Mutex, OnceLock};
#[cfg(not(feature = "std"))]
use spin::Mutex;
#[cfg(feature = "std")]
static PRESET_PIN_HASH: OnceLock<Mutex<Option<[u8; 32]>>> = OnceLock::new();
#[cfg(not(feature = "std"))]
static PRESET_PIN_HASH: Mutex<Option<[u8; 32]>> = Mutex::new(None);
struct NoOpPinStorage;
impl PinStorageCallbacks for NoOpPinStorage {
fn load_pin_state(&self) -> core::result::Result<PinState, StatusCode> {
Err(StatusCode::Other)
}
fn save_pin_state(&self, _state: &PinState) -> core::result::Result<(), StatusCode> {
Ok(())
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UpResult {
Denied,
Accepted,
Timeout,
}
impl From<UpResult> for CtapUpResult {
fn from(result: UpResult) -> Self {
match result {
UpResult::Denied => CtapUpResult::Denied,
UpResult::Accepted => CtapUpResult::Accepted,
UpResult::Timeout => CtapUpResult::Timeout,
}
}
}
impl From<CtapUpResult> for UpResult {
fn from(result: CtapUpResult) -> Self {
match result {
CtapUpResult::Denied => UpResult::Denied,
CtapUpResult::Accepted => UpResult::Accepted,
CtapUpResult::Timeout => UpResult::Timeout,
_ => UpResult::Denied,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UvResult {
Denied,
Accepted,
AcceptedWithUp,
Timeout,
}
impl From<UvResult> for CtapUvResult {
fn from(result: UvResult) -> Self {
match result {
UvResult::Denied => CtapUvResult::Denied,
UvResult::Accepted => CtapUvResult::Accepted,
UvResult::AcceptedWithUp => CtapUvResult::AcceptedWithUp,
UvResult::Timeout => CtapUvResult::Timeout,
}
}
}
impl From<CtapUvResult> for UvResult {
fn from(result: CtapUvResult) -> Self {
match result {
CtapUvResult::Denied => UvResult::Denied,
CtapUvResult::Accepted => UvResult::Accepted,
CtapUvResult::AcceptedWithUp => UvResult::AcceptedWithUp,
CtapUvResult::Timeout => UvResult::Timeout,
_ => UvResult::Denied,
}
}
}
pub trait AuthenticatorCallbacks: Send + Sync {
fn request_up(&self, info: &str, user_name: Option<&str>, rp_id: &str) -> Result<UpResult>;
fn request_uv(&self, info: &str, user_name: Option<&str>, rp_id: &str) -> Result<UvResult>;
fn write_credential(&self, credential: &CredentialRef) -> Result<()>;
fn read_credential(&self, cred_id: &[u8]) -> Result<Option<Credential>>;
fn delete_credential(&self, cred_id: &[u8]) -> Result<()>;
fn list_credentials(&self, rp_id: &str, user_id: Option<&[u8]>) -> Result<Vec<Credential>>;
fn select_credential(&self, _rp_id: &str, _credentials: &[Credential]) -> Result<usize> {
Ok(0)
}
fn enumerate_rps(&self) -> Result<Vec<(String, Option<String>, usize)>>;
fn credential_count(&self) -> Result<usize>;
fn get_timestamp_ms(&self) -> u64;
}
struct CallbackAdapter<C: AuthenticatorCallbacks> {
callbacks: Arc<C>,
}
impl<C: AuthenticatorCallbacks> soft_fido2_ctap::callbacks::PlatformCallbacks
for CallbackAdapter<C>
{
fn get_timestamp_ms(&self) -> u64 {
self.callbacks.get_timestamp_ms()
}
}
impl<C: AuthenticatorCallbacks> UserInteractionCallbacks for CallbackAdapter<C> {
fn request_up(
&self,
info: &str,
user_name: Option<&str>,
rp_id: &str,
) -> soft_fido2_ctap::Result<CtapUpResult> {
let result = self
.callbacks
.request_up(info, user_name, rp_id)
.map_err(|_| StatusCode::Other)?;
Ok(result.into())
}
fn request_uv(
&self,
info: &str,
user_name: Option<&str>,
rp_id: &str,
) -> soft_fido2_ctap::Result<CtapUvResult> {
let result = self
.callbacks
.request_uv(info, user_name, rp_id)
.map_err(|_| StatusCode::Other)?;
Ok(result.into())
}
fn select_credential(
&self,
rp_id: &str,
_user_names: &[String],
) -> soft_fido2_ctap::Result<usize> {
let credentials = self
.callbacks
.list_credentials(rp_id, None)
.map_err(|_| StatusCode::Other)?;
self.callbacks
.select_credential(rp_id, &credentials)
.map_err(|_| StatusCode::Other)
}
}
impl<C: AuthenticatorCallbacks> CredentialStorageCallbacks for CallbackAdapter<C> {
fn write_credential(&self, credential: &CtapCredential) -> soft_fido2_ctap::Result<()> {
let cred_ref = CredentialRef {
id: &credential.id,
rp_id: &credential.rp_id,
rp_name: credential.rp_name.as_deref(),
user_id: &credential.user_id,
user_name: credential.user_name.as_deref(),
user_display_name: credential.user_display_name.as_deref(),
sign_count: &credential.sign_count,
alg: &credential.algorithm,
key: &credential.key,
created: &credential.created,
discoverable: &credential.discoverable,
cred_protect: Some(&credential.cred_protect),
backup_state: &credential.backup_state,
cred_random: credential.cred_random.as_ref(),
};
self.callbacks
.write_credential(&cred_ref)
.map_err(|_| StatusCode::Other)
}
fn delete_credential(&self, credential_id: &[u8]) -> soft_fido2_ctap::Result<()> {
self.callbacks
.delete_credential(credential_id)
.map_err(|_| StatusCode::Other)
}
fn read_credentials(
&self,
rp_id: &str,
user_id: Option<&[u8]>,
) -> soft_fido2_ctap::Result<Vec<CtapCredential>> {
let credentials = self
.callbacks
.list_credentials(rp_id, user_id)
.map_err(|_| StatusCode::NoCredentials)?;
Ok(credentials.into_iter().map(|c| c.into()).collect())
}
fn credential_exists(&self, credential_id: &[u8]) -> soft_fido2_ctap::Result<bool> {
match self.callbacks.read_credential(credential_id) {
Ok(Some(_)) => Ok(true),
Ok(None) => Ok(false),
Err(_) => Ok(false),
}
}
fn get_credential(&self, credential_id: &[u8]) -> soft_fido2_ctap::Result<CtapCredential> {
let cred = self
.callbacks
.read_credential(credential_id)
.map_err(|_| StatusCode::NoCredentials)?
.ok_or(StatusCode::NoCredentials)?;
Ok(cred.into())
}
fn update_credential(&self, credential: &CtapCredential) -> soft_fido2_ctap::Result<()> {
self.write_credential(credential)
}
fn enumerate_rps(&self) -> soft_fido2_ctap::Result<Vec<(String, Option<String>, usize)>> {
self.callbacks
.enumerate_rps()
.map_err(|_| StatusCode::Other)
}
fn credential_count(&self) -> soft_fido2_ctap::Result<usize> {
self.callbacks
.credential_count()
.map_err(|_| StatusCode::Other)
}
}
#[derive(Debug, Clone)]
pub struct AuthenticatorConfig {
pub aaguid: [u8; 16],
pub commands: Vec<crate::ctap::CtapCommand>,
pub options: Option<crate::options::AuthenticatorOptions>,
pub max_credentials: usize,
pub extensions: Vec<String>,
pub force_resident_keys: bool,
pub firmware_version: Option<u32>,
pub constant_sign_count: bool,
pub default_credential_backup_state: CredentialBackupState,
pub max_msg_size: usize,
pub algorithms: Vec<i32>,
pub device_name: Option<String>,
pub vendor_id: Option<u16>,
pub product_id: Option<u16>,
pub device_version: Option<u16>,
pub max_pin_retries: u8,
pub auto_lock_timeout: u32,
}
impl Default for AuthenticatorConfig {
fn default() -> Self {
Self {
aaguid: [0u8; 16],
commands: crate::ctap::CtapCommand::default_commands(),
options: None,
max_credentials: 100,
extensions: vec![],
force_resident_keys: true,
firmware_version: None,
constant_sign_count: false,
default_credential_backup_state: CredentialBackupState::NotEligible,
max_msg_size: MAX_CTAP_MESSAGE_SIZE,
algorithms: vec![-7, -19], device_name: None,
vendor_id: None,
product_id: None,
device_version: None,
max_pin_retries: 8,
auto_lock_timeout: 0,
}
}
}
impl AuthenticatorConfig {
pub fn builder() -> AuthenticatorConfigBuilder {
AuthenticatorConfigBuilder::default()
}
}
pub struct AuthenticatorConfigBuilder {
aaguid: [u8; 16],
commands: Vec<crate::ctap::CtapCommand>,
options: Option<crate::options::AuthenticatorOptions>,
max_credentials: usize,
extensions: Vec<String>,
force_resident_keys: bool,
firmware_version: Option<u32>,
constant_sign_count: bool,
default_credential_backup_state: CredentialBackupState,
max_msg_size: usize,
algorithms: Vec<i32>,
device_name: Option<String>,
vendor_id: Option<u16>,
product_id: Option<u16>,
device_version: Option<u16>,
max_pin_retries: u8,
auto_lock_timeout: u32,
}
impl Default for AuthenticatorConfigBuilder {
fn default() -> Self {
Self {
aaguid: [0u8; 16],
commands: vec![],
options: None,
max_credentials: 0,
extensions: vec![],
force_resident_keys: true,
firmware_version: None,
default_credential_backup_state: CredentialBackupState::NotEligible,
constant_sign_count: false,
algorithms: vec![-7, -19], device_name: None,
vendor_id: None,
product_id: None,
device_version: None,
max_pin_retries: 0, auto_lock_timeout: 0,
max_msg_size: MAX_CTAP_MESSAGE_SIZE,
}
}
}
impl AuthenticatorConfigBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn aaguid(mut self, aaguid: [u8; 16]) -> Self {
self.aaguid = aaguid;
self
}
pub fn commands(mut self, commands: Vec<crate::ctap::CtapCommand>) -> Self {
self.commands = commands;
self
}
pub fn options(mut self, options: crate::options::AuthenticatorOptions) -> Self {
self.options = Some(options);
self
}
pub fn max_credentials(mut self, max: usize) -> Self {
self.max_credentials = max;
self
}
pub fn extensions(mut self, extensions: Vec<String>) -> Self {
self.extensions = extensions;
self
}
pub fn force_resident_keys(mut self, force: bool) -> Self {
self.force_resident_keys = force;
self
}
pub fn firmware_version(mut self, version: u32) -> Self {
self.firmware_version = Some(version);
self
}
pub fn constant_sign_count(mut self, constant: bool) -> Self {
self.constant_sign_count = constant;
self
}
pub fn default_credential_backup_state(mut self, state: CredentialBackupState) -> Self {
self.default_credential_backup_state = state;
self
}
pub fn max_msg_size(mut self, size: usize) -> Self {
self.max_msg_size = size;
self
}
pub fn device_name(mut self, name: String) -> Self {
self.device_name = Some(name);
self
}
pub fn vendor_id(mut self, id: u16) -> Self {
self.vendor_id = Some(id);
self
}
pub fn product_id(mut self, id: u16) -> Self {
self.product_id = Some(id);
self
}
pub fn device_version(mut self, version: u16) -> Self {
self.device_version = Some(version);
self
}
pub fn algorithms(mut self, algorithms: Vec<i32>) -> Self {
self.algorithms = algorithms;
self
}
pub fn max_pin_retries(mut self, retries: u8) -> Self {
self.max_pin_retries = retries;
self
}
pub fn auto_lock_timeout(mut self, timeout_seconds: u32) -> Self {
self.auto_lock_timeout = timeout_seconds;
self
}
pub fn build(self) -> AuthenticatorConfig {
AuthenticatorConfig {
aaguid: self.aaguid,
commands: if self.commands.is_empty() {
crate::ctap::CtapCommand::default_commands()
} else {
self.commands
},
options: self.options,
max_credentials: if self.max_credentials == 0 {
100
} else {
self.max_credentials
},
extensions: self.extensions,
force_resident_keys: self.force_resident_keys,
firmware_version: self.firmware_version,
constant_sign_count: self.constant_sign_count,
default_credential_backup_state: self.default_credential_backup_state,
max_msg_size: self.max_msg_size,
algorithms: if self.algorithms.is_empty() {
vec![-7, -8] } else {
self.algorithms
},
device_name: self.device_name,
vendor_id: self.vendor_id,
product_id: self.product_id,
device_version: self.device_version,
max_pin_retries: if self.max_pin_retries == 0 {
8
} else {
self.max_pin_retries
},
auto_lock_timeout: self.auto_lock_timeout,
}
}
}
pub struct Authenticator<
C: AuthenticatorCallbacks,
K: CredentialKeyProvider = SoftwareCredentialKeyProvider,
> {
dispatcher: Arc<Mutex<CommandDispatcher<CallbackAdapter<C>, K>>>,
}
impl<C: AuthenticatorCallbacks> Authenticator<C, SoftwareCredentialKeyProvider> {
pub fn new(callbacks: C) -> Result<Self>
where
C: 'static,
{
Self::with_config(callbacks, AuthenticatorConfig::default())
}
pub fn with_config(callbacks: C, config: AuthenticatorConfig) -> Result<Self>
where
C: 'static,
{
Self::with_config_internal(
callbacks,
config,
None::<NoOpPinStorage>,
SoftwareCredentialKeyProvider,
)
}
pub fn with_config_and_pin_storage<P>(
callbacks: C,
config: AuthenticatorConfig,
pin_storage: P,
) -> Result<Self>
where
C: 'static,
P: PinStorageCallbacks + Send + Sync + 'static,
{
Self::with_config_internal(
callbacks,
config,
Some(pin_storage),
SoftwareCredentialKeyProvider,
)
}
}
impl<C: AuthenticatorCallbacks, K: CredentialKeyProvider + Send + 'static> Authenticator<C, K> {
pub fn set_pin_hash(pin_hash: &[u8]) {
if pin_hash.len() == 32 {
let mut hash = [0u8; 32];
hash.copy_from_slice(pin_hash);
#[cfg(feature = "std")]
{
let lock = PRESET_PIN_HASH.get_or_init(|| Mutex::new(None));
if let Ok(mut guard) = lock.lock() {
*guard = Some(hash);
}
}
#[cfg(not(feature = "std"))]
{
*PRESET_PIN_HASH.lock() = Some(hash);
}
}
}
pub fn with_key_provider(callbacks: C, key_provider: K) -> Result<Self>
where
C: 'static,
{
Self::with_config_and_key_provider(callbacks, AuthenticatorConfig::default(), key_provider)
}
pub fn with_config_and_key_provider(
callbacks: C,
config: AuthenticatorConfig,
key_provider: K,
) -> Result<Self>
where
C: 'static,
{
Self::with_config_internal(callbacks, config, None::<NoOpPinStorage>, key_provider)
}
pub fn with_config_and_pin_storage_and_key_provider<P>(
callbacks: C,
config: AuthenticatorConfig,
pin_storage: P,
key_provider: K,
) -> Result<Self>
where
C: 'static,
P: PinStorageCallbacks + Send + Sync + 'static,
{
Self::with_config_internal(callbacks, config, Some(pin_storage), key_provider)
}
fn with_config_internal<P>(
callbacks: C,
config: AuthenticatorConfig,
pin_storage: Option<P>,
key_provider: K,
) -> Result<Self>
where
C: 'static,
P: PinStorageCallbacks + Send + Sync + 'static,
{
let adapter = CallbackAdapter {
callbacks: Arc::new(callbacks),
};
let mut ctap_config = CtapConfig::new()
.with_aaguid(config.aaguid)
.with_max_credentials(config.max_credentials)
.with_extensions(config.extensions)
.with_force_resident_keys(config.force_resident_keys)
.with_constant_sign_count(config.constant_sign_count)
.with_default_credential_backup_state(config.default_credential_backup_state)
.with_max_msg_size(config.max_msg_size)
.with_algorithms(config.algorithms)
.with_max_pin_retries(config.max_pin_retries)
.with_auto_lock_timeout(config.auto_lock_timeout);
if let Some(fw_version) = config.firmware_version {
ctap_config = ctap_config.with_firmware_version(fw_version);
}
if let Some(ref hl_options) = config.options {
let ctap_options = soft_fido2_ctap::authenticator::AuthenticatorOptions {
plat: hl_options.plat,
rk: hl_options.rk,
client_pin: hl_options.client_pin,
up: hl_options.up,
uv: hl_options.uv,
always_uv: hl_options.always_uv.unwrap_or(false),
cred_mgmt: hl_options.cred_mgmt.unwrap_or(true),
authnr_cfg: false,
bio_enroll: hl_options.bio_enroll,
ep: hl_options.ep,
large_blobs: hl_options.large_blobs,
pin_uv_auth_token: hl_options.pin_uv_auth_token.unwrap_or(true),
set_min_pin_length: false,
make_cred_uv_not_rqd: hl_options.make_cred_uv_not_required.unwrap_or(false),
};
ctap_config = ctap_config.with_options(ctap_options);
}
let authenticator =
CtapAuthenticator::new_with_key_provider(ctap_config, adapter, key_provider);
let authenticator = if let Some(storage) = pin_storage {
authenticator.with_pin_storage(storage)
} else {
authenticator
};
#[cfg(feature = "std")]
let mut authenticator = authenticator;
#[cfg(feature = "std")]
if let Some(lock) = PRESET_PIN_HASH.get()
&& let Ok(mut guard) = lock.lock()
&& let Some(pin_hash) = guard.take()
{
authenticator.set_pin_hash_for_testing(pin_hash);
}
#[cfg(not(feature = "std"))]
let mut authenticator = authenticator;
#[cfg(not(feature = "std"))]
{
let mut guard = PRESET_PIN_HASH.lock();
if let Some(pin_hash) = guard.take() {
authenticator.set_pin_hash_for_testing(pin_hash);
}
}
let dispatcher = CommandDispatcher::new(authenticator);
Ok(Self {
dispatcher: Arc::new(Mutex::new(dispatcher)),
})
}
pub fn handle(&mut self, request: &[u8], response: &mut Vec<u8>) -> Result<usize> {
#[cfg(feature = "std")]
let mut dispatcher = self.dispatcher.lock().map_err(|_| Error::Other)?;
#[cfg(not(feature = "std"))]
let mut dispatcher = self.dispatcher.lock();
match dispatcher.dispatch(request) {
Ok(response_data) => {
response.clear();
response.push(0x00);
response.extend_from_slice(&response_data);
Ok(response.len())
}
Err(status_code) => {
*response = vec![status_code as u8];
Ok(1)
}
}
}
pub fn register_custom_command<F>(&mut self, command: u8, handler: F)
where
F: Fn(&[u8]) -> core::result::Result<Vec<u8>, StatusCode> + Send + Sync + 'static,
{
#[cfg(feature = "std")]
let mut dispatcher = self
.dispatcher
.lock()
.expect("Failed to lock dispatcher for custom command registration");
#[cfg(not(feature = "std"))]
let mut dispatcher = self.dispatcher.lock();
dispatcher
.authenticator_mut()
.register_custom_command(command, handler);
}
pub fn uv_retries(&self) -> Result<u8> {
#[cfg(feature = "std")]
let dispatcher = self.dispatcher.lock().map_err(|_| Error::Other)?;
#[cfg(not(feature = "std"))]
let dispatcher = self.dispatcher.lock();
Ok(dispatcher.authenticator().uv_retries())
}
pub fn built_in_uv_state(&self) -> Result<BuiltInUvState> {
#[cfg(feature = "std")]
let dispatcher = self.dispatcher.lock().map_err(|_| Error::Other)?;
#[cfg(not(feature = "std"))]
let dispatcher = self.dispatcher.lock();
Ok(dispatcher.authenticator().built_in_uv_state())
}
pub fn set_built_in_uv_state(&mut self, state: BuiltInUvState) -> Result<()> {
#[cfg(feature = "std")]
let mut dispatcher = self.dispatcher.lock().map_err(|_| Error::Other)?;
#[cfg(not(feature = "std"))]
let mut dispatcher = self.dispatcher.lock();
dispatcher
.authenticator_mut()
.set_built_in_uv_state(state)
.map_err(Into::into)
}
pub fn set_built_in_uv_configured(&mut self, configured: bool) -> Result<()> {
let state = if configured {
BuiltInUvState::Configured
} else {
BuiltInUvState::SupportedNotConfigured
};
self.set_built_in_uv_state(state)
}
pub fn reset_uv_retries(&mut self) -> Result<()> {
#[cfg(feature = "std")]
let mut dispatcher = self.dispatcher.lock().map_err(|_| Error::Other)?;
#[cfg(not(feature = "std"))]
let mut dispatcher = self.dispatcher.lock();
dispatcher.authenticator_mut().reset_uv_retries();
Ok(())
}
pub fn verify_credential_management_pin_uv_auth(
&mut self,
pin_uv_auth_protocol: u8,
pin_uv_auth_param: &[u8],
auth_data: &[u8],
) -> Result<()> {
#[cfg(feature = "std")]
let mut dispatcher = self.dispatcher.lock().map_err(|_| Error::Other)?;
#[cfg(not(feature = "std"))]
let mut dispatcher = self.dispatcher.lock();
dispatcher.authenticator_mut().verify_pin_uv_auth_param(
pin_uv_auth_protocol,
pin_uv_auth_param,
auth_data,
)?;
dispatcher.authenticator_mut().verify_pin_uv_auth_token(
soft_fido2_ctap::pin_token::Permission::CredentialManagement,
None,
)?;
Ok(())
}
#[cfg(test)]
fn decrement_uv_retries_for_testing(&mut self) -> Result<()> {
#[cfg(feature = "std")]
let mut dispatcher = self.dispatcher.lock().map_err(|_| Error::Other)?;
#[cfg(not(feature = "std"))]
let mut dispatcher = self.dispatcher.lock();
dispatcher.authenticator_mut().decrement_uv_retries();
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use soft_fido2_ctap::SecBytes;
use soft_fido2_ctap::key_provider::{
CredentialKey, CredentialKeyError, CredentialKeyProviderId, GeneratedCredentialKey,
};
#[cfg(feature = "std")]
use std::sync::Mutex as StdMutex;
#[cfg(not(feature = "std"))]
use spin::Mutex as SpinMutex;
#[cfg(feature = "std")]
type TestMutex<T> = StdMutex<T>;
#[cfg(not(feature = "std"))]
type TestMutex<T> = SpinMutex<T>;
struct TestCallbacks;
impl AuthenticatorCallbacks for TestCallbacks {
fn request_up(&self, _: &str, _: Option<&str>, _: &str) -> Result<UpResult> {
Ok(UpResult::Accepted)
}
fn request_uv(&self, _: &str, _: Option<&str>, _: &str) -> Result<UvResult> {
Ok(UvResult::Accepted)
}
fn write_credential(&self, _: &CredentialRef) -> Result<()> {
Ok(())
}
fn read_credential(&self, _: &[u8]) -> Result<Option<Credential>> {
Ok(None)
}
fn delete_credential(&self, _: &[u8]) -> Result<()> {
Ok(())
}
fn list_credentials(&self, _: &str, _: Option<&[u8]>) -> Result<Vec<Credential>> {
Ok(vec![])
}
fn enumerate_rps(&self) -> Result<Vec<(String, Option<String>, usize)>> {
Ok(vec![])
}
fn credential_count(&self) -> Result<usize> {
Ok(0)
}
fn get_timestamp_ms(&self) -> u64 {
0
}
}
struct MockKeyProvider {
generate_called: TestMutex<bool>,
sign_called: TestMutex<bool>,
}
impl MockKeyProvider {
fn new() -> Self {
Self {
generate_called: TestMutex::new(false),
sign_called: TestMutex::new(false),
}
}
}
impl CredentialKeyProvider for MockKeyProvider {
fn provider_id(&self) -> CredentialKeyProviderId {
CredentialKeyProviderId::new(b"mock-high-level-v1")
}
fn supports_algorithm(&self, algorithm: i32) -> bool {
algorithm == -7
}
fn generate(
&self,
algorithm: i32,
) -> core::result::Result<GeneratedCredentialKey, CredentialKeyError> {
if algorithm != -7 {
return Err(CredentialKeyError::UnsupportedAlgorithm);
}
#[cfg(feature = "std")]
{
*self.generate_called.lock().unwrap() = true;
}
#[cfg(not(feature = "std"))]
{
*self.generate_called.lock() = true;
}
let (sk, pk) = soft_fido2_crypto::ecdsa::generate_keypair();
let key = CredentialKey::new(self.provider_id(), 1, SecBytes::from_slice(&sk[..]));
Ok(GeneratedCredentialKey {
key,
cose_public_key: pk,
})
}
fn sign(
&self,
key: &CredentialKey,
algorithm: i32,
message: &[u8],
) -> core::result::Result<Vec<u8>, CredentialKeyError> {
if key.provider != self.provider_id() {
return Err(CredentialKeyError::UnsupportedProvider);
}
#[cfg(feature = "std")]
{
*self.sign_called.lock().unwrap() = true;
}
#[cfg(not(feature = "std"))]
{
*self.sign_called.lock() = true;
}
let key_bytes = key.material.as_slice();
if key_bytes.len() != 32 {
return Err(CredentialKeyError::InvalidKeyMaterial);
}
let mut arr = [0u8; 32];
arr.copy_from_slice(key_bytes);
let priv_key = zeroize::Zeroizing::new(arr);
match algorithm {
-7 => soft_fido2_crypto::ecdsa::sign(&priv_key, message)
.map_err(|e| CredentialKeyError::TransientFailure(alloc::format!("{:?}", e))),
_ => Err(CredentialKeyError::UnsupportedAlgorithm),
}
}
}
#[test]
fn test_authenticator_creation() {
let callbacks = TestCallbacks;
let config = AuthenticatorConfig::default();
let result = Authenticator::with_config(callbacks, config);
assert!(result.is_ok());
}
#[test]
fn test_config_builder() {
let config = AuthenticatorConfig::builder()
.aaguid([1u8; 16])
.max_credentials(50)
.build();
assert_eq!(config.aaguid, [1u8; 16]);
assert_eq!(config.max_credentials, 50);
}
#[test]
fn test_built_in_uv_state_machine_is_exposed() {
let options = crate::options::AuthenticatorOptions {
uv: Some(true),
..Default::default()
};
let config = AuthenticatorConfig::builder().options(options).build();
let mut auth = Authenticator::with_config(TestCallbacks, config).unwrap();
assert_eq!(
auth.built_in_uv_state().unwrap(),
BuiltInUvState::Configured
);
auth.set_built_in_uv_state(BuiltInUvState::SupportedNotConfigured)
.unwrap();
assert_eq!(
auth.built_in_uv_state().unwrap(),
BuiltInUvState::SupportedNotConfigured
);
auth.set_built_in_uv_configured(true).unwrap();
assert_eq!(
auth.built_in_uv_state().unwrap(),
BuiltInUvState::Configured
);
}
#[test]
fn test_reset_uv_retries_restores_exhausted_counter() {
let callbacks = TestCallbacks;
let mut auth = Authenticator::new(callbacks).unwrap();
assert_eq!(auth.uv_retries().unwrap(), 8);
auth.decrement_uv_retries_for_testing().unwrap();
auth.decrement_uv_retries_for_testing().unwrap();
auth.decrement_uv_retries_for_testing().unwrap();
auth.decrement_uv_retries_for_testing().unwrap();
auth.decrement_uv_retries_for_testing().unwrap();
auth.decrement_uv_retries_for_testing().unwrap();
auth.decrement_uv_retries_for_testing().unwrap();
auth.decrement_uv_retries_for_testing().unwrap();
assert_eq!(auth.uv_retries().unwrap(), 0);
auth.reset_uv_retries().unwrap();
assert_eq!(auth.uv_retries().unwrap(), 8);
}
#[test]
fn test_credential_management_pin_uv_auth_rejects_invalid_param() {
let callbacks = TestCallbacks;
let mut auth = Authenticator::new(callbacks).unwrap();
let result = auth.verify_credential_management_pin_uv_auth(1, &[0u8; 16], &[0u8; 32]);
assert!(result.is_err());
}
#[test]
fn test_high_level_authenticator_with_key_provider() {
use soft_fido2_ctap::cbor;
let provider = MockKeyProvider::new();
let config = AuthenticatorConfig::builder().aaguid([1u8; 16]).build();
let mut auth =
Authenticator::with_config_and_key_provider(TestCallbacks, config, provider).unwrap();
let client_data_hash = [0xAAu8; 32];
let rp = cbor::Value::Map(vec![
(
cbor::Value::Text("id".to_string()),
cbor::Value::Text("example.com".to_string()),
),
(
cbor::Value::Text("name".to_string()),
cbor::Value::Text("Example".to_string()),
),
]);
let user = cbor::Value::Map(vec![
(
cbor::Value::Text("id".to_string()),
cbor::Value::Bytes(vec![0x01, 0x02, 0x03]),
),
(
cbor::Value::Text("name".to_string()),
cbor::Value::Text("testuser".to_string()),
),
(
cbor::Value::Text("displayName".to_string()),
cbor::Value::Text("Test User".to_string()),
),
]);
let pub_key_cred_params = cbor::Value::Array(vec![cbor::Value::Map(vec![
(
cbor::Value::Text("type".to_string()),
cbor::Value::Text("public-key".to_string()),
),
(
cbor::Value::Text("alg".to_string()),
cbor::Value::Integer(-7),
),
])]);
let request_map = cbor::Value::Map(vec![
(
cbor::Value::Integer(1),
cbor::Value::Bytes(client_data_hash.to_vec()),
),
(cbor::Value::Integer(2), rp),
(cbor::Value::Integer(3), user),
(cbor::Value::Integer(4), pub_key_cred_params),
]);
let mut request_bytes = vec![0x01];
cbor::into_writer(&request_map, &mut request_bytes).unwrap();
let mut response = Vec::new();
let result = auth.handle(&request_bytes, &mut response);
assert!(result.is_ok());
assert_eq!(response[0], 0x00);
}
#[test]
fn test_default_authenticator_still_works() {
let callbacks = TestCallbacks;
let config = AuthenticatorConfig::default();
let auth = Authenticator::with_config(callbacks, config);
assert!(auth.is_ok());
}
}