Merge branch 'master' into aaguid

This commit is contained in:
Julien Cretin
2020-06-09 11:56:05 +02:00
8 changed files with 735 additions and 435 deletions

View File

@@ -99,6 +99,10 @@ a few things you can personalize:
4. Depending on your available flash storage, choose an appropriate maximum
number of supported residential keys and number of pages in
`ctap/storage.rs`.
5. Change the default level for the credProtect extension in `ctap/mod.rs`.
When changing the default, resident credentials become undiscoverable without
user verification. This helps privacy, but can make usage less comfortable
for credentials that need less protection.
### 3D printed enclosure

View File

@@ -1,9 +1,9 @@
1003863864e06553e730eec6df4bf8d30c99f697ef9380efdc35eba679b4db78 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840dk.bin
7ffc52ea6bfd1c3fde3398da4e894b5659770a74b466e052b4c3999436f9d78e target/nrf52840dk_merged.hex
022268c93fa8bbd9e54e082982b87c10a0e7c0486704de8219d1bb374304636a target/nrf52840dk_merged.hex
88f00a5e1dae6ab3f7571c254ac75f5f3e29ebea7f3ca46c16cfdc3708e804fc third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle.bin
a0cd9144582b616a51d4f097713cbd697d418c19d031906f58fc630d7286ed85 target/nrf52840_dongle_merged.hex
8d68ecc700527789b8edf318f0872ca8fc3b72fa73236f4e06bec89a3682fcf8 target/nrf52840_dongle_merged.hex
1bc69b48a2c48da55db8b322902e1fe3f2e095c0dd8517db28837d86e0addc85 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle_dfu.bin
5879d90971a7429e8890ce4a5db694499f391ffd7c6707c6820538ee8126ff5f target/nrf52840_dongle_dfu_merged.hex
af5465e4209914aaf74ee878d03e883a717827119e47b9295aa279ee21f0c5f4 target/nrf52840_dongle_dfu_merged.hex
f38ee31d3a09e7e11848e78b5318f95517b6dcd076afcb37e6e3d3e5e9995cc7 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_mdk_dfu.bin
a4e7451174ee75a27acfb9bdd3c977f5cf3e756e40219b706c97eab3a21c7ac0 target/nrf52840_mdk_dfu_merged.hex
f364a923a4c56b5bbba8b590c8c296b29f4448f3117cedf433d4752867fac6ef target/tab/ctap2.tab
23603386a615e4e8cb2173c5ce4762110e6cbb979efdbb6e8bef9bc1e3988de4 target/nrf52840_mdk_dfu_merged.hex
c2cbcc28b835934be4c3d3e3c5bdaba642a5811d760c1d2cb73d26b6474e4219 target/tab/ctap2.tab

View File

@@ -1,9 +1,9 @@
c182bb4902fff51b2f56810fc2a27df3646cd66ba21359162354d53445623ab8 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840dk.bin
9624888f52510e1e7a13681a959ecb9dd0e325b3856422b48d28abadc6546211 target/nrf52840dk_merged.hex
d8b62ece387a77cc21f2c10a5f5d65d0d57bf4739b47fd86d2c9ecdd90fbfd7e target/nrf52840dk_merged.hex
0a9929ba8fa57e8a502a49fc7c53177397202e1b11f4c7c3cb6ed68b2b99dd46 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle.bin
31b41cc1010c621765a4385ecd678950ddb2e1eaa11e0efaa9df818a1abfd022 target/nrf52840_dongle_merged.hex
380de1a910b4d9eeb0c814b11b074b2e66334968cc99a4bd34d52a1fce3c5a79 target/nrf52840_dongle_merged.hex
cca9086c9149c607589b23ffa599a5e4c26db7c20bd3700b79528bd3a5df991d third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle_dfu.bin
0a9c92d56b02b42c7d783606f7711c474fc73518a32b9c7e244c078011a67e6d target/nrf52840_dongle_dfu_merged.hex
4edd988b3e37991f1e58fc520e41f7666f8ae3e8d3993e1bb2fb71657a71fa50 target/nrf52840_dongle_dfu_merged.hex
8857488ba6a69e366f0da229bbfc012a2ad291d3a88d9494247d600c10bb19b7 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_mdk_dfu.bin
a5fb5ebcf475f88be0273a4679975bcfee72014102a6191370a80120ca287f11 target/nrf52840_mdk_dfu_merged.hex
7940a87663cf40941ea8c50ad1d99abf2ccbcacfcd157c1b0449dd3ed78e180e target/tab/ctap2.tab
a51aba1cd12e55aa33fd9017af406583ebf14e1c690295b15cf147713dfe2561 target/nrf52840_mdk_dfu_merged.hex
40b413a8b645b4b47fae62a4311acb12cb0c57faff2757e45c18d9e5d441e52d target/tab/ctap2.tab

View File

