adds new client Pin subcommand minPinLength implementation

This commit is contained in:
Fabian Kaczmarczyck
2020-06-26 14:30:27 +02:00
parent 033f544c47
commit 26595db810
6 changed files with 337 additions and 70 deletions

View File

@@ -103,6 +103,9 @@ a few things you can personalize:
When changing the default, resident credentials become undiscoverable without When changing the default, resident credentials become undiscoverable without
user verification. This helps privacy, but can make usage less comfortable user verification. This helps privacy, but can make usage less comfortable
for credentials that need less protection. 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.
You can add relying parties to the list of readers of the minimum PIN length.
### 3D printed enclosure ### 3D printed enclosure

View File

@@ -279,7 +279,7 @@ pub struct AuthenticatorClientPinParameters {
pub new_pin_enc: Option<Vec<u8>>, pub new_pin_enc: Option<Vec<u8>>,
pub pin_hash_enc: Option<Vec<u8>>, pub pin_hash_enc: Option<Vec<u8>>,
#[cfg(feature = "with_ctap2_1")] #[cfg(feature = "with_ctap2_1")]
pub min_pin_length: Option<u64>, pub min_pin_length: Option<u8>,
#[cfg(feature = "with_ctap2_1")] #[cfg(feature = "with_ctap2_1")]
pub min_pin_length_rp_ids: Option<Vec<String>>, pub min_pin_length_rp_ids: Option<Vec<String>>,
#[cfg(feature = "with_ctap2_1")] #[cfg(feature = "with_ctap2_1")]
@@ -329,7 +329,10 @@ impl TryFrom<cbor::Value> for AuthenticatorClientPinParameters {
let new_pin_enc = new_pin_enc.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()?; let pin_hash_enc = pin_hash_enc.map(extract_byte_string).transpose()?;
#[cfg(feature = "with_ctap2_1")] #[cfg(feature = "with_ctap2_1")]
let min_pin_length = min_pin_length.map(extract_unsigned).transpose()?; let min_pin_length = min_pin_length
.map(extract_unsigned)
.transpose()?
.map(|m| m as u8);
#[cfg(feature = "with_ctap2_1")] #[cfg(feature = "with_ctap2_1")]
let min_pin_length_rp_ids = match min_pin_length_rp_ids { let min_pin_length_rp_ids = match min_pin_length_rp_ids {
Some(entry) => Some( Some(entry) => Some(

View File

@@ -730,6 +730,8 @@ where
algorithms: Some(vec![ES256_CRED_PARAM]), algorithms: Some(vec![ES256_CRED_PARAM]),
default_cred_protect: DEFAULT_CRED_PROTECT, default_cred_protect: DEFAULT_CRED_PROTECT,
#[cfg(feature = "with_ctap2_1")] #[cfg(feature = "with_ctap2_1")]
min_pin_length: self.persistent_store.min_pin_length(),
#[cfg(feature = "with_ctap2_1")]
firmware_version: None, firmware_version: None,
}, },
)) ))
@@ -812,7 +814,7 @@ mod test {
let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID); let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID);
#[cfg(feature = "with_ctap2_1")] #[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"))] #[cfg(not(feature = "with_ctap2_1"))]
let mut expected_response = vec![0x00, 0xA6, 0x01]; let mut expected_response = vec![0x00, 0xA6, 0x01];
// The difference here is a longer array of supported versions. // The difference here is a longer array of supported versions.
@@ -837,7 +839,7 @@ mod test {
[ [
0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, 0x6C, 0x67, 0x26, 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, 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B,
0x65, 0x79, 0x65, 0x79, 0x0D, 0x04,
] ]
.iter(), .iter(),
); );

View File

