adds the command logic for credential management

This commit is contained in:
Fabian Kaczmarczyck
2021-01-13 11:22:41 +01:00
parent 46b9a0262c
commit c6726660ac
7 changed files with 1366 additions and 42 deletions

View File

@@ -14,10 +14,10 @@
use super::data_formats::{ use super::data_formats::{
extract_array, extract_bool, extract_byte_string, extract_map, extract_text_string, extract_array, extract_bool, extract_byte_string, extract_map, extract_text_string,
extract_unsigned, ok_or_missing, ClientPinSubCommand, CoseKey, GetAssertionExtensions, extract_unsigned, ok_or_missing, ClientPinSubCommand, CoseKey, CredentialManagementSubCommand,
GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions, CredentialManagementSubCommandParameters, GetAssertionExtensions, GetAssertionOptions,
PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, MakeCredentialExtensions, MakeCredentialOptions, PublicKeyCredentialDescriptor,
PublicKeyCredentialUserEntity, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity,
}; };
use super::key_material; use super::key_material;
use super::status_code::Ctap2StatusCode; use super::status_code::Ctap2StatusCode;
@@ -41,6 +41,7 @@ pub enum Command {
AuthenticatorClientPin(AuthenticatorClientPinParameters), AuthenticatorClientPin(AuthenticatorClientPinParameters),
AuthenticatorReset, AuthenticatorReset,
AuthenticatorGetNextAssertion, AuthenticatorGetNextAssertion,
AuthenticatorCredentialManagement(AuthenticatorCredentialManagementParameters),
AuthenticatorSelection, AuthenticatorSelection,
// TODO(kaczmarczyck) implement FIDO 2.1 commands (see below consts) // TODO(kaczmarczyck) implement FIDO 2.1 commands (see below consts)
// Vendor specific commands // Vendor specific commands
@@ -110,6 +111,12 @@ impl Command {
// Parameters are ignored. // Parameters are ignored.
Ok(Command::AuthenticatorGetNextAssertion) Ok(Command::AuthenticatorGetNextAssertion)
} }
Command::AUTHENTICATOR_CREDENTIAL_MANAGEMENT => {
let decoded_cbor = cbor::read(&bytes[1..])?;
Ok(Command::AuthenticatorCredentialManagement(
AuthenticatorCredentialManagementParameters::try_from(decoded_cbor)?,
))
}
Command::AUTHENTICATOR_SELECTION => { Command::AUTHENTICATOR_SELECTION => {
// Parameters are ignored. // Parameters are ignored.
Ok(Command::AuthenticatorSelection) Ok(Command::AuthenticatorSelection)
@@ -388,6 +395,43 @@ impl TryFrom<cbor::Value> for AuthenticatorAttestationMaterial {
} }
} }
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct AuthenticatorCredentialManagementParameters {
pub sub_command: CredentialManagementSubCommand,
pub sub_command_params: Option<CredentialManagementSubCommandParameters>,
pub pin_protocol: Option<u64>,
pub pin_auth: Option<Vec<u8>>,
}
impl TryFrom<cbor::Value> for AuthenticatorCredentialManagementParameters {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
destructure_cbor_map! {
let {
0x01 => sub_command,
0x02 => sub_command_params,
0x03 => pin_protocol,
0x04 => pin_auth,
} = extract_map(cbor_value)?;
}
let sub_command = CredentialManagementSubCommand::try_from(ok_or_missing(sub_command)?)?;
let sub_command_params = sub_command_params
.map(CredentialManagementSubCommandParameters::try_from)
.transpose()?;
let pin_protocol = pin_protocol.map(extract_unsigned).transpose()?;
let pin_auth = pin_auth.map(extract_byte_string).transpose()?;
Ok(AuthenticatorCredentialManagementParameters {
sub_command,
sub_command_params,
pin_protocol,
pin_auth,
})
}
}
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct AuthenticatorVendorConfigureParameters { pub struct AuthenticatorVendorConfigureParameters {
pub lockdown: bool, pub lockdown: bool,
@@ -551,10 +595,10 @@ mod test {
9 => 0x03, 9 => 0x03,
10 => "example.com", 10 => "example.com",
}; };
let returned_pin_protocol_parameters = let returned_client_pin_parameters =
AuthenticatorClientPinParameters::try_from(cbor_value).unwrap(); AuthenticatorClientPinParameters::try_from(cbor_value).unwrap();
let expected_pin_protocol_parameters = AuthenticatorClientPinParameters { let expected_client_pin_parameters = AuthenticatorClientPinParameters {
pin_protocol: 1, pin_protocol: 1,
sub_command: ClientPinSubCommand::GetPinRetries, sub_command: ClientPinSubCommand::GetPinRetries,
key_agreement: Some(cose_key), key_agreement: Some(cose_key),
@@ -568,8 +612,8 @@ mod test {
}; };
assert_eq!( assert_eq!(
returned_pin_protocol_parameters, returned_client_pin_parameters,
expected_pin_protocol_parameters expected_client_pin_parameters
); );
} }
@@ -595,6 +639,37 @@ mod test {
assert_eq!(command, Ok(Command::AuthenticatorGetNextAssertion)); assert_eq!(command, Ok(Command::AuthenticatorGetNextAssertion));
} }
#[test]
fn test_from_cbor_cred_management_parameters() {
let cbor_value = cbor_map! {
1 => CredentialManagementSubCommand::EnumerateCredentialsBegin as u64,
2 => cbor_map!{
0x01 => vec![0x1D; 32],
},
3 => 1,
4 => vec! [0x9A; 16],
};
let returned_cred_management_parameters =
AuthenticatorCredentialManagementParameters::try_from(cbor_value).unwrap();
let params = CredentialManagementSubCommandParameters {
rp_id_hash: Some(vec![0x1D; 32]),
credential_id: None,
user: None,
};
let expected_cred_management_parameters = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateCredentialsBegin,
sub_command_params: Some(params),
pin_protocol: Some(1),
pin_auth: Some(vec![0x9A; 16]),
};
assert_eq!(
returned_cred_management_parameters,
expected_cred_management_parameters
);
}
#[test] #[test]
fn test_deserialize_selection() { fn test_deserialize_selection() {
let cbor_bytes = [Command::AUTHENTICATOR_SELECTION]; let cbor_bytes = [Command::AUTHENTICATOR_SELECTION];

View File

@@ -0,0 +1,912 @@
// Copyright 2020-2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::command::AuthenticatorCredentialManagementParameters;
use super::data_formats::{
CoseKey, CredentialManagementSubCommand, CredentialManagementSubCommandParameters,
PublicKeyCredentialDescriptor, PublicKeyCredentialRpEntity, PublicKeyCredentialSource,
PublicKeyCredentialUserEntity,
};
use super::pin_protocol_v1::{PinPermission, PinProtocolV1};
use super::response::{AuthenticatorCredentialManagementResponse, ResponseData};
use super::status_code::Ctap2StatusCode;
use super::storage::PersistentStore;
use super::timed_permission::TimedPermission;
use super::{check_command_permission, StatefulCommand, STATEFUL_COMMAND_TIMEOUT_DURATION};
use alloc::collections::BTreeSet;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use core::iter::FromIterator;
use crypto::sha256::Sha256;
use crypto::Hash256;
use libtock_drivers::timer::ClockValue;
/// Generates the response for subcommands enumerating RPs.
fn enumerate_rps_response(
rp_id: Option<String>,
total_rps: Option<u64>,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
let rp = rp_id.clone().map(|rp_id| PublicKeyCredentialRpEntity {
rp_id,
rp_name: None,
rp_icon: None,
});
let rp_id_hash = rp_id.map(|rp_id| Sha256::hash(rp_id.as_bytes()).to_vec());
Ok(AuthenticatorCredentialManagementResponse {
existing_resident_credentials_count: None,
max_possible_remaining_resident_credentials_count: None,
rp,
rp_id_hash,
total_rps,
user: None,
credential_id: None,
public_key: None,
total_credentials: None,
cred_protect: None,
large_blob_key: None,
})
}
/// Generates the response for subcommands enumerating credentials.
fn enumerate_credentials_response(
credential: PublicKeyCredentialSource,
total_credentials: Option<u64>,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
let PublicKeyCredentialSource {
key_type,
credential_id,
private_key,
rp_id: _,
user_handle,
user_display_name,
cred_protect_policy,
creation_order: _,
user_name,
user_icon,
} = credential;
let user = PublicKeyCredentialUserEntity {
user_id: user_handle,
user_name,
user_display_name,
user_icon,
};
let credential_id = PublicKeyCredentialDescriptor {
key_type,
key_id: credential_id,
transports: None, // You can set USB as a hint here.
};
let public_key = CoseKey::from(private_key.genpk());
Ok(AuthenticatorCredentialManagementResponse {
existing_resident_credentials_count: None,
max_possible_remaining_resident_credentials_count: None,
rp: None,
rp_id_hash: None,
total_rps: None,
user: Some(user),
credential_id: Some(credential_id),
public_key: Some(public_key),
total_credentials,
cred_protect: cred_protect_policy,
// TODO(kaczmarczyck) add when largeBlobKey is implemented
large_blob_key: None,
})
}
/// Processes the subcommand getCredsMetadata for CredentialManagement.
fn process_get_creds_metadata(
persistent_store: &PersistentStore,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
Ok(AuthenticatorCredentialManagementResponse {
existing_resident_credentials_count: Some(persistent_store.count_credentials()? as u64),
max_possible_remaining_resident_credentials_count: Some(
persistent_store.remaining_credentials()? as u64,
),
rp: None,
rp_id_hash: None,
total_rps: None,
user: None,
credential_id: None,
public_key: None,
total_credentials: None,
cred_protect: None,
large_blob_key: None,
})
}
/// Processes the subcommand enumerateRPsBegin for CredentialManagement.
fn process_enumerate_rps_begin(
persistent_store: &PersistentStore,
stateful_command_permission: &mut TimedPermission,
stateful_command_type: &mut Option<StatefulCommand>,
now: ClockValue,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
let mut rp_set = BTreeSet::new();
let mut iter_result = Ok(());
for (_, credential) in persistent_store.iter_credentials(&mut iter_result)? {
rp_set.insert(credential.rp_id);
}
iter_result?;
let mut rp_ids = Vec::from_iter(rp_set);
let total_rps = rp_ids.len();
// TODO(kaczmarczyck) behaviour with empty list?
let rp_id = rp_ids.pop();
if total_rps > 1 {
*stateful_command_permission =
TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION);
*stateful_command_type = Some(StatefulCommand::EnumerateRps(rp_ids));
}
enumerate_rps_response(rp_id, Some(total_rps as u64))
}
/// Processes the subcommand enumerateRPsGetNextRP for CredentialManagement.
fn process_enumerate_rps_get_next_rp(
stateful_command_permission: &mut TimedPermission,
stateful_command_type: &mut Option<StatefulCommand>,
now: ClockValue,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
check_command_permission(stateful_command_permission, now)?;
if let Some(StatefulCommand::EnumerateRps(rp_ids)) = stateful_command_type {
let rp_id = rp_ids.pop().ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?;
enumerate_rps_response(Some(rp_id), None)
} else {
Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)
}
}
/// Processes the subcommand enumerateCredentialsBegin for CredentialManagement.
fn process_enumerate_credentials_begin(
persistent_store: &PersistentStore,
stateful_command_permission: &mut TimedPermission,
stateful_command_type: &mut Option<StatefulCommand>,
sub_command_params: CredentialManagementSubCommandParameters,
now: ClockValue,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
let rp_id_hash = sub_command_params
.rp_id_hash
.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?;
let mut iter_result = Ok(());
let iter = persistent_store.iter_credentials(&mut iter_result)?;
let mut rp_credentials: Vec<usize> = iter
.filter_map(|(key, credential)| {
let cred_rp_id_hash = Sha256::hash(credential.rp_id.as_bytes());
if cred_rp_id_hash == rp_id_hash.as_slice() {
Some(key)
} else {
None
}
})
.collect();
iter_result?;
let total_credentials = rp_credentials.len();
let current_key = rp_credentials
.pop()
.ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?;
let credential = persistent_store.get_credential(current_key)?;
if total_credentials > 1 {
*stateful_command_permission =
TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION);
*stateful_command_type = Some(StatefulCommand::EnumerateCredentials(rp_credentials));
}
enumerate_credentials_response(credential, Some(total_credentials as u64))
}
/// Processes the subcommand enumerateCredentialsGetNextCredential for CredentialManagement.
fn process_enumerate_credentials_get_next_credential(
persistent_store: &PersistentStore,
stateful_command_permission: &mut TimedPermission,
mut stateful_command_type: &mut Option<StatefulCommand>,
now: ClockValue,
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
check_command_permission(stateful_command_permission, now)?;
if let Some(StatefulCommand::EnumerateCredentials(rp_credentials)) = &mut stateful_command_type
{
let current_key = rp_credentials
.pop()
.ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?;
let credential = persistent_store.get_credential(current_key)?;
enumerate_credentials_response(credential, None)
} else {
Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)
}
}
/// Processes the subcommand deleteCredential for CredentialManagement.
fn process_delete_credential(
persistent_store: &mut PersistentStore,
sub_command_params: CredentialManagementSubCommandParameters,
) -> Result<(), Ctap2StatusCode> {
let credential_id = sub_command_params
.credential_id
.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?
.key_id;
persistent_store.delete_credential(&credential_id)
}
/// Processes the subcommand updateUserInformation for CredentialManagement.
fn process_update_user_information(
persistent_store: &mut PersistentStore,
sub_command_params: CredentialManagementSubCommandParameters,
) -> Result<(), Ctap2StatusCode> {
let credential_id = sub_command_params
.credential_id
.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?
.key_id;
let user = sub_command_params
.user
.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?;
persistent_store.update_credential(&credential_id, user)
}
/// Checks the PIN protocol.
///
/// TODO(#246) refactor after #246 is merged
fn pin_uv_auth_protocol_check(pin_uv_auth_protocol: Option<u64>) -> Result<(), Ctap2StatusCode> {
match pin_uv_auth_protocol {
Some(1) => Ok(()),
_ => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID),
}
}
/// Processes the CredentialManagement command and all its subcommands.
pub fn process_credential_management(
persistent_store: &mut PersistentStore,
stateful_command_permission: &mut TimedPermission,
mut stateful_command_type: &mut Option<StatefulCommand>,
pin_protocol_v1: &mut PinProtocolV1,
cred_management_params: AuthenticatorCredentialManagementParameters,
now: ClockValue,
) -> Result<ResponseData, Ctap2StatusCode> {
let AuthenticatorCredentialManagementParameters {
sub_command,
sub_command_params,
pin_protocol,
pin_auth,
} = cred_management_params;
match (sub_command, &mut stateful_command_type) {
(
CredentialManagementSubCommand::EnumerateRpsGetNextRp,
Some(StatefulCommand::EnumerateRps(_)),
) => (),
(
CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential,
Some(StatefulCommand::EnumerateCredentials(_)),
) => (),
(_, _) => {
*stateful_command_type = None;
}
}
match sub_command {
CredentialManagementSubCommand::GetCredsMetadata
| CredentialManagementSubCommand::EnumerateRpsBegin
| CredentialManagementSubCommand::DeleteCredential
| CredentialManagementSubCommand::EnumerateCredentialsBegin
| CredentialManagementSubCommand::UpdateUserInformation => {
pin_uv_auth_protocol_check(pin_protocol)?;
persistent_store
.pin_hash()?
.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?;
let pin_auth = pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?;
let mut management_data = vec![sub_command as u8];
if let Some(sub_command_params) = sub_command_params.clone() {
if !cbor::write(sub_command_params.into(), &mut management_data) {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
}
}
if !pin_protocol_v1.verify_pin_auth_token(&management_data, &pin_auth) {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID);
}
pin_protocol_v1.has_permission(PinPermission::CredentialManagement)?;
pin_protocol_v1.has_no_permission_rp_id()?;
// TODO(kaczmarczyck) sometimes allow a RP ID
}
CredentialManagementSubCommand::EnumerateRpsGetNextRp
| CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential => {}
}
let response = match sub_command {
CredentialManagementSubCommand::GetCredsMetadata => {
Some(process_get_creds_metadata(persistent_store)?)
}
CredentialManagementSubCommand::EnumerateRpsBegin => Some(process_enumerate_rps_begin(
persistent_store,
stateful_command_permission,
stateful_command_type,
now,
)?),
CredentialManagementSubCommand::EnumerateRpsGetNextRp => {
Some(process_enumerate_rps_get_next_rp(
stateful_command_permission,
stateful_command_type,
now,
)?)
}
CredentialManagementSubCommand::EnumerateCredentialsBegin => {
Some(process_enumerate_credentials_begin(
persistent_store,
stateful_command_permission,
stateful_command_type,
sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
now,
)?)
}
CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential => {
Some(process_enumerate_credentials_get_next_credential(
persistent_store,
stateful_command_permission,
stateful_command_type,
now,
)?)
}
CredentialManagementSubCommand::DeleteCredential => {
process_delete_credential(
persistent_store,
sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
)?;
None
}
CredentialManagementSubCommand::UpdateUserInformation => {
process_update_user_information(
persistent_store,
sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
)?;
None
}
};
Ok(ResponseData::AuthenticatorCredentialManagement(response))
}
#[cfg(test)]
mod test {
use super::super::data_formats::PublicKeyCredentialType;
use super::super::CtapState;
use super::*;
use crypto::rng256::{Rng256, ThreadRng256};
const CLOCK_FREQUENCY_HZ: usize = 32768;
const DUMMY_CLOCK_VALUE: ClockValue = ClockValue::new(0, CLOCK_FREQUENCY_HZ);
fn create_credential_source(rng: &mut impl Rng256) -> PublicKeyCredentialSource {
let private_key = crypto::ecdsa::SecKey::gensk(rng);
PublicKeyCredentialSource {
key_type: PublicKeyCredentialType::PublicKey,
credential_id: rng.gen_uniform_u8x32().to_vec(),
private_key,
rp_id: String::from("example.com"),
user_handle: vec![0x01],
user_display_name: Some("display_name".to_string()),
cred_protect_policy: None,
creation_order: 0,
user_name: Some("name".to_string()),
user_icon: Some("icon".to_string()),
}
}
#[test]
fn test_process_get_creds_metadata() {
let mut rng = ThreadRng256 {};
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
let pin_uv_auth_token = [0x55; 32];
let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token);
let credential_source = create_credential_source(&mut rng);
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
ctap_state.pin_protocol_v1 = pin_protocol_v1;
ctap_state
.persistent_store
.set_pin_hash(&[0u8; 16])
.unwrap();
let pin_auth = Some(vec![
0xC5, 0xFB, 0x75, 0x55, 0x98, 0xB5, 0x19, 0x01, 0xB3, 0x31, 0x7D, 0xFE, 0x1D, 0xF5,
0xFB, 0x00,
]);
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::GetCredsMetadata,
sub_command_params: None,
pin_protocol: Some(1),
pin_auth: pin_auth.clone(),
};
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.stateful_command_type,
&mut ctap_state.pin_protocol_v1,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
let initial_capacity = match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
assert_eq!(response.existing_resident_credentials_count, Some(0));
response
.max_possible_remaining_resident_credentials_count
.unwrap()
}
_ => panic!("Invalid response type"),
};
ctap_state
.persistent_store
.store_credential(credential_source)
.unwrap();
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::GetCredsMetadata,
sub_command_params: None,
pin_protocol: Some(1),
pin_auth,
};
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.stateful_command_type,
&mut ctap_state.pin_protocol_v1,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
assert_eq!(response.existing_resident_credentials_count, Some(1));
assert_eq!(
response.max_possible_remaining_resident_credentials_count,
Some(initial_capacity - 1)
);
}
_ => panic!("Invalid response type"),
};
}
#[test]
fn test_process_enumerate_rps_with_uv() {
let mut rng = ThreadRng256 {};
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
let pin_uv_auth_token = [0x55; 32];
let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token);
let credential_source1 = create_credential_source(&mut rng);
let mut credential_source2 = create_credential_source(&mut rng);
credential_source2.rp_id = "another.example.com".to_string();
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
ctap_state.pin_protocol_v1 = pin_protocol_v1;
ctap_state
.persistent_store
.store_credential(credential_source1)
.unwrap();
ctap_state
.persistent_store
.store_credential(credential_source2)
.unwrap();
ctap_state
.persistent_store
.set_pin_hash(&[0u8; 16])
.unwrap();
let pin_auth = Some(vec![
0x1A, 0xA4, 0x96, 0xDA, 0x62, 0x80, 0x28, 0x13, 0xEB, 0x32, 0xB9, 0xF1, 0xD2, 0xA9,
0xD0, 0xD1,
]);
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateRpsBegin,
sub_command_params: None,
pin_protocol: Some(1),
pin_auth,
};
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.stateful_command_type,
&mut ctap_state.pin_protocol_v1,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
let first_rp_id = match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
assert_eq!(response.total_rps, Some(2));
let rp_id = response.rp.unwrap().rp_id;
let rp_id_hash = Sha256::hash(rp_id.as_bytes());
assert_eq!(rp_id_hash, response.rp_id_hash.unwrap().as_slice());
rp_id
}
_ => panic!("Invalid response type"),
};
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp,
sub_command_params: None,
pin_protocol: None,
pin_auth: None,
};
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.stateful_command_type,
&mut ctap_state.pin_protocol_v1,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
let second_rp_id = match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
assert_eq!(response.total_rps, None);
let rp_id = response.rp.unwrap().rp_id;
let rp_id_hash = Sha256::hash(rp_id.as_bytes());
assert_eq!(rp_id_hash, response.rp_id_hash.unwrap().as_slice());
rp_id
}
_ => panic!("Invalid response type"),
};
assert!(first_rp_id != second_rp_id);
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp,
sub_command_params: None,
pin_protocol: None,
pin_auth: None,
};
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.stateful_command_type,
&mut ctap_state.pin_protocol_v1,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
assert_eq!(
cred_management_response,
Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)
);
}
#[test]
fn test_process_enumerate_credentials_with_uv() {
let mut rng = ThreadRng256 {};
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
let pin_uv_auth_token = [0x55; 32];
let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token);
let credential_source1 = create_credential_source(&mut rng);
let mut credential_source2 = create_credential_source(&mut rng);
credential_source2.user_handle = vec![0x02];
credential_source2.user_name = Some("user2".to_string());
credential_source2.user_display_name = Some("User Two".to_string());
credential_source2.user_icon = Some("icon2".to_string());
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
ctap_state.pin_protocol_v1 = pin_protocol_v1;
ctap_state
.persistent_store
.store_credential(credential_source1)
.unwrap();
ctap_state
.persistent_store
.store_credential(credential_source2)
.unwrap();
ctap_state
.persistent_store
.set_pin_hash(&[0u8; 16])
.unwrap();
let pin_auth = Some(vec![
0xF8, 0xB0, 0x3C, 0xC1, 0xD5, 0x58, 0x9C, 0xB7, 0x4D, 0x42, 0xA1, 0x64, 0x14, 0x28,
0x2B, 0x68,
]);
let sub_command_params = CredentialManagementSubCommandParameters {
rp_id_hash: Some(Sha256::hash(b"example.com").to_vec()),
credential_id: None,
user: None,
};
// RP ID hash:
// A379A6F6EEAFB9A55E378C118034E2751E682FAB9F2D30AB13D2125586CE1947
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateCredentialsBegin,
sub_command_params: Some(sub_command_params),
pin_protocol: Some(1),
pin_auth,
};
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.stateful_command_type,
&mut ctap_state.pin_protocol_v1,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
let first_credential_id = match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
assert!(response.user.is_some());
assert!(response.public_key.is_some());
assert_eq!(response.total_credentials, Some(2));
response.credential_id.unwrap().key_id
}
_ => panic!("Invalid response type"),
};
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential,
sub_command_params: None,
pin_protocol: None,
pin_auth: None,
};
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.stateful_command_type,
&mut ctap_state.pin_protocol_v1,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
let second_credential_id = match cred_management_response.unwrap() {
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
assert!(response.user.is_some());
assert!(response.public_key.is_some());
assert_eq!(response.total_credentials, None);
response.credential_id.unwrap().key_id
}
_ => panic!("Invalid response type"),
};
assert!(first_credential_id != second_credential_id);
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential,
sub_command_params: None,
pin_protocol: None,
pin_auth: None,
};
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.stateful_command_type,
&mut ctap_state.pin_protocol_v1,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
assert_eq!(
cred_management_response,
Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)
);
}
#[test]
fn test_process_delete_credential() {
let mut rng = ThreadRng256 {};
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
let pin_uv_auth_token = [0x55; 32];
let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token);
let mut credential_source = create_credential_source(&mut rng);
credential_source.credential_id = vec![0x1D; 32];
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
ctap_state.pin_protocol_v1 = pin_protocol_v1;
ctap_state
.persistent_store
.store_credential(credential_source)
.unwrap();
ctap_state
.persistent_store
.set_pin_hash(&[0u8; 16])
.unwrap();
let pin_auth = Some(vec![
0xBD, 0xE3, 0xEF, 0x8A, 0x77, 0x01, 0xB1, 0x69, 0x19, 0xE6, 0x62, 0xB9, 0x9B, 0x89,
0x9C, 0x64,
]);
let credential_id = PublicKeyCredentialDescriptor {
key_type: PublicKeyCredentialType::PublicKey,
key_id: vec![0x1D; 32],
transports: None, // You can set USB as a hint here.
};
let sub_command_params = CredentialManagementSubCommandParameters {
rp_id_hash: None,
credential_id: Some(credential_id),
user: None,
};
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::DeleteCredential,
sub_command_params: Some(sub_command_params.clone()),
pin_protocol: Some(1),
pin_auth: pin_auth.clone(),
};
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.stateful_command_type,
&mut ctap_state.pin_protocol_v1,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
assert_eq!(
cred_management_response,
Ok(ResponseData::AuthenticatorCredentialManagement(None))
);
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::DeleteCredential,
sub_command_params: Some(sub_command_params),
pin_protocol: Some(1),
pin_auth,
};
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.stateful_command_type,
&mut ctap_state.pin_protocol_v1,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
assert_eq!(
cred_management_response,
Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)
);
}
#[test]
fn test_process_update_user_information() {
let mut rng = ThreadRng256 {};
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
let pin_uv_auth_token = [0x55; 32];
let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token);
let mut credential_source = create_credential_source(&mut rng);
credential_source.credential_id = vec![0x1D; 32];
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
ctap_state.pin_protocol_v1 = pin_protocol_v1;
ctap_state
.persistent_store
.store_credential(credential_source)
.unwrap();
ctap_state
.persistent_store
.set_pin_hash(&[0u8; 16])
.unwrap();
let pin_auth = Some(vec![
0xA5, 0x55, 0x8F, 0x03, 0xC3, 0xD3, 0x73, 0x1C, 0x07, 0xDA, 0x1F, 0x8C, 0xC7, 0xBD,
0x9D, 0xB7,
]);
let credential_id = PublicKeyCredentialDescriptor {
key_type: PublicKeyCredentialType::PublicKey,
key_id: vec![0x1D; 32],
transports: None, // You can set USB as a hint here.
};
let new_user = PublicKeyCredentialUserEntity {
user_id: vec![0xFF],
user_name: Some("new_name".to_string()),
user_display_name: Some("new_display_name".to_string()),
user_icon: Some("new_icon".to_string()),
};
let sub_command_params = CredentialManagementSubCommandParameters {
rp_id_hash: None,
credential_id: Some(credential_id),
user: Some(new_user),
};
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::UpdateUserInformation,
sub_command_params: Some(sub_command_params),
pin_protocol: Some(1),
pin_auth,
};
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.stateful_command_type,
&mut ctap_state.pin_protocol_v1,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
assert_eq!(
cred_management_response,
Ok(ResponseData::AuthenticatorCredentialManagement(None))
);
let updated_credential = ctap_state
.persistent_store
.find_credential("example.com", &[0x1D; 32], false)
.unwrap()
.unwrap();
assert_eq!(updated_credential.user_handle, vec![0x01]);
assert_eq!(&updated_credential.user_name.unwrap(), "new_name");
assert_eq!(
&updated_credential.user_display_name.unwrap(),
"new_display_name"
);
assert_eq!(&updated_credential.user_icon.unwrap(), "new_icon");
}
#[test]
fn test_process_credential_management_invalid_pin_protocol() {
let mut rng = ThreadRng256 {};
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
let pin_uv_auth_token = [0x55; 32];
let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token);
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
ctap_state.pin_protocol_v1 = pin_protocol_v1;
ctap_state
.persistent_store
.set_pin_hash(&[0u8; 16])
.unwrap();
let pin_auth = Some(vec![
0xC5, 0xFB, 0x75, 0x55, 0x98, 0xB5, 0x19, 0x01, 0xB3, 0x31, 0x7D, 0xFE, 0x1D, 0xF5,
0xFB, 0x00,
]);
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::GetCredsMetadata,
sub_command_params: None,
pin_protocol: Some(123456),
pin_auth,
};
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.stateful_command_type,
&mut ctap_state.pin_protocol_v1,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
assert_eq!(
cred_management_response,
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
#[test]
fn test_process_credential_management_invalid_pin_auth() {
let mut rng = ThreadRng256 {};
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
ctap_state
.persistent_store
.set_pin_hash(&[0u8; 16])
.unwrap();
let cred_management_params = AuthenticatorCredentialManagementParameters {
sub_command: CredentialManagementSubCommand::GetCredsMetadata,
sub_command_params: None,
pin_protocol: Some(1),
pin_auth: Some(vec![0u8; 16]),
};
let cred_management_response = process_credential_management(
&mut ctap_state.persistent_store,
&mut ctap_state.stateful_command_permission,
&mut ctap_state.stateful_command_type,
&mut ctap_state.pin_protocol_v1,
cred_management_params,
DUMMY_CLOCK_VALUE,
);
assert_eq!(
cred_management_response,
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
}

View File

@@ -27,6 +27,7 @@ use enum_iterator::IntoEnumIterator;
const ES256_ALGORITHM: i64 = -7; const ES256_ALGORITHM: i64 = -7;
// https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrpentity // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrpentity
#[derive(Clone)]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct PublicKeyCredentialRpEntity { pub struct PublicKeyCredentialRpEntity {
pub rp_id: String, pub rp_id: String,
@@ -58,8 +59,19 @@ impl TryFrom<cbor::Value> for PublicKeyCredentialRpEntity {
} }
} }
impl From<PublicKeyCredentialRpEntity> for cbor::Value {
fn from(entity: PublicKeyCredentialRpEntity) -> Self {
cbor_map_options! {
"id" => entity.rp_id,
"name" => entity.rp_name,
"icon" => entity.rp_icon,
}
}
}
// https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialuserentity // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialuserentity
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] #[derive(Clone)]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct PublicKeyCredentialUserEntity { pub struct PublicKeyCredentialUserEntity {
pub user_id: Vec<u8>, pub user_id: Vec<u8>,
pub user_name: Option<String>, pub user_name: Option<String>,
@@ -173,7 +185,8 @@ impl From<PublicKeyCredentialParameter> for cbor::Value {
} }
// https://www.w3.org/TR/webauthn/#enumdef-authenticatortransport // https://www.w3.org/TR/webauthn/#enumdef-authenticatortransport
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] #[derive(Clone)]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
#[cfg_attr(test, derive(IntoEnumIterator))] #[cfg_attr(test, derive(IntoEnumIterator))]
pub enum AuthenticatorTransport { pub enum AuthenticatorTransport {
Usb, Usb,
@@ -210,7 +223,8 @@ impl TryFrom<cbor::Value> for AuthenticatorTransport {
} }
// https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialdescriptor // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialdescriptor
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] #[derive(Clone)]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct PublicKeyCredentialDescriptor { pub struct PublicKeyCredentialDescriptor {
pub key_type: PublicKeyCredentialType, pub key_type: PublicKeyCredentialType,
pub key_id: Vec<u8>, pub key_id: Vec<u8>,
@@ -788,6 +802,88 @@ impl TryFrom<cbor::Value> for ClientPinSubCommand {
} }
} }
#[derive(Clone, Copy)]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
#[cfg_attr(test, derive(IntoEnumIterator))]
pub enum CredentialManagementSubCommand {
GetCredsMetadata = 0x01,
EnumerateRpsBegin = 0x02,
EnumerateRpsGetNextRp = 0x03,
EnumerateCredentialsBegin = 0x04,
EnumerateCredentialsGetNextCredential = 0x05,
DeleteCredential = 0x06,
UpdateUserInformation = 0x07,
}
impl From<CredentialManagementSubCommand> for cbor::Value {
fn from(subcommand: CredentialManagementSubCommand) -> Self {
(subcommand as u64).into()
}
}
impl TryFrom<cbor::Value> for CredentialManagementSubCommand {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
let subcommand_int = extract_unsigned(cbor_value)?;
match subcommand_int {
0x01 => Ok(CredentialManagementSubCommand::GetCredsMetadata),
0x02 => Ok(CredentialManagementSubCommand::EnumerateRpsBegin),
0x03 => Ok(CredentialManagementSubCommand::EnumerateRpsGetNextRp),
0x04 => Ok(CredentialManagementSubCommand::EnumerateCredentialsBegin),
0x05 => Ok(CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential),
0x06 => Ok(CredentialManagementSubCommand::DeleteCredential),
0x07 => Ok(CredentialManagementSubCommand::UpdateUserInformation),
_ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND),
}
}
}
#[derive(Clone)]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct CredentialManagementSubCommandParameters {
pub rp_id_hash: Option<Vec<u8>>,
pub credential_id: Option<PublicKeyCredentialDescriptor>,
pub user: Option<PublicKeyCredentialUserEntity>,
}
impl TryFrom<cbor::Value> for CredentialManagementSubCommandParameters {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
destructure_cbor_map! {
let {
0x01 => rp_id_hash,
0x02 => credential_id,
0x03 => user,
} = extract_map(cbor_value)?;
}
let rp_id_hash = rp_id_hash.map(extract_byte_string).transpose()?;
let credential_id = credential_id
.map(PublicKeyCredentialDescriptor::try_from)
.transpose()?;
let user = user
.map(PublicKeyCredentialUserEntity::try_from)
.transpose()?;
Ok(Self {
rp_id_hash,
credential_id,
user,
})
}
}
impl From<CredentialManagementSubCommandParameters> for cbor::Value {
fn from(sub_command_params: CredentialManagementSubCommandParameters) -> Self {
cbor_map_options! {
0x01 => sub_command_params.rp_id_hash,
0x02 => sub_command_params.credential_id,
0x03 => sub_command_params.user,
}
}
}
pub(super) fn extract_unsigned(cbor_value: cbor::Value) -> Result<u64, Ctap2StatusCode> { pub(super) fn extract_unsigned(cbor_value: cbor::Value) -> Result<u64, Ctap2StatusCode> {
match cbor_value { match cbor_value {
cbor::Value::KeyValue(cbor::KeyType::Unsigned(unsigned)) => Ok(unsigned), cbor::Value::KeyValue(cbor::KeyType::Unsigned(unsigned)) => Ok(unsigned),
@@ -1504,6 +1600,52 @@ mod test {
} }
} }
#[test]
fn test_from_into_cred_management_sub_command() {
let cbor_sub_command: cbor::Value = cbor_int!(0x01);
let sub_command = CredentialManagementSubCommand::try_from(cbor_sub_command.clone());
let expected_sub_command = CredentialManagementSubCommand::GetCredsMetadata;
assert_eq!(sub_command, Ok(expected_sub_command));
let created_cbor: cbor::Value = sub_command.unwrap().into();
assert_eq!(created_cbor, cbor_sub_command);
for command in CredentialManagementSubCommand::into_enum_iter() {
let created_cbor: cbor::Value = command.clone().into();
let reconstructed = CredentialManagementSubCommand::try_from(created_cbor).unwrap();
assert_eq!(command, reconstructed);
}
}
#[test]
fn test_from_into_cred_management_sub_command_params() {
let credential_id = PublicKeyCredentialDescriptor {
key_type: PublicKeyCredentialType::PublicKey,
key_id: vec![0x2D, 0x2D, 0x2D, 0x2D],
transports: Some(vec![AuthenticatorTransport::Usb]),
};
let user_entity = PublicKeyCredentialUserEntity {
user_id: vec![0x1D, 0x1D, 0x1D, 0x1D],
user_name: Some("foo".to_string()),
user_display_name: Some("bar".to_string()),
user_icon: Some("example.com/foo/icon.png".to_string()),
};
let cbor_sub_command_params = cbor_map! {
0x01 => vec![0x1D; 32],
0x02 => credential_id.clone(),
0x03 => user_entity.clone(),
};
let sub_command_params =
CredentialManagementSubCommandParameters::try_from(cbor_sub_command_params.clone());
let expected_sub_command_params = CredentialManagementSubCommandParameters {
rp_id_hash: Some(vec![0x1D; 32]),
credential_id: Some(credential_id),
user: Some(user_entity),
};
assert_eq!(sub_command_params, Ok(expected_sub_command_params));
let created_cbor: cbor::Value = sub_command_params.unwrap().into();
assert_eq!(created_cbor, cbor_sub_command_params);
}
#[test] #[test]
fn test_credential_source_cbor_round_trip() { fn test_credential_source_cbor_round_trip() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};

View File

@@ -14,6 +14,7 @@
pub mod apdu; pub mod apdu;
pub mod command; pub mod command;
mod credential_management;
#[cfg(feature = "with_ctap1")] #[cfg(feature = "with_ctap1")]
mod ctap1; mod ctap1;
pub mod data_formats; pub mod data_formats;
@@ -30,6 +31,7 @@ use self::command::{
AuthenticatorMakeCredentialParameters, AuthenticatorVendorConfigureParameters, Command, AuthenticatorMakeCredentialParameters, AuthenticatorVendorConfigureParameters, Command,
MAX_CREDENTIAL_COUNT_IN_LIST, MAX_CREDENTIAL_COUNT_IN_LIST,
}; };
use self::credential_management::process_credential_management;
use self::data_formats::{ use self::data_formats::{
AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, GetAssertionHmacSecretInput, AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, GetAssertionHmacSecretInput,
PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter,
@@ -98,7 +100,7 @@ pub const TOUCH_TIMEOUT_MS: isize = 30000;
#[cfg(feature = "with_ctap1")] #[cfg(feature = "with_ctap1")]
const U2F_UP_PROMPT_TIMEOUT: Duration<isize> = Duration::from_ms(10000); const U2F_UP_PROMPT_TIMEOUT: Duration<isize> = Duration::from_ms(10000);
const RESET_TIMEOUT_DURATION: Duration<isize> = Duration::from_ms(10000); const RESET_TIMEOUT_DURATION: Duration<isize> = Duration::from_ms(10000);
const STATEFUL_COMMAND_TIMEOUT_DURATION: Duration<isize> = Duration::from_ms(30000); pub const STATEFUL_COMMAND_TIMEOUT_DURATION: Duration<isize> = Duration::from_ms(30000);
pub const FIDO2_VERSION_STRING: &str = "FIDO_2_0"; pub const FIDO2_VERSION_STRING: &str = "FIDO_2_0";
#[cfg(feature = "with_ctap1")] #[cfg(feature = "with_ctap1")]
@@ -139,15 +141,29 @@ struct AssertionInput {
has_uv: bool, has_uv: bool,
} }
struct AssertionState { pub struct AssertionState {
assertion_input: AssertionInput, assertion_input: AssertionInput,
// Sorted by ascending order of creation, so the last element is the most recent one. // Sorted by ascending order of creation, so the last element is the most recent one.
next_credential_keys: Vec<usize>, next_credential_keys: Vec<usize>,
} }
enum StatefulCommand { pub enum StatefulCommand {
Reset, Reset,
GetAssertion(AssertionState), GetAssertion(AssertionState),
EnumerateRps(Vec<String>),
EnumerateCredentials(Vec<usize>),
}
pub fn check_command_permission(
stateful_command_permission: &mut TimedPermission,
now: ClockValue,
) -> Result<(), Ctap2StatusCode> {
*stateful_command_permission = stateful_command_permission.check_expiration(now);
if stateful_command_permission.is_granted(now) {
Ok(())
} else {
Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)
}
} }
// This struct currently holds all state, not only the persistent memory. The persistent members are // This struct currently holds all state, not only the persistent memory. The persistent members are
@@ -200,15 +216,6 @@ where
self.stateful_command_permission = self.stateful_command_permission.check_expiration(now); self.stateful_command_permission = self.stateful_command_permission.check_expiration(now);
} }
fn check_command_permission(&mut self, now: ClockValue) -> Result<(), Ctap2StatusCode> {
self.update_command_permission(now);
if self.stateful_command_permission.is_granted(now) {
Ok(())
} else {
Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)
}
}
pub fn increment_global_signature_counter(&mut self) -> Result<(), Ctap2StatusCode> { pub fn increment_global_signature_counter(&mut self) -> Result<(), Ctap2StatusCode> {
if USE_SIGNATURE_COUNTER { if USE_SIGNATURE_COUNTER {
let increment = self.rng.gen_uniform_u32x8()[0] % 8 + 1; let increment = self.rng.gen_uniform_u32x8()[0] % 8 + 1;
@@ -330,6 +337,14 @@ where
Command::AuthenticatorGetNextAssertion, Command::AuthenticatorGetNextAssertion,
Some(StatefulCommand::GetAssertion(_)), Some(StatefulCommand::GetAssertion(_)),
) => (), ) => (),
(
Command::AuthenticatorCredentialManagement(_),
Some(StatefulCommand::EnumerateRps(_)),
) => (),
(
Command::AuthenticatorCredentialManagement(_),
Some(StatefulCommand::EnumerateCredentials(_)),
) => (),
(Command::AuthenticatorReset, Some(StatefulCommand::Reset)) => (), (Command::AuthenticatorReset, Some(StatefulCommand::Reset)) => (),
// GetInfo does not reset stateful commands. // GetInfo does not reset stateful commands.
(Command::AuthenticatorGetInfo, _) => (), (Command::AuthenticatorGetInfo, _) => (),
@@ -350,6 +365,16 @@ where
Command::AuthenticatorGetInfo => self.process_get_info(), Command::AuthenticatorGetInfo => self.process_get_info(),
Command::AuthenticatorClientPin(params) => self.process_client_pin(params), Command::AuthenticatorClientPin(params) => self.process_client_pin(params),
Command::AuthenticatorReset => self.process_reset(cid, now), Command::AuthenticatorReset => self.process_reset(cid, now),
Command::AuthenticatorCredentialManagement(params) => {
process_credential_management(
&mut self.persistent_store,
&mut self.stateful_command_permission,
&mut self.stateful_command_type,
&mut self.pin_protocol_v1,
params,
now,
)
}
Command::AuthenticatorSelection => self.process_selection(cid), Command::AuthenticatorSelection => self.process_selection(cid),
// TODO(kaczmarczyck) implement FIDO 2.1 commands // TODO(kaczmarczyck) implement FIDO 2.1 commands
// Vendor specific commands // Vendor specific commands
@@ -818,7 +843,7 @@ where
&mut self, &mut self,
now: ClockValue, now: ClockValue,
) -> Result<ResponseData, Ctap2StatusCode> { ) -> Result<ResponseData, Ctap2StatusCode> {
self.check_command_permission(now)?; check_command_permission(&mut self.stateful_command_permission, now)?;
let (assertion_input, credential) = let (assertion_input, credential) =
if let Some(StatefulCommand::GetAssertion(assertion_state)) = if let Some(StatefulCommand::GetAssertion(assertion_state)) =
&mut self.stateful_command_type &mut self.stateful_command_type
@@ -844,6 +869,7 @@ where
String::from("clientPin"), String::from("clientPin"),
self.persistent_store.pin_hash()?.is_some(), self.persistent_store.pin_hash()?.is_some(),
); );
options_map.insert(String::from("credMgmt"), true);
Ok(ResponseData::AuthenticatorGetInfo( Ok(ResponseData::AuthenticatorGetInfo(
AuthenticatorGetInfoResponse { AuthenticatorGetInfoResponse {
versions: vec![ versions: vec![
@@ -895,7 +921,7 @@ where
) -> Result<ResponseData, Ctap2StatusCode> { ) -> Result<ResponseData, Ctap2StatusCode> {
// Resets are only possible in the first 10 seconds after booting. // Resets are only possible in the first 10 seconds after booting.
// TODO(kaczmarczyck) 2.1 allows Reset after Reset and 15 seconds? // TODO(kaczmarczyck) 2.1 allows Reset after Reset and 15 seconds?
self.check_command_permission(now)?; check_command_permission(&mut self.stateful_command_permission, now)?;
match &self.stateful_command_type { match &self.stateful_command_type {
Some(StatefulCommand::Reset) => (), Some(StatefulCommand::Reset) => (),
_ => return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED), _ => return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED),
@@ -1053,11 +1079,12 @@ mod test {
expected_response.extend(&ctap_state.persistent_store.aaguid().unwrap()); expected_response.extend(&ctap_state.persistent_store.aaguid().unwrap());
expected_response.extend( expected_response.extend(
[ [
0x04, 0xA3, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x69, 0x63, 0x6C, 0x69, 0x04, 0xA4, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x68, 0x63, 0x72, 0x65,
0x65, 0x6E, 0x74, 0x50, 0x69, 0x6E, 0xF4, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, 0x01, 0x64, 0x4D, 0x67, 0x6D, 0x74, 0xF5, 0x69, 0x63, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x50,
0x08, 0x18, 0x70, 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, 0x69, 0x6E, 0xF4, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, 0x01, 0x08, 0x18, 0x70, 0x09,
0x6C, 0x67, 0x26, 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, 0x6C, 0x67, 0x26, 0x64,
0x63, 0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04, 0x14, 0x18, 0x96, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65,
0x79, 0x0D, 0x04, 0x14, 0x18, 0x96,
] ]
.iter(), .iter(),
); );
@@ -2034,6 +2061,22 @@ mod test {
assert_eq!(reset_reponse, Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)); assert_eq!(reset_reponse, Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED));
} }
#[test]
fn test_process_credential_management_unknown_subcommand() {
let mut rng = ThreadRng256 {};
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
// The subcommand 0xEE does not exist.
let reponse = ctap_state.process_command(
&[0x0A, 0xA1, 0x01, 0x18, 0xEE],
DUMMY_CHANNEL_ID,
DUMMY_CLOCK_VALUE,
);
let expected_response = vec![Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND as u8];
assert_eq!(reponse, expected_response);
}
#[test] #[test]
fn test_process_unknown_command() { fn test_process_unknown_command() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};
@@ -2041,10 +2084,9 @@ mod test {
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
// This command does not exist. // This command does not exist.
let reset_reponse = let reponse = ctap_state.process_command(&[0xDF], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE);
ctap_state.process_command(&[0xDF], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE);
let expected_response = vec![Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND as u8]; let expected_response = vec![Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND as u8];
assert_eq!(reset_reponse, expected_response); assert_eq!(reponse, expected_response);
} }
#[test] #[test]

View File

@@ -563,6 +563,13 @@ impl PinProtocolV1 {
} }
} }
pub fn has_no_permission_rp_id(&self) -> Result<(), Ctap2StatusCode> {
if self.permissions_rp_id.is_some() {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID);
}
Ok(())
}
pub fn has_permission_for_rp_id(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { pub fn has_permission_for_rp_id(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> {
if let Some(permissions_rp_id) = &self.permissions_rp_id { if let Some(permissions_rp_id) = &self.permissions_rp_id {
if rp_id != permissions_rp_id { if rp_id != permissions_rp_id {
@@ -1187,6 +1194,19 @@ mod test {
} }
} }
#[test]
fn test_has_no_permission_rp_id() {
let mut rng = ThreadRng256 {};
let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng);
assert_eq!(pin_protocol_v1.has_no_permission_rp_id(), Ok(()));
assert_eq!(pin_protocol_v1.permissions_rp_id, None,);
pin_protocol_v1.permissions_rp_id = Some("example.com".to_string());
assert_eq!(
pin_protocol_v1.has_no_permission_rp_id(),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
#[test] #[test]
fn test_has_permission_for_rp_id() { fn test_has_permission_for_rp_id() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};

View File

@@ -14,7 +14,8 @@
use super::data_formats::{ use super::data_formats::{
AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, PackedAttestationStatement, AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, PackedAttestationStatement,
PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialUserEntity, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity,
PublicKeyCredentialUserEntity,
}; };
use alloc::collections::BTreeMap; use alloc::collections::BTreeMap;
use alloc::string::String; use alloc::string::String;
@@ -30,6 +31,7 @@ pub enum ResponseData {
AuthenticatorGetInfo(AuthenticatorGetInfoResponse), AuthenticatorGetInfo(AuthenticatorGetInfoResponse),
AuthenticatorClientPin(Option<AuthenticatorClientPinResponse>), AuthenticatorClientPin(Option<AuthenticatorClientPinResponse>),
AuthenticatorReset, AuthenticatorReset,
AuthenticatorCredentialManagement(Option<AuthenticatorCredentialManagementResponse>),
AuthenticatorSelection, AuthenticatorSelection,
AuthenticatorVendor(AuthenticatorVendorResponse), AuthenticatorVendor(AuthenticatorVendorResponse),
} }
@@ -41,9 +43,9 @@ impl From<ResponseData> for Option<cbor::Value> {
ResponseData::AuthenticatorGetAssertion(data) => Some(data.into()), ResponseData::AuthenticatorGetAssertion(data) => Some(data.into()),
ResponseData::AuthenticatorGetNextAssertion(data) => Some(data.into()), ResponseData::AuthenticatorGetNextAssertion(data) => Some(data.into()),
ResponseData::AuthenticatorGetInfo(data) => Some(data.into()), ResponseData::AuthenticatorGetInfo(data) => Some(data.into()),
ResponseData::AuthenticatorClientPin(Some(data)) => Some(data.into()), ResponseData::AuthenticatorClientPin(data) => data.map(|d| d.into()),
ResponseData::AuthenticatorClientPin(None) => None,
ResponseData::AuthenticatorReset => None, ResponseData::AuthenticatorReset => None,
ResponseData::AuthenticatorCredentialManagement(data) => data.map(|d| d.into()),
ResponseData::AuthenticatorSelection => None, ResponseData::AuthenticatorSelection => None,
ResponseData::AuthenticatorVendor(data) => Some(data.into()), ResponseData::AuthenticatorVendor(data) => Some(data.into()),
} }
@@ -199,6 +201,54 @@ impl From<AuthenticatorClientPinResponse> for cbor::Value {
} }
} }
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))]
pub struct AuthenticatorCredentialManagementResponse {
pub existing_resident_credentials_count: Option<u64>,
pub max_possible_remaining_resident_credentials_count: Option<u64>,
pub rp: Option<PublicKeyCredentialRpEntity>,
pub rp_id_hash: Option<Vec<u8>>,
pub total_rps: Option<u64>,
pub user: Option<PublicKeyCredentialUserEntity>,
pub credential_id: Option<PublicKeyCredentialDescriptor>,
pub public_key: Option<CoseKey>,
pub total_credentials: Option<u64>,
pub cred_protect: Option<CredentialProtectionPolicy>,
pub large_blob_key: Option<Vec<u8>>,
}
impl From<AuthenticatorCredentialManagementResponse> for cbor::Value {
fn from(cred_management_response: AuthenticatorCredentialManagementResponse) -> Self {
let AuthenticatorCredentialManagementResponse {
existing_resident_credentials_count,
max_possible_remaining_resident_credentials_count,
rp,
rp_id_hash,
total_rps,
user,
credential_id,
public_key,
total_credentials,
cred_protect,
large_blob_key,
} = cred_management_response;
cbor_map_options! {
0x01 => existing_resident_credentials_count,
0x02 => max_possible_remaining_resident_credentials_count,
0x03 => rp,
0x04 => rp_id_hash,
0x05 => total_rps,
0x06 => user,
0x07 => credential_id,
0x08 => public_key.map(cbor::Value::from),
0x09 => total_credentials,
0x0A => cred_protect,
0x0B => large_blob_key,
}
}
}
#[cfg_attr(test, derive(PartialEq))] #[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))]
pub struct AuthenticatorVendorResponse { pub struct AuthenticatorVendorResponse {
@@ -222,10 +272,11 @@ impl From<AuthenticatorVendorResponse> for cbor::Value {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::super::data_formats::PackedAttestationStatement; use super::super::data_formats::{PackedAttestationStatement, PublicKeyCredentialType};
use super::super::ES256_CRED_PARAM; use super::super::ES256_CRED_PARAM;
use super::*; use super::*;
use cbor::{cbor_bytes, cbor_map}; use cbor::{cbor_bytes, cbor_map};
use crypto::rng256::ThreadRng256;
#[test] #[test]
fn test_make_credential_into_cbor() { fn test_make_credential_into_cbor() {
@@ -379,6 +430,88 @@ mod test {
assert_eq!(response_cbor, None); assert_eq!(response_cbor, None);
} }
#[test]
fn test_used_credential_management_into_cbor() {
let cred_management_response = AuthenticatorCredentialManagementResponse {
existing_resident_credentials_count: None,
max_possible_remaining_resident_credentials_count: None,
rp: None,
rp_id_hash: None,
total_rps: None,
user: None,
credential_id: None,
public_key: None,
total_credentials: None,
cred_protect: None,
large_blob_key: None,
};
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorCredentialManagement(Some(cred_management_response)).into();
let expected_cbor = cbor_map_options! {};
assert_eq!(response_cbor, Some(expected_cbor));
}
#[test]
fn test_used_credential_management_optionals_into_cbor() {
let mut rng = ThreadRng256 {};
let sk = crypto::ecdh::SecKey::gensk(&mut rng);
let rp = PublicKeyCredentialRpEntity {
rp_id: String::from("example.com"),
rp_name: None,
rp_icon: None,
};
let user = PublicKeyCredentialUserEntity {
user_id: vec![0xFA, 0xB1, 0xA2],
user_name: None,
user_display_name: None,
user_icon: None,
};
let cred_descriptor = PublicKeyCredentialDescriptor {
key_type: PublicKeyCredentialType::PublicKey,
key_id: vec![0x1D; 32],
transports: None,
};
let pk = sk.genpk();
let cose_key = CoseKey::from(pk);
let cred_management_response = AuthenticatorCredentialManagementResponse {
existing_resident_credentials_count: Some(100),
max_possible_remaining_resident_credentials_count: Some(96),
rp: Some(rp.clone()),
rp_id_hash: Some(vec![0x1D; 32]),
total_rps: Some(3),
user: Some(user.clone()),
credential_id: Some(cred_descriptor.clone()),
public_key: Some(cose_key.clone()),
total_credentials: Some(2),
cred_protect: Some(CredentialProtectionPolicy::UserVerificationOptional),
large_blob_key: Some(vec![0xBB; 64]),
};
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorCredentialManagement(Some(cred_management_response)).into();
let expected_cbor = cbor_map_options! {
0x01 => 100,
0x02 => 96,
0x03 => rp,
0x04 => vec![0x1D; 32],
0x05 => 3,
0x06 => user,
0x07 => cred_descriptor,
0x08 => cbor::Value::from(cose_key),
0x09 => 2,
0x0A => 0x01,
0x0B => vec![0xBB; 64],
};
assert_eq!(response_cbor, Some(expected_cbor));
}
#[test]
fn test_empty_credential_management_into_cbor() {
let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorCredentialManagement(None).into();
assert_eq!(response_cbor, None);
}
#[test] #[test]
fn test_selection_into_cbor() { fn test_selection_into_cbor() {
let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorSelection.into(); let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorSelection.into();

View File

@@ -238,7 +238,7 @@ impl PersistentStore {
/// # Errors /// # Errors
/// ///
/// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found. /// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found.
pub fn _delete_credential(&mut self, credential_id: &[u8]) -> Result<(), Ctap2StatusCode> { pub fn delete_credential(&mut self, credential_id: &[u8]) -> Result<(), Ctap2StatusCode> {
let (key, _) = self.find_credential_item(credential_id)?; let (key, _) = self.find_credential_item(credential_id)?;
Ok(self.store.remove(key)?) Ok(self.store.remove(key)?)
} }
@@ -248,7 +248,7 @@ impl PersistentStore {
/// # Errors /// # Errors
/// ///
/// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found. /// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found.
pub fn _update_credential( pub fn update_credential(
&mut self, &mut self,
credential_id: &[u8], credential_id: &[u8],
user: PublicKeyCredentialUserEntity, user: PublicKeyCredentialUserEntity,
@@ -692,7 +692,7 @@ mod test {
} }
let mut count = persistent_store.count_credentials().unwrap(); let mut count = persistent_store.count_credentials().unwrap();
for credential_id in credential_ids { for credential_id in credential_ids {
assert!(persistent_store._delete_credential(&credential_id).is_ok()); assert!(persistent_store.delete_credential(&credential_id).is_ok());
count -= 1; count -= 1;
assert_eq!(persistent_store.count_credentials().unwrap(), count); assert_eq!(persistent_store.count_credentials().unwrap(), count);
} }
@@ -710,7 +710,7 @@ mod test {
user_icon: Some("icon".to_string()), user_icon: Some("icon".to_string()),
}; };
assert_eq!( assert_eq!(
persistent_store._update_credential(&[0x1D], user.clone()), persistent_store.update_credential(&[0x1D], user.clone()),
Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS) Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)
); );
@@ -725,7 +725,7 @@ mod test {
assert_eq!(stored_credential.user_display_name, None); assert_eq!(stored_credential.user_display_name, None);
assert_eq!(stored_credential.user_icon, None); assert_eq!(stored_credential.user_icon, None);
assert!(persistent_store assert!(persistent_store
._update_credential(&credential_id, user.clone()) .update_credential(&credential_id, user.clone())
.is_ok()); .is_ok());
let stored_credential = persistent_store let stored_credential = persistent_store
.find_credential("example.com", &credential_id, false) .find_credential("example.com", &credential_id, false)