@@ -13,10 +13,10 @@
// limitations under the License.
use super::data_formats::{
ok_or_missing, read_array, read_byte_string, read_map, read_text_string, read_unsigned,
ClientPinSubCommand, CoseKey, Extensions, GetAssertionOptions, MakeCredentialOptions,
PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity,
PublicKeyCredentialUserEntity,
extract_array, extract_byte_string, extract_map, extract_text_string, extract_unsigned,
ok_or_missing, ClientPinSubCommand, CoseKey, GetAssertionExtensions, GetAssertionOptions,
MakeCredentialExtensions, MakeCredentialOptions, PublicKeyCredentialDescriptor,
PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity,
};
use super::status_code::Ctap2StatusCode;
use alloc::string::String;
@@ -113,7 +113,7 @@ pub struct AuthenticatorMakeCredentialParameters {
pub user: PublicKeyCredentialUserEntity,
pub pub_key_cred_params: Vec<PublicKeyCredentialParameter>,
pub exclude_list: Option<Vec<PublicKeyCredentialDescriptor>>,
pub extensions: Option<Extensions>,
pub extensions: Option<MakeCredentialExtensions>,
// Even though options are optional, we can use the default if not present.
pub options: MakeCredentialOptions,
pub pin_uv_auth_param: Option<Vec<u8>>,
@@ -124,30 +124,32 @@ impl TryFrom<cbor::Value> for AuthenticatorMakeCredentialParameters {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
let param_map = read_map(&cbor_value)?;
let mut param_map = extract_map(cbor_value)?;
let client_data_hash = read_byte_string(ok_or_missing(param_map.get(&cbor_unsigned!(1)))?)?;
let client_data_hash =
extract_byte_string(ok_or_missing(param_map.remove(&cbor_unsigned!(1)))?)?;
let rp = PublicKeyCredentialRpEntity::try_from(ok_or_missing(
param_map.get(&cbor_unsigned!(2)),
param_map.remove(&cbor_unsigned!(2)),
)?)?;
let user = PublicKeyCredentialUserEntity::try_from(ok_or_missing(
param_map.get(&cbor_unsigned!(3)),
param_map.remove(&cbor_unsigned!(3)),
)?)?;
let cred_param_vec = read_array(ok_or_missing(param_map.get(&cbor_unsigned!(4)))?)?;
let cred_param_vec = extract_array(ok_or_missing(param_map.remove(&cbor_unsigned!(4)))?)?;
let pub_key_cred_params = cred_param_vec
.iter()
.into_iter()
.map(PublicKeyCredentialParameter::try_from)
.collect::<Result<Vec<PublicKeyCredentialParameter>, Ctap2StatusCode>>()?;
let exclude_list = match param_map.get(&cbor_unsigned!(5)) {
let exclude_list = match param_map.remove(&cbor_unsigned!(5)) {
Some(entry) => {
let exclude_list_vec = read_array(entry)?;
let exclude_list_vec = extract_array(entry)?;
let list_len = MAX_CREDENTIAL_COUNT_IN_LIST.unwrap_or(exclude_list_vec.len());
let exclude_list = exclude_list_vec
.iter()
.take(MAX_CREDENTIAL_COUNT_IN_LIST.unwrap_or(exclude_list_vec.len()))
.into_iter()
.take(list_len)
.map(PublicKeyCredentialDescriptor::try_from)
.collect::<Result<Vec<PublicKeyCredentialDescriptor>, Ctap2StatusCode>>()?;
Some(exclude_list)
@@ -156,11 +158,11 @@ impl TryFrom<cbor::Value> for AuthenticatorMakeCredentialParameters {
};
let extensions = param_map
.get(&cbor_unsigned!(6))
.map(Extensions::try_from)
.remove(&cbor_unsigned!(6))
.map(MakeCredentialExtensions::try_from)
.transpose()?;
let options = match param_map.get(&cbor_unsigned!(7)) {
let options = match param_map.remove(&cbor_unsigned!(7)) {
Some(entry) => MakeCredentialOptions::try_from(entry)?,
None => MakeCredentialOptions {
rk: false,
@@ -169,13 +171,13 @@ impl TryFrom<cbor::Value> for AuthenticatorMakeCredentialParameters {
};
let pin_uv_auth_param = param_map
.get(&cbor_unsigned!(8))
.map(read_byte_string)
.remove(&cbor_unsigned!(8))
.map(extract_byte_string)
.transpose()?;
let pin_uv_auth_protocol = param_map
.get(&cbor_unsigned!(9))
.map(read_unsigned)
.remove(&cbor_unsigned!(9))
.map(extract_unsigned)
.transpose()?;
Ok(AuthenticatorMakeCredentialParameters {
@@ -197,7 +199,7 @@ pub struct AuthenticatorGetAssertionParameters {
pub rp_id: String,
pub client_data_hash: Vec<u8>,
pub allow_list: Option<Vec<PublicKeyCredentialDescriptor>>,
pub extensions: Option<Extensions>,
pub extensions: Option<GetAssertionExtensions>,
// Even though options are optional, we can use the default if not present.
pub options: GetAssertionOptions,
pub pin_uv_auth_param: Option<Vec<u8>>,
@@ -208,18 +210,20 @@ impl TryFrom<cbor::Value> for AuthenticatorGetAssertionParameters {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
let param_map = read_map(&cbor_value)?;
let mut param_map = extract_map(cbor_value)?;
let rp_id = read_text_string(ok_or_missing(param_map.get(&cbor_unsigned!(1)))?)?;
let rp_id = extract_text_string(ok_or_missing(param_map.remove(&cbor_unsigned!(1)))?)?;
let client_data_hash = read_byte_string(ok_or_missing(param_map.get(&cbor_unsigned!(2)))?)?;
let client_data_hash =
extract_byte_string(ok_or_missing(param_map.remove(&cbor_unsigned!(2)))?)?;
let allow_list = match param_map.get(&cbor_unsigned!(3)) {
let allow_list = match param_map.remove(&cbor_unsigned!(3)) {
Some(entry) => {
let allow_list_vec = read_array(entry)?;
let allow_list_vec = extract_array(entry)?;
let list_len = MAX_CREDENTIAL_COUNT_IN_LIST.unwrap_or(allow_list_vec.len());
let allow_list = allow_list_vec
.iter()
.take(MAX_CREDENTIAL_COUNT_IN_LIST.unwrap_or(allow_list_vec.len()))
.into_iter()
.take(list_len)
.map(PublicKeyCredentialDescriptor::try_from)
.collect::<Result<Vec<PublicKeyCredentialDescriptor>, Ctap2StatusCode>>()?;
Some(allow_list)
@@ -228,11 +232,11 @@ impl TryFrom<cbor::Value> for AuthenticatorGetAssertionParameters {
};
let extensions = param_map
.get(&cbor_unsigned!(4))
.map(Extensions::try_from)
.remove(&cbor_unsigned!(4))
.map(GetAssertionExtensions::try_from)
.transpose()?;
let options = match param_map.get(&cbor_unsigned!(5)) {
let options = match param_map.remove(&cbor_unsigned!(5)) {
Some(entry) => GetAssertionOptions::try_from(entry)?,
None => GetAssertionOptions {
up: true,
@@ -241,13 +245,13 @@ impl TryFrom<cbor::Value> for AuthenticatorGetAssertionParameters {
};
let pin_uv_auth_param = param_map
.get(&cbor_unsigned!(6))
.map(read_byte_string)
.remove(&cbor_unsigned!(6))
.map(extract_byte_string)
.transpose()?;
let pin_uv_auth_protocol = param_map
.get(&cbor_unsigned!(7))
.map(read_unsigned)
.remove(&cbor_unsigned!(7))
.map(extract_unsigned)
.transpose()?;
Ok(AuthenticatorGetAssertionParameters {
@@ -276,32 +280,32 @@ impl TryFrom<cbor::Value> for AuthenticatorClientPinParameters {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
let param_map = read_map(&cbor_value)?;
let mut param_map = extract_map(cbor_value)?;
let pin_protocol = read_unsigned(ok_or_missing(param_map.get(&cbor_unsigned!(1)))?)?;
let pin_protocol = extract_unsigned(ok_or_missing(param_map.remove(&cbor_unsigned!(1)))?)?;
let sub_command =
ClientPinSubCommand::try_from(ok_or_missing(param_map.get(&cbor_unsigned!(2)))?)?;
ClientPinSubCommand::try_from(ok_or_missing(param_map.remove(&cbor_unsigned!(2)))?)?;
let key_agreement = param_map
.get(&cbor_unsigned!(3))
.map(read_map)
.remove(&cbor_unsigned!(3))
.map(extract_map)
.transpose()?
.map(|x| CoseKey(x.clone()));
.map(|x| CoseKey(x));
let pin_auth = param_map
.get(&cbor_unsigned!(4))
.map(read_byte_string)
.remove(&cbor_unsigned!(4))
.map(extract_byte_string)
.transpose()?;
let new_pin_enc = param_map
.get(&cbor_unsigned!(5))
.map(read_byte_string)
.remove(&cbor_unsigned!(5))
.map(extract_byte_string)
.transpose()?;
let pin_hash_enc = param_map
.get(&cbor_unsigned!(6))
.map(read_byte_string)
.remove(&cbor_unsigned!(6))
.map(extract_byte_string)
.transpose()?;
Ok(AuthenticatorClientPinParameters {

File diff suppressed because it is too large Load Diff

View File

@@ -32,9 +32,10 @@ use self::command::{
#[cfg(feature = "with_ctap2_1")]
use self::data_formats::AuthenticatorTransport;
use self::data_formats::{
ClientPinSubCommand, CoseKey, GetAssertionHmacSecretInput, PackedAttestationStatement,
PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialSource,
PublicKeyCredentialType, PublicKeyCredentialUserEntity, SignatureAlgorithm,
ClientPinSubCommand, CoseKey, CredentialProtectionPolicy, GetAssertionHmacSecretInput,
PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter,
PublicKeyCredentialSource, PublicKeyCredentialType, PublicKeyCredentialUserEntity,
SignatureAlgorithm,
};
use self::hid::ChannelID;
use self::response::{
@@ -108,6 +109,10 @@ pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa
cred_type: PublicKeyCredentialType::PublicKey,
alg: SignatureAlgorithm::ES256,
};
// You can change this value to one of the following for more privacy.
// - Some(CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList)
// - Some(CredentialProtectionPolicy::UserVerificationRequired)
const DEFAULT_CRED_PROTECT: Option<CredentialProtectionPolicy> = None;
fn check_pin_auth(hmac_key: &[u8], hmac_contents: &[u8], pin_auth: &[u8]) -> bool {
if pin_auth.len() != PIN_AUTH_LENGTH {
@@ -334,6 +339,7 @@ where
user_handle: vec![],
other_ui: None,
cred_random: None,
cred_protect_policy: None,
})
}
@@ -427,30 +433,42 @@ where
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM);
}
let use_hmac_extension =
extensions.map_or(Ok(false), |e| e.has_make_credential_hmac_secret())?;
if use_hmac_extension && !options.rk {
// The extension is actually supported, but we need resident keys.
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION);
}
let (use_hmac_extension, cred_protect_policy) = if let Some(extensions) = extensions {
let mut cred_protect = extensions.cred_protect;
if cred_protect.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional)
< DEFAULT_CRED_PROTECT
.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional)
{
cred_protect = DEFAULT_CRED_PROTECT;
}
(extensions.hmac_secret, cred_protect)
} else {
(false, None)
};
let cred_random = if use_hmac_extension {
if !options.rk {
// The extension is actually supported, but we need resident keys.
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION);
}
Some(self.rng.gen_uniform_u8x32().to_vec())
} else {
None
};
let ed_flag = if use_hmac_extension { ED_FLAG } else { 0 };
// TODO(kaczmarczyck) unsolicited output for default credProtect level
let has_extension_output = use_hmac_extension || cred_protect_policy.is_some();
let rp_id = rp.rp_id;
if let Some(exclude_list) = exclude_list {
for cred_desc in exclude_list {
if self
.persistent_store
.find_credential(&rp_id, &cred_desc.key_id)
.find_credential(&rp_id, &cred_desc.key_id, pin_uv_auth_param.is_none())
.is_some()
{
// Perform this check, so bad actors can't brute force exclude_list
// without user interaction. Discard the user presence check's outcome.
let _ = (self.check_user_presence)(cid);
// without user interaction.
(self.check_user_presence)(cid)?;
return Err(Ctap2StatusCode::CTAP2_ERR_CREDENTIAL_EXCLUDED);
}
}
@@ -458,6 +476,7 @@ where
// MakeCredential always requires user presence.
// User verification depends on the PIN auth inputs, which are checked here.
let ed_flag = if has_extension_output { ED_FLAG } else { 0 };
let flags = match pin_uv_auth_param {
Some(pin_auth) => {
if self.persistent_store.pin_hash().is_none() {
@@ -500,6 +519,7 @@ where
.user_display_name
.map(|s| truncate_to_char_boundary(&s, 64).to_string()),
cred_random,
cred_protect_policy,
};
self.persistent_store.store_credential(credential_source)?;
random_id
@@ -520,11 +540,13 @@ where
None => return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR),
};
auth_data.extend(cose_key);
if use_hmac_extension {
let extensions = cbor_map! {
"hmac-secret" => true,
if has_extension_output {
let hmac_secret_output = if use_hmac_extension { Some(true) } else { None };
let extensions_output = cbor_map_options! {
"hmac-secret" => hmac_secret_output,
"credProtect" => cred_protect_policy,
};
if !cbor::write(extensions, &mut auth_data) {
if !cbor::write(extensions_output, &mut auth_data) {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR);
}
}
@@ -621,17 +643,15 @@ where
}
}
let get_assertion_hmac_secret_input = match extensions {
Some(extensions) => extensions.get_assertion_hmac_secret().transpose()?,
None => None,
};
if get_assertion_hmac_secret_input.is_some() && !options.up {
let hmac_secret_input = extensions.map(|e| e.hmac_secret).flatten();
if hmac_secret_input.is_some() && !options.up {
// The extension is actually supported, but we need user presence.
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION);
}
// The user verification bit depends on the existance of PIN auth, whereas
// user presence is requested as an option.
// The user verification bit depends on the existance of PIN auth, since we do
// not support internal UV. User presence is requested as an option.
let has_uv = pin_uv_auth_param.is_some();
let mut flags = match pin_uv_auth_param {
Some(pin_auth) => {
if self.persistent_store.pin_hash().is_none() {
@@ -654,7 +674,7 @@ where
if options.up {
flags |= UP_FLAG;
}
if get_assertion_hmac_secret_input.is_some() {
if hmac_secret_input.is_some() {
flags |= ED_FLAG;
}
@@ -663,11 +683,14 @@ where
let credentials = if let Some(allow_list) = allow_list {
let mut found_credentials = vec![];
for allowed_credential in allow_list {
match self
.persistent_store
.find_credential(&rp_id, &allowed_credential.key_id)
{
Some(credential) => found_credentials.push(credential),
match self.persistent_store.find_credential(
&rp_id,
&allowed_credential.key_id,
!has_uv,
) {
Some(credential) => {
found_credentials.push(credential);
}
None => {
if decrypted_credential.is_none() {
decrypted_credential = self
@@ -679,7 +702,7 @@ where
found_credentials
} else {
// TODO(kaczmarczyck) use GetNextAssertion
self.persistent_store.filter_credential(&rp_id)
self.persistent_store.filter_credential(&rp_id, !has_uv)
};
let credential = if let Some(credential) = credentials.first() {
@@ -698,12 +721,12 @@ where
let mut auth_data = self.generate_auth_data(&rp_id_hash, flags);
// Process extensions.
if let Some(get_assertion_hmac_secret_input) = get_assertion_hmac_secret_input {
if let Some(hmac_secret_input) = hmac_secret_input {
let GetAssertionHmacSecretInput {
key_agreement,
salt_enc,
salt_auth,
} = get_assertion_hmac_secret_input;
} = hmac_secret_input;
let pk: crypto::ecdh::PubKey = CoseKey::try_into(key_agreement)?;
let shared_secret = self.key_agreement_key.exchange_x_sha256(&pk);
// HMAC-secret does the same 16 byte truncated check.
@@ -718,10 +741,10 @@ where
None => return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION),
};
let extensions = cbor_map! {
let extensions_output = cbor_map! {
"hmac-secret" => encrypted_output,
};
if !cbor::write(extensions, &mut auth_data) {
if !cbor::write(extensions_output, &mut auth_data) {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR);
}
}
@@ -791,6 +814,7 @@ where
transports: Some(vec![AuthenticatorTransport::Usb]),
#[cfg(feature = "with_ctap2_1")]
algorithms: Some(vec![ES256_CRED_PARAM]),
default_cred_protect: DEFAULT_CRED_PROTECT,
#[cfg(feature = "with_ctap2_1")]
firmware_version: None,
},
@@ -1095,8 +1119,8 @@ where
#[cfg(test)]
mod test {
use super::data_formats::{
Extensions, GetAssertionOptions, MakeCredentialOptions, PublicKeyCredentialRpEntity,
PublicKeyCredentialUserEntity,
GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions,
MakeCredentialOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity,
};
use super::*;
use crypto::rng256::ThreadRng256;
@@ -1179,6 +1203,32 @@ mod test {
}
}
fn create_make_credential_parameters_with_exclude_list(
excluded_credential_id: &[u8],
) -> AuthenticatorMakeCredentialParameters {
let excluded_credential_descriptor = PublicKeyCredentialDescriptor {
key_type: PublicKeyCredentialType::PublicKey,
key_id: excluded_credential_id.to_vec(),
transports: None,
};
let exclude_list = Some(vec![excluded_credential_descriptor]);
let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.exclude_list = exclude_list;
make_credential_params
}
fn create_make_credential_parameters_with_cred_protect_policy(
policy: CredentialProtectionPolicy,
) -> AuthenticatorMakeCredentialParameters {
let extensions = Some(MakeCredentialExtensions {
hmac_secret: false,
cred_protect: Some(policy),
});
let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.extensions = extensions;
make_credential_params
}
#[test]
fn test_residential_process_make_credential() {
let mut rng = ThreadRng256 {};
@@ -1277,46 +1327,93 @@ mod test {
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present);
let excluded_credential_id = vec![0x01, 0x23, 0x45, 0x67];
let make_credential_params =
create_make_credential_parameters_with_exclude_list(&excluded_credential_id);
let excluded_credential_source = PublicKeyCredentialSource {
key_type: PublicKeyCredentialType::PublicKey,
credential_id: excluded_credential_id.clone(),
credential_id: excluded_credential_id,
private_key: excluded_private_key,
rp_id: String::from("example.com"),
user_handle: vec![],
other_ui: None,
cred_random: None,
cred_protect_policy: None,
};
assert!(ctap_state
.persistent_store
.store_credential(excluded_credential_source)
.is_ok());
let excluded_credential_descriptor = PublicKeyCredentialDescriptor {
key_type: PublicKeyCredentialType::PublicKey,
key_id: excluded_credential_id,
transports: None,
};
let exclude_list = Some(vec![excluded_credential_descriptor]);
let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.exclude_list = exclude_list;
let make_credential_response =
ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID);
assert_eq!(
make_credential_response,
Err(Ctap2StatusCode::CTAP2_ERR_CREDENTIAL_EXCLUDED)
);
}
#[test]
fn test_process_make_credential_credential_with_cred_protect() {
let mut rng = ThreadRng256 {};
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present);
let test_policy = CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList;
let make_credential_params =
create_make_credential_parameters_with_cred_protect_policy(test_policy);
let make_credential_response =
ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID);
assert!(make_credential_response.is_ok());
let stored_credential = ctap_state
.persistent_store
.filter_credential("example.com", false)
.pop()
.unwrap();
let credential_id = stored_credential.credential_id;
assert_eq!(stored_credential.cred_protect_policy, Some(test_policy));
let make_credential_params =
create_make_credential_parameters_with_exclude_list(&credential_id);
let make_credential_response =
ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID);
assert_eq!(
make_credential_response,
Err(Ctap2StatusCode::CTAP2_ERR_CREDENTIAL_EXCLUDED)
);
let test_policy = CredentialProtectionPolicy::UserVerificationRequired;
let make_credential_params =
create_make_credential_parameters_with_cred_protect_policy(test_policy);
let make_credential_response =
ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID);
assert!(make_credential_response.is_ok());
let stored_credential = ctap_state
.persistent_store
.filter_credential("example.com", false)
.pop()
.unwrap();
let credential_id = stored_credential.credential_id;
assert_eq!(stored_credential.cred_protect_policy, Some(test_policy));
let make_credential_params =
create_make_credential_parameters_with_exclude_list(&credential_id);
let make_credential_response =
ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID);
assert!(make_credential_response.is_ok());
}
#[test]
fn test_process_make_credential_hmac_secret() {
let mut rng = ThreadRng256 {};
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present);
let mut extension_map = BTreeMap::new();
extension_map.insert("hmac-secret".to_string(), cbor_bool!(true));
let extensions = Some(Extensions::new(extension_map));
let extensions = Some(MakeCredentialExtensions {
hmac_secret: true,
cred_protect: None,
});
let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.extensions = extensions;
let make_credential_response =
@@ -1426,9 +1523,10 @@ mod test {
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present);
let mut extension_map = BTreeMap::new();
extension_map.insert("hmac-secret".to_string(), cbor_bool!(true));
let make_extensions = Some(Extensions::new(extension_map));
let make_extensions = Some(MakeCredentialExtensions {
hmac_secret: true,
cred_protect: None,
});
let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.extensions = make_extensions;
assert!(ctap_state
@@ -1436,15 +1534,15 @@ mod test {
.is_ok());
let pk = sk.genpk();
let hmac_secret_parameters = cbor_map! {
1 => cbor::Value::Map(CoseKey::from(pk).0),
2 => vec![0; 32],
3 => vec![0; 16],
let hmac_secret_input = GetAssertionHmacSecretInput {
key_agreement: CoseKey::from(pk),
salt_enc: vec![0x02; 32],
salt_auth: vec![0x03; 16],
};
let mut extension_map = BTreeMap::new();
extension_map.insert("hmac-secret".to_string(), hmac_secret_parameters);
let get_extensions = Some(GetAssertionExtensions {
hmac_secret: Some(hmac_secret_input),
});
let get_extensions = Some(Extensions::new(extension_map));
let get_assertion_params = AuthenticatorGetAssertionParameters {
rp_id: String::from("example.com"),
client_data_hash: vec![0xCD],
@@ -1466,6 +1564,106 @@ mod test {
);
}
#[test]
fn test_residential_process_get_assertion_with_cred_protect() {
let mut rng = ThreadRng256 {};
let private_key = crypto::ecdsa::SecKey::gensk(&mut rng);
let credential_id = rng.gen_uniform_u8x32().to_vec();
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present);
let cred_desc = PublicKeyCredentialDescriptor {
key_type: PublicKeyCredentialType::PublicKey,
key_id: credential_id.clone(),
transports: None, // You can set USB as a hint here.
};
let credential = PublicKeyCredentialSource {
key_type: PublicKeyCredentialType::PublicKey,
credential_id: credential_id.clone(),
private_key: private_key.clone(),
rp_id: String::from("example.com"),
user_handle: vec![0x00],
other_ui: None,
cred_random: None,
cred_protect_policy: Some(
CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList,
),
};
assert!(ctap_state
.persistent_store
.store_credential(credential)
.is_ok());
let get_assertion_params = AuthenticatorGetAssertionParameters {
rp_id: String::from("example.com"),
client_data_hash: vec![0xCD],
allow_list: None,
extensions: None,
options: GetAssertionOptions {
up: false,
uv: false,
},
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let get_assertion_response =
ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID);
assert_eq!(
get_assertion_response,
Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS),
);
let get_assertion_params = AuthenticatorGetAssertionParameters {
rp_id: String::from("example.com"),
client_data_hash: vec![0xCD],
allow_list: Some(vec![cred_desc.clone()]),
extensions: None,
options: GetAssertionOptions {
up: false,
uv: false,
},
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let get_assertion_response =
ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID);
assert!(get_assertion_response.is_ok());
let credential = PublicKeyCredentialSource {
key_type: PublicKeyCredentialType::PublicKey,
credential_id,
private_key,
rp_id: String::from("example.com"),
user_handle: vec![0x00],
other_ui: None,
cred_random: None,
cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationRequired),
};
assert!(ctap_state
.persistent_store
.store_credential(credential)
.is_ok());
let get_assertion_params = AuthenticatorGetAssertionParameters {
rp_id: String::from("example.com"),
client_data_hash: vec![0xCD],
allow_list: Some(vec![cred_desc]),
extensions: None,
options: GetAssertionOptions {
up: false,
uv: false,
},
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let get_assertion_response =
ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID);
assert_eq!(
get_assertion_response,
Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS),
);
}
#[test]
fn test_process_reset() {
let mut rng = ThreadRng256 {};
@@ -1482,6 +1680,7 @@ mod test {
user_handle: vec![],
other_ui: None,
cred_random: None,
cred_protect_policy: None,
};
assert!(ctap_state
.persistent_store

View File

@@ -15,7 +15,7 @@
#[cfg(feature = "with_ctap2_1")]
use super::data_formats::{AuthenticatorTransport, PublicKeyCredentialParameter};
use super::data_formats::{
CoseKey, PackedAttestationStatement, PublicKeyCredentialDescriptor,
CoseKey, CredentialProtectionPolicy, PackedAttestationStatement, PublicKeyCredentialDescriptor,
PublicKeyCredentialUserEntity,
};
use alloc::collections::BTreeMap;
@@ -119,6 +119,7 @@ pub struct AuthenticatorGetInfoResponse {
pub transports: Option<Vec<AuthenticatorTransport>>,
#[cfg(feature = "with_ctap2_1")]
pub algorithms: Option<Vec<PublicKeyCredentialParameter>>,
pub default_cred_protect: Option<CredentialProtectionPolicy>,
#[cfg(feature = "with_ctap2_1")]
pub firmware_version: Option<u64>,
}
@@ -137,6 +138,7 @@ impl From<AuthenticatorGetInfoResponse> for cbor::Value {
max_credential_id_length,
transports,
algorithms,
default_cred_protect,
firmware_version,
} = get_info_response;
@@ -159,6 +161,7 @@ impl From<AuthenticatorGetInfoResponse> for cbor::Value {
0x08 => max_credential_id_length,
0x09 => transports.map(|vec| cbor_array_vec!(vec)),
0x0A => algorithms.map(|vec| cbor_array_vec!(vec)),
0x0C => default_cred_protect.map(|p| p as u64),
0x0E => firmware_version,
}
}
@@ -172,6 +175,7 @@ impl From<AuthenticatorGetInfoResponse> for cbor::Value {
options,
max_msg_size,
pin_protocols,
default_cred_protect,
} = get_info_response;
let options_cbor: Option<cbor::Value> = options.map(|options| {
@@ -189,6 +193,7 @@ impl From<AuthenticatorGetInfoResponse> for cbor::Value {
0x04 => options_cbor,
0x05 => max_msg_size,
0x06 => pin_protocols.map(|vec| cbor_array_vec!(vec)),
0x0C => default_cred_protect.map(|p| p as u64),
}
}
}
@@ -290,6 +295,7 @@ mod test {
transports: None,
#[cfg(feature = "with_ctap2_1")]
algorithms: None,
default_cred_protect: None,
#[cfg(feature = "with_ctap2_1")]
firmware_version: None,
};
@@ -318,6 +324,7 @@ mod test {
max_credential_id_length: Some(256),
transports: Some(vec![AuthenticatorTransport::Usb]),
algorithms: Some(vec![ES256_CRED_PARAM]),
default_cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired),
firmware_version: Some(0),
};
let response_cbor: Option<cbor::Value> =
@@ -333,6 +340,7 @@ mod test {
0x08 => 256,
0x09 => cbor_array_vec![vec!["usb"]],
0x0A => cbor_array_vec![vec![ES256_CRED_PARAM]],
0x0C => CredentialProtectionPolicy::UserVerificationRequired as u64,
0x0E => 0,
};
assert_eq!(response_cbor, Some(expected_cbor));

View File

@@ -13,7 +13,7 @@
// limitations under the License.
use crate::crypto::rng256::Rng256;
use crate::ctap::data_formats::PublicKeyCredentialSource;
use crate::ctap::data_formats::{CredentialProtectionPolicy, PublicKeyCredentialSource};
use crate::ctap::status_code::Ctap2StatusCode;
use crate::ctap::{key_material, PIN_AUTH_LENGTH, USE_BATCH_ATTESTATION};
use alloc::string::String;
@@ -228,6 +228,7 @@ impl PersistentStore {
&self,
rp_id: &str,
credential_id: &[u8],
check_cred_protect: bool,
) -> Option<PublicKeyCredentialSource> {
let key = Key::Credential {
rp_id: Some(rp_id.into()),
@@ -238,7 +239,16 @@ impl PersistentStore {
debug_assert_eq!(entry.tag, TAG_CREDENTIAL);
let result = deserialize_credential(entry.data);
debug_assert!(result.is_some());
result
if check_cred_protect
&& result.as_ref().map_or(false, |cred| {
cred.cred_protect_policy
== Some(CredentialProtectionPolicy::UserVerificationRequired)
})
{
None
} else {
result
}
}
pub fn store_credential(
@@ -270,7 +280,11 @@ impl PersistentStore {
Ok(())
}
pub fn filter_credential(&self, rp_id: &str) -> Vec<PublicKeyCredentialSource> {
pub fn filter_credential(
&self,
rp_id: &str,
check_cred_protect: bool,
) -> Vec<PublicKeyCredentialSource> {
self.store
.find_all(&Key::Credential {
rp_id: Some(rp_id.into()),
@@ -283,6 +297,7 @@ impl PersistentStore {
debug_assert!(credential.is_some());
credential
})
.filter(|cred| !check_cred_protect || cred.is_discoverable())
.collect()
}
@@ -550,6 +565,7 @@ mod test {
user_handle,
other_ui: None,
cred_random: None,
cred_protect_policy: None,
}
}
@@ -634,7 +650,7 @@ mod test {
.is_ok());
assert_eq!(persistent_store.count_credentials(), 1);
assert_eq!(
&persistent_store.filter_credential("example.com"),
&persistent_store.filter_credential("example.com", false),
&[expected_credential]
);
@@ -682,7 +698,7 @@ mod test {
.store_credential(credential_source2)
.is_ok());
let filtered_credentials = persistent_store.filter_credential("example.com");
let filtered_credentials = persistent_store.filter_credential("example.com", false);
assert_eq!(filtered_credentials.len(), 2);
assert!(
(filtered_credentials[0].credential_id == id0
@@ -692,6 +708,30 @@ mod test {
);
}
#[test]
fn test_filter_with_cred_protect() {
let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng);
assert_eq!(persistent_store.count_credentials(), 0);
let private_key = crypto::ecdsa::SecKey::gensk(&mut rng);
let credential = PublicKeyCredentialSource {
key_type: PublicKeyCredentialType::PublicKey,
credential_id: rng.gen_uniform_u8x32().to_vec(),
private_key,
rp_id: String::from("example.com"),
user_handle: vec![0x00],
other_ui: None,
cred_random: None,
cred_protect_policy: Some(
CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList,
),
};
assert!(persistent_store.store_credential(credential).is_ok());
let no_credential = persistent_store.filter_credential("example.com", true);
assert_eq!(no_credential, vec![]);
}
#[test]
fn test_find() {
let mut rng = ThreadRng256 {};
@@ -708,9 +748,9 @@ mod test {
.store_credential(credential_source1)
.is_ok());
let no_credential = persistent_store.find_credential("another.example.com", &id0);
let no_credential = persistent_store.find_credential("another.example.com", &id0, false);
assert_eq!(no_credential, None);
let found_credential = persistent_store.find_credential("example.com", &id0);
let found_credential = persistent_store.find_credential("example.com", &id0, false);
let expected_credential = PublicKeyCredentialSource {
key_type: PublicKeyCredentialType::PublicKey,
credential_id: id0,
@@ -719,10 +759,33 @@ mod test {
user_handle: vec![0x00],
other_ui: None,
cred_random: None,
cred_protect_policy: None,
};
assert_eq!(found_credential, Some(expected_credential));
}
#[test]
fn test_find_with_cred_protect() {
let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng);
assert_eq!(persistent_store.count_credentials(), 0);
let private_key = crypto::ecdsa::SecKey::gensk(&mut rng);
let credential = PublicKeyCredentialSource {
key_type: PublicKeyCredentialType::PublicKey,
credential_id: rng.gen_uniform_u8x32().to_vec(),
private_key,
rp_id: String::from("example.com"),
user_handle: vec![0x00],
other_ui: None,
cred_random: None,
cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationRequired),
};
assert!(persistent_store.store_credential(credential).is_ok());
let no_credential = persistent_store.find_credential("example.com", &vec![0x00], true);
assert_eq!(no_credential, None);
}
#[test]
fn test_master_keys() {
let mut rng = ThreadRng256 {};