implements the credBlob extensions

This commit is contained in:
Fabian Kaczmarczyck
2021-01-20 18:46:38 +01:00
parent 03401778b3
commit 14189a398a
6 changed files with 356 additions and 102 deletions

View File

@@ -123,6 +123,8 @@ a few things you can personalize:
allows some relying parties to read the minimum PIN length by default. The allows some relying parties to read the minimum PIN length by default. The
latter allows storing more relying parties that may check the minimum PIN latter allows storing more relying parties that may check the minimum PIN
length. length.
1. Increase the `MAX_CRED_BLOB_LENGTH` in `ctap/mod.rs`, if you expect blobs
bigger than the default value.
### 3D printed enclosure ### 3D printed enclosure

View File

@@ -147,8 +147,9 @@ pub struct AuthenticatorMakeCredentialParameters {
pub user: PublicKeyCredentialUserEntity, pub user: PublicKeyCredentialUserEntity,
pub pub_key_cred_params: Vec<PublicKeyCredentialParameter>, pub pub_key_cred_params: Vec<PublicKeyCredentialParameter>,
pub exclude_list: Option<Vec<PublicKeyCredentialDescriptor>>, pub exclude_list: Option<Vec<PublicKeyCredentialDescriptor>>,
pub extensions: Option<MakeCredentialExtensions>, // Extensions are optional, but we can use defaults for all missing fields.
// Even though options are optional, we can use the default if not present. pub extensions: MakeCredentialExtensions,
// Same for options, use defaults when not present.
pub options: MakeCredentialOptions, pub options: MakeCredentialOptions,
pub pin_uv_auth_param: Option<Vec<u8>>, pub pin_uv_auth_param: Option<Vec<u8>>,
pub pin_uv_auth_protocol: Option<u64>, pub pin_uv_auth_protocol: Option<u64>,
@@ -198,15 +199,13 @@ impl TryFrom<cbor::Value> for AuthenticatorMakeCredentialParameters {
let extensions = extensions let extensions = extensions
.map(MakeCredentialExtensions::try_from) .map(MakeCredentialExtensions::try_from)
.transpose()?; .transpose()?
.unwrap_or_default();
let options = match options { let options = options
Some(entry) => MakeCredentialOptions::try_from(entry)?, .map(MakeCredentialOptions::try_from)
None => MakeCredentialOptions { .transpose()?
rk: false, .unwrap_or_default();
uv: false,
},
};
let pin_uv_auth_param = pin_uv_auth_param.map(extract_byte_string).transpose()?; let pin_uv_auth_param = pin_uv_auth_param.map(extract_byte_string).transpose()?;
let pin_uv_auth_protocol = pin_uv_auth_protocol.map(extract_unsigned).transpose()?; let pin_uv_auth_protocol = pin_uv_auth_protocol.map(extract_unsigned).transpose()?;
@@ -230,8 +229,9 @@ pub struct AuthenticatorGetAssertionParameters {
pub rp_id: String, pub rp_id: String,
pub client_data_hash: Vec<u8>, pub client_data_hash: Vec<u8>,
pub allow_list: Option<Vec<PublicKeyCredentialDescriptor>>, pub allow_list: Option<Vec<PublicKeyCredentialDescriptor>>,
pub extensions: Option<GetAssertionExtensions>, // Extensions are optional, but we can use defaults for all missing fields.
// Even though options are optional, we can use the default if not present. pub extensions: GetAssertionExtensions,
// Same for options, use defaults when not present.
pub options: GetAssertionOptions, pub options: GetAssertionOptions,
pub pin_uv_auth_param: Option<Vec<u8>>, pub pin_uv_auth_param: Option<Vec<u8>>,
pub pin_uv_auth_protocol: Option<u64>, pub pin_uv_auth_protocol: Option<u64>,
@@ -272,15 +272,13 @@ impl TryFrom<cbor::Value> for AuthenticatorGetAssertionParameters {
let extensions = extensions let extensions = extensions
.map(GetAssertionExtensions::try_from) .map(GetAssertionExtensions::try_from)
.transpose()?; .transpose()?
.unwrap_or_default();
let options = match options { let options = options
Some(entry) => GetAssertionOptions::try_from(entry)?, .map(GetAssertionOptions::try_from)
None => GetAssertionOptions { .transpose()?
up: true, .unwrap_or_default();
uv: false,
},
};
let pin_uv_auth_param = pin_uv_auth_param.map(extract_byte_string).transpose()?; let pin_uv_auth_param = pin_uv_auth_param.map(extract_byte_string).transpose()?;
let pin_uv_auth_protocol = pin_uv_auth_protocol.map(extract_unsigned).transpose()?; let pin_uv_auth_protocol = pin_uv_auth_protocol.map(extract_unsigned).transpose()?;
@@ -545,7 +543,7 @@ mod test {
user, user,
pub_key_cred_params: vec![ES256_CRED_PARAM], pub_key_cred_params: vec![ES256_CRED_PARAM],
exclude_list: Some(vec![]), exclude_list: Some(vec![]),
extensions: None, extensions: MakeCredentialExtensions::default(),
options, options,
pin_uv_auth_param: Some(vec![0x12, 0x34]), pin_uv_auth_param: Some(vec![0x12, 0x34]),
pin_uv_auth_protocol: Some(1), pin_uv_auth_protocol: Some(1),
@@ -591,7 +589,7 @@ mod test {
rp_id, rp_id,
client_data_hash, client_data_hash,
allow_list: Some(vec![pub_key_cred_descriptor]), allow_list: Some(vec![pub_key_cred_descriptor]),
extensions: None, extensions: GetAssertionExtensions::default(),
options, options,
pin_uv_auth_param: Some(vec![0x12, 0x34]), pin_uv_auth_param: Some(vec![0x12, 0x34]),
pin_uv_auth_protocol: Some(1), pin_uv_auth_protocol: Some(1),

View File

@@ -80,6 +80,7 @@ fn enumerate_credentials_response(
creation_order: _, creation_order: _,
user_name, user_name,
user_icon, user_icon,
cred_blob: _,
} = credential; } = credential;
let user = PublicKeyCredentialUserEntity { let user = PublicKeyCredentialUserEntity {
user_id: user_handle, user_id: user_handle,
@@ -346,6 +347,7 @@ mod test {
creation_order: 0, creation_order: 0,
user_name: Some("name".to_string()), user_name: Some("name".to_string()),
user_icon: Some("icon".to_string()), user_icon: Some("icon".to_string()),
cred_blob: None,
} }
} }

View File

@@ -275,11 +275,13 @@ impl From<PublicKeyCredentialDescriptor> for cbor::Value {
} }
} }
#[derive(Default)]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))]
pub struct MakeCredentialExtensions { pub struct MakeCredentialExtensions {
pub hmac_secret: bool, pub hmac_secret: bool,
pub cred_protect: Option<CredentialProtectionPolicy>, pub cred_protect: Option<CredentialProtectionPolicy>,
pub min_pin_length: bool, pub min_pin_length: bool,
pub cred_blob: Option<Vec<u8>>,
} }
impl TryFrom<cbor::Value> for MakeCredentialExtensions { impl TryFrom<cbor::Value> for MakeCredentialExtensions {
@@ -288,6 +290,7 @@ impl TryFrom<cbor::Value> for MakeCredentialExtensions {
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> { fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
destructure_cbor_map! { destructure_cbor_map! {
let { let {
"credBlob" => cred_blob,
"credProtect" => cred_protect, "credProtect" => cred_protect,
"hmac-secret" => hmac_secret, "hmac-secret" => hmac_secret,
"minPinLength" => min_pin_length, "minPinLength" => min_pin_length,
@@ -299,17 +302,21 @@ impl TryFrom<cbor::Value> for MakeCredentialExtensions {
.map(CredentialProtectionPolicy::try_from) .map(CredentialProtectionPolicy::try_from)
.transpose()?; .transpose()?;
let min_pin_length = min_pin_length.map_or(Ok(false), extract_bool)?; let min_pin_length = min_pin_length.map_or(Ok(false), extract_bool)?;
let cred_blob = cred_blob.map(extract_byte_string).transpose()?;
Ok(Self { Ok(Self {
hmac_secret, hmac_secret,
cred_protect, cred_protect,
min_pin_length, min_pin_length,
cred_blob,
}) })
} }
} }
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] #[derive(Clone, Default)]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct GetAssertionExtensions { pub struct GetAssertionExtensions {
pub hmac_secret: Option<GetAssertionHmacSecretInput>, pub hmac_secret: Option<GetAssertionHmacSecretInput>,
pub cred_blob: bool,
} }
impl TryFrom<cbor::Value> for GetAssertionExtensions { impl TryFrom<cbor::Value> for GetAssertionExtensions {
@@ -318,6 +325,7 @@ impl TryFrom<cbor::Value> for GetAssertionExtensions {
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> { fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
destructure_cbor_map! { destructure_cbor_map! {
let { let {
"credBlob" => cred_blob,
"hmac-secret" => hmac_secret, "hmac-secret" => hmac_secret,
} = extract_map(cbor_value)?; } = extract_map(cbor_value)?;
} }
@@ -325,7 +333,11 @@ impl TryFrom<cbor::Value> for GetAssertionExtensions {
let hmac_secret = hmac_secret let hmac_secret = hmac_secret
.map(GetAssertionHmacSecretInput::try_from) .map(GetAssertionHmacSecretInput::try_from)
.transpose()?; .transpose()?;
Ok(Self { hmac_secret }) let cred_blob = cred_blob.map_or(Ok(false), extract_bool)?;
Ok(Self {
hmac_secret,
cred_blob,
})
} }
} }
@@ -361,6 +373,7 @@ impl TryFrom<cbor::Value> for GetAssertionHmacSecretInput {
} }
// Even though options are optional, we can use the default if not present. // Even though options are optional, we can use the default if not present.
#[derive(Default)]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct MakeCredentialOptions { pub struct MakeCredentialOptions {
pub rk: bool, pub rk: bool,
@@ -400,6 +413,15 @@ pub struct GetAssertionOptions {
pub uv: bool, pub uv: bool,
} }
impl Default for GetAssertionOptions {
fn default() -> Self {
GetAssertionOptions {
up: true,
uv: false,
}
}
}
impl TryFrom<cbor::Value> for GetAssertionOptions { impl TryFrom<cbor::Value> for GetAssertionOptions {
type Error = Ctap2StatusCode; type Error = Ctap2StatusCode;
@@ -523,6 +545,7 @@ pub struct PublicKeyCredentialSource {
pub creation_order: u64, pub creation_order: u64,
pub user_name: Option<String>, pub user_name: Option<String>,
pub user_icon: Option<String>, pub user_icon: Option<String>,
pub cred_blob: Option<Vec<u8>>,
} }
// We serialize credentials for the persistent storage using CBOR maps. Each field of a credential // We serialize credentials for the persistent storage using CBOR maps. Each field of a credential
@@ -537,6 +560,7 @@ enum PublicKeyCredentialSourceField {
CreationOrder = 7, CreationOrder = 7,
UserName = 8, UserName = 8,
UserIcon = 9, UserIcon = 9,
CredBlob = 10,
// When a field is removed, its tag should be reserved and not used for new fields. We document // When a field is removed, its tag should be reserved and not used for new fields. We document
// those reserved tags below. // those reserved tags below.
// Reserved tags: // Reserved tags:
@@ -563,6 +587,7 @@ impl From<PublicKeyCredentialSource> for cbor::Value {
PublicKeyCredentialSourceField::CreationOrder => credential.creation_order, PublicKeyCredentialSourceField::CreationOrder => credential.creation_order,
PublicKeyCredentialSourceField::UserName => credential.user_name, PublicKeyCredentialSourceField::UserName => credential.user_name,
PublicKeyCredentialSourceField::UserIcon => credential.user_icon, PublicKeyCredentialSourceField::UserIcon => credential.user_icon,
PublicKeyCredentialSourceField::CredBlob => credential.cred_blob,
} }
} }
} }
@@ -582,6 +607,7 @@ impl TryFrom<cbor::Value> for PublicKeyCredentialSource {
PublicKeyCredentialSourceField::CreationOrder => creation_order, PublicKeyCredentialSourceField::CreationOrder => creation_order,
PublicKeyCredentialSourceField::UserName => user_name, PublicKeyCredentialSourceField::UserName => user_name,
PublicKeyCredentialSourceField::UserIcon => user_icon, PublicKeyCredentialSourceField::UserIcon => user_icon,
PublicKeyCredentialSourceField::CredBlob => cred_blob,
} = extract_map(cbor_value)?; } = extract_map(cbor_value)?;
} }
@@ -601,6 +627,7 @@ impl TryFrom<cbor::Value> for PublicKeyCredentialSource {
let creation_order = creation_order.map(extract_unsigned).unwrap_or(Ok(0))?; let creation_order = creation_order.map(extract_unsigned).unwrap_or(Ok(0))?;
let user_name = user_name.map(extract_text_string).transpose()?; let user_name = user_name.map(extract_text_string).transpose()?;
let user_icon = user_icon.map(extract_text_string).transpose()?; let user_icon = user_icon.map(extract_text_string).transpose()?;
let cred_blob = cred_blob.map(extract_byte_string).transpose()?;
// We don't return whether there were unknown fields in the CBOR value. This means that // We don't return whether there were unknown fields in the CBOR value. This means that
// deserialization is not injective. In particular deserialization is only an inverse of // deserialization is not injective. In particular deserialization is only an inverse of
// serialization at a given version of OpenSK. This is not a problem because: // serialization at a given version of OpenSK. This is not a problem because:
@@ -622,6 +649,7 @@ impl TryFrom<cbor::Value> for PublicKeyCredentialSource {
creation_order, creation_order,
user_name, user_name,
user_icon, user_icon,
cred_blob,
}) })
} }
} }
@@ -1493,12 +1521,14 @@ mod test {
"hmac-secret" => true, "hmac-secret" => true,
"credProtect" => CredentialProtectionPolicy::UserVerificationRequired, "credProtect" => CredentialProtectionPolicy::UserVerificationRequired,
"minPinLength" => true, "minPinLength" => true,
"credBlob" => vec![0xCB],
}; };
let extensions = MakeCredentialExtensions::try_from(cbor_extensions); let extensions = MakeCredentialExtensions::try_from(cbor_extensions);
let expected_extensions = MakeCredentialExtensions { let expected_extensions = MakeCredentialExtensions {
hmac_secret: true, hmac_secret: true,
cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired), cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired),
min_pin_length: true, min_pin_length: true,
cred_blob: Some(vec![0xCB]),
}; };
assert_eq!(extensions, Ok(expected_extensions)); assert_eq!(extensions, Ok(expected_extensions));
} }
@@ -1515,6 +1545,7 @@ mod test {
2 => vec![0x02; 32], 2 => vec![0x02; 32],
3 => vec![0x03; 16], 3 => vec![0x03; 16],
}, },
"credBlob" => true,
}; };
let extensions = GetAssertionExtensions::try_from(cbor_extensions); let extensions = GetAssertionExtensions::try_from(cbor_extensions);
let expected_input = GetAssertionHmacSecretInput { let expected_input = GetAssertionHmacSecretInput {
@@ -1524,6 +1555,7 @@ mod test {
}; };
let expected_extensions = GetAssertionExtensions { let expected_extensions = GetAssertionExtensions {
hmac_secret: Some(expected_input), hmac_secret: Some(expected_input),
cred_blob: true,
}; };
assert_eq!(extensions, Ok(expected_extensions)); assert_eq!(extensions, Ok(expected_extensions));
} }
@@ -1816,6 +1848,7 @@ mod test {
creation_order: 0, creation_order: 0,
user_name: None, user_name: None,
user_icon: None, user_icon: None,
cred_blob: None,
}; };
assert_eq!( assert_eq!(
@@ -1858,6 +1891,16 @@ mod test {
..credential ..credential
}; };
assert_eq!(
PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())),
Ok(credential.clone())
);
let credential = PublicKeyCredentialSource {
cred_blob: Some(vec![0xCB]),
..credential
};
assert_eq!( assert_eq!(
PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())),
Ok(credential) Ok(credential)

