Merge pull request #256 from kaczmarczyck/command-cred-mgmt
Command logic for credential management
This commit is contained in:
@@ -15,9 +15,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, ConfigSubCommand, ConfigSubCommandParams,
|
extract_unsigned, ok_or_missing, ClientPinSubCommand, ConfigSubCommand, ConfigSubCommandParams,
|
||||||
CoseKey, GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions,
|
CoseKey, CredentialManagementSubCommand, CredentialManagementSubCommandParameters,
|
||||||
MakeCredentialOptions, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter,
|
GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions,
|
||||||
PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, SetMinPinLengthParams,
|
PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity,
|
||||||
|
PublicKeyCredentialUserEntity, SetMinPinLengthParams,
|
||||||
};
|
};
|
||||||
use super::key_material;
|
use super::key_material;
|
||||||
use super::status_code::Ctap2StatusCode;
|
use super::status_code::Ctap2StatusCode;
|
||||||
@@ -41,6 +42,7 @@ pub enum Command {
|
|||||||
AuthenticatorClientPin(AuthenticatorClientPinParameters),
|
AuthenticatorClientPin(AuthenticatorClientPinParameters),
|
||||||
AuthenticatorReset,
|
AuthenticatorReset,
|
||||||
AuthenticatorGetNextAssertion,
|
AuthenticatorGetNextAssertion,
|
||||||
|
AuthenticatorCredentialManagement(AuthenticatorCredentialManagementParameters),
|
||||||
AuthenticatorSelection,
|
AuthenticatorSelection,
|
||||||
AuthenticatorConfig(AuthenticatorConfigParameters),
|
AuthenticatorConfig(AuthenticatorConfigParameters),
|
||||||
// TODO(kaczmarczyck) implement FIDO 2.1 commands (see below consts)
|
// TODO(kaczmarczyck) implement FIDO 2.1 commands (see below consts)
|
||||||
@@ -111,6 +113,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)
|
||||||
@@ -414,6 +422,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,
|
||||||
@@ -575,10 +620,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),
|
||||||
@@ -590,8 +635,8 @@ mod test {
|
|||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
returned_pin_protocol_parameters,
|
returned_client_pin_parameters,
|
||||||
expected_pin_protocol_parameters
|
expected_client_pin_parameters
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,6 +662,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];
|
||||||
|
|||||||
912
src/ctap/credential_management.rs
Normal file
912
src/ctap/credential_management.rs
Normal 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::{check_pin_uv_auth_protocol, StatefulCommand, StatefulPermission};
|
||||||
|
use alloc::collections::BTreeSet;
|
||||||
|
use alloc::string::String;
|
||||||
|
use alloc::vec;
|
||||||
|
use alloc::vec::Vec;
|
||||||
|
use crypto::sha256::Sha256;
|
||||||
|
use crypto::Hash256;
|
||||||
|
use libtock_drivers::timer::ClockValue;
|
||||||
|
|
||||||
|
/// Generates a set with all existing RP IDs.
|
||||||
|
fn get_stored_rp_ids(
|
||||||
|
persistent_store: &PersistentStore,
|
||||||
|
) -> Result<BTreeSet<String>, 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?;
|
||||||
|
Ok(rp_set)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
rp,
|
||||||
|
rp_id_hash,
|
||||||
|
total_rps,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
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,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Processes the subcommand enumerateRPsBegin for CredentialManagement.
|
||||||
|
fn process_enumerate_rps_begin(
|
||||||
|
persistent_store: &PersistentStore,
|
||||||
|
stateful_command_permission: &mut StatefulPermission,
|
||||||
|
now: ClockValue,
|
||||||
|
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
|
||||||
|
let rp_set = get_stored_rp_ids(persistent_store)?;
|
||||||
|
let total_rps = rp_set.len();
|
||||||
|
|
||||||
|
// TODO(kaczmarczyck) should we return CTAP2_ERR_NO_CREDENTIALS if empty?
|
||||||
|
if total_rps > 1 {
|
||||||
|
stateful_command_permission.set_command(now, StatefulCommand::EnumerateRps(1));
|
||||||
|
}
|
||||||
|
// TODO https://github.com/rust-lang/rust/issues/62924 replace with pop_first()
|
||||||
|
enumerate_rps_response(rp_set.into_iter().next(), Some(total_rps as u64))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Processes the subcommand enumerateRPsGetNextRP for CredentialManagement.
|
||||||
|
fn process_enumerate_rps_get_next_rp(
|
||||||
|
persistent_store: &PersistentStore,
|
||||||
|
stateful_command_permission: &mut StatefulPermission,
|
||||||
|
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
|
||||||
|
let rp_id_index = stateful_command_permission.next_enumerate_rp()?;
|
||||||
|
let rp_set = get_stored_rp_ids(persistent_store)?;
|
||||||
|
// A BTreeSet is already sorted.
|
||||||
|
let rp_id = rp_set
|
||||||
|
.into_iter()
|
||||||
|
.nth(rp_id_index)
|
||||||
|
.ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?;
|
||||||
|
enumerate_rps_response(Some(rp_id), None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Processes the subcommand enumerateCredentialsBegin for CredentialManagement.
|
||||||
|
fn process_enumerate_credentials_begin(
|
||||||
|
persistent_store: &PersistentStore,
|
||||||
|
stateful_command_permission: &mut StatefulPermission,
|
||||||
|
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
|
||||||
|
.set_command(now, 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 StatefulPermission,
|
||||||
|
) -> Result<AuthenticatorCredentialManagementResponse, Ctap2StatusCode> {
|
||||||
|
let credential_key = stateful_command_permission.next_enumerate_credential()?;
|
||||||
|
let credential = persistent_store.get_credential(credential_key)?;
|
||||||
|
enumerate_credentials_response(credential, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Processes the CredentialManagement command and all its subcommands.
|
||||||
|
pub fn process_credential_management(
|
||||||
|
persistent_store: &mut PersistentStore,
|
||||||
|
stateful_command_permission: &mut StatefulPermission,
|
||||||
|
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, stateful_command_permission.get_command()) {
|
||||||
|
(
|
||||||
|
CredentialManagementSubCommand::EnumerateRpsGetNextRp,
|
||||||
|
Ok(StatefulCommand::EnumerateRps(_)),
|
||||||
|
)
|
||||||
|
| (
|
||||||
|
CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential,
|
||||||
|
Ok(StatefulCommand::EnumerateCredentials(_)),
|
||||||
|
) => stateful_command_permission.check_command_permission(now)?,
|
||||||
|
(_, _) => {
|
||||||
|
stateful_command_permission.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match sub_command {
|
||||||
|
CredentialManagementSubCommand::GetCredsMetadata
|
||||||
|
| CredentialManagementSubCommand::EnumerateRpsBegin
|
||||||
|
| CredentialManagementSubCommand::DeleteCredential
|
||||||
|
| CredentialManagementSubCommand::EnumerateCredentialsBegin
|
||||||
|
| CredentialManagementSubCommand::UpdateUserInformation => {
|
||||||
|
check_pin_uv_auth_protocol(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,
|
||||||
|
now,
|
||||||
|
)?),
|
||||||
|
CredentialManagementSubCommand::EnumerateRpsGetNextRp => Some(
|
||||||
|
process_enumerate_rps_get_next_rp(persistent_store, stateful_command_permission)?,
|
||||||
|
),
|
||||||
|
CredentialManagementSubCommand::EnumerateCredentialsBegin => {
|
||||||
|
Some(process_enumerate_credentials_begin(
|
||||||
|
persistent_store,
|
||||||
|
stateful_command_permission,
|
||||||
|
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,
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
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(&[0u8; 16], 4).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.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.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(&[0u8; 16], 4).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.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.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.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_rps_completeness() {
|
||||||
|
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;
|
||||||
|
|
||||||
|
const NUM_CREDENTIALS: usize = 20;
|
||||||
|
for i in 0..NUM_CREDENTIALS {
|
||||||
|
let mut credential = credential_source.clone();
|
||||||
|
credential.rp_id = i.to_string();
|
||||||
|
ctap_state
|
||||||
|
.persistent_store
|
||||||
|
.store_credential(credential)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap();
|
||||||
|
let pin_auth = Some(vec![
|
||||||
|
0x1A, 0xA4, 0x96, 0xDA, 0x62, 0x80, 0x28, 0x13, 0xEB, 0x32, 0xB9, 0xF1, 0xD2, 0xA9,
|
||||||
|
0xD0, 0xD1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let mut rp_set = BTreeSet::new();
|
||||||
|
// This mut is just to make the test code shorter.
|
||||||
|
// The command is different on the first loop iteration.
|
||||||
|
let mut cred_management_params = AuthenticatorCredentialManagementParameters {
|
||||||
|
sub_command: CredentialManagementSubCommand::EnumerateRpsBegin,
|
||||||
|
sub_command_params: None,
|
||||||
|
pin_protocol: Some(1),
|
||||||
|
pin_auth,
|
||||||
|
};
|
||||||
|
|
||||||
|
for _ in 0..NUM_CREDENTIALS {
|
||||||
|
let cred_management_response = process_credential_management(
|
||||||
|
&mut ctap_state.persistent_store,
|
||||||
|
&mut ctap_state.stateful_command_permission,
|
||||||
|
&mut ctap_state.pin_protocol_v1,
|
||||||
|
cred_management_params,
|
||||||
|
DUMMY_CLOCK_VALUE,
|
||||||
|
);
|
||||||
|
match cred_management_response.unwrap() {
|
||||||
|
ResponseData::AuthenticatorCredentialManagement(Some(response)) => {
|
||||||
|
if rp_set.is_empty() {
|
||||||
|
assert_eq!(response.total_rps, Some(NUM_CREDENTIALS as u64));
|
||||||
|
} else {
|
||||||
|
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());
|
||||||
|
assert!(!rp_set.contains(&rp_id));
|
||||||
|
rp_set.insert(rp_id);
|
||||||
|
}
|
||||||
|
_ => panic!("Invalid response type"),
|
||||||
|
};
|
||||||
|
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.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(&[0u8; 16], 4).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.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.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.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(&[0u8; 16], 4).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.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.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(&[0u8; 16], 4).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.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(&[0u8; 16], 4).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.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(&[0u8; 16], 4).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.pin_protocol_v1,
|
||||||
|
cred_management_params,
|
||||||
|
DUMMY_CLOCK_VALUE,
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cred_management_response,
|
||||||
|
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>,
|
||||||
@@ -892,6 +906,88 @@ impl From<SetMinPinLengthParams> for cbor::Value {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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),
|
||||||
@@ -1660,6 +1756,52 @@ mod test {
|
|||||||
assert_eq!(cbor::Value::from(config_sub_command_params), cbor_params);
|
assert_eq!(cbor::Value::from(config_sub_command_params), cbor_params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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 {};
|
||||||
|
|||||||
244
src/ctap/mod.rs
244
src/ctap/mod.rs
@@ -15,6 +15,7 @@
|
|||||||
pub mod apdu;
|
pub mod apdu;
|
||||||
pub mod command;
|
pub mod command;
|
||||||
mod config_command;
|
mod config_command;
|
||||||
|
mod credential_management;
|
||||||
#[cfg(feature = "with_ctap1")]
|
#[cfg(feature = "with_ctap1")]
|
||||||
mod ctap1;
|
mod ctap1;
|
||||||
pub mod data_formats;
|
pub mod data_formats;
|
||||||
@@ -32,6 +33,7 @@ use self::command::{
|
|||||||
MAX_CREDENTIAL_COUNT_IN_LIST,
|
MAX_CREDENTIAL_COUNT_IN_LIST,
|
||||||
};
|
};
|
||||||
use self::config_command::process_config;
|
use self::config_command::process_config;
|
||||||
|
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,
|
||||||
@@ -99,6 +101,7 @@ const ED_FLAG: u8 = 0x80;
|
|||||||
pub const TOUCH_TIMEOUT_MS: isize = 30000;
|
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);
|
||||||
|
// TODO(kaczmarczyck) 2.1 allows Reset after Reset and 15 seconds?
|
||||||
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);
|
const STATEFUL_COMMAND_TIMEOUT_DURATION: Duration<isize> = Duration::from_ms(30000);
|
||||||
|
|
||||||
@@ -146,23 +149,131 @@ fn truncate_to_char_boundary(s: &str, mut max: usize) -> &str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Holds data necessary to sign an assertion for a credential.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct AssertionInput {
|
pub struct AssertionInput {
|
||||||
client_data_hash: Vec<u8>,
|
client_data_hash: Vec<u8>,
|
||||||
auth_data: Vec<u8>,
|
auth_data: Vec<u8>,
|
||||||
hmac_secret_input: Option<GetAssertionHmacSecretInput>,
|
hmac_secret_input: Option<GetAssertionHmacSecretInput>,
|
||||||
has_uv: bool,
|
has_uv: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AssertionState {
|
/// Contains the state we need to store for GetNextAssertion.
|
||||||
|
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 {
|
/// Stores which command currently holds state for subsequent calls.
|
||||||
|
pub enum StatefulCommand {
|
||||||
Reset,
|
Reset,
|
||||||
GetAssertion(AssertionState),
|
GetAssertion(AssertionState),
|
||||||
|
EnumerateRps(usize),
|
||||||
|
EnumerateCredentials(Vec<usize>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stores the current CTAP command state and when it times out.
|
||||||
|
///
|
||||||
|
/// Some commands are executed in a series of calls to the authenticator.
|
||||||
|
/// Interleaving calls to other commands interrupt the current command and
|
||||||
|
/// remove all state and permissions. Power cycling allows the Reset command,
|
||||||
|
/// and to prevent misuse or accidents, we disallow Reset after receiving
|
||||||
|
/// different commands. Therefore, Reset behaves just like all other stateful
|
||||||
|
/// commands and is included here. Please note that the allowed time for Reset
|
||||||
|
/// differs from all other stateful commands.
|
||||||
|
pub struct StatefulPermission {
|
||||||
|
permission: TimedPermission,
|
||||||
|
command_type: Option<StatefulCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatefulPermission {
|
||||||
|
/// Creates the command state at device startup.
|
||||||
|
///
|
||||||
|
/// Resets are only possible after a power cycle. Therefore, initialization
|
||||||
|
/// means allowing Reset, and Reset cannot be granted later.
|
||||||
|
pub fn new_reset(now: ClockValue) -> StatefulPermission {
|
||||||
|
StatefulPermission {
|
||||||
|
permission: TimedPermission::granted(now, RESET_TIMEOUT_DURATION),
|
||||||
|
command_type: Some(StatefulCommand::Reset),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears all permissions and state.
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.permission = TimedPermission::waiting();
|
||||||
|
self.command_type = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks the permission timeout.
|
||||||
|
pub fn check_command_permission(&mut self, now: ClockValue) -> Result<(), Ctap2StatusCode> {
|
||||||
|
if self.permission.is_granted(now) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
self.clear();
|
||||||
|
Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a reference to the current command state, if any exists.
|
||||||
|
pub fn get_command(&self) -> Result<&StatefulCommand, Ctap2StatusCode> {
|
||||||
|
self.command_type
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a new command state, and starts a new clock for timeouts.
|
||||||
|
pub fn set_command(&mut self, now: ClockValue, new_command_type: StatefulCommand) {
|
||||||
|
match &new_command_type {
|
||||||
|
// Reset is only allowed after a power cycle.
|
||||||
|
StatefulCommand::Reset => unreachable!(),
|
||||||
|
_ => {
|
||||||
|
self.permission = TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION);
|
||||||
|
self.command_type = Some(new_command_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the state for the next assertion and advances it.
|
||||||
|
///
|
||||||
|
/// The state includes all information from GetAssertion and the storage key
|
||||||
|
/// to the next credential that needs to be processed.
|
||||||
|
pub fn next_assertion_credential(
|
||||||
|
&mut self,
|
||||||
|
) -> Result<(AssertionInput, usize), Ctap2StatusCode> {
|
||||||
|
if let Some(StatefulCommand::GetAssertion(assertion_state)) = &mut self.command_type {
|
||||||
|
let credential_key = assertion_state
|
||||||
|
.next_credential_keys
|
||||||
|
.pop()
|
||||||
|
.ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?;
|
||||||
|
Ok((assertion_state.assertion_input.clone(), credential_key))
|
||||||
|
} else {
|
||||||
|
Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the index to the next RP ID for enumeration and advances it.
|
||||||
|
pub fn next_enumerate_rp(&mut self) -> Result<usize, Ctap2StatusCode> {
|
||||||
|
if let Some(StatefulCommand::EnumerateRps(rp_id_index)) = &mut self.command_type {
|
||||||
|
let current_index = *rp_id_index;
|
||||||
|
*rp_id_index += 1;
|
||||||
|
Ok(current_index)
|
||||||
|
} else {
|
||||||
|
Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the next storage credential key for enumeration and advances it.
|
||||||
|
pub fn next_enumerate_credential(&mut self) -> Result<usize, Ctap2StatusCode> {
|
||||||
|
if let Some(StatefulCommand::EnumerateCredentials(rp_credentials)) = &mut self.command_type
|
||||||
|
{
|
||||||
|
rp_credentials
|
||||||
|
.pop()
|
||||||
|
.ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)
|
||||||
|
} 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
|
||||||
@@ -178,8 +289,7 @@ pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<(
|
|||||||
#[cfg(feature = "with_ctap1")]
|
#[cfg(feature = "with_ctap1")]
|
||||||
pub u2f_up_state: U2fUserPresenceState,
|
pub u2f_up_state: U2fUserPresenceState,
|
||||||
// The state initializes to Reset and its timeout, and never goes back to Reset.
|
// The state initializes to Reset and its timeout, and never goes back to Reset.
|
||||||
stateful_command_permission: TimedPermission,
|
stateful_command_permission: StatefulPermission,
|
||||||
stateful_command_type: Option<StatefulCommand>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, R, CheckUserPresence> CtapState<'a, R, CheckUserPresence>
|
impl<'a, R, CheckUserPresence> CtapState<'a, R, CheckUserPresence>
|
||||||
@@ -204,22 +314,15 @@ where
|
|||||||
U2F_UP_PROMPT_TIMEOUT,
|
U2F_UP_PROMPT_TIMEOUT,
|
||||||
Duration::from_ms(TOUCH_TIMEOUT_MS),
|
Duration::from_ms(TOUCH_TIMEOUT_MS),
|
||||||
),
|
),
|
||||||
stateful_command_permission: TimedPermission::granted(now, RESET_TIMEOUT_DURATION),
|
stateful_command_permission: StatefulPermission::new_reset(now),
|
||||||
stateful_command_type: Some(StatefulCommand::Reset),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_command_permission(&mut self, now: ClockValue) {
|
pub fn update_command_permission(&mut self, now: ClockValue) {
|
||||||
self.stateful_command_permission = self.stateful_command_permission.check_expiration(now);
|
// Ignore the result, just update.
|
||||||
}
|
let _ = self
|
||||||
|
.stateful_command_permission
|
||||||
fn check_command_permission(&mut self, now: ClockValue) -> Result<(), Ctap2StatusCode> {
|
.check_command_permission(now);
|
||||||
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> {
|
||||||
@@ -338,19 +441,23 @@ where
|
|||||||
Duration::from_ms(TOUCH_TIMEOUT_MS),
|
Duration::from_ms(TOUCH_TIMEOUT_MS),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
match (&command, &self.stateful_command_type) {
|
match (&command, self.stateful_command_permission.get_command()) {
|
||||||
(
|
(Command::AuthenticatorGetNextAssertion, Ok(StatefulCommand::GetAssertion(_)))
|
||||||
Command::AuthenticatorGetNextAssertion,
|
| (Command::AuthenticatorReset, Ok(StatefulCommand::Reset))
|
||||||
Some(StatefulCommand::GetAssertion(_)),
|
// AuthenticatorGetInfo still allows Reset.
|
||||||
|
| (Command::AuthenticatorGetInfo, Ok(StatefulCommand::Reset))
|
||||||
|
// AuthenticatorSelection still allows Reset.
|
||||||
|
| (Command::AuthenticatorSelection, Ok(StatefulCommand::Reset))
|
||||||
|
// AuthenticatorCredentialManagement handles its subcommands later.
|
||||||
|
| (
|
||||||
|
Command::AuthenticatorCredentialManagement(_),
|
||||||
|
Ok(StatefulCommand::EnumerateRps(_)),
|
||||||
|
)
|
||||||
|
| (
|
||||||
|
Command::AuthenticatorCredentialManagement(_),
|
||||||
|
Ok(StatefulCommand::EnumerateCredentials(_)),
|
||||||
) => (),
|
) => (),
|
||||||
(Command::AuthenticatorReset, Some(StatefulCommand::Reset)) => (),
|
(_, _) => self.stateful_command_permission.clear(),
|
||||||
// GetInfo does not reset stateful commands.
|
|
||||||
(Command::AuthenticatorGetInfo, _) => (),
|
|
||||||
// AuthenticatorSelection does not reset stateful commands.
|
|
||||||
(Command::AuthenticatorSelection, _) => (),
|
|
||||||
(_, _) => {
|
|
||||||
self.stateful_command_type = None;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let response = match command {
|
let response = match command {
|
||||||
Command::AuthenticatorMakeCredential(params) => {
|
Command::AuthenticatorMakeCredential(params) => {
|
||||||
@@ -363,6 +470,15 @@ 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.pin_protocol_v1,
|
||||||
|
params,
|
||||||
|
now,
|
||||||
|
)
|
||||||
|
}
|
||||||
Command::AuthenticatorSelection => self.process_selection(cid),
|
Command::AuthenticatorSelection => self.process_selection(cid),
|
||||||
Command::AuthenticatorConfig(params) => process_config(
|
Command::AuthenticatorConfig(params) => process_config(
|
||||||
&mut self.persistent_store,
|
&mut self.persistent_store,
|
||||||
@@ -830,12 +946,12 @@ where
|
|||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
let number_of_credentials = Some(next_credential_keys.len() + 1);
|
let number_of_credentials = Some(next_credential_keys.len() + 1);
|
||||||
self.stateful_command_permission =
|
let assertion_state = StatefulCommand::GetAssertion(AssertionState {
|
||||||
TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION);
|
|
||||||
self.stateful_command_type = Some(StatefulCommand::GetAssertion(AssertionState {
|
|
||||||
assertion_input: assertion_input.clone(),
|
assertion_input: assertion_input.clone(),
|
||||||
next_credential_keys,
|
next_credential_keys,
|
||||||
}));
|
});
|
||||||
|
self.stateful_command_permission
|
||||||
|
.set_command(now, assertion_state);
|
||||||
number_of_credentials
|
number_of_credentials
|
||||||
};
|
};
|
||||||
self.assertion_response(credential, assertion_input, number_of_credentials)
|
self.assertion_response(credential, assertion_input, number_of_credentials)
|
||||||
@@ -845,20 +961,12 @@ where
|
|||||||
&mut self,
|
&mut self,
|
||||||
now: ClockValue,
|
now: ClockValue,
|
||||||
) -> Result<ResponseData, Ctap2StatusCode> {
|
) -> Result<ResponseData, Ctap2StatusCode> {
|
||||||
self.check_command_permission(now)?;
|
self.stateful_command_permission
|
||||||
let (assertion_input, credential) =
|
.check_command_permission(now)?;
|
||||||
if let Some(StatefulCommand::GetAssertion(assertion_state)) =
|
let (assertion_input, credential_key) = self
|
||||||
&mut self.stateful_command_type
|
.stateful_command_permission
|
||||||
{
|
.next_assertion_credential()?;
|
||||||
let credential_key = assertion_state
|
|
||||||
.next_credential_keys
|
|
||||||
.pop()
|
|
||||||
.ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?;
|
|
||||||
let credential = self.persistent_store.get_credential(credential_key)?;
|
let credential = self.persistent_store.get_credential(credential_key)?;
|
||||||
(assertion_state.assertion_input.clone(), credential)
|
|
||||||
} else {
|
|
||||||
return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED);
|
|
||||||
};
|
|
||||||
self.assertion_response(credential, assertion_input, None)
|
self.assertion_response(credential, assertion_input, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -871,6 +979,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);
|
||||||
options_map.insert(String::from("setMinPINLength"), true);
|
options_map.insert(String::from("setMinPINLength"), true);
|
||||||
Ok(ResponseData::AuthenticatorGetInfo(
|
Ok(ResponseData::AuthenticatorGetInfo(
|
||||||
AuthenticatorGetInfoResponse {
|
AuthenticatorGetInfoResponse {
|
||||||
@@ -923,11 +1032,10 @@ where
|
|||||||
cid: ChannelID,
|
cid: ChannelID,
|
||||||
now: ClockValue,
|
now: ClockValue,
|
||||||
) -> Result<ResponseData, Ctap2StatusCode> {
|
) -> Result<ResponseData, Ctap2StatusCode> {
|
||||||
// Resets are only possible in the first 10 seconds after booting.
|
self.stateful_command_permission
|
||||||
// TODO(kaczmarczyck) 2.1 allows Reset after Reset and 15 seconds?
|
.check_command_permission(now)?;
|
||||||
self.check_command_permission(now)?;
|
match self.stateful_command_permission.get_command()? {
|
||||||
match &self.stateful_command_type {
|
StatefulCommand::Reset => (),
|
||||||
Some(StatefulCommand::Reset) => (),
|
|
||||||
_ => return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED),
|
_ => return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED),
|
||||||
}
|
}
|
||||||
(self.check_user_presence)(cid)?;
|
(self.check_user_presence)(cid)?;
|
||||||
@@ -1123,12 +1231,13 @@ 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, 0xA4, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x69, 0x63, 0x6C, 0x69,
|
0x04, 0xA5, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x68, 0x63, 0x72, 0x65,
|
||||||
0x65, 0x6E, 0x74, 0x50, 0x69, 0x6E, 0xF4, 0x6F, 0x73, 0x65, 0x74, 0x4D, 0x69, 0x6E,
|
0x64, 0x4D, 0x67, 0x6D, 0x74, 0xF5, 0x69, 0x63, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x50,
|
||||||
0x50, 0x49, 0x4E, 0x4C, 0x65, 0x6E, 0x67, 0x74, 0x68, 0xF5, 0x05, 0x19, 0x04, 0x00,
|
0x69, 0x6E, 0xF4, 0x6F, 0x73, 0x65, 0x74, 0x4D, 0x69, 0x6E, 0x50, 0x49, 0x4E, 0x4C,
|
||||||
0x06, 0x81, 0x01, 0x08, 0x18, 0x70, 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81,
|
0x65, 0x6E, 0x67, 0x74, 0x68, 0xF5, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, 0x01, 0x08,
|
||||||
0xA2, 0x63, 0x61, 0x6C, 0x67, 0x26, 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75,
|
0x18, 0x70, 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, 0x6C,
|
||||||
0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04, 0x14, 0x18, 0x96,
|
0x67, 0x26, 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63,
|
||||||
|
0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04, 0x14, 0x18, 0x96,
|
||||||
]
|
]
|
||||||
.iter(),
|
.iter(),
|
||||||
);
|
);
|
||||||
@@ -2080,6 +2189,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 {};
|
||||||
@@ -2087,10 +2212,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]
|
||||||
|
|||||||
@@ -506,6 +506,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 {
|
||||||
@@ -1092,6 +1099,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 {};
|
||||||
|
|||||||
@@ -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,
|
||||||
// TODO(kaczmarczyck) dummy, extend
|
// TODO(kaczmarczyck) dummy, extend
|
||||||
AuthenticatorConfig,
|
AuthenticatorConfig,
|
||||||
@@ -43,9 +45,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::AuthenticatorConfig => None,
|
ResponseData::AuthenticatorConfig => None,
|
||||||
ResponseData::AuthenticatorVendor(data) => Some(data.into()),
|
ResponseData::AuthenticatorVendor(data) => Some(data.into()),
|
||||||
@@ -202,6 +204,55 @@ impl From<AuthenticatorClientPinResponse> for cbor::Value {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
#[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 {
|
||||||
@@ -225,10 +276,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() {
|
||||||
@@ -382,6 +434,76 @@ 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::default();
|
||||||
|
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();
|
||||||
|
|||||||
@@ -242,7 +242,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)?)
|
||||||
}
|
}
|
||||||
@@ -252,7 +252,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,
|
||||||
@@ -719,7 +719,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);
|
||||||
}
|
}
|
||||||
@@ -737,7 +737,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)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -752,7 +752,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)
|
||||||
|
|||||||
Reference in New Issue
Block a user