Merge branch 'master' into aaguid
This commit is contained in:
@@ -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
319
src/ctap/mod.rs
319
src/ctap/mod.rs
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
Reference in New Issue
Block a user