Merge pull request #109 from kaczmarczyck/cred-protect

credProtect extension
This commit is contained in:
kaczmarczyck
2020-06-05 11:47:03 +02:00
committed by GitHub
9 changed files with 481 additions and 57 deletions

View File

@@ -99,6 +99,10 @@ a few things you can personalize:
4. Depending on your available flash storage, choose an appropriate maximum 4. Depending on your available flash storage, choose an appropriate maximum
number of supported residential keys and number of pages in number of supported residential keys and number of pages in
`ctap/storage.rs`. `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 ### 3D printed enclosure

View File

@@ -0,0 +1,9 @@
c182bb4902fff51b2f56810fc2a27df3646cd66ba21359162354d53445623ab8 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840dk.bin
2c2879a0263ebaa6e841db4b352346cc5b4cef5084ce85525cf49669d3b0b41d target/nrf52840dk_merged.hex
0a9929ba8fa57e8a502a49fc7c53177397202e1b11f4c7c3cb6ed68b2b99dd46 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle.bin
652824a90674dc3199070f2f46a791ab4951e367982ecae6f65fc41338a5a856 target/nrf52840_dongle_merged.hex
cca9086c9149c607589b23ffa599a5e4c26db7c20bd3700b79528bd3a5df991d third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle_dfu.bin
a030505f5576129954a3977f97957b8b4e023b2b51a29d45d4511566458666ac target/nrf52840_dongle_dfu_merged.hex
8857488ba6a69e366f0da229bbfc012a2ad291d3a88d9494247d600c10bb19b7 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_mdk_dfu.bin
82ac4290967ae67a78c986444fd6c2a2aa5668ac254a2af642c98be4a064f913 target/nrf52840_mdk_dfu_merged.hex
c3e901a80fd779b15a6f266e48dcef5140b318f5f62b23a96547498572ac1666 target/tab/ctap2.tab

View File

@@ -1,9 +1,9 @@
1003863864e06553e730eec6df4bf8d30c99f697ef9380efdc35eba679b4db78 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840dk.bin 1003863864e06553e730eec6df4bf8d30c99f697ef9380efdc35eba679b4db78 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840dk.bin
84d97929d7592d89c7f321ffccafa4148263607e28918e53d9286be8ca55c209 target/nrf52840dk_merged.hex 63dda5b708add5ac72db0c0757de451f95173b0fc7d71fc85c3b25460850e23c target/nrf52840dk_merged.hex
88f00a5e1dae6ab3f7571c254ac75f5f3e29ebea7f3ca46c16cfdc3708e804fc third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle.bin 88f00a5e1dae6ab3f7571c254ac75f5f3e29ebea7f3ca46c16cfdc3708e804fc third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle.bin
02009688b6ef8583f78f9b94ba8af65dfa3749b20516972cdb0d8ea7ac409268 target/nrf52840_dongle_merged.hex 2fca47df0053c6750d9f8fe61e8e6a0e9eee2457c908aa1c2225e2aab2690cb4 target/nrf52840_dongle_merged.hex
1bc69b48a2c48da55db8b322902e1fe3f2e095c0dd8517db28837d86e0addc85 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle_dfu.bin 1bc69b48a2c48da55db8b322902e1fe3f2e095c0dd8517db28837d86e0addc85 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle_dfu.bin
a24e7459b93eea1fc7557ecd9e4271a2ed729425990d6198be6791f364b1384b target/nrf52840_dongle_dfu_merged.hex 33b5a96bb427de653440cd8bfa31e518e1a2c269368a6cd103602d2fd6f3e127 target/nrf52840_dongle_dfu_merged.hex
f38ee31d3a09e7e11848e78b5318f95517b6dcd076afcb37e6e3d3e5e9995cc7 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_mdk_dfu.bin f38ee31d3a09e7e11848e78b5318f95517b6dcd076afcb37e6e3d3e5e9995cc7 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_mdk_dfu.bin
5315b77b997952de10e239289e54b44c24105646f2411074332bb46f4b686ae6 target/nrf52840_mdk_dfu_merged.hex dce8f1f02e2f7e93634b1edd02343547e139445d23c2d17ce6ccdc4895a67c8c target/nrf52840_mdk_dfu_merged.hex
9012744b93f8bac122fa18916cf8c22d1b8f729a284366802ed222bbc985e3f0 target/tab/ctap2.tab 1e216599d58e6c66845845a2ec709f72842e21d48a2185022223d1ffe006de07 target/tab/ctap2.tab

View File

@@ -1,9 +1,9 @@
c182bb4902fff51b2f56810fc2a27df3646cd66ba21359162354d53445623ab8 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840dk.bin c182bb4902fff51b2f56810fc2a27df3646cd66ba21359162354d53445623ab8 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840dk.bin
805ca30b898b41a091cc136ab9b78b4e566e10c5469902d18c326c132ed4193e target/nrf52840dk_merged.hex d978bbb7d56e54f1d8dc43a8b73e01ede50f404f8640ed8969112acac9efa36e target/nrf52840dk_merged.hex
0a9929ba8fa57e8a502a49fc7c53177397202e1b11f4c7c3cb6ed68b2b99dd46 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle.bin 0a9929ba8fa57e8a502a49fc7c53177397202e1b11f4c7c3cb6ed68b2b99dd46 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle.bin
960dce1eb78f34a3c4cfdb543314da8ce211dced41f34da053669c8773926e1d target/nrf52840_dongle_merged.hex 53f3f390fce878d4d6108d34a0f4f305375ca125a02a4160839eeee3acd55e88 target/nrf52840_dongle_merged.hex
cca9086c9149c607589b23ffa599a5e4c26db7c20bd3700b79528bd3a5df991d third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle_dfu.bin cca9086c9149c607589b23ffa599a5e4c26db7c20bd3700b79528bd3a5df991d third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle_dfu.bin
1755746cb3a28162a0bbd0b994332fa9ffaedca684803dfd9ef584040cea73ca target/nrf52840_dongle_dfu_merged.hex 68ef4c5d2e5b53761c31e185fa6237eaaa13736f8e0657c68d235f27497efe85 target/nrf52840_dongle_dfu_merged.hex
8857488ba6a69e366f0da229bbfc012a2ad291d3a88d9494247d600c10bb19b7 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_mdk_dfu.bin 8857488ba6a69e366f0da229bbfc012a2ad291d3a88d9494247d600c10bb19b7 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_mdk_dfu.bin
04b94cd65bf83fa12030c4deaa831e0251f5f8b9684ec972d03a46e8f32e98b4 target/nrf52840_mdk_dfu_merged.hex 898842aeaf9a7b03f3f5828871fc8a41fff8fc550683c18f9cfd718f8da26ba4 target/nrf52840_mdk_dfu_merged.hex
69dd51b947013b77e3577784384c935ed76930d1fb3ba46e9a5b6b5d71941057 target/tab/ctap2.tab d7d8e13bd8a183a6868a463512a999e1c3843d9d5b13f1d35909bfa80d24619e target/tab/ctap2.tab

BIN
reproducible/reproduced.tar Normal file

Binary file not shown.

View File

@@ -155,13 +155,13 @@ impl From<PublicKeyCredentialParameter> for cbor::Value {
fn from(cred_param: PublicKeyCredentialParameter) -> Self { fn from(cred_param: PublicKeyCredentialParameter) -> Self {
cbor_map_options! { cbor_map_options! {
"type" => cred_param.cred_type, "type" => cred_param.cred_type,
"alg" => cred_param.alg as i64, "alg" => cred_param.alg,
} }
} }
} }
// https://www.w3.org/TR/webauthn/#enumdef-authenticatortransport // https://www.w3.org/TR/webauthn/#enumdef-authenticatortransport
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))]
pub enum AuthenticatorTransport { pub enum AuthenticatorTransport {
Usb, Usb,
Nfc, Nfc,
@@ -197,7 +197,7 @@ impl TryFrom<&cbor::Value> for AuthenticatorTransport {
} }
// https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialdescriptor // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialdescriptor
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))]
pub struct PublicKeyCredentialDescriptor { pub struct PublicKeyCredentialDescriptor {
pub key_type: PublicKeyCredentialType, pub key_type: PublicKeyCredentialType,
pub key_id: Vec<u8>, pub key_id: Vec<u8>,
@@ -292,6 +292,14 @@ impl Extensions {
.get("hmac-secret") .get("hmac-secret")
.map(GetAssertionHmacSecretInput::try_from) .map(GetAssertionHmacSecretInput::try_from)
} }
pub fn make_credential_cred_protect_policy(
&self,
) -> Option<Result<CredentialProtectionPolicy, Ctap2StatusCode>> {
self.0
.get("credProtect")
.map(CredentialProtectionPolicy::try_from)
}
} }
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
@@ -421,6 +429,12 @@ pub enum SignatureAlgorithm {
Unknown = 0, Unknown = 0,
} }
impl From<SignatureAlgorithm> for cbor::Value {
fn from(alg: SignatureAlgorithm) -> Self {
(alg as i64).into()
}
}
impl TryFrom<&cbor::Value> for SignatureAlgorithm { impl TryFrom<&cbor::Value> for SignatureAlgorithm {
type Error = Ctap2StatusCode; type Error = Ctap2StatusCode;
@@ -432,6 +446,46 @@ impl TryFrom<&cbor::Value> for SignatureAlgorithm {
} }
} }
#[derive(Clone, Copy, PartialEq, PartialOrd)]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))]
pub enum CredentialProtectionPolicy {
UserVerificationOptional = 0x01,
UserVerificationOptionalWithCredentialIdList = 0x02,
UserVerificationRequired = 0x03,
}
impl From<CredentialProtectionPolicy> for cbor::Value {
fn from(policy: CredentialProtectionPolicy) -> Self {
(policy as i64).into()
}
}
impl TryFrom<cbor::Value> for CredentialProtectionPolicy {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
match extract_integer(cbor_value)? {
0x01 => Ok(CredentialProtectionPolicy::UserVerificationOptional),
0x02 => Ok(CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList),
0x03 => Ok(CredentialProtectionPolicy::UserVerificationRequired),
_ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE),
}
}
}
impl TryFrom<&cbor::Value> for CredentialProtectionPolicy {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: &cbor::Value) -> Result<Self, Ctap2StatusCode> {
match read_integer(cbor_value)? {
0x01 => Ok(CredentialProtectionPolicy::UserVerificationOptional),
0x02 => Ok(CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList),
0x03 => Ok(CredentialProtectionPolicy::UserVerificationRequired),
_ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE),
}
}
}
// https://www.w3.org/TR/webauthn/#public-key-credential-source // https://www.w3.org/TR/webauthn/#public-key-credential-source
// //
// Note that we only use the WebAuthn definition as an example. This data-structure is not specified // Note that we only use the WebAuthn definition as an example. This data-structure is not specified
@@ -448,6 +502,7 @@ pub struct PublicKeyCredentialSource {
pub user_handle: Vec<u8>, // not optional, but nullable pub user_handle: Vec<u8>, // not optional, but nullable
pub other_ui: Option<String>, pub other_ui: Option<String>,
pub cred_random: Option<Vec<u8>>, pub cred_random: Option<Vec<u8>>,
pub cred_protect_policy: Option<CredentialProtectionPolicy>,
} }
// 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
@@ -459,6 +514,7 @@ enum PublicKeyCredentialSourceField {
UserHandle = 3, UserHandle = 3,
OtherUi = 4, OtherUi = 4,
CredRandom = 5, CredRandom = 5,
CredProtectPolicy = 6,
// 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: none. // Reserved tags: none.
@@ -481,7 +537,8 @@ impl From<PublicKeyCredentialSource> for cbor::Value {
RpId => Some(credential.rp_id), RpId => Some(credential.rp_id),
UserHandle => Some(credential.user_handle), UserHandle => Some(credential.user_handle),
OtherUi => credential.other_ui, OtherUi => credential.other_ui,
CredRandom => credential.cred_random CredRandom => credential.cred_random,
CredProtectPolicy => credential.cred_protect_policy,
} }
} }
} }
@@ -509,6 +566,10 @@ impl TryFrom<cbor::Value> for PublicKeyCredentialSource {
.remove(&CredRandom.into()) .remove(&CredRandom.into())
.map(extract_byte_string) .map(extract_byte_string)
.transpose()?; .transpose()?;
let cred_protect_policy = map
.remove(&CredProtectPolicy.into())
.map(CredentialProtectionPolicy::try_from)
.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:
@@ -527,10 +588,20 @@ impl TryFrom<cbor::Value> for PublicKeyCredentialSource {
user_handle, user_handle,
other_ui, other_ui,
cred_random, cred_random,
cred_protect_policy,
}) })
} }
} }
impl PublicKeyCredentialSource {
// Relying parties do not need to provide the credential ID in an allow_list if true.
pub fn is_discoverable(&self) -> bool {
self.cred_protect_policy.is_none()
|| self.cred_protect_policy
== Some(CredentialProtectionPolicy::UserVerificationOptional)
}
}
// TODO(kaczmarczyck) we could decide to split this data type up // TODO(kaczmarczyck) we could decide to split this data type up
// It depends on the algorithm though, I think. // It depends on the algorithm though, I think.
// So before creating a mess, this is my workaround. // So before creating a mess, this is my workaround.
@@ -667,6 +738,20 @@ pub(super) fn read_integer(cbor_value: &cbor::Value) -> Result<i64, Ctap2StatusC
} }
} }
fn extract_integer(cbor_value: cbor::Value) -> Result<i64, Ctap2StatusCode> {
match cbor_value {
cbor::Value::KeyValue(cbor::KeyType::Unsigned(unsigned)) => {
if unsigned <= core::i64::MAX as u64 {
Ok(unsigned as i64)
} else {
Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE)
}
}
cbor::Value::KeyValue(cbor::KeyType::Negative(signed)) => Ok(signed),
_ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE),
}
}
pub fn read_byte_string(cbor_value: &cbor::Value) -> Result<Vec<u8>, Ctap2StatusCode> { pub fn read_byte_string(cbor_value: &cbor::Value) -> Result<Vec<u8>, Ctap2StatusCode> {
match cbor_value { match cbor_value {
cbor::Value::KeyValue(cbor::KeyType::ByteString(byte_string)) => Ok(byte_string.to_vec()), cbor::Value::KeyValue(cbor::KeyType::ByteString(byte_string)) => Ok(byte_string.to_vec()),
@@ -1064,7 +1149,7 @@ mod test {
let signature_algorithm = SignatureAlgorithm::try_from(&cbor_signature_algorithm); let signature_algorithm = SignatureAlgorithm::try_from(&cbor_signature_algorithm);
let expected_signature_algorithm = SignatureAlgorithm::ES256; let expected_signature_algorithm = SignatureAlgorithm::ES256;
assert_eq!(signature_algorithm, Ok(expected_signature_algorithm)); assert_eq!(signature_algorithm, Ok(expected_signature_algorithm));
let created_cbor: cbor::Value = cbor_int!(signature_algorithm.unwrap() as i64); let created_cbor = cbor::Value::from(signature_algorithm.unwrap());
assert_eq!(created_cbor, cbor_signature_algorithm); assert_eq!(created_cbor, cbor_signature_algorithm);
let cbor_unknown_algorithm = cbor_int!(-1); let cbor_unknown_algorithm = cbor_int!(-1);
@@ -1073,6 +1158,37 @@ mod test {
assert_eq!(unknown_algorithm, Ok(expected_unknown_algorithm)); assert_eq!(unknown_algorithm, Ok(expected_unknown_algorithm));
} }
#[test]
fn test_cred_protection_policy_order() {
assert!(
CredentialProtectionPolicy::UserVerificationOptional
< CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList
);
assert!(
CredentialProtectionPolicy::UserVerificationOptional
< CredentialProtectionPolicy::UserVerificationRequired
);
assert!(
CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList
< CredentialProtectionPolicy::UserVerificationRequired
);
}
#[test]
fn test_from_into_cred_protection_policy() {
let cbor_policy = cbor::Value::from(CredentialProtectionPolicy::UserVerificationOptional);
let policy = CredentialProtectionPolicy::try_from(&cbor_policy);
let expected_policy = CredentialProtectionPolicy::UserVerificationOptional;
assert_eq!(policy, Ok(expected_policy));
let created_cbor = cbor::Value::from(policy.unwrap());
assert_eq!(created_cbor, cbor_policy);
let cbor_policy_error = cbor_int!(-1);
let policy_error = CredentialProtectionPolicy::try_from(&cbor_policy_error);
let expected_error = Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE);
assert_eq!(policy_error, expected_error);
}
#[test] #[test]
fn test_from_into_authenticator_transport() { fn test_from_into_authenticator_transport() {
let cbor_authenticator_transport = cbor_text!("usb"); let cbor_authenticator_transport = cbor_text!("usb");
@@ -1183,6 +1299,18 @@ mod test {
assert_eq!(get_assertion_input, Some(Ok(expected_input))); assert_eq!(get_assertion_input, Some(Ok(expected_input)));
} }
#[test]
fn test_cred_protect_extension() {
let cbor_extensions = cbor_map! {
"credProtect" => CredentialProtectionPolicy::UserVerificationRequired,
};
let extensions = Extensions::try_from(&cbor_extensions).unwrap();
assert_eq!(
extensions.make_credential_cred_protect_policy(),
Some(Ok(CredentialProtectionPolicy::UserVerificationRequired))
);
}
#[test] #[test]
fn test_from_make_credential_options() { fn test_from_make_credential_options() {
let cbor_make_options = cbor_map! { let cbor_make_options = cbor_map! {
@@ -1261,6 +1389,7 @@ mod test {
user_handle: b"foo".to_vec(), user_handle: b"foo".to_vec(),
other_ui: None, other_ui: None,
cred_random: None, cred_random: None,
cred_protect_policy: None,
}; };
assert_eq!( assert_eq!(
@@ -1283,6 +1412,16 @@ mod test {
..credential ..credential
}; };
assert_eq!(
PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())),
Ok(credential.clone())
);
let credential = PublicKeyCredentialSource {
cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationOptional),
..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

@@ -32,9 +32,10 @@ use self::command::{
#[cfg(feature = "with_ctap2_1")] #[cfg(feature = "with_ctap2_1")]
use self::data_formats::AuthenticatorTransport; use self::data_formats::AuthenticatorTransport;
use self::data_formats::{ use self::data_formats::{
ClientPinSubCommand, CoseKey, GetAssertionHmacSecretInput, PackedAttestationStatement, ClientPinSubCommand, CoseKey, CredentialProtectionPolicy, GetAssertionHmacSecretInput,
PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialSource, PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter,
PublicKeyCredentialType, PublicKeyCredentialUserEntity, SignatureAlgorithm, PublicKeyCredentialSource, PublicKeyCredentialType, PublicKeyCredentialUserEntity,
SignatureAlgorithm,
}; };
use self::hid::ChannelID; use self::hid::ChannelID;
use self::key_material::{AAGUID, ATTESTATION_CERTIFICATE, ATTESTATION_PRIVATE_KEY}; use self::key_material::{AAGUID, ATTESTATION_CERTIFICATE, ATTESTATION_PRIVATE_KEY};
@@ -109,6 +110,10 @@ pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa
cred_type: PublicKeyCredentialType::PublicKey, cred_type: PublicKeyCredentialType::PublicKey,
alg: SignatureAlgorithm::ES256, 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 { fn check_pin_auth(hmac_key: &[u8], hmac_contents: &[u8], pin_auth: &[u8]) -> bool {
if pin_auth.len() != PIN_AUTH_LENGTH { if pin_auth.len() != PIN_AUTH_LENGTH {
@@ -335,6 +340,7 @@ where
user_handle: vec![], user_handle: vec![],
other_ui: None, other_ui: None,
cred_random: None, cred_random: None,
cred_protect_policy: None,
}) })
} }
@@ -428,30 +434,44 @@ where
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM);
} }
let use_hmac_extension = let (use_hmac_extension, cred_protect_policy) = if let Some(extensions) = extensions {
extensions.map_or(Ok(false), |e| e.has_make_credential_hmac_secret())?; let mut cred_protect = extensions
if use_hmac_extension && !options.rk { .make_credential_cred_protect_policy()
.transpose()?;
if cred_protect.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional)
< DEFAULT_CRED_PROTECT
.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional)
{
cred_protect = DEFAULT_CRED_PROTECT;
}
(extensions.has_make_credential_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. // The extension is actually supported, but we need resident keys.
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION);
} }
let cred_random = if use_hmac_extension {
Some(self.rng.gen_uniform_u8x32().to_vec()) Some(self.rng.gen_uniform_u8x32().to_vec())
} else { } else {
None 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; let rp_id = rp.rp_id;
if let Some(exclude_list) = exclude_list { if let Some(exclude_list) = exclude_list {
for cred_desc in exclude_list { for cred_desc in exclude_list {
if self if self
.persistent_store .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() .is_some()
{ {
// Perform this check, so bad actors can't brute force exclude_list // Perform this check, so bad actors can't brute force exclude_list
// without user interaction. Discard the user presence check's outcome. // without user interaction.
let _ = (self.check_user_presence)(cid); (self.check_user_presence)(cid)?;
return Err(Ctap2StatusCode::CTAP2_ERR_CREDENTIAL_EXCLUDED); return Err(Ctap2StatusCode::CTAP2_ERR_CREDENTIAL_EXCLUDED);
} }
} }
@@ -459,6 +479,7 @@ where
// MakeCredential always requires user presence. // MakeCredential always requires user presence.
// User verification depends on the PIN auth inputs, which are checked here. // 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 { let flags = match pin_uv_auth_param {
Some(pin_auth) => { Some(pin_auth) => {
if self.persistent_store.pin_hash().is_none() { if self.persistent_store.pin_hash().is_none() {
@@ -501,6 +522,7 @@ where
.user_display_name .user_display_name
.map(|s| truncate_to_char_boundary(&s, 64).to_string()), .map(|s| truncate_to_char_boundary(&s, 64).to_string()),
cred_random, cred_random,
cred_protect_policy,
}; };
self.persistent_store.store_credential(credential_source)?; self.persistent_store.store_credential(credential_source)?;
random_id random_id
@@ -521,9 +543,11 @@ where
None => return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR), None => return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR),
}; };
auth_data.extend(cose_key); auth_data.extend(cose_key);
if use_hmac_extension { if has_extension_output {
let extensions = cbor_map! { let hmac_secret_output = if use_hmac_extension { Some(true) } else { None };
"hmac-secret" => true, let extensions = cbor_map_options! {
"hmac-secret" => hmac_secret_output,
"credProtect" => cred_protect_policy,
}; };
if !cbor::write(extensions, &mut auth_data) { if !cbor::write(extensions, &mut auth_data) {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR); return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR);
@@ -624,8 +648,9 @@ where
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION);
} }
// The user verification bit depends on the existance of PIN auth, whereas // The user verification bit depends on the existance of PIN auth, since we do
// user presence is requested as an option. // 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 { let mut flags = match pin_uv_auth_param {
Some(pin_auth) => { Some(pin_auth) => {
if self.persistent_store.pin_hash().is_none() { if self.persistent_store.pin_hash().is_none() {
@@ -657,11 +682,14 @@ where
let credentials = if let Some(allow_list) = allow_list { let credentials = if let Some(allow_list) = allow_list {
let mut found_credentials = vec![]; let mut found_credentials = vec![];
for allowed_credential in allow_list { for allowed_credential in allow_list {
match self match self.persistent_store.find_credential(
.persistent_store &rp_id,
.find_credential(&rp_id, &allowed_credential.key_id) &allowed_credential.key_id,
{ !has_uv,
Some(credential) => found_credentials.push(credential), ) {
Some(credential) => {
found_credentials.push(credential);
}
None => { None => {
if decrypted_credential.is_none() { if decrypted_credential.is_none() {
decrypted_credential = self decrypted_credential = self
@@ -673,7 +701,7 @@ where
found_credentials found_credentials
} else { } else {
// TODO(kaczmarczyck) use GetNextAssertion // 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() { let credential = if let Some(credential) = credentials.first() {
@@ -785,6 +813,7 @@ where
transports: Some(vec![AuthenticatorTransport::Usb]), transports: Some(vec![AuthenticatorTransport::Usb]),
#[cfg(feature = "with_ctap2_1")] #[cfg(feature = "with_ctap2_1")]
algorithms: Some(vec![ES256_CRED_PARAM]), algorithms: Some(vec![ES256_CRED_PARAM]),
default_cred_protect: DEFAULT_CRED_PROTECT,
#[cfg(feature = "with_ctap2_1")] #[cfg(feature = "with_ctap2_1")]
firmware_version: None, firmware_version: None,
}, },
@@ -1173,6 +1202,31 @@ 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 mut extension_map = BTreeMap::new();
extension_map.insert("credProtect".to_string(), cbor::Value::from(policy));
let extensions = Some(Extensions::new(extension_map));
let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.extensions = extensions;
make_credential_params
}
#[test] #[test]
fn test_residential_process_make_credential() { fn test_residential_process_make_credential() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};
@@ -1271,37 +1325,83 @@ mod test {
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present);
let excluded_credential_id = vec![0x01, 0x23, 0x45, 0x67]; 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 { let excluded_credential_source = PublicKeyCredentialSource {
key_type: PublicKeyCredentialType::PublicKey, key_type: PublicKeyCredentialType::PublicKey,
credential_id: excluded_credential_id.clone(), credential_id: excluded_credential_id,
private_key: excluded_private_key, private_key: excluded_private_key,
rp_id: String::from("example.com"), rp_id: String::from("example.com"),
user_handle: vec![], user_handle: vec![],
other_ui: None, other_ui: None,
cred_random: None, cred_random: None,
cred_protect_policy: None,
}; };
assert!(ctap_state assert!(ctap_state
.persistent_store .persistent_store
.store_credential(excluded_credential_source) .store_credential(excluded_credential_source)
.is_ok()); .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 = let make_credential_response =
ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID);
assert_eq!( assert_eq!(
make_credential_response, make_credential_response,
Err(Ctap2StatusCode::CTAP2_ERR_CREDENTIAL_EXCLUDED) 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] #[test]
fn test_process_make_credential_hmac_secret() { fn test_process_make_credential_hmac_secret() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};
@@ -1460,6 +1560,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] #[test]
fn test_process_reset() { fn test_process_reset() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};
@@ -1476,6 +1676,7 @@ mod test {
user_handle: vec![], user_handle: vec![],
other_ui: None, other_ui: None,
cred_random: None, cred_random: None,
cred_protect_policy: None,
}; };
assert!(ctap_state assert!(ctap_state
.persistent_store .persistent_store

View File

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

View File

@@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
use crate::crypto::rng256::Rng256; 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::status_code::Ctap2StatusCode;
use crate::ctap::PIN_AUTH_LENGTH; use crate::ctap::PIN_AUTH_LENGTH;
use alloc::string::String; use alloc::string::String;
@@ -203,6 +203,7 @@ impl PersistentStore {
&self, &self,
rp_id: &str, rp_id: &str,
credential_id: &[u8], credential_id: &[u8],
check_cred_protect: bool,
) -> Option<PublicKeyCredentialSource> { ) -> Option<PublicKeyCredentialSource> {
let key = Key::Credential { let key = Key::Credential {
rp_id: Some(rp_id.into()), rp_id: Some(rp_id.into()),
@@ -213,8 +214,17 @@ impl PersistentStore {
debug_assert_eq!(entry.tag, TAG_CREDENTIAL); debug_assert_eq!(entry.tag, TAG_CREDENTIAL);
let result = deserialize_credential(entry.data); let result = deserialize_credential(entry.data);
debug_assert!(result.is_some()); debug_assert!(result.is_some());
if check_cred_protect
&& result.as_ref().map_or(false, |cred| {
cred.cred_protect_policy
== Some(CredentialProtectionPolicy::UserVerificationRequired)
})
{
None
} else {
result result
} }
}
pub fn store_credential( pub fn store_credential(
&mut self, &mut self,
@@ -245,7 +255,11 @@ impl PersistentStore {
Ok(()) 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 self.store
.find_all(&Key::Credential { .find_all(&Key::Credential {
rp_id: Some(rp_id.into()), rp_id: Some(rp_id.into()),
@@ -258,6 +272,7 @@ impl PersistentStore {
debug_assert!(credential.is_some()); debug_assert!(credential.is_some());
credential credential
}) })
.filter(|cred| !check_cred_protect || cred.is_discoverable())
.collect() .collect()
} }
@@ -440,6 +455,7 @@ mod test {
user_handle, user_handle,
other_ui: None, other_ui: None,
cred_random: None, cred_random: None,
cred_protect_policy: None,
} }
} }
@@ -524,7 +540,7 @@ mod test {
.is_ok()); .is_ok());
assert_eq!(persistent_store.count_credentials(), 1); assert_eq!(persistent_store.count_credentials(), 1);
assert_eq!( assert_eq!(
&persistent_store.filter_credential("example.com"), &persistent_store.filter_credential("example.com", false),
&[expected_credential] &[expected_credential]
); );
@@ -572,7 +588,7 @@ mod test {
.store_credential(credential_source2) .store_credential(credential_source2)
.is_ok()); .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_eq!(filtered_credentials.len(), 2);
assert!( assert!(
(filtered_credentials[0].credential_id == id0 (filtered_credentials[0].credential_id == id0
@@ -582,6 +598,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] #[test]
fn test_find() { fn test_find() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};
@@ -598,9 +638,9 @@ mod test {
.store_credential(credential_source1) .store_credential(credential_source1)
.is_ok()); .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); 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 { let expected_credential = PublicKeyCredentialSource {
key_type: PublicKeyCredentialType::PublicKey, key_type: PublicKeyCredentialType::PublicKey,
credential_id: id0, credential_id: id0,
@@ -609,10 +649,33 @@ mod test {
user_handle: vec![0x00], user_handle: vec![0x00],
other_ui: None, other_ui: None,
cred_random: None, cred_random: None,
cred_protect_policy: None,
}; };
assert_eq!(found_credential, Some(expected_credential)); 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] #[test]
fn test_master_keys() { fn test_master_keys() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};