Moves CTAP secrets to the key store (#617)
The PIN hash can be encrypted and decrypted, and CredRandom is part of the master secrets.
This commit is contained in:
@@ -23,12 +23,26 @@ use rand_core::RngCore;
|
||||
///
|
||||
/// Implementations may use the environment store: [`STORAGE_KEY`] is reserved for this usage.
|
||||
pub trait KeyStore {
|
||||
/// Initializes the key store (if needed).
|
||||
///
|
||||
/// This function should be a no-op if the key store is already initialized.
|
||||
fn init(&mut self) -> Result<(), Error>;
|
||||
|
||||
/// Returns the AES key for key handles encryption.
|
||||
fn key_handle_encryption(&mut self) -> Result<Secret<[u8; 32]>, Error>;
|
||||
|
||||
/// Returns the key for key handles authentication.
|
||||
fn key_handle_authentication(&mut self) -> Result<Secret<[u8; 32]>, Error>;
|
||||
|
||||
/// Returns the key for the CredRandom feature.
|
||||
fn cred_random(&mut self, has_uv: bool) -> Result<Secret<[u8; 32]>, Error>;
|
||||
|
||||
/// Encrypts a PIN hash.
|
||||
fn encrypt_pin_hash(&mut self, plain: &[u8; 16]) -> Result<[u8; 16], Error>;
|
||||
|
||||
/// Decrypts a PIN hash.
|
||||
fn decrypt_pin_hash(&mut self, cipher: &[u8; 16]) -> Result<Secret<[u8; 16]>, Error>;
|
||||
|
||||
/// Derives an ECDSA private key from a seed.
|
||||
///
|
||||
/// The result is big-endian.
|
||||
@@ -54,6 +68,10 @@ pub const STORAGE_KEY: usize = 2046;
|
||||
pub trait Helper: Env {}
|
||||
|
||||
impl<T: Helper> KeyStore for T {
|
||||
fn init(&mut self) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn key_handle_encryption(&mut self) -> Result<Secret<[u8; 32]>, Error> {
|
||||
Ok(get_master_keys(self)?.encryption.clone())
|
||||
}
|
||||
@@ -62,6 +80,18 @@ impl<T: Helper> KeyStore for T {
|
||||
Ok(get_master_keys(self)?.authentication.clone())
|
||||
}
|
||||
|
||||
fn cred_random(&mut self, has_uv: bool) -> Result<Secret<[u8; 32]>, Error> {
|
||||
Ok(get_master_keys(self)?.cred_random[has_uv as usize].clone())
|
||||
}
|
||||
|
||||
fn encrypt_pin_hash(&mut self, plain: &[u8; 16]) -> Result<[u8; 16], Error> {
|
||||
Ok(*plain)
|
||||
}
|
||||
|
||||
fn decrypt_pin_hash(&mut self, cipher: &[u8; 16]) -> Result<Secret<[u8; 16]>, Error> {
|
||||
Ok(Secret::from_exposed_secret(*cipher))
|
||||
}
|
||||
|
||||
fn derive_ecdsa(&mut self, seed: &[u8; 32]) -> Result<Secret<[u8; 32]>, Error> {
|
||||
match EcdsaSk::<T>::from_slice(seed) {
|
||||
None => Err(Error),
|
||||
@@ -91,14 +121,17 @@ struct MasterKeys {
|
||||
|
||||
/// Master authentication key.
|
||||
authentication: Secret<[u8; 32]>,
|
||||
|
||||
/// Cred random keys (without and with UV in that order).
|
||||
cred_random: [Secret<[u8; 32]>; 2],
|
||||
}
|
||||
|
||||
fn get_master_keys(env: &mut impl Env) -> Result<MasterKeys, Error> {
|
||||
let master_keys = match env.store().find(STORAGE_KEY)? {
|
||||
Some(x) if x.len() == 64 => x,
|
||||
Some(x) if x.len() == 128 => x,
|
||||
Some(_) => return Err(Error),
|
||||
None => {
|
||||
let mut master_keys = vec![0; 64];
|
||||
let mut master_keys = vec![0; 128];
|
||||
env.rng().fill_bytes(&mut master_keys);
|
||||
env.store().insert(STORAGE_KEY, &master_keys)?;
|
||||
master_keys
|
||||
@@ -108,9 +141,14 @@ fn get_master_keys(env: &mut impl Env) -> Result<MasterKeys, Error> {
|
||||
encryption.copy_from_slice(array_ref![master_keys, 0, 32]);
|
||||
let mut authentication: Secret<[u8; 32]> = Secret::default();
|
||||
authentication.copy_from_slice(array_ref![master_keys, 32, 32]);
|
||||
let mut cred_random_no_uv: Secret<[u8; 32]> = Secret::default();
|
||||
cred_random_no_uv.copy_from_slice(array_ref![master_keys, 64, 32]);
|
||||
let mut cred_random_with_uv: Secret<[u8; 32]> = Secret::default();
|
||||
cred_random_with_uv.copy_from_slice(array_ref![master_keys, 96, 32]);
|
||||
Ok(MasterKeys {
|
||||
encryption,
|
||||
authentication,
|
||||
cred_random: [cred_random_no_uv, cred_random_with_uv],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -133,11 +171,15 @@ mod test {
|
||||
// Master keys are well-defined and stable.
|
||||
let encryption_key = key_store.key_handle_encryption().unwrap();
|
||||
let authentication_key = key_store.key_handle_authentication().unwrap();
|
||||
let cred_random_no_uv = key_store.cred_random(false).unwrap();
|
||||
let cred_random_with_uv = key_store.cred_random(true).unwrap();
|
||||
assert_eq!(&key_store.key_handle_encryption().unwrap(), &encryption_key);
|
||||
assert_eq!(
|
||||
&key_store.key_handle_authentication().unwrap(),
|
||||
&authentication_key
|
||||
);
|
||||
assert_eq!(&key_store.cred_random(false).unwrap(), &cred_random_no_uv);
|
||||
assert_eq!(&key_store.cred_random(true).unwrap(), &cred_random_with_uv);
|
||||
|
||||
// ECDSA seeds are well-defined and stable.
|
||||
let ecdsa_seed = key_store.generate_ecdsa_seed().unwrap();
|
||||
@@ -152,5 +194,19 @@ mod test {
|
||||
key_store.key_handle_authentication().unwrap(),
|
||||
authentication_key
|
||||
);
|
||||
assert_ne!(&key_store.cred_random(false).unwrap(), &cred_random_no_uv);
|
||||
assert_ne!(&key_store.cred_random(true).unwrap(), &cred_random_with_uv);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pin_hash_encrypt_decrypt() {
|
||||
let mut env = TestEnv::default();
|
||||
let key_store = env.key_store();
|
||||
assert_eq!(key_store.init(), Ok(()));
|
||||
|
||||
let pin_hash = [0x55; 16];
|
||||
let encrypted = key_store.encrypt_pin_hash(&pin_hash).unwrap();
|
||||
let decrypted = key_store.decrypt_pin_hash(&encrypted).unwrap();
|
||||
assert_eq!(pin_hash, *decrypted);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ use crate::api::crypto::ecdh::SecretKey as _;
|
||||
use crate::api::crypto::hmac256::Hmac256;
|
||||
use crate::api::crypto::sha256::Sha256;
|
||||
use crate::api::customization::Customization;
|
||||
use crate::api::key_store::KeyStore;
|
||||
use crate::ctap::storage;
|
||||
#[cfg(test)]
|
||||
use crate::env::EcdhSk;
|
||||
@@ -98,6 +99,9 @@ fn check_and_store_new_pin<E: Env>(
|
||||
}
|
||||
let mut pin_hash = Secret::default();
|
||||
Sha::<E>::digest_mut(&pin, &mut pin_hash);
|
||||
let pin_hash = env
|
||||
.key_store()
|
||||
.encrypt_pin_hash(array_ref![pin_hash, 0, PIN_AUTH_LENGTH])?;
|
||||
// The PIN length is always < PIN_PADDED_LENGTH < 256.
|
||||
storage::set_pin(
|
||||
env,
|
||||
@@ -182,6 +186,7 @@ impl<E: Env> ClientPin<E> {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED);
|
||||
}
|
||||
storage::decr_pin_retries(env)?;
|
||||
let pin_hash = env.key_store().decrypt_pin_hash(&pin_hash)?;
|
||||
let pin_hash_dec = shared_secret
|
||||
.decrypt(&pin_hash_enc)
|
||||
.map_err(|_| Ctap2StatusCode::CTAP2_ERR_PIN_INVALID)?;
|
||||
|
||||
@@ -70,6 +70,7 @@ use crate::api::crypto::hkdf256::Hkdf256;
|
||||
use crate::api::crypto::sha256::Sha256;
|
||||
use crate::api::crypto::HASH_SIZE;
|
||||
use crate::api::customization::Customization;
|
||||
use crate::api::key_store::KeyStore;
|
||||
use crate::api::rng::Rng;
|
||||
use crate::api::user_presence::{UserPresence, UserPresenceError};
|
||||
use crate::env::{EcdsaSk, Env, Hkdf, Sha};
|
||||
@@ -956,9 +957,9 @@ impl<E: Env> CtapState<E> {
|
||||
) -> Result<Secret<[u8; HASH_SIZE]>, Ctap2StatusCode> {
|
||||
let private_key_bytes = private_key.to_bytes();
|
||||
let salt = array_ref!(private_key_bytes, 0, 32);
|
||||
let key = storage::cred_random_secret(env, has_uv)?;
|
||||
let key = env.key_store().cred_random(has_uv)?;
|
||||
let mut output = Secret::default();
|
||||
Hkdf::<E>::hkdf_256(&key, salt, b"credRandom", &mut output);
|
||||
Hkdf::<E>::hkdf_256(&*key, salt, b"credRandom", &mut output);
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ mod key;
|
||||
use crate::api::attestation_store::{self, AttestationStore};
|
||||
use crate::api::customization::Customization;
|
||||
use crate::api::key_store::KeyStore;
|
||||
use crate::api::rng::Rng;
|
||||
use crate::ctap::client_pin::PIN_AUTH_LENGTH;
|
||||
use crate::ctap::data_formats::{
|
||||
extract_array, extract_text_string, PublicKeyCredentialSource, PublicKeyCredentialUserEntity,
|
||||
@@ -45,15 +44,7 @@ struct PinProperties {
|
||||
|
||||
/// Initializes the store by creating missing objects.
|
||||
pub fn init(env: &mut impl Env) -> Result<(), Ctap2StatusCode> {
|
||||
// Generate and store the CredRandom secrets if they are missing.
|
||||
if env.store().find_handle(key::CRED_RANDOM_SECRET)?.is_none() {
|
||||
let cred_random_with_uv = env.rng().gen_uniform_u8x32();
|
||||
let cred_random_without_uv = env.rng().gen_uniform_u8x32();
|
||||
let mut cred_random = Vec::with_capacity(64);
|
||||
cred_random.extend_from_slice(&cred_random_without_uv);
|
||||
cred_random.extend_from_slice(&cred_random_with_uv);
|
||||
env.store().insert(key::CRED_RANDOM_SECRET, &cred_random)?;
|
||||
}
|
||||
env.key_store().init()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -255,19 +246,6 @@ pub fn incr_global_signature_counter(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the CredRandom secret.
|
||||
pub fn cred_random_secret(env: &mut impl Env, has_uv: bool) -> Result<[u8; 32], Ctap2StatusCode> {
|
||||
let cred_random_secret = env
|
||||
.store()
|
||||
.find(key::CRED_RANDOM_SECRET)?
|
||||
.ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?;
|
||||
if cred_random_secret.len() != 64 {
|
||||
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR);
|
||||
}
|
||||
let offset = if has_uv { 32 } else { 0 };
|
||||
Ok(*array_ref![cred_random_secret, offset, 32])
|
||||
}
|
||||
|
||||
/// Reads the PIN properties and wraps them into PinProperties.
|
||||
fn pin_properties(env: &mut impl Env) -> Result<Option<PinProperties>, Ctap2StatusCode> {
|
||||
let pin_properties = match env.store().find(key::PIN_PROPERTIES)? {
|
||||
@@ -620,6 +598,7 @@ fn serialize_min_pin_length_rp_ids(rp_ids: Vec<String>) -> Result<Vec<u8>, Ctap2
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::api::attestation_store::{self, Attestation, AttestationStore};
|
||||
use crate::api::rng::Rng;
|
||||
use crate::ctap::crypto_wrapper::PrivateKey;
|
||||
use crate::ctap::data_formats::{
|
||||
CredentialProtectionPolicy, PublicKeyCredentialSource, PublicKeyCredentialType,
|
||||
@@ -846,28 +825,6 @@ mod test {
|
||||
assert_eq!(found_credential, Some(expected_credential));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cred_random_secret() {
|
||||
let mut env = TestEnv::default();
|
||||
init(&mut env).unwrap();
|
||||
|
||||
// CredRandom secrets stay the same within the same CTAP reset cycle.
|
||||
let cred_random_with_uv_1 = cred_random_secret(&mut env, true).unwrap();
|
||||
let cred_random_without_uv_1 = cred_random_secret(&mut env, false).unwrap();
|
||||
let cred_random_with_uv_2 = cred_random_secret(&mut env, true).unwrap();
|
||||
let cred_random_without_uv_2 = cred_random_secret(&mut env, false).unwrap();
|
||||
assert_eq!(cred_random_with_uv_1, cred_random_with_uv_2);
|
||||
assert_eq!(cred_random_without_uv_1, cred_random_without_uv_2);
|
||||
|
||||
// CredRandom secrets change after reset. This test may fail if the random generator produces the
|
||||
// same keys.
|
||||
reset(&mut env).unwrap();
|
||||
let cred_random_with_uv_3 = cred_random_secret(&mut env, true).unwrap();
|
||||
let cred_random_without_uv_3 = cred_random_secret(&mut env, false).unwrap();
|
||||
assert!(cred_random_with_uv_1 != cred_random_with_uv_3);
|
||||
assert!(cred_random_without_uv_1 != cred_random_without_uv_3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pin_hash_and_length() {
|
||||
let mut env = TestEnv::default();
|
||||
|
||||
@@ -101,7 +101,7 @@ make_partition! {
|
||||
FORCE_PIN_CHANGE = 2040;
|
||||
|
||||
/// The secret of the CredRandom feature.
|
||||
CRED_RANDOM_SECRET = 2041;
|
||||
_CRED_RANDOM_SECRET = 2041;
|
||||
|
||||
/// List of RP IDs allowed to read the minimum PIN length.
|
||||
MIN_PIN_LENGTH_RP_IDS = 2042;
|
||||
|
||||
Reference in New Issue
Block a user