Files
OpenSK/src/ctap/client_pin.rs
kaczmarczyck 4782d7e186 Separate RNG library (#470)
* seperates the RNG library

* fixes crypto tests

* adds rng256 workflow

* fixes formatting
2022-04-28 11:36:43 +02:00

1692 lines
62 KiB
Rust

// Copyright 2020-2021 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::super::clock::CtapInstant;
use super::command::AuthenticatorClientPinParameters;
use super::data_formats::{
ok_or_missing, ClientPinSubCommand, CoseKey, GetAssertionHmacSecretInput, PinUvAuthProtocol,
};
use super::pin_protocol::{verify_pin_uv_auth_token, PinProtocol, SharedSecret};
use super::response::{AuthenticatorClientPinResponse, ResponseData};
use super::status_code::Ctap2StatusCode;
use super::token_state::PinUvAuthTokenState;
use crate::ctap::storage;
use crate::env::Env;
use alloc::boxed::Box;
use alloc::str;
use alloc::string::String;
use alloc::vec::Vec;
use crypto::hmac::hmac_256;
use crypto::sha256::Sha256;
use crypto::Hash256;
#[cfg(test)]
use enum_iterator::IntoEnumIterator;
use rng256::Rng256;
use subtle::ConstantTimeEq;
/// The prefix length of the PIN hash that is stored and compared.
///
/// The code assumes that this value is a multiple of the AES block length, fits
/// an u8 and is at most as long as a SHA256. The value is fixed for all PIN
/// protocols.
pub const PIN_AUTH_LENGTH: usize = 16;
/// The length of the pinUvAuthToken used throughout PIN protocols.
///
/// The code assumes that this value is a multiple of the AES block length. It
/// is fixed since CTAP2.1, and the specification suggests that it coincides
/// with the HMAC key length. Therefore a change would require a more general
/// HMAC implementation.
pub const PIN_TOKEN_LENGTH: usize = 32;
/// The length of the encrypted PINs when received by SetPin or ChangePin.
///
/// The code assumes that this value is a multiple of the AES block length. It
/// is fixed since CTAP2.1.
const PIN_PADDED_LENGTH: usize = 64;
/// Decrypts the new_pin_enc and outputs the found PIN.
fn decrypt_pin(
shared_secret: &dyn SharedSecret,
new_pin_enc: Vec<u8>,
) -> Result<Vec<u8>, Ctap2StatusCode> {
let decrypted_pin = shared_secret.decrypt(&new_pin_enc)?;
if decrypted_pin.len() != PIN_PADDED_LENGTH {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
// In CTAP 2.1, the specification changed. The new wording might lead to
// different behavior when there are non-zero bytes after zero bytes.
// This implementation consistently ignores those degenerate cases.
Ok(decrypted_pin.into_iter().take_while(|&c| c != 0).collect())
}
/// Stores a hash prefix of the new PIN in the persistent storage, if correct.
///
/// The new PIN is passed encrypted, so it is first decrypted and stripped from
/// padding. Next, it is checked against the PIN policy. Last, it is hashed and
/// truncated for persistent storage.
fn check_and_store_new_pin(
env: &mut impl Env,
shared_secret: &dyn SharedSecret,
new_pin_enc: Vec<u8>,
) -> Result<(), Ctap2StatusCode> {
let pin = decrypt_pin(shared_secret, new_pin_enc)?;
let min_pin_length = storage::min_pin_length(env)? as usize;
let pin_length = str::from_utf8(&pin).unwrap_or("").chars().count();
if pin_length < min_pin_length || pin.len() == PIN_PADDED_LENGTH {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION);
}
let mut pin_hash = [0u8; PIN_AUTH_LENGTH];
pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..PIN_AUTH_LENGTH]);
// The PIN length is always < PIN_PADDED_LENGTH < 256.
storage::set_pin(env, &pin_hash, pin_length as u8)?;
Ok(())
}
#[cfg_attr(test, derive(IntoEnumIterator))]
pub enum PinPermission {
// All variants should use integers with a single bit set.
MakeCredential = 0x01,
GetAssertion = 0x02,
CredentialManagement = 0x04,
_BioEnrollment = 0x08,
LargeBlobWrite = 0x10,
AuthenticatorConfiguration = 0x20,
}
pub struct ClientPin {
pin_protocol_v1: PinProtocol,
pin_protocol_v2: PinProtocol,
consecutive_pin_mismatches: u8,
pin_uv_auth_token_state: PinUvAuthTokenState,
}
impl ClientPin {
pub fn new(rng: &mut impl Rng256) -> ClientPin {
ClientPin {
pin_protocol_v1: PinProtocol::new(rng),
pin_protocol_v2: PinProtocol::new(rng),
consecutive_pin_mismatches: 0,
pin_uv_auth_token_state: PinUvAuthTokenState::new(),
}
}
/// Gets a reference to the PIN protocol of the given version.
fn get_pin_protocol(&self, pin_uv_auth_protocol: PinUvAuthProtocol) -> &PinProtocol {
match pin_uv_auth_protocol {
PinUvAuthProtocol::V1 => &self.pin_protocol_v1,
PinUvAuthProtocol::V2 => &self.pin_protocol_v2,
}
}
/// Gets a mutable reference to the PIN protocol of the given version.
fn get_mut_pin_protocol(
&mut self,
pin_uv_auth_protocol: PinUvAuthProtocol,
) -> &mut PinProtocol {
match pin_uv_auth_protocol {
PinUvAuthProtocol::V1 => &mut self.pin_protocol_v1,
PinUvAuthProtocol::V2 => &mut self.pin_protocol_v2,
}
}
/// Computes the shared secret for the given version.
fn get_shared_secret(
&self,
pin_uv_auth_protocol: PinUvAuthProtocol,
key_agreement: CoseKey,
) -> Result<Box<dyn SharedSecret>, Ctap2StatusCode> {
self.get_pin_protocol(pin_uv_auth_protocol)
.decapsulate(key_agreement, pin_uv_auth_protocol)
}
/// Checks the given encrypted PIN hash against the stored PIN hash.
///
/// Decrypts the encrypted pin_hash and compares it to the stored pin_hash.
/// Resets or decreases the PIN retries, depending on success or failure.
/// Also, in case of failure, the key agreement key is randomly reset.
fn verify_pin_hash_enc(
&mut self,
env: &mut impl Env,
pin_uv_auth_protocol: PinUvAuthProtocol,
shared_secret: &dyn SharedSecret,
pin_hash_enc: Vec<u8>,
) -> Result<(), Ctap2StatusCode> {
match storage::pin_hash(env)? {
Some(pin_hash) => {
if self.consecutive_pin_mismatches >= 3 {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED);
}
storage::decr_pin_retries(env)?;
let pin_hash_dec = shared_secret
.decrypt(&pin_hash_enc)
.map_err(|_| Ctap2StatusCode::CTAP2_ERR_PIN_INVALID)?;
if !bool::from(pin_hash.ct_eq(&pin_hash_dec)) {
self.get_mut_pin_protocol(pin_uv_auth_protocol)
.regenerate(env.rng());
if storage::pin_retries(env)? == 0 {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED);
}
self.consecutive_pin_mismatches += 1;
if self.consecutive_pin_mismatches >= 3 {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED);
}
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID);
}
}
// This status code is not explicitly mentioned in the specification.
None => return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED),
}
storage::reset_pin_retries(env)?;
self.consecutive_pin_mismatches = 0;
Ok(())
}
fn process_get_pin_retries(
&self,
env: &mut impl Env,
) -> Result<AuthenticatorClientPinResponse, Ctap2StatusCode> {
Ok(AuthenticatorClientPinResponse {
key_agreement: None,
pin_uv_auth_token: None,
retries: Some(storage::pin_retries(env)? as u64),
power_cycle_state: Some(self.consecutive_pin_mismatches >= 3),
})
}
fn process_get_key_agreement(
&self,
client_pin_params: AuthenticatorClientPinParameters,
) -> Result<AuthenticatorClientPinResponse, Ctap2StatusCode> {
let key_agreement = Some(
self.get_pin_protocol(client_pin_params.pin_uv_auth_protocol)
.get_public_key(),
);
Ok(AuthenticatorClientPinResponse {
key_agreement,
pin_uv_auth_token: None,
retries: None,
power_cycle_state: None,
})
}
fn process_set_pin(
&mut self,
env: &mut impl Env,
client_pin_params: AuthenticatorClientPinParameters,
) -> Result<(), Ctap2StatusCode> {
let AuthenticatorClientPinParameters {
pin_uv_auth_protocol,
key_agreement,
pin_uv_auth_param,
new_pin_enc,
..
} = client_pin_params;
let key_agreement = ok_or_missing(key_agreement)?;
let pin_uv_auth_param = ok_or_missing(pin_uv_auth_param)?;
let new_pin_enc = ok_or_missing(new_pin_enc)?;
if storage::pin_hash(env)?.is_some() {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID);
}
let shared_secret = self.get_shared_secret(pin_uv_auth_protocol, key_agreement)?;
shared_secret.verify(&new_pin_enc, &pin_uv_auth_param)?;
check_and_store_new_pin(env, shared_secret.as_ref(), new_pin_enc)?;
storage::reset_pin_retries(env)?;
Ok(())
}
fn process_change_pin(
&mut self,
env: &mut impl Env,
client_pin_params: AuthenticatorClientPinParameters,
) -> Result<(), Ctap2StatusCode> {
let AuthenticatorClientPinParameters {
pin_uv_auth_protocol,
key_agreement,
pin_uv_auth_param,
new_pin_enc,
pin_hash_enc,
..
} = client_pin_params;
let key_agreement = ok_or_missing(key_agreement)?;
let pin_uv_auth_param = ok_or_missing(pin_uv_auth_param)?;
let new_pin_enc = ok_or_missing(new_pin_enc)?;
let pin_hash_enc = ok_or_missing(pin_hash_enc)?;
if storage::pin_retries(env)? == 0 {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED);
}
let shared_secret = self.get_shared_secret(pin_uv_auth_protocol, key_agreement)?;
let mut auth_param_data = new_pin_enc.clone();
auth_param_data.extend(&pin_hash_enc);
shared_secret.verify(&auth_param_data, &pin_uv_auth_param)?;
self.verify_pin_hash_enc(
env,
pin_uv_auth_protocol,
shared_secret.as_ref(),
pin_hash_enc,
)?;
check_and_store_new_pin(env, shared_secret.as_ref(), new_pin_enc)?;
self.pin_protocol_v1.reset_pin_uv_auth_token(env.rng());
self.pin_protocol_v2.reset_pin_uv_auth_token(env.rng());
Ok(())
}
fn process_get_pin_token(
&mut self,
env: &mut impl Env,
client_pin_params: AuthenticatorClientPinParameters,
now: CtapInstant,
) -> Result<AuthenticatorClientPinResponse, Ctap2StatusCode> {
let AuthenticatorClientPinParameters {
pin_uv_auth_protocol,
key_agreement,
pin_hash_enc,
permissions,
permissions_rp_id,
..
} = client_pin_params;
let key_agreement = ok_or_missing(key_agreement)?;
let pin_hash_enc = ok_or_missing(pin_hash_enc)?;
if permissions.is_some() || permissions_rp_id.is_some() {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
if storage::pin_retries(env)? == 0 {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED);
}
let shared_secret = self.get_shared_secret(pin_uv_auth_protocol, key_agreement)?;
self.verify_pin_hash_enc(
env,
pin_uv_auth_protocol,
shared_secret.as_ref(),
pin_hash_enc,
)?;
if storage::has_force_pin_change(env)? {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID);
}
self.pin_protocol_v1.reset_pin_uv_auth_token(env.rng());
self.pin_protocol_v2.reset_pin_uv_auth_token(env.rng());
self.pin_uv_auth_token_state
.begin_using_pin_uv_auth_token(now);
self.pin_uv_auth_token_state.set_default_permissions();
let pin_uv_auth_token = shared_secret.encrypt(
env.rng(),
self.get_pin_protocol(pin_uv_auth_protocol)
.get_pin_uv_auth_token(),
)?;
Ok(AuthenticatorClientPinResponse {
key_agreement: None,
pin_uv_auth_token: Some(pin_uv_auth_token),
retries: None,
power_cycle_state: None,
})
}
fn process_get_pin_uv_auth_token_using_uv_with_permissions(
&self,
// If you want to support local user verification, implement this function.
// Lacking a fingerprint reader, this subcommand is currently unsupported.
_client_pin_params: AuthenticatorClientPinParameters,
) -> Result<AuthenticatorClientPinResponse, Ctap2StatusCode> {
// User verification is only supported through PIN currently.
Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND)
}
fn process_get_uv_retries(&self) -> Result<AuthenticatorClientPinResponse, Ctap2StatusCode> {
// User verification is only supported through PIN currently.
Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND)
}
fn process_get_pin_uv_auth_token_using_pin_with_permissions(
&mut self,
env: &mut impl Env,
mut client_pin_params: AuthenticatorClientPinParameters,
now: CtapInstant,
) -> Result<AuthenticatorClientPinResponse, Ctap2StatusCode> {
// Mutating client_pin_params is just an optimization to move it into
// process_get_pin_token, without cloning permissions_rp_id here.
// getPinToken requires permissions* to be None.
let permissions = ok_or_missing(client_pin_params.permissions.take())?;
let permissions_rp_id = client_pin_params.permissions_rp_id.take();
if permissions == 0 {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
// This check is not mentioned protocol steps, but mentioned in a side note.
if permissions & 0x03 != 0 && permissions_rp_id.is_none() {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
let response = self.process_get_pin_token(env, client_pin_params, now)?;
self.pin_uv_auth_token_state.set_permissions(permissions);
self.pin_uv_auth_token_state
.set_permissions_rp_id(permissions_rp_id);
Ok(response)
}
/// Processes the authenticatorClientPin command.
pub fn process_command(
&mut self,
env: &mut impl Env,
client_pin_params: AuthenticatorClientPinParameters,
now: CtapInstant,
) -> Result<ResponseData, Ctap2StatusCode> {
let response = match client_pin_params.sub_command {
ClientPinSubCommand::GetPinRetries => Some(self.process_get_pin_retries(env)?),
ClientPinSubCommand::GetKeyAgreement => {
Some(self.process_get_key_agreement(client_pin_params)?)
}
ClientPinSubCommand::SetPin => {
self.process_set_pin(env, client_pin_params)?;
None
}
ClientPinSubCommand::ChangePin => {
self.process_change_pin(env, client_pin_params)?;
None
}
ClientPinSubCommand::GetPinToken => {
Some(self.process_get_pin_token(env, client_pin_params, now)?)
}
ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions => Some(
self.process_get_pin_uv_auth_token_using_uv_with_permissions(client_pin_params)?,
),
ClientPinSubCommand::GetUvRetries => Some(self.process_get_uv_retries()?),
ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => Some(
self.process_get_pin_uv_auth_token_using_pin_with_permissions(
env,
client_pin_params,
now,
)?,
),
};
Ok(ResponseData::AuthenticatorClientPin(response))
}
/// Verifies the HMAC for the pinUvAuthToken of the given version.
pub fn verify_pin_uv_auth_token(
&self,
hmac_contents: &[u8],
pin_uv_auth_param: &[u8],
pin_uv_auth_protocol: PinUvAuthProtocol,
) -> Result<(), Ctap2StatusCode> {
if !self.pin_uv_auth_token_state.is_in_use() {
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID);
}
verify_pin_uv_auth_token(
self.get_pin_protocol(pin_uv_auth_protocol)
.get_pin_uv_auth_token(),
hmac_contents,
pin_uv_auth_param,
pin_uv_auth_protocol,
)
}
/// Resets all held state.
pub fn reset(&mut self, rng: &mut impl Rng256) {
self.pin_protocol_v1.regenerate(rng);
self.pin_protocol_v1.reset_pin_uv_auth_token(rng);
self.pin_protocol_v2.regenerate(rng);
self.pin_protocol_v2.reset_pin_uv_auth_token(rng);
self.consecutive_pin_mismatches = 0;
self.pin_uv_auth_token_state.stop_using_pin_uv_auth_token();
}
/// Verifies, computes and encrypts the HMAC-secret outputs.
///
/// The salt_enc is
/// - verified with the shared secret and salt_auth,
/// - decrypted with the shared secret,
/// - HMAC'ed with cred_random.
/// The length of the output matches salt_enc and has to be 1 or 2 blocks of
/// 32 byte.
pub fn process_hmac_secret(
&self,
rng: &mut impl Rng256,
hmac_secret_input: GetAssertionHmacSecretInput,
cred_random: &[u8; 32],
) -> Result<Vec<u8>, Ctap2StatusCode> {
let GetAssertionHmacSecretInput {
key_agreement,
salt_enc,
salt_auth,
pin_uv_auth_protocol,
} = hmac_secret_input;
let shared_secret = self
.get_pin_protocol(pin_uv_auth_protocol)
.decapsulate(key_agreement, pin_uv_auth_protocol)?;
shared_secret.verify(&salt_enc, &salt_auth)?;
let decrypted_salts = shared_secret.decrypt(&salt_enc)?;
if decrypted_salts.len() != 32 && decrypted_salts.len() != 64 {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
let mut output = hmac_256::<Sha256>(cred_random, &decrypted_salts[..32]).to_vec();
if decrypted_salts.len() == 64 {
let mut output2 = hmac_256::<Sha256>(cred_random, &decrypted_salts[32..]).to_vec();
output.append(&mut output2);
}
shared_secret.encrypt(rng, &output)
}
/// Consumes flags and permissions related to the pinUvAuthToken.
pub fn clear_token_flags(&mut self) {
self.pin_uv_auth_token_state.clear_user_verified_flag();
self.pin_uv_auth_token_state
.clear_pin_uv_auth_token_permissions_except_lbw();
}
/// Updates the running timers, triggers timeout events.
pub fn update_timeouts(&mut self, now: CtapInstant) {
self.pin_uv_auth_token_state
.pin_uv_auth_token_usage_timer_observer(now);
}
/// Checks if user verification is cached for use of the pinUvAuthToken.
pub fn check_user_verified_flag(&mut self) -> Result<(), Ctap2StatusCode> {
if self.pin_uv_auth_token_state.get_user_verified_flag_value() {
Ok(())
} else {
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
}
}
/// Check if the required command's token permission is granted.
pub fn has_permission(&self, permission: PinPermission) -> Result<(), Ctap2StatusCode> {
self.pin_uv_auth_token_state.has_permission(permission)
}
/// Check if no RP ID is associated with the token permission.
pub fn has_no_rp_id_permission(&self) -> Result<(), Ctap2StatusCode> {
self.pin_uv_auth_token_state.has_no_permissions_rp_id()
}
/// Check if no or the passed RP ID is associated with the token permission.
pub fn has_no_or_rp_id_permission(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> {
self.pin_uv_auth_token_state
.has_no_permissions_rp_id()
.or_else(|_| self.pin_uv_auth_token_state.has_permissions_rp_id(rp_id))
}
/// Check if no RP ID is associated with the token permission, or it matches the hash.
pub fn has_no_or_rp_id_hash_permission(
&self,
rp_id_hash: &[u8],
) -> Result<(), Ctap2StatusCode> {
self.pin_uv_auth_token_state
.has_no_permissions_rp_id()
.or_else(|_| {
self.pin_uv_auth_token_state
.has_permissions_rp_id_hash(rp_id_hash)
})
}
/// Check if the passed RP ID is associated with the token permission.
///
/// If no RP ID is associated, associate the passed RP ID as a side effect.
pub fn ensure_rp_id_permission(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> {
if self
.pin_uv_auth_token_state
.has_no_permissions_rp_id()
.is_ok()
{
self.pin_uv_auth_token_state
.set_permissions_rp_id(Some(String::from(rp_id)));
return Ok(());
}
self.pin_uv_auth_token_state.has_permissions_rp_id(rp_id)
}
#[cfg(test)]
pub fn new_test(
key_agreement_key: crypto::ecdh::SecKey,
pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH],
pin_uv_auth_protocol: PinUvAuthProtocol,
) -> ClientPin {
let mut env = crate::env::test::TestEnv::new();
let (key_agreement_key_v1, key_agreement_key_v2) = match pin_uv_auth_protocol {
PinUvAuthProtocol::V1 => (key_agreement_key, crypto::ecdh::SecKey::gensk(env.rng())),
PinUvAuthProtocol::V2 => (crypto::ecdh::SecKey::gensk(env.rng()), key_agreement_key),
};
let mut pin_uv_auth_token_state = PinUvAuthTokenState::new();
pin_uv_auth_token_state.set_permissions(0xFF);
pin_uv_auth_token_state.begin_using_pin_uv_auth_token(CtapInstant::new(0));
ClientPin {
pin_protocol_v1: PinProtocol::new_test(key_agreement_key_v1, pin_uv_auth_token),
pin_protocol_v2: PinProtocol::new_test(key_agreement_key_v2, pin_uv_auth_token),
consecutive_pin_mismatches: 0,
pin_uv_auth_token_state,
}
}
}
#[cfg(test)]
mod test {
use super::super::pin_protocol::authenticate_pin_uv_auth_token;
use super::*;
use crate::env::test::TestEnv;
use alloc::vec;
use embedded_time::duration::Milliseconds;
/// Stores a PIN hash corresponding to the dummy PIN "1234".
fn set_standard_pin(env: &mut TestEnv) {
let mut pin = [0u8; 64];
pin[..4].copy_from_slice(b"1234");
let mut pin_hash = [0u8; 16];
pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]);
storage::set_pin(env, &pin_hash, 4).unwrap();
}
/// Fails on PINs bigger than 64 bytes.
fn encrypt_pin(shared_secret: &dyn SharedSecret, pin: Vec<u8>) -> Vec<u8> {
assert!(pin.len() <= 64);
let mut env = TestEnv::new();
let mut padded_pin = [0u8; 64];
padded_pin[..pin.len()].copy_from_slice(&pin[..]);
shared_secret.encrypt(env.rng(), &padded_pin).unwrap()
}
/// Generates a ClientPin instance and a shared secret for testing.
///
/// The shared secret for the desired PIN protocol is generated in a
/// handshake with itself. The other protocol has a random private key, so
/// tests using the wrong combination of PIN protocol and shared secret
/// should fail.
fn create_client_pin_and_shared_secret(
pin_uv_auth_protocol: PinUvAuthProtocol,
) -> (ClientPin, Box<dyn SharedSecret>) {
let mut env = TestEnv::new();
let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng());
let pk = key_agreement_key.genpk();
let key_agreement = CoseKey::from(pk);
let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH];
let client_pin =
ClientPin::new_test(key_agreement_key, pin_uv_auth_token, pin_uv_auth_protocol);
let shared_secret = client_pin
.get_pin_protocol(pin_uv_auth_protocol)
.decapsulate(key_agreement, pin_uv_auth_protocol)
.unwrap();
(client_pin, shared_secret)
}
/// Generates standard input parameters to the ClientPin command.
///
/// All fields are populated for simplicity, even though most are unused.
fn create_client_pin_and_parameters(
pin_uv_auth_protocol: PinUvAuthProtocol,
sub_command: ClientPinSubCommand,
) -> (ClientPin, AuthenticatorClientPinParameters) {
let mut env = TestEnv::new();
let (client_pin, shared_secret) = create_client_pin_and_shared_secret(pin_uv_auth_protocol);
let pin = b"1234";
let mut padded_pin = [0u8; 64];
padded_pin[..pin.len()].copy_from_slice(&pin[..]);
let pin_hash = Sha256::hash(&padded_pin);
let new_pin_enc = shared_secret
.as_ref()
.encrypt(env.rng(), &padded_pin)
.unwrap();
let pin_uv_auth_param = shared_secret.as_ref().authenticate(&new_pin_enc);
let pin_hash_enc = shared_secret
.as_ref()
.encrypt(env.rng(), &pin_hash[..16])
.unwrap();
let (permissions, permissions_rp_id) = match sub_command {
ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions
| ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => {
(Some(0x03), Some("example.com".to_string()))
}
_ => (None, None),
};
let params = AuthenticatorClientPinParameters {
pin_uv_auth_protocol,
sub_command,
key_agreement: Some(
client_pin
.get_pin_protocol(pin_uv_auth_protocol)
.get_public_key(),
),
pin_uv_auth_param: Some(pin_uv_auth_param),
new_pin_enc: Some(new_pin_enc),
pin_hash_enc: Some(pin_hash_enc),
permissions,
permissions_rp_id,
};
(client_pin, params)
}
#[test]
fn test_mix_pin_protocols() {
let mut env = TestEnv::new();
let client_pin = ClientPin::new(env.rng());
let pin_protocol_v1 = client_pin.get_pin_protocol(PinUvAuthProtocol::V1);
let pin_protocol_v2 = client_pin.get_pin_protocol(PinUvAuthProtocol::V2);
let message = vec![0xAA; 16];
let shared_secret_v1 = pin_protocol_v1
.decapsulate(pin_protocol_v1.get_public_key(), PinUvAuthProtocol::V1)
.unwrap();
let shared_secret_v2 = pin_protocol_v2
.decapsulate(pin_protocol_v2.get_public_key(), PinUvAuthProtocol::V2)
.unwrap();
let ciphertext = shared_secret_v1.encrypt(env.rng(), &message).unwrap();
let plaintext = shared_secret_v2.decrypt(&ciphertext).unwrap();
assert_ne!(&message, &plaintext);
let ciphertext = shared_secret_v2.encrypt(env.rng(), &message).unwrap();
let plaintext = shared_secret_v1.decrypt(&ciphertext).unwrap();
assert_ne!(&message, &plaintext);
let fake_secret_v1 = pin_protocol_v1
.decapsulate(pin_protocol_v2.get_public_key(), PinUvAuthProtocol::V1)
.unwrap();
let ciphertext = shared_secret_v1.encrypt(env.rng(), &message).unwrap();
let plaintext = fake_secret_v1.decrypt(&ciphertext).unwrap();
assert_ne!(&message, &plaintext);
let ciphertext = fake_secret_v1.encrypt(env.rng(), &message).unwrap();
let plaintext = shared_secret_v1.decrypt(&ciphertext).unwrap();
assert_ne!(&message, &plaintext);
let fake_secret_v2 = pin_protocol_v2
.decapsulate(pin_protocol_v1.get_public_key(), PinUvAuthProtocol::V2)
.unwrap();
let ciphertext = shared_secret_v2.encrypt(env.rng(), &message).unwrap();
let plaintext = fake_secret_v2.decrypt(&ciphertext).unwrap();
assert_ne!(&message, &plaintext);
let ciphertext = fake_secret_v2.encrypt(env.rng(), &message).unwrap();
let plaintext = shared_secret_v2.decrypt(&ciphertext).unwrap();
assert_ne!(&message, &plaintext);
}
fn test_helper_verify_pin_hash_enc(pin_uv_auth_protocol: PinUvAuthProtocol) {
let mut env = TestEnv::new();
let mut client_pin = ClientPin::new(env.rng());
let pin_protocol = client_pin.get_pin_protocol(pin_uv_auth_protocol);
let shared_secret = pin_protocol
.decapsulate(pin_protocol.get_public_key(), pin_uv_auth_protocol)
.unwrap();
// The PIN is "1234".
let pin_hash = [
0x01, 0xD9, 0x88, 0x40, 0x50, 0xBB, 0xD0, 0x7A, 0x23, 0x1A, 0xEB, 0x69, 0xD8, 0x36,
0xC4, 0x12,
];
storage::set_pin(&mut env, &pin_hash, 4).unwrap();
let pin_hash_enc = shared_secret
.as_ref()
.encrypt(env.rng(), &pin_hash)
.unwrap();
assert_eq!(
client_pin.verify_pin_hash_enc(
&mut env,
pin_uv_auth_protocol,
shared_secret.as_ref(),
pin_hash_enc
),
Ok(())
);
let pin_hash_enc = vec![0xEE; 16];
assert_eq!(
client_pin.verify_pin_hash_enc(
&mut env,
pin_uv_auth_protocol,
shared_secret.as_ref(),
pin_hash_enc
),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID)
);
let pin_hash_enc = shared_secret
.as_ref()
.encrypt(env.rng(), &pin_hash)
.unwrap();
client_pin.consecutive_pin_mismatches = 3;
assert_eq!(
client_pin.verify_pin_hash_enc(
&mut env,
pin_uv_auth_protocol,
shared_secret.as_ref(),
pin_hash_enc
),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED)
);
client_pin.consecutive_pin_mismatches = 0;
let pin_hash_enc = vec![0x77; PIN_AUTH_LENGTH - 1];
assert_eq!(
client_pin.verify_pin_hash_enc(
&mut env,
pin_uv_auth_protocol,
shared_secret.as_ref(),
pin_hash_enc
),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID)
);
let pin_hash_enc = vec![0x77; PIN_AUTH_LENGTH + 1];
assert_eq!(
client_pin.verify_pin_hash_enc(
&mut env,
pin_uv_auth_protocol,
shared_secret.as_ref(),
pin_hash_enc
),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID)
);
}
#[test]
fn test_verify_pin_hash_enc_v1() {
test_helper_verify_pin_hash_enc(PinUvAuthProtocol::V1);
}
#[test]
fn test_verify_pin_hash_enc_v2() {
test_helper_verify_pin_hash_enc(PinUvAuthProtocol::V2);
}
fn test_helper_process_get_pin_retries(pin_uv_auth_protocol: PinUvAuthProtocol) {
let (mut client_pin, params) = create_client_pin_and_parameters(
pin_uv_auth_protocol,
ClientPinSubCommand::GetPinRetries,
);
let mut env = TestEnv::new();
let expected_response = Some(AuthenticatorClientPinResponse {
key_agreement: None,
pin_uv_auth_token: None,
retries: Some(storage::pin_retries(&mut env).unwrap() as u64),
power_cycle_state: Some(false),
});
assert_eq!(
client_pin.process_command(&mut env, params.clone(), CtapInstant::new(0)),
Ok(ResponseData::AuthenticatorClientPin(expected_response))
);
client_pin.consecutive_pin_mismatches = 3;
let expected_response = Some(AuthenticatorClientPinResponse {
key_agreement: None,
pin_uv_auth_token: None,
retries: Some(storage::pin_retries(&mut env).unwrap() as u64),
power_cycle_state: Some(true),
});
assert_eq!(
client_pin.process_command(&mut env, params, CtapInstant::new(0)),
Ok(ResponseData::AuthenticatorClientPin(expected_response))
);
}
#[test]
fn test_process_get_pin_retries_v1() {
test_helper_process_get_pin_retries(PinUvAuthProtocol::V1);
}
#[test]
fn test_process_get_pin_retries_v2() {
test_helper_process_get_pin_retries(PinUvAuthProtocol::V2);
}
fn test_helper_process_get_key_agreement(pin_uv_auth_protocol: PinUvAuthProtocol) {
let (mut client_pin, params) = create_client_pin_and_parameters(
pin_uv_auth_protocol,
ClientPinSubCommand::GetKeyAgreement,
);
let mut env = TestEnv::new();
let expected_response = Some(AuthenticatorClientPinResponse {
key_agreement: params.key_agreement.clone(),
pin_uv_auth_token: None,
retries: None,
power_cycle_state: None,
});
assert_eq!(
client_pin.process_command(&mut env, params, CtapInstant::new(0)),
Ok(ResponseData::AuthenticatorClientPin(expected_response))
);
}
#[test]
fn test_process_get_key_agreement_v1() {
test_helper_process_get_key_agreement(PinUvAuthProtocol::V1);
}
#[test]
fn test_process_get_key_agreement_v2() {
test_helper_process_get_key_agreement(PinUvAuthProtocol::V2);
}
fn test_helper_process_set_pin(pin_uv_auth_protocol: PinUvAuthProtocol) {
let (mut client_pin, params) =
create_client_pin_and_parameters(pin_uv_auth_protocol, ClientPinSubCommand::SetPin);
let mut env = TestEnv::new();
assert_eq!(
client_pin.process_command(&mut env, params, CtapInstant::new(0)),
Ok(ResponseData::AuthenticatorClientPin(None))
);
}
#[test]
fn test_process_set_pin_v1() {
test_helper_process_set_pin(PinUvAuthProtocol::V1);
}
#[test]
fn test_process_set_pin_v2() {
test_helper_process_set_pin(PinUvAuthProtocol::V2);
}
fn test_helper_process_change_pin(pin_uv_auth_protocol: PinUvAuthProtocol) {
let (mut client_pin, mut params) =
create_client_pin_and_parameters(pin_uv_auth_protocol, ClientPinSubCommand::ChangePin);
let shared_secret = client_pin
.get_pin_protocol(pin_uv_auth_protocol)
.decapsulate(
params.key_agreement.clone().unwrap(),
params.pin_uv_auth_protocol,
)
.unwrap();
let mut env = TestEnv::new();
set_standard_pin(&mut env);
let mut auth_param_data = params.new_pin_enc.clone().unwrap();
auth_param_data.extend(params.pin_hash_enc.as_ref().unwrap());
let pin_uv_auth_param = shared_secret.authenticate(&auth_param_data);
params.pin_uv_auth_param = Some(pin_uv_auth_param);
assert_eq!(
client_pin.process_command(&mut env, params.clone(), CtapInstant::new(0)),
Ok(ResponseData::AuthenticatorClientPin(None))
);
let mut bad_params = params.clone();
bad_params.pin_hash_enc = Some(vec![0xEE; 16]);
assert_eq!(
client_pin.process_command(&mut env, bad_params, CtapInstant::new(0)),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
while storage::pin_retries(&mut env).unwrap() > 0 {
storage::decr_pin_retries(&mut env).unwrap();
}
assert_eq!(
client_pin.process_command(&mut env, params, CtapInstant::new(0)),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED)
);
}
#[test]
fn test_process_change_pin_v1() {
test_helper_process_change_pin(PinUvAuthProtocol::V1);
}
#[test]
fn test_process_change_pin_v2() {
test_helper_process_change_pin(PinUvAuthProtocol::V2);
}
fn test_helper_process_get_pin_token(pin_uv_auth_protocol: PinUvAuthProtocol) {
let (mut client_pin, params) = create_client_pin_and_parameters(
pin_uv_auth_protocol,
ClientPinSubCommand::GetPinToken,
);
let shared_secret = client_pin
.get_pin_protocol(pin_uv_auth_protocol)
.decapsulate(
params.key_agreement.clone().unwrap(),
params.pin_uv_auth_protocol,
)
.unwrap();
let mut env = TestEnv::new();
set_standard_pin(&mut env);
let response = client_pin
.process_command(&mut env, params.clone(), CtapInstant::new(0))
.unwrap();
let encrypted_token = match response {
ResponseData::AuthenticatorClientPin(Some(response)) => {
response.pin_uv_auth_token.unwrap()
}
_ => panic!("Invalid response type"),
};
assert_eq!(
&shared_secret.decrypt(&encrypted_token).unwrap(),
client_pin
.get_pin_protocol(pin_uv_auth_protocol)
.get_pin_uv_auth_token()
);
assert_eq!(
client_pin
.pin_uv_auth_token_state
.has_permission(PinPermission::MakeCredential),
Ok(())
);
assert_eq!(
client_pin
.pin_uv_auth_token_state
.has_permission(PinPermission::GetAssertion),
Ok(())
);
assert_eq!(
client_pin
.pin_uv_auth_token_state
.has_no_permissions_rp_id(),
Ok(())
);
let mut bad_params = params;
bad_params.pin_hash_enc = Some(vec![0xEE; 16]);
assert_eq!(
client_pin.process_command(&mut env, bad_params, CtapInstant::new(0)),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID)
);
}
#[test]
fn test_process_get_pin_token_v1() {
test_helper_process_get_pin_token(PinUvAuthProtocol::V1);
}
#[test]
fn test_process_get_pin_token_v2() {
test_helper_process_get_pin_token(PinUvAuthProtocol::V2);
}
fn test_helper_process_get_pin_token_force_pin_change(pin_uv_auth_protocol: PinUvAuthProtocol) {
let (mut client_pin, params) = create_client_pin_and_parameters(
pin_uv_auth_protocol,
ClientPinSubCommand::GetPinToken,
);
let mut env = TestEnv::new();
set_standard_pin(&mut env);
assert_eq!(storage::force_pin_change(&mut env), Ok(()));
assert_eq!(
client_pin.process_command(&mut env, params, CtapInstant::new(0)),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID),
);
}
#[test]
fn test_process_get_pin_token_force_pin_change_v1() {
test_helper_process_get_pin_token_force_pin_change(PinUvAuthProtocol::V1);
}
#[test]
fn test_process_get_pin_token_force_pin_change_v2() {
test_helper_process_get_pin_token_force_pin_change(PinUvAuthProtocol::V2);
}
fn test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions(
pin_uv_auth_protocol: PinUvAuthProtocol,
) {
let (mut client_pin, params) = create_client_pin_and_parameters(
pin_uv_auth_protocol,
ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions,
);
let shared_secret = client_pin
.get_pin_protocol(pin_uv_auth_protocol)
.decapsulate(
params.key_agreement.clone().unwrap(),
params.pin_uv_auth_protocol,
)
.unwrap();
let mut env = TestEnv::new();
set_standard_pin(&mut env);
let response = client_pin
.process_command(&mut env, params.clone(), CtapInstant::new(0))
.unwrap();
let encrypted_token = match response {
ResponseData::AuthenticatorClientPin(Some(response)) => {
response.pin_uv_auth_token.unwrap()
}
_ => panic!("Invalid response type"),
};
assert_eq!(
&shared_secret.decrypt(&encrypted_token).unwrap(),
client_pin
.get_pin_protocol(pin_uv_auth_protocol)
.get_pin_uv_auth_token()
);
assert_eq!(
client_pin
.pin_uv_auth_token_state
.has_permission(PinPermission::MakeCredential),
Ok(())
);
assert_eq!(
client_pin
.pin_uv_auth_token_state
.has_permission(PinPermission::GetAssertion),
Ok(())
);
assert_eq!(
client_pin
.pin_uv_auth_token_state
.has_permissions_rp_id("example.com"),
Ok(())
);
let mut bad_params = params.clone();
bad_params.permissions = Some(0x00);
assert_eq!(
client_pin.process_command(&mut env, bad_params, CtapInstant::new(0)),
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
let mut bad_params = params.clone();
bad_params.permissions_rp_id = None;
assert_eq!(
client_pin.process_command(&mut env, bad_params, CtapInstant::new(0)),
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
let mut bad_params = params;
bad_params.pin_hash_enc = Some(vec![0xEE; 16]);
assert_eq!(
client_pin.process_command(&mut env, bad_params, CtapInstant::new(0)),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID)
);
}
#[test]
fn test_process_get_pin_uv_auth_token_using_pin_with_permissions_v1() {
test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions(PinUvAuthProtocol::V1);
}
#[test]
fn test_process_get_pin_uv_auth_token_using_pin_with_permissions_v2() {
test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions(PinUvAuthProtocol::V2);
}
fn test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions_force_pin_change(
pin_uv_auth_protocol: PinUvAuthProtocol,
) {
let (mut client_pin, params) = create_client_pin_and_parameters(
pin_uv_auth_protocol,
ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions,
);
let mut env = TestEnv::new();
set_standard_pin(&mut env);
assert_eq!(storage::force_pin_change(&mut env), Ok(()));
assert_eq!(
client_pin.process_command(&mut env, params, CtapInstant::new(0)),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID)
);
}
#[test]
fn test_process_get_pin_uv_auth_token_using_pin_with_permissions_force_pin_change_v1() {
test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions_force_pin_change(
PinUvAuthProtocol::V1,
);
}
#[test]
fn test_process_get_pin_uv_auth_token_using_pin_with_permissions_force_pin_change_v2() {
test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions_force_pin_change(
PinUvAuthProtocol::V2,
);
}
fn test_helper_decrypt_pin(pin_uv_auth_protocol: PinUvAuthProtocol) {
let mut env = TestEnv::new();
let pin_protocol = PinProtocol::new(env.rng());
let shared_secret = pin_protocol
.decapsulate(pin_protocol.get_public_key(), pin_uv_auth_protocol)
.unwrap();
let new_pin_enc = encrypt_pin(shared_secret.as_ref(), b"1234".to_vec());
assert_eq!(
decrypt_pin(shared_secret.as_ref(), new_pin_enc),
Ok(b"1234".to_vec()),
);
let new_pin_enc = encrypt_pin(shared_secret.as_ref(), b"123".to_vec());
assert_eq!(
decrypt_pin(shared_secret.as_ref(), new_pin_enc),
Ok(b"123".to_vec()),
);
// Encrypted PIN is too short.
let new_pin_enc = vec![0x44; 63];
assert_eq!(
decrypt_pin(shared_secret.as_ref(), new_pin_enc),
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
// Encrypted PIN is too long.
let new_pin_enc = vec![0x44; 65];
assert_eq!(
decrypt_pin(shared_secret.as_ref(), new_pin_enc),
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
}
#[test]
fn test_decrypt_pin_v1() {
test_helper_decrypt_pin(PinUvAuthProtocol::V1);
}
#[test]
fn test_decrypt_pin_v2() {
test_helper_decrypt_pin(PinUvAuthProtocol::V2);
}
fn test_helper_check_and_store_new_pin(pin_uv_auth_protocol: PinUvAuthProtocol) {
let mut env = TestEnv::new();
let pin_protocol = PinProtocol::new(env.rng());
let shared_secret = pin_protocol
.decapsulate(pin_protocol.get_public_key(), pin_uv_auth_protocol)
.unwrap();
let test_cases = vec![
// Accept PIN "1234".
(b"1234".to_vec(), Ok(())),
// Reject PIN "123" since it is too short.
(
b"123".to_vec(),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION),
),
// Reject PIN "12'\0'4" (a zero byte at index 2).
(
b"12\04".to_vec(),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION),
),
// PINs must be at most 63 bytes long, to allow for a trailing 0u8 padding.
(
vec![0x30; 64],
Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION),
),
];
for (pin, result) in test_cases {
let old_pin_hash = storage::pin_hash(&mut env).unwrap();
let new_pin_enc = encrypt_pin(shared_secret.as_ref(), pin);
assert_eq!(
check_and_store_new_pin(&mut env, shared_secret.as_ref(), new_pin_enc),
result
);
if result.is_ok() {
assert_ne!(old_pin_hash, storage::pin_hash(&mut env).unwrap());
} else {
assert_eq!(old_pin_hash, storage::pin_hash(&mut env).unwrap());
}
}
}
#[test]
fn test_check_and_store_new_pin_v1() {
test_helper_check_and_store_new_pin(PinUvAuthProtocol::V1);
}
#[test]
fn test_check_and_store_new_pin_v2() {
test_helper_check_and_store_new_pin(PinUvAuthProtocol::V2);
}
/// Generates valid inputs for process_hmac_secret and returns the output.
fn get_process_hmac_secret_decrypted_output(
pin_uv_auth_protocol: PinUvAuthProtocol,
cred_random: &[u8; 32],
salt: Vec<u8>,
) -> Result<Vec<u8>, Ctap2StatusCode> {
let mut env = TestEnv::new();
let (client_pin, shared_secret) = create_client_pin_and_shared_secret(pin_uv_auth_protocol);
let salt_enc = shared_secret.as_ref().encrypt(env.rng(), &salt).unwrap();
let salt_auth = shared_secret.authenticate(&salt_enc);
let hmac_secret_input = GetAssertionHmacSecretInput {
key_agreement: client_pin
.get_pin_protocol(pin_uv_auth_protocol)
.get_public_key(),
salt_enc,
salt_auth,
pin_uv_auth_protocol,
};
let output = client_pin.process_hmac_secret(env.rng(), hmac_secret_input, cred_random);
output.map(|v| shared_secret.as_ref().decrypt(&v).unwrap())
}
fn test_helper_process_hmac_secret_bad_salt_auth(pin_uv_auth_protocol: PinUvAuthProtocol) {
let mut env = TestEnv::new();
let (client_pin, shared_secret) = create_client_pin_and_shared_secret(pin_uv_auth_protocol);
let cred_random = [0xC9; 32];
let salt_enc = vec![0x01; 32];
let mut salt_auth = shared_secret.authenticate(&salt_enc);
salt_auth[0] ^= 0x01;
let hmac_secret_input = GetAssertionHmacSecretInput {
key_agreement: client_pin
.get_pin_protocol(pin_uv_auth_protocol)
.get_public_key(),
salt_enc,
salt_auth,
pin_uv_auth_protocol,
};
let output = client_pin.process_hmac_secret(env.rng(), hmac_secret_input, &cred_random);
assert_eq!(output, Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID));
}
#[test]
fn test_process_hmac_secret_bad_salt_auth_v1() {
test_helper_process_hmac_secret_bad_salt_auth(PinUvAuthProtocol::V1);
}
#[test]
fn test_process_hmac_secret_bad_salt_auth_v2() {
test_helper_process_hmac_secret_bad_salt_auth(PinUvAuthProtocol::V2);
}
fn test_helper_process_hmac_secret_one_salt(pin_uv_auth_protocol: PinUvAuthProtocol) {
let cred_random = [0xC9; 32];
let salt = vec![0x01; 32];
let expected_output = hmac_256::<Sha256>(&cred_random, &salt);
let output =
get_process_hmac_secret_decrypted_output(pin_uv_auth_protocol, &cred_random, salt)
.unwrap();
assert_eq!(&output, &expected_output);
}
#[test]
fn test_process_hmac_secret_one_salt_v1() {
test_helper_process_hmac_secret_one_salt(PinUvAuthProtocol::V1);
}
#[test]
fn test_process_hmac_secret_one_salt_v2() {
test_helper_process_hmac_secret_one_salt(PinUvAuthProtocol::V2);
}
fn test_helper_process_hmac_secret_two_salts(pin_uv_auth_protocol: PinUvAuthProtocol) {
let cred_random = [0xC9; 32];
let salt1 = [0x01; 32];
let salt2 = [0x02; 32];
let expected_output1 = hmac_256::<Sha256>(&cred_random, &salt1);
let expected_output2 = hmac_256::<Sha256>(&cred_random, &salt2);
let mut salt12 = vec![0x00; 64];
salt12[..32].copy_from_slice(&salt1);
salt12[32..].copy_from_slice(&salt2);
let output =
get_process_hmac_secret_decrypted_output(pin_uv_auth_protocol, &cred_random, salt12)
.unwrap();
assert_eq!(&output[..32], &expected_output1);
assert_eq!(&output[32..], &expected_output2);
let mut salt02 = vec![0x00; 64];
salt02[32..].copy_from_slice(&salt2);
let output =
get_process_hmac_secret_decrypted_output(pin_uv_auth_protocol, &cred_random, salt02)
.unwrap();
assert_eq!(&output[32..], &expected_output2);
let mut salt10 = vec![0x00; 64];
salt10[..32].copy_from_slice(&salt1);
let output =
get_process_hmac_secret_decrypted_output(pin_uv_auth_protocol, &cred_random, salt10)
.unwrap();
assert_eq!(&output[..32], &expected_output1);
}
#[test]
fn test_process_hmac_secret_two_salts_v1() {
test_helper_process_hmac_secret_two_salts(PinUvAuthProtocol::V1);
}
#[test]
fn test_process_hmac_secret_two_salts_v2() {
test_helper_process_hmac_secret_two_salts(PinUvAuthProtocol::V2);
}
fn test_helper_process_hmac_secret_wrong_length(pin_uv_auth_protocol: PinUvAuthProtocol) {
let cred_random = [0xC9; 32];
let output = get_process_hmac_secret_decrypted_output(
pin_uv_auth_protocol,
&cred_random,
vec![0x5E; 48],
);
assert_eq!(output, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER));
}
#[test]
fn test_process_hmac_secret_wrong_length_v1() {
test_helper_process_hmac_secret_wrong_length(PinUvAuthProtocol::V1);
}
#[test]
fn test_process_hmac_secret_wrong_length_v2() {
test_helper_process_hmac_secret_wrong_length(PinUvAuthProtocol::V2);
}
#[test]
fn test_has_permission() {
let mut env = TestEnv::new();
let mut client_pin = ClientPin::new(env.rng());
client_pin.pin_uv_auth_token_state.set_permissions(0x7F);
for permission in PinPermission::into_enum_iter() {
assert_eq!(
client_pin
.pin_uv_auth_token_state
.has_permission(permission),
Ok(())
);
}
client_pin.pin_uv_auth_token_state.set_permissions(0x00);
for permission in PinPermission::into_enum_iter() {
assert_eq!(
client_pin
.pin_uv_auth_token_state
.has_permission(permission),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
}
#[test]
fn test_has_no_rp_id_permission() {
let mut env = TestEnv::new();
let mut client_pin = ClientPin::new(env.rng());
assert_eq!(client_pin.has_no_rp_id_permission(), Ok(()));
client_pin
.pin_uv_auth_token_state
.set_permissions_rp_id(Some("example.com".to_string()));
assert_eq!(
client_pin.has_no_rp_id_permission(),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
#[test]
fn test_has_no_or_rp_id_permission() {
let mut env = TestEnv::new();
let mut client_pin = ClientPin::new(env.rng());
assert_eq!(client_pin.has_no_or_rp_id_permission("example.com"), Ok(()));
client_pin
.pin_uv_auth_token_state
.set_permissions_rp_id(Some("example.com".to_string()));
assert_eq!(client_pin.has_no_or_rp_id_permission("example.com"), Ok(()));
assert_eq!(
client_pin.has_no_or_rp_id_permission("another.example.com"),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
#[test]
fn test_has_no_or_rp_id_hash_permission() {
let mut env = TestEnv::new();
let mut client_pin = ClientPin::new(env.rng());
let rp_id_hash = Sha256::hash(b"example.com");
assert_eq!(
client_pin.has_no_or_rp_id_hash_permission(&rp_id_hash),
Ok(())
);
client_pin
.pin_uv_auth_token_state
.set_permissions_rp_id(Some("example.com".to_string()));
assert_eq!(
client_pin.has_no_or_rp_id_hash_permission(&rp_id_hash),
Ok(())
);
assert_eq!(
client_pin.has_no_or_rp_id_hash_permission(&[0x4A; 32]),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
#[test]
fn test_ensure_rp_id_permission() {
let mut env = TestEnv::new();
let mut client_pin = ClientPin::new(env.rng());
assert_eq!(client_pin.ensure_rp_id_permission("example.com"), Ok(()));
assert_eq!(
client_pin
.pin_uv_auth_token_state
.has_permissions_rp_id("example.com"),
Ok(())
);
assert_eq!(client_pin.ensure_rp_id_permission("example.com"), Ok(()));
assert_eq!(
client_pin.ensure_rp_id_permission("another.example.com"),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
#[test]
fn test_verify_pin_uv_auth_token() {
let mut env = TestEnv::new();
let mut client_pin = ClientPin::new(env.rng());
let message = [0xAA];
client_pin
.pin_uv_auth_token_state
.begin_using_pin_uv_auth_token(CtapInstant::new(0));
let pin_uv_auth_token_v1 = client_pin
.get_pin_protocol(PinUvAuthProtocol::V1)
.get_pin_uv_auth_token();
let pin_uv_auth_param_v1 =
authenticate_pin_uv_auth_token(pin_uv_auth_token_v1, &message, PinUvAuthProtocol::V1);
let pin_uv_auth_token_v2 = client_pin
.get_pin_protocol(PinUvAuthProtocol::V2)
.get_pin_uv_auth_token();
let pin_uv_auth_param_v2 =
authenticate_pin_uv_auth_token(pin_uv_auth_token_v2, &message, PinUvAuthProtocol::V2);
let pin_uv_auth_param_v1_from_v2_token =
authenticate_pin_uv_auth_token(pin_uv_auth_token_v2, &message, PinUvAuthProtocol::V1);
let pin_uv_auth_param_v2_from_v1_token =
authenticate_pin_uv_auth_token(pin_uv_auth_token_v1, &message, PinUvAuthProtocol::V2);
assert_eq!(
client_pin.verify_pin_uv_auth_token(
&message,
&pin_uv_auth_param_v1,
PinUvAuthProtocol::V1
),
Ok(())
);
assert_eq!(
client_pin.verify_pin_uv_auth_token(
&message,
&pin_uv_auth_param_v2,
PinUvAuthProtocol::V2
),
Ok(())
);
assert_eq!(
client_pin.verify_pin_uv_auth_token(
&message,
&pin_uv_auth_param_v1,
PinUvAuthProtocol::V2
),
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
assert_eq!(
client_pin.verify_pin_uv_auth_token(
&message,
&pin_uv_auth_param_v2,
PinUvAuthProtocol::V1
),
Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)
);
assert_eq!(
client_pin.verify_pin_uv_auth_token(
&message,
&pin_uv_auth_param_v1_from_v2_token,
PinUvAuthProtocol::V1
),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
client_pin.verify_pin_uv_auth_token(
&message,
&pin_uv_auth_param_v2_from_v1_token,
PinUvAuthProtocol::V2
),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
#[test]
fn test_verify_pin_uv_auth_token_not_in_use() {
let mut env = TestEnv::new();
let client_pin = ClientPin::new(env.rng());
let message = [0xAA];
let pin_uv_auth_token_v1 = client_pin
.get_pin_protocol(PinUvAuthProtocol::V1)
.get_pin_uv_auth_token();
let pin_uv_auth_param_v1 =
authenticate_pin_uv_auth_token(pin_uv_auth_token_v1, &message, PinUvAuthProtocol::V1);
assert_eq!(
client_pin.verify_pin_uv_auth_token(
&message,
&pin_uv_auth_param_v1,
PinUvAuthProtocol::V1
),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
#[test]
fn test_reset() {
let mut env = TestEnv::new();
let mut client_pin = ClientPin::new(env.rng());
let public_key_v1 = client_pin.pin_protocol_v1.get_public_key();
let public_key_v2 = client_pin.pin_protocol_v2.get_public_key();
let token_v1 = *client_pin.pin_protocol_v1.get_pin_uv_auth_token();
let token_v2 = *client_pin.pin_protocol_v2.get_pin_uv_auth_token();
client_pin.pin_uv_auth_token_state.set_permissions(0xFF);
client_pin
.pin_uv_auth_token_state
.set_permissions_rp_id(Some(String::from("example.com")));
client_pin.reset(env.rng());
assert_ne!(public_key_v1, client_pin.pin_protocol_v1.get_public_key());
assert_ne!(public_key_v2, client_pin.pin_protocol_v2.get_public_key());
assert_ne!(
&token_v1,
client_pin.pin_protocol_v1.get_pin_uv_auth_token()
);
assert_ne!(
&token_v2,
client_pin.pin_protocol_v2.get_pin_uv_auth_token()
);
for permission in PinPermission::into_enum_iter() {
assert_eq!(
client_pin.has_permission(permission),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
assert_eq!(client_pin.has_no_rp_id_permission(), Ok(()));
}
#[test]
fn test_update_timeouts() {
let (mut client_pin, mut params) = create_client_pin_and_parameters(
PinUvAuthProtocol::V2,
ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions,
);
let mut env = TestEnv::new();
set_standard_pin(&mut env);
params.permissions = Some(0xFF);
assert!(client_pin
.process_command(&mut env, params, CtapInstant::new(0))
.is_ok());
for permission in PinPermission::into_enum_iter() {
assert_eq!(
client_pin
.pin_uv_auth_token_state
.has_permission(permission),
Ok(())
);
}
assert_eq!(
client_pin
.pin_uv_auth_token_state
.has_permissions_rp_id("example.com"),
Ok(())
);
let timeout = CtapInstant::new(0) + Milliseconds::new(30001_u32);
client_pin.update_timeouts(timeout);
for permission in PinPermission::into_enum_iter() {
assert_eq!(
client_pin
.pin_uv_auth_token_state
.has_permission(permission),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
assert_eq!(
client_pin
.pin_uv_auth_token_state
.has_permissions_rp_id("example.com"),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
#[test]
fn test_clear_token_flags() {
let (mut client_pin, mut params) = create_client_pin_and_parameters(
PinUvAuthProtocol::V2,
ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions,
);
let mut env = TestEnv::new();
set_standard_pin(&mut env);
params.permissions = Some(0xFF);
assert!(client_pin
.process_command(&mut env, params, CtapInstant::new(0))
.is_ok());
for permission in PinPermission::into_enum_iter() {
assert_eq!(
client_pin
.pin_uv_auth_token_state
.has_permission(permission),
Ok(())
);
}
assert_eq!(client_pin.check_user_verified_flag(), Ok(()));
client_pin.clear_token_flags();
assert_eq!(
client_pin
.pin_uv_auth_token_state
.has_permission(PinPermission::CredentialManagement),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
assert_eq!(
client_pin
.pin_uv_auth_token_state
.has_permission(PinPermission::LargeBlobWrite),
Ok(())
);
assert_eq!(
client_pin.check_user_verified_flag(),
Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)
);
}
}