@@ -17,6 +17,7 @@ use super::data_formats::{ClientPinSubCommand, CoseKey, GetAssertionHmacSecretIn
use super::response::{AuthenticatorClientPinResponse, ResponseData}; use super::response::{AuthenticatorClientPinResponse, ResponseData};
use super::status_code::Ctap2StatusCode; use super::status_code::Ctap2StatusCode;
use super::storage::PersistentStore; use super::storage::PersistentStore;
#[cfg(feature = "with_ctap2_1")]
use alloc::string::String; use alloc::string::String;
use alloc::vec::Vec; use alloc::vec::Vec;
use core::convert::TryInto; use core::convert::TryInto;
@@ -101,7 +102,6 @@ fn encrypt_hmac_secret_output(
Ok(encrypted_output) Ok(encrypted_output)
} }
/// Checks if the decrypted PIN satisfies the PIN policy and stores it persistently.
fn check_and_store_new_pin( fn check_and_store_new_pin(
persistent_store: &mut PersistentStore, persistent_store: &mut PersistentStore,
aes_dec_key: &crypto::aes256::DecryptionKey, aes_dec_key: &crypto::aes256::DecryptionKey,
@@ -127,7 +127,11 @@ fn check_and_store_new_pin(
} }
} }
} }
if pin.len() < 4 || pin.len() == PIN_PADDED_LENGTH { #[cfg(feature = "with_ctap2_1")]
let min_pin_length = persistent_store.min_pin_length() as usize;
#[cfg(not(feature = "with_ctap2_1"))]
let min_pin_length = 4;
if pin.len() < min_pin_length || pin.len() == PIN_PADDED_LENGTH {
// TODO(kaczmarczyck) check 4 code point minimum instead // TODO(kaczmarczyck) check 4 code point minimum instead
// TODO(kaczmarczyck) check last byte == 0x00 // TODO(kaczmarczyck) check last byte == 0x00
return false; return false;
@@ -141,7 +145,7 @@ fn check_and_store_new_pin(
pub struct PinProtocolV1 { pub struct PinProtocolV1 {
key_agreement_key: crypto::ecdh::SecKey, key_agreement_key: crypto::ecdh::SecKey,
pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH],
consecutive_pin_mismatches: u64, consecutive_pin_mismatches: u8,
} }
impl PinProtocolV1 { impl PinProtocolV1 {
@@ -341,31 +345,69 @@ impl PinProtocolV1 {
#[cfg(feature = "with_ctap2_1")] #[cfg(feature = "with_ctap2_1")]
fn process_set_min_pin_length( fn process_set_min_pin_length(
&mut self, &mut self,
_min_pin_length: u64, persistent_store: &mut PersistentStore,
_min_pin_length_rp_ids: Vec<String>, min_pin_length: u8,
_pin_auth: Vec<u8>, min_pin_length_rp_ids: Option<Vec<String>>,
) -> Result<AuthenticatorClientPinResponse, Ctap2StatusCode> { pin_auth: Option<Vec<u8>>,
// TODO ) -> Result<(), Ctap2StatusCode> {
Ok(AuthenticatorClientPinResponse { if min_pin_length_rp_ids.is_some() {
key_agreement: None, return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION);
pin_token: None, }
retries: Some(0), if persistent_store.pin_hash().is_some() {
}) match pin_auth {
Some(pin_auth) => {
// TODO(kaczmarczyck) not mentioned, but maybe useful?
// if persistent_store.pin_retries() == 0 {
// return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED);
// }
if self.consecutive_pin_mismatches >= 3 {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED);
}
let mut message = vec![0xFF; 32];
message.extend(&[0x06, 0x08]);
message.extend(&[min_pin_length as u8, 0x00, 0x00, 0x00]);
// TODO(kaczmarczyck) commented code is useful for the extension
// if !cbor::write(cbor_array_vec!(min_pin_length_rp_ids), &mut message) {
// return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR);
// }
if !check_pin_auth(&self.pin_uv_auth_token, &message, &pin_auth) {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID);
}
}
None => return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID),
};
}
if min_pin_length < persistent_store.min_pin_length() {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION);
}
persistent_store.set_min_pin_length(min_pin_length);
// if let Some(min_pin_length_rp_ids) = min_pin_length_rp_ids {
// persistent_store.set_min_pin_length_rp_ids(min_pin_length_rp_ids)?;
// }
Ok(())
} }
#[cfg(feature = "with_ctap2_1")] #[cfg(feature = "with_ctap2_1")]
fn process_get_pin_uv_auth_token_using_pin_with_permissions( fn process_get_pin_uv_auth_token_using_pin_with_permissions(
&mut self, &mut self,
_key_agreement: CoseKey, rng: &mut impl Rng256,
_pin_hash_enc: Vec<u8>, persistent_store: &mut PersistentStore,
_permissions: u8, key_agreement: CoseKey,
_permissions_rp_id: String, pin_hash_enc: Vec<u8>,
permissions: u8,
_permissions_rp_id: Option<String>,
) -> Result<AuthenticatorClientPinResponse, Ctap2StatusCode> { ) -> Result<AuthenticatorClientPinResponse, Ctap2StatusCode> {
if permissions == 0 {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
// TODO(kaczmarczyck) split the implementation to omit the unnecessary token generation
self.process_get_pin_token(rng, persistent_store, key_agreement, pin_hash_enc)?;
// TODO // TODO
Ok(AuthenticatorClientPinResponse { Ok(AuthenticatorClientPinResponse {
key_agreement: None, key_agreement: None,
pin_token: None, pin_token: None,
retries: Some(0), retries: None,
}) })
} }
@@ -441,22 +483,24 @@ impl PinProtocolV1 {
#[cfg(feature = "with_ctap2_1")] #[cfg(feature = "with_ctap2_1")]
ClientPinSubCommand::SetMinPinLength => { ClientPinSubCommand::SetMinPinLength => {
self.process_set_min_pin_length( self.process_set_min_pin_length(
persistent_store,
min_pin_length.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, min_pin_length.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
min_pin_length_rp_ids.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, min_pin_length_rp_ids,
pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, pin_auth,
)?; )?;
None None
} }
#[cfg(feature = "with_ctap2_1")] #[cfg(feature = "with_ctap2_1")]
ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => { ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => Some(
self.process_get_pin_uv_auth_token_using_pin_with_permissions( self.process_get_pin_uv_auth_token_using_pin_with_permissions(
rng,
persistent_store,
key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
pin_hash_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, pin_hash_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
permissions.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, permissions.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
permissions_rp_id.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, permissions_rp_id,
)?; )?,
None ),
}
}; };
Ok(ResponseData::AuthenticatorClientPin(response)) Ok(ResponseData::AuthenticatorClientPin(response))
} }
@@ -744,6 +788,40 @@ mod test {
); );
} }
#[cfg(feature = "with_ctap2_1")]
#[test]
fn test_process_set_min_pin_length() {
let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng);
let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng);
let min_pin_length = 8;
pin_protocol_v1.pin_uv_auth_token = [0x55; PIN_TOKEN_LENGTH];
let pin_auth = vec![
0x94, 0x86, 0xEF, 0x4C, 0xB3, 0x84, 0x2C, 0x85, 0x72, 0x02, 0xBF, 0xE4, 0x36, 0x22,
0xFE, 0xC9,
];
// TODO(kaczmarczyck) implement test for the min PIN length extension
let response = pin_protocol_v1.process_set_min_pin_length(
&mut persistent_store,
min_pin_length,
None,
Some(pin_auth.clone()),
);
assert_eq!(response, Ok(()));
assert_eq!(persistent_store.min_pin_length(), min_pin_length);
let response = pin_protocol_v1.process_set_min_pin_length(
&mut persistent_store,
7,
None,
Some(pin_auth),
);
assert_eq!(
response,
Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)
);
assert_eq!(persistent_store.min_pin_length(), min_pin_length);
}
#[test] #[test]
fn test_process() { fn test_process() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};

