adds the config command

This commit is contained in:
Fabian Kaczmarczyck
2021-01-07 18:17:21 +01:00
parent 32d5ff91d4
commit f4eb6c938e
9 changed files with 796 additions and 335 deletions

View File

@@ -93,32 +93,37 @@ a few things you can personalize:
1. If you have multiple buttons, choose the buttons responsible for user 1. If you have multiple buttons, choose the buttons responsible for user
presence in `main.rs`. presence in `main.rs`.
2. Decide whether you want to use batch attestation. There is a boolean flag in 1. Decide whether you want to use batch attestation. There is a boolean flag in
`ctap/mod.rs`. It is mandatory for U2F, and you can create your own `ctap/mod.rs`. It is mandatory for U2F, and you can create your own
self-signed certificate. The flag is used for FIDO2 and has some privacy self-signed certificate. The flag is used for FIDO2 and has some privacy
implications. Please check implications. Please check
[WebAuthn](https://www.w3.org/TR/webauthn/#attestation) for more [WebAuthn](https://www.w3.org/TR/webauthn/#attestation) for more
information. information.
3. Decide whether you want to use signature counters. Currently, only global 1. Decide whether you want to use signature counters. Currently, only global
signature counters are implemented, as they are the default option for U2F. signature counters are implemented, as they are the default option for U2F.
The flag in `ctap/mod.rs` only turns them off for FIDO2. The most privacy The flag in `ctap/mod.rs` only turns them off for FIDO2. The most privacy
preserving solution is individual or no signature counters. Again, please preserving solution is individual or no signature counters. Again, please
check [WebAuthn](https://www.w3.org/TR/webauthn/#signature-counter) for check [WebAuthn](https://www.w3.org/TR/webauthn/#signature-counter) for
documentation. documentation.
4. Depending on your available flash storage, choose an appropriate maximum 1. Depending on your available flash storage, choose an appropriate maximum
number of supported residential keys and number of pages in number of supported residential keys and number of pages in
`ctap/storage.rs`. `ctap/storage.rs`.
5. Change the default level for the credProtect extension in `ctap/mod.rs`. 1. Change the default level for the credProtect extension in `ctap/mod.rs`.
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`. 1. 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 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 PINs can help establish trust between users and relying parties. It makes
user verification harder to break, but less convenient. user verification harder to break, but less convenient.
NIST recommends at least 6-digit PINs in section 5.1.9.1: NIST recommends at least 6-digit PINs in section 5.1.9.1:
https://pages.nist.gov/800-63-3/sp800-63b.html 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. You can add relying parties to the list of readers of the minimum PIN length.
1. In an enterprise setting, you can adapt `DEFAULT_MIN_PIN_LENGTH_RP_IDS` and
`MAX_RP_IDS_LENGTH` for tuning the `minPinLength` extension. The former
allows some relying parties to read the minimum PIN length by default. The
latter allows storing more relying parties that may check the minimum PIN
length.
### 3D printed enclosure ### 3D printed enclosure

View File

@@ -14,10 +14,10 @@
use super::data_formats::{ use super::data_formats::{
extract_array, extract_bool, extract_byte_string, extract_map, extract_text_string, extract_array, extract_bool, extract_byte_string, extract_map, extract_text_string,
extract_unsigned, ok_or_missing, ClientPinSubCommand, CoseKey, GetAssertionExtensions, extract_unsigned, ok_or_missing, ClientPinSubCommand, ConfigSubCommand, ConfigSubCommandParams,
GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions, CoseKey, GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions,
PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, MakeCredentialOptions, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter,
PublicKeyCredentialUserEntity, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, SetMinPinLengthParams,
}; };
use super::key_material; use super::key_material;
use super::status_code::Ctap2StatusCode; use super::status_code::Ctap2StatusCode;
@@ -42,6 +42,7 @@ pub enum Command {
AuthenticatorReset, AuthenticatorReset,
AuthenticatorGetNextAssertion, AuthenticatorGetNextAssertion,
AuthenticatorSelection, AuthenticatorSelection,
AuthenticatorConfig(AuthenticatorConfigParameters),
// TODO(kaczmarczyck) implement FIDO 2.1 commands (see below consts) // TODO(kaczmarczyck) implement FIDO 2.1 commands (see below consts)
// Vendor specific commands // Vendor specific commands
AuthenticatorVendorConfigure(AuthenticatorVendorConfigureParameters), AuthenticatorVendorConfigure(AuthenticatorVendorConfigureParameters),
@@ -114,6 +115,12 @@ impl Command {
// Parameters are ignored. // Parameters are ignored.
Ok(Command::AuthenticatorSelection) Ok(Command::AuthenticatorSelection)
} }
Command::AUTHENTICATOR_CONFIG => {
let decoded_cbor = cbor::read(&bytes[1..])?;
Ok(Command::AuthenticatorConfig(
AuthenticatorConfigParameters::try_from(decoded_cbor)?,
))
}
Command::AUTHENTICATOR_VENDOR_CONFIGURE => { Command::AUTHENTICATOR_VENDOR_CONFIGURE => {
let decoded_cbor = cbor::read(&bytes[1..])?; let decoded_cbor = cbor::read(&bytes[1..])?;
Ok(Command::AuthenticatorVendorConfigure( Ok(Command::AuthenticatorVendorConfigure(
@@ -290,8 +297,6 @@ pub struct AuthenticatorClientPinParameters {
pub pin_auth: Option<Vec<u8>>, pub pin_auth: Option<Vec<u8>>,
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>>,
pub min_pin_length: Option<u8>,
pub min_pin_length_rp_ids: Option<Vec<String>>,
pub permissions: Option<u8>, pub permissions: Option<u8>,
pub permissions_rp_id: Option<String>, pub permissions_rp_id: Option<String>,
} }
@@ -308,8 +313,6 @@ impl TryFrom<cbor::Value> for AuthenticatorClientPinParameters {
4 => pin_auth, 4 => pin_auth,
5 => new_pin_enc, 5 => new_pin_enc,
6 => pin_hash_enc, 6 => pin_hash_enc,
7 => min_pin_length,
8 => min_pin_length_rp_ids,
9 => permissions, 9 => permissions,
10 => permissions_rp_id, 10 => permissions_rp_id,
} = extract_map(cbor_value)?; } = extract_map(cbor_value)?;
@@ -321,21 +324,6 @@ impl TryFrom<cbor::Value> for AuthenticatorClientPinParameters {
let pin_auth = pin_auth.map(extract_byte_string).transpose()?; let pin_auth = pin_auth.map(extract_byte_string).transpose()?;
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()?;
let min_pin_length = min_pin_length
.map(extract_unsigned)
.transpose()?
.map(u8::try_from)
.transpose()
.map_err(|_| Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)?;
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,
};
// We expect a bit field of 8 bits, and drop everything else. // We expect a bit field of 8 bits, and drop everything else.
// This means we ignore extensions in future versions. // This means we ignore extensions in future versions.
let permissions = permissions let permissions = permissions
@@ -351,14 +339,52 @@ impl TryFrom<cbor::Value> for AuthenticatorClientPinParameters {
pin_auth, pin_auth,
new_pin_enc, new_pin_enc,
pin_hash_enc, pin_hash_enc,
min_pin_length,
min_pin_length_rp_ids,
permissions, permissions,
permissions_rp_id, permissions_rp_id,
}) })
} }
} }
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct AuthenticatorConfigParameters {
pub sub_command: ConfigSubCommand,
pub sub_command_params: Option<ConfigSubCommandParams>,
pub pin_uv_auth_param: Option<Vec<u8>>,
pub pin_uv_auth_protocol: Option<u64>,
}
impl TryFrom<cbor::Value> for AuthenticatorConfigParameters {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
destructure_cbor_map! {
let {
0x01 => sub_command,
0x02 => sub_command_params,
0x03 => pin_uv_auth_param,
0x04 => pin_uv_auth_protocol,
} = extract_map(cbor_value)?;
}
let sub_command = ConfigSubCommand::try_from(ok_or_missing(sub_command)?)?;
let sub_command_params = match sub_command {
ConfigSubCommand::SetMinPinLength => Some(ConfigSubCommandParams::SetMinPinLength(
SetMinPinLengthParams::try_from(ok_or_missing(sub_command_params)?)?,
)),
_ => None,
};
let pin_uv_auth_param = pin_uv_auth_param.map(extract_byte_string).transpose()?;
let pin_uv_auth_protocol = pin_uv_auth_protocol.map(extract_unsigned).transpose()?;
Ok(AuthenticatorConfigParameters {
sub_command,
sub_command_params,
pin_uv_auth_param,
pin_uv_auth_protocol,
})
}
}
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct AuthenticatorAttestationMaterial { pub struct AuthenticatorAttestationMaterial {
pub certificate: Vec<u8>, pub certificate: Vec<u8>,
@@ -541,8 +567,6 @@ mod test {
4 => vec! [0xBB], 4 => vec! [0xBB],
5 => vec! [0xCC], 5 => vec! [0xCC],
6 => vec! [0xDD], 6 => vec! [0xDD],
7 => 4,
8 => cbor_array!["example.com"],
9 => 0x03, 9 => 0x03,
10 => "example.com", 10 => "example.com",
}; };
@@ -556,8 +580,6 @@ mod test {
pin_auth: Some(vec![0xBB]), pin_auth: Some(vec![0xBB]),
new_pin_enc: Some(vec![0xCC]), new_pin_enc: Some(vec![0xCC]),
pin_hash_enc: Some(vec![0xDD]), pin_hash_enc: Some(vec![0xDD]),
min_pin_length: Some(4),
min_pin_length_rp_ids: Some(vec!["example.com".to_string()]),
permissions: Some(0x03), permissions: Some(0x03),
permissions_rp_id: Some("example.com".to_string()), permissions_rp_id: Some("example.com".to_string()),
}; };

269
src/ctap/config_command.rs Normal file
View File

@@ -0,0 +1,269 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::check_pin_uv_auth_protocol;
use super::command::AuthenticatorConfigParameters;
use super::data_formats::{ConfigSubCommand, ConfigSubCommandParams, SetMinPinLengthParams};
use super::pin_protocol_v1::PinProtocolV1;
use super::response::ResponseData;
use super::status_code::Ctap2StatusCode;
use super::storage::PersistentStore;
use alloc::vec;
fn process_set_min_pin_length(
persistent_store: &mut PersistentStore,
pin_protocol_v1: &mut PinProtocolV1,
params: SetMinPinLengthParams,
) -> Result<ResponseData, Ctap2StatusCode> {
let SetMinPinLengthParams {
new_min_pin_length,
min_pin_length_rp_ids,
force_change_pin,
} = params;
let store_min_pin_length = persistent_store.min_pin_length()?;
let new_min_pin_length = new_min_pin_length.unwrap_or(store_min_pin_length);
if new_min_pin_length < store_min_pin_length {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION);
}
let mut force_change_pin = force_change_pin.unwrap_or(false);
if force_change_pin && persistent_store.pin_hash()?.is_none() {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET);
}
if let Some(old_length) = persistent_store.pin_code_point_length()? {
force_change_pin |= new_min_pin_length > old_length;
}
pin_protocol_v1.force_pin_change |= force_change_pin;
// TODO(kaczmarczyck) actually force a PIN change
persistent_store.set_min_pin_length(new_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(ResponseData::AuthenticatorConfig)
}
pub fn process_config(
persistent_store: &mut PersistentStore,
pin_protocol_v1: &mut PinProtocolV1,
params: AuthenticatorConfigParameters,
) -> Result<ResponseData, Ctap2StatusCode> {
let AuthenticatorConfigParameters {
sub_command,
sub_command_params,
pin_uv_auth_param,
pin_uv_auth_protocol,
} = params;
if persistent_store.pin_hash()?.is_some() {
// TODO(kaczmarczyck) The error code is specified inconsistently with other commands.
check_pin_uv_auth_protocol(pin_uv_auth_protocol)
.map_err(|_| Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?;
let auth_param = pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?;
let mut config_data = vec![0xFF; 32];
config_data.extend(&[0x0D, sub_command as u8]);
if let Some(sub_command_params) = sub_command_params.clone() {
if !cbor::write(sub_command_params.into(), &mut config_data) {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
}
}
if !pin_protocol_v1.verify_pin_auth_token(&config_data, &auth_param) {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID);
}
}
match sub_command {
ConfigSubCommand::SetMinPinLength => {
if let Some(ConfigSubCommandParams::SetMinPinLength(params)) = sub_command_params {
process_set_min_pin_length(persistent_store, pin_protocol_v1, params)
} else {
Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)
}
}
_ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER),
}
}
#[cfg(test)]
mod test {
use super::super::command::AuthenticatorConfigParameters;
use super::*;
use crypto::rng256::ThreadRng256;
fn create_min_pin_config_params(
min_pin_length: u8,
min_pin_length_rp_ids: Option<Vec<String>>,
) -> AuthenticatorConfigParameters {
let set_min_pin_length_params = SetMinPinLengthParams {
new_min_pin_length: Some(min_pin_length),
min_pin_length_rp_ids,
force_change_pin: None,
};
AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::SetMinPinLength,
sub_command_params: Some(ConfigSubCommandParams::SetMinPinLength(
set_min_pin_length_params,
)),
pin_uv_auth_param: None,
pin_uv_auth_protocol: Some(1),
}
}
#[test]
fn test_process_set_min_pin_length() {
let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng);
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
let pin_uv_auth_token = [0x55; 32];
let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token);
// First, increase minimum PIN length from 4 to 6 without PIN auth.
let min_pin_length = 6;
let config_params = create_min_pin_config_params(min_pin_length, None);
let config_response =
process_config(&mut persistent_store, &mut pin_protocol_v1, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length));
// Second, increase minimum PIN length from 6 to 8 with PIN auth.
// The stored PIN or its length don't matter since we control the token.
persistent_store.set_pin(&[0x88; 16], 8).unwrap();
let min_pin_length = 8;
let mut config_params = create_min_pin_config_params(min_pin_length, None);
let pin_auth = vec![
0x5C, 0x69, 0x71, 0x29, 0xBD, 0xCC, 0x53, 0xE8, 0x3C, 0x97, 0x62, 0xDD, 0x90, 0x29,
0xB2, 0xDE,
];
config_params.pin_uv_auth_param = Some(pin_auth);
let config_response =
process_config(&mut persistent_store, &mut pin_protocol_v1, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length));
// Third, decreasing the minimum PIN length from 8 to 7 fails.
let mut config_params = create_min_pin_config_params(7, None);
let pin_auth = vec![
0xC5, 0xEA, 0xC1, 0x5E, 0x7F, 0x80, 0x70, 0x1A, 0x4E, 0xC4, 0xAD, 0x85, 0x35, 0xD8,
0xA7, 0x71,
];
config_params.pin_uv_auth_param = Some(pin_auth);
let config_response =
process_config(&mut persistent_store, &mut pin_protocol_v1, config_params);
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)
);
assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length));
}
#[test]
fn test_process_set_min_pin_length_rp_ids() {
let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng);
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
let pin_uv_auth_token = [0x55; 32];
let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token);
// First, set RP IDs without PIN auth.
let min_pin_length = 6;
let min_pin_length_rp_ids = vec!["example.com".to_string()];
let config_params =
create_min_pin_config_params(min_pin_length, Some(min_pin_length_rp_ids.clone()));
let config_response =
process_config(&mut persistent_store, &mut pin_protocol_v1, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length));
assert_eq!(
persistent_store.min_pin_length_rp_ids(),
Ok(min_pin_length_rp_ids)
);
// Second, change the RP IDs with PIN auth.
let min_pin_length = 8;
let min_pin_length_rp_ids = vec!["another.example.com".to_string()];
// The stored PIN or its length don't matter since we control the token.
persistent_store.set_pin(&[0x88; 16], 8).unwrap();
let mut config_params =
create_min_pin_config_params(min_pin_length, Some(min_pin_length_rp_ids.clone()));
let pin_auth = vec![
0x40, 0x51, 0x2D, 0xAC, 0x2D, 0xE2, 0x15, 0x77, 0x5C, 0xF9, 0x5B, 0x62, 0x9A, 0x2D,
0xD6, 0xDA,
];
config_params.pin_uv_auth_param = Some(pin_auth.clone());
let config_response =
process_config(&mut persistent_store, &mut pin_protocol_v1, config_params);
assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig));
assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length));
assert_eq!(
persistent_store.min_pin_length_rp_ids(),
Ok(min_pin_length_rp_ids.clone())
);
// Third, changing RP IDs with bad PIN auth fails.
// One PIN auth shouldn't work for different lengths.
let mut config_params =
create_min_pin_config_params(9, Some(min_pin_length_rp_ids.clone()));
config_params.pin_uv_auth_param = Some(pin_auth.clone());
let config_response =
process_config(&mut persistent_store, &mut pin_protocol_v1, config_params);
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length));
assert_eq!(
persistent_store.min_pin_length_rp_ids(),
Ok(min_pin_length_rp_ids.clone())
);
// Forth, changing RP IDs with bad PIN auth fails.
// One PIN auth shouldn't work for different RP IDs.
let mut config_params = create_min_pin_config_params(
min_pin_length,
Some(vec!["counter.example.com".to_string()]),
);
config_params.pin_uv_auth_param = Some(pin_auth);
let config_response =
process_config(&mut persistent_store, &mut pin_protocol_v1, config_params);
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length));
assert_eq!(
persistent_store.min_pin_length_rp_ids(),
Ok(min_pin_length_rp_ids)
);
}
#[test]
fn test_process_config_vendor_prototype() {
let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng);
let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng);
let pin_uv_auth_token = [0x55; 32];
let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token);
let config_params = AuthenticatorConfigParameters {
sub_command: ConfigSubCommand::VendorPrototype,
sub_command_params: None,
pin_uv_auth_param: None,
pin_uv_auth_protocol: None,
};
let config_response =
process_config(&mut persistent_store, &mut pin_protocol_v1, config_params);
assert_eq!(
config_response,
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
}
}

View File

@@ -262,6 +262,7 @@ impl From<PublicKeyCredentialDescriptor> for cbor::Value {
pub struct MakeCredentialExtensions { pub struct MakeCredentialExtensions {
pub hmac_secret: bool, pub hmac_secret: bool,
pub cred_protect: Option<CredentialProtectionPolicy>, pub cred_protect: Option<CredentialProtectionPolicy>,
pub min_pin_length: bool,
} }
impl TryFrom<cbor::Value> for MakeCredentialExtensions { impl TryFrom<cbor::Value> for MakeCredentialExtensions {
@@ -272,6 +273,7 @@ impl TryFrom<cbor::Value> for MakeCredentialExtensions {
let { let {
"credProtect" => cred_protect, "credProtect" => cred_protect,
"hmac-secret" => hmac_secret, "hmac-secret" => hmac_secret,
"minPinLength" => min_pin_length,
} = extract_map(cbor_value)?; } = extract_map(cbor_value)?;
} }
@@ -279,9 +281,11 @@ impl TryFrom<cbor::Value> for MakeCredentialExtensions {
let cred_protect = cred_protect let cred_protect = cred_protect
.map(CredentialProtectionPolicy::try_from) .map(CredentialProtectionPolicy::try_from)
.transpose()?; .transpose()?;
let min_pin_length = min_pin_length.map_or(Ok(false), extract_bool)?;
Ok(Self { Ok(Self {
hmac_secret, hmac_secret,
cred_protect, cred_protect,
min_pin_length,
}) })
} }
} }
@@ -706,7 +710,6 @@ pub enum ClientPinSubCommand {
GetPinToken = 0x05, GetPinToken = 0x05,
GetPinUvAuthTokenUsingUvWithPermissions = 0x06, GetPinUvAuthTokenUsingUvWithPermissions = 0x06,
GetUvRetries = 0x07, GetUvRetries = 0x07,
SetMinPinLength = 0x08,
GetPinUvAuthTokenUsingPinWithPermissions = 0x09, GetPinUvAuthTokenUsingPinWithPermissions = 0x09,
} }
@@ -729,13 +732,114 @@ impl TryFrom<cbor::Value> for ClientPinSubCommand {
0x05 => Ok(ClientPinSubCommand::GetPinToken), 0x05 => Ok(ClientPinSubCommand::GetPinToken),
0x06 => Ok(ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions), 0x06 => Ok(ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions),
0x07 => Ok(ClientPinSubCommand::GetUvRetries), 0x07 => Ok(ClientPinSubCommand::GetUvRetries),
0x08 => Ok(ClientPinSubCommand::SetMinPinLength),
0x09 => Ok(ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions), 0x09 => Ok(ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions),
_ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND), _ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND),
} }
} }
} }
#[derive(Clone, Copy)]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
#[cfg_attr(test, derive(IntoEnumIterator))]
pub enum ConfigSubCommand {
EnableEnterpriseAttestation = 0x01,
ToggleAlwaysUv = 0x02,
SetMinPinLength = 0x03,
VendorPrototype = 0xFF,
}
impl From<ConfigSubCommand> for cbor::Value {
fn from(subcommand: ConfigSubCommand) -> Self {
(subcommand as u64).into()
}
}
impl TryFrom<cbor::Value> for ConfigSubCommand {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
let subcommand_int = extract_unsigned(cbor_value)?;
match subcommand_int {
0x01 => Ok(ConfigSubCommand::EnableEnterpriseAttestation),
0x02 => Ok(ConfigSubCommand::ToggleAlwaysUv),
0x03 => Ok(ConfigSubCommand::SetMinPinLength),
0xFF => Ok(ConfigSubCommand::VendorPrototype),
_ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND),
}
}
}
#[derive(Clone)]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub enum ConfigSubCommandParams {
SetMinPinLength(SetMinPinLengthParams),
}
impl From<ConfigSubCommandParams> for cbor::Value {
fn from(params: ConfigSubCommandParams) -> Self {
match params {
ConfigSubCommandParams::SetMinPinLength(set_min_pin_length_params) => {
set_min_pin_length_params.into()
}
}
}
}
#[derive(Clone)]
#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))]
pub struct SetMinPinLengthParams {
pub new_min_pin_length: Option<u8>,
pub min_pin_length_rp_ids: Option<Vec<String>>,
pub force_change_pin: Option<bool>,
}
impl TryFrom<cbor::Value> for SetMinPinLengthParams {
type Error = Ctap2StatusCode;
fn try_from(cbor_value: cbor::Value) -> Result<Self, Ctap2StatusCode> {
destructure_cbor_map! {
let {
0x01 => new_min_pin_length,
0x02 => min_pin_length_rp_ids,
0x03 => force_change_pin,
} = extract_map(cbor_value)?;
}
let new_min_pin_length = new_min_pin_length
.map(extract_unsigned)
.transpose()?
.map(u8::try_from)
.transpose()
.map_err(|_| Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)?;
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,
};
let force_change_pin = force_change_pin.map(extract_bool).transpose()?;
Ok(Self {
new_min_pin_length,
min_pin_length_rp_ids,
force_change_pin,
})
}
}
impl From<SetMinPinLengthParams> for cbor::Value {
fn from(params: SetMinPinLengthParams) -> Self {
cbor_map_options! {
0x01 => params.new_min_pin_length.map(|u| u as u64),
0x02 => params.min_pin_length_rp_ids.map(|vec| cbor_array_vec!(vec)),
0x03 => params.force_change_pin,
}
}
}
pub(super) fn extract_unsigned(cbor_value: cbor::Value) -> Result<u64, Ctap2StatusCode> { pub(super) fn extract_unsigned(cbor_value: cbor::Value) -> Result<u64, Ctap2StatusCode> {
match cbor_value { match cbor_value {
cbor::Value::KeyValue(cbor::KeyType::Unsigned(unsigned)) => Ok(unsigned), cbor::Value::KeyValue(cbor::KeyType::Unsigned(unsigned)) => Ok(unsigned),
@@ -1240,11 +1344,13 @@ mod test {
let cbor_extensions = cbor_map! { let cbor_extensions = cbor_map! {
"hmac-secret" => true, "hmac-secret" => true,
"credProtect" => CredentialProtectionPolicy::UserVerificationRequired, "credProtect" => CredentialProtectionPolicy::UserVerificationRequired,
"minPinLength" => true,
}; };
let extensions = MakeCredentialExtensions::try_from(cbor_extensions); let extensions = MakeCredentialExtensions::try_from(cbor_extensions);
let expected_extensions = MakeCredentialExtensions { let expected_extensions = MakeCredentialExtensions {
hmac_secret: true, hmac_secret: true,
cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired), cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired),
min_pin_length: true,
}; };
assert_eq!(extensions, Ok(expected_extensions)); assert_eq!(extensions, Ok(expected_extensions));
} }
@@ -1347,6 +1453,56 @@ mod test {
} }
} }
#[test]
fn test_from_into_config_sub_command() {
let cbor_sub_command: cbor::Value = cbor_int!(0x01);
let sub_command = ConfigSubCommand::try_from(cbor_sub_command.clone());
let expected_sub_command = ConfigSubCommand::EnableEnterpriseAttestation;
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 ConfigSubCommand::into_enum_iter() {
let created_cbor: cbor::Value = command.clone().into();
let reconstructed = ConfigSubCommand::try_from(created_cbor).unwrap();
assert_eq!(command, reconstructed);
}
}
#[test]
fn test_from_set_min_pin_length_params() {
let params = SetMinPinLengthParams {
new_min_pin_length: Some(6),
min_pin_length_rp_ids: Some(vec!["example.com".to_string()]),
force_change_pin: Some(true),
};
let cbor_params = cbor_map! {
0x01 => 6,
0x02 => cbor_array_vec!(vec!["example.com".to_string()]),
0x03 => true,
};
assert_eq!(cbor::Value::from(params.clone()), cbor_params);
let reconstructed_params = SetMinPinLengthParams::try_from(cbor_params);
assert_eq!(reconstructed_params, Ok(params));
}
#[test]
fn test_from_config_sub_command_params() {
let set_min_pin_length_params = SetMinPinLengthParams {
new_min_pin_length: Some(6),
min_pin_length_rp_ids: Some(vec!["example.com".to_string()]),
force_change_pin: Some(true),
};
let config_sub_command_params =
ConfigSubCommandParams::SetMinPinLength(set_min_pin_length_params);
let cbor_params = cbor_map! {
0x01 => 6,
0x02 => cbor_array_vec!(vec!["example.com".to_string()]),
0x03 => true,
};
assert_eq!(cbor::Value::from(config_sub_command_params), cbor_params);
}
#[test] #[test]
fn test_credential_source_cbor_round_trip() { fn test_credential_source_cbor_round_trip() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};

View File

@@ -14,6 +14,7 @@
pub mod apdu; pub mod apdu;
pub mod command; pub mod command;
mod config_command;
#[cfg(feature = "with_ctap1")] #[cfg(feature = "with_ctap1")]
mod ctap1; mod ctap1;
pub mod data_formats; pub mod data_formats;
@@ -30,6 +31,7 @@ use self::command::{
AuthenticatorMakeCredentialParameters, AuthenticatorVendorConfigureParameters, Command, AuthenticatorMakeCredentialParameters, AuthenticatorVendorConfigureParameters, Command,
MAX_CREDENTIAL_COUNT_IN_LIST, MAX_CREDENTIAL_COUNT_IN_LIST,
}; };
use self::config_command::process_config;
use self::data_formats::{ use self::data_formats::{
AuthenticatorTransport, CredentialProtectionPolicy, GetAssertionHmacSecretInput, AuthenticatorTransport, CredentialProtectionPolicy, GetAssertionHmacSecretInput,
PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter,
@@ -106,6 +108,9 @@ pub const U2F_VERSION_STRING: &str = "U2F_V2";
// TODO(#106) change to final string when ready // TODO(#106) change to final string when ready
pub const FIDO2_1_VERSION_STRING: &str = "FIDO_2_1_PRE"; pub const FIDO2_1_VERSION_STRING: &str = "FIDO_2_1_PRE";
// This is the currently supported PIN protocol version.
const PIN_PROTOCOL_VERSION: u64 = 1;
// We currently only support one algorithm for signatures: ES256. // We currently only support one algorithm for signatures: ES256.
// This algorithm is requested in MakeCredential and advertized in GetInfo. // This algorithm is requested in MakeCredential and advertized in GetInfo.
pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialParameter { pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialParameter {
@@ -117,6 +122,17 @@ pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa
// - Some(CredentialProtectionPolicy::UserVerificationRequired) // - Some(CredentialProtectionPolicy::UserVerificationRequired)
const DEFAULT_CRED_PROTECT: Option<CredentialProtectionPolicy> = None; const DEFAULT_CRED_PROTECT: Option<CredentialProtectionPolicy> = None;
// Checks the PIN protocol parameter against all supported versions.
pub fn check_pin_uv_auth_protocol(
pin_uv_auth_protocol: Option<u64>,
) -> Result<(), Ctap2StatusCode> {
match pin_uv_auth_protocol {
Some(PIN_PROTOCOL_VERSION) => Ok(()),
Some(_) => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID),
None => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID),
}
}
// This function is adapted from https://doc.rust-lang.org/nightly/src/core/str/mod.rs.html#2110 // 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. // (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. // We change the return value, since we don't need the bool.
@@ -172,8 +188,6 @@ where
R: Rng256, R: Rng256,
CheckUserPresence: Fn(ChannelID) -> Result<(), Ctap2StatusCode>, CheckUserPresence: Fn(ChannelID) -> Result<(), Ctap2StatusCode>,
{ {
pub const PIN_PROTOCOL_VERSION: u64 = 1;
pub fn new( pub fn new(
rng: &'a mut R, rng: &'a mut R,
check_user_presence: CheckUserPresence, check_user_presence: CheckUserPresence,
@@ -351,6 +365,11 @@ where
Command::AuthenticatorClientPin(params) => self.process_client_pin(params), Command::AuthenticatorClientPin(params) => self.process_client_pin(params),
Command::AuthenticatorReset => self.process_reset(cid, now), Command::AuthenticatorReset => self.process_reset(cid, now),
Command::AuthenticatorSelection => self.process_selection(cid), Command::AuthenticatorSelection => self.process_selection(cid),
Command::AuthenticatorConfig(params) => process_config(
&mut self.persistent_store,
&mut self.pin_protocol_v1,
params,
),
// TODO(kaczmarczyck) implement FIDO 2.1 commands // TODO(kaczmarczyck) implement FIDO 2.1 commands
// Vendor specific commands // Vendor specific commands
Command::AuthenticatorVendorConfigure(params) => { Command::AuthenticatorVendorConfigure(params) => {
@@ -394,11 +413,7 @@ where
} }
} }
match pin_uv_auth_protocol { check_pin_uv_auth_protocol(pin_uv_auth_protocol)
Some(CtapState::<R, CheckUserPresence>::PIN_PROTOCOL_VERSION) => Ok(()),
Some(_) => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID),
None => Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER),
}
} else { } else {
Ok(()) Ok(())
} }
@@ -427,7 +442,9 @@ where
return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM);
} }
let (use_hmac_extension, cred_protect_policy) = if let Some(extensions) = extensions { let rp_id = rp.rp_id;
let (use_hmac_extension, cred_protect_policy, min_pin_length) =
if let Some(extensions) = extensions {
let mut cred_protect = extensions.cred_protect; let mut cred_protect = extensions.cred_protect;
if cred_protect.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) if cred_protect.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional)
< DEFAULT_CRED_PROTECT < DEFAULT_CRED_PROTECT
@@ -435,14 +452,19 @@ where
{ {
cred_protect = DEFAULT_CRED_PROTECT; cred_protect = DEFAULT_CRED_PROTECT;
} }
(extensions.hmac_secret, cred_protect) let min_pin_length = extensions.min_pin_length
&& self
.persistent_store
.min_pin_length_rp_ids()?
.contains(&rp_id);
(extensions.hmac_secret, cred_protect, min_pin_length)
} else { } else {
(false, DEFAULT_CRED_PROTECT) (false, DEFAULT_CRED_PROTECT, false)
}; };
let has_extension_output = use_hmac_extension || cred_protect_policy.is_some(); let has_extension_output =
use_hmac_extension || cred_protect_policy.is_some() || min_pin_length;
let rp_id = rp.rp_id;
let rp_id_hash = Sha256::hash(rp_id.as_bytes()); let rp_id_hash = Sha256::hash(rp_id.as_bytes());
if let Some(exclude_list) = exclude_list { if let Some(exclude_list) = exclude_list {
for cred_desc in exclude_list { for cred_desc in exclude_list {
@@ -541,9 +563,15 @@ where
auth_data.extend(cose_key); auth_data.extend(cose_key);
if has_extension_output { if has_extension_output {
let hmac_secret_output = if use_hmac_extension { Some(true) } else { None }; let hmac_secret_output = if use_hmac_extension { Some(true) } else { None };
let min_pin_length_output = if min_pin_length {
Some(self.persistent_store.min_pin_length()? as u64)
} else {
None
};
let extensions_output = cbor_map_options! { let extensions_output = cbor_map_options! {
"hmac-secret" => hmac_secret_output, "hmac-secret" => hmac_secret_output,
"credProtect" => cred_protect_policy, "credProtect" => cred_protect_policy,
"minPinLength" => min_pin_length_output,
}; };
if !cbor::write(extensions_output, &mut auth_data) { if !cbor::write(extensions_output, &mut auth_data) {
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
@@ -832,6 +860,7 @@ where
String::from("clientPin"), String::from("clientPin"),
self.persistent_store.pin_hash()?.is_some(), self.persistent_store.pin_hash()?.is_some(),
); );
options_map.insert(String::from("setMinPINLength"), true);
Ok(ResponseData::AuthenticatorGetInfo( Ok(ResponseData::AuthenticatorGetInfo(
AuthenticatorGetInfoResponse { AuthenticatorGetInfoResponse {
versions: vec![ versions: vec![
@@ -840,13 +869,15 @@ where
String::from(FIDO2_VERSION_STRING), String::from(FIDO2_VERSION_STRING),
String::from(FIDO2_1_VERSION_STRING), String::from(FIDO2_1_VERSION_STRING),
], ],
extensions: Some(vec![String::from("hmac-secret")]), extensions: Some(vec![
String::from("hmac-secret"),
String::from("credProtect"),
String::from("minPinLength"),
]),
aaguid: self.persistent_store.aaguid()?, aaguid: self.persistent_store.aaguid()?,
options: Some(options_map), options: Some(options_map),
max_msg_size: Some(1024), max_msg_size: Some(1024),
pin_protocols: Some(vec![ pin_protocols: Some(vec![PIN_PROTOCOL_VERSION]),
CtapState::<R, CheckUserPresence>::PIN_PROTOCOL_VERSION,
]),
max_credential_count_in_list: MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), max_credential_count_in_list: MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64),
// #TODO(106) update with version 2.1 of HMAC-secret // #TODO(106) update with version 2.1 of HMAC-secret
max_credential_id_length: Some(CREDENTIAL_ID_SIZE as u64), max_credential_id_length: Some(CREDENTIAL_ID_SIZE as u64),
@@ -1008,6 +1039,49 @@ mod test {
// ID is irrelevant, so we pass this (dummy but valid) value. // ID is irrelevant, so we pass this (dummy but valid) value.
const DUMMY_CHANNEL_ID: ChannelID = [0x12, 0x34, 0x56, 0x78]; const DUMMY_CHANNEL_ID: ChannelID = [0x12, 0x34, 0x56, 0x78];
fn check_make_response(
make_credential_response: Result<ResponseData, Ctap2StatusCode>,
flags: u8,
expected_aaguid: &[u8],
expected_credential_id_size: u8,
expected_extension_cbor: &[u8],
) {
match make_credential_response.unwrap() {
ResponseData::AuthenticatorMakeCredential(make_credential_response) => {
let AuthenticatorMakeCredentialResponse {
fmt,
auth_data,
att_stmt,
} = make_credential_response;
// The expected response is split to only assert the non-random parts.
assert_eq!(fmt, "packed");
let mut expected_auth_data = vec![
0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80,
0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2,
0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, flags, 0x00, 0x00, 0x00,
];
expected_auth_data.push(INITIAL_SIGNATURE_COUNTER as u8);
expected_auth_data.extend(expected_aaguid);
expected_auth_data.extend(&[0x00, expected_credential_id_size]);
assert_eq!(
auth_data[0..expected_auth_data.len()],
expected_auth_data[..]
);
/*assert_eq!(
&auth_data[expected_auth_data.len()
..expected_auth_data.len() + expected_attested_cred_data.len()],
expected_attested_cred_data
);*/
assert_eq!(
&auth_data[auth_data.len() - expected_extension_cbor.len()..auth_data.len()],
expected_extension_cbor
);
assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64);
}
_ => panic!("Invalid response type"),
}
}
#[test] #[test]
fn test_get_info() { fn test_get_info() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};
@@ -1027,19 +1101,22 @@ mod test {
expected_response.extend( expected_response.extend(
[ [
0x68, 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30, 0x6C, 0x46, 0x49, 0x44, 0x4F, 0x68, 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30, 0x6C, 0x46, 0x49, 0x44, 0x4F,
0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52, 0x45, 0x02, 0x81, 0x6B, 0x68, 0x6D, 0x61, 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52, 0x45, 0x02, 0x83, 0x6B, 0x68, 0x6D, 0x61,
0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x03, 0x50, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x6B, 0x63, 0x72, 0x65, 0x64, 0x50,
0x72, 0x6F, 0x74, 0x65, 0x63, 0x74, 0x6C, 0x6D, 0x69, 0x6E, 0x50, 0x69, 0x6E, 0x4C,
0x65, 0x6E, 0x67, 0x74, 0x68, 0x03, 0x50,
] ]
.iter(), .iter(),
); );
expected_response.extend(&ctap_state.persistent_store.aaguid().unwrap()); expected_response.extend(&ctap_state.persistent_store.aaguid().unwrap());
expected_response.extend( expected_response.extend(
[ [
0x04, 0xA3, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x69, 0x63, 0x6C, 0x69, 0x04, 0xA4, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x69, 0x63, 0x6C, 0x69,
0x65, 0x6E, 0x74, 0x50, 0x69, 0x6E, 0xF4, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, 0x01, 0x65, 0x6E, 0x74, 0x50, 0x69, 0x6E, 0xF4, 0x6F, 0x73, 0x65, 0x74, 0x4D, 0x69, 0x6E,
0x08, 0x18, 0x70, 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, 0x50, 0x49, 0x4E, 0x4C, 0x65, 0x6E, 0x67, 0x74, 0x68, 0xF5, 0x05, 0x19, 0x04, 0x00,
0x6C, 0x67, 0x26, 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x06, 0x81, 0x01, 0x08, 0x18, 0x70, 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81,
0x63, 0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04, 0xA2, 0x63, 0x61, 0x6C, 0x67, 0x26, 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75,
0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04,
] ]
.iter(), .iter(),
); );
@@ -1098,6 +1175,7 @@ mod test {
let extensions = Some(MakeCredentialExtensions { let extensions = Some(MakeCredentialExtensions {
hmac_secret: false, hmac_secret: false,
cred_protect: Some(policy), cred_protect: Some(policy),
min_pin_length: false,
}); });
let mut make_credential_params = create_minimal_make_credential_parameters(); let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.extensions = extensions; make_credential_params.extensions = extensions;
@@ -1114,31 +1192,13 @@ mod test {
let make_credential_response = let make_credential_response =
ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID);
match make_credential_response.unwrap() { check_make_response(
ResponseData::AuthenticatorMakeCredential(make_credential_response) => { make_credential_response,
let AuthenticatorMakeCredentialResponse { 0x41,
fmt, &ctap_state.persistent_store.aaguid().unwrap(),
auth_data, 0x20,
att_stmt, &[],
} = make_credential_response;
// The expected response is split to only assert the non-random parts.
assert_eq!(fmt, "packed");
let mut expected_auth_data = vec![
0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80,
0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2,
0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x41, 0x00, 0x00, 0x00,
];
expected_auth_data.push(INITIAL_SIGNATURE_COUNTER as u8);
expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap());
expected_auth_data.extend(&[0x00, 0x20]);
assert_eq!(
auth_data[0..expected_auth_data.len()],
expected_auth_data[..]
); );
assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64);
}
_ => panic!("Invalid response type"),
}
} }
#[test] #[test]
@@ -1152,31 +1212,13 @@ mod test {
let make_credential_response = let make_credential_response =
ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID);
match make_credential_response.unwrap() { check_make_response(
ResponseData::AuthenticatorMakeCredential(make_credential_response) => { make_credential_response,
let AuthenticatorMakeCredentialResponse { 0x41,
fmt, &ctap_state.persistent_store.aaguid().unwrap(),
auth_data, CREDENTIAL_ID_SIZE as u8,
att_stmt, &[],
} = make_credential_response;
// The expected response is split to only assert the non-random parts.
assert_eq!(fmt, "packed");
let mut expected_auth_data = vec![
0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80,
0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2,
0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x41, 0x00, 0x00, 0x00,
];
expected_auth_data.push(INITIAL_SIGNATURE_COUNTER as u8);
expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap());
expected_auth_data.extend(&[0x00, CREDENTIAL_ID_SIZE as u8]);
assert_eq!(
auth_data[0..expected_auth_data.len()],
expected_auth_data[..]
); );
assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64);
}
_ => panic!("Invalid response type"),
}
} }
#[test] #[test]
@@ -1294,6 +1336,7 @@ mod test {
let extensions = Some(MakeCredentialExtensions { let extensions = Some(MakeCredentialExtensions {
hmac_secret: true, hmac_secret: true,
cred_protect: None, cred_protect: None,
min_pin_length: false,
}); });
let mut make_credential_params = create_minimal_make_credential_parameters(); let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.options.rk = false; make_credential_params.options.rk = false;
@@ -1301,39 +1344,16 @@ mod test {
let make_credential_response = let make_credential_response =
ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID);
match make_credential_response.unwrap() { let expected_extension_cbor = [
ResponseData::AuthenticatorMakeCredential(make_credential_response) => { 0xA1, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0xF5,
let AuthenticatorMakeCredentialResponse {
fmt,
auth_data,
att_stmt,
} = make_credential_response;
// The expected response is split to only assert the non-random parts.
assert_eq!(fmt, "packed");
let mut expected_auth_data = vec![
0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80,
0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2,
0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0xC1, 0x00, 0x00, 0x00,
]; ];
expected_auth_data.push(INITIAL_SIGNATURE_COUNTER as u8); check_make_response(
expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); make_credential_response,
expected_auth_data.extend(&[0x00, CREDENTIAL_ID_SIZE as u8]); 0xC1,
assert_eq!( &ctap_state.persistent_store.aaguid().unwrap(),
auth_data[0..expected_auth_data.len()], CREDENTIAL_ID_SIZE as u8,
expected_auth_data[..] &expected_extension_cbor,
); );
let expected_extension_cbor = vec![
0xA1, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74,
0xF5,
];
assert_eq!(
auth_data[auth_data.len() - expected_extension_cbor.len()..auth_data.len()],
expected_extension_cbor[..]
);
assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64);
}
_ => panic!("Invalid response type"),
}
} }
#[test] #[test]
@@ -1345,45 +1365,80 @@ mod test {
let extensions = Some(MakeCredentialExtensions { let extensions = Some(MakeCredentialExtensions {
hmac_secret: true, hmac_secret: true,
cred_protect: None, cred_protect: None,
min_pin_length: false,
}); });
let mut make_credential_params = create_minimal_make_credential_parameters(); let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.extensions = extensions; make_credential_params.extensions = extensions;
let make_credential_response = let make_credential_response =
ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID);
match make_credential_response.unwrap() { let expected_extension_cbor = [
ResponseData::AuthenticatorMakeCredential(make_credential_response) => { 0xA1, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0xF5,
let AuthenticatorMakeCredentialResponse {
fmt,
auth_data,
att_stmt,
} = make_credential_response;
// The expected response is split to only assert the non-random parts.
assert_eq!(fmt, "packed");
let mut expected_auth_data = vec![
0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80,
0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2,
0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0xC1, 0x00, 0x00, 0x00,
]; ];
expected_auth_data.push(INITIAL_SIGNATURE_COUNTER as u8); check_make_response(
expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); make_credential_response,
expected_auth_data.extend(&[0x00, 0x20]); 0xC1,
assert_eq!( &ctap_state.persistent_store.aaguid().unwrap(),
auth_data[0..expected_auth_data.len()], 0x20,
expected_auth_data[..] &expected_extension_cbor,
); );
let expected_extension_cbor = vec![ }
0xA1, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74,
0xF5, #[test]
fn test_process_make_credential_min_pin_length() {
let mut rng = ThreadRng256 {};
let user_immediately_present = |_| Ok(());
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE);
// First part: The extension is ignored, since the RP ID is not on the list.
let extensions = Some(MakeCredentialExtensions {
hmac_secret: false,
cred_protect: None,
min_pin_length: true,
});
let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.extensions = extensions;
let make_credential_response =
ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID);
let mut expected_attested_cred_data =
ctap_state.persistent_store.aaguid().unwrap().to_vec();
expected_attested_cred_data.extend(&[0x00, 0x20]);
check_make_response(
make_credential_response,
0x41,
&ctap_state.persistent_store.aaguid().unwrap(),
0x20,
&[],
);
// Second part: The extension is used.
assert_eq!(
ctap_state
.persistent_store
.set_min_pin_length_rp_ids(vec!["example.com".to_string()]),
Ok(())
);
let extensions = Some(MakeCredentialExtensions {
hmac_secret: false,
cred_protect: None,
min_pin_length: true,
});
let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.extensions = extensions;
let make_credential_response =
ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID);
let expected_extension_cbor = [
0xA1, 0x6C, 0x6D, 0x69, 0x6E, 0x50, 0x69, 0x6E, 0x4C, 0x65, 0x6E, 0x67, 0x74, 0x68,
0x04,
]; ];
assert_eq!( check_make_response(
auth_data[auth_data.len() - expected_extension_cbor.len()..auth_data.len()], make_credential_response,
expected_extension_cbor[..] 0xC1,
&ctap_state.persistent_store.aaguid().unwrap(),
0x20,
&expected_extension_cbor,
); );
assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64);
}
_ => panic!("Invalid response type"),
}
} }
#[test] #[test]
@@ -1502,6 +1557,7 @@ mod test {
let make_extensions = Some(MakeCredentialExtensions { let make_extensions = Some(MakeCredentialExtensions {
hmac_secret: true, hmac_secret: true,
cred_protect: None, cred_protect: None,
min_pin_length: false,
}); });
let mut make_credential_params = create_minimal_make_credential_parameters(); let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.options.rk = false; make_credential_params.options.rk = false;
@@ -1569,6 +1625,7 @@ mod test {
let make_extensions = Some(MakeCredentialExtensions { let make_extensions = Some(MakeCredentialExtensions {
hmac_secret: true, hmac_secret: true,
cred_protect: None, cred_protect: None,
min_pin_length: false,
}); });
let mut make_credential_params = create_minimal_make_credential_parameters(); let mut make_credential_params = create_minimal_make_credential_parameters();
make_credential_params.extensions = make_extensions; make_credential_params.extensions = make_extensions;
@@ -1761,10 +1818,8 @@ mod test {
.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID)
.is_ok()); .is_ok());
ctap_state // The PIN length is outside of the test scope and most likely incorrect.
.persistent_store ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap();
.set_pin_hash(&[0u8; 16])
.unwrap();
let pin_uv_auth_param = Some(vec![ let pin_uv_auth_param = Some(vec![
0x6F, 0x52, 0x83, 0xBF, 0x1A, 0x91, 0xEE, 0x67, 0xE9, 0xD4, 0x4C, 0x80, 0x08, 0x79, 0x6F, 0x52, 0x83, 0xBF, 0x1A, 0x91, 0xEE, 0x67, 0xE9, 0xD4, 0x4C, 0x80, 0x08, 0x79,
0x90, 0x8D, 0x90, 0x8D,

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;
use alloc::str;
use alloc::string::String; use alloc::string::String;
use alloc::vec; use alloc::vec;
use alloc::vec::Vec; use alloc::vec::Vec;
@@ -141,13 +142,14 @@ fn check_and_store_new_pin(
.ok_or(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)?; .ok_or(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)?;
let min_pin_length = persistent_store.min_pin_length()? as usize; let min_pin_length = persistent_store.min_pin_length()? as usize;
if pin.len() < min_pin_length || pin.len() == PIN_PADDED_LENGTH { let pin_length = str::from_utf8(&pin).unwrap_or("").chars().count();
// TODO(kaczmarczyck) check 4 code point minimum instead if pin_length < min_pin_length || pin.len() == PIN_PADDED_LENGTH {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION); return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION);
} }
let mut pin_hash = [0u8; 16]; let mut pin_hash = [0u8; 16];
pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]); pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]);
persistent_store.set_pin_hash(&pin_hash)?; // The PIN length is always < 64.
persistent_store.set_pin(&pin_hash, pin_length as u8)?;
Ok(()) Ok(())
} }
@@ -170,6 +172,7 @@ pub struct PinProtocolV1 {
consecutive_pin_mismatches: u8, consecutive_pin_mismatches: u8,
permissions: u8, permissions: u8,
permissions_rp_id: Option<String>, permissions_rp_id: Option<String>,
pub force_pin_change: bool,
} }
impl PinProtocolV1 { impl PinProtocolV1 {
@@ -182,6 +185,7 @@ impl PinProtocolV1 {
consecutive_pin_mismatches: 0, consecutive_pin_mismatches: 0,
permissions: 0, permissions: 0,
permissions_rp_id: None, permissions_rp_id: None,
force_pin_change: false,
} }
} }
@@ -363,54 +367,6 @@ impl PinProtocolV1 {
Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND) Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND)
} }
fn process_set_min_pin_length(
&mut self,
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_VENDOR_INTERNAL_ERROR);
}
if persistent_store.pin_hash()?.is_some() {
match pin_auth {
Some(pin_auth) => {
if self.consecutive_pin_mismatches >= 3 {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED);
}
// TODO(kaczmarczyck) Values are taken from the (not yet public) new revision
// of CTAP 2.1. The code should link the specification when published.
// From CTAP2.1: "If request contains pinUvAuthParam, the Authenticator calls
// verify(pinUvAuthToken, 32×0xff || 0x0608 || uint32LittleEndian(minPINLength)
// || minPinLengthRPIDs, pinUvAuthParam)"
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
// https://github.com/google/OpenSK/issues/129
// if !cbor::write(cbor_array_vec!(min_pin_length_rp_ids), &mut message) {
// return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
// }
if !verify_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)?;
// TODO(kaczmarczyck) commented code is useful for the extension
// https://github.com/google/OpenSK/issues/129
// 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(())
}
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,
rng: &mut impl Rng256, rng: &mut impl Rng256,
@@ -450,8 +406,6 @@ impl PinProtocolV1 {
pin_auth, pin_auth,
new_pin_enc, new_pin_enc,
pin_hash_enc, pin_hash_enc,
min_pin_length,
min_pin_length_rp_ids,
permissions, permissions,
permissions_rp_id, permissions_rp_id,
} = client_pin_params; } = client_pin_params;
@@ -499,15 +453,6 @@ impl PinProtocolV1 {
)?, )?,
), ),
ClientPinSubCommand::GetUvRetries => Some(self.process_get_uv_retries()?), ClientPinSubCommand::GetUvRetries => Some(self.process_get_uv_retries()?),
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,
pin_auth,
)?;
None
}
ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => Some( 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, rng,
@@ -577,7 +522,7 @@ impl PinProtocolV1 {
#[cfg(test)] #[cfg(test)]
pub fn new_test( pub fn new_test(
key_agreement_key: crypto::ecdh::SecKey, key_agreement_key: crypto::ecdh::SecKey,
pin_uv_auth_token: [u8; 32], pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH],
) -> PinProtocolV1 { ) -> PinProtocolV1 {
PinProtocolV1 { PinProtocolV1 {
key_agreement_key, key_agreement_key,
@@ -585,6 +530,7 @@ impl PinProtocolV1 {
consecutive_pin_mismatches: 0, consecutive_pin_mismatches: 0,
permissions: 0xFF, permissions: 0xFF,
permissions_rp_id: None, permissions_rp_id: None,
force_pin_change: false,
} }
} }
} }
@@ -600,7 +546,7 @@ mod test {
pin[..4].copy_from_slice(b"1234"); pin[..4].copy_from_slice(b"1234");
let mut pin_hash = [0u8; 16]; let mut pin_hash = [0u8; 16];
pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]); pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]);
persistent_store.set_pin_hash(&pin_hash).unwrap(); persistent_store.set_pin(&pin_hash, 4).unwrap();
} }
// Encrypts the message with a zero IV and key derived from shared_secret. // Encrypts the message with a zero IV and key derived from shared_secret.
@@ -662,7 +608,7 @@ mod test {
0x01, 0xD9, 0x88, 0x40, 0x50, 0xBB, 0xD0, 0x7A, 0x23, 0x1A, 0xEB, 0x69, 0xD8, 0x36, 0x01, 0xD9, 0x88, 0x40, 0x50, 0xBB, 0xD0, 0x7A, 0x23, 0x1A, 0xEB, 0x69, 0xD8, 0x36,
0xC4, 0x12, 0xC4, 0x12,
]; ];
persistent_store.set_pin_hash(&pin_hash).unwrap(); persistent_store.set_pin(&pin_hash, 4).unwrap();
let shared_secret = [0x88; 32]; let shared_secret = [0x88; 32];
let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret);
let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key);
@@ -935,40 +881,6 @@ mod test {
); );
} }
#[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
// https://github.com/google/OpenSK/issues/129
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().unwrap(), 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().unwrap(), min_pin_length);
}
#[test] #[test]
fn test_process() { fn test_process() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};
@@ -981,8 +893,6 @@ mod test {
pin_auth: None, pin_auth: None,
new_pin_enc: None, new_pin_enc: None,
pin_hash_enc: None, pin_hash_enc: None,
min_pin_length: None,
min_pin_length_rp_ids: None,
permissions: None, permissions: None,
permissions_rp_id: None, permissions_rp_id: None,
}; };
@@ -997,8 +907,6 @@ mod test {
pin_auth: None, pin_auth: None,
new_pin_enc: None, new_pin_enc: None,
pin_hash_enc: None, pin_hash_enc: None,
min_pin_length: None,
min_pin_length_rp_ids: None,
permissions: None, permissions: None,
permissions_rp_id: None, permissions_rp_id: None,
}; };

View File

@@ -31,6 +31,8 @@ pub enum ResponseData {
AuthenticatorClientPin(Option<AuthenticatorClientPinResponse>), AuthenticatorClientPin(Option<AuthenticatorClientPinResponse>),
AuthenticatorReset, AuthenticatorReset,
AuthenticatorSelection, AuthenticatorSelection,
// TODO(kaczmarczyck) dummy, extend
AuthenticatorConfig,
AuthenticatorVendor(AuthenticatorVendorResponse), AuthenticatorVendor(AuthenticatorVendorResponse),
} }
@@ -45,6 +47,7 @@ impl From<ResponseData> for Option<cbor::Value> {
ResponseData::AuthenticatorClientPin(None) => None, ResponseData::AuthenticatorClientPin(None) => None,
ResponseData::AuthenticatorReset => None, ResponseData::AuthenticatorReset => None,
ResponseData::AuthenticatorSelection => None, ResponseData::AuthenticatorSelection => None,
ResponseData::AuthenticatorConfig => None,
ResponseData::AuthenticatorVendor(data) => Some(data.into()), ResponseData::AuthenticatorVendor(data) => Some(data.into()),
} }
} }
@@ -368,6 +371,12 @@ mod test {
assert_eq!(response_cbor, None); assert_eq!(response_cbor, None);
} }
#[test]
fn test_config_into_cbor() {
let response_cbor: Option<cbor::Value> = ResponseData::AuthenticatorConfig.into();
assert_eq!(response_cbor, None);
}
#[test] #[test]
fn test_vendor_response_into_cbor() { fn test_vendor_response_into_cbor() {
let response_cbor: Option<cbor::Value> = let response_cbor: Option<cbor::Value> =

View File

@@ -53,11 +53,10 @@ const MAX_SUPPORTED_RESIDENTIAL_KEYS: usize = 150;
const MAX_PIN_RETRIES: u8 = 8; const MAX_PIN_RETRIES: u8 = 8;
const DEFAULT_MIN_PIN_LENGTH: u8 = 4; const DEFAULT_MIN_PIN_LENGTH: u8 = 4;
// TODO(kaczmarczyck) use this for the minPinLength extension const DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec<String> = Vec::new();
// https://github.com/google/OpenSK/issues/129 // This constant is an attempt to limit storage requirements. If you don't set it to 0,
const _DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec<String> = Vec::new(); // the stored strings can still be unbounded, but that is true for all RP IDs.
// TODO(kaczmarczyck) Check whether this constant is necessary, or replace it accordingly. const MAX_RP_IDS_LENGTH: usize = 8;
const _MAX_RP_IDS_LENGTH: usize = 8;
/// Wrapper for master keys. /// Wrapper for master keys.
pub struct MasterKeys { pub struct MasterKeys {
@@ -68,6 +67,15 @@ pub struct MasterKeys {
pub hmac: [u8; 32], pub hmac: [u8; 32],
} }
/// Wrapper for PIN properties.
struct PinProperties {
/// 16 byte prefix of SHA256 of the currently set PIN.
hash: [u8; PIN_AUTH_LENGTH],
/// Length of the current PIN in code points.
code_point_length: u8,
}
/// CTAP persistent storage. /// CTAP persistent storage.
pub struct PersistentStore { pub struct PersistentStore {
store: persistent_store::Store<Storage>, store: persistent_store::Store<Storage>,
@@ -296,26 +304,44 @@ impl PersistentStore {
Ok(*array_ref![cred_random_secret, offset, 32]) Ok(*array_ref![cred_random_secret, offset, 32])
} }
/// Returns the PIN hash if defined. /// Reads the PIN properties and wraps them into PinProperties.
pub fn pin_hash(&self) -> Result<Option<[u8; PIN_AUTH_LENGTH]>, Ctap2StatusCode> { fn pin_properties(&self) -> Result<Option<PinProperties>, Ctap2StatusCode> {
let pin_hash = match self.store.find(key::PIN_HASH)? { let pin_properties = match self.store.find(key::PIN_PROPERTIES)? {
None => return Ok(None), None => return Ok(None),
Some(pin_hash) => pin_hash, Some(pin_properties) => pin_properties,
}; };
if pin_hash.len() != PIN_AUTH_LENGTH { const PROPERTIES_LENGTH: usize = PIN_AUTH_LENGTH + 1;
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); match pin_properties.len() {
PROPERTIES_LENGTH => Ok(Some(PinProperties {
hash: *array_ref![pin_properties, 1, PIN_AUTH_LENGTH],
code_point_length: pin_properties[0],
})),
_ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR),
} }
Ok(Some(*array_ref![pin_hash, 0, PIN_AUTH_LENGTH]))
} }
/// Sets the PIN hash. /// Returns the PIN hash if defined.
pub fn pin_hash(&self) -> Result<Option<[u8; PIN_AUTH_LENGTH]>, Ctap2StatusCode> {
Ok(self.pin_properties()?.map(|p| p.hash))
}
/// Returns the length of the currently set PIN if defined.
pub fn pin_code_point_length(&self) -> Result<Option<u8>, Ctap2StatusCode> {
Ok(self.pin_properties()?.map(|p| p.code_point_length))
}
/// Sets the PIN hash and length.
/// ///
/// If it was already defined, it is updated. /// If it was already defined, it is updated.
pub fn set_pin_hash( pub fn set_pin(
&mut self, &mut self,
pin_hash: &[u8; PIN_AUTH_LENGTH], pin_hash: &[u8; PIN_AUTH_LENGTH],
pin_code_point_length: u8,
) -> Result<(), Ctap2StatusCode> { ) -> Result<(), Ctap2StatusCode> {
Ok(self.store.insert(key::PIN_HASH, pin_hash)?) let mut pin_properties = [0; 1 + PIN_AUTH_LENGTH];
pin_properties[0] = pin_code_point_length;
pin_properties[1..].clone_from_slice(pin_hash);
Ok(self.store.insert(key::PIN_PROPERTIES, &pin_properties)?)
} }
/// Returns the number of remaining PIN retries. /// Returns the number of remaining PIN retries.
@@ -358,34 +384,34 @@ impl PersistentStore {
/// Returns the list of RP IDs that are used to check if reading the minimum PIN length is /// Returns the list of RP IDs that are used to check if reading the minimum PIN length is
/// allowed. /// allowed.
pub fn _min_pin_length_rp_ids(&self) -> Result<Vec<String>, Ctap2StatusCode> { pub fn min_pin_length_rp_ids(&self) -> Result<Vec<String>, Ctap2StatusCode> {
let rp_ids = self let rp_ids = self
.store .store
.find(key::_MIN_PIN_LENGTH_RP_IDS)? .find(key::MIN_PIN_LENGTH_RP_IDS)?
.map_or(Some(_DEFAULT_MIN_PIN_LENGTH_RP_IDS), |value| { .map_or(Some(DEFAULT_MIN_PIN_LENGTH_RP_IDS), |value| {
_deserialize_min_pin_length_rp_ids(&value) deserialize_min_pin_length_rp_ids(&value)
}); });
debug_assert!(rp_ids.is_some()); debug_assert!(rp_ids.is_some());
Ok(rp_ids.unwrap_or_default()) Ok(rp_ids.unwrap_or_default())
} }
/// Sets the list of RP IDs that are used to check if reading the minimum PIN length is allowed. /// Sets the list of RP IDs that are used to check if reading the minimum PIN length is allowed.
pub fn _set_min_pin_length_rp_ids( pub fn set_min_pin_length_rp_ids(
&mut self, &mut self,
min_pin_length_rp_ids: Vec<String>, min_pin_length_rp_ids: Vec<String>,
) -> Result<(), Ctap2StatusCode> { ) -> Result<(), Ctap2StatusCode> {
let mut min_pin_length_rp_ids = min_pin_length_rp_ids; let mut min_pin_length_rp_ids = min_pin_length_rp_ids;
for rp_id in _DEFAULT_MIN_PIN_LENGTH_RP_IDS { for rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS {
if !min_pin_length_rp_ids.contains(&rp_id) { if !min_pin_length_rp_ids.contains(&rp_id) {
min_pin_length_rp_ids.push(rp_id); min_pin_length_rp_ids.push(rp_id);
} }
} }
if min_pin_length_rp_ids.len() > _MAX_RP_IDS_LENGTH { if min_pin_length_rp_ids.len() > MAX_RP_IDS_LENGTH {
return Err(Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL); return Err(Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL);
} }
Ok(self.store.insert( Ok(self.store.insert(
key::_MIN_PIN_LENGTH_RP_IDS, key::MIN_PIN_LENGTH_RP_IDS,
&_serialize_min_pin_length_rp_ids(min_pin_length_rp_ids)?, &serialize_min_pin_length_rp_ids(min_pin_length_rp_ids)?,
)?) )?)
} }
@@ -573,7 +599,7 @@ fn serialize_credential(credential: PublicKeyCredentialSource) -> Result<Vec<u8>
} }
/// Deserializes a list of RP IDs from storage representation. /// Deserializes a list of RP IDs from storage representation.
fn _deserialize_min_pin_length_rp_ids(data: &[u8]) -> Option<Vec<String>> { fn deserialize_min_pin_length_rp_ids(data: &[u8]) -> Option<Vec<String>> {
let cbor = cbor::read(data).ok()?; let cbor = cbor::read(data).ok()?;
extract_array(cbor) extract_array(cbor)
.ok()? .ok()?
@@ -584,7 +610,7 @@ fn _deserialize_min_pin_length_rp_ids(data: &[u8]) -> Option<Vec<String>> {
} }
/// Serializes a list of RP IDs to storage representation. /// Serializes a list of RP IDs to storage representation.
fn _serialize_min_pin_length_rp_ids(rp_ids: Vec<String>) -> Result<Vec<u8>, Ctap2StatusCode> { fn serialize_min_pin_length_rp_ids(rp_ids: Vec<String>) -> Result<Vec<u8>, Ctap2StatusCode> {
let mut data = Vec::new(); let mut data = Vec::new();
if cbor::write(cbor_array_vec!(rp_ids), &mut data) { if cbor::write(cbor_array_vec!(rp_ids), &mut data) {
Ok(data) Ok(data)
@@ -891,28 +917,38 @@ mod test {
} }
#[test] #[test]
fn test_pin_hash() { fn test_pin_hash_and_length() {
let mut rng = ThreadRng256 {}; let mut rng = ThreadRng256 {};
let mut persistent_store = PersistentStore::new(&mut rng); let mut persistent_store = PersistentStore::new(&mut rng);
// Pin hash is initially not set. // Pin hash is initially not set.
assert!(persistent_store.pin_hash().unwrap().is_none()); assert!(persistent_store.pin_hash().unwrap().is_none());
assert!(persistent_store.pin_code_point_length().unwrap().is_none());
// Setting the pin hash sets the pin hash. // Setting the pin sets the pin hash.
let random_data = rng.gen_uniform_u8x32(); let random_data = rng.gen_uniform_u8x32();
assert_eq!(random_data.len(), 2 * PIN_AUTH_LENGTH); assert_eq!(random_data.len(), 2 * PIN_AUTH_LENGTH);
let pin_hash_1 = *array_ref!(random_data, 0, PIN_AUTH_LENGTH); let pin_hash_1 = *array_ref!(random_data, 0, PIN_AUTH_LENGTH);
let pin_hash_2 = *array_ref!(random_data, PIN_AUTH_LENGTH, PIN_AUTH_LENGTH); let pin_hash_2 = *array_ref!(random_data, PIN_AUTH_LENGTH, PIN_AUTH_LENGTH);
persistent_store.set_pin_hash(&pin_hash_1).unwrap(); let pin_length_1 = 4;
let pin_length_2 = 63;
persistent_store.set_pin(&pin_hash_1, pin_length_1).unwrap();
assert_eq!(persistent_store.pin_hash().unwrap(), Some(pin_hash_1)); assert_eq!(persistent_store.pin_hash().unwrap(), Some(pin_hash_1));
assert_eq!(persistent_store.pin_hash().unwrap(), Some(pin_hash_1)); assert_eq!(
persistent_store.set_pin_hash(&pin_hash_2).unwrap(); persistent_store.pin_code_point_length().unwrap(),
assert_eq!(persistent_store.pin_hash().unwrap(), Some(pin_hash_2)); Some(pin_length_1)
);
persistent_store.set_pin(&pin_hash_2, pin_length_2).unwrap();
assert_eq!(persistent_store.pin_hash().unwrap(), Some(pin_hash_2)); assert_eq!(persistent_store.pin_hash().unwrap(), Some(pin_hash_2));
assert_eq!(
persistent_store.pin_code_point_length().unwrap(),
Some(pin_length_2)
);
// Resetting the storage resets the pin hash. // Resetting the storage resets the pin hash.
persistent_store.reset(&mut rng).unwrap(); persistent_store.reset(&mut rng).unwrap();
assert!(persistent_store.pin_hash().unwrap().is_none()); assert!(persistent_store.pin_hash().unwrap().is_none());
assert!(persistent_store.pin_code_point_length().unwrap().is_none());
} }
#[test] #[test]
@@ -1006,22 +1042,22 @@ mod test {
// The minimum PIN length RP IDs are initially at the default. // The minimum PIN length RP IDs are initially at the default.
assert_eq!( assert_eq!(
persistent_store._min_pin_length_rp_ids().unwrap(), persistent_store.min_pin_length_rp_ids().unwrap(),
_DEFAULT_MIN_PIN_LENGTH_RP_IDS DEFAULT_MIN_PIN_LENGTH_RP_IDS
); );
// Changes by the setter are reflected by the getter. // Changes by the setter are reflected by the getter.
let mut rp_ids = vec![String::from("example.com")]; let mut rp_ids = vec![String::from("example.com")];
assert_eq!( assert_eq!(
persistent_store._set_min_pin_length_rp_ids(rp_ids.clone()), persistent_store.set_min_pin_length_rp_ids(rp_ids.clone()),
Ok(()) Ok(())
); );
for rp_id in _DEFAULT_MIN_PIN_LENGTH_RP_IDS { for rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS {
if !rp_ids.contains(&rp_id) { if !rp_ids.contains(&rp_id) {
rp_ids.push(rp_id); rp_ids.push(rp_id);
} }
} }
assert_eq!(persistent_store._min_pin_length_rp_ids().unwrap(), rp_ids); assert_eq!(persistent_store.min_pin_length_rp_ids().unwrap(), rp_ids);
} }
#[test] #[test]
@@ -1070,8 +1106,8 @@ mod test {
#[test] #[test]
fn test_serialize_deserialize_min_pin_length_rp_ids() { fn test_serialize_deserialize_min_pin_length_rp_ids() {
let rp_ids = vec![String::from("example.com")]; let rp_ids = vec![String::from("example.com")];
let serialized = _serialize_min_pin_length_rp_ids(rp_ids.clone()).unwrap(); let serialized = serialize_min_pin_length_rp_ids(rp_ids.clone()).unwrap();
let reconstructed = _deserialize_min_pin_length_rp_ids(&serialized).unwrap(); let reconstructed = deserialize_min_pin_length_rp_ids(&serialized).unwrap();
assert_eq!(rp_ids, reconstructed); assert_eq!(rp_ids, reconstructed);
} }
} }

View File

@@ -92,7 +92,7 @@ make_partition! {
CRED_RANDOM_SECRET = 2041; CRED_RANDOM_SECRET = 2041;
/// List of RP IDs allowed to read the minimum PIN length. /// List of RP IDs allowed to read the minimum PIN length.
_MIN_PIN_LENGTH_RP_IDS = 2042; MIN_PIN_LENGTH_RP_IDS = 2042;
/// The minimum PIN length. /// The minimum PIN length.
/// ///
@@ -104,10 +104,11 @@ make_partition! {
/// If the entry is absent, the number of PIN retries is `MAX_PIN_RETRIES`. /// If the entry is absent, the number of PIN retries is `MAX_PIN_RETRIES`.
PIN_RETRIES = 2044; PIN_RETRIES = 2044;
/// The PIN hash. /// The PIN hash and length.
/// ///
/// If the entry is absent, there is no PIN set. /// If the entry is absent, there is no PIN set. The first byte represents
PIN_HASH = 2045; /// the length, the following are an array with the hash.
PIN_PROPERTIES = 2045;
/// The encryption and hmac keys. /// The encryption and hmac keys.
/// ///