View File

@@ -35,7 +35,7 @@ use self::command::{
use self::config_command::process_config; use self::config_command::process_config;
use self::credential_management::process_credential_management; use self::credential_management::process_credential_management;
use self::data_formats::{ use self::data_formats::{
AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, GetAssertionHmacSecretInput, AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, GetAssertionExtensions,
PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter,
PublicKeyCredentialSource, PublicKeyCredentialType, PublicKeyCredentialUserEntity, PublicKeyCredentialSource, PublicKeyCredentialType, PublicKeyCredentialUserEntity,
SignatureAlgorithm, SignatureAlgorithm,
@@ -47,17 +47,18 @@ use self::response::{
AuthenticatorMakeCredentialResponse, AuthenticatorVendorResponse, ResponseData, AuthenticatorMakeCredentialResponse, AuthenticatorVendorResponse, ResponseData,
}; };
use self::status_code::Ctap2StatusCode; use self::status_code::Ctap2StatusCode;
use self::storage::PersistentStore; use self::storage::{PersistentStore, MAX_RP_IDS_LENGTH};
use self::timed_permission::TimedPermission; use self::timed_permission::TimedPermission;
#[cfg(feature = "with_ctap1")] #[cfg(feature = "with_ctap1")]
use self::timed_permission::U2fUserPresenceState; use self::timed_permission::U2fUserPresenceState;
use alloc::boxed::Box;
use alloc::collections::BTreeMap; use alloc::collections::BTreeMap;
use alloc::string::{String, ToString}; use alloc::string::{String, ToString};
use alloc::vec; use alloc::vec;
use alloc::vec::Vec; use alloc::vec::Vec;
use arrayref::array_ref; use arrayref::array_ref;
use byteorder::{BigEndian, ByteOrder}; use byteorder::{BigEndian, ByteOrder};
use cbor::{cbor_map, cbor_map_options}; use cbor::cbor_map_options;
#[cfg(feature = "debug_ctap")] #[cfg(feature = "debug_ctap")]
use core::fmt::Write; use core::fmt::Write;
use crypto::cbc::{cbc_decrypt, cbc_encrypt}; use crypto::cbc::{cbc_decrypt, cbc_encrypt};
@@ -124,6 +125,8 @@ pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa
// - Some(CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList) // - Some(CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList)
// - Some(CredentialProtectionPolicy::UserVerificationRequired) // - Some(CredentialProtectionPolicy::UserVerificationRequired)
const DEFAULT_CRED_PROTECT: Option<CredentialProtectionPolicy> = None; const DEFAULT_CRED_PROTECT: Option<CredentialProtectionPolicy> = None;
// Maximum size stored with the credBlob extension. Must be at least 32.
const MAX_CRED_BLOB_LENGTH: usize = 32;
// Checks the PIN protocol parameter against all supported versions. // Checks the PIN protocol parameter against all supported versions.
pub fn check_pin_uv_auth_protocol( pub fn check_pin_uv_auth_protocol(
@@ -154,7 +157,7 @@ fn truncate_to_char_boundary(s: &str, mut max: usize) -> &str {
pub 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>, extensions: GetAssertionExtensions,
has_uv: bool, has_uv: bool,
} }
@@ -168,7 +171,7 @@ pub struct AssertionState {
/// Stores which command currently holds state for subsequent calls. /// Stores which command currently holds state for subsequent calls.
pub enum StatefulCommand { pub enum StatefulCommand {
Reset, Reset,
GetAssertion(AssertionState), GetAssertion(Box<AssertionState>),
EnumerateRps(usize), EnumerateRps(usize),
EnumerateCredentials(Vec<usize>), EnumerateCredentials(Vec<usize>),
} }
@@ -419,6 +422,7 @@ where
creation_order: 0, creation_order: 0,
user_name: None, user_name: None,
user_icon: None, user_icon: None,
cred_blob: None,
})) }))
} }
@@ -558,27 +562,31 @@ where
} }
let rp_id = rp.rp_id; let rp_id = rp.rp_id;
let (use_hmac_extension, cred_protect_policy, min_pin_length) = let mut cred_protect_policy = extensions.cred_protect;
if let Some(extensions) = extensions { if cred_protect_policy.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional)
let mut cred_protect = extensions.cred_protect; < DEFAULT_CRED_PROTECT.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional)
if cred_protect.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) {
< DEFAULT_CRED_PROTECT cred_protect_policy = DEFAULT_CRED_PROTECT;
.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) }
{ let min_pin_length = extensions.min_pin_length
cred_protect = DEFAULT_CRED_PROTECT; && self
} .persistent_store
let min_pin_length = extensions.min_pin_length .min_pin_length_rp_ids()?
&& self .contains(&rp_id);
.persistent_store // None for no input, false for invalid input, true for valid input.
.min_pin_length_rp_ids()? let has_cred_blob_output = extensions.cred_blob.is_some();
.contains(&rp_id); let cred_blob = extensions
(extensions.hmac_secret, cred_protect, min_pin_length) .cred_blob
} else { .filter(|c| options.rk && c.len() <= MAX_CRED_BLOB_LENGTH);
(false, DEFAULT_CRED_PROTECT, false) let cred_blob_output = if has_cred_blob_output {
}; Some(cred_blob.is_some())
} else {
let has_extension_output = None
use_hmac_extension || cred_protect_policy.is_some() || min_pin_length; };
let has_extension_output = extensions.hmac_secret
|| cred_protect_policy.is_some()
|| min_pin_length
|| has_cred_blob_output;
let rp_id_hash = Sha256::hash(rp_id.as_bytes()); let rp_id_hash = Sha256::hash(rp_id.as_bytes());
if let Some(exclude_list) = exclude_list { if let Some(exclude_list) = exclude_list {
@@ -656,6 +664,7 @@ where
user_icon: user user_icon: user
.user_icon .user_icon
.map(|s| truncate_to_char_boundary(&s, 64).to_string()), .map(|s| truncate_to_char_boundary(&s, 64).to_string()),
cred_blob,
}; };
self.persistent_store.store_credential(credential_source)?; self.persistent_store.store_credential(credential_source)?;
random_id random_id
@@ -675,7 +684,11 @@ where
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
} }
if has_extension_output { if has_extension_output {
let hmac_secret_output = if use_hmac_extension { Some(true) } else { None }; let hmac_secret_output = if extensions.hmac_secret {
Some(true)
} else {
None
};
let min_pin_length_output = if min_pin_length { let min_pin_length_output = if min_pin_length {
Some(self.persistent_store.min_pin_length()? as u64) Some(self.persistent_store.min_pin_length()? as u64)
} else { } else {
@@ -685,6 +698,7 @@ where
"hmac-secret" => hmac_secret_output, "hmac-secret" => hmac_secret_output,
"credProtect" => cred_protect_policy, "credProtect" => cred_protect_policy,
"minPinLength" => min_pin_length_output, "minPinLength" => min_pin_length_output,
"credBlob" => cred_blob_output,
}; };
if !cbor::write(extensions_output, &mut auth_data) { if !cbor::write(extensions_output, &mut auth_data) {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
@@ -754,18 +768,30 @@ where
let AssertionInput { let AssertionInput {
client_data_hash, client_data_hash,
mut auth_data, mut auth_data,
hmac_secret_input, extensions,
has_uv, has_uv,
} = assertion_input; } = assertion_input;
// Process extensions. // Process extensions.
if let Some(hmac_secret_input) = hmac_secret_input { if extensions.hmac_secret.is_some() || extensions.cred_blob {
let cred_random = self.generate_cred_random(&credential.private_key, has_uv)?; let encrypted_output = if let Some(hmac_secret_input) = extensions.hmac_secret {
let encrypted_output = self let cred_random = self.generate_cred_random(&credential.private_key, has_uv)?;
.pin_protocol_v1 Some(
.process_hmac_secret(hmac_secret_input, &cred_random)?; self.pin_protocol_v1
let extensions_output = cbor_map! { .process_hmac_secret(hmac_secret_input, &cred_random)?,
)
} else {
None
};
// This could be written more nicely with `then_some` when stable.
let cred_blob = if extensions.cred_blob {
Some(credential.cred_blob.unwrap_or_default())
} else {
None
};
let extensions_output = cbor_map_options! {
"hmac-secret" => encrypted_output, "hmac-secret" => encrypted_output,
"credBlob" => cred_blob,
}; };
if !cbor::write(extensions_output, &mut auth_data) { if !cbor::write(extensions_output, &mut auth_data) {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
@@ -854,8 +880,7 @@ where
self.pin_uv_auth_precheck(&pin_uv_auth_param, pin_uv_auth_protocol, cid)?; self.pin_uv_auth_precheck(&pin_uv_auth_param, pin_uv_auth_protocol, cid)?;
let hmac_secret_input = extensions.map(|e| e.hmac_secret).flatten(); if extensions.hmac_secret.is_some() && !options.up {
if hmac_secret_input.is_some() && !options.up {
// The extension is actually supported, but we need user presence. // The extension is actually supported, but we need user presence.
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_OPTION); return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_OPTION);
} }
@@ -891,7 +916,7 @@ where
if options.up { if options.up {
flags |= UP_FLAG; flags |= UP_FLAG;
} }
if hmac_secret_input.is_some() { if extensions.hmac_secret.is_some() || extensions.cred_blob {
flags |= ED_FLAG; flags |= ED_FLAG;
} }
@@ -939,17 +964,17 @@ where
let assertion_input = AssertionInput { let assertion_input = AssertionInput {
client_data_hash, client_data_hash,
auth_data: self.generate_auth_data(&rp_id_hash, flags)?, auth_data: self.generate_auth_data(&rp_id_hash, flags)?,
hmac_secret_input, extensions,
has_uv, has_uv,
}; };
let number_of_credentials = if next_credential_keys.is_empty() { let number_of_credentials = if next_credential_keys.is_empty() {
None None
} else { } else {
let number_of_credentials = Some(next_credential_keys.len() + 1); let number_of_credentials = Some(next_credential_keys.len() + 1);
let assertion_state = StatefulCommand::GetAssertion(AssertionState { let assertion_state = StatefulCommand::GetAssertion(Box::new(AssertionState {
assertion_input: assertion_input.clone(), assertion_input: assertion_input.clone(),
next_credential_keys, next_credential_keys,
}); }));
self.stateful_command_permission self.stateful_command_permission
.set_command(now, assertion_state); .set_command(now, assertion_state);
number_of_credentials number_of_credentials
@@ -993,22 +1018,21 @@ where
String::from("hmac-secret"), String::from("hmac-secret"),
String::from("credProtect"), String::from("credProtect"),
String::from("minPinLength"), String::from("minPinLength"),
String::from("credBlob"),
]), ]),
aaguid: self.persistent_store.aaguid()?, aaguid: self.persistent_store.aaguid()?,
options: Some(options_map), options: Some(options_map),
max_msg_size: Some(1024), max_msg_size: Some(1024),
pin_protocols: Some(vec![PIN_PROTOCOL_VERSION]), pin_protocols: Some(vec![PIN_PROTOCOL_VERSION]),
max_credential_count_in_list: MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), max_credential_count_in_list: MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64),
// TODO(#106) update with version 2.1 of HMAC-secret
max_credential_id_length: Some(CREDENTIAL_ID_SIZE as u64), max_credential_id_length: Some(CREDENTIAL_ID_SIZE as u64),
transports: Some(vec![AuthenticatorTransport::Usb]), transports: Some(vec![AuthenticatorTransport::Usb]),
algorithms: Some(vec![ES256_CRED_PARAM]), algorithms: Some(vec![ES256_CRED_PARAM]),
default_cred_protect: DEFAULT_CRED_PROTECT, default_cred_protect: DEFAULT_CRED_PROTECT,
min_pin_length: self.persistent_store.min_pin_length()?, min_pin_length: self.persistent_store.min_pin_length()?,
firmware_version: None, firmware_version: None,
max_cred_blob_length: None, max_cred_blob_length: Some(MAX_CRED_BLOB_LENGTH as u64),
// TODO(kaczmarczyck) update when extension is implemented max_rp_ids_for_set_min_pin_length: Some(MAX_RP_IDS_LENGTH as u64),
max_rp_ids_for_set_min_pin_length: None,
remaining_discoverable_credentials: Some( remaining_discoverable_credentials: Some(
self.persistent_store.remaining_credentials()? as u64, self.persistent_store.remaining_credentials()? as u64,
), ),
@@ -1149,11 +1173,11 @@ where
mod test { mod test {
use super::command::AuthenticatorAttestationMaterial; use super::command::AuthenticatorAttestationMaterial;
use super::data_formats::{ use super::data_formats::{
CoseKey, GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions, CoseKey, GetAssertionHmacSecretInput, GetAssertionOptions, MakeCredentialExtensions,
MakeCredentialOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, MakeCredentialOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity,
}; };
use super::*; use super::*;
use cbor::cbor_array; use cbor::{cbor_array, cbor_map};
use crypto::rng256::ThreadRng256; use crypto::rng256::ThreadRng256;
const CLOCK_FREQUENCY_HZ: usize = 32768; const CLOCK_FREQUENCY_HZ: usize = 32768;
@@ -1209,7 +1233,7 @@ 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);
let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE);
let mut expected_response = vec![0x00, 0xAB, 0x01]; let mut expected_response = vec![0x00, 0xAD, 0x01];
// The version array differs with CTAP1, always including 2.0 and 2.1. // The version array differs with CTAP1, always including 2.0 and 2.1.
#[cfg(not(feature = "with_ctap1"))] #[cfg(not(feature = "with_ctap1"))]
let version_count = 2; let version_count = 2;
@@ -1221,10 +1245,11 @@ mod test {
expected_response.extend( expected_response.extend(
[ [
0x68, 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30, 0x6C, 0x46, 0x49, 0x44, 0x4F, 0x68, 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30, 0x6C, 0x46, 0x49, 0x44, 0x4F,
0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52, 0x45, 0x02, 0x83, 0x6B, 0x68, 0x6D, 0x61, 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52, 0x45, 0x02, 0x84, 0x6B, 0x68, 0x6D, 0x61,
0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x6B, 0x63, 0x72, 0x65, 0x64, 0x50, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x6B, 0x63, 0x72, 0x65, 0x64, 0x50,
0x72, 0x6F, 0x74, 0x65, 0x63, 0x74, 0x6C, 0x6D, 0x69, 0x6E, 0x50, 0x69, 0x6E, 0x4C, 0x72, 0x6F, 0x74, 0x65, 0x63, 0x74, 0x6C, 0x6D, 0x69, 0x6E, 0x50, 0x69, 0x6E, 0x4C,
0x65, 0x6E, 0x67, 0x74, 0x68, 0x03, 0x50, 0x65, 0x6E, 0x67, 0x74, 0x68, 0x68, 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62,
0x03, 0x50,
] ]
.iter(), .iter(),
); );
@@ -1237,7 +1262,7 @@ mod test {
0x65, 0x6E, 0x67, 0x74, 0x68, 0xF5, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, 0x01, 0x08, 0x65, 0x6E, 0x67, 0x74, 0x68, 0xF5, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, 0x01, 0x08,
0x18, 0x70, 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, 0x6C, 0x18, 0x70, 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, 0x6C,
0x67, 0x26, 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x67, 0x26, 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63,
0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04, 0x14, 0x18, 0x96, 0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04, 0x0F, 0x18, 0x20, 0x10, 0x08, 0x14, 0x18, 0x96,
] ]
.iter(), .iter(),
); );
@@ -1269,7 +1294,7 @@ mod test {
user, user,
pub_key_cred_params, pub_key_cred_params,
exclude_list: None, exclude_list: None,
extensions: None, extensions: MakeCredentialExtensions::default(),
options, options,
pin_uv_auth_param: None, pin_uv_auth_param: None,
pin_uv_auth_protocol: None, pin_uv_auth_protocol: None,
@@ -1293,11 +1318,12 @@ mod test {
fn create_make_credential_parameters_with_cred_protect_policy( fn create_make_credential_parameters_with_cred_protect_policy(
policy: CredentialProtectionPolicy, policy: CredentialProtectionPolicy,
) -> AuthenticatorMakeCredentialParameters { ) -> AuthenticatorMakeCredentialParameters {
let extensions = Some(MakeCredentialExtensions { let extensions = MakeCredentialExtensions {
hmac_secret: false, hmac_secret: false,
cred_protect: Some(policy), cred_protect: Some(policy),
min_pin_length: false, min_pin_length: false,
}); cred_blob: None,
};
let mut make_credential_params = create_minimal_make_credential_parameters(); let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.extensions = extensions; make_credential_params.extensions = extensions;
make_credential_params make_credential_params
@@ -1380,6 +1406,7 @@ mod test {
creation_order: 0, creation_order: 0,
user_name: None, user_name: None,
user_icon: None, user_icon: None,
cred_blob: None,
}; };
assert!(ctap_state assert!(ctap_state
.persistent_store .persistent_store
@@ -1458,11 +1485,12 @@ mod test {
let user_immediately_present = |_| Ok(()); let user_immediately_present = |_| Ok(());
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);
let extensions = Some(MakeCredentialExtensions { let extensions = MakeCredentialExtensions {
hmac_secret: true, hmac_secret: true,
cred_protect: None, cred_protect: None,
min_pin_length: false, min_pin_length: false,
}); cred_blob: None,
};
let mut make_credential_params = create_minimal_make_credential_parameters(); let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.options.rk = false; make_credential_params.options.rk = false;
make_credential_params.extensions = extensions; make_credential_params.extensions = extensions;
@@ -1487,11 +1515,12 @@ mod test {
let user_immediately_present = |_| Ok(()); let user_immediately_present = |_| Ok(());
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);
let extensions = Some(MakeCredentialExtensions { let extensions = MakeCredentialExtensions {
hmac_secret: true, hmac_secret: true,
cred_protect: None, cred_protect: None,
min_pin_length: false, min_pin_length: false,
}); cred_blob: None,
};
let mut make_credential_params = create_minimal_make_credential_parameters(); let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.extensions = extensions; make_credential_params.extensions = extensions;
let make_credential_response = let make_credential_response =
@@ -1516,11 +1545,12 @@ 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);
// First part: The extension is ignored, since the RP ID is not on the list. // First part: The extension is ignored, since the RP ID is not on the list.
let extensions = Some(MakeCredentialExtensions { let extensions = MakeCredentialExtensions {
hmac_secret: false, hmac_secret: false,
cred_protect: None, cred_protect: None,
min_pin_length: true, min_pin_length: true,
}); cred_blob: None,
};
let mut make_credential_params = create_minimal_make_credential_parameters(); let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.extensions = extensions; make_credential_params.extensions = extensions;
let make_credential_response = let make_credential_response =
@@ -1541,11 +1571,12 @@ mod test {
Ok(()) Ok(())
); );
let extensions = Some(MakeCredentialExtensions { let extensions = MakeCredentialExtensions {
hmac_secret: false, hmac_secret: false,
cred_protect: None, cred_protect: None,
min_pin_length: true, min_pin_length: true,
}); cred_blob: None,
};
let mut make_credential_params = create_minimal_make_credential_parameters(); let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.extensions = extensions; make_credential_params.extensions = extensions;
let make_credential_response = let make_credential_response =
@@ -1563,6 +1594,82 @@ mod test {
); );
} }
#[test]
fn test_process_make_credential_cred_blob_ok() {
let mut rng = ThreadRng256 {};
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
let extensions = MakeCredentialExtensions {
hmac_secret: false,
cred_protect: None,
min_pin_length: false,
cred_blob: Some(vec![0xCB]),
};
let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.extensions = extensions;
let make_credential_response =
ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID);
let expected_extension_cbor = [
0xA1, 0x68, 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, 0xF5,
];
check_make_response(
make_credential_response,
0xC1,
&ctap_state.persistent_store.aaguid().unwrap(),
0x20,
&expected_extension_cbor,
);
let mut iter_result = Ok(());
let iter = ctap_state
.persistent_store
.iter_credentials(&mut iter_result)
.unwrap();
// There is only 1 credential, so last is good enough.
let (_, stored_credential) = iter.last().unwrap();
iter_result.unwrap();
assert_eq!(stored_credential.cred_blob, Some(vec![0xCB]));
}
#[test]
fn test_process_make_credential_cred_blob_too_big() {
let mut rng = ThreadRng256 {};
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
let extensions = MakeCredentialExtensions {
hmac_secret: false,
cred_protect: None,
min_pin_length: false,
cred_blob: Some(vec![0xCB; MAX_CRED_BLOB_LENGTH + 1]),
};
let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.extensions = extensions;
let make_credential_response =
ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID);
let expected_extension_cbor = [
0xA1, 0x68, 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, 0xF4,
];
check_make_response(
make_credential_response,
0xC1,
&ctap_state.persistent_store.aaguid().unwrap(),
0x20,
&expected_extension_cbor,
);
let mut iter_result = Ok(());
let iter = ctap_state
.persistent_store
.iter_credentials(&mut iter_result)
.unwrap();
// There is only 1 credential, so last is good enough.
let (_, stored_credential) = iter.last().unwrap();
iter_result.unwrap();
assert_eq!(stored_credential.cred_blob, None);
}
#[test] #[test]
fn test_process_make_credential_cancelled() { fn test_process_make_credential_cancelled() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};
@@ -1586,6 +1693,7 @@ mod test {
flags: u8, flags: u8,
signature_counter: u32, signature_counter: u32,
expected_number_of_credentials: Option<u64>, expected_number_of_credentials: Option<u64>,
expected_extension_cbor: &[u8],
) { ) {
match response.unwrap() { match response.unwrap() {
ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { ResponseData::AuthenticatorGetAssertion(get_assertion_response) => {
@@ -1605,6 +1713,7 @@ mod test {
&mut expected_auth_data[signature_counter_position..], &mut expected_auth_data[signature_counter_position..],
signature_counter, signature_counter,
); );
expected_auth_data.extend(expected_extension_cbor);
assert_eq!(auth_data, expected_auth_data); assert_eq!(auth_data, expected_auth_data);
assert_eq!(user, Some(expected_user)); assert_eq!(user, Some(expected_user));
assert_eq!(number_of_credentials, expected_number_of_credentials); assert_eq!(number_of_credentials, expected_number_of_credentials);
@@ -1613,6 +1722,29 @@ mod test {
} }
} }
fn check_assertion_response_with_extension(
response: Result<ResponseData, Ctap2StatusCode>,
expected_user_id: Vec<u8>,
signature_counter: u32,
expected_number_of_credentials: Option<u64>,
expected_extension_cbor: &[u8],
) {
let expected_user = PublicKeyCredentialUserEntity {
user_id: expected_user_id,
user_name: None,
user_display_name: None,
user_icon: None,
};
check_assertion_response_with_user(
response,
expected_user,
0x80,
signature_counter,
expected_number_of_credentials,
expected_extension_cbor,
);
}
fn check_assertion_response( fn check_assertion_response(
response: Result<ResponseData, Ctap2StatusCode>, response: Result<ResponseData, Ctap2StatusCode>,
expected_user_id: Vec<u8>, expected_user_id: Vec<u8>,
@@ -1631,6 +1763,7 @@ mod test {
0x00, 0x00,
signature_counter, signature_counter,
expected_number_of_credentials, expected_number_of_credentials,
&[],
); );
} }
@@ -1649,7 +1782,7 @@ mod test {
rp_id: String::from("example.com"), rp_id: String::from("example.com"),
client_data_hash: vec![0xCD], client_data_hash: vec![0xCD],
allow_list: None, allow_list: None,
extensions: None, extensions: GetAssertionExtensions::default(),
options: GetAssertionOptions { options: GetAssertionOptions {
up: false, up: false,
uv: false, uv: false,
@@ -1676,11 +1809,12 @@ mod test {
let user_immediately_present = |_| Ok(()); let user_immediately_present = |_| Ok(());
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);
let make_extensions = Some(MakeCredentialExtensions { let make_extensions = MakeCredentialExtensions {
hmac_secret: true, hmac_secret: true,
cred_protect: None, cred_protect: None,
min_pin_length: false, min_pin_length: false,
}); cred_blob: None,
};
let mut make_credential_params = create_minimal_make_credential_parameters(); let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.options.rk = false; make_credential_params.options.rk = false;
make_credential_params.extensions = make_extensions; make_credential_params.extensions = make_extensions;
@@ -1704,9 +1838,10 @@ mod test {
salt_enc: vec![0x02; 32], salt_enc: vec![0x02; 32],
salt_auth: vec![0x03; 16], salt_auth: vec![0x03; 16],
}; };
let get_extensions = Some(GetAssertionExtensions { let get_extensions = GetAssertionExtensions {
hmac_secret: Some(hmac_secret_input), hmac_secret: Some(hmac_secret_input),
}); cred_blob: false,
};
let cred_desc = PublicKeyCredentialDescriptor { let cred_desc = PublicKeyCredentialDescriptor {
key_type: PublicKeyCredentialType::PublicKey, key_type: PublicKeyCredentialType::PublicKey,
@@ -1744,11 +1879,12 @@ mod test {
let user_immediately_present = |_| Ok(()); let user_immediately_present = |_| Ok(());
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);
let make_extensions = Some(MakeCredentialExtensions { let make_extensions = MakeCredentialExtensions {
hmac_secret: true, hmac_secret: true,
cred_protect: None, cred_protect: None,
min_pin_length: false, min_pin_length: false,
}); cred_blob: None,
};
let mut make_credential_params = create_minimal_make_credential_parameters(); let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.extensions = make_extensions; make_credential_params.extensions = make_extensions;
assert!(ctap_state assert!(ctap_state
@@ -1761,9 +1897,10 @@ mod test {
salt_enc: vec![0x02; 32], salt_enc: vec![0x02; 32],
salt_auth: vec![0x03; 16], salt_auth: vec![0x03; 16],
}; };
let get_extensions = Some(GetAssertionExtensions { let get_extensions = GetAssertionExtensions {
hmac_secret: Some(hmac_secret_input), hmac_secret: Some(hmac_secret_input),
}); cred_blob: false,
};
let get_assertion_params = AuthenticatorGetAssertionParameters { let get_assertion_params = AuthenticatorGetAssertionParameters {
rp_id: String::from("example.com"), rp_id: String::from("example.com"),
@@ -1800,7 +1937,7 @@ mod test {
let cred_desc = PublicKeyCredentialDescriptor { let cred_desc = PublicKeyCredentialDescriptor {
key_type: PublicKeyCredentialType::PublicKey, key_type: PublicKeyCredentialType::PublicKey,
key_id: credential_id.clone(), key_id: credential_id.clone(),
transports: None, // You can set USB as a hint here. transports: None,
}; };
let credential = PublicKeyCredentialSource { let credential = PublicKeyCredentialSource {
key_type: PublicKeyCredentialType::PublicKey, key_type: PublicKeyCredentialType::PublicKey,
@@ -1815,6 +1952,7 @@ mod test {
creation_order: 0, creation_order: 0,
user_name: None, user_name: None,
user_icon: None, user_icon: None,
cred_blob: None,
}; };
assert!(ctap_state assert!(ctap_state
.persistent_store .persistent_store
@@ -1825,7 +1963,7 @@ mod test {
rp_id: String::from("example.com"), rp_id: String::from("example.com"),
client_data_hash: vec![0xCD], client_data_hash: vec![0xCD],
allow_list: None, allow_list: None,
extensions: None, extensions: GetAssertionExtensions::default(),
options: GetAssertionOptions { options: GetAssertionOptions {
up: false, up: false,
uv: false, uv: false,
@@ -1847,7 +1985,7 @@ mod test {
rp_id: String::from("example.com"), rp_id: String::from("example.com"),
client_data_hash: vec![0xCD], client_data_hash: vec![0xCD],
allow_list: Some(vec![cred_desc.clone()]), allow_list: Some(vec![cred_desc.clone()]),
extensions: None, extensions: GetAssertionExtensions::default(),
options: GetAssertionOptions { options: GetAssertionOptions {
up: false, up: false,
uv: false, uv: false,
@@ -1877,6 +2015,7 @@ mod test {
creation_order: 0, creation_order: 0,
user_name: None, user_name: None,
user_icon: None, user_icon: None,
cred_blob: None,
}; };
assert!(ctap_state assert!(ctap_state
.persistent_store .persistent_store
@@ -1887,7 +2026,7 @@ mod test {
rp_id: String::from("example.com"), rp_id: String::from("example.com"),
client_data_hash: vec![0xCD], client_data_hash: vec![0xCD],
allow_list: Some(vec![cred_desc]), allow_list: Some(vec![cred_desc]),
extensions: None, extensions: GetAssertionExtensions::default(),
options: GetAssertionOptions { options: GetAssertionOptions {
up: false, up: false,
uv: false, uv: false,
@@ -1906,6 +2045,69 @@ mod test {
); );
} }
#[test]
fn test_process_get_assertion_with_cred_blob() {
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, DUMMY_CLOCK_VALUE);
let credential = PublicKeyCredentialSource {
key_type: PublicKeyCredentialType::PublicKey,
credential_id,
private_key,
rp_id: String::from("example.com"),
user_handle: vec![0x1D],
user_display_name: None,
cred_protect_policy: None,
creation_order: 0,
user_name: None,
user_icon: None,
cred_blob: Some(vec![0xCB]),
};
assert!(ctap_state
.persistent_store
.store_credential(credential)
.is_ok());
let extensions = GetAssertionExtensions {
hmac_secret: None,
cred_blob: true,
};
let get_assertion_params = AuthenticatorGetAssertionParameters {
rp_id: String::from("example.com"),
client_data_hash: vec![0xCD],
allow_list: None,
extensions,
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,
DUMMY_CLOCK_VALUE,
);
let signature_counter = ctap_state
.persistent_store
.global_signature_counter()
.unwrap();
let expected_extension_cbor = [
0xA1, 0x68, 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, 0x41, 0xCB,
];
check_assertion_response_with_extension(
get_assertion_response,
vec![0x1D],
signature_counter,
None,
&expected_extension_cbor,
);
}
#[test] #[test]
fn test_process_get_next_assertion_two_credentials_with_uv() { fn test_process_get_next_assertion_two_credentials_with_uv() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};
@@ -1951,7 +2153,7 @@ mod test {
rp_id: String::from("example.com"), rp_id: String::from("example.com"),
client_data_hash: vec![0xCD], client_data_hash: vec![0xCD],
allow_list: None, allow_list: None,
extensions: None, extensions: GetAssertionExtensions::default(),
options: GetAssertionOptions { options: GetAssertionOptions {
up: false, up: false,
uv: true, uv: true,
@@ -1974,6 +2176,7 @@ mod test {
0x04, 0x04,
signature_counter, signature_counter,
Some(2), Some(2),
&[],
); );
let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE);
@@ -1983,6 +2186,7 @@ mod test {
0x04, 0x04,
signature_counter, signature_counter,
None, None,
&[],
); );
let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE);
@@ -2027,7 +2231,7 @@ mod test {
rp_id: String::from("example.com"), rp_id: String::from("example.com"),
client_data_hash: vec![0xCD], client_data_hash: vec![0xCD],
allow_list: None, allow_list: None,
extensions: None, extensions: GetAssertionExtensions::default(),
options: GetAssertionOptions { options: GetAssertionOptions {
up: false, up: false,
uv: false, uv: false,
@@ -2091,7 +2295,7 @@ mod test {
rp_id: String::from("example.com"), rp_id: String::from("example.com"),
client_data_hash: vec![0xCD], client_data_hash: vec![0xCD],
allow_list: None, allow_list: None,
extensions: None, extensions: GetAssertionExtensions::default(),
options: GetAssertionOptions { options: GetAssertionOptions {
up: false, up: false,
uv: false, uv: false,
@@ -2147,6 +2351,7 @@ mod test {
creation_order: 0, creation_order: 0,
user_name: None, user_name: None,
user_icon: None, user_icon: None,
cred_blob: None,
}; };
assert!(ctap_state assert!(ctap_state
.persistent_store .persistent_store

View File

@@ -57,7 +57,7 @@ const DEFAULT_MIN_PIN_LENGTH: u8 = 4;
const DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec<String> = Vec::new(); const DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec<String> = Vec::new();
// This constant is an attempt to limit storage requirements. If you don't set it to 0, // This constant is an attempt to limit storage requirements. If you don't set it to 0,
// the stored strings can still be unbounded, but that is true for all RP IDs. // the stored strings can still be unbounded, but that is true for all RP IDs.
const MAX_RP_IDS_LENGTH: usize = 8; pub const MAX_RP_IDS_LENGTH: usize = 8;
/// Wrapper for master keys. /// Wrapper for master keys.
pub struct MasterKeys { pub struct MasterKeys {
@@ -690,6 +690,7 @@ mod test {
creation_order: 0, creation_order: 0,
user_name: None, user_name: None,
user_icon: None, user_icon: None,
cred_blob: None,
} }
} }
@@ -906,6 +907,7 @@ mod test {
creation_order: 0, creation_order: 0,
user_name: None, user_name: None,
user_icon: None, user_icon: None,
cred_blob: None,
}; };
assert_eq!(found_credential, Some(expected_credential)); assert_eq!(found_credential, Some(expected_credential));
} }
@@ -927,6 +929,7 @@ mod test {
creation_order: 0, creation_order: 0,
user_name: None, user_name: None,
user_icon: None, user_icon: None,
cred_blob: None,
}; };
assert!(persistent_store.store_credential(credential).is_ok()); assert!(persistent_store.store_credential(credential).is_ok());
@@ -1160,6 +1163,7 @@ mod test {
creation_order: 0, creation_order: 0,
user_name: None, user_name: None,
user_icon: None, user_icon: None,
cred_blob: None,
}; };
let serialized = serialize_credential(credential.clone()).unwrap(); let serialized = serialize_credential(credential.clone()).unwrap();
let reconstructed = deserialize_credential(&serialized).unwrap(); let reconstructed = deserialize_credential(&serialized).unwrap();