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:
kaczmarczyck
2023-04-21 16:32:58 +02:00
committed by GitHub
parent a88a1b2a22
commit bcd382e5e9
5 changed files with 69 additions and 50 deletions

View File

@@ -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);
}
}

View File

@@ -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)?;

View File

@@ -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)
}

View File

@@ -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();

View File

@@ -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;