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
number of supported residential keys and number of pages in
`ctap/storage.rs`.
5. Change the default level for the credProtect extension in `ctap/mod.rs`.
When changing the default, resident credentials become undiscoverable without
user verification. This helps privacy, but can make usage less comfortable
for credentials that need less protection.
### 3D printed enclosure

View File

@@ -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
84d97929d7592d89c7f321ffccafa4148263607e28918e53d9286be8ca55c209 target/nrf52840dk_merged.hex
63dda5b708add5ac72db0c0757de451f95173b0fc7d71fc85c3b25460850e23c target/nrf52840dk_merged.hex
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
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
5315b77b997952de10e239289e54b44c24105646f2411074332bb46f4b686ae6 target/nrf52840_mdk_dfu_merged.hex
9012744b93f8bac122fa18916cf8c22d1b8f729a284366802ed222bbc985e3f0 target/tab/ctap2.tab
dce8f1f02e2f7e93634b1edd02343547e139445d23c2d17ce6ccdc4895a67c8c target/nrf52840_mdk_dfu_merged.hex
1e216599d58e6c66845845a2ec709f72842e21d48a2185022223d1ffe006de07 target/tab/ctap2.tab

View File

@@ -1,9 +1,9 @@
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
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
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
04b94cd65bf83fa12030c4deaa831e0251f5f8b9684ec972d03a46e8f32e98b4 target/nrf52840_mdk_dfu_merged.hex
69dd51b947013b77e3577784384c935ed76930d1fb3ba46e9a5b6b5d71941057 target/tab/ctap2.tab
898842aeaf9a7b03f3f5828871fc8a41fff8fc550683c18f9cfd718f8da26ba4 target/nrf52840_mdk_dfu_merged.hex
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 {
cbor_map_options! {
"type" => cred_param.cred_type,
"alg" => cred_param.alg as i64,
"alg" => cred_param.alg,
}
}
}
// 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 {
Usb,
Nfc,
@@ -197,7 +197,7 @@ impl TryFrom<&cbor::Value> for AuthenticatorTransport {
}
// 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 key_type: PublicKeyCredentialType,
pub key_id: Vec<u8>,
@@ -292,6 +292,14 @@ impl Extensions {
.get("hmac-secret")
.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))]
@@ -421,6 +429,12 @@ pub enum SignatureAlgorithm {
Unknown = 0,
}
impl From<SignatureAlgorithm> for cbor::Value {
fn from(alg: SignatureAlgorithm) -> Self {
(alg as i64).into()
}
}
impl TryFrom<&cbor::Value> for SignatureAlgorithm {
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
//
// 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 other_ui: Option<String>,
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
@@ -459,6 +514,7 @@ enum PublicKeyCredentialSourceField {
UserHandle = 3,
OtherUi = 4,
CredRandom = 5,
CredProtectPolicy = 6,
// When a field is removed, its tag should be reserved and not used for new fields. We document
// those reserved tags below.
// Reserved tags: none.
@@ -481,7 +537,8 @@ impl From<PublicKeyCredentialSource> for cbor::Value {
RpId => Some(credential.rp_id),
UserHandle => Some(credential.user_handle),
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())
.map(extract_byte_string)
.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
// 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:
@@ -527,10 +588,20 @@ impl TryFrom<cbor::Value> for PublicKeyCredentialSource {
user_handle,
other_ui,
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
// It depends on the algorithm though, I think.
// 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> {
match cbor_value {
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 expected_signature_algorithm = SignatureAlgorithm::ES256;
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);
let cbor_unknown_algorithm = cbor_int!(-1);
@@ -1073,6 +1158,37 @@ mod test {
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]
fn test_from_into_authenticator_transport() {
let cbor_authenticator_transport = cbor_text!("usb");
@@ -1183,6 +1299,18 @@ mod test {
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]
fn test_from_make_credential_options() {
let cbor_make_options = cbor_map! {
@@ -1261,6 +1389,7 @@ mod test {
user_handle: b"foo".to_vec(),
other_ui: None,
cred_random: None,
cred_protect_policy: None,
};
assert_eq!(
@@ -1283,6 +1412,16 @@ mod test {
..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!(
PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())),
Ok(credential)

View File

@@ -32,9 +32,10 @@ use self::command::{
#[cfg(feature = "with_ctap2_1")]
use self::data_formats::AuthenticatorTransport;
use self::data_formats::{
ClientPinSubCommand, CoseKey, GetAssertionHmacSecretInput, PackedAttestationStatement,
PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialSource,
PublicKeyCredentialType, PublicKeyCredentialUserEntity, SignatureAlgorithm,
ClientPinSubCommand, CoseKey, CredentialProtectionPolicy, GetAssertionHmacSecretInput,
PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter,
PublicKeyCredentialSource, PublicKeyCredentialType, PublicKeyCredentialUserEntity,
SignatureAlgorithm,
};
use self::hid::ChannelID;
use self::key_material::{AAGUID, ATTESTATION_CERTIFICATE, ATTESTATION_PRIVATE_KEY};
@@ -109,6 +110,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 {
@@ -335,6 +340,7 @@ where
user_handle: vec![],
other_ui: None,
cred_random: None,
cred_protect_policy: None,
})
}
@@ -428,30 +434,44 @@ 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
.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.
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);
}
}
@@ -459,6 +479,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() {
@@ -501,6 +522,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
@@ -521,9 +543,11 @@ 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 = cbor_map_options! {
"hmac-secret" => hmac_secret_output,
"credProtect" => cred_protect_policy,
};
if !cbor::write(extensions, &mut auth_data) {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR);
@@ -624,8 +648,9 @@ where
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() {
@@ -657,11 +682,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
@@ -673,7 +701,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() {
@@ -785,6 +813,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,
},
@@ -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]
fn test_residential_process_make_credential() {
let mut rng = ThreadRng256 {};
@@ -1271,37 +1325,83 @@ 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 {};
@@ -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]
fn test_process_reset() {
let mut rng = ThreadRng256 {};
@@ -1476,6 +1676,7 @@ mod test {
user_handle: vec![],
other_ui: None,
cred_random: None,
cred_protect_policy: None,
};
assert!(ctap_state
.persistent_store

View File

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

View File

@@ -13,7 +13,7 @@
// limitations under the License.
use crate::crypto::rng256::Rng256;
use crate::ctap::data_formats::PublicKeyCredentialSource;
use crate::ctap::data_formats::{CredentialProtectionPolicy, PublicKeyCredentialSource};
use crate::ctap::status_code::Ctap2StatusCode;
use crate::ctap::PIN_AUTH_LENGTH;
use alloc::string::String;
@@ -203,6 +203,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()),
@@ -213,7 +214,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(
@@ -245,7 +255,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()),
@@ -258,6 +272,7 @@ impl PersistentStore {
debug_assert!(credential.is_some());
credential
})
.filter(|cred| !check_cred_protect || cred.is_discoverable())
.collect()
}
@@ -440,6 +455,7 @@ mod test {
user_handle,
other_ui: None,
cred_random: None,
cred_protect_policy: None,
}
}
@@ -524,7 +540,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]
);
@@ -572,7 +588,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
@@ -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]
fn test_find() {
let mut rng = ThreadRng256 {};
@@ -598,9 +638,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,
@@ -609,10 +649,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 {};