Merge pull request #127 from kaczmarczyck/client-pin-features

Client pin features
This commit is contained in:
kaczmarczyck
2020-08-20 20:01:03 +02:00
committed by GitHub
13 changed files with 1708 additions and 508 deletions

View File

@@ -31,6 +31,7 @@ with_ctap2_1 = []
[dev-dependencies]
elf2tab = "0.4.0"
enum-iterator = "0.6.0"
[build-dependencies]
openssl = "0.10"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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!(

View File

@@ -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]

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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));

View File

@@ -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,

View File

@@ -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,44 +397,119 @@ 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);
self.store
.replace(
index,
StoreEntry {
tag: PIN_RETRIES,
data: &[new_value],
sensitive: false,
},
)
.unwrap();
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,
StoreEntry {
tag: PIN_RETRIES,
data: &[new_value],
sensitive: false,
},
)
.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],
sensitive: false,
},
)
.unwrap();
.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,
};
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);
}
}