diff --git a/libraries/opensk/src/api/key_store.rs b/libraries/opensk/src/api/key_store.rs index 90d6d0b..91cbb53 100644 --- a/libraries/opensk/src/api/key_store.rs +++ b/libraries/opensk/src/api/key_store.rs @@ -12,12 +12,60 @@ // See the License for the specific language governing permissions and // 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::env::{EcdsaSk, Env}; +use crate::ctap::{cbor_read, cbor_write}; +use crate::env::{AesKey, EcdsaSk, Env, Hmac}; use alloc::vec; +use alloc::vec::Vec; +use core::convert::{TryFrom, TryInto}; use persistent_store::StoreError; 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; +pub(crate) const MAX_CREDENTIAL_ID_SIZE: usize = CBOR_CREDENTIAL_ID_SIZE; + +pub const CBOR_CREDENTIAL_ID_VERSION: u8 = 0x01; +const MAX_PADDING_LENGTH: u8 = 0xBF; + +/// Stored data for credentials. +#[derive(Clone, Debug)] +#[cfg_attr(test, derive(PartialEq, Eq))] +pub struct CredentialSource { + pub private_key: PrivateKey, + pub rp_id_hash: [u8; 32], + pub cred_protect_policy: Option, + pub cred_blob: Option>, +} + +/// CBOR map keys for serialized credential IDs. +enum CredentialSourceField { + PrivateKey = 0, + RpIdHash = 1, + CredProtectPolicy = 2, + CredBlob = 3, +} + +impl From for cbor::Value { + fn from(field: CredentialSourceField) -> cbor::Value { + (field as u64).into() + } +} /// Provides storage for secret keys. /// @@ -28,11 +76,29 @@ pub trait KeyStore { /// 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, Error>; + /// Encodes a credential as a binary strings. + /// + /// The output is encrypted and authenticated. Since the wrapped credentials are passed to the + /// relying party, the choice for credential wrapping impacts privacy. Looking at their size and + /// structure, a relying party can guess the authenticator model that produced it. + /// + /// A credential ID that imitates the default needs the following structure: + /// - The length is [`CBOR_CREDENTIAL_ID_SIZE`]. + /// - The first byte is the version. The latest version is [`CBOR_CREDENTIAL_ID_VERSION`]. + /// - All other bytes appear to be drawn from a uniform random distribution. + /// + /// Without attestation, a relying party can't distinguish such credentials from other OpenSK + /// implementations. + fn wrap_credential(&mut self, credential: CredentialSource) -> Result, Error>; - /// Returns the key for key handles authentication. - fn key_handle_authentication(&mut self) -> Result, Error>; + /// Decodes the credential. + /// + /// Returns None if the data was not created by this authenticator. + fn unwrap_credential( + &mut self, + bytes: &[u8], + rp_id_hash: &[u8], + ) -> Result, Error>; /// Returns the key for the CredRandom feature. fn cred_random(&mut self, has_uv: bool) -> Result, Error>; @@ -72,12 +138,98 @@ impl KeyStore for T { Ok(()) } - fn key_handle_encryption(&mut self) -> Result, Error> { - Ok(get_master_keys(self)?.encryption.clone()) + /// 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, Error> { + let mut payload = Vec::new(); + let cbor = cbor_map_options! { + CredentialSourceField::PrivateKey => credential.private_key, + CredentialSourceField::RpIdHash => credential.rp_id_hash, + CredentialSourceField::CredProtectPolicy => credential.cred_protect_policy, + CredentialSourceField::CredBlob => credential.cred_blob, + }; + cbor_write(cbor, &mut payload).map_err(|_| Error)?; + add_padding(&mut payload)?; + let master_keys = get_master_keys(self)?; + let aes_key = AesKey::::new(&master_keys.encryption); + let encrypted_payload = + aes256_cbc_encrypt(self, &aes_key, &payload, true).map_err(|_| Error)?; + let mut credential_id = encrypted_payload; + credential_id.insert(0, CBOR_CREDENTIAL_ID_VERSION); + + let mut id_hmac = [0; HASH_SIZE]; + Hmac::::mac( + &master_keys.authentication, + &credential_id[..], + &mut id_hmac, + ); + credential_id.extend(&id_hmac); + Ok(credential_id) } - fn key_handle_authentication(&mut self) -> Result, Error> { - Ok(get_master_keys(self)?.authentication.clone()) + /// Decrypts the given credential ID, populating only the recorded fields. + /// + /// Returns None if + /// - 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, + /// - 192 bytes: encrypted CBOR-encoded credential source fields, + /// - 32 bytes: HMAC-SHA256 over everything else. + fn unwrap_credential( + &mut self, + bytes: &[u8], + rp_id_hash: &[u8], + ) -> Result, Error> { + if bytes.len() < MIN_CREDENTIAL_ID_SIZE { + return Ok(None); + } + let hmac_message_size = bytes.len() - 32; + let master_keys = get_master_keys(self)?; + if !Hmac::::verify( + &master_keys.authentication, + &bytes[..hmac_message_size], + array_ref![bytes, hmac_message_size, 32], + ) { + return Ok(None); + } + + let credential_source = if bytes.len() == LEGACY_CREDENTIAL_ID_SIZE { + decrypt_legacy_credential_id::( + &*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::( + &*master_keys.encryption, + &bytes[1..hmac_message_size], + )? + } + _ => return Ok(None), + } + }; + + if let Some(credential_source) = &credential_source { + if rp_id_hash != credential_source.rp_id_hash { + return Ok(None); + } + } + Ok(credential_source) } fn cred_random(&mut self, has_uv: bool) -> Result, Error> { @@ -152,32 +304,139 @@ fn get_master_keys(env: &mut impl Env) -> Result { }) } +/// Pad data to MAX_PADDING_LENGTH+1 (192) bytes using PKCS padding scheme. +/// +/// Let N = 192 - data.len(), the PKCS padding scheme would pad N bytes of N after the data. +fn add_padding(data: &mut Vec) -> Result<(), Error> { + // The data should be between 1 to MAX_PADDING_LENGTH bytes for the padding scheme to be valid. + if data.is_empty() || data.len() > MAX_PADDING_LENGTH as usize { + return Err(Error); + } + let pad_length = MAX_PADDING_LENGTH - (data.len() as u8 - 1); + data.extend(core::iter::repeat(pad_length).take(pad_length as usize)); + Ok(()) +} + +fn remove_padding(data: &[u8]) -> Result<&[u8], Error> { + if data.len() != MAX_PADDING_LENGTH as usize + 1 { + // This is an internal error instead of corrupted credential ID which we should just ignore because + // we've already checked that the HMAC matched. + return Err(Error); + } + let pad_length = *data.last().unwrap(); + if pad_length == 0 || pad_length > MAX_PADDING_LENGTH { + return Err(Error); + } + if !data[(data.len() - pad_length as usize)..] + .iter() + .all(|x| *x == pad_length) + { + return Err(Error); + } + Ok(&data[..data.len() - pad_length as usize]) +} + +fn decrypt_legacy_credential_id( + encryption_key_bytes: &[u8; 32], + bytes: &[u8], +) -> Result, Error> { + let aes_key = AesKey::::new(encryption_key_bytes); + let plaintext = aes256_cbc_decrypt::(&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( + encryption_key_bytes: &[u8; 32], + bytes: &[u8], +) -> Result, Error> { + let aes_key = AesKey::::new(encryption_key_bytes); + let plaintext = aes256_cbc_decrypt::(&aes_key, bytes, true).map_err(|_| Error)?; + let unpadded = remove_padding(&plaintext)?; + + let cbor_credential_source = cbor_read(unpadded).map_err(|_| Error)?; + destructure_cbor_map! { + let { + CredentialSourceField::PrivateKey => private_key, + CredentialSourceField::RpIdHash => rp_id_hash, + CredentialSourceField::CredProtectPolicy => cred_protect_policy, + CredentialSourceField::CredBlob => cred_blob, + } = extract_map(cbor_credential_source)?; + } + 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 rp_id_hash = extract_byte_string(rp_id_hash)?; + if rp_id_hash.len() != 32 { + return Err(Error); + } + let cred_protect_policy = cred_protect_policy + .map(CredentialProtectionPolicy::try_from) + .transpose() + .map_err(|_| Error)?; + let cred_blob = cred_blob.map(extract_byte_string).transpose()?; + Some(CredentialSource { + private_key, + rp_id_hash: rp_id_hash.try_into().unwrap(), + cred_protect_policy, + cred_blob, + }) + } + _ => None, + }) +} + impl From for Error { fn from(_: StoreError) -> Self { Error } } +fn extract_byte_string(cbor_value: cbor::Value) -> Result, Error> { + match cbor_value { + cbor::Value::ByteString(byte_string) => Ok(byte_string), + _ => Err(Error), + } +} + +fn extract_map(cbor_value: cbor::Value) -> Result, Error> { + match cbor_value { + cbor::Value::Map(map) => Ok(map), + _ => Err(Error), + } +} + #[cfg(test)] mod test { use super::*; + use crate::api::customization::Customization; + use crate::ctap::data_formats::SignatureAlgorithm; use crate::env::test::TestEnv; + const UNSUPPORTED_CREDENTIAL_ID_VERSION: u8 = 0x80; + #[test] fn test_key_store() { let mut env = TestEnv::default(); let key_store = env.key_store(); // 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); @@ -189,11 +448,6 @@ mod test { // 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.key_handle_encryption().unwrap(), encryption_key); - assert_ne!( - 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); } @@ -209,4 +463,204 @@ mod test { let decrypted = key_store.decrypt_pin_hash(&encrypted).unwrap(); assert_eq!(pin_hash, *decrypted); } + + fn test_wrap_unwrap_credential(signature_algorithm: SignatureAlgorithm) { + let mut env = TestEnv::default(); + let private_key = PrivateKey::new(&mut env, signature_algorithm); + let credential_source = CredentialSource { + private_key, + rp_id_hash: [0x55; 32], + cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationOptional), + cred_blob: Some(vec![0xAA; 32]), + }; + let credential_id = env + .key_store() + .wrap_credential(credential_source.clone()) + .unwrap(); + let unwrapped = env + .key_store() + .unwrap_credential(&credential_id, &[0x55; 32]) + .unwrap() + .unwrap(); + assert_eq!(credential_source, unwrapped); + } + + #[test] + fn test_wrap_unwrap_credential_ecdsa() { + test_wrap_unwrap_credential(SignatureAlgorithm::Es256); + } + + #[test] + #[cfg(feature = "ed25519")] + fn test_wrap_unwrap_credential_ed25519() { + test_wrap_unwrap_credential(SignatureAlgorithm::Eddsa); + } + + fn test_wrap_unwrap_credential_bad_version(signature_algorithm: SignatureAlgorithm) { + let mut env = TestEnv::default(); + let private_key = PrivateKey::new(&mut env, signature_algorithm); + let credential_source = CredentialSource { + private_key, + rp_id_hash: [0x55; 32], + cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationOptional), + cred_blob: Some(vec![0xAA; 32]), + }; + let mut credential_id = env.key_store().wrap_credential(credential_source).unwrap(); + credential_id[0] = UNSUPPORTED_CREDENTIAL_ID_VERSION; + // Override the HMAC to pass the check. + credential_id.truncate(&credential_id.len() - 32); + let hmac_key = get_master_keys(&mut env).unwrap().authentication; + let mut id_hmac = [0; HASH_SIZE]; + Hmac::::mac(&hmac_key, &credential_id[..], &mut id_hmac); + credential_id.extend(&id_hmac); + let unwrapped = env + .key_store() + .unwrap_credential(&credential_id, &[0x55; 32]); + assert_eq!(unwrapped, Ok(None)); + } + + #[test] + fn test_wrap_unwrap_credential_bad_version_ecdsa() { + test_wrap_unwrap_credential_bad_version(SignatureAlgorithm::Es256); + } + + #[test] + #[cfg(feature = "ed25519")] + fn test_wrap_unwrap_credential_bad_version_ed25519() { + test_wrap_unwrap_credential_bad_version(SignatureAlgorithm::Eddsa); + } + + fn test_wrap_unwrap_credential_bad_hmac(signature_algorithm: SignatureAlgorithm) { + let mut env = TestEnv::default(); + let private_key = PrivateKey::new(&mut env, signature_algorithm); + let credential_source = CredentialSource { + private_key, + rp_id_hash: [0x55; 32], + cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationOptional), + cred_blob: Some(vec![0xAA; 32]), + }; + let mut credential_id = env.key_store().wrap_credential(credential_source).unwrap(); + let hmac_byte_index = credential_id.len() - 1; + credential_id[hmac_byte_index] ^= 0x01; + let unwrapped = env + .key_store() + .unwrap_credential(&credential_id, &[0x55; 32]); + assert_eq!(unwrapped, Ok(None)); + } + + #[test] + fn test_wrap_unwrap_credential_bad_hmac_ecdsa() { + test_wrap_unwrap_credential_bad_hmac(SignatureAlgorithm::Es256); + } + + #[test] + #[cfg(feature = "ed25519")] + fn test_wrap_unwrap_credential_bad_hmac_ed25519() { + test_wrap_unwrap_credential_bad_hmac(SignatureAlgorithm::Eddsa); + } + + fn test_wrap_unwrap_credential_missing_blocks(signature_algorithm: SignatureAlgorithm) { + let mut env = TestEnv::default(); + let private_key = PrivateKey::new(&mut env, signature_algorithm); + let credential_source = CredentialSource { + private_key, + rp_id_hash: [0x55; 32], + cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationOptional), + cred_blob: Some(vec![0xAA; 32]), + }; + let credential_id = env.key_store().wrap_credential(credential_source).unwrap(); + for length in (1..CBOR_CREDENTIAL_ID_SIZE).step_by(16) { + let unwrapped = env + .key_store() + .unwrap_credential(&credential_id[..length], &[0x55; 32]); + assert_eq!(unwrapped, Ok(None)); + } + } + + #[test] + fn test_wrap_unwrap_credential_missing_blocks_ecdsa() { + test_wrap_unwrap_credential_missing_blocks(SignatureAlgorithm::Es256); + } + + #[test] + #[cfg(feature = "ed25519")] + fn test_wrap_unwrap_credential_missing_blocks_ed25519() { + 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, + application: &[u8; 32], + ) -> Result, Error> { + let master_keys = get_master_keys(env).unwrap(); + let aes_key = AesKey::::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::::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(); + let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256); + let credential_source = CredentialSource { + private_key, + rp_id_hash: [0x55; 32], + cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationOptional), + cred_blob: Some(vec![0xAA; 32]), + }; + let credential_id = env + .key_store() + .wrap_credential(credential_source.clone()) + .unwrap(); + assert_eq!(credential_id.len(), CBOR_CREDENTIAL_ID_SIZE); + } + + #[test] + fn test_wrap_credential_max_size() { + // The CBOR encoding length is variadic and depends on size of fields. Ensure that contents + // still fit into the padded size when we use maximum length entries. + let mut env = TestEnv::default(); + let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256); + let credential_source = CredentialSource { + private_key, + rp_id_hash: [0x55; 32], + cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationOptional), + cred_blob: Some(vec![0xAA; env.customization().max_cred_blob_length()]), + }; + let credential_id = env.key_store().wrap_credential(credential_source.clone()); + assert!(credential_id.is_ok()); + } } diff --git a/libraries/opensk/src/api/mod.rs b/libraries/opensk/src/api/mod.rs index 41e733d..65baab5 100644 --- a/libraries/opensk/src/api/mod.rs +++ b/libraries/opensk/src/api/mod.rs @@ -24,5 +24,6 @@ pub mod crypto; pub mod customization; pub mod firmware_protection; pub mod key_store; +pub mod private_key; pub mod rng; pub mod user_presence; diff --git a/libraries/opensk/src/api/private_key.rs b/libraries/opensk/src/api/private_key.rs new file mode 100644 index 0000000..e9ecf82 --- /dev/null +++ b/libraries/opensk/src/api/private_key.rs @@ -0,0 +1,328 @@ +// Copyright 2021-2023 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 crate::api::crypto::ecdsa::{SecretKey as _, Signature}; +use crate::api::key_store::KeyStore; +use crate::ctap::data_formats::{extract_array, extract_byte_string, CoseKey, SignatureAlgorithm}; +use crate::ctap::secret::Secret; +use crate::ctap::status_code::Ctap2StatusCode; +use crate::env::{EcdsaSk, Env}; +use alloc::vec; +use alloc::vec::Vec; +use core::convert::TryFrom; +use core::ops::Deref; +#[cfg(feature = "ed25519")] +use core::ops::DerefMut; +#[cfg(feature = "ed25519")] +use rand_core::RngCore; +use sk_cbor as cbor; +use sk_cbor::{cbor_array, cbor_bytes, cbor_int}; + +/// An asymmetric private key that can sign messages. +#[derive(Clone, Debug)] +// We shouldn't compare private keys in prod without constant-time operations. +#[cfg_attr(test, derive(PartialEq, Eq))] +pub enum PrivateKey { + // We store the seed instead of the key since we can't get the seed back from the key. We could + // store both if we believe deriving the key is done more than once and costly. + Ecdsa(Secret<[u8; 32]>), + #[cfg(feature = "ed25519")] + Ed25519(ed25519_compact::SecretKey), +} + +impl PrivateKey { + /// Creates a new private key for the given algorithm. + /// + /// # Panics + /// + /// Panics if the algorithm is [`SignatureAlgorithm::Unknown`]. + pub fn new(env: &mut impl Env, alg: SignatureAlgorithm) -> Self { + match alg { + SignatureAlgorithm::Es256 => { + PrivateKey::Ecdsa(env.key_store().generate_ecdsa_seed().unwrap()) + } + #[cfg(feature = "ed25519")] + SignatureAlgorithm::Eddsa => { + let mut bytes: Secret<[u8; 32]> = Secret::default(); + env.rng().fill_bytes(bytes.deref_mut()); + Self::new_ed25519_from_bytes(&*bytes).unwrap() + } + SignatureAlgorithm::Unknown => unreachable!(), + } + } + + /// Creates a new ecdsa private key. + pub fn new_ecdsa(env: &mut impl Env) -> PrivateKey { + Self::new(env, SignatureAlgorithm::Es256) + } + + /// Helper function that creates a private key of type ECDSA. + /// + /// This function is public for legacy credential source parsing only. + pub fn new_ecdsa_from_bytes(bytes: &[u8]) -> Option { + if bytes.len() != 32 { + return None; + } + let mut seed: Secret<[u8; 32]> = Secret::default(); + seed.copy_from_slice(bytes); + Some(PrivateKey::Ecdsa(seed)) + } + + #[cfg(feature = "ed25519")] + pub fn new_ed25519_from_bytes(bytes: &[u8]) -> Option { + if bytes.len() != 32 { + return None; + } + let seed = ed25519_compact::Seed::from_slice(bytes).unwrap(); + Some(Self::Ed25519(ed25519_compact::KeyPair::from_seed(seed).sk)) + } + + /// Returns the ECDSA private key. + pub fn ecdsa_key(&self, env: &mut E) -> Result, Ctap2StatusCode> { + match self { + PrivateKey::Ecdsa(seed) => ecdsa_key_from_seed(env, seed), + #[allow(unreachable_patterns)] + _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), + } + } + + /// Returns the corresponding public key. + pub fn get_pub_key(&self, env: &mut impl Env) -> Result { + Ok(match self { + PrivateKey::Ecdsa(ecdsa_seed) => { + CoseKey::from_ecdsa_public_key(ecdsa_key_from_seed(env, ecdsa_seed)?.public_key()) + } + #[cfg(feature = "ed25519")] + PrivateKey::Ed25519(ed25519_key) => CoseKey::from(ed25519_key.public_key()), + }) + } + + /// Returns the encoded signature for a given message. + pub(crate) fn sign_and_encode( + &self, + env: &mut impl Env, + message: &[u8], + ) -> Result, Ctap2StatusCode> { + Ok(match self { + PrivateKey::Ecdsa(ecdsa_seed) => { + ecdsa_key_from_seed(env, ecdsa_seed)?.sign(message).to_der() + } + #[cfg(feature = "ed25519")] + PrivateKey::Ed25519(ed25519_key) => ed25519_key.sign(message, None).to_vec(), + }) + } + + /// The associated COSE signature algorithm identifier. + pub fn signature_algorithm(&self) -> SignatureAlgorithm { + match self { + PrivateKey::Ecdsa(_) => SignatureAlgorithm::Es256, + #[cfg(feature = "ed25519")] + PrivateKey::Ed25519(_) => SignatureAlgorithm::Eddsa, + } + } + + /// Writes the key bytes. + pub fn to_bytes(&self) -> Secret<[u8]> { + let mut bytes = Secret::new(32); + match self { + PrivateKey::Ecdsa(ecdsa_seed) => bytes.copy_from_slice(ecdsa_seed.deref()), + #[cfg(feature = "ed25519")] + PrivateKey::Ed25519(ed25519_key) => bytes.copy_from_slice(ed25519_key.seed().deref()), + } + bytes + } +} + +fn ecdsa_key_from_seed( + env: &mut E, + seed: &[u8; 32], +) -> Result, Ctap2StatusCode> { + let ecdsa_bytes = env.key_store().derive_ecdsa(seed)?; + Ok(EcdsaSk::::from_slice(&ecdsa_bytes).unwrap()) +} + +impl From<&PrivateKey> for cbor::Value { + /// Writes a private key into CBOR format. This exposes the cryptographic secret. + // TODO needs zeroization if seed is secret + // called in wrap_credential and PublicKeyCredentialSource + fn from(private_key: &PrivateKey) -> Self { + cbor_array![ + cbor_int!(private_key.signature_algorithm() as i64), + cbor_bytes!(private_key.to_bytes().expose_secret_to_vec()), + ] + } +} + +impl TryFrom for PrivateKey { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + let mut array = extract_array(cbor_value)?; + if array.len() != 2 { + return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR); + } + let key_bytes = extract_byte_string(array.pop().unwrap())?; + match SignatureAlgorithm::try_from(array.pop().unwrap())? { + SignatureAlgorithm::Es256 => PrivateKey::new_ecdsa_from_bytes(&key_bytes) + .ok_or(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR), + #[cfg(feature = "ed25519")] + SignatureAlgorithm::Eddsa => PrivateKey::new_ed25519_from_bytes(&key_bytes) + .ok_or(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR), + _ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::env::test::TestEnv; + + #[test] + fn test_new_ecdsa_from_bytes() { + let mut env = TestEnv::default(); + let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256); + let key_bytes = private_key.to_bytes(); + assert_eq!( + PrivateKey::new_ecdsa_from_bytes(&key_bytes), + Some(private_key) + ); + } + + #[test] + #[cfg(feature = "ed25519")] + fn test_new_ed25519_from_bytes() { + let mut env = TestEnv::default(); + let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Eddsa); + let key_bytes = private_key.to_bytes(); + assert_eq!( + PrivateKey::new_ed25519_from_bytes(&key_bytes), + Some(private_key) + ); + } + + #[test] + fn test_new_ecdsa_from_bytes_wrong_length() { + assert_eq!(PrivateKey::new_ecdsa_from_bytes(&[0x55; 16]), None); + assert_eq!(PrivateKey::new_ecdsa_from_bytes(&[0x55; 31]), None); + assert_eq!(PrivateKey::new_ecdsa_from_bytes(&[0x55; 33]), None); + assert_eq!(PrivateKey::new_ecdsa_from_bytes(&[0x55; 64]), None); + } + + #[test] + #[cfg(feature = "ed25519")] + fn test_new_ed25519_from_bytes_wrong_length() { + assert_eq!(PrivateKey::new_ed25519_from_bytes(&[0x55; 16]), None); + assert_eq!(PrivateKey::new_ed25519_from_bytes(&[0x55; 31]), None); + assert_eq!(PrivateKey::new_ed25519_from_bytes(&[0x55; 33]), None); + assert_eq!(PrivateKey::new_ed25519_from_bytes(&[0x55; 64]), None); + } + + #[test] + fn test_private_key_get_pub_key() { + let mut env = TestEnv::default(); + let private_key = PrivateKey::new_ecdsa(&mut env); + let ecdsa_key = private_key.ecdsa_key(&mut env).unwrap(); + let public_key = ecdsa_key.public_key(); + assert_eq!( + private_key.get_pub_key(&mut env), + Ok(CoseKey::from_ecdsa_public_key(public_key)) + ); + } + + #[test] + fn test_private_key_sign_and_encode() { + let mut env = TestEnv::default(); + let message = [0x5A; 32]; + let private_key = PrivateKey::new_ecdsa(&mut env); + let ecdsa_key = private_key.ecdsa_key(&mut env).unwrap(); + let signature = ecdsa_key.sign(&message).to_der(); + assert_eq!( + private_key.sign_and_encode(&mut env, &message), + Ok(signature) + ); + } + + fn test_private_key_signature_algorithm(signature_algorithm: SignatureAlgorithm) { + let mut env = TestEnv::default(); + let private_key = PrivateKey::new(&mut env, signature_algorithm); + assert_eq!(private_key.signature_algorithm(), signature_algorithm); + } + + #[test] + fn test_ecdsa_private_key_signature_algorithm() { + test_private_key_signature_algorithm(SignatureAlgorithm::Es256); + } + + #[test] + #[cfg(feature = "ed25519")] + fn test_ed25519_private_key_signature_algorithm() { + test_private_key_signature_algorithm(SignatureAlgorithm::Eddsa); + } + + fn test_private_key_from_to_cbor(signature_algorithm: SignatureAlgorithm) { + let mut env = TestEnv::default(); + let private_key = PrivateKey::new(&mut env, signature_algorithm); + let cbor = cbor::Value::from(&private_key); + assert_eq!(PrivateKey::try_from(cbor), Ok(private_key),); + } + + #[test] + fn test_ecdsa_private_key_from_to_cbor() { + test_private_key_from_to_cbor(SignatureAlgorithm::Es256); + } + + #[test] + #[cfg(feature = "ed25519")] + fn test_ed25519_private_key_from_to_cbor() { + test_private_key_from_to_cbor(SignatureAlgorithm::Eddsa); + } + + fn test_private_key_from_bad_cbor(signature_algorithm: SignatureAlgorithm) { + let cbor = cbor_array![ + cbor_int!(signature_algorithm as i64), + cbor_bytes!(vec![0x88; 32]), + // The array is too long. + cbor_int!(0), + ]; + assert_eq!( + PrivateKey::try_from(cbor), + Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR), + ); + } + + #[test] + fn test_ecdsa_private_key_from_bad_cbor() { + test_private_key_from_bad_cbor(SignatureAlgorithm::Es256); + } + + #[test] + #[cfg(feature = "ed25519")] + fn test_ed25519_private_key_from_bad_cbor() { + test_private_key_from_bad_cbor(SignatureAlgorithm::Eddsa); + } + + #[test] + fn test_private_key_from_bad_cbor_unsupported_algo() { + let cbor = cbor_array![ + // This algorithms doesn't exist. + cbor_int!(-1), + cbor_bytes!(vec![0x88; 32]), + ]; + assert_eq!( + PrivateKey::try_from(cbor), + Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR), + ); + } +} diff --git a/libraries/opensk/src/ctap/credential_id.rs b/libraries/opensk/src/ctap/credential_id.rs deleted file mode 100644 index 49b204f..0000000 --- a/libraries/opensk/src/ctap/credential_id.rs +++ /dev/null @@ -1,500 +0,0 @@ -// Copyright 2022-2023 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::crypto_wrapper::{aes256_cbc_decrypt, aes256_cbc_encrypt, PrivateKey}; -use super::data_formats::{ - CredentialProtectionPolicy, PublicKeyCredentialSource, PublicKeyCredentialType, -}; -use super::status_code::Ctap2StatusCode; -use super::{cbor_read, cbor_write}; -use crate::api::crypto::aes256::Aes256; -use crate::api::crypto::hmac256::Hmac256; -use crate::api::crypto::HASH_SIZE; -use crate::api::key_store::KeyStore; -use crate::ctap::data_formats::{extract_byte_string, extract_map}; -use crate::env::{AesKey, Env, Hmac}; -use alloc::string::String; -use alloc::vec::Vec; -use core::convert::{TryFrom, TryInto}; -use sk_cbor::{cbor_map_options, destructure_cbor_map}; - -pub 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; -pub const MIN_CREDENTIAL_ID_SIZE: usize = LEGACY_CREDENTIAL_ID_SIZE; -pub const MAX_CREDENTIAL_ID_SIZE: usize = CBOR_CREDENTIAL_ID_SIZE; - -pub const CBOR_CREDENTIAL_ID_VERSION: u8 = 0x01; - -pub const MAX_PADDING_LENGTH: u8 = 0xBF; - -// Data fields that are contained in the credential ID of non-discoverable credentials. -struct CredentialSource { - private_key: PrivateKey, - rp_id_hash: [u8; 32], - cred_protect_policy: Option, - cred_blob: Option>, -} - -// The data fields contained in the credential ID are serialized using CBOR maps. -// Each field is associated with a unique tag, implemented with a CBOR unsigned key. -enum CredentialSourceField { - PrivateKey = 0, - RpIdHash = 1, - CredProtectPolicy = 2, - CredBlob = 3, -} - -impl From for sk_cbor::Value { - fn from(field: CredentialSourceField) -> sk_cbor::Value { - (field as u64).into() - } -} - -fn decrypt_legacy_credential_id( - env: &mut E, - bytes: &[u8], -) -> Result, Ctap2StatusCode> { - let aes_key = AesKey::::new(&*env.key_store().key_handle_encryption()?); - let plaintext = aes256_cbc_decrypt::(&aes_key, bytes, true)?.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( - env: &mut E, - bytes: &[u8], -) -> Result, Ctap2StatusCode> { - let aes_key = AesKey::::new(&*env.key_store().key_handle_encryption()?); - let plaintext = aes256_cbc_decrypt::(&aes_key, bytes, true)?; - let unpadded = remove_padding(&plaintext)?; - - let cbor_credential_source = cbor_read(unpadded)?; - destructure_cbor_map! { - let { - CredentialSourceField::PrivateKey => private_key, - CredentialSourceField::RpIdHash=> rp_id_hash, - CredentialSourceField::CredProtectPolicy => cred_protect_policy, - CredentialSourceField::CredBlob => cred_blob, - } = extract_map(cbor_credential_source)?; - } - Ok(match (private_key, rp_id_hash) { - (Some(private_key), Some(rp_id_hash)) => { - let private_key = PrivateKey::try_from(private_key)?; - let rp_id_hash = extract_byte_string(rp_id_hash)?; - if rp_id_hash.len() != 32 { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); - } - let cred_protect_policy = cred_protect_policy - .map(CredentialProtectionPolicy::try_from) - .transpose()?; - let cred_blob = cred_blob.map(extract_byte_string).transpose()?; - Some(CredentialSource { - private_key, - rp_id_hash: rp_id_hash.try_into().unwrap(), - cred_protect_policy, - cred_blob, - }) - } - _ => None, - }) -} - -/// Pad data to MAX_PADDING_LENGTH+1 (192) bytes using PKCS padding scheme. -/// Let N = 192 - data.len(), the PKCS padding scheme would pad N bytes of N after the data. -fn add_padding(data: &mut Vec) -> Result<(), Ctap2StatusCode> { - // The data should be between 1 to MAX_PADDING_LENGTH bytes for the padding scheme to be valid. - if data.is_empty() || data.len() > MAX_PADDING_LENGTH as usize { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); - } - let pad_length = MAX_PADDING_LENGTH - (data.len() as u8 - 1); - data.extend(core::iter::repeat(pad_length).take(pad_length as usize)); - Ok(()) -} - -fn remove_padding(data: &[u8]) -> Result<&[u8], Ctap2StatusCode> { - if data.len() != MAX_PADDING_LENGTH as usize + 1 { - // This is an internal error instead of corrupted credential ID which we should just ignore because - // we've already checked that the HMAC matched. - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); - } - let pad_length = *data.last().unwrap(); - if pad_length == 0 || pad_length > MAX_PADDING_LENGTH { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); - } - if !data[(data.len() - pad_length as usize)..] - .iter() - .all(|x| *x == pad_length) - { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); - } - Ok(&data[..data.len() - pad_length as usize]) -} - -/// Encrypts the given private key, relying party ID hash, and some other metadata 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). -pub fn encrypt_to_credential_id( - env: &mut E, - private_key: &PrivateKey, - rp_id_hash: &[u8; 32], - cred_protect_policy: Option, - cred_blob: Option>, -) -> Result, Ctap2StatusCode> { - let mut payload = Vec::new(); - let cbor = cbor_map_options! { - CredentialSourceField::PrivateKey => private_key, - CredentialSourceField::RpIdHash => rp_id_hash, - CredentialSourceField::CredProtectPolicy => cred_protect_policy, - CredentialSourceField::CredBlob => cred_blob, - }; - cbor_write(cbor, &mut payload)?; - add_padding(&mut payload)?; - - let aes_key = AesKey::::new(&*env.key_store().key_handle_encryption()?); - let encrypted_payload = aes256_cbc_encrypt(env, &aes_key, &payload, true)?; - let mut credential_id = encrypted_payload; - credential_id.insert(0, CBOR_CREDENTIAL_ID_VERSION); - - let mut id_hmac = [0; HASH_SIZE]; - Hmac::::mac( - &*env.key_store().key_handle_authentication()?, - &credential_id[..], - &mut id_hmac, - ); - credential_id.extend(&id_hmac); - Ok(credential_id) -} - -/// Decrypts the given credential ID into a PublicKeyCredentialSource, populating only the recorded fields. -/// -/// Returns None if -/// - 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, -/// - 192 bytes: encrypted CBOR-encoded credential source fields, -/// - 32 bytes: HMAC-SHA256 over everything else. -pub fn decrypt_credential_id( - env: &mut E, - credential_id: Vec, - rp_id_hash: &[u8], -) -> Result, Ctap2StatusCode> { - if credential_id.len() < MIN_CREDENTIAL_ID_SIZE { - return Ok(None); - } - let hmac_message_size = credential_id.len() - 32; - if !Hmac::::verify( - &*env.key_store().key_handle_authentication()?, - &credential_id[..hmac_message_size], - array_ref![credential_id, hmac_message_size, 32], - ) { - return Ok(None); - } - - let credential_source = if credential_id.len() == LEGACY_CREDENTIAL_ID_SIZE { - decrypt_legacy_credential_id(env, &credential_id[..hmac_message_size])? - } else { - match credential_id[0] { - CBOR_CREDENTIAL_ID_VERSION => { - if credential_id.len() != CBOR_CREDENTIAL_ID_SIZE { - return Ok(None); - } - decrypt_cbor_credential_id(env, &credential_id[1..hmac_message_size])? - } - _ => return Ok(None), - } - }; - - let credential_source = if let Some(credential_source) = credential_source { - credential_source - } else { - return Ok(None); - }; - - if rp_id_hash != credential_source.rp_id_hash { - return Ok(None); - } - - Ok(Some(PublicKeyCredentialSource { - key_type: PublicKeyCredentialType::PublicKey, - credential_id, - private_key: credential_source.private_key, - rp_id: String::new(), - user_handle: Vec::new(), - user_display_name: None, - cred_protect_policy: credential_source.cred_protect_policy, - creation_order: 0, - user_name: None, - user_icon: None, - cred_blob: credential_source.cred_blob, - large_blob_key: None, - })) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::api::crypto::ecdsa::SecretKey as _; - use crate::api::customization::Customization; - use crate::ctap::credential_id::CBOR_CREDENTIAL_ID_SIZE; - use crate::ctap::SignatureAlgorithm; - use crate::env::test::TestEnv; - use crate::env::EcdsaSk; - - const UNSUPPORTED_CREDENTIAL_ID_VERSION: u8 = 0x80; - - fn test_encrypt_decrypt_credential(signature_algorithm: SignatureAlgorithm) { - let mut env = TestEnv::default(); - let private_key = PrivateKey::new(&mut env, signature_algorithm); - - let rp_id_hash = [0x55; 32]; - let encrypted_id = - encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None).unwrap(); - let decrypted_source = decrypt_credential_id(&mut env, encrypted_id, &rp_id_hash) - .unwrap() - .unwrap(); - - assert_eq!(private_key, decrypted_source.private_key); - } - - #[test] - fn test_encrypt_decrypt_ecdsa_credential() { - test_encrypt_decrypt_credential(SignatureAlgorithm::Es256); - } - - #[test] - #[cfg(feature = "ed25519")] - fn test_encrypt_decrypt_ed25519_credential() { - test_encrypt_decrypt_credential(SignatureAlgorithm::Eddsa); - } - - #[test] - fn test_encrypt_decrypt_bad_version() { - let mut env = TestEnv::default(); - let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256); - - let rp_id_hash = [0x55; 32]; - let mut encrypted_id = - encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None).unwrap(); - encrypted_id[0] = UNSUPPORTED_CREDENTIAL_ID_VERSION; - // Override the HMAC to pass the check. - encrypted_id.truncate(&encrypted_id.len() - 32); - let hmac_key = env.key_store().key_handle_authentication().unwrap(); - let mut id_hmac = [0; HASH_SIZE]; - Hmac::::mac(&hmac_key, &encrypted_id[..], &mut id_hmac); - encrypted_id.extend(&id_hmac); - - assert_eq!( - decrypt_credential_id(&mut env, encrypted_id, &rp_id_hash), - Ok(None) - ); - } - - fn test_encrypt_decrypt_bad_hmac(signature_algorithm: SignatureAlgorithm) { - let mut env = TestEnv::default(); - let private_key = PrivateKey::new(&mut env, signature_algorithm); - - let rp_id_hash = [0x55; 32]; - let encrypted_id = - encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None).unwrap(); - for i in 0..encrypted_id.len() { - let mut modified_id = encrypted_id.clone(); - modified_id[i] ^= 0x01; - assert_eq!( - decrypt_credential_id(&mut env, modified_id, &rp_id_hash), - Ok(None) - ); - } - } - - #[test] - fn test_ecdsa_encrypt_decrypt_bad_hmac() { - test_encrypt_decrypt_bad_hmac(SignatureAlgorithm::Es256); - } - - #[test] - #[cfg(feature = "ed25519")] - fn test_ed25519_encrypt_decrypt_bad_hmac() { - test_encrypt_decrypt_bad_hmac(SignatureAlgorithm::Eddsa); - } - - fn test_decrypt_credential_missing_blocks(signature_algorithm: SignatureAlgorithm) { - let mut env = TestEnv::default(); - let private_key = PrivateKey::new(&mut env, signature_algorithm); - - let rp_id_hash = [0x55; 32]; - let encrypted_id = - encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None).unwrap(); - - for length in (1..CBOR_CREDENTIAL_ID_SIZE).step_by(16) { - assert_eq!( - decrypt_credential_id(&mut env, encrypted_id[..length].to_vec(), &rp_id_hash), - Ok(None) - ); - } - } - - #[test] - fn test_ecdsa_decrypt_credential_missing_blocks() { - test_decrypt_credential_missing_blocks(SignatureAlgorithm::Es256); - } - - #[test] - #[cfg(feature = "ed25519")] - fn test_ed25519_decrypt_credential_missing_blocks() { - test_decrypt_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, - application: &[u8; 32], - ) -> Result, Ctap2StatusCode> { - let aes_key = AesKey::::new(&*env.key_store().key_handle_encryption()?); - 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)?; - let mut id_hmac = [0; HASH_SIZE]; - Hmac::::mac( - &*env.key_store().key_handle_authentication()?, - &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_ecdsa(&mut env); - let ecdsa_key = private_key.ecdsa_key(&mut env).unwrap(); - - let rp_id_hash = [0x55; 32]; - let encrypted_id = - legacy_encrypt_to_credential_id(&mut env, ecdsa_key, &rp_id_hash).unwrap(); - let decrypted_source = decrypt_credential_id(&mut env, encrypted_id, &rp_id_hash) - .unwrap() - .unwrap(); - - assert_eq!(private_key, decrypted_source.private_key); - // Legacy credentials didn't persist credProtectPolicy info, so it should be treated as None. - assert!(decrypted_source.cred_protect_policy.is_none()); - } - - #[test] - fn test_encrypt_credential_size() { - let mut env = TestEnv::default(); - let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256); - - let rp_id_hash = [0x55; 32]; - let encrypted_id = - encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, None).unwrap(); - assert_eq!(encrypted_id.len(), CBOR_CREDENTIAL_ID_SIZE); - } - - #[test] - fn test_encrypt_credential_max_cbor_size() { - // The cbor encoding length is variadic and depends on size of fields. Try to put maximum length - // for each encoded field and ensure that it doesn't go over the padding size. - let mut env = TestEnv::default(); - // Currently all private key types have same length when transformed to bytes. - let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256); - let rp_id_hash = [0x55; 32]; - let cred_protect_policy = Some(CredentialProtectionPolicy::UserVerificationOptional); - let cred_blob = Some(vec![0x55; env.customization().max_cred_blob_length()]); - - let encrypted_id = encrypt_to_credential_id( - &mut env, - &private_key, - &rp_id_hash, - cred_protect_policy, - cred_blob, - ); - - assert!(encrypted_id.is_ok()); - } - - #[test] - fn test_cred_protect_persisted() { - let mut env = TestEnv::default(); - let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256); - - let rp_id_hash = [0x55; 32]; - let encrypted_id = encrypt_to_credential_id( - &mut env, - &private_key, - &rp_id_hash, - Some(CredentialProtectionPolicy::UserVerificationRequired), - None, - ) - .unwrap(); - - let decrypted_source = decrypt_credential_id(&mut env, encrypted_id, &rp_id_hash) - .unwrap() - .unwrap(); - assert_eq!(decrypted_source.private_key, private_key); - assert_eq!( - decrypted_source.cred_protect_policy, - Some(CredentialProtectionPolicy::UserVerificationRequired) - ); - } - - #[test] - fn test_cred_blob_persisted() { - let mut env = TestEnv::default(); - let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256); - - let rp_id_hash = [0x55; 32]; - let cred_blob = Some(vec![0x55; env.customization().max_cred_blob_length()]); - let encrypted_id = - encrypt_to_credential_id(&mut env, &private_key, &rp_id_hash, None, cred_blob.clone()) - .unwrap(); - - let decrypted_source = decrypt_credential_id(&mut env, encrypted_id, &rp_id_hash) - .unwrap() - .unwrap(); - assert_eq!(decrypted_source.private_key, private_key); - assert_eq!(decrypted_source.cred_blob, cred_blob); - } -} diff --git a/libraries/opensk/src/ctap/credential_management.rs b/libraries/opensk/src/ctap/credential_management.rs index 9b6dbde..f23522a 100644 --- a/libraries/opensk/src/ctap/credential_management.rs +++ b/libraries/opensk/src/ctap/credential_management.rs @@ -353,12 +353,12 @@ pub fn process_credential_management( #[cfg(test)] mod test { - use super::super::crypto_wrapper::PrivateKey; use super::super::data_formats::{PinUvAuthProtocol, PublicKeyCredentialType}; use super::super::pin_protocol::authenticate_pin_uv_auth_token; use super::super::CtapState; use super::*; use crate::api::crypto::ecdh::SecretKey as _; + use crate::api::private_key::PrivateKey; use crate::api::rng::Rng; use crate::env::test::TestEnv; use crate::env::EcdhSk; diff --git a/libraries/opensk/src/ctap/crypto_wrapper.rs b/libraries/opensk/src/ctap/crypto_wrapper.rs index a641b24..f96fda2 100644 --- a/libraries/opensk/src/ctap/crypto_wrapper.rs +++ b/libraries/opensk/src/ctap/crypto_wrapper.rs @@ -13,21 +13,11 @@ // limitations under the License. use crate::api::crypto::aes256::Aes256; -use crate::api::crypto::ecdsa::{SecretKey as _, Signature}; -use crate::api::key_store::KeyStore; -use crate::ctap::data_formats::{extract_array, extract_byte_string, CoseKey, SignatureAlgorithm}; use crate::ctap::secret::Secret; use crate::ctap::status_code::Ctap2StatusCode; -use crate::env::{AesKey, EcdsaSk, Env}; -use alloc::vec; +use crate::env::{AesKey, Env}; use alloc::vec::Vec; -use core::convert::TryFrom; -use core::ops::Deref; -#[cfg(feature = "ed25519")] -use core::ops::DerefMut; use rand_core::RngCore; -use sk_cbor as cbor; -use sk_cbor::{cbor_array, cbor_bytes, cbor_int}; /// Wraps the AES256-CBC encryption to match what we need in CTAP. pub fn aes256_cbc_encrypt( @@ -74,160 +64,6 @@ pub fn aes256_cbc_decrypt( Ok(plaintext) } -/// An asymmetric private key that can sign messages. -#[derive(Clone, Debug)] -// We shouldn't compare private keys in prod without constant-time operations. -#[cfg_attr(test, derive(PartialEq, Eq))] -pub enum PrivateKey { - // We store the seed instead of the key since we can't get the seed back from the key. We could - // store both if we believe deriving the key is done more than once and costly. - Ecdsa(Secret<[u8; 32]>), - #[cfg(feature = "ed25519")] - Ed25519(ed25519_compact::SecretKey), -} - -impl PrivateKey { - /// Creates a new private key for the given algorithm. - /// - /// # Panics - /// - /// Panics if the algorithm is [`SignatureAlgorithm::Unknown`]. - pub fn new(env: &mut impl Env, alg: SignatureAlgorithm) -> Self { - match alg { - SignatureAlgorithm::Es256 => { - PrivateKey::Ecdsa(env.key_store().generate_ecdsa_seed().unwrap()) - } - #[cfg(feature = "ed25519")] - SignatureAlgorithm::Eddsa => { - let mut bytes: Secret<[u8; 32]> = Secret::default(); - env.rng().fill_bytes(bytes.deref_mut()); - Self::new_ed25519_from_bytes(&*bytes).unwrap() - } - SignatureAlgorithm::Unknown => unreachable!(), - } - } - - /// Creates a new ecdsa private key. - pub fn new_ecdsa(env: &mut impl Env) -> PrivateKey { - Self::new(env, SignatureAlgorithm::Es256) - } - - /// Helper function that creates a private key of type ECDSA. - /// - /// This function is public for legacy credential source parsing only. - pub fn new_ecdsa_from_bytes(bytes: &[u8]) -> Option { - if bytes.len() != 32 { - return None; - } - let mut seed: Secret<[u8; 32]> = Secret::default(); - seed.copy_from_slice(bytes); - Some(PrivateKey::Ecdsa(seed)) - } - - #[cfg(feature = "ed25519")] - pub fn new_ed25519_from_bytes(bytes: &[u8]) -> Option { - if bytes.len() != 32 { - return None; - } - let seed = ed25519_compact::Seed::from_slice(bytes).unwrap(); - Some(Self::Ed25519(ed25519_compact::KeyPair::from_seed(seed).sk)) - } - - /// Returns the ECDSA private key. - pub fn ecdsa_key(&self, env: &mut E) -> Result, Ctap2StatusCode> { - match self { - PrivateKey::Ecdsa(seed) => ecdsa_key_from_seed(env, seed), - #[allow(unreachable_patterns)] - _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), - } - } - - /// Returns the corresponding public key. - pub fn get_pub_key(&self, env: &mut impl Env) -> Result { - Ok(match self { - PrivateKey::Ecdsa(ecdsa_seed) => { - CoseKey::from_ecdsa_public_key(ecdsa_key_from_seed(env, ecdsa_seed)?.public_key()) - } - #[cfg(feature = "ed25519")] - PrivateKey::Ed25519(ed25519_key) => CoseKey::from(ed25519_key.public_key()), - }) - } - - /// Returns the encoded signature for a given message. - pub fn sign_and_encode( - &self, - env: &mut impl Env, - message: &[u8], - ) -> Result, Ctap2StatusCode> { - Ok(match self { - PrivateKey::Ecdsa(ecdsa_seed) => { - ecdsa_key_from_seed(env, ecdsa_seed)?.sign(message).to_der() - } - #[cfg(feature = "ed25519")] - PrivateKey::Ed25519(ed25519_key) => ed25519_key.sign(message, None).to_vec(), - }) - } - - /// The associated COSE signature algorithm identifier. - pub fn signature_algorithm(&self) -> SignatureAlgorithm { - match self { - PrivateKey::Ecdsa(_) => SignatureAlgorithm::Es256, - #[cfg(feature = "ed25519")] - PrivateKey::Ed25519(_) => SignatureAlgorithm::Eddsa, - } - } - - /// Writes the key bytes. - pub fn to_bytes(&self) -> Secret<[u8]> { - let mut bytes = Secret::new(32); - match self { - PrivateKey::Ecdsa(ecdsa_seed) => bytes.copy_from_slice(ecdsa_seed.deref()), - #[cfg(feature = "ed25519")] - PrivateKey::Ed25519(ed25519_key) => bytes.copy_from_slice(ed25519_key.seed().deref()), - } - bytes - } -} - -fn ecdsa_key_from_seed( - env: &mut E, - seed: &[u8; 32], -) -> Result, Ctap2StatusCode> { - let ecdsa_bytes = env.key_store().derive_ecdsa(seed)?; - Ok(EcdsaSk::::from_slice(&ecdsa_bytes).unwrap()) -} - -impl From<&PrivateKey> for cbor::Value { - /// Writes a private key into CBOR format. This exposes the cryptographic secret. - // TODO called in encrypt_to_credential_id and PublicKeyCredentialSource, needs zeroization - fn from(private_key: &PrivateKey) -> Self { - cbor_array![ - cbor_int!(private_key.signature_algorithm() as i64), - cbor_bytes!(private_key.to_bytes().expose_secret_to_vec()), - ] - } -} - -impl TryFrom for PrivateKey { - type Error = Ctap2StatusCode; - - fn try_from(cbor_value: cbor::Value) -> Result { - let mut array = extract_array(cbor_value)?; - if array.len() != 2 { - return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR); - } - let key_bytes = extract_byte_string(array.pop().unwrap())?; - match SignatureAlgorithm::try_from(array.pop().unwrap())? { - SignatureAlgorithm::Es256 => PrivateKey::new_ecdsa_from_bytes(&key_bytes) - .ok_or(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR), - #[cfg(feature = "ed25519")] - SignatureAlgorithm::Eddsa => PrivateKey::new_ed25519_from_bytes(&key_bytes) - .ok_or(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR), - _ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR), - } - } -} - #[cfg(test)] mod test { use super::*; @@ -297,141 +133,4 @@ mod test { assert_ne!(block1, block2); } } - - #[test] - fn test_new_ecdsa_from_bytes() { - let mut env = TestEnv::default(); - let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Es256); - let key_bytes = private_key.to_bytes(); - assert_eq!( - PrivateKey::new_ecdsa_from_bytes(&key_bytes), - Some(private_key) - ); - } - - #[test] - #[cfg(feature = "ed25519")] - fn test_new_ed25519_from_bytes() { - let mut env = TestEnv::default(); - let private_key = PrivateKey::new(&mut env, SignatureAlgorithm::Eddsa); - let key_bytes = private_key.to_bytes(); - assert_eq!( - PrivateKey::new_ed25519_from_bytes(&key_bytes), - Some(private_key) - ); - } - - #[test] - fn test_new_ecdsa_from_bytes_wrong_length() { - assert_eq!(PrivateKey::new_ecdsa_from_bytes(&[0x55; 16]), None); - assert_eq!(PrivateKey::new_ecdsa_from_bytes(&[0x55; 31]), None); - assert_eq!(PrivateKey::new_ecdsa_from_bytes(&[0x55; 33]), None); - assert_eq!(PrivateKey::new_ecdsa_from_bytes(&[0x55; 64]), None); - } - - #[test] - #[cfg(feature = "ed25519")] - fn test_new_ed25519_from_bytes_wrong_length() { - assert_eq!(PrivateKey::new_ed25519_from_bytes(&[0x55; 16]), None); - assert_eq!(PrivateKey::new_ed25519_from_bytes(&[0x55; 31]), None); - assert_eq!(PrivateKey::new_ed25519_from_bytes(&[0x55; 33]), None); - assert_eq!(PrivateKey::new_ed25519_from_bytes(&[0x55; 64]), None); - } - - #[test] - fn test_private_key_get_pub_key() { - let mut env = TestEnv::default(); - let private_key = PrivateKey::new_ecdsa(&mut env); - let ecdsa_key = private_key.ecdsa_key(&mut env).unwrap(); - let public_key = ecdsa_key.public_key(); - assert_eq!( - private_key.get_pub_key(&mut env), - Ok(CoseKey::from_ecdsa_public_key(public_key)) - ); - } - - #[test] - fn test_private_key_sign_and_encode() { - let mut env = TestEnv::default(); - let message = [0x5A; 32]; - let private_key = PrivateKey::new_ecdsa(&mut env); - let ecdsa_key = private_key.ecdsa_key(&mut env).unwrap(); - let signature = ecdsa_key.sign(&message).to_der(); - assert_eq!( - private_key.sign_and_encode(&mut env, &message), - Ok(signature) - ); - } - - fn test_private_key_signature_algorithm(signature_algorithm: SignatureAlgorithm) { - let mut env = TestEnv::default(); - let private_key = PrivateKey::new(&mut env, signature_algorithm); - assert_eq!(private_key.signature_algorithm(), signature_algorithm); - } - - #[test] - fn test_ecdsa_private_key_signature_algorithm() { - test_private_key_signature_algorithm(SignatureAlgorithm::Es256); - } - - #[test] - #[cfg(feature = "ed25519")] - fn test_ed25519_private_key_signature_algorithm() { - test_private_key_signature_algorithm(SignatureAlgorithm::Eddsa); - } - - fn test_private_key_from_to_cbor(signature_algorithm: SignatureAlgorithm) { - let mut env = TestEnv::default(); - let private_key = PrivateKey::new(&mut env, signature_algorithm); - let cbor = cbor::Value::from(&private_key); - assert_eq!(PrivateKey::try_from(cbor), Ok(private_key),); - } - - #[test] - fn test_ecdsa_private_key_from_to_cbor() { - test_private_key_from_to_cbor(SignatureAlgorithm::Es256); - } - - #[test] - #[cfg(feature = "ed25519")] - fn test_ed25519_private_key_from_to_cbor() { - test_private_key_from_to_cbor(SignatureAlgorithm::Eddsa); - } - - fn test_private_key_from_bad_cbor(signature_algorithm: SignatureAlgorithm) { - let cbor = cbor_array![ - cbor_int!(signature_algorithm as i64), - cbor_bytes!(vec![0x88; 32]), - // The array is too long. - cbor_int!(0), - ]; - assert_eq!( - PrivateKey::try_from(cbor), - Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR), - ); - } - - #[test] - fn test_ecdsa_private_key_from_bad_cbor() { - test_private_key_from_bad_cbor(SignatureAlgorithm::Es256); - } - - #[test] - #[cfg(feature = "ed25519")] - fn test_ed25519_private_key_from_bad_cbor() { - test_private_key_from_bad_cbor(SignatureAlgorithm::Eddsa); - } - - #[test] - fn test_private_key_from_bad_cbor_unsupported_algo() { - let cbor = cbor_array![ - // This algorithms doesn't exist. - cbor_int!(-1), - cbor_bytes!(vec![0x88; 32]), - ]; - assert_eq!( - PrivateKey::try_from(cbor), - Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR), - ); - } } diff --git a/libraries/opensk/src/ctap/ctap1.rs b/libraries/opensk/src/ctap/ctap1.rs index 6d598ce..87beb1c 100644 --- a/libraries/opensk/src/ctap/ctap1.rs +++ b/libraries/opensk/src/ctap/ctap1.rs @@ -13,12 +13,12 @@ // limitations under the License. use super::apdu::{Apdu, ApduStatusCode}; -use super::credential_id::{decrypt_credential_id, encrypt_to_credential_id}; -use super::crypto_wrapper::PrivateKey; use super::CtapState; use crate::api::attestation_store::{self, Attestation, AttestationStore}; use crate::api::crypto::ecdsa::{self, SecretKey as _, Signature}; use crate::api::crypto::EC_FIELD_SIZE; +use crate::api::key_store::{CredentialSource, KeyStore}; +use crate::api::private_key::PrivateKey; use crate::env::{EcdsaSk, Env}; use alloc::vec::Vec; use arrayref::{array_ref, mut_array_refs}; @@ -259,7 +259,15 @@ impl Ctap1Command { .ecdsa_key(env) .map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?; let pk = sk.public_key(); - let key_handle = encrypt_to_credential_id(env, &private_key, &application, None, None) + let credential_source = CredentialSource { + private_key, + rp_id_hash: application, + cred_protect_policy: None, + cred_blob: None, + }; + let key_handle = env + .key_store() + .wrap_credential(credential_source) .map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?; if key_handle.len() > 0xFF { // This is just being defensive with unreachable code. @@ -319,7 +327,9 @@ impl Ctap1Command { flags: Ctap1Flags, ctap_state: &mut CtapState, ) -> Result, Ctap1StatusCode> { - let credential_source = decrypt_credential_id(env, key_handle, &application) + let credential_source = env + .key_store() + .unwrap_credential(&key_handle, &application) .map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?; if let Some(credential_source) = credential_source { let ecdsa_key = credential_source @@ -353,12 +363,12 @@ impl Ctap1Command { #[cfg(test)] mod test { - use super::super::credential_id::CBOR_CREDENTIAL_ID_SIZE; use super::super::data_formats::SignatureAlgorithm; use super::super::TOUCH_TIMEOUT_MS; use super::*; use crate::api::crypto::sha256::Sha256; use crate::api::customization::Customization; + use crate::api::key_store::CBOR_CREDENTIAL_ID_SIZE; use crate::ctap::secret::Secret; use crate::ctap::storage; use crate::env::test::TestEnv; @@ -401,6 +411,20 @@ mod test { message } + /// Creates an example wrapped credential and RP ID hash. + fn create_wrapped_credential(env: &mut TestEnv) -> (Vec, [u8; 32]) { + let private_key = PrivateKey::new(env, SignatureAlgorithm::Es256); + let rp_id_hash = Sha::::digest(b"example.com"); + let credential_source = CredentialSource { + private_key, + rp_id_hash, + cred_protect_policy: None, + cred_blob: None, + }; + let key_handle = env.key_store().wrap_credential(credential_source).unwrap(); + (key_handle, rp_id_hash) + } + #[test] fn test_process_allowed() { let mut env = TestEnv::default(); @@ -444,13 +468,11 @@ mod test { let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state).unwrap(); assert_eq!(response[0], Ctap1Command::LEGACY_BYTE); assert_eq!(response[66], CBOR_CREDENTIAL_ID_SIZE as u8); - assert!(decrypt_credential_id( - &mut env, - response[67..67 + CBOR_CREDENTIAL_ID_SIZE].to_vec(), - &application, - ) - .unwrap() - .is_some()); + let credential_source = env + .key_store() + .unwrap_credential(&response[67..67 + CBOR_CREDENTIAL_ID_SIZE], &application) + .unwrap(); + assert!(credential_source.is_some()); const CERT_START: usize = 67 + CBOR_CREDENTIAL_ID_SIZE; assert_eq!( &response[CERT_START..][..attestation.certificate.len()], @@ -495,12 +517,9 @@ mod test { let mut env = TestEnv::default(); env.user_presence() .set(|| panic!("Unexpected user presence check in CTAP1")); - let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256); let mut ctap_state = CtapState::new(&mut env); - let rp_id = "example.com"; - let application = Sha::::digest(rp_id.as_bytes()); - let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap(); + let (key_handle, application) = create_wrapped_credential(&mut env); let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state); @@ -512,13 +531,10 @@ mod test { let mut env = TestEnv::default(); env.user_presence() .set(|| panic!("Unexpected user presence check in CTAP1")); - let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256); let mut ctap_state = CtapState::new(&mut env); - let rp_id = "example.com"; - let application = Sha::::digest(rp_id.as_bytes()); - let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap(); - let application = [0x55; 32]; + let (key_handle, mut application) = create_wrapped_credential(&mut env); + application[0] ^= 0x01; let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); let response = Ctap1Command::process_command(&mut env, &message, &mut ctap_state); @@ -530,12 +546,9 @@ mod test { let mut env = TestEnv::default(); env.user_presence() .set(|| panic!("Unexpected user presence check in CTAP1")); - let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256); let mut ctap_state = CtapState::new(&mut env); - let rp_id = "example.com"; - let application = Sha::::digest(rp_id.as_bytes()); - let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap(); + let (key_handle, application) = create_wrapped_credential(&mut env); let mut message = create_authenticate_message( &application, Ctap1Flags::DontEnforceUpAndSign, @@ -564,12 +577,9 @@ mod test { let mut env = TestEnv::default(); env.user_presence() .set(|| panic!("Unexpected user presence check in CTAP1")); - let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256); let mut ctap_state = CtapState::new(&mut env); - let rp_id = "example.com"; - let application = Sha::::digest(rp_id.as_bytes()); - let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap(); + let (key_handle, application) = create_wrapped_credential(&mut env); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); message[0] = 0xEE; @@ -583,12 +593,9 @@ mod test { let mut env = TestEnv::default(); env.user_presence() .set(|| panic!("Unexpected user presence check in CTAP1")); - let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256); let mut ctap_state = CtapState::new(&mut env); - let rp_id = "example.com"; - let application = Sha::::digest(rp_id.as_bytes()); - let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap(); + let (key_handle, application) = create_wrapped_credential(&mut env); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); message[1] = 0xEE; @@ -602,12 +609,9 @@ mod test { let mut env = TestEnv::default(); env.user_presence() .set(|| panic!("Unexpected user presence check in CTAP1")); - let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256); let mut ctap_state = CtapState::new(&mut env); - let rp_id = "example.com"; - let application = Sha::::digest(rp_id.as_bytes()); - let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap(); + let (key_handle, application) = create_wrapped_credential(&mut env); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); message[2] = 0xEE; @@ -629,12 +633,9 @@ mod test { let mut env = TestEnv::default(); env.user_presence() .set(|| panic!("Unexpected user presence check in CTAP1")); - let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256); let mut ctap_state = CtapState::new(&mut env); - let rp_id = "example.com"; - let application = Sha::::digest(rp_id.as_bytes()); - let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap(); + let (key_handle, application) = create_wrapped_credential(&mut env); let message = create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle); @@ -655,12 +656,9 @@ mod test { let mut env = TestEnv::default(); env.user_presence() .set(|| panic!("Unexpected user presence check in CTAP1")); - let sk = PrivateKey::new(&mut env, SignatureAlgorithm::Es256); let mut ctap_state = CtapState::new(&mut env); - let rp_id = "example.com"; - let application = Sha::::digest(rp_id.as_bytes()); - let key_handle = encrypt_to_credential_id(&mut env, &sk, &application, None, None).unwrap(); + let (key_handle, application) = create_wrapped_credential(&mut env); let message = create_authenticate_message( &application, Ctap1Flags::DontEnforceUpAndSign, diff --git a/libraries/opensk/src/ctap/data_formats.rs b/libraries/opensk/src/ctap/data_formats.rs index 3021449..d10d240 100644 --- a/libraries/opensk/src/ctap/data_formats.rs +++ b/libraries/opensk/src/ctap/data_formats.rs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::crypto_wrapper::PrivateKey; use super::status_code::Ctap2StatusCode; use crate::api::crypto::{ecdh, ecdsa, EC_FIELD_SIZE}; +use crate::api::private_key::PrivateKey; use alloc::string::String; use alloc::vec::Vec; #[cfg(feature = "fuzz")] diff --git a/libraries/opensk/src/ctap/mod.rs b/libraries/opensk/src/ctap/mod.rs index 4340e98..177f5d7 100644 --- a/libraries/opensk/src/ctap/mod.rs +++ b/libraries/opensk/src/ctap/mod.rs @@ -16,9 +16,8 @@ pub mod apdu; mod client_pin; pub mod command; mod config_command; -mod credential_id; mod credential_management; -mod crypto_wrapper; +pub mod crypto_wrapper; #[cfg(feature = "with_ctap1")] mod ctap1; pub mod data_formats; @@ -41,11 +40,7 @@ use self::command::{ AuthenticatorGetAssertionParameters, AuthenticatorMakeCredentialParameters, Command, }; use self::config_command::process_config; -use self::credential_id::{ - decrypt_credential_id, encrypt_to_credential_id, MAX_CREDENTIAL_ID_SIZE, -}; use self::credential_management::process_credential_management; -use self::crypto_wrapper::PrivateKey; use self::data_formats::{ AuthenticatorTransport, CredentialProtectionPolicy, EnterpriseAttestationMode, GetAssertionExtensions, PackedAttestationStatement, PinUvAuthProtocol, @@ -70,7 +65,8 @@ 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::key_store::{CredentialSource, KeyStore, MAX_CREDENTIAL_ID_SIZE}; +use crate::api::private_key::PrivateKey; use crate::api::rng::Rng; use crate::api::user_presence::{UserPresence, UserPresenceError}; use crate::env::{EcdsaSk, Env, Hkdf, Sha}; @@ -188,6 +184,30 @@ pub fn cbor_write(value: cbor::Value, encoded_cbor: &mut Vec) -> Result<(), .map_err(|_e| Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR) } +fn decrypt_credential_id( + env: &mut E, + credential_id: Vec, + rp_id_hash: &[u8], +) -> Result, Ctap2StatusCode> { + let credential_source = env + .key_store() + .unwrap_credential(&credential_id, rp_id_hash)?; + Ok(credential_source.map(|c| PublicKeyCredentialSource { + key_type: PublicKeyCredentialType::PublicKey, + credential_id, + private_key: c.private_key, + rp_id: String::new(), + user_handle: Vec::new(), + user_display_name: None, + cred_protect_policy: c.cred_protect_policy, + creation_order: 0, + user_name: None, + user_icon: None, + cred_blob: c.cred_blob, + large_blob_key: None, + })) +} + // 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. // We change the return value, since we don't need the bool. @@ -862,13 +882,15 @@ impl CtapState { storage::store_credential(env, credential_source)?; random_id } else { - encrypt_to_credential_id( - env, - &private_key, - &rp_id_hash, + let credential_source = CredentialSource { + private_key: private_key.clone(), + rp_id_hash, cred_protect_policy, cred_blob, - )? + }; + env.key_store() + .wrap_credential(credential_source) + .map_err(|_| Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)? }; let mut auth_data = self.generate_auth_data(env, &rp_id_hash, flags)?; @@ -1361,7 +1383,6 @@ mod test { use super::command::{ AuthenticatorClientPinParameters, AuthenticatorCredentialManagementParameters, }; - use super::credential_id::CBOR_CREDENTIAL_ID_SIZE; use super::data_formats::{ ClientPinSubCommand, CoseKey, CredentialManagementSubCommand, GetAssertionHmacSecretInput, GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions, PinUvAuthProtocol, @@ -1371,6 +1392,7 @@ mod test { use super::*; use crate::api::crypto::ecdh::SecretKey as _; use crate::api::customization; + use crate::api::key_store::CBOR_CREDENTIAL_ID_SIZE; use crate::api::user_presence::UserPresenceResult; use crate::env::test::TestEnv; use crate::env::EcdhSk; diff --git a/libraries/opensk/src/ctap/storage.rs b/libraries/opensk/src/ctap/storage.rs index 7f3df85..a781f79 100644 --- a/libraries/opensk/src/ctap/storage.rs +++ b/libraries/opensk/src/ctap/storage.rs @@ -598,8 +598,8 @@ fn serialize_min_pin_length_rp_ids(rp_ids: Vec) -> Result, Ctap2 mod test { use super::*; use crate::api::attestation_store::{self, Attestation, AttestationStore}; + use crate::api::private_key::PrivateKey; use crate::api::rng::Rng; - use crate::ctap::crypto_wrapper::PrivateKey; use crate::ctap::data_formats::{ CredentialProtectionPolicy, PublicKeyCredentialSource, PublicKeyCredentialType, };