adds new client Pin subcommand minPinLength implementation
This commit is contained in:
@@ -103,6 +103,9 @@ 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.
|
||||
You can add relying parties to the list of readers of the minimum PIN length.
|
||||
|
||||
### 3D printed enclosure
|
||||
|
||||
|
||||
@@ -279,7 +279,7 @@ pub struct AuthenticatorClientPinParameters {
|
||||
pub new_pin_enc: Option<Vec<u8>>,
|
||||
pub pin_hash_enc: Option<Vec<u8>>,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
pub min_pin_length: Option<u64>,
|
||||
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")]
|
||||
@@ -329,7 +329,10 @@ impl TryFrom<cbor::Value> for AuthenticatorClientPinParameters {
|
||||
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()?;
|
||||
let min_pin_length = min_pin_length
|
||||
.map(extract_unsigned)
|
||||
.transpose()?
|
||||
.map(|m| m as u8);
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
let min_pin_length_rp_ids = match min_pin_length_rp_ids {
|
||||
Some(entry) => Some(
|
||||
|
||||
@@ -730,6 +730,8 @@ 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,
|
||||
},
|
||||
))
|
||||
@@ -812,7 +814,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.
|
||||
@@ -837,7 +839,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(),
|
||||
);
|
||||
|
||||
@@ -17,6 +17,7 @@ use super::data_formats::{ClientPinSubCommand, CoseKey, GetAssertionHmacSecretIn
|
||||
use super::response::{AuthenticatorClientPinResponse, ResponseData};
|
||||
use super::status_code::Ctap2StatusCode;
|
||||
use super::storage::PersistentStore;
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use core::convert::TryInto;
|
||||
@@ -101,7 +102,6 @@ fn encrypt_hmac_secret_output(
|
||||
Ok(encrypted_output)
|
||||
}
|
||||
|
||||
/// Checks if the decrypted PIN satisfies the PIN policy and stores it persistently.
|
||||
fn check_and_store_new_pin(
|
||||
persistent_store: &mut PersistentStore,
|
||||
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 last byte == 0x00
|
||||
return false;
|
||||
@@ -141,7 +145,7 @@ fn check_and_store_new_pin(
|
||||
pub struct PinProtocolV1 {
|
||||
key_agreement_key: crypto::ecdh::SecKey,
|
||||
pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH],
|
||||
consecutive_pin_mismatches: u64,
|
||||
consecutive_pin_mismatches: u8,
|
||||
}
|
||||
|
||||
impl PinProtocolV1 {
|
||||
@@ -341,31 +345,69 @@ impl PinProtocolV1 {
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
fn process_set_min_pin_length(
|
||||
&mut self,
|
||||
_min_pin_length: u64,
|
||||
_min_pin_length_rp_ids: Vec<String>,
|
||||
_pin_auth: Vec<u8>,
|
||||
) -> Result<AuthenticatorClientPinResponse, Ctap2StatusCode> {
|
||||
// TODO
|
||||
Ok(AuthenticatorClientPinResponse {
|
||||
key_agreement: None,
|
||||
pin_token: None,
|
||||
retries: Some(0),
|
||||
})
|
||||
persistent_store: &mut PersistentStore,
|
||||
min_pin_length: u8,
|
||||
min_pin_length_rp_ids: Option<Vec<String>>,
|
||||
pin_auth: Option<Vec<u8>>,
|
||||
) -> Result<(), Ctap2StatusCode> {
|
||||
if min_pin_length_rp_ids.is_some() {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION);
|
||||
}
|
||||
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")]
|
||||
fn process_get_pin_uv_auth_token_using_pin_with_permissions(
|
||||
&mut self,
|
||||
_key_agreement: CoseKey,
|
||||
_pin_hash_enc: Vec<u8>,
|
||||
_permissions: u8,
|
||||
_permissions_rp_id: String,
|
||||
rng: &mut impl Rng256,
|
||||
persistent_store: &mut PersistentStore,
|
||||
key_agreement: CoseKey,
|
||||
pin_hash_enc: Vec<u8>,
|
||||
permissions: u8,
|
||||
_permissions_rp_id: Option<String>,
|
||||
) -> 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
|
||||
Ok(AuthenticatorClientPinResponse {
|
||||
key_agreement: None,
|
||||
pin_token: None,
|
||||
retries: Some(0),
|
||||
retries: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -441,22 +483,24 @@ impl PinProtocolV1 {
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
ClientPinSubCommand::SetMinPinLength => {
|
||||
self.process_set_min_pin_length(
|
||||
persistent_store,
|
||||
min_pin_length.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
|
||||
min_pin_length_rp_ids.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
|
||||
pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
|
||||
min_pin_length_rp_ids,
|
||||
pin_auth,
|
||||
)?;
|
||||
None
|
||||
}
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => {
|
||||
ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => Some(
|
||||
self.process_get_pin_uv_auth_token_using_pin_with_permissions(
|
||||
rng,
|
||||
persistent_store,
|
||||
key_agreement.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_rp_id.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?,
|
||||
)?;
|
||||
None
|
||||
}
|
||||
permissions_rp_id,
|
||||
)?,
|
||||
),
|
||||
};
|
||||
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]
|
||||
fn test_process() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -301,14 +305,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"]],
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -329,6 +342,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 +359,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));
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
// 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;
|
||||
@@ -20,7 +22,7 @@ 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;
|
||||
@@ -60,11 +62,25 @@ const PIN_RETRIES: usize = 4;
|
||||
const ATTESTATION_PRIVATE_KEY: usize = 5;
|
||||
const ATTESTATION_CERTIFICATE: usize = 6;
|
||||
const AAGUID: usize = 7;
|
||||
#[cfg(not(feature = "with_ctap2_1"))]
|
||||
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 ATTESTATION_PRIVATE_KEY_LENGTH: usize = 32;
|
||||
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)]
|
||||
enum Key {
|
||||
@@ -82,6 +98,10 @@ enum Key {
|
||||
AttestationPrivateKey,
|
||||
AttestationCertificate,
|
||||
Aaguid,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
MinPinLength,
|
||||
#[cfg(feature = "with_ctap2_1")]
|
||||
MinPinLengthRpIds,
|
||||
}
|
||||
|
||||
pub struct MasterKeys<'a> {
|
||||
@@ -136,6 +156,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),
|
||||
}
|
||||
}
|
||||
@@ -200,15 +224,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() {
|
||||
@@ -381,44 +396,110 @@ 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)| 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);
|
||||
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)| 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(
|
||||
@@ -541,7 +622,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -892,4 +994,68 @@ 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 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user