Merge pull request #127 from kaczmarczyck/client-pin-features
Client pin features
This commit is contained in:
@@ -31,6 +31,7 @@ with_ctap2_1 = []
|
||||
|
||||
[dev-dependencies]
|
||||
elf2tab = "0.4.0"
|
||||
enum-iterator = "0.6.0"
|
||||
|
||||
[build-dependencies]
|
||||
openssl = "0.10"
|
||||
|
||||
@@ -103,6 +103,13 @@ a few things you can personalize:
|
||||
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.
|
||||
6. Increase the default minimum length for PINs in `ctap/storage.rs`.
|
||||
The current minimum is 4. Values from 4 to 63 are allowed. Requiring longer
|
||||
PINs can help establish trust between users and relying parties. It makes
|
||||
user verification harder to break, but less convenient.
|
||||
NIST recommends at least 6-digit PINs in section 5.1.9.1:
|
||||
https://pages.nist.gov/800-63-3/sp800-63b.html
|
||||
You can add relying parties to the list of readers of the minimum PIN length.
|
||||
|
||||
### 3D printed enclosure
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
91a98f475cb3042dd5184598a8292edb2a414df8d967a35c8f2295826b5a161b third_party/tock/target/thumbv7em-none-eabi/release/nrf52840dk.bin
|
||||
33164f39a0b5354cdf61236c301242476284c6b96d55275aa603734054ca7928 target/nrf52840dk_merged.hex
|
||||
7cc5c802e22e73c1edfd5b890642c5f6c4a1f888b61f0cd6d638a770eb0af739 target/nrf52840dk_merged.hex
|
||||
a5943c5311158b0f99370246d37782eb9b12fc36c56387eadb6587a3a4fe8fd5 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle.bin
|
||||
1232b44947f302900291692690f2e94cdfb165e00e74c682433100882754a516 target/nrf52840_dongle_merged.hex
|
||||
9ff31263bd33e92b5f1f59d83f557046cb4d022919a5082931a197a2f6ec4398 target/nrf52840_dongle_merged.hex
|
||||
663297e3e29b9e2a972b68cea1592aaf965d797242579bb5bca09cd73cdfb637 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle_dfu.bin
|
||||
b95ce848465523e98cf0c30f94f6430e99dc8ac4b33da5bc0d0f643523ff4b50 target/nrf52840_dongle_dfu_merged.hex
|
||||
4b1f17b3dda2460fe83adc157f8ae1fb2559fb151b8897806e7b0aa25c898ec1 target/nrf52840_dongle_dfu_merged.hex
|
||||
162a05d056aafc16d4868d5c3aa10518e41299dddd60608f96954dc9cf964cd3 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_mdk_dfu.bin
|
||||
1085e1789c4429430c47d28b23a975223717eddd7c8aa23114acbc3ec2ec7080 target/nrf52840_mdk_dfu_merged.hex
|
||||
5bd063ce44e9ddcad8c4d17165a247387e4f1a9c6db81060fbb97244be1929b8 target/tab/ctap2.tab
|
||||
90369c2f5c1b3b3a443114be069fd2da0806444865830a7e992ed52e036c5a39 target/nrf52840_mdk_dfu_merged.hex
|
||||
299201ff87cd84bd767516143b4e6a54759e01fcd864c0e579c62217b21d4fa4 target/tab/ctap2.tab
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
3feb5d29a3d669107b460a00391440be4ebc5e50461f9ef3248714f4f99c070e third_party/tock/target/thumbv7em-none-eabi/release/nrf52840dk.bin
|
||||
a02f078e165373113adbaf7fa5d272e7e01134061e8212331d54f0b0a8809aaa target/nrf52840dk_merged.hex
|
||||
875fdc2bbd473a5c77c119ba860e54a43f8097c20931cc5ae83a26e9311ce124 target/nrf52840dk_merged.hex
|
||||
8eebe1c1dfe22003466c2570b3735c54c58ae91b8168582ad363ab79c9230a15 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle.bin
|
||||
973bf7d0b6ddb37bb9698cf8f2ef3c2a3dd27cd482b7a4c02e452902394ffa37 target/nrf52840_dongle_merged.hex
|
||||
70c1249370144c6ca55ad490dc5e418f9c2994c2649941dec41d769963a0e0ad target/nrf52840_dongle_merged.hex
|
||||
779d77071d1e629f92210ac313e230bcaea6d77c710210c1ac4b40f8085cdad7 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle_dfu.bin
|
||||
d0e7ecc1d2a45ef4c77b39720b95b3e349a0d48d7b9ca99fa591019a9f2cafee target/nrf52840_dongle_dfu_merged.hex
|
||||
6c12edd4ec4d952619e976e635df39235d821eec9902e8882563fc43a1690ddb target/nrf52840_dongle_dfu_merged.hex
|
||||
f466490d6498f6c06c7c4a717eb437ba2fb06d1985532c23f145d38b9daa8259 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_mdk_dfu.bin
|
||||
d3d4a9d3442bb8cf924f553f8df7085e3d6331f1b6d9557115d485e584285d68 target/nrf52840_mdk_dfu_merged.hex
|
||||
6cda1346503867ef18d3fe7a3d32de6e22585c6134ef3347877894c5469390f5 target/tab/ctap2.tab
|
||||
7b67e726071ac5161344212821b9869c8f289559c8b91a5f2a0f17624855ce8a target/nrf52840_mdk_dfu_merged.hex
|
||||
4dd8753dba382bdbadd0c9761949f7bdacbd77408cfc8dc466107a81ff664b15 target/tab/ctap2.tab
|
||||
|
||||
@@ -5,8 +5,8 @@ Min RAM size from sections in ELF: 20 bytes
|
||||
Number of writeable flash regions: 0
|
||||
Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes.
|
||||
Entry point is in .text section
|
||||
Adding .text section. Offset: 128 (0x80). Length: 187792 (0x2dd90) bytes.
|
||||
Adding .stack section. Offset: 187920 (0x2de10). Length: 16384 (0x4000) bytes.
|
||||
Adding .text section. Offset: 128 (0x80). Length: 178688 (0x2ba00) bytes.
|
||||
Adding .stack section. Offset: 178816 (0x2ba80). Length: 16384 (0x4000) bytes.
|
||||
Searching for .rel.X sections to add.
|
||||
TBF Header:
|
||||
version: 2 0x2
|
||||
@@ -24,8 +24,8 @@ Min RAM size from sections in ELF: 20 bytes
|
||||
Number of writeable flash regions: 0
|
||||
Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes.
|
||||
Entry point is in .text section
|
||||
Adding .text section. Offset: 128 (0x80). Length: 187792 (0x2dd90) bytes.
|
||||
Adding .stack section. Offset: 187920 (0x2de10). Length: 16384 (0x4000) bytes.
|
||||
Adding .text section. Offset: 128 (0x80). Length: 178688 (0x2ba00) bytes.
|
||||
Adding .stack section. Offset: 178816 (0x2ba80). Length: 16384 (0x4000) bytes.
|
||||
Searching for .rel.X sections to add.
|
||||
TBF Header:
|
||||
version: 2 0x2
|
||||
@@ -43,8 +43,8 @@ Min RAM size from sections in ELF: 20 bytes
|
||||
Number of writeable flash regions: 0
|
||||
Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes.
|
||||
Entry point is in .text section
|
||||
Adding .text section. Offset: 128 (0x80). Length: 187792 (0x2dd90) bytes.
|
||||
Adding .stack section. Offset: 187920 (0x2de10). Length: 16384 (0x4000) bytes.
|
||||
Adding .text section. Offset: 128 (0x80). Length: 178688 (0x2ba00) bytes.
|
||||
Adding .stack section. Offset: 178816 (0x2ba80). Length: 16384 (0x4000) bytes.
|
||||
Searching for .rel.X sections to add.
|
||||
TBF Header:
|
||||
version: 2 0x2
|
||||
@@ -62,8 +62,8 @@ Min RAM size from sections in ELF: 20 bytes
|
||||
Number of writeable flash regions: 0
|
||||
Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes.
|
||||
Entry point is in .text section
|
||||
Adding .text section. Offset: 128 (0x80). Length: 187792 (0x2dd90) bytes.
|
||||
Adding .stack section. Offset: 187920 (0x2de10). Length: 16384 (0x4000) bytes.
|
||||
Adding .text section. Offset: 128 (0x80). Length: 178688 (0x2ba00) bytes.
|
||||
Adding .stack section. Offset: 178816 (0x2ba80). Length: 16384 (0x4000) bytes.
|
||||
Searching for .rel.X sections to add.
|
||||
TBF Header:
|
||||
version: 2 0x2
|
||||
|
||||
@@ -5,8 +5,8 @@ Min RAM size from sections in ELF: 20 bytes
|
||||
Number of writeable flash regions: 0
|
||||
Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes.
|
||||
Entry point is in .text section
|
||||
Adding .text section. Offset: 128 (0x80). Length: 187736 (0x2dd58) bytes.
|
||||
Adding .stack section. Offset: 187864 (0x2ddd8). Length: 16384 (0x4000) bytes.
|
||||
Adding .text section. Offset: 128 (0x80). Length: 178536 (0x2b968) bytes.
|
||||
Adding .stack section. Offset: 178664 (0x2b9e8). Length: 16384 (0x4000) bytes.
|
||||
Searching for .rel.X sections to add.
|
||||
TBF Header:
|
||||
version: 2 0x2
|
||||
@@ -24,8 +24,8 @@ Min RAM size from sections in ELF: 20 bytes
|
||||
Number of writeable flash regions: 0
|
||||
Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes.
|
||||
Entry point is in .text section
|
||||
Adding .text section. Offset: 128 (0x80). Length: 187736 (0x2dd58) bytes.
|
||||
Adding .stack section. Offset: 187864 (0x2ddd8). Length: 16384 (0x4000) bytes.
|
||||
Adding .text section. Offset: 128 (0x80). Length: 178536 (0x2b968) bytes.
|
||||
Adding .stack section. Offset: 178664 (0x2b9e8). Length: 16384 (0x4000) bytes.
|
||||
Searching for .rel.X sections to add.
|
||||
TBF Header:
|
||||
version: 2 0x2
|
||||
@@ -43,8 +43,8 @@ Min RAM size from sections in ELF: 20 bytes
|
||||
Number of writeable flash regions: 0
|
||||
Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes.
|
||||
Entry point is in .text section
|
||||
Adding .text section. Offset: 128 (0x80). Length: 187736 (0x2dd58) bytes.
|
||||
Adding .stack section. Offset: 187864 (0x2ddd8). Length: 16384 (0x4000) bytes.
|
||||
Adding .text section. Offset: 128 (0x80). Length: 178536 (0x2b968) bytes.
|
||||
Adding .stack section. Offset: 178664 (0x2b9e8). Length: 16384 (0x4000) bytes.
|
||||
Searching for .rel.X sections to add.
|
||||
TBF Header:
|
||||
version: 2 0x2
|
||||
@@ -62,8 +62,8 @@ Min RAM size from sections in ELF: 20 bytes
|
||||
Number of writeable flash regions: 0
|
||||
Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes.
|
||||
Entry point is in .text section
|
||||
Adding .text section. Offset: 128 (0x80). Length: 187736 (0x2dd58) bytes.
|
||||
Adding .stack section. Offset: 187864 (0x2ddd8). Length: 16384 (0x4000) bytes.
|
||||
Adding .text section. Offset: 128 (0x80). Length: 178536 (0x2b968) bytes.
|
||||
Adding .stack section. Offset: 178664 (0x2b9e8). Length: 16384 (0x4000) bytes.
|
||||
Searching for .rel.X sections to add.
|
||||
TBF Header:
|
||||
version: 2 0x2
|
||||
|
||||
@@ -278,12 +278,21 @@ pub struct AuthenticatorClientPinParameters {
|
||||
pub pin_auth: Option<Vec<u8>>,
|
||||
pub new_pin_enc: Option<Vec<u8>>,
|
||||
pub pin_hash_enc: Option<Vec<u8>>,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
pub min_pin_length: Option<u8>,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
pub min_pin_length_rp_ids: Option<Vec<String>>,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
pub permissions: Option<u8>,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
pub permissions_rp_id: Option<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<cbor::Value> for AuthenticatorClientPinParameters {
|
||||
type Error = Ctap2StatusCode;
|
||||
|
||||
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
|
||||
#[cfg(not(feature = "with_ctap2_1"))]
|
||||
destructure_cbor_map! {
|
||||
let {
|
||||
1 => pin_protocol,
|
||||
@@ -294,6 +303,21 @@ impl TryFrom<cbor::Value> for AuthenticatorClientPinParameters {
|
||||
6 => pin_hash_enc,
|
||||
} = extract_map(cbor_value)?;
|
||||
}
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
destructure_cbor_map! {
|
||||
let {
|
||||
1 => pin_protocol,
|
||||
2 => sub_command,
|
||||
3 => key_agreement,
|
||||
4 => pin_auth,
|
||||
5 => new_pin_enc,
|
||||
6 => pin_hash_enc,
|
||||
7 => min_pin_length,
|
||||
8 => min_pin_length_rp_ids,
|
||||
9 => permissions,
|
||||
10 => permissions_rp_id,
|
||||
} = extract_map(cbor_value)?;
|
||||
}
|
||||
|
||||
let pin_protocol = extract_unsigned(ok_or_missing(pin_protocol)?)?;
|
||||
let sub_command = ClientPinSubCommand::try_from(ok_or_missing(sub_command)?)?;
|
||||
@@ -304,6 +328,32 @@ impl TryFrom<cbor::Value> for AuthenticatorClientPinParameters {
|
||||
let pin_auth = pin_auth.map(extract_byte_string).transpose()?;
|
||||
let new_pin_enc = new_pin_enc.map(extract_byte_string).transpose()?;
|
||||
let pin_hash_enc = pin_hash_enc.map(extract_byte_string).transpose()?;
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
let min_pin_length = min_pin_length
|
||||
.map(extract_unsigned)
|
||||
.transpose()?
|
||||
.map(u8::try_from)
|
||||
.transpose()
|
||||
.map_err(|_| Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)?;
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
let min_pin_length_rp_ids = match min_pin_length_rp_ids {
|
||||
Some(entry) => Some(
|
||||
extract_array(entry)?
|
||||
.into_iter()
|
||||
.map(extract_text_string)
|
||||
.collect::<Result<Vec<String>, Ctap2StatusCode>>()?,
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
// We expect a bit field of 8 bits, and drop everything else.
|
||||
// This means we ignore extensions in future versions.
|
||||
let permissions = permissions
|
||||
.map(extract_unsigned)
|
||||
.transpose()?
|
||||
.map(|p| p as u8);
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
let permissions_rp_id = permissions_rp_id.map(extract_text_string).transpose()?;
|
||||
|
||||
Ok(AuthenticatorClientPinParameters {
|
||||
pin_protocol,
|
||||
@@ -312,6 +362,14 @@ impl TryFrom<cbor::Value> for AuthenticatorClientPinParameters {
|
||||
pin_auth,
|
||||
new_pin_enc,
|
||||
pin_hash_enc,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
min_pin_length,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
min_pin_length_rp_ids,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
permissions,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
permissions_rp_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -434,6 +492,9 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_from_cbor_client_pin_parameters() {
|
||||
// TODO(kaczmarczyck) inline the #cfg when #128 is resolved:
|
||||
// https://github.com/google/OpenSK/issues/128
|
||||
#[cfg(not(feature = "with_ctap2_1"))]
|
||||
let cbor_value = cbor_map! {
|
||||
1 => 1,
|
||||
2 => ClientPinSubCommand::GetPinRetries,
|
||||
@@ -442,6 +503,19 @@ mod test {
|
||||
5 => vec! [0xCC],
|
||||
6 => vec! [0xDD],
|
||||
};
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
let cbor_value = cbor_map! {
|
||||
1 => 1,
|
||||
2 => ClientPinSubCommand::GetPinRetries,
|
||||
3 => cbor_map!{},
|
||||
4 => vec! [0xBB],
|
||||
5 => vec! [0xCC],
|
||||
6 => vec! [0xDD],
|
||||
7 => 4,
|
||||
8 => cbor_array!["example.com"],
|
||||
9 => 0x03,
|
||||
10 => "example.com",
|
||||
};
|
||||
let returned_pin_protocol_parameters =
|
||||
AuthenticatorClientPinParameters::try_from(cbor_value).unwrap();
|
||||
|
||||
@@ -452,6 +526,14 @@ mod test {
|
||||
pin_auth: Some(vec![0xBB]),
|
||||
new_pin_enc: Some(vec![0xCC]),
|
||||
pin_hash_enc: Some(vec![0xDD]),
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
min_pin_length: Some(4),
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
min_pin_length_rp_ids: Some(vec!["example.com".to_string()]),
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
permissions: Some(0x03),
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
permissions_rp_id: Some("example.com".to_string()),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -18,6 +18,8 @@ use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use core::convert::TryFrom;
|
||||
use crypto::{ecdh, ecdsa};
|
||||
#[cfg(test)]
|
||||
use enum_iterator::IntoEnumIterator;
|
||||
|
||||
// https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrpentity
|
||||
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
|
||||
@@ -167,6 +169,7 @@ impl From<PublicKeyCredentialParameter> for cbor::Value {
|
||||
|
||||
// https://www.w3.org/TR/webauthn/#enumdef-authenticatortransport
|
||||
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))]
|
||||
#[cfg_attr(test, derive(IntoEnumIterator))]
|
||||
pub enum AuthenticatorTransport {
|
||||
Usb,
|
||||
Nfc,
|
||||
@@ -453,6 +456,7 @@ impl TryFrom<cbor::Value> for SignatureAlgorithm {
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, PartialOrd)]
|
||||
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))]
|
||||
#[cfg_attr(test, derive(IntoEnumIterator))]
|
||||
pub enum CredentialProtectionPolicy {
|
||||
UserVerificationOptional = 0x01,
|
||||
UserVerificationOptionalWithCredentialIdList = 0x02,
|
||||
@@ -520,17 +524,16 @@ impl From<PublicKeyCredentialSourceField> for cbor::KeyType {
|
||||
|
||||
impl From<PublicKeyCredentialSource> for cbor::Value {
|
||||
fn from(credential: PublicKeyCredentialSource) -> cbor::Value {
|
||||
use PublicKeyCredentialSourceField::*;
|
||||
let mut private_key = [0u8; 32];
|
||||
credential.private_key.to_bytes(&mut private_key);
|
||||
cbor_map_options! {
|
||||
CredentialId => Some(credential.credential_id),
|
||||
PrivateKey => Some(private_key.to_vec()),
|
||||
RpId => Some(credential.rp_id),
|
||||
UserHandle => Some(credential.user_handle),
|
||||
OtherUi => credential.other_ui,
|
||||
CredRandom => credential.cred_random,
|
||||
CredProtectPolicy => credential.cred_protect_policy,
|
||||
PublicKeyCredentialSourceField::CredentialId => Some(credential.credential_id),
|
||||
PublicKeyCredentialSourceField::PrivateKey => Some(private_key.to_vec()),
|
||||
PublicKeyCredentialSourceField::RpId => Some(credential.rp_id),
|
||||
PublicKeyCredentialSourceField::UserHandle => Some(credential.user_handle),
|
||||
PublicKeyCredentialSourceField::OtherUi => credential.other_ui,
|
||||
PublicKeyCredentialSourceField::CredRandom => credential.cred_random,
|
||||
PublicKeyCredentialSourceField::CredProtectPolicy => credential.cred_protect_policy,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -539,18 +542,15 @@ impl TryFrom<cbor::Value> for PublicKeyCredentialSource {
|
||||
type Error = Ctap2StatusCode;
|
||||
|
||||
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
|
||||
use PublicKeyCredentialSourceField::{
|
||||
CredProtectPolicy, CredRandom, CredentialId, OtherUi, PrivateKey, RpId, UserHandle,
|
||||
};
|
||||
destructure_cbor_map! {
|
||||
let {
|
||||
CredentialId => credential_id,
|
||||
PrivateKey => private_key,
|
||||
RpId => rp_id,
|
||||
UserHandle => user_handle,
|
||||
OtherUi => other_ui,
|
||||
CredRandom => cred_random,
|
||||
CredProtectPolicy => cred_protect_policy,
|
||||
PublicKeyCredentialSourceField::CredentialId => credential_id,
|
||||
PublicKeyCredentialSourceField::PrivateKey => private_key,
|
||||
PublicKeyCredentialSourceField::RpId => rp_id,
|
||||
PublicKeyCredentialSourceField::UserHandle => user_handle,
|
||||
PublicKeyCredentialSourceField::OtherUi => other_ui,
|
||||
PublicKeyCredentialSourceField::CredRandom => cred_random,
|
||||
PublicKeyCredentialSourceField::CredProtectPolicy => cred_protect_policy,
|
||||
} = extract_map(cbor_value)?;
|
||||
}
|
||||
|
||||
@@ -681,29 +681,27 @@ impl TryFrom<CoseKey> for ecdh::PubKey {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
|
||||
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))]
|
||||
#[cfg_attr(test, derive(IntoEnumIterator))]
|
||||
pub enum ClientPinSubCommand {
|
||||
GetPinRetries,
|
||||
GetKeyAgreement,
|
||||
SetPin,
|
||||
ChangePin,
|
||||
GetPinUvAuthTokenUsingPin,
|
||||
GetPinUvAuthTokenUsingUv,
|
||||
GetUvRetries,
|
||||
GetPinRetries = 0x01,
|
||||
GetKeyAgreement = 0x02,
|
||||
SetPin = 0x03,
|
||||
ChangePin = 0x04,
|
||||
GetPinToken = 0x05,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
GetPinUvAuthTokenUsingUvWithPermissions = 0x06,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
GetUvRetries = 0x07,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
SetMinPinLength = 0x08,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
GetPinUvAuthTokenUsingPinWithPermissions = 0x09,
|
||||
}
|
||||
|
||||
impl From<ClientPinSubCommand> for cbor::Value {
|
||||
fn from(subcommand: ClientPinSubCommand) -> Self {
|
||||
match subcommand {
|
||||
ClientPinSubCommand::GetPinRetries => 0x01,
|
||||
ClientPinSubCommand::GetKeyAgreement => 0x02,
|
||||
ClientPinSubCommand::SetPin => 0x03,
|
||||
ClientPinSubCommand::ChangePin => 0x04,
|
||||
ClientPinSubCommand::GetPinUvAuthTokenUsingPin => 0x05,
|
||||
ClientPinSubCommand::GetPinUvAuthTokenUsingUv => 0x06,
|
||||
ClientPinSubCommand::GetUvRetries => 0x07,
|
||||
}
|
||||
.into()
|
||||
(subcommand as u64).into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -717,10 +715,18 @@ impl TryFrom<cbor::Value> for ClientPinSubCommand {
|
||||
0x02 => Ok(ClientPinSubCommand::GetKeyAgreement),
|
||||
0x03 => Ok(ClientPinSubCommand::SetPin),
|
||||
0x04 => Ok(ClientPinSubCommand::ChangePin),
|
||||
0x05 => Ok(ClientPinSubCommand::GetPinUvAuthTokenUsingPin),
|
||||
0x06 => Ok(ClientPinSubCommand::GetPinUvAuthTokenUsingUv),
|
||||
0x05 => Ok(ClientPinSubCommand::GetPinToken),
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
0x06 => Ok(ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions),
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
0x07 => Ok(ClientPinSubCommand::GetUvRetries),
|
||||
// TODO(kaczmarczyck) what is the correct status code for this error?
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
0x08 => Ok(ClientPinSubCommand::SetMinPinLength),
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
0x09 => Ok(ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions),
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
_ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND),
|
||||
#[cfg(not(feature = "with_ctap2_1"))]
|
||||
_ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER),
|
||||
}
|
||||
}
|
||||
@@ -1157,6 +1163,12 @@ mod test {
|
||||
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);
|
||||
|
||||
for policy in CredentialProtectionPolicy::into_enum_iter() {
|
||||
let created_cbor: cbor::Value = policy.into();
|
||||
let reconstructed = CredentialProtectionPolicy::try_from(created_cbor).unwrap();
|
||||
assert_eq!(policy, reconstructed);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1171,6 +1183,12 @@ mod test {
|
||||
);
|
||||
let created_cbor: cbor::Value = authenticator_transport.unwrap().into();
|
||||
assert_eq!(created_cbor, cbor_authenticator_transport);
|
||||
|
||||
for transport in AuthenticatorTransport::into_enum_iter() {
|
||||
let created_cbor: cbor::Value = transport.clone().into();
|
||||
let reconstructed = AuthenticatorTransport::try_from(created_cbor).unwrap();
|
||||
assert_eq!(transport, reconstructed);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1313,6 +1331,12 @@ mod test {
|
||||
assert_eq!(sub_command, Ok(expected_sub_command));
|
||||
let created_cbor: cbor::Value = sub_command.unwrap().into();
|
||||
assert_eq!(created_cbor, cbor_sub_command);
|
||||
|
||||
for command in ClientPinSubCommand::into_enum_iter() {
|
||||
let created_cbor: cbor::Value = command.clone().into();
|
||||
let reconstructed = ClientPinSubCommand::try_from(created_cbor).unwrap();
|
||||
assert_eq!(command, reconstructed);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
446
src/ctap/mod.rs
446
src/ctap/mod.rs
@@ -18,6 +18,7 @@ mod ctap1;
|
||||
pub mod data_formats;
|
||||
pub mod hid;
|
||||
mod key_material;
|
||||
mod pin_protocol_v1;
|
||||
pub mod response;
|
||||
pub mod status_code;
|
||||
mod storage;
|
||||
@@ -32,15 +33,17 @@ use self::command::{
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
use self::data_formats::AuthenticatorTransport;
|
||||
use self::data_formats::{
|
||||
ClientPinSubCommand, CoseKey, CredentialProtectionPolicy, GetAssertionHmacSecretInput,
|
||||
PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter,
|
||||
PublicKeyCredentialSource, PublicKeyCredentialType, PublicKeyCredentialUserEntity,
|
||||
SignatureAlgorithm,
|
||||
CredentialProtectionPolicy, PackedAttestationStatement, PublicKeyCredentialDescriptor,
|
||||
PublicKeyCredentialParameter, PublicKeyCredentialSource, PublicKeyCredentialType,
|
||||
PublicKeyCredentialUserEntity, SignatureAlgorithm,
|
||||
};
|
||||
use self::hid::ChannelID;
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
use self::pin_protocol_v1::PinPermission;
|
||||
use self::pin_protocol_v1::PinProtocolV1;
|
||||
use self::response::{
|
||||
AuthenticatorClientPinResponse, AuthenticatorGetAssertionResponse,
|
||||
AuthenticatorGetInfoResponse, AuthenticatorMakeCredentialResponse, ResponseData,
|
||||
AuthenticatorGetAssertionResponse, AuthenticatorGetInfoResponse,
|
||||
AuthenticatorMakeCredentialResponse, ResponseData,
|
||||
};
|
||||
use self::status_code::Ctap2StatusCode;
|
||||
use self::storage::PersistentStore;
|
||||
@@ -50,18 +53,16 @@ use alloc::collections::BTreeMap;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
use byteorder::{BigEndian, ByteOrder};
|
||||
use core::convert::TryInto;
|
||||
#[cfg(feature = "debug_ctap")]
|
||||
use core::fmt::Write;
|
||||
use crypto::cbc::{cbc_decrypt, cbc_encrypt};
|
||||
use crypto::hmac::{hmac_256, verify_hmac_256, verify_hmac_256_first_128bits};
|
||||
use crypto::hmac::{hmac_256, verify_hmac_256};
|
||||
use crypto::rng256::Rng256;
|
||||
use crypto::sha256::Sha256;
|
||||
use crypto::Hash256;
|
||||
#[cfg(feature = "debug_ctap")]
|
||||
use libtock_drivers::console::Console;
|
||||
use libtock_drivers::timer::{Duration, Timestamp};
|
||||
use subtle::ConstantTimeEq;
|
||||
|
||||
// This flag enables or disables basic attestation for FIDO2. U2F is unaffected by
|
||||
// this setting. The basic attestation uses the signing key from key_material.rs
|
||||
@@ -75,10 +76,6 @@ const USE_BATCH_ATTESTATION: bool = false;
|
||||
// need a flash storage friendly way to implement this feature. The implemented
|
||||
// solution is a compromise to be compatible with U2F and not wasting storage.
|
||||
const USE_SIGNATURE_COUNTER: bool = true;
|
||||
// Those constants have to be multiples of 16, the AES block size.
|
||||
const PIN_AUTH_LENGTH: usize = 16;
|
||||
const PIN_TOKEN_LENGTH: usize = 32;
|
||||
const PIN_PADDED_LENGTH: usize = 64;
|
||||
// Our credential ID consists of
|
||||
// - 16 byte initialization vector for AES-256,
|
||||
// - 32 byte ECDSA private key for the credential,
|
||||
@@ -114,74 +111,6 @@ pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa
|
||||
// - 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 {
|
||||
return false;
|
||||
}
|
||||
verify_hmac_256_first_128bits::<Sha256>(
|
||||
hmac_key,
|
||||
hmac_contents,
|
||||
array_ref![pin_auth, 0, PIN_AUTH_LENGTH],
|
||||
)
|
||||
}
|
||||
|
||||
// Decrypts the HMAC secret salt(s) that were encrypted with the shared secret.
|
||||
// The credRandom is used as a secret to HMAC those salts.
|
||||
// The last step is to re-encrypt the outputs.
|
||||
pub fn encrypt_hmac_secret_output(
|
||||
shared_secret: &[u8; 32],
|
||||
salt_enc: &[u8],
|
||||
cred_random: &[u8],
|
||||
) -> Result<Vec<u8>, Ctap2StatusCode> {
|
||||
if salt_enc.len() != 32 && salt_enc.len() != 64 {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION);
|
||||
}
|
||||
if cred_random.len() != 32 {
|
||||
// We are strict here. We need at least 32 byte, but expect exactly 32.
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION);
|
||||
}
|
||||
let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret);
|
||||
let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key);
|
||||
// The specification specifically asks for a zero IV.
|
||||
let iv = [0; 16];
|
||||
|
||||
let mut cred_random_secret = [0; 32];
|
||||
cred_random_secret.clone_from_slice(cred_random);
|
||||
|
||||
// Initialization of 4 blocks in any case makes this function more readable.
|
||||
let mut blocks = [[0u8; 16]; 4];
|
||||
let block_len = salt_enc.len() / 16;
|
||||
for i in 0..block_len {
|
||||
blocks[i].copy_from_slice(&salt_enc[16 * i..16 * (i + 1)]);
|
||||
}
|
||||
cbc_decrypt(&aes_dec_key, iv, &mut blocks[..block_len]);
|
||||
|
||||
let mut decrypted_salt1 = [0; 32];
|
||||
decrypted_salt1[..16].clone_from_slice(&blocks[0]);
|
||||
let output1 = hmac_256::<Sha256>(&cred_random_secret, &decrypted_salt1[..]);
|
||||
decrypted_salt1[16..].clone_from_slice(&blocks[1]);
|
||||
for i in 0..2 {
|
||||
blocks[i].copy_from_slice(&output1[16 * i..16 * (i + 1)]);
|
||||
}
|
||||
|
||||
if block_len == 4 {
|
||||
let mut decrypted_salt2 = [0; 32];
|
||||
decrypted_salt2[..16].clone_from_slice(&blocks[2]);
|
||||
decrypted_salt2[16..].clone_from_slice(&blocks[3]);
|
||||
let output2 = hmac_256::<Sha256>(&cred_random_secret, &decrypted_salt2[..]);
|
||||
for i in 0..2 {
|
||||
blocks[i + 2].copy_from_slice(&output2[16 * i..16 * (i + 1)]);
|
||||
}
|
||||
}
|
||||
|
||||
cbc_encrypt(&aes_enc_key, iv, &mut blocks[..block_len]);
|
||||
let mut encrypted_output = Vec::with_capacity(salt_enc.len());
|
||||
for b in &blocks[..block_len] {
|
||||
encrypted_output.extend(b);
|
||||
}
|
||||
Ok(encrypted_output)
|
||||
}
|
||||
|
||||
// This function is adapted from https://doc.rust-lang.org/nightly/src/core/str/mod.rs.html#2110
|
||||
// (as of 2020-01-20) and truncates to "max" bytes, not breaking the encoding.
|
||||
// We change the return value, since we don't need the bool.
|
||||
@@ -205,9 +134,7 @@ pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<(
|
||||
// false otherwise.
|
||||
check_user_presence: CheckUserPresence,
|
||||
persistent_store: PersistentStore,
|
||||
key_agreement_key: crypto::ecdh::SecKey,
|
||||
pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH],
|
||||
consecutive_pin_mismatches: u64,
|
||||
pin_protocol_v1: PinProtocolV1,
|
||||
// This variable will be irreversibly set to false RESET_TIMEOUT_MS milliseconds after boot.
|
||||
accepts_reset: bool,
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
@@ -225,16 +152,13 @@ where
|
||||
rng: &'a mut R,
|
||||
check_user_presence: CheckUserPresence,
|
||||
) -> CtapState<'a, R, CheckUserPresence> {
|
||||
let key_agreement_key = crypto::ecdh::SecKey::gensk(rng);
|
||||
let pin_uv_auth_token = rng.gen_uniform_u8x32();
|
||||
let persistent_store = PersistentStore::new(rng);
|
||||
let pin_protocol_v1 = PinProtocolV1::new(rng);
|
||||
CtapState {
|
||||
rng,
|
||||
check_user_presence,
|
||||
persistent_store,
|
||||
key_agreement_key,
|
||||
pin_uv_auth_token,
|
||||
consecutive_pin_mismatches: 0,
|
||||
pin_protocol_v1,
|
||||
accepts_reset: true,
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
u2f_up_state: U2fUserPresenceState::new(
|
||||
@@ -485,9 +409,18 @@ where
|
||||
// Specification is unclear, could be CTAP2_ERR_INVALID_OPTION.
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET);
|
||||
}
|
||||
if !check_pin_auth(&self.pin_uv_auth_token, &client_data_hash, &pin_auth) {
|
||||
if !self
|
||||
.pin_protocol_v1
|
||||
.verify_pin_auth_token(&client_data_hash, &pin_auth)
|
||||
{
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID);
|
||||
}
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
{
|
||||
self.pin_protocol_v1
|
||||
.has_permission(PinPermission::MakeCredential)?;
|
||||
self.pin_protocol_v1.has_permission_for_rp_id(&rp_id)?;
|
||||
}
|
||||
UP_FLAG | UV_FLAG | AT_FLAG | ed_flag
|
||||
}
|
||||
None => {
|
||||
@@ -660,9 +593,18 @@ where
|
||||
// Specification is unclear, could be CTAP2_ERR_UNSUPPORTED_OPTION.
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET);
|
||||
}
|
||||
if !check_pin_auth(&self.pin_uv_auth_token, &client_data_hash, &pin_auth) {
|
||||
if !self
|
||||
.pin_protocol_v1
|
||||
.verify_pin_auth_token(&client_data_hash, &pin_auth)
|
||||
{
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID);
|
||||
}
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
{
|
||||
self.pin_protocol_v1
|
||||
.has_permission(PinPermission::GetAssertion)?;
|
||||
self.pin_protocol_v1.has_permission_for_rp_id(&rp_id)?;
|
||||
}
|
||||
UV_FLAG
|
||||
}
|
||||
None => {
|
||||
@@ -724,25 +666,9 @@ where
|
||||
let mut auth_data = self.generate_auth_data(&rp_id_hash, flags);
|
||||
// Process extensions.
|
||||
if let Some(hmac_secret_input) = hmac_secret_input {
|
||||
let GetAssertionHmacSecretInput {
|
||||
key_agreement,
|
||||
salt_enc,
|
||||
salt_auth,
|
||||
} = 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.
|
||||
if !check_pin_auth(&shared_secret, &salt_enc, &salt_auth) {
|
||||
// Again, hard to tell what the correct error code here is.
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION);
|
||||
}
|
||||
|
||||
let encrypted_output = match &credential.cred_random {
|
||||
Some(cr) => encrypt_hmac_secret_output(&shared_secret, &salt_enc[..], cr)?,
|
||||
// This is the case if the credential was not created with HMAC-secret.
|
||||
None => return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION),
|
||||
};
|
||||
|
||||
let encrypted_output = self
|
||||
.pin_protocol_v1
|
||||
.process_hmac_secret(hmac_secret_input, &credential.cred_random)?;
|
||||
let extensions_output = cbor_map! {
|
||||
"hmac-secret" => encrypted_output,
|
||||
};
|
||||
@@ -818,267 +744,22 @@ where
|
||||
algorithms: Some(vec![ES256_CRED_PARAM]),
|
||||
default_cred_protect: DEFAULT_CRED_PROTECT,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
min_pin_length: self.persistent_store.min_pin_length(),
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
firmware_version: None,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn check_and_store_new_pin(
|
||||
&mut self,
|
||||
aes_dec_key: &crypto::aes256::DecryptionKey,
|
||||
new_pin_enc: Vec<u8>,
|
||||
) -> bool {
|
||||
if new_pin_enc.len() != PIN_PADDED_LENGTH {
|
||||
return false;
|
||||
}
|
||||
let iv = [0; 16];
|
||||
// Assuming PIN_PADDED_LENGTH % block_size == 0 here.
|
||||
let mut blocks = [[0u8; 16]; PIN_PADDED_LENGTH / 16];
|
||||
for i in 0..PIN_PADDED_LENGTH / 16 {
|
||||
blocks[i].copy_from_slice(&new_pin_enc[i * 16..(i + 1) * 16]);
|
||||
}
|
||||
cbc_decrypt(aes_dec_key, iv, &mut blocks);
|
||||
let mut pin = vec![];
|
||||
'pin_block_loop: for block in blocks.iter().take(PIN_PADDED_LENGTH / 16) {
|
||||
for cur_char in block.iter() {
|
||||
if *cur_char != 0 {
|
||||
pin.push(*cur_char);
|
||||
} else {
|
||||
break 'pin_block_loop;
|
||||
}
|
||||
}
|
||||
}
|
||||
if pin.len() < 4 || pin.len() == PIN_PADDED_LENGTH {
|
||||
return false;
|
||||
}
|
||||
let mut pin_hash = [0; 16];
|
||||
pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]);
|
||||
self.persistent_store.set_pin_hash(&pin_hash);
|
||||
true
|
||||
}
|
||||
|
||||
fn check_pin_hash_enc(
|
||||
&mut self,
|
||||
aes_dec_key: &crypto::aes256::DecryptionKey,
|
||||
pin_hash_enc: Vec<u8>,
|
||||
) -> Result<(), Ctap2StatusCode> {
|
||||
match self.persistent_store.pin_hash() {
|
||||
Some(pin_hash) => {
|
||||
if self.consecutive_pin_mismatches >= 3 {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED);
|
||||
}
|
||||
// We need to copy the pin hash, because decrementing the pin retries below may
|
||||
// invalidate the reference (if the page containing the pin hash is compacted).
|
||||
let pin_hash = pin_hash.to_vec();
|
||||
self.persistent_store.decr_pin_retries();
|
||||
if pin_hash_enc.len() != PIN_AUTH_LENGTH {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID);
|
||||
}
|
||||
|
||||
let iv = [0; 16];
|
||||
let mut blocks = [[0u8; 16]; 1];
|
||||
blocks[0].copy_from_slice(&pin_hash_enc[0..PIN_AUTH_LENGTH]);
|
||||
cbc_decrypt(aes_dec_key, iv, &mut blocks);
|
||||
|
||||
let pin_comparison = array_ref![pin_hash, 0, PIN_AUTH_LENGTH].ct_eq(&blocks[0]);
|
||||
if !bool::from(pin_comparison) {
|
||||
self.key_agreement_key = crypto::ecdh::SecKey::gensk(self.rng);
|
||||
if self.persistent_store.pin_retries() == 0 {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED);
|
||||
}
|
||||
self.consecutive_pin_mismatches += 1;
|
||||
if self.consecutive_pin_mismatches >= 3 {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED);
|
||||
}
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID);
|
||||
}
|
||||
}
|
||||
// This status code is not explicitly mentioned in the specification.
|
||||
None => return Err(Ctap2StatusCode::CTAP2_ERR_PIN_REQUIRED),
|
||||
}
|
||||
self.persistent_store.reset_pin_retries();
|
||||
self.consecutive_pin_mismatches = 0;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_get_pin_retries(&self) -> Result<AuthenticatorClientPinResponse, Ctap2StatusCode> {
|
||||
Ok(AuthenticatorClientPinResponse {
|
||||
key_agreement: None,
|
||||
pin_token: None,
|
||||
retries: Some(self.persistent_store.pin_retries() as u64),
|
||||
})
|
||||
}
|
||||
|
||||
fn process_get_key_agreement(&self) -> Result<AuthenticatorClientPinResponse, Ctap2StatusCode> {
|
||||
let pk = self.key_agreement_key.genpk();
|
||||
Ok(AuthenticatorClientPinResponse {
|
||||
key_agreement: Some(CoseKey::from(pk)),
|
||||
pin_token: None,
|
||||
retries: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn process_set_pin(
|
||||
&mut self,
|
||||
key_agreement: CoseKey,
|
||||
pin_auth: Vec<u8>,
|
||||
new_pin_enc: Vec<u8>,
|
||||
) -> Result<(), Ctap2StatusCode> {
|
||||
if self.persistent_store.pin_hash().is_some() {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID);
|
||||
}
|
||||
let pk: crypto::ecdh::PubKey = CoseKey::try_into(key_agreement)?;
|
||||
let shared_secret = self.key_agreement_key.exchange_x_sha256(&pk);
|
||||
|
||||
if !check_pin_auth(&shared_secret, &new_pin_enc, &pin_auth) {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID);
|
||||
}
|
||||
|
||||
let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret);
|
||||
let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key);
|
||||
if !self.check_and_store_new_pin(&aes_dec_key, new_pin_enc) {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION);
|
||||
}
|
||||
self.persistent_store.reset_pin_retries();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_change_pin(
|
||||
&mut self,
|
||||
key_agreement: CoseKey,
|
||||
pin_auth: Vec<u8>,
|
||||
new_pin_enc: Vec<u8>,
|
||||
pin_hash_enc: Vec<u8>,
|
||||
) -> Result<(), Ctap2StatusCode> {
|
||||
if self.persistent_store.pin_retries() == 0 {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED);
|
||||
}
|
||||
let pk: crypto::ecdh::PubKey = CoseKey::try_into(key_agreement)?;
|
||||
let shared_secret = self.key_agreement_key.exchange_x_sha256(&pk);
|
||||
|
||||
let mut auth_param_data = new_pin_enc.clone();
|
||||
auth_param_data.extend(&pin_hash_enc);
|
||||
if !check_pin_auth(&shared_secret, &auth_param_data, &pin_auth) {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID);
|
||||
}
|
||||
|
||||
let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret);
|
||||
let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key);
|
||||
self.check_pin_hash_enc(&aes_dec_key, pin_hash_enc)?;
|
||||
|
||||
if !self.check_and_store_new_pin(&aes_dec_key, new_pin_enc) {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION);
|
||||
}
|
||||
self.pin_uv_auth_token = self.rng.gen_uniform_u8x32();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_get_pin_uv_auth_token_using_pin(
|
||||
&mut self,
|
||||
key_agreement: CoseKey,
|
||||
pin_hash_enc: Vec<u8>,
|
||||
) -> Result<AuthenticatorClientPinResponse, Ctap2StatusCode> {
|
||||
if self.persistent_store.pin_retries() == 0 {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED);
|
||||
}
|
||||
let pk: crypto::ecdh::PubKey = CoseKey::try_into(key_agreement)?;
|
||||
let shared_secret = self.key_agreement_key.exchange_x_sha256(&pk);
|
||||
|
||||
let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret);
|
||||
let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key);
|
||||
self.check_pin_hash_enc(&aes_dec_key, pin_hash_enc)?;
|
||||
|
||||
// Assuming PIN_TOKEN_LENGTH % block_size == 0 here.
|
||||
let iv = [0; 16];
|
||||
let mut blocks = [[0u8; 16]; PIN_TOKEN_LENGTH / 16];
|
||||
for (i, item) in blocks.iter_mut().take(PIN_TOKEN_LENGTH / 16).enumerate() {
|
||||
item.copy_from_slice(&self.pin_uv_auth_token[i * 16..(i + 1) * 16]);
|
||||
}
|
||||
cbc_encrypt(&aes_enc_key, iv, &mut blocks);
|
||||
let mut pin_token = vec![];
|
||||
for item in blocks.iter().take(PIN_TOKEN_LENGTH / 16) {
|
||||
pin_token.extend(item);
|
||||
}
|
||||
|
||||
Ok(AuthenticatorClientPinResponse {
|
||||
key_agreement: None,
|
||||
pin_token: Some(pin_token),
|
||||
retries: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn process_get_pin_uv_auth_token_using_uv(
|
||||
&self,
|
||||
_: CoseKey,
|
||||
) -> Result<AuthenticatorClientPinResponse, Ctap2StatusCode> {
|
||||
Ok(AuthenticatorClientPinResponse {
|
||||
// User verifications is only supported through PIN currently.
|
||||
key_agreement: None,
|
||||
pin_token: Some(vec![]),
|
||||
retries: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn process_get_uv_retries(&self) -> Result<AuthenticatorClientPinResponse, Ctap2StatusCode> {
|
||||
// User verifications is only supported through PIN currently.
|
||||
Ok(AuthenticatorClientPinResponse {
|
||||
key_agreement: None,
|
||||
pin_token: None,
|
||||
retries: Some(0),
|
||||
})
|
||||
}
|
||||
|
||||
fn process_client_pin(
|
||||
&mut self,
|
||||
client_pin_params: AuthenticatorClientPinParameters,
|
||||
) -> Result<ResponseData, Ctap2StatusCode> {
|
||||
let AuthenticatorClientPinParameters {
|
||||
pin_protocol,
|
||||
sub_command,
|
||||
key_agreement,
|
||||
pin_auth,
|
||||
new_pin_enc,
|
||||
pin_hash_enc,
|
||||
} = client_pin_params;
|
||||
|
||||
if pin_protocol != 1 {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID);
|
||||
}
|
||||
|
||||
let response = match sub_command {
|
||||
ClientPinSubCommand::GetPinRetries => Some(self.process_get_pin_retries()?),
|
||||
ClientPinSubCommand::GetKeyAgreement => Some(self.process_get_key_agreement()?),
|
||||
ClientPinSubCommand::SetPin => {
|
||||
self.process_set_pin(
|
||||
key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
|
||||
pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
|
||||
new_pin_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
|
||||
)?;
|
||||
None
|
||||
}
|
||||
ClientPinSubCommand::ChangePin => {
|
||||
self.process_change_pin(
|
||||
key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
|
||||
pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
|
||||
new_pin_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
|
||||
pin_hash_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
|
||||
)?;
|
||||
None
|
||||
}
|
||||
ClientPinSubCommand::GetPinUvAuthTokenUsingPin => {
|
||||
Some(self.process_get_pin_uv_auth_token_using_pin(
|
||||
key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
|
||||
pin_hash_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
|
||||
)?)
|
||||
}
|
||||
ClientPinSubCommand::GetPinUvAuthTokenUsingUv => {
|
||||
Some(self.process_get_pin_uv_auth_token_using_uv(
|
||||
key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
|
||||
)?)
|
||||
}
|
||||
ClientPinSubCommand::GetUvRetries => Some(self.process_get_uv_retries()?),
|
||||
};
|
||||
Ok(ResponseData::AuthenticatorClientPin(response))
|
||||
self.pin_protocol_v1.process_subcommand(
|
||||
self.rng,
|
||||
&mut self.persistent_store,
|
||||
client_pin_params,
|
||||
)
|
||||
}
|
||||
|
||||
fn process_reset(&mut self, cid: ChannelID) -> Result<ResponseData, Ctap2StatusCode> {
|
||||
@@ -1089,9 +770,7 @@ where
|
||||
(self.check_user_presence)(cid)?;
|
||||
|
||||
self.persistent_store.reset(self.rng);
|
||||
self.key_agreement_key = crypto::ecdh::SecKey::gensk(self.rng);
|
||||
self.pin_uv_auth_token = self.rng.gen_uniform_u8x32();
|
||||
self.consecutive_pin_mismatches = 0;
|
||||
self.pin_protocol_v1.reset(self.rng);
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
{
|
||||
self.u2f_up_state = U2fUserPresenceState::new(
|
||||
@@ -1131,8 +810,9 @@ where
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::data_formats::{
|
||||
GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions,
|
||||
MakeCredentialOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity,
|
||||
CoseKey, GetAssertionExtensions, GetAssertionHmacSecretInput, GetAssertionOptions,
|
||||
MakeCredentialExtensions, MakeCredentialOptions, PublicKeyCredentialRpEntity,
|
||||
PublicKeyCredentialUserEntity,
|
||||
};
|
||||
use super::*;
|
||||
use crypto::rng256::ThreadRng256;
|
||||
@@ -1151,7 +831,7 @@ mod test {
|
||||
let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID);
|
||||
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
let mut expected_response = vec![0x00, 0xA8, 0x01];
|
||||
let mut expected_response = vec![0x00, 0xA9, 0x01];
|
||||
#[cfg(not(feature = "with_ctap2_1"))]
|
||||
let mut expected_response = vec![0x00, 0xA6, 0x01];
|
||||
// The difference here is a longer array of supported versions.
|
||||
@@ -1176,7 +856,7 @@ mod test {
|
||||
[
|
||||
0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, 0x6C, 0x67, 0x26,
|
||||
0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B,
|
||||
0x65, 0x79,
|
||||
0x65, 0x79, 0x0D, 0x04,
|
||||
]
|
||||
.iter(),
|
||||
);
|
||||
@@ -1768,32 +1448,4 @@ mod test {
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_hmac_secret_output() {
|
||||
let shared_secret = [0x55; 32];
|
||||
let salt_enc = [0x5E; 32];
|
||||
let cred_random = [0xC9; 32];
|
||||
let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random);
|
||||
assert_eq!(output.unwrap().len(), 32);
|
||||
|
||||
let salt_enc = [0x5E; 48];
|
||||
let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random);
|
||||
assert_eq!(
|
||||
output,
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION)
|
||||
);
|
||||
|
||||
let salt_enc = [0x5E; 64];
|
||||
let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random);
|
||||
assert_eq!(output.unwrap().len(), 64);
|
||||
|
||||
let salt_enc = [0x5E; 32];
|
||||
let cred_random = [0xC9; 33];
|
||||
let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random);
|
||||
assert_eq!(
|
||||
output,
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
1229
src/ctap/pin_protocol_v1.rs
Normal file
1229
src/ctap/pin_protocol_v1.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -125,6 +125,8 @@ pub struct AuthenticatorGetInfoResponse {
|
||||
pub algorithms: Option<Vec<PublicKeyCredentialParameter>>,
|
||||
pub default_cred_protect: Option<CredentialProtectionPolicy>,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
pub min_pin_length: u8,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
pub firmware_version: Option<u64>,
|
||||
}
|
||||
|
||||
@@ -143,6 +145,7 @@ impl From<AuthenticatorGetInfoResponse> for cbor::Value {
|
||||
transports,
|
||||
algorithms,
|
||||
default_cred_protect,
|
||||
min_pin_length,
|
||||
firmware_version,
|
||||
} = get_info_response;
|
||||
|
||||
@@ -166,6 +169,7 @@ impl From<AuthenticatorGetInfoResponse> for cbor::Value {
|
||||
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),
|
||||
0x0D => min_pin_length as u64,
|
||||
0x0E => firmware_version,
|
||||
}
|
||||
}
|
||||
@@ -284,8 +288,9 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_get_info_into_cbor() {
|
||||
let versions = vec!["FIDO_2_0".to_string()];
|
||||
let get_info_response = AuthenticatorGetInfoResponse {
|
||||
versions: vec!["FIDO_2_0".to_string()],
|
||||
versions: versions.clone(),
|
||||
extensions: None,
|
||||
aaguid: [0x00; 16],
|
||||
options: None,
|
||||
@@ -301,14 +306,23 @@ mod test {
|
||||
algorithms: None,
|
||||
default_cred_protect: None,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
min_pin_length: 4,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
firmware_version: None,
|
||||
};
|
||||
let response_cbor: Option<cbor::Value> =
|
||||
ResponseData::AuthenticatorGetInfo(get_info_response).into();
|
||||
#[cfg(not(feature = "with_ctap2_1"))]
|
||||
let expected_cbor = cbor_map_options! {
|
||||
0x01 => cbor_array_vec![vec!["FIDO_2_0"]],
|
||||
0x01 => cbor_array_vec![versions],
|
||||
0x03 => vec![0x00; 16],
|
||||
};
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
let expected_cbor = cbor_map_options! {
|
||||
0x01 => cbor_array_vec![versions],
|
||||
0x03 => vec![0x00; 16],
|
||||
0x0D => 4,
|
||||
};
|
||||
assert_eq!(response_cbor, Some(expected_cbor));
|
||||
}
|
||||
|
||||
@@ -329,6 +343,7 @@ mod test {
|
||||
transports: Some(vec![AuthenticatorTransport::Usb]),
|
||||
algorithms: Some(vec![ES256_CRED_PARAM]),
|
||||
default_cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired),
|
||||
min_pin_length: 4,
|
||||
firmware_version: Some(0),
|
||||
};
|
||||
let response_cbor: Option<cbor::Value> =
|
||||
@@ -345,6 +360,7 @@ mod test {
|
||||
0x09 => cbor_array_vec![vec!["usb"]],
|
||||
0x0A => cbor_array_vec![vec![ES256_CRED_PARAM]],
|
||||
0x0C => CredentialProtectionPolicy::UserVerificationRequired as u64,
|
||||
0x0D => 4,
|
||||
0x0E => 0,
|
||||
};
|
||||
assert_eq!(response_cbor, Some(expected_cbor));
|
||||
|
||||
@@ -32,6 +32,10 @@ pub enum Ctap2StatusCode {
|
||||
CTAP2_ERR_MISSING_PARAMETER = 0x14,
|
||||
CTAP2_ERR_LIMIT_EXCEEDED = 0x15,
|
||||
CTAP2_ERR_UNSUPPORTED_EXTENSION = 0x16,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
CTAP2_ERR_FP_DATABASE_FULL = 0x17,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
CTAP2_ERR_PC_STORAGE_FULL = 0x18,
|
||||
CTAP2_ERR_CREDENTIAL_EXCLUDED = 0x19,
|
||||
CTAP2_ERR_PROCESSING = 0x21,
|
||||
CTAP2_ERR_INVALID_CREDENTIAL = 0x22,
|
||||
@@ -60,6 +64,10 @@ pub enum Ctap2StatusCode {
|
||||
CTAP2_ERR_ACTION_TIMEOUT = 0x3A,
|
||||
CTAP2_ERR_UP_REQUIRED = 0x3B,
|
||||
CTAP2_ERR_UV_BLOCKED = 0x3C,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
CTAP2_ERR_INTEGRITY_FAILURE = 0x3D,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
CTAP2_ERR_INVALID_SUBCOMMAND = 0x3E,
|
||||
CTAP1_ERR_OTHER = 0x7F,
|
||||
CTAP2_ERR_SPEC_LAST = 0xDF,
|
||||
CTAP2_ERR_EXTENSION_FIRST = 0xE0,
|
||||
|
||||
@@ -13,13 +13,16 @@
|
||||
// limitations under the License.
|
||||
|
||||
use crate::crypto::rng256::Rng256;
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
use crate::ctap::data_formats::{extract_array, extract_text_string};
|
||||
use crate::ctap::data_formats::{CredentialProtectionPolicy, PublicKeyCredentialSource};
|
||||
use crate::ctap::pin_protocol_v1::PIN_AUTH_LENGTH;
|
||||
use crate::ctap::status_code::Ctap2StatusCode;
|
||||
use crate::ctap::{key_material, PIN_AUTH_LENGTH, USE_BATCH_ATTESTATION};
|
||||
use crate::ctap::{key_material, USE_BATCH_ATTESTATION};
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use core::convert::TryInto;
|
||||
use ctap2::embedded_flash::{self, StoreConfig, StoreEntry, StoreError, StoreIndex};
|
||||
use ctap2::embedded_flash::{self, StoreConfig, StoreEntry, StoreError};
|
||||
|
||||
#[cfg(any(test, feature = "ram_storage"))]
|
||||
type Storage = embedded_flash::BufferStorage;
|
||||
@@ -59,11 +62,26 @@ const PIN_RETRIES: usize = 4;
|
||||
const ATTESTATION_PRIVATE_KEY: usize = 5;
|
||||
const ATTESTATION_CERTIFICATE: usize = 6;
|
||||
const AAGUID: usize = 7;
|
||||
const NUM_TAGS: usize = 8;
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
const MIN_PIN_LENGTH: usize = 8;
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
const MIN_PIN_LENGTH_RP_IDS: usize = 9;
|
||||
// Different NUM_TAGS depending on the CTAP version make the storage incompatible,
|
||||
// so we use the maximum.
|
||||
const NUM_TAGS: usize = 10;
|
||||
|
||||
const MAX_PIN_RETRIES: u8 = 6;
|
||||
const ATTESTATION_PRIVATE_KEY_LENGTH: usize = 32;
|
||||
const AAGUID_LENGTH: usize = 16;
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
const DEFAULT_MIN_PIN_LENGTH: u8 = 4;
|
||||
// TODO(kaczmarczyck) use this for the minPinLength extension
|
||||
// https://github.com/google/OpenSK/issues/129
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
const _DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec<String> = Vec::new();
|
||||
// TODO(kaczmarczyck) Check whether this constant is necessary, or replace it accordingly.
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
const _MAX_RP_IDS_LENGTH: usize = 8;
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum Key {
|
||||
@@ -81,6 +99,10 @@ enum Key {
|
||||
AttestationPrivateKey,
|
||||
AttestationCertificate,
|
||||
Aaguid,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
MinPinLength,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
MinPinLengthRpIds,
|
||||
}
|
||||
|
||||
pub struct MasterKeys<'a> {
|
||||
@@ -135,6 +157,10 @@ impl StoreConfig for Config {
|
||||
ATTESTATION_PRIVATE_KEY => add(Key::AttestationPrivateKey),
|
||||
ATTESTATION_CERTIFICATE => add(Key::AttestationCertificate),
|
||||
AAGUID => add(Key::Aaguid),
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
MIN_PIN_LENGTH => add(Key::MinPinLength),
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
MIN_PIN_LENGTH_RP_IDS => add(Key::MinPinLengthRpIds),
|
||||
_ => debug_assert!(false),
|
||||
}
|
||||
}
|
||||
@@ -199,15 +225,6 @@ impl PersistentStore {
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
if self.store.find_one(&Key::PinRetries).is_none() {
|
||||
self.store
|
||||
.insert(StoreEntry {
|
||||
tag: PIN_RETRIES,
|
||||
data: &[MAX_PIN_RETRIES],
|
||||
sensitive: false,
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
// The following 3 entries are meant to be written by vendor-specific commands.
|
||||
if USE_BATCH_ATTESTATION {
|
||||
if self.store.find_one(&Key::AttestationPrivateKey).is_none() {
|
||||
@@ -380,20 +397,32 @@ impl PersistentStore {
|
||||
}
|
||||
}
|
||||
|
||||
fn pin_retries_entry(&self) -> (StoreIndex, u8) {
|
||||
let (index, entry) = self.store.find_one(&Key::PinRetries).unwrap();
|
||||
let data = entry.data;
|
||||
debug_assert_eq!(data.len(), 1);
|
||||
(index, data[0])
|
||||
}
|
||||
|
||||
pub fn pin_retries(&self) -> u8 {
|
||||
self.pin_retries_entry().1
|
||||
self.store
|
||||
.find_one(&Key::PinRetries)
|
||||
.map_or(MAX_PIN_RETRIES, |(_, entry)| {
|
||||
debug_assert_eq!(entry.data.len(), 1);
|
||||
entry.data[0]
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decr_pin_retries(&mut self) {
|
||||
let (index, old_value) = self.pin_retries_entry();
|
||||
let new_value = old_value.saturating_sub(1);
|
||||
match self.store.find_one(&Key::PinRetries) {
|
||||
None => {
|
||||
self.store
|
||||
.insert(StoreEntry {
|
||||
tag: PIN_RETRIES,
|
||||
data: &[MAX_PIN_RETRIES.saturating_sub(1)],
|
||||
sensitive: false,
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
Some((index, entry)) => {
|
||||
debug_assert_eq!(entry.data.len(), 1);
|
||||
if entry.data[0] == 0 {
|
||||
return;
|
||||
}
|
||||
let new_value = entry.data[0].saturating_sub(1);
|
||||
self.store
|
||||
.replace(
|
||||
index,
|
||||
@@ -405,19 +434,82 @@ impl PersistentStore {
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_pin_retries(&mut self) {
|
||||
let (index, _) = self.pin_retries_entry();
|
||||
if let Some((index, _)) = self.store.find_one(&Key::PinRetries) {
|
||||
self.store.delete(index).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
pub fn min_pin_length(&self) -> u8 {
|
||||
self.store
|
||||
.replace(
|
||||
index,
|
||||
StoreEntry {
|
||||
tag: PIN_RETRIES,
|
||||
data: &[MAX_PIN_RETRIES],
|
||||
.find_one(&Key::MinPinLength)
|
||||
.map_or(DEFAULT_MIN_PIN_LENGTH, |(_, entry)| {
|
||||
debug_assert_eq!(entry.data.len(), 1);
|
||||
entry.data[0]
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
pub fn set_min_pin_length(&mut self, min_pin_length: u8) {
|
||||
let entry = StoreEntry {
|
||||
tag: MIN_PIN_LENGTH,
|
||||
data: &[min_pin_length],
|
||||
sensitive: false,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
};
|
||||
match self.store.find_one(&Key::MinPinLength) {
|
||||
None => {
|
||||
self.store.insert(entry).unwrap();
|
||||
}
|
||||
Some((index, _)) => {
|
||||
self.store.replace(index, entry).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
pub fn _min_pin_length_rp_ids(&self) -> Vec<String> {
|
||||
let rp_ids = self
|
||||
.store
|
||||
.find_one(&Key::MinPinLengthRpIds)
|
||||
.map_or(Some(_DEFAULT_MIN_PIN_LENGTH_RP_IDS), |(_, entry)| {
|
||||
_deserialize_min_pin_length_rp_ids(entry.data)
|
||||
});
|
||||
debug_assert!(rp_ids.is_some());
|
||||
rp_ids.unwrap_or(vec![])
|
||||
}
|
||||
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
pub fn _set_min_pin_length_rp_ids(
|
||||
&mut self,
|
||||
min_pin_length_rp_ids: Vec<String>,
|
||||
) -> Result<(), Ctap2StatusCode> {
|
||||
let mut min_pin_length_rp_ids = min_pin_length_rp_ids;
|
||||
for rp_id in _DEFAULT_MIN_PIN_LENGTH_RP_IDS {
|
||||
if !min_pin_length_rp_ids.contains(&rp_id) {
|
||||
min_pin_length_rp_ids.push(rp_id);
|
||||
}
|
||||
}
|
||||
if min_pin_length_rp_ids.len() > _MAX_RP_IDS_LENGTH {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL);
|
||||
}
|
||||
let entry = StoreEntry {
|
||||
tag: MIN_PIN_LENGTH_RP_IDS,
|
||||
data: &_serialize_min_pin_length_rp_ids(min_pin_length_rp_ids)?,
|
||||
sensitive: false,
|
||||
};
|
||||
match self.store.find_one(&Key::MinPinLengthRpIds) {
|
||||
None => {
|
||||
self.store.insert(entry).unwrap();
|
||||
}
|
||||
Some((index, _)) => {
|
||||
self.store.replace(index, entry).unwrap();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn attestation_private_key(
|
||||
@@ -540,7 +632,28 @@ fn serialize_credential(credential: PublicKeyCredentialSource) -> Result<Vec<u8>
|
||||
if cbor::write(credential.into(), &mut data) {
|
||||
Ok(data)
|
||||
} else {
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CREDENTIAL)
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
fn _deserialize_min_pin_length_rp_ids(data: &[u8]) -> Option<Vec<String>> {
|
||||
let cbor = cbor::read(data).ok()?;
|
||||
extract_array(cbor)
|
||||
.ok()?
|
||||
.into_iter()
|
||||
.map(extract_text_string)
|
||||
.collect::<Result<Vec<String>, Ctap2StatusCode>>()
|
||||
.ok()
|
||||
}
|
||||
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
fn _serialize_min_pin_length_rp_ids(rp_ids: Vec<String>) -> Result<Vec<u8>, Ctap2StatusCode> {
|
||||
let mut data = Vec::new();
|
||||
if cbor::write(cbor_array_vec!(rp_ids), &mut data) {
|
||||
Ok(data)
|
||||
} else {
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -809,7 +922,6 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_pin_hash() {
|
||||
use crate::ctap::PIN_AUTH_LENGTH;
|
||||
let mut rng = ThreadRng256 {};
|
||||
let mut persistent_store = PersistentStore::new(&mut rng);
|
||||
|
||||
@@ -892,4 +1004,73 @@ mod test {
|
||||
);
|
||||
assert_eq!(persistent_store.aaguid().unwrap(), key_material::AAGUID);
|
||||
}
|
||||
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
#[test]
|
||||
fn test_min_pin_length() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let mut persistent_store = PersistentStore::new(&mut rng);
|
||||
|
||||
// The minimum PIN length is initially at the default.
|
||||
assert_eq!(persistent_store.min_pin_length(), DEFAULT_MIN_PIN_LENGTH);
|
||||
|
||||
// Changes by the setter are reflected by the getter..
|
||||
let new_min_pin_length = 8;
|
||||
persistent_store.set_min_pin_length(new_min_pin_length);
|
||||
assert_eq!(persistent_store.min_pin_length(), new_min_pin_length);
|
||||
}
|
||||
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
#[test]
|
||||
fn test_min_pin_length_rp_ids() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let mut persistent_store = PersistentStore::new(&mut rng);
|
||||
|
||||
// The minimum PIN length RP IDs are initially at the default.
|
||||
assert_eq!(
|
||||
persistent_store._min_pin_length_rp_ids(),
|
||||
_DEFAULT_MIN_PIN_LENGTH_RP_IDS
|
||||
);
|
||||
|
||||
// Changes by the setter are reflected by the getter.
|
||||
let mut rp_ids = vec![String::from("example.com")];
|
||||
assert_eq!(
|
||||
persistent_store._set_min_pin_length_rp_ids(rp_ids.clone()),
|
||||
Ok(())
|
||||
);
|
||||
for rp_id in _DEFAULT_MIN_PIN_LENGTH_RP_IDS {
|
||||
if !rp_ids.contains(&rp_id) {
|
||||
rp_ids.push(rp_id);
|
||||
}
|
||||
}
|
||||
assert_eq!(persistent_store._min_pin_length_rp_ids(), rp_ids);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_deserialize_credential() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
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: None,
|
||||
};
|
||||
let serialized = serialize_credential(credential.clone()).unwrap();
|
||||
let reconstructed = deserialize_credential(&serialized).unwrap();
|
||||
assert_eq!(credential, reconstructed);
|
||||
}
|
||||
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
#[test]
|
||||
fn test_serialize_deserialize_min_pin_length_rp_ids() {
|
||||
let rp_ids = vec![String::from("example.com")];
|
||||
let serialized = _serialize_min_pin_length_rp_ids(rp_ids.clone()).unwrap();
|
||||
let reconstructed = _deserialize_min_pin_length_rp_ids(&serialized).unwrap();
|
||||
assert_eq!(rp_ids, reconstructed);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user