View File

@@ -125,6 +125,8 @@ pub struct AuthenticatorGetInfoResponse {
pub algorithms: Option<Vec<PublicKeyCredentialParameter>>, pub algorithms: Option<Vec<PublicKeyCredentialParameter>>,
pub default_cred_protect: Option<CredentialProtectionPolicy>, pub default_cred_protect: Option<CredentialProtectionPolicy>,
#[cfg(feature = "with_ctap2_1")] #[cfg(feature = "with_ctap2_1")]
pub min_pin_length: u8,
#[cfg(feature = "with_ctap2_1")]
pub firmware_version: Option<u64>, pub firmware_version: Option<u64>,
} }
@@ -143,6 +145,7 @@ impl From<AuthenticatorGetInfoResponse> for cbor::Value {
transports, transports,
algorithms, algorithms,
default_cred_protect, default_cred_protect,
min_pin_length,
firmware_version, firmware_version,
} = get_info_response; } = get_info_response;
@@ -166,6 +169,7 @@ impl From<AuthenticatorGetInfoResponse> for cbor::Value {
0x09 => transports.map(|vec| cbor_array_vec!(vec)), 0x09 => transports.map(|vec| cbor_array_vec!(vec)),
0x0A => algorithms.map(|vec| cbor_array_vec!(vec)), 0x0A => algorithms.map(|vec| cbor_array_vec!(vec)),
0x0C => default_cred_protect.map(|p| p as u64), 0x0C => default_cred_protect.map(|p| p as u64),
0x0D => min_pin_length as u64,
0x0E => firmware_version, 0x0E => firmware_version,
} }
} }
@@ -301,14 +305,23 @@ mod test {
algorithms: None, algorithms: None,
default_cred_protect: None, default_cred_protect: None,
#[cfg(feature = "with_ctap2_1")] #[cfg(feature = "with_ctap2_1")]
min_pin_length: 4,
#[cfg(feature = "with_ctap2_1")]
firmware_version: None, firmware_version: None,
}; };
let response_cbor: Option<cbor::Value> = let response_cbor: Option<cbor::Value> =
ResponseData::AuthenticatorGetInfo(get_info_response).into(); ResponseData::AuthenticatorGetInfo(get_info_response).into();
#[cfg(not(feature = "with_ctap2_1"))]
let expected_cbor = cbor_map_options! { let expected_cbor = cbor_map_options! {
0x01 => cbor_array_vec![vec!["FIDO_2_0"]], 0x01 => cbor_array_vec![vec!["FIDO_2_0"]],
0x03 => vec![0x00; 16], 0x03 => vec![0x00; 16],
}; };
#[cfg(feature = "with_ctap2_1")]
let expected_cbor = cbor_map_options! {
0x01 => cbor_array_vec![vec!["FIDO_2_0"]],
0x03 => vec![0x00; 16],
0x0D => 4,
};
assert_eq!(response_cbor, Some(expected_cbor)); assert_eq!(response_cbor, Some(expected_cbor));
} }
@@ -329,6 +342,7 @@ mod test {
transports: Some(vec![AuthenticatorTransport::Usb]), transports: Some(vec![AuthenticatorTransport::Usb]),
algorithms: Some(vec![ES256_CRED_PARAM]), algorithms: Some(vec![ES256_CRED_PARAM]),
default_cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired), default_cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired),
min_pin_length: 4,
firmware_version: Some(0), firmware_version: Some(0),
}; };
let response_cbor: Option<cbor::Value> = let response_cbor: Option<cbor::Value> =
@@ -345,6 +359,7 @@ mod test {
0x09 => cbor_array_vec![vec!["usb"]], 0x09 => cbor_array_vec![vec!["usb"]],
0x0A => cbor_array_vec![vec![ES256_CRED_PARAM]], 0x0A => cbor_array_vec![vec![ES256_CRED_PARAM]],
0x0C => CredentialProtectionPolicy::UserVerificationRequired as u64, 0x0C => CredentialProtectionPolicy::UserVerificationRequired as u64,
0x0D => 4,
0x0E => 0, 0x0E => 0,
}; };
assert_eq!(response_cbor, Some(expected_cbor)); assert_eq!(response_cbor, Some(expected_cbor));

View File

@@ -13,6 +13,8 @@
// limitations under the License. // limitations under the License.
use crate::crypto::rng256::Rng256; 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::data_formats::{CredentialProtectionPolicy, PublicKeyCredentialSource};
use crate::ctap::pin_protocol_v1::PIN_AUTH_LENGTH; use crate::ctap::pin_protocol_v1::PIN_AUTH_LENGTH;
use crate::ctap::status_code::Ctap2StatusCode; use crate::ctap::status_code::Ctap2StatusCode;
@@ -20,7 +22,7 @@ use crate::ctap::{key_material, USE_BATCH_ATTESTATION};
use alloc::string::String; use alloc::string::String;
use alloc::vec::Vec; use alloc::vec::Vec;
use core::convert::TryInto; 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"))] #[cfg(any(test, feature = "ram_storage"))]
type Storage = embedded_flash::BufferStorage; type Storage = embedded_flash::BufferStorage;
@@ -60,11 +62,25 @@ const PIN_RETRIES: usize = 4;
const ATTESTATION_PRIVATE_KEY: usize = 5; const ATTESTATION_PRIVATE_KEY: usize = 5;
const ATTESTATION_CERTIFICATE: usize = 6; const ATTESTATION_CERTIFICATE: usize = 6;
const AAGUID: usize = 7; const AAGUID: usize = 7;
#[cfg(not(feature = "with_ctap2_1"))]
const NUM_TAGS: usize = 8; 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;
#[cfg(feature = "with_ctap2_1")]
const NUM_TAGS: usize = 10;
const MAX_PIN_RETRIES: u8 = 6; const MAX_PIN_RETRIES: u8 = 6;
const ATTESTATION_PRIVATE_KEY_LENGTH: usize = 32; const ATTESTATION_PRIVATE_KEY_LENGTH: usize = 32;
const AAGUID_LENGTH: usize = 16; const AAGUID_LENGTH: usize = 16;
#[cfg(feature = "with_ctap2_1")]
const DEFAULT_MIN_PIN_LENGTH: u8 = 4;
#[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)] #[derive(PartialEq, Eq, PartialOrd, Ord)]
enum Key { enum Key {
@@ -82,6 +98,10 @@ enum Key {
AttestationPrivateKey, AttestationPrivateKey,
AttestationCertificate, AttestationCertificate,
Aaguid, Aaguid,
#[cfg(feature = "with_ctap2_1")]
MinPinLength,
#[cfg(feature = "with_ctap2_1")]
MinPinLengthRpIds,
} }
pub struct MasterKeys<'a> { pub struct MasterKeys<'a> {
@@ -136,6 +156,10 @@ impl StoreConfig for Config {
ATTESTATION_PRIVATE_KEY => add(Key::AttestationPrivateKey), ATTESTATION_PRIVATE_KEY => add(Key::AttestationPrivateKey),
ATTESTATION_CERTIFICATE => add(Key::AttestationCertificate), ATTESTATION_CERTIFICATE => add(Key::AttestationCertificate),
AAGUID => add(Key::Aaguid), 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), _ => debug_assert!(false),
} }
} }
@@ -200,15 +224,6 @@ impl PersistentStore {
}) })
.unwrap(); .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. // The following 3 entries are meant to be written by vendor-specific commands.
if USE_BATCH_ATTESTATION { if USE_BATCH_ATTESTATION {
if self.store.find_one(&Key::AttestationPrivateKey).is_none() { if self.store.find_one(&Key::AttestationPrivateKey).is_none() {
@@ -381,20 +396,26 @@ 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 { pub fn pin_retries(&self) -> u8 {
self.pin_retries_entry().1 self.store
.find_one(&Key::PinRetries)
.map_or(MAX_PIN_RETRIES, |(_, entry)| entry.data[0])
} }
pub fn decr_pin_retries(&mut self) { pub fn decr_pin_retries(&mut self) {
let (index, old_value) = self.pin_retries_entry(); match self.store.find_one(&Key::PinRetries) {
let new_value = old_value.saturating_sub(1); 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);
let new_value = entry.data[0].saturating_sub(1);
self.store self.store
.replace( .replace(
index, index,
@@ -406,19 +427,79 @@ impl PersistentStore {
) )
.unwrap(); .unwrap();
} }
}
}
pub fn reset_pin_retries(&mut self) { 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 self.store
.replace( .find_one(&Key::MinPinLength)
index, .map_or(DEFAULT_MIN_PIN_LENGTH, |(_, entry)| entry.data[0])
StoreEntry { }
tag: PIN_RETRIES,
data: &[MAX_PIN_RETRIES], #[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, sensitive: false,
}, };
) match self.store.find_one(&Key::MinPinLength) {
.unwrap(); 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( pub fn attestation_private_key(
@@ -541,7 +622,28 @@ fn serialize_credential(credential: PublicKeyCredentialSource) -> Result<Vec<u8>
if cbor::write(credential.into(), &mut data) { if cbor::write(credential.into(), &mut data) {
Ok(data) Ok(data)
} else { } 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)
} }
} }
@@ -892,4 +994,68 @@ mod test {
); );
assert_eq!(persistent_store.aaguid().unwrap(), key_material::AAGUID); 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 lenght 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 lenght is 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 rp_ids = vec![String::from("example.com")];
assert_eq!(
persistent_store._set_min_pin_length_rp_ids(rp_ids.clone()),
Ok(())
);
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);
}
} }