Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New key wrapping API #642

Merged
merged 5 commits into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 50 additions & 122 deletions libraries/opensk/src/api/key_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,14 @@
// limitations under the License.

use crate::api::crypto::aes256::Aes256;
use crate::api::crypto::ecdsa::SecretKey as _;
use crate::api::crypto::hmac256::Hmac256;
use crate::api::crypto::HASH_SIZE;
use crate::api::private_key::PrivateKey;
use crate::ctap::crypto_wrapper::{aes256_cbc_decrypt, aes256_cbc_encrypt};
use crate::ctap::data_formats::CredentialProtectionPolicy;
use crate::ctap::secret::Secret;
use crate::ctap::{cbor_read, cbor_write};
use crate::env::{AesKey, EcdsaSk, Env, Hmac};
use crate::env::{AesKey, Env, Hmac};
use alloc::vec;
use alloc::vec::Vec;
use core::convert::{TryFrom, TryInto};
Expand All @@ -30,14 +29,13 @@ use rand_core::RngCore;
use sk_cbor as cbor;
use sk_cbor::{cbor_map_options, destructure_cbor_map};

const LEGACY_CREDENTIAL_ID_SIZE: usize = 112;
// CBOR credential IDs consist of
// - 1 byte : version number
// - 16 bytes: initialization vector for AES-256,
// - 192 bytes: encrypted block of the key handle cbor,
// - 32 bytes: HMAC-SHA256 over everything else.
pub const CBOR_CREDENTIAL_ID_SIZE: usize = 241;
const MIN_CREDENTIAL_ID_SIZE: usize = LEGACY_CREDENTIAL_ID_SIZE;
const MIN_CREDENTIAL_ID_SIZE: usize = CBOR_CREDENTIAL_ID_SIZE;
pub(crate) const MAX_CREDENTIAL_ID_SIZE: usize = CBOR_CREDENTIAL_ID_SIZE;

pub const CBOR_CREDENTIAL_ID_VERSION: u8 = 0x01;
Expand Down Expand Up @@ -76,6 +74,14 @@ pub trait KeyStore {
/// This function should be a no-op if the key store is already initialized.
fn init(&mut self) -> Result<(), Error>;

/// Key to wrap (secret) data.
///
/// Useful for encrypting data before
/// - writing it to persistent storage,
/// - CBOR encoding it,
/// - doing anything that does not support [`Secret`].
fn wrap_key<E: Env>(&mut self) -> Result<AesKey<E>, Error>;

/// Encodes a credential as a binary strings.
///
/// The output is encrypted and authenticated. Since the wrapped credentials are passed to the
Expand Down Expand Up @@ -109,14 +115,6 @@ pub trait KeyStore {
/// 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.
fn derive_ecdsa(&mut self, seed: &[u8; 32]) -> Result<Secret<[u8; 32]>, Error>;

/// Generates a seed to derive an ECDSA private key.
fn generate_ecdsa_seed(&mut self) -> Result<Secret<[u8; 32]>, Error>;

/// Resets the key store.
fn reset(&mut self) -> Result<(), Error>;
}
Expand All @@ -138,14 +136,23 @@ impl<T: Helper> KeyStore for T {
Ok(())
}

fn wrap_key<E: Env>(&mut self) -> Result<AesKey<E>, Error> {
Ok(AesKey::<E>::new(&get_master_keys(self)?.encryption))
}

/// Encrypts the given credential source data into a credential ID.
///
/// Other information, such as a user name, are not stored. Since encrypted credential IDs are
/// stored server-side, this information is already available (unencrypted).
fn wrap_credential(&mut self, credential: CredentialSource) -> Result<Vec<u8>, Error> {
let mut payload = Vec::new();
let wrap_key = self.wrap_key::<T>()?;
let private_key_cbor = credential
.private_key
.to_cbor::<T>(self.rng(), &wrap_key)
.map_err(|_| Error)?;
let cbor = cbor_map_options! {
CredentialSourceField::PrivateKey => credential.private_key,
CredentialSourceField::PrivateKey => private_key_cbor,
CredentialSourceField::RpIdHash => credential.rp_id_hash,
CredentialSourceField::CredProtectPolicy => credential.cred_protect_policy,
CredentialSourceField::CredBlob => credential.cred_blob,
Expand All @@ -155,7 +162,7 @@ impl<T: Helper> KeyStore for T {
let master_keys = get_master_keys(self)?;
let aes_key = AesKey::<T>::new(&master_keys.encryption);
let encrypted_payload =
aes256_cbc_encrypt(self, &aes_key, &payload, true).map_err(|_| Error)?;
aes256_cbc_encrypt::<T>(self.rng(), &aes_key, &payload, true).map_err(|_| Error)?;
let mut credential_id = encrypted_payload;
credential_id.insert(0, CBOR_CREDENTIAL_ID_VERSION);

Expand All @@ -175,12 +182,6 @@ impl<T: Helper> KeyStore for T {
/// - the format does not match any known versions, or
/// - the HMAC test fails.
///
/// For v0 (legacy U2F) the credential ID consists of:
/// - 16 bytes: initialization vector for AES-256,
/// - 32 bytes: encrypted ECDSA private key for the credential,
/// - 32 bytes: encrypted relying party ID hashed with SHA256,
/// - 32 bytes: HMAC-SHA256 over everything else.
///
/// For v1 (CBOR) the credential ID consists of:
/// - 1 byte : version number,
/// - 16 bytes: initialization vector for AES-256,
Expand All @@ -204,21 +205,18 @@ impl<T: Helper> KeyStore for T {
return Ok(None);
}

let credential_source = if bytes.len() == LEGACY_CREDENTIAL_ID_SIZE {
decrypt_legacy_credential_id::<T>(&master_keys.encryption, &bytes[..hmac_message_size])?
} else {
match bytes[0] {
CBOR_CREDENTIAL_ID_VERSION => {
if bytes.len() != CBOR_CREDENTIAL_ID_SIZE {
return Ok(None);
}
decrypt_cbor_credential_id::<T>(
&master_keys.encryption,
&bytes[1..hmac_message_size],
)?
let credential_source = match bytes[0] {
CBOR_CREDENTIAL_ID_VERSION => {
if bytes.len() != CBOR_CREDENTIAL_ID_SIZE {
return Ok(None);
}
_ => return Ok(None),
decrypt_cbor_credential_id::<T>(
self,
&master_keys.encryption,
&bytes[1..hmac_message_size],
)?
}
_ => return Ok(None),
};

if let Some(credential_source) = &credential_source {
Expand All @@ -241,24 +239,8 @@ impl<T: Helper> KeyStore for T {
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),
Some(_) => {
let mut derived: Secret<[u8; 32]> = Secret::default();
derived.copy_from_slice(seed);
Ok(derived)
}
}
}

fn generate_ecdsa_seed(&mut self) -> Result<Secret<[u8; 32]>, Error> {
let mut seed = Secret::default();
EcdsaSk::<T>::random(self.rng()).to_slice(&mut seed);
Ok(seed)
}

fn reset(&mut self) -> Result<(), Error> {
// The storage also removes `STORAGE_KEY`, but this makes KeyStore more self-sufficient.
Ok(self.store().remove(STORAGE_KEY)?)
}
}
Expand Down Expand Up @@ -333,31 +315,8 @@ fn remove_padding(data: &[u8]) -> Result<&[u8], Error> {
Ok(&data[..data.len() - pad_length as usize])
}

fn decrypt_legacy_credential_id<E: Env>(
encryption_key_bytes: &[u8; 32],
bytes: &[u8],
) -> Result<Option<CredentialSource>, Error> {
let aes_key = AesKey::<E>::new(encryption_key_bytes);
let plaintext = aes256_cbc_decrypt::<E>(&aes_key, bytes, true)
.map_err(|_| Error)?
.expose_secret_to_vec();
if plaintext.len() != 64 {
return Ok(None);
}
let private_key = if let Some(key) = PrivateKey::new_ecdsa_from_bytes(&plaintext[..32]) {
key
} else {
return Ok(None);
};
Ok(Some(CredentialSource {
private_key,
rp_id_hash: plaintext[32..64].try_into().unwrap(),
cred_protect_policy: None,
cred_blob: None,
}))
}

fn decrypt_cbor_credential_id<E: Env>(
env: &mut E,
encryption_key_bytes: &[u8; 32],
bytes: &[u8],
) -> Result<Option<CredentialSource>, Error> {
Expand All @@ -376,7 +335,9 @@ fn decrypt_cbor_credential_id<E: Env>(
}
Ok(match (private_key, rp_id_hash) {
(Some(private_key), Some(rp_id_hash)) => {
let private_key = PrivateKey::try_from(private_key).map_err(|_| Error)?;
let wrap_key = env.key_store().wrap_key::<E>()?;
let private_key =
PrivateKey::from_cbor::<E>(&wrap_key, private_key).map_err(|_| Error)?;
let rp_id_hash = extract_byte_string(rp_id_hash)?;
if rp_id_hash.len() != 32 {
return Err(Error);
Expand Down Expand Up @@ -420,9 +381,11 @@ fn extract_map(cbor_value: cbor::Value) -> Result<Vec<(cbor::Value, cbor::Value)
#[cfg(test)]
mod test {
use super::*;
use crate::api::crypto::ecdsa::SecretKey;
use crate::api::customization::Customization;
use crate::ctap::data_formats::SignatureAlgorithm;
use crate::env::test::TestEnv;
use crate::env::EcdsaSk;

const UNSUPPORTED_CREDENTIAL_ID_VERSION: u8 = 0x80;

Expand All @@ -437,16 +400,24 @@ mod test {
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();
let ecdsa_key = key_store.derive_ecdsa(&ecdsa_seed).unwrap();
assert_eq!(key_store.derive_ecdsa(&ecdsa_seed), Ok(ecdsa_key));
// Same for wrap key.
let wrap_key = key_store.wrap_key::<TestEnv>().unwrap();
let mut test_block = [0x33; 16];
wrap_key.encrypt_block(&mut test_block);
let new_wrap_key = key_store.wrap_key::<TestEnv>().unwrap();
let mut new_test_block = [0x33; 16];
new_wrap_key.encrypt_block(&mut new_test_block);
assert_eq!(&new_test_block, &test_block);

// Master keys change after reset. We don't require this for ECDSA seeds because it's not
// the case, but it might be better.
key_store.reset().unwrap();
assert_ne!(&key_store.cred_random(false).unwrap(), &cred_random_no_uv);
assert_ne!(&key_store.cred_random(true).unwrap(), &cred_random_with_uv);
let new_wrap_key = key_store.wrap_key::<TestEnv>().unwrap();
let mut new_test_block = [0x33; 16];
new_wrap_key.encrypt_block(&mut new_test_block);
assert_ne!(&new_test_block, &test_block);
}

#[test]
Expand Down Expand Up @@ -585,49 +556,6 @@ mod test {
test_wrap_unwrap_credential_missing_blocks(SignatureAlgorithm::Eddsa);
}

/// This is a copy of the function that genereated deprecated key handles.
fn legacy_encrypt_to_credential_id(
env: &mut TestEnv,
private_key: EcdsaSk<TestEnv>,
application: &[u8; 32],
) -> Result<Vec<u8>, Error> {
let master_keys = get_master_keys(env).unwrap();
let aes_key = AesKey::<TestEnv>::new(&master_keys.encryption);
let hmac_key = master_keys.authentication;
let mut plaintext = [0; 64];
private_key.to_slice(array_mut_ref!(plaintext, 0, 32));
plaintext[32..64].copy_from_slice(application);

let mut encrypted_id =
aes256_cbc_encrypt(env, &aes_key, &plaintext, true).map_err(|_| Error)?;
let mut id_hmac = [0; HASH_SIZE];
Hmac::<TestEnv>::mac(&hmac_key, &encrypted_id[..], &mut id_hmac);
encrypted_id.extend(&id_hmac);
Ok(encrypted_id)
}

#[test]
fn test_encrypt_decrypt_credential_legacy() {
let mut env = TestEnv::default();
let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256);
let rp_id_hash = [0x55; 32];
let credential_source = CredentialSource {
private_key,
rp_id_hash,
cred_protect_policy: None,
cred_blob: None,
};
let ecdsa_key = credential_source.private_key.ecdsa_key(&mut env).unwrap();
let credential_id =
legacy_encrypt_to_credential_id(&mut env, ecdsa_key, &rp_id_hash).unwrap();
let unwrapped = env
.key_store()
.unwrap_credential(&credential_id, &rp_id_hash)
.unwrap()
.unwrap();
assert_eq!(credential_source, unwrapped);
}

#[test]
fn test_wrap_credential_size() {
let mut env = TestEnv::default();
Expand Down
Loading
Loading