From 983bc5c5b2121b5678df8a5163987f149cc9f67b Mon Sep 17 00:00:00 2001 From: Egor Duda Date: Thu, 12 May 2022 22:56:33 +0300 Subject: [PATCH 01/20] Add support for ed25519 keys (#335) --- Cargo.toml | 2 + deploy.py | 10 +- src/ctap/crypto_wrapper.rs | 235 ++++++++++++++++++++++++++++++++----- src/ctap/data_formats.rs | 88 ++++++++++++-- src/ctap/mod.rs | 35 +++++- third_party/libtock-rs | 2 +- 6 files changed, 325 insertions(+), 47 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 29527e7..8da47c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ serde_json = { version = "=1.0.69", default-features = false, features = ["alloc embedded-time = "0.12.1" arbitrary = { version = "0.4.7", features = ["derive"], optional = true } rand = { version = "0.8.4", optional = true } +ed25519-dalek = { version = "1", default-features = false, features = ["u32_backend", "rand"], optional = true } [features] debug_allocations = ["lang_items/debug_allocations"] @@ -36,6 +37,7 @@ with_ctap1 = ["crypto/with_ctap1"] with_nfc = ["libtock_drivers/with_nfc"] vendor_hid = ["libtock_drivers/vendor_hid"] fuzz = ["arbitrary", "std"] +with_ed25519 = ["ed25519-dalek"] [dev-dependencies] enum-iterator = "0.6.0" diff --git a/deploy.py b/deploy.py index 9b92fd4..24c1817 100755 --- a/deploy.py +++ b/deploy.py @@ -442,8 +442,6 @@ class OpenSKInstaller: f"link-arg=-T{props.app_ldscript}", "-C", "relocation-model=static", - "-D", - "warnings", f"--remap-path-prefix={os.getcwd()}=", "-C", "link-arg=-icf=all", @@ -1091,6 +1089,14 @@ if __name__ == "__main__": help=("When set, the output of elf2tab is appended to this file."), ) + main_parser.add_argument( + "--with_ed25519", + action="append_const", + const="with_ed25519", + dest="features", + help=("Enable Ed25519 support"), + ) + main_parser.set_defaults(features=["with_ctap1"]) # Start parsing to know if we're going to list things or not. diff --git a/src/ctap/crypto_wrapper.rs b/src/ctap/crypto_wrapper.rs index 3420081..68c7d01 100644 --- a/src/ctap/crypto_wrapper.rs +++ b/src/ctap/crypto_wrapper.rs @@ -14,7 +14,7 @@ use crate::ctap::data_formats::{ extract_array, extract_byte_string, CoseKey, PublicKeyCredentialSource, - PublicKeyCredentialType, SignatureAlgorithm, + PublicKeyCredentialType, SignatureAlgorithm, ES256_ALGORITHM, EDDSA_ALGORITHM, }; use crate::ctap::status_code::Ctap2StatusCode; use crate::ctap::storage; @@ -30,6 +30,8 @@ use crypto::sha256::Sha256; use rng256::Rng256; use sk_cbor as cbor; use sk_cbor::{cbor_array, cbor_bytes, cbor_int}; +#[cfg(feature = "with_ed25519")] +use ed25519_dalek::Signer; // Legacy credential IDs consist of // - 16 bytes: initialization vector for AES-256, @@ -42,6 +44,16 @@ pub const ECDSA_CREDENTIAL_ID_SIZE: usize = 113; // See encrypt_key_handle v1 documentation. pub const MAX_CREDENTIAL_ID_SIZE: usize = 113; +const ECDSA_CREDENTIAL_ID_VERSION: u8 = 0x01; +#[cfg(feature = "with_ed25519")] +const ED25519_CREDENTIAL_ID_VERSION: u8 = 0x02; +#[cfg(test)] +#[cfg(feature = "with_ed25519")] +const UNSUPPORTED_CREDENTIAL_ID_VERSION: u8 = 0x03; +#[cfg(test)] +#[cfg(not(feature = "with_ed25519"))] +const UNSUPPORTED_CREDENTIAL_ID_VERSION: u8 = 0x02; + /// Wraps the AES256-CBC encryption to match what we need in CTAP. pub fn aes256_cbc_encrypt( rng: &mut dyn Rng256, @@ -89,11 +101,37 @@ pub fn aes256_cbc_decrypt( } /// An asymmetric private key that can sign messages. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Debug)] pub enum PrivateKey { Ecdsa(ecdsa::SecKey), + #[cfg(feature = "with_ed25519")] + Ed25519(ed25519_dalek::Keypair), } +impl Clone for PrivateKey { + fn clone(&self) -> Self { + match self { + Self::Ecdsa(sk) => Self::Ecdsa (sk.clone ()), + #[cfg(feature = "with_ed25519")] + Self::Ed25519(keypair) => Self::Ed25519 (ed25519_dalek::Keypair::from_bytes (&keypair.to_bytes()).unwrap()), + } + } +} + +impl PartialEq for PrivateKey { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (&Self::Ecdsa(ref a), &Self::Ecdsa(ref b)) => a == b, + #[cfg(feature = "with_ed25519")] + (&Self::Ed25519(ref a), &Self::Ed25519(ref b)) => a.to_bytes() == b.to_bytes(), + #[cfg(feature = "with_ed25519")] + _ => false, + } + } +} + +impl Eq for PrivateKey {} + impl PrivateKey { /// Creates a new private key for the given algorithm. /// @@ -103,6 +141,11 @@ impl PrivateKey { pub fn new(rng: &mut impl Rng256, alg: SignatureAlgorithm) -> Self { match alg { SignatureAlgorithm::ES256 => PrivateKey::Ecdsa(crypto::ecdsa::SecKey::gensk(rng)), + #[cfg(feature = "with_ed25519")] + SignatureAlgorithm::EDDSA => { + let bytes = rng.gen_uniform_u8x32(); + Self::new_ed25519_from_bytes(&bytes).unwrap() + }, SignatureAlgorithm::Unknown => unreachable!(), } } @@ -117,10 +160,25 @@ impl PrivateKey { ecdsa::SecKey::from_bytes(array_ref!(bytes, 0, 32)).map(PrivateKey::from) } + #[cfg(feature = "with_ed25519")] + pub fn new_ed25519_from_bytes(bytes: &[u8]) -> Option { + if bytes.len() != 32 { + return None; + } + if let Ok(secret) = ed25519_dalek::SecretKey::from_bytes(bytes) { + let public = ed25519_dalek::PublicKey::from (&secret); + Some(Self::Ed25519(ed25519_dalek::Keypair{secret, public})) + } else { + None + } + } + /// Returns the corresponding public key. pub fn get_pub_key(&self) -> CoseKey { match self { PrivateKey::Ecdsa(ecdsa_key) => CoseKey::from(ecdsa_key.genpk()), + #[cfg(feature = "with_ed25519")] + PrivateKey::Ed25519(ed25519_keypair) => CoseKey::from(ed25519_keypair.public), } } @@ -128,6 +186,8 @@ impl PrivateKey { pub fn sign_and_encode(&self, message: &[u8]) -> Vec { match self { PrivateKey::Ecdsa(ecdsa_key) => ecdsa_key.sign_rfc6979::(message).to_asn1_der(), + #[cfg(feature = "with_ed25519")] + PrivateKey::Ed25519(ed25519_keypair) => ed25519_keypair.try_sign(message).unwrap().to_bytes().to_vec(), } } @@ -135,6 +195,8 @@ impl PrivateKey { pub fn signature_algorithm(&self) -> SignatureAlgorithm { match self { PrivateKey::Ecdsa(_) => SignatureAlgorithm::ES256, + #[cfg(feature = "with_ed25519")] + PrivateKey::Ed25519(_) => SignatureAlgorithm::EDDSA, } } @@ -146,6 +208,8 @@ impl PrivateKey { ecdsa_key.to_bytes(array_mut_ref!(key_bytes, 0, 32)); key_bytes } + #[cfg(feature = "with_ed25519")] + PrivateKey::Ed25519(ed25519_keypair) => ed25519_keypair.secret.to_bytes().to_vec(), } } } @@ -171,6 +235,9 @@ impl TryFrom for PrivateKey { 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 = "with_ed25519")] + SignatureAlgorithm::EDDSA => PrivateKey::new_ed25519_from_bytes(&key_bytes) + .ok_or(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR), _ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR), } } @@ -190,12 +257,19 @@ impl From for PrivateKey { /// Also, by limiting ourselves to private key and RP ID hash, we are compatible with U2F for /// ECDSA private keys. /// -/// This is v1, and we write the following data for ECDSA (algorithm -7): +/// For v1 we write the following data for ECDSA (algorithm -7): /// - 1 byte : version number /// - 16 bytes: initialization vector for AES-256, /// - 32 bytes: ECDSA private key for the credential, /// - 32 bytes: relying party ID hashed with SHA256, /// - 32 bytes: HMAC-SHA256 over everything else. +/// +/// For v2 we write the following data for EdDSA over curve Ed25519 (algorithm -8, curve 6): +/// - 1 byte : version number +/// - 16 bytes: initialization vector for AES-256, +/// - 32 bytes: Ed25519 private key for the credential, +/// - 32 bytes: relying party ID hashed with SHA256, +/// - 32 bytes: HMAC-SHA256 over everything else. pub fn encrypt_key_handle( env: &mut impl Env, private_key: &PrivateKey, @@ -204,17 +278,22 @@ pub fn encrypt_key_handle( let master_keys = storage::master_keys(env)?; let aes_enc_key = crypto::aes256::EncryptionKey::new(&master_keys.encryption); - let mut encrypted_id = match private_key { + let mut plaintext = [0; 64]; + let version; + match private_key { PrivateKey::Ecdsa(ecdsa_key) => { - let mut plaintext = [0; 64]; ecdsa_key.to_bytes(array_mut_ref!(plaintext, 0, 32)); - plaintext[32..64].copy_from_slice(application); - let mut encrypted_id = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true)?; - // Version number - encrypted_id.insert(0, 0x01); - encrypted_id + version = ECDSA_CREDENTIAL_ID_VERSION; + } + #[cfg(feature = "with_ed25519")] + PrivateKey::Ed25519(keypair) => { + plaintext[0..32].copy_from_slice(&keypair.secret.to_bytes()); + version = ED25519_CREDENTIAL_ID_VERSION; } }; + plaintext[32..64].copy_from_slice(application); + let mut encrypted_id = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true)?; + encrypted_id.insert(0, version); let id_hmac = hmac_256::(&master_keys.hmac, &encrypted_id[..]); encrypted_id.extend(&id_hmac); @@ -231,6 +310,7 @@ pub fn encrypt_key_handle( /// This functions reads: /// - legacy credentials (no version number), /// - v1 (ECDSA) +/// - v2 (EdDSA over curve Ed25519) pub fn decrypt_credential_source( env: &mut impl Env, credential_id: Vec, @@ -249,12 +329,17 @@ pub fn decrypt_credential_source( return Ok(None); } + let algorithm; let payload = if credential_id.len() == LEGACY_CREDENTIAL_ID_SIZE { + algorithm = ES256_ALGORITHM; &credential_id[..hmac_message_size] } else { // Version number check - if credential_id[0] != 1 { - return Ok(None); + match credential_id[0] { + ECDSA_CREDENTIAL_ID_VERSION => algorithm = ES256_ALGORITHM, + #[cfg(feature = "with_ed25519")] + ED25519_CREDENTIAL_ID_VERSION => algorithm = EDDSA_ALGORITHM, + _ => return Ok(None), } &credential_id[1..hmac_message_size] }; @@ -269,7 +354,15 @@ pub fn decrypt_credential_source( if rp_id_hash != &decrypted_id[32..] { return Ok(None); } - let sk_option = PrivateKey::new_ecdsa_from_bytes(&decrypted_id[..32]); + let sk_option; + match algorithm { + ES256_ALGORITHM => sk_option = PrivateKey::new_ecdsa_from_bytes(&decrypted_id[..32]), + #[cfg(feature = "with_ed25519")] + EDDSA_ALGORITHM => sk_option = PrivateKey::new_ed25519_from_bytes(&decrypted_id[..32]), + #[cfg(not(feature = "with_ed25519"))] + EDDSA_ALGORITHM => return Ok(None), + _ => return Ok(None), + } Ok(sk_option.map(|sk| PublicKeyCredentialSource { key_type: PublicKeyCredentialType::PublicKey, @@ -368,6 +461,18 @@ mod test { ); } + #[test] + #[cfg(feature = "with_ed25519")] + fn test_new_ed25519_from_bytes() { + let mut env = TestEnv::new(); + let private_key = PrivateKey::new(env.rng(), 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); @@ -376,6 +481,15 @@ mod test { assert_eq!(PrivateKey::new_ecdsa_from_bytes(&[0x55; 64]), None); } + #[test] + #[cfg(feature = "with_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::new(); @@ -395,22 +509,41 @@ mod test { assert_eq!(private_key.sign_and_encode(&message), signature); } - #[test] - fn test_private_key_signature_algorithm() { + fn test_private_key_signature_algorithm(signature_algorithm: SignatureAlgorithm) { let mut env = TestEnv::new(); - let algorithm = SignatureAlgorithm::ES256; - let private_key = PrivateKey::new(env.rng(), algorithm); - assert_eq!(private_key.signature_algorithm(), algorithm); + let private_key = PrivateKey::new(env.rng(), signature_algorithm); + assert_eq!(private_key.signature_algorithm(), signature_algorithm); } #[test] - fn test_private_key_from_to_cbor() { + fn test_ecdsa_private_key_signature_algorithm() { + test_private_key_signature_algorithm(SignatureAlgorithm::ES256); + } + + #[test] + #[cfg(feature = "with_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::new(); - let private_key = PrivateKey::new(env.rng(), SignatureAlgorithm::ES256); + let private_key = PrivateKey::new(env.rng(), signature_algorithm); let cbor = cbor::Value::from(private_key.clone()); 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 = "with_ed25519")] + fn test_ed25519_private_key_from_to_cbor() { + test_private_key_from_to_cbor(SignatureAlgorithm::EDDSA); + } + #[test] fn test_private_key_from_bad_cbor() { let cbor = cbor_array![ @@ -424,6 +557,20 @@ mod test { Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR), ); + #[cfg(feature = "with_ed25519")] + { + let cbor = cbor_array![ + cbor_int!(SignatureAlgorithm::EDDSA 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), + ); + } + let cbor = cbor_array![ // This algorithms doesn't exist. cbor_int!(-1), @@ -435,11 +582,10 @@ mod test { ); } - #[test] - fn test_encrypt_decrypt_credential() { + fn test_encrypt_decrypt_credential(signature_algorithm: SignatureAlgorithm) { let mut env = TestEnv::new(); storage::init(&mut env).ok().unwrap(); - let private_key = PrivateKey::new(env.rng(), SignatureAlgorithm::ES256); + let private_key = PrivateKey::new(env.rng(), signature_algorithm); let rp_id_hash = [0x55; 32]; let encrypted_id = encrypt_key_handle(&mut env, &private_key, &rp_id_hash).unwrap(); @@ -450,6 +596,17 @@ mod test { assert_eq!(private_key, decrypted_source.private_key); } + #[test] + fn test_encrypt_decrypt_ecdsa_credential() { + test_encrypt_decrypt_credential(SignatureAlgorithm::ES256); + } + + #[test] + #[cfg(feature = "with_ed25519")] + fn test_encrypt_decrypt_ed25519_credential() { + test_encrypt_decrypt_credential(SignatureAlgorithm::EDDSA); + } + #[test] fn test_encrypt_decrypt_bad_version() { let mut env = TestEnv::new(); @@ -459,7 +616,7 @@ mod test { let rp_id_hash = [0x55; 32]; let mut encrypted_id = encrypt_key_handle(&mut env, &private_key, &rp_id_hash).unwrap(); // Version 2 does not exist yet. - encrypted_id[0] = 0x02; + encrypted_id[0] = UNSUPPORTED_CREDENTIAL_ID_VERSION; // Override the HMAC to pass the check. encrypted_id.truncate(&encrypted_id.len() - 32); let master_keys = storage::master_keys(&mut env).unwrap(); @@ -472,11 +629,10 @@ mod test { ); } - #[test] - fn test_encrypt_decrypt_bad_hmac() { + fn test_encrypt_decrypt_bad_hmac(signature_algorithm: SignatureAlgorithm) { let mut env = TestEnv::new(); storage::init(&mut env).ok().unwrap(); - let private_key = PrivateKey::new(env.rng(), SignatureAlgorithm::ES256); + let private_key = PrivateKey::new(env.rng(), signature_algorithm); let rp_id_hash = [0x55; 32]; let encrypted_id = encrypt_key_handle(&mut env, &private_key, &rp_id_hash).unwrap(); @@ -491,10 +647,20 @@ mod test { } #[test] - fn test_decrypt_credential_missing_blocks() { + fn test_ecdsa_encrypt_decrypt_bad_hmac() { + test_encrypt_decrypt_bad_hmac(SignatureAlgorithm::ES256); + } + + #[test] + #[cfg(feature = "with_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::new(); storage::init(&mut env).ok().unwrap(); - let private_key = PrivateKey::new(env.rng(), SignatureAlgorithm::ES256); + let private_key = PrivateKey::new(env.rng(), signature_algorithm); let rp_id_hash = [0x55; 32]; let encrypted_id = encrypt_key_handle(&mut env, &private_key, &rp_id_hash).unwrap(); @@ -507,6 +673,17 @@ mod test { } } + #[test] + fn test_ecdsa_decrypt_credential_missing_blocks() { + test_decrypt_credential_missing_blocks(SignatureAlgorithm::ES256); + } + + #[test] + #[cfg(feature = "with_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_key_handle( env: &mut impl Env, diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index b5866de..e7c9cc4 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -27,7 +27,8 @@ use sk_cbor as cbor; use sk_cbor::{cbor_array_vec, cbor_map, cbor_map_options, destructure_cbor_map}; // Used as the identifier for ECDSA in assertion signatures and COSE. -const ES256_ALGORITHM: i64 = -7; +pub const ES256_ALGORITHM: i64 = -7; +pub const EDDSA_ALGORITHM: i64 = -8; // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrpentity #[derive(Clone, Debug, PartialEq, Eq)] @@ -503,6 +504,8 @@ impl From for cbor::Value { #[cfg_attr(feature = "fuzz", derive(Arbitrary))] pub enum SignatureAlgorithm { ES256 = ES256_ALGORITHM as isize, + #[cfg(feature = "with_ed25519")] + EDDSA = EDDSA_ALGORITHM as isize, // This is the default for all numbers not covered above. // Unknown types should be ignored, instead of returning errors. Unknown = 0, @@ -518,6 +521,8 @@ impl From for SignatureAlgorithm { fn from(int: i64) -> Self { match int { ES256_ALGORITHM => SignatureAlgorithm::ES256, + #[cfg(feature = "with_ed25519")] + EDDSA_ALGORITHM => SignatureAlgorithm::EDDSA, _ => SignatureAlgorithm::Unknown, } } @@ -720,6 +725,8 @@ pub struct CoseKey { x_bytes: [u8; ecdh::NBYTES], y_bytes: [u8; ecdh::NBYTES], algorithm: i64, + key_type: i64, + curve: i64, } impl CoseKey { @@ -729,8 +736,12 @@ impl CoseKey { const ECDH_ALGORITHM: i64 = -25; // The parameter behind map key 1. const EC2_KEY_TYPE: i64 = 2; + #[cfg(feature = "with_ed25519")] + const OKP_KEY_TYPE: i64 = 1; // The parameter behind map key -1. const P_256_CURVE: i64 = 1; + #[cfg(feature = "with_ed25519")] + const ED25519_CURVE: i64 = 6; } // This conversion accepts both ECDH and ECDSA. @@ -776,6 +787,8 @@ impl TryFrom for CoseKey { x_bytes: *array_ref![x_bytes.as_slice(), 0, ecdh::NBYTES], y_bytes: *array_ref![y_bytes.as_slice(), 0, ecdh::NBYTES], algorithm, + key_type, + curve, }) } } @@ -786,12 +799,14 @@ impl From for cbor::Value { x_bytes, y_bytes, algorithm, + key_type, + curve, } = cose_key; cbor_map! { - 1 => CoseKey::EC2_KEY_TYPE, + 1 => key_type, 3 => algorithm, - -1 => CoseKey::P_256_CURVE, + -1 => curve, -2 => x_bytes, -3 => y_bytes, } @@ -807,6 +822,8 @@ impl From for CoseKey { x_bytes, y_bytes, algorithm: CoseKey::ECDH_ALGORITHM, + key_type: CoseKey::EC2_KEY_TYPE, + curve: CoseKey::P_256_CURVE, } } } @@ -820,6 +837,21 @@ impl From for CoseKey { x_bytes, y_bytes, algorithm: ES256_ALGORITHM, + key_type: CoseKey::EC2_KEY_TYPE, + curve: CoseKey::P_256_CURVE, + } + } +} + +#[cfg(feature = "with_ed25519")] +impl From for CoseKey { + fn from(pk: ed25519_dalek::PublicKey) -> Self { + CoseKey { + x_bytes: pk.to_bytes(), + y_bytes: [0u8; 32], + key_type: CoseKey::OKP_KEY_TYPE, + curve: CoseKey::ED25519_CURVE, + algorithm: EDDSA_ALGORITHM, } } } @@ -832,6 +864,8 @@ impl TryFrom for ecdh::PubKey { x_bytes, y_bytes, algorithm, + key_type, + curve, } = cose_key; // Since algorithm can be used for different COSE key types, we check @@ -841,6 +875,9 @@ impl TryFrom for ecdh::PubKey { if algorithm != CoseKey::ECDH_ALGORITHM && algorithm != ES256_ALGORITHM { return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); } + if key_type != CoseKey::EC2_KEY_TYPE || curve != CoseKey::P_256_CURVE { + return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); + } ecdh::PubKey::from_coordinates(&x_bytes, &y_bytes) .ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) } @@ -854,9 +891,11 @@ impl TryFrom for ecdsa::PubKey { x_bytes, y_bytes, algorithm, + key_type, + curve, } = cose_key; - if algorithm != ES256_ALGORITHM { + if algorithm != ES256_ALGORITHM || key_type != CoseKey::EC2_KEY_TYPE || curve != CoseKey::P_256_CURVE { return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); } ecdsa::PubKey::from_coordinates(&x_bytes, &y_bytes) @@ -904,7 +943,11 @@ impl TryFrom for ecdsa::Signature { match cose_signature.algorithm { SignatureAlgorithm::ES256 => ecdsa::Signature::from_bytes(&cose_signature.bytes) .ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), - SignatureAlgorithm::Unknown => Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM), + #[cfg(feature = "with_ed25519")] + SignatureAlgorithm::EDDSA => + Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM), + SignatureAlgorithm::Unknown => + Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM), } } } @@ -1564,6 +1607,13 @@ mod test { let signature_algorithm = SignatureAlgorithm::from(alg_int); assert_eq!(signature_algorithm, SignatureAlgorithm::ES256); + #[cfg(feature = "with_ed25519")] + { + let alg_int = SignatureAlgorithm::EDDSA as i64; + let signature_algorithm = SignatureAlgorithm::from(alg_int); + assert_eq!(signature_algorithm, SignatureAlgorithm::EDDSA); + } + let unknown_alg_int = -1; let unknown_algorithm = SignatureAlgorithm::from(unknown_alg_int); assert_eq!(unknown_algorithm, SignatureAlgorithm::Unknown); @@ -1578,6 +1628,16 @@ mod test { let created_cbor: cbor::Value = signature_algorithm.unwrap().into(); assert_eq!(created_cbor, cbor_signature_algorithm); + #[cfg(feature = "with_ed25519")] + { + let cbor_signature_algorithm: cbor::Value = cbor_int!(EDDSA_ALGORITHM); + let signature_algorithm = SignatureAlgorithm::try_from(cbor_signature_algorithm.clone()); + let expected_signature_algorithm = SignatureAlgorithm::EDDSA; + assert_eq!(signature_algorithm, Ok(expected_signature_algorithm)); + let created_cbor: cbor::Value = signature_algorithm.unwrap().into(); + assert_eq!(created_cbor, cbor_signature_algorithm); + } + let cbor_unknown_algorithm: cbor::Value = cbor_int!(-1); let unknown_algorithm = SignatureAlgorithm::try_from(cbor_unknown_algorithm); let expected_unknown_algorithm = SignatureAlgorithm::Unknown; @@ -1641,23 +1701,33 @@ mod test { } } - #[test] - fn test_from_into_public_key_credential_parameter() { + fn test_from_into_public_key_credential_parameter(alg_int: i64, signature_algorithm: SignatureAlgorithm) { let cbor_credential_parameter = cbor_map! { - "alg" => ES256_ALGORITHM, + "alg" => alg_int, "type" => "public-key", }; let credential_parameter = PublicKeyCredentialParameter::try_from(cbor_credential_parameter.clone()); let expected_credential_parameter = PublicKeyCredentialParameter { cred_type: PublicKeyCredentialType::PublicKey, - alg: SignatureAlgorithm::ES256, + alg: signature_algorithm, }; assert_eq!(credential_parameter, Ok(expected_credential_parameter)); let created_cbor: cbor::Value = credential_parameter.unwrap().into(); assert_eq!(created_cbor, cbor_credential_parameter); } + #[test] + fn test_from_into_ecdsa_public_key_credential_parameter() { + test_from_into_public_key_credential_parameter(ES256_ALGORITHM, SignatureAlgorithm::ES256); + } + + #[test] + #[cfg(feature = "with_ed25519")] + fn test_from_into_ed25519_public_key_credential_parameter() { + test_from_into_public_key_credential_parameter(EDDSA_ALGORITHM, SignatureAlgorithm::EDDSA); + } + #[test] fn test_from_into_public_key_credential_descriptor() { let cbor_credential_descriptor = cbor_map! { diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 4cc392d..547629d 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -117,6 +117,30 @@ pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa alg: SignatureAlgorithm::ES256, }; +#[cfg(feature = "with_ed25519")] +pub const EDDSA_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialParameter { + cred_type: PublicKeyCredentialType::PublicKey, + alg: SignatureAlgorithm::EDDSA, +}; + +fn get_supported_cred_params() -> Vec { + let mut ret_val = vec!(); + ret_val.push(ES256_CRED_PARAM); + #[cfg(feature = "with_ed25519")] + ret_val.push(EDDSA_CRED_PARAM); + ret_val +} + +fn get_preferred_cred_param (params: &Vec) -> Option<&PublicKeyCredentialParameter> { + let supported_cred_params = get_supported_cred_params(); + for param in params { + if supported_cred_params.contains(param) { + return Some(param); + } + } + return None; +} + /// Transports supported by OpenSK. /// /// An OpenSK library user annotates incoming data with this data type. @@ -604,10 +628,9 @@ impl CtapState { self.pin_uv_auth_precheck(env, &pin_uv_auth_param, pin_uv_auth_protocol, channel)?; // When more algorithms are supported, iterate and pick the first match. - if !pub_key_cred_params.contains(&ES256_CRED_PARAM) { - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); - } - let algorithm = SignatureAlgorithm::ES256; + let cred_param = get_preferred_cred_param(&pub_key_cred_params) + .ok_or(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM)?; + let algorithm = cred_param.alg; let rp_id = rp.rp_id; let ep_att = if let Some(enterprise_attestation) = enterprise_attestation { @@ -1146,7 +1169,7 @@ impl CtapState { .map(|c| c as u64), max_credential_id_length: Some(MAX_CREDENTIAL_ID_SIZE as u64), transports: Some(vec![AuthenticatorTransport::Usb]), - algorithms: Some(vec![ES256_CRED_PARAM]), + algorithms: Some(get_supported_cred_params()), max_serialized_large_blob_array: Some( env.customization().max_large_blob_array_size() as u64, ), @@ -1461,7 +1484,7 @@ mod test { 0x07 => env.customization().max_credential_count_in_list().map(|c| c as u64), 0x08 => MAX_CREDENTIAL_ID_SIZE as u64, 0x09 => cbor_array!["usb"], - 0x0A => cbor_array![ES256_CRED_PARAM], + 0x0A => cbor_array_vec!(get_supported_cred_params()), 0x0B => env.customization().max_large_blob_array_size() as u64, 0x0C => false, 0x0D => storage::min_pin_length(&mut env).unwrap() as u64, diff --git a/third_party/libtock-rs b/third_party/libtock-rs index 15e837e..1ece29b 160000 --- a/third_party/libtock-rs +++ b/third_party/libtock-rs @@ -1 +1 @@ -Subproject commit 15e837e4495df2d7a39ae6d6c057fdec515cc430 +Subproject commit 1ece29ba2b061e6d06c3344ba4c6cd6005c17eb3 From f24445b3257e006ab5863216e590680d20de10f6 Mon Sep 17 00:00:00 2001 From: Egor Duda Date: Fri, 13 May 2022 20:43:09 +0300 Subject: [PATCH 02/20] with_ed25519 -> ed25519 ("with_*" naming is discouraged) --- Cargo.toml | 2 +- deploy.py | 4 +-- src/ctap/crypto_wrapper.rs | 54 +++++++++++++++++++------------------- src/ctap/data_formats.rs | 18 ++++++------- src/ctap/mod.rs | 4 +-- 5 files changed, 41 insertions(+), 41 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8da47c2..7987184 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ with_ctap1 = ["crypto/with_ctap1"] with_nfc = ["libtock_drivers/with_nfc"] vendor_hid = ["libtock_drivers/vendor_hid"] fuzz = ["arbitrary", "std"] -with_ed25519 = ["ed25519-dalek"] +ed25519 = ["ed25519-dalek"] [dev-dependencies] enum-iterator = "0.6.0" diff --git a/deploy.py b/deploy.py index 24c1817..e99a1b2 100755 --- a/deploy.py +++ b/deploy.py @@ -1090,9 +1090,9 @@ if __name__ == "__main__": ) main_parser.add_argument( - "--with_ed25519", + "--ed25519", action="append_const", - const="with_ed25519", + const="ed25519", dest="features", help=("Enable Ed25519 support"), ) diff --git a/src/ctap/crypto_wrapper.rs b/src/ctap/crypto_wrapper.rs index 68c7d01..baca32b 100644 --- a/src/ctap/crypto_wrapper.rs +++ b/src/ctap/crypto_wrapper.rs @@ -30,7 +30,7 @@ use crypto::sha256::Sha256; use rng256::Rng256; use sk_cbor as cbor; use sk_cbor::{cbor_array, cbor_bytes, cbor_int}; -#[cfg(feature = "with_ed25519")] +#[cfg(feature = "ed25519")] use ed25519_dalek::Signer; // Legacy credential IDs consist of @@ -45,13 +45,13 @@ pub const ECDSA_CREDENTIAL_ID_SIZE: usize = 113; pub const MAX_CREDENTIAL_ID_SIZE: usize = 113; const ECDSA_CREDENTIAL_ID_VERSION: u8 = 0x01; -#[cfg(feature = "with_ed25519")] +#[cfg(feature = "ed25519")] const ED25519_CREDENTIAL_ID_VERSION: u8 = 0x02; #[cfg(test)] -#[cfg(feature = "with_ed25519")] +#[cfg(feature = "ed25519")] const UNSUPPORTED_CREDENTIAL_ID_VERSION: u8 = 0x03; #[cfg(test)] -#[cfg(not(feature = "with_ed25519"))] +#[cfg(not(feature = "ed25519"))] const UNSUPPORTED_CREDENTIAL_ID_VERSION: u8 = 0x02; /// Wraps the AES256-CBC encryption to match what we need in CTAP. @@ -104,7 +104,7 @@ pub fn aes256_cbc_decrypt( #[derive(Debug)] pub enum PrivateKey { Ecdsa(ecdsa::SecKey), - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] Ed25519(ed25519_dalek::Keypair), } @@ -112,7 +112,7 @@ impl Clone for PrivateKey { fn clone(&self) -> Self { match self { Self::Ecdsa(sk) => Self::Ecdsa (sk.clone ()), - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] Self::Ed25519(keypair) => Self::Ed25519 (ed25519_dalek::Keypair::from_bytes (&keypair.to_bytes()).unwrap()), } } @@ -122,9 +122,9 @@ impl PartialEq for PrivateKey { fn eq(&self, other: &Self) -> bool { match (self, other) { (&Self::Ecdsa(ref a), &Self::Ecdsa(ref b)) => a == b, - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] (&Self::Ed25519(ref a), &Self::Ed25519(ref b)) => a.to_bytes() == b.to_bytes(), - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] _ => false, } } @@ -141,7 +141,7 @@ impl PrivateKey { pub fn new(rng: &mut impl Rng256, alg: SignatureAlgorithm) -> Self { match alg { SignatureAlgorithm::ES256 => PrivateKey::Ecdsa(crypto::ecdsa::SecKey::gensk(rng)), - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] SignatureAlgorithm::EDDSA => { let bytes = rng.gen_uniform_u8x32(); Self::new_ed25519_from_bytes(&bytes).unwrap() @@ -160,7 +160,7 @@ impl PrivateKey { ecdsa::SecKey::from_bytes(array_ref!(bytes, 0, 32)).map(PrivateKey::from) } - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] pub fn new_ed25519_from_bytes(bytes: &[u8]) -> Option { if bytes.len() != 32 { return None; @@ -177,7 +177,7 @@ impl PrivateKey { pub fn get_pub_key(&self) -> CoseKey { match self { PrivateKey::Ecdsa(ecdsa_key) => CoseKey::from(ecdsa_key.genpk()), - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] PrivateKey::Ed25519(ed25519_keypair) => CoseKey::from(ed25519_keypair.public), } } @@ -186,7 +186,7 @@ impl PrivateKey { pub fn sign_and_encode(&self, message: &[u8]) -> Vec { match self { PrivateKey::Ecdsa(ecdsa_key) => ecdsa_key.sign_rfc6979::(message).to_asn1_der(), - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] PrivateKey::Ed25519(ed25519_keypair) => ed25519_keypair.try_sign(message).unwrap().to_bytes().to_vec(), } } @@ -195,7 +195,7 @@ impl PrivateKey { pub fn signature_algorithm(&self) -> SignatureAlgorithm { match self { PrivateKey::Ecdsa(_) => SignatureAlgorithm::ES256, - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] PrivateKey::Ed25519(_) => SignatureAlgorithm::EDDSA, } } @@ -208,7 +208,7 @@ impl PrivateKey { ecdsa_key.to_bytes(array_mut_ref!(key_bytes, 0, 32)); key_bytes } - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] PrivateKey::Ed25519(ed25519_keypair) => ed25519_keypair.secret.to_bytes().to_vec(), } } @@ -235,7 +235,7 @@ impl TryFrom for PrivateKey { 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 = "with_ed25519")] + #[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), @@ -285,7 +285,7 @@ pub fn encrypt_key_handle( ecdsa_key.to_bytes(array_mut_ref!(plaintext, 0, 32)); version = ECDSA_CREDENTIAL_ID_VERSION; } - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] PrivateKey::Ed25519(keypair) => { plaintext[0..32].copy_from_slice(&keypair.secret.to_bytes()); version = ED25519_CREDENTIAL_ID_VERSION; @@ -337,7 +337,7 @@ pub fn decrypt_credential_source( // Version number check match credential_id[0] { ECDSA_CREDENTIAL_ID_VERSION => algorithm = ES256_ALGORITHM, - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] ED25519_CREDENTIAL_ID_VERSION => algorithm = EDDSA_ALGORITHM, _ => return Ok(None), } @@ -357,9 +357,9 @@ pub fn decrypt_credential_source( let sk_option; match algorithm { ES256_ALGORITHM => sk_option = PrivateKey::new_ecdsa_from_bytes(&decrypted_id[..32]), - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] EDDSA_ALGORITHM => sk_option = PrivateKey::new_ed25519_from_bytes(&decrypted_id[..32]), - #[cfg(not(feature = "with_ed25519"))] + #[cfg(not(feature = "ed25519"))] EDDSA_ALGORITHM => return Ok(None), _ => return Ok(None), } @@ -462,7 +462,7 @@ mod test { } #[test] - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] fn test_new_ed25519_from_bytes() { let mut env = TestEnv::new(); let private_key = PrivateKey::new(env.rng(), SignatureAlgorithm::EDDSA); @@ -482,7 +482,7 @@ mod test { } #[test] - #[cfg(feature = "with_ed25519")] + #[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); @@ -521,7 +521,7 @@ mod test { } #[test] - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] fn test_ed25519_private_key_signature_algorithm() { test_private_key_signature_algorithm(SignatureAlgorithm::EDDSA); } @@ -539,7 +539,7 @@ mod test { } #[test] - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] fn test_ed25519_private_key_from_to_cbor() { test_private_key_from_to_cbor(SignatureAlgorithm::EDDSA); } @@ -557,7 +557,7 @@ mod test { Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR), ); - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] { let cbor = cbor_array![ cbor_int!(SignatureAlgorithm::EDDSA as i64), @@ -602,7 +602,7 @@ mod test { } #[test] - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] fn test_encrypt_decrypt_ed25519_credential() { test_encrypt_decrypt_credential(SignatureAlgorithm::EDDSA); } @@ -652,7 +652,7 @@ mod test { } #[test] - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] fn test_ed25519_encrypt_decrypt_bad_hmac() { test_encrypt_decrypt_bad_hmac(SignatureAlgorithm::EDDSA); } @@ -679,7 +679,7 @@ mod test { } #[test] - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] fn test_ed25519_decrypt_credential_missing_blocks() { test_decrypt_credential_missing_blocks(SignatureAlgorithm::EDDSA); } diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index e7c9cc4..337e5af 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -504,7 +504,7 @@ impl From for cbor::Value { #[cfg_attr(feature = "fuzz", derive(Arbitrary))] pub enum SignatureAlgorithm { ES256 = ES256_ALGORITHM as isize, - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] EDDSA = EDDSA_ALGORITHM as isize, // This is the default for all numbers not covered above. // Unknown types should be ignored, instead of returning errors. @@ -521,7 +521,7 @@ impl From for SignatureAlgorithm { fn from(int: i64) -> Self { match int { ES256_ALGORITHM => SignatureAlgorithm::ES256, - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] EDDSA_ALGORITHM => SignatureAlgorithm::EDDSA, _ => SignatureAlgorithm::Unknown, } @@ -736,11 +736,11 @@ impl CoseKey { const ECDH_ALGORITHM: i64 = -25; // The parameter behind map key 1. const EC2_KEY_TYPE: i64 = 2; - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] const OKP_KEY_TYPE: i64 = 1; // The parameter behind map key -1. const P_256_CURVE: i64 = 1; - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] const ED25519_CURVE: i64 = 6; } @@ -843,7 +843,7 @@ impl From for CoseKey { } } -#[cfg(feature = "with_ed25519")] +#[cfg(feature = "ed25519")] impl From for CoseKey { fn from(pk: ed25519_dalek::PublicKey) -> Self { CoseKey { @@ -943,7 +943,7 @@ impl TryFrom for ecdsa::Signature { match cose_signature.algorithm { SignatureAlgorithm::ES256 => ecdsa::Signature::from_bytes(&cose_signature.bytes) .ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] SignatureAlgorithm::EDDSA => Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM), SignatureAlgorithm::Unknown => @@ -1607,7 +1607,7 @@ mod test { let signature_algorithm = SignatureAlgorithm::from(alg_int); assert_eq!(signature_algorithm, SignatureAlgorithm::ES256); - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] { let alg_int = SignatureAlgorithm::EDDSA as i64; let signature_algorithm = SignatureAlgorithm::from(alg_int); @@ -1628,7 +1628,7 @@ mod test { let created_cbor: cbor::Value = signature_algorithm.unwrap().into(); assert_eq!(created_cbor, cbor_signature_algorithm); - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] { let cbor_signature_algorithm: cbor::Value = cbor_int!(EDDSA_ALGORITHM); let signature_algorithm = SignatureAlgorithm::try_from(cbor_signature_algorithm.clone()); @@ -1723,7 +1723,7 @@ mod test { } #[test] - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] fn test_from_into_ed25519_public_key_credential_parameter() { test_from_into_public_key_credential_parameter(EDDSA_ALGORITHM, SignatureAlgorithm::EDDSA); } diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 547629d..43267cf 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -117,7 +117,7 @@ pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa alg: SignatureAlgorithm::ES256, }; -#[cfg(feature = "with_ed25519")] +#[cfg(feature = "ed25519")] pub const EDDSA_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialParameter { cred_type: PublicKeyCredentialType::PublicKey, alg: SignatureAlgorithm::EDDSA, @@ -126,7 +126,7 @@ pub const EDDSA_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa fn get_supported_cred_params() -> Vec { let mut ret_val = vec!(); ret_val.push(ES256_CRED_PARAM); - #[cfg(feature = "with_ed25519")] + #[cfg(feature = "ed25519")] ret_val.push(EDDSA_CRED_PARAM); ret_val } From 658dbe2381d170523e2f450c44ff22d1a2e722f9 Mon Sep 17 00:00:00 2001 From: Egor Duda Date: Fri, 13 May 2022 21:06:34 +0300 Subject: [PATCH 03/20] Add patch to build libtock-rs with Rust 2021, needed for ed25519 support --- patches/libtock-rs/02-rust2021.patch | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 patches/libtock-rs/02-rust2021.patch diff --git a/patches/libtock-rs/02-rust2021.patch b/patches/libtock-rs/02-rust2021.patch new file mode 100644 index 0000000..22b6e6b --- /dev/null +++ b/patches/libtock-rs/02-rust2021.patch @@ -0,0 +1,38 @@ +diff --git a/core/src/entry_point/start_item_arm.rs b/core/src/entry_point/start_item_arm.rs +index 5d88d6b..4cdaef9 100644 +--- a/core/src/entry_point/start_item_arm.rs ++++ b/core/src/entry_point/start_item_arm.rs +@@ -20,16 +20,16 @@ pub unsafe extern "C" fn _start() -> ! { + subw r4, pc, #4 // r4 = pc + ldr r5, =.start // r5 = address of .start + cmp r4, r5 +- beq .Lstack_init // Jump to stack initialization if pc was correct ++ beq 2f // Jump to stack initialization if pc was correct + movw r0, #8 // LowLevelDebug driver number + movw r1, #1 // LowLevelDebug 'print status code' command + movw r2, #2 // LowLevelDebug relocation failed status code + svc 2 // command() syscall +- .Lyield_loop: ++ 1: + svc 0 // yield() syscall (in infinite loop) +- b .Lyield_loop ++ b 1b + +- .Lstack_init: ++ 2: + // Compute the stacktop (stack_start). The stacktop is computed as + // stack_size + mem_start plus padding to align the stack to a multiple + // of 8 bytes. The 8 byte alignment is to follow ARM AAPCS: +diff --git a/rust-toolchain b/rust-toolchain +index 1674405..64ed934 100644 +--- a/rust-toolchain ++++ b/rust-toolchain +@@ -1,7 +1,7 @@ + [toolchain] + # See https://rust-lang.github.io/rustup-components-history/ for a list of + # recently nightlies and what components are available for them. +-channel = "nightly-2021-03-25" ++channel = "nightly-2021-10-21" + components = ["clippy", "miri", "rustfmt"] + targets = ["thumbv7em-none-eabi", + "riscv32imac-unknown-none-elf", From e473af7118578b533422a4bfa573e3f5fb3476be Mon Sep 17 00:00:00 2001 From: Egor Duda Date: Fri, 13 May 2022 21:55:18 +0300 Subject: [PATCH 04/20] Cleaner assignment syntax --- src/ctap/crypto_wrapper.rs | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/ctap/crypto_wrapper.rs b/src/ctap/crypto_wrapper.rs index baca32b..c1adbe3 100644 --- a/src/ctap/crypto_wrapper.rs +++ b/src/ctap/crypto_wrapper.rs @@ -279,16 +279,15 @@ pub fn encrypt_key_handle( let aes_enc_key = crypto::aes256::EncryptionKey::new(&master_keys.encryption); let mut plaintext = [0; 64]; - let version; - match private_key { + let version = match private_key { PrivateKey::Ecdsa(ecdsa_key) => { ecdsa_key.to_bytes(array_mut_ref!(plaintext, 0, 32)); - version = ECDSA_CREDENTIAL_ID_VERSION; + ECDSA_CREDENTIAL_ID_VERSION } #[cfg(feature = "ed25519")] PrivateKey::Ed25519(keypair) => { plaintext[0..32].copy_from_slice(&keypair.secret.to_bytes()); - version = ED25519_CREDENTIAL_ID_VERSION; + ED25519_CREDENTIAL_ID_VERSION } }; plaintext[32..64].copy_from_slice(application); @@ -329,19 +328,17 @@ pub fn decrypt_credential_source( return Ok(None); } - let algorithm; - let payload = if credential_id.len() == LEGACY_CREDENTIAL_ID_SIZE { - algorithm = ES256_ALGORITHM; - &credential_id[..hmac_message_size] + let (payload, algorithm) = if credential_id.len() == LEGACY_CREDENTIAL_ID_SIZE { + (&credential_id[..hmac_message_size], ES256_ALGORITHM) } else { // Version number check - match credential_id[0] { - ECDSA_CREDENTIAL_ID_VERSION => algorithm = ES256_ALGORITHM, + let algorithm = match credential_id[0] { + ECDSA_CREDENTIAL_ID_VERSION => ES256_ALGORITHM, #[cfg(feature = "ed25519")] - ED25519_CREDENTIAL_ID_VERSION => algorithm = EDDSA_ALGORITHM, + ED25519_CREDENTIAL_ID_VERSION => EDDSA_ALGORITHM, _ => return Ok(None), - } - &credential_id[1..hmac_message_size] + }; + (&credential_id[1..hmac_message_size], algorithm) }; if payload.len() != 80 { // We shouldn't have HMAC'ed anything of different length. The check is cheap though. @@ -354,15 +351,14 @@ pub fn decrypt_credential_source( if rp_id_hash != &decrypted_id[32..] { return Ok(None); } - let sk_option; - match algorithm { - ES256_ALGORITHM => sk_option = PrivateKey::new_ecdsa_from_bytes(&decrypted_id[..32]), + let sk_option = match algorithm { + ES256_ALGORITHM => PrivateKey::new_ecdsa_from_bytes(&decrypted_id[..32]), #[cfg(feature = "ed25519")] - EDDSA_ALGORITHM => sk_option = PrivateKey::new_ed25519_from_bytes(&decrypted_id[..32]), + EDDSA_ALGORITHM => PrivateKey::new_ed25519_from_bytes(&decrypted_id[..32]), #[cfg(not(feature = "ed25519"))] EDDSA_ALGORITHM => return Ok(None), _ => return Ok(None), - } + }; Ok(sk_option.map(|sk| PublicKeyCredentialSource { key_type: PublicKeyCredentialType::PublicKey, From 3b8884c088bdfba465c82e94d83ce9c0bfa250dd Mon Sep 17 00:00:00 2001 From: Egor Duda Date: Fri, 13 May 2022 22:06:10 +0300 Subject: [PATCH 05/20] Separate test for reading ed25519 key from bad cbor --- src/ctap/crypto_wrapper.rs | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/ctap/crypto_wrapper.rs b/src/ctap/crypto_wrapper.rs index c1adbe3..40c750c 100644 --- a/src/ctap/crypto_wrapper.rs +++ b/src/ctap/crypto_wrapper.rs @@ -540,10 +540,9 @@ mod test { test_private_key_from_to_cbor(SignatureAlgorithm::EDDSA); } - #[test] - fn test_private_key_from_bad_cbor() { + fn test_private_key_from_bad_cbor(signature_algorithm: SignatureAlgorithm) { let cbor = cbor_array![ - cbor_int!(SignatureAlgorithm::ES256 as i64), + cbor_int!(signature_algorithm as i64), cbor_bytes!(vec![0x88; 32]), // The array is too long. cbor_int!(0), @@ -552,21 +551,21 @@ mod test { PrivateKey::try_from(cbor), Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR), ); + } - #[cfg(feature = "ed25519")] - { - let cbor = cbor_array![ - cbor_int!(SignatureAlgorithm::EDDSA 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), From 9713332eff9919d4da45730aa65fc91e8bc79ea1 Mon Sep 17 00:00:00 2001 From: Egor Duda Date: Mon, 16 May 2022 18:08:11 +0300 Subject: [PATCH 06/20] third-party code is modified via patching --- third_party/libtock-rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/third_party/libtock-rs b/third_party/libtock-rs index 1ece29b..15e837e 160000 --- a/third_party/libtock-rs +++ b/third_party/libtock-rs @@ -1 +1 @@ -Subproject commit 1ece29ba2b061e6d06c3344ba4c6cd6005c17eb3 +Subproject commit 15e837e4495df2d7a39ae6d6c057fdec515cc430 From 7f6ff31dd16e7dbbe49d2f34f3aa521d45300c0e Mon Sep 17 00:00:00 2001 From: Egor Duda Date: Mon, 16 May 2022 18:12:15 +0300 Subject: [PATCH 07/20] Static allocation for list of supported algorithms --- src/ctap/mod.rs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 43267cf..6ccb2ed 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -123,18 +123,15 @@ pub const EDDSA_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa alg: SignatureAlgorithm::EDDSA, }; -fn get_supported_cred_params() -> Vec { - let mut ret_val = vec!(); - ret_val.push(ES256_CRED_PARAM); - #[cfg(feature = "ed25519")] - ret_val.push(EDDSA_CRED_PARAM); - ret_val -} +const SUPPORTED_CRED_PARAMS: &[PublicKeyCredentialParameter] = &[ + ES256_CRED_PARAM, +#[cfg(feature = "ed25519")] + EDDSA_CRED_PARAM, +]; -fn get_preferred_cred_param (params: &Vec) -> Option<&PublicKeyCredentialParameter> { - let supported_cred_params = get_supported_cred_params(); +fn get_preferred_cred_param (params: &[PublicKeyCredentialParameter]) -> Option<&PublicKeyCredentialParameter> { for param in params { - if supported_cred_params.contains(param) { + if SUPPORTED_CRED_PARAMS.contains(param) { return Some(param); } } @@ -1169,7 +1166,7 @@ impl CtapState { .map(|c| c as u64), max_credential_id_length: Some(MAX_CREDENTIAL_ID_SIZE as u64), transports: Some(vec![AuthenticatorTransport::Usb]), - algorithms: Some(get_supported_cred_params()), + algorithms: Some(SUPPORTED_CRED_PARAMS.to_vec()), max_serialized_large_blob_array: Some( env.customization().max_large_blob_array_size() as u64, ), @@ -1484,7 +1481,7 @@ mod test { 0x07 => env.customization().max_credential_count_in_list().map(|c| c as u64), 0x08 => MAX_CREDENTIAL_ID_SIZE as u64, 0x09 => cbor_array!["usb"], - 0x0A => cbor_array_vec!(get_supported_cred_params()), + 0x0A => cbor_array_vec!(SUPPORTED_CRED_PARAMS.to_vec()), 0x0B => env.customization().max_large_blob_array_size() as u64, 0x0C => false, 0x0D => storage::min_pin_length(&mut env).unwrap() as u64, From 0ef0bb23f42d787ef50fc48ae82689bd1b0e1e36 Mon Sep 17 00:00:00 2001 From: Egor Duda Date: Mon, 16 May 2022 18:15:03 +0300 Subject: [PATCH 08/20] Remove unneeded code (covered by default branch) --- src/ctap/crypto_wrapper.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ctap/crypto_wrapper.rs b/src/ctap/crypto_wrapper.rs index 40c750c..8abe29f 100644 --- a/src/ctap/crypto_wrapper.rs +++ b/src/ctap/crypto_wrapper.rs @@ -355,8 +355,6 @@ pub fn decrypt_credential_source( ES256_ALGORITHM => PrivateKey::new_ecdsa_from_bytes(&decrypted_id[..32]), #[cfg(feature = "ed25519")] EDDSA_ALGORITHM => PrivateKey::new_ed25519_from_bytes(&decrypted_id[..32]), - #[cfg(not(feature = "ed25519"))] - EDDSA_ALGORITHM => return Ok(None), _ => return Ok(None), }; From 55056b721cfeb10a054f97bec44ea7b96bb40817 Mon Sep 17 00:00:00 2001 From: Egor Duda Date: Mon, 16 May 2022 21:48:43 +0300 Subject: [PATCH 09/20] Use ed25519-compact crate instead of ed25519-dalek ed25519-dalek does not implement Clone and Eq for secret keys, and relevant PR in its repository wait for merge from long time ago, leading to potential problems with maintainability --- Cargo.toml | 4 +-- src/ctap/crypto_wrapper.rs | 53 ++++++++++---------------------------- src/ctap/data_formats.rs | 6 ++--- 3 files changed, 18 insertions(+), 45 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7987184..b33cc1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ serde_json = { version = "=1.0.69", default-features = false, features = ["alloc embedded-time = "0.12.1" arbitrary = { version = "0.4.7", features = ["derive"], optional = true } rand = { version = "0.8.4", optional = true } -ed25519-dalek = { version = "1", default-features = false, features = ["u32_backend", "rand"], optional = true } +ed25519-compact = { version = "1", default-features = false, optional = true } [features] debug_allocations = ["lang_items/debug_allocations"] @@ -37,7 +37,7 @@ with_ctap1 = ["crypto/with_ctap1"] with_nfc = ["libtock_drivers/with_nfc"] vendor_hid = ["libtock_drivers/vendor_hid"] fuzz = ["arbitrary", "std"] -ed25519 = ["ed25519-dalek"] +ed25519 = ["ed25519-compact"] [dev-dependencies] enum-iterator = "0.6.0" diff --git a/src/ctap/crypto_wrapper.rs b/src/ctap/crypto_wrapper.rs index 8abe29f..73da5eb 100644 --- a/src/ctap/crypto_wrapper.rs +++ b/src/ctap/crypto_wrapper.rs @@ -14,8 +14,10 @@ use crate::ctap::data_formats::{ extract_array, extract_byte_string, CoseKey, PublicKeyCredentialSource, - PublicKeyCredentialType, SignatureAlgorithm, ES256_ALGORITHM, EDDSA_ALGORITHM, + PublicKeyCredentialType, SignatureAlgorithm, ES256_ALGORITHM, }; +#[cfg(feature="ed25519")] +use crate::ctap::data_formats::EDDSA_ALGORITHM; use crate::ctap::status_code::Ctap2StatusCode; use crate::ctap::storage; use crate::env::Env; @@ -30,8 +32,6 @@ use crypto::sha256::Sha256; use rng256::Rng256; use sk_cbor as cbor; use sk_cbor::{cbor_array, cbor_bytes, cbor_int}; -#[cfg(feature = "ed25519")] -use ed25519_dalek::Signer; // Legacy credential IDs consist of // - 16 bytes: initialization vector for AES-256, @@ -101,37 +101,13 @@ pub fn aes256_cbc_decrypt( } /// An asymmetric private key that can sign messages. -#[derive(Debug)] +#[derive(Clone,Debug,PartialEq,Eq)] pub enum PrivateKey { Ecdsa(ecdsa::SecKey), #[cfg(feature = "ed25519")] - Ed25519(ed25519_dalek::Keypair), + Ed25519(ed25519_compact::SecretKey), } -impl Clone for PrivateKey { - fn clone(&self) -> Self { - match self { - Self::Ecdsa(sk) => Self::Ecdsa (sk.clone ()), - #[cfg(feature = "ed25519")] - Self::Ed25519(keypair) => Self::Ed25519 (ed25519_dalek::Keypair::from_bytes (&keypair.to_bytes()).unwrap()), - } - } -} - -impl PartialEq for PrivateKey { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (&Self::Ecdsa(ref a), &Self::Ecdsa(ref b)) => a == b, - #[cfg(feature = "ed25519")] - (&Self::Ed25519(ref a), &Self::Ed25519(ref b)) => a.to_bytes() == b.to_bytes(), - #[cfg(feature = "ed25519")] - _ => false, - } - } -} - -impl Eq for PrivateKey {} - impl PrivateKey { /// Creates a new private key for the given algorithm. /// @@ -165,12 +141,8 @@ impl PrivateKey { if bytes.len() != 32 { return None; } - if let Ok(secret) = ed25519_dalek::SecretKey::from_bytes(bytes) { - let public = ed25519_dalek::PublicKey::from (&secret); - Some(Self::Ed25519(ed25519_dalek::Keypair{secret, public})) - } else { - None - } + let seed = ed25519_compact::Seed::from_slice(bytes).unwrap(); + Some(Self::Ed25519(ed25519_compact::KeyPair::from_seed(seed).sk)) } /// Returns the corresponding public key. @@ -178,7 +150,7 @@ impl PrivateKey { match self { PrivateKey::Ecdsa(ecdsa_key) => CoseKey::from(ecdsa_key.genpk()), #[cfg(feature = "ed25519")] - PrivateKey::Ed25519(ed25519_keypair) => CoseKey::from(ed25519_keypair.public), + PrivateKey::Ed25519(ed25519_key) => CoseKey::from(ed25519_key.public_key()), } } @@ -187,7 +159,7 @@ impl PrivateKey { match self { PrivateKey::Ecdsa(ecdsa_key) => ecdsa_key.sign_rfc6979::(message).to_asn1_der(), #[cfg(feature = "ed25519")] - PrivateKey::Ed25519(ed25519_keypair) => ed25519_keypair.try_sign(message).unwrap().to_bytes().to_vec(), + PrivateKey::Ed25519(ed25519_key) => ed25519_key.sign(message,None).to_vec(), } } @@ -209,7 +181,7 @@ impl PrivateKey { key_bytes } #[cfg(feature = "ed25519")] - PrivateKey::Ed25519(ed25519_keypair) => ed25519_keypair.secret.to_bytes().to_vec(), + PrivateKey::Ed25519(ed25519_key) => ed25519_key.seed().to_vec(), } } } @@ -285,8 +257,9 @@ pub fn encrypt_key_handle( ECDSA_CREDENTIAL_ID_VERSION } #[cfg(feature = "ed25519")] - PrivateKey::Ed25519(keypair) => { - plaintext[0..32].copy_from_slice(&keypair.secret.to_bytes()); + PrivateKey::Ed25519(ed25519_key) => { + let sk_bytes = *ed25519_key.seed(); + plaintext[0..32].copy_from_slice(&sk_bytes); ED25519_CREDENTIAL_ID_VERSION } }; diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 337e5af..30b73d9 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -844,10 +844,10 @@ impl From for CoseKey { } #[cfg(feature = "ed25519")] -impl From for CoseKey { - fn from(pk: ed25519_dalek::PublicKey) -> Self { +impl From for CoseKey { + fn from(pk: ed25519_compact::PublicKey) -> Self { CoseKey { - x_bytes: pk.to_bytes(), + x_bytes: *pk, y_bytes: [0u8; 32], key_type: CoseKey::OKP_KEY_TYPE, curve: CoseKey::ED25519_CURVE, From 245436f135097336f61ca675328380b866243f8e Mon Sep 17 00:00:00 2001 From: Egor Duda Date: Tue, 17 May 2022 17:15:03 +0300 Subject: [PATCH 10/20] ed25519-compact crate does not require newer edition of Rust --- patches/libtock-rs/02-rust2021.patch | 38 ---------------------------- 1 file changed, 38 deletions(-) delete mode 100644 patches/libtock-rs/02-rust2021.patch diff --git a/patches/libtock-rs/02-rust2021.patch b/patches/libtock-rs/02-rust2021.patch deleted file mode 100644 index 22b6e6b..0000000 --- a/patches/libtock-rs/02-rust2021.patch +++ /dev/null @@ -1,38 +0,0 @@ -diff --git a/core/src/entry_point/start_item_arm.rs b/core/src/entry_point/start_item_arm.rs -index 5d88d6b..4cdaef9 100644 ---- a/core/src/entry_point/start_item_arm.rs -+++ b/core/src/entry_point/start_item_arm.rs -@@ -20,16 +20,16 @@ pub unsafe extern "C" fn _start() -> ! { - subw r4, pc, #4 // r4 = pc - ldr r5, =.start // r5 = address of .start - cmp r4, r5 -- beq .Lstack_init // Jump to stack initialization if pc was correct -+ beq 2f // Jump to stack initialization if pc was correct - movw r0, #8 // LowLevelDebug driver number - movw r1, #1 // LowLevelDebug 'print status code' command - movw r2, #2 // LowLevelDebug relocation failed status code - svc 2 // command() syscall -- .Lyield_loop: -+ 1: - svc 0 // yield() syscall (in infinite loop) -- b .Lyield_loop -+ b 1b - -- .Lstack_init: -+ 2: - // Compute the stacktop (stack_start). The stacktop is computed as - // stack_size + mem_start plus padding to align the stack to a multiple - // of 8 bytes. The 8 byte alignment is to follow ARM AAPCS: -diff --git a/rust-toolchain b/rust-toolchain -index 1674405..64ed934 100644 ---- a/rust-toolchain -+++ b/rust-toolchain -@@ -1,7 +1,7 @@ - [toolchain] - # See https://rust-lang.github.io/rustup-components-history/ for a list of - # recently nightlies and what components are available for them. --channel = "nightly-2021-03-25" -+channel = "nightly-2021-10-21" - components = ["clippy", "miri", "rustfmt"] - targets = ["thumbv7em-none-eabi", - "riscv32imac-unknown-none-elf", From 06230d15e165dc53511d6d6f2aec7053788b06ff Mon Sep 17 00:00:00 2001 From: Egor Duda Date: Tue, 17 May 2022 17:15:47 +0300 Subject: [PATCH 11/20] Fix compile warning --- src/ctap/data_formats.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 30b73d9..093b31c 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -28,6 +28,7 @@ use sk_cbor::{cbor_array_vec, cbor_map, cbor_map_options, destructure_cbor_map}; // Used as the identifier for ECDSA in assertion signatures and COSE. pub const ES256_ALGORITHM: i64 = -7; +#[cfg(feature = "ed25519")] pub const EDDSA_ALGORITHM: i64 = -8; // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrpentity From c7116b1c21c4354a1cf18d4086896d4beffb4f29 Mon Sep 17 00:00:00 2001 From: Egor Duda Date: Tue, 17 May 2022 17:16:01 +0300 Subject: [PATCH 12/20] Return strict warning checking llvm_asm! don't cause warnings in older edition of Rust --- deploy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/deploy.py b/deploy.py index e99a1b2..96d867a 100755 --- a/deploy.py +++ b/deploy.py @@ -442,6 +442,8 @@ class OpenSKInstaller: f"link-arg=-T{props.app_ldscript}", "-C", "relocation-model=static", + "-D", + "warnings", f"--remap-path-prefix={os.getcwd()}=", "-C", "link-arg=-icf=all", From dc7311a3bda6cf587d134751aaded133de39fab8 Mon Sep 17 00:00:00 2001 From: Egor Duda Date: Tue, 17 May 2022 17:33:21 +0300 Subject: [PATCH 13/20] Shorter idiomatic expression for finding preferred crypto algorithm --- src/ctap/mod.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 6ccb2ed..d7f5c6f 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -130,12 +130,7 @@ const SUPPORTED_CRED_PARAMS: &[PublicKeyCredentialParameter] = &[ ]; fn get_preferred_cred_param (params: &[PublicKeyCredentialParameter]) -> Option<&PublicKeyCredentialParameter> { - for param in params { - if SUPPORTED_CRED_PARAMS.contains(param) { - return Some(param); - } - } - return None; + params.iter().find(|¶m| SUPPORTED_CRED_PARAMS.contains(param)) } /// Transports supported by OpenSK. From b9c48b480a46761d7e0fe019a5665a86eda3ddaf Mon Sep 17 00:00:00 2001 From: Egor Duda Date: Tue, 17 May 2022 19:49:25 +0300 Subject: [PATCH 14/20] Common setting for unsupported CredentialId version --- src/ctap/crypto_wrapper.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/ctap/crypto_wrapper.rs b/src/ctap/crypto_wrapper.rs index 73da5eb..e4a43ab 100644 --- a/src/ctap/crypto_wrapper.rs +++ b/src/ctap/crypto_wrapper.rs @@ -48,11 +48,7 @@ const ECDSA_CREDENTIAL_ID_VERSION: u8 = 0x01; #[cfg(feature = "ed25519")] const ED25519_CREDENTIAL_ID_VERSION: u8 = 0x02; #[cfg(test)] -#[cfg(feature = "ed25519")] -const UNSUPPORTED_CREDENTIAL_ID_VERSION: u8 = 0x03; -#[cfg(test)] -#[cfg(not(feature = "ed25519"))] -const UNSUPPORTED_CREDENTIAL_ID_VERSION: u8 = 0x02; +const UNSUPPORTED_CREDENTIAL_ID_VERSION: u8 = 0x80; /// Wraps the AES256-CBC encryption to match what we need in CTAP. pub fn aes256_cbc_encrypt( From 42bfd7860d0dbbdfb4eec5d0c14bb3580b713bd9 Mon Sep 17 00:00:00 2001 From: Egor Duda Date: Tue, 17 May 2022 19:53:45 +0300 Subject: [PATCH 15/20] whitespace cleanup --- src/ctap/data_formats.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 093b31c..954917f 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -947,7 +947,7 @@ impl TryFrom for ecdsa::Signature { #[cfg(feature = "ed25519")] SignatureAlgorithm::EDDSA => Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM), - SignatureAlgorithm::Unknown => + SignatureAlgorithm::Unknown => Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM), } } From 25d538cde6e05edb6fbf7595affbdd6968cb8454 Mon Sep 17 00:00:00 2001 From: Egor Duda Date: Tue, 17 May 2022 23:03:22 +0300 Subject: [PATCH 16/20] fix formatting --- src/ctap/crypto_wrapper.rs | 10 +++++----- src/ctap/data_formats.rs | 19 ++++++++++++------- src/ctap/mod.rs | 10 +++++++--- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/ctap/crypto_wrapper.rs b/src/ctap/crypto_wrapper.rs index e4a43ab..a550101 100644 --- a/src/ctap/crypto_wrapper.rs +++ b/src/ctap/crypto_wrapper.rs @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +#[cfg(feature = "ed25519")] +use crate::ctap::data_formats::EDDSA_ALGORITHM; use crate::ctap::data_formats::{ extract_array, extract_byte_string, CoseKey, PublicKeyCredentialSource, PublicKeyCredentialType, SignatureAlgorithm, ES256_ALGORITHM, }; -#[cfg(feature="ed25519")] -use crate::ctap::data_formats::EDDSA_ALGORITHM; use crate::ctap::status_code::Ctap2StatusCode; use crate::ctap::storage; use crate::env::Env; @@ -97,7 +97,7 @@ pub fn aes256_cbc_decrypt( } /// An asymmetric private key that can sign messages. -#[derive(Clone,Debug,PartialEq,Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum PrivateKey { Ecdsa(ecdsa::SecKey), #[cfg(feature = "ed25519")] @@ -117,7 +117,7 @@ impl PrivateKey { SignatureAlgorithm::EDDSA => { let bytes = rng.gen_uniform_u8x32(); Self::new_ed25519_from_bytes(&bytes).unwrap() - }, + } SignatureAlgorithm::Unknown => unreachable!(), } } @@ -155,7 +155,7 @@ impl PrivateKey { match self { PrivateKey::Ecdsa(ecdsa_key) => ecdsa_key.sign_rfc6979::(message).to_asn1_der(), #[cfg(feature = "ed25519")] - PrivateKey::Ed25519(ed25519_key) => ed25519_key.sign(message,None).to_vec(), + PrivateKey::Ed25519(ed25519_key) => ed25519_key.sign(message, None).to_vec(), } } diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 954917f..b22d4f4 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -896,7 +896,10 @@ impl TryFrom for ecdsa::PubKey { curve, } = cose_key; - if algorithm != ES256_ALGORITHM || key_type != CoseKey::EC2_KEY_TYPE || curve != CoseKey::P_256_CURVE { + if algorithm != ES256_ALGORITHM + || key_type != CoseKey::EC2_KEY_TYPE + || curve != CoseKey::P_256_CURVE + { return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); } ecdsa::PubKey::from_coordinates(&x_bytes, &y_bytes) @@ -945,10 +948,8 @@ impl TryFrom for ecdsa::Signature { SignatureAlgorithm::ES256 => ecdsa::Signature::from_bytes(&cose_signature.bytes) .ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), #[cfg(feature = "ed25519")] - SignatureAlgorithm::EDDSA => - Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM), - SignatureAlgorithm::Unknown => - Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM), + SignatureAlgorithm::EDDSA => Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM), + SignatureAlgorithm::Unknown => Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM), } } } @@ -1632,7 +1633,8 @@ mod test { #[cfg(feature = "ed25519")] { let cbor_signature_algorithm: cbor::Value = cbor_int!(EDDSA_ALGORITHM); - let signature_algorithm = SignatureAlgorithm::try_from(cbor_signature_algorithm.clone()); + let signature_algorithm = + SignatureAlgorithm::try_from(cbor_signature_algorithm.clone()); let expected_signature_algorithm = SignatureAlgorithm::EDDSA; assert_eq!(signature_algorithm, Ok(expected_signature_algorithm)); let created_cbor: cbor::Value = signature_algorithm.unwrap().into(); @@ -1702,7 +1704,10 @@ mod test { } } - fn test_from_into_public_key_credential_parameter(alg_int: i64, signature_algorithm: SignatureAlgorithm) { + fn test_from_into_public_key_credential_parameter( + alg_int: i64, + signature_algorithm: SignatureAlgorithm, + ) { let cbor_credential_parameter = cbor_map! { "alg" => alg_int, "type" => "public-key", diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index d7f5c6f..4fbc14e 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -125,12 +125,16 @@ pub const EDDSA_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa const SUPPORTED_CRED_PARAMS: &[PublicKeyCredentialParameter] = &[ ES256_CRED_PARAM, -#[cfg(feature = "ed25519")] + #[cfg(feature = "ed25519")] EDDSA_CRED_PARAM, ]; -fn get_preferred_cred_param (params: &[PublicKeyCredentialParameter]) -> Option<&PublicKeyCredentialParameter> { - params.iter().find(|¶m| SUPPORTED_CRED_PARAMS.contains(param)) +fn get_preferred_cred_param( + params: &[PublicKeyCredentialParameter], +) -> Option<&PublicKeyCredentialParameter> { + params + .iter() + .find(|¶m| SUPPORTED_CRED_PARAMS.contains(param)) } /// Transports supported by OpenSK. From e4d32626234920b1e63d026d9c65e0bd7abed3e8 Mon Sep 17 00:00:00 2001 From: Egor Duda Date: Wed, 18 May 2022 20:50:56 +0300 Subject: [PATCH 17/20] Reserve version IDs to avoid accidental reuse with other options --- src/ctap/crypto_wrapper.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ctap/crypto_wrapper.rs b/src/ctap/crypto_wrapper.rs index a550101..f661f77 100644 --- a/src/ctap/crypto_wrapper.rs +++ b/src/ctap/crypto_wrapper.rs @@ -45,7 +45,7 @@ pub const ECDSA_CREDENTIAL_ID_SIZE: usize = 113; pub const MAX_CREDENTIAL_ID_SIZE: usize = 113; const ECDSA_CREDENTIAL_ID_VERSION: u8 = 0x01; -#[cfg(feature = "ed25519")] +#[allow(dead_code)] const ED25519_CREDENTIAL_ID_VERSION: u8 = 0x02; #[cfg(test)] const UNSUPPORTED_CREDENTIAL_ID_VERSION: u8 = 0x80; From 1277b97018b210a47c898f7cf4f8e77c8667151a Mon Sep 17 00:00:00 2001 From: Egor Duda Date: Wed, 18 May 2022 20:51:28 +0300 Subject: [PATCH 18/20] Remove obsolete comment --- src/ctap/crypto_wrapper.rs | 1 - src/ctap/crypto_wrapper.rs.orig | 693 ++++++++++++++++++++++++++++++++ 2 files changed, 693 insertions(+), 1 deletion(-) create mode 100644 src/ctap/crypto_wrapper.rs.orig diff --git a/src/ctap/crypto_wrapper.rs b/src/ctap/crypto_wrapper.rs index f661f77..f56fbde 100644 --- a/src/ctap/crypto_wrapper.rs +++ b/src/ctap/crypto_wrapper.rs @@ -577,7 +577,6 @@ mod test { let rp_id_hash = [0x55; 32]; let mut encrypted_id = encrypt_key_handle(&mut env, &private_key, &rp_id_hash).unwrap(); - // Version 2 does not exist yet. encrypted_id[0] = UNSUPPORTED_CREDENTIAL_ID_VERSION; // Override the HMAC to pass the check. encrypted_id.truncate(&encrypted_id.len() - 32); diff --git a/src/ctap/crypto_wrapper.rs.orig b/src/ctap/crypto_wrapper.rs.orig new file mode 100644 index 0000000..f661f77 --- /dev/null +++ b/src/ctap/crypto_wrapper.rs.orig @@ -0,0 +1,693 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[cfg(feature = "ed25519")] +use crate::ctap::data_formats::EDDSA_ALGORITHM; +use crate::ctap::data_formats::{ + extract_array, extract_byte_string, CoseKey, PublicKeyCredentialSource, + PublicKeyCredentialType, SignatureAlgorithm, ES256_ALGORITHM, +}; +use crate::ctap::status_code::Ctap2StatusCode; +use crate::ctap::storage; +use crate::env::Env; +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; +use core::convert::TryFrom; +use crypto::cbc::{cbc_decrypt, cbc_encrypt}; +use crypto::ecdsa; +use crypto::hmac::{hmac_256, verify_hmac_256}; +use crypto::sha256::Sha256; +use rng256::Rng256; +use sk_cbor as cbor; +use sk_cbor::{cbor_array, cbor_bytes, cbor_int}; + +// Legacy credential IDs consist of +// - 16 bytes: initialization vector for AES-256, +// - 32 bytes: ECDSA private key for the credential, +// - 32 bytes: relying party ID hashed with SHA256, +// - 32 bytes: HMAC-SHA256 over everything else. +pub const LEGACY_CREDENTIAL_ID_SIZE: usize = 112; +#[cfg(test)] +pub const ECDSA_CREDENTIAL_ID_SIZE: usize = 113; +// See encrypt_key_handle v1 documentation. +pub const MAX_CREDENTIAL_ID_SIZE: usize = 113; + +const ECDSA_CREDENTIAL_ID_VERSION: u8 = 0x01; +#[allow(dead_code)] +const ED25519_CREDENTIAL_ID_VERSION: u8 = 0x02; +#[cfg(test)] +const UNSUPPORTED_CREDENTIAL_ID_VERSION: u8 = 0x80; + +/// Wraps the AES256-CBC encryption to match what we need in CTAP. +pub fn aes256_cbc_encrypt( + rng: &mut dyn Rng256, + aes_enc_key: &crypto::aes256::EncryptionKey, + plaintext: &[u8], + embeds_iv: bool, +) -> Result, Ctap2StatusCode> { + if plaintext.len() % 16 != 0 { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + // The extra 1 capacity is because encrypt_key_handle adds a version number. + let mut ciphertext = Vec::with_capacity(plaintext.len() + 16 * embeds_iv as usize + 1); + let iv = if embeds_iv { + let random_bytes = rng.gen_uniform_u8x32(); + ciphertext.extend_from_slice(&random_bytes[..16]); + *array_ref!(ciphertext, 0, 16) + } else { + [0u8; 16] + }; + let start = ciphertext.len(); + ciphertext.extend_from_slice(plaintext); + cbc_encrypt(aes_enc_key, iv, &mut ciphertext[start..]); + Ok(ciphertext) +} + +/// Wraps the AES256-CBC decryption to match what we need in CTAP. +pub fn aes256_cbc_decrypt( + aes_enc_key: &crypto::aes256::EncryptionKey, + ciphertext: &[u8], + embeds_iv: bool, +) -> Result, Ctap2StatusCode> { + if ciphertext.len() % 16 != 0 || (embeds_iv && ciphertext.is_empty()) { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + let (iv, ciphertext) = if embeds_iv { + let (iv, ciphertext) = ciphertext.split_at(16); + (*array_ref!(iv, 0, 16), ciphertext) + } else { + ([0u8; 16], ciphertext) + }; + let mut plaintext = ciphertext.to_vec(); + let aes_dec_key = crypto::aes256::DecryptionKey::new(aes_enc_key); + cbc_decrypt(&aes_dec_key, iv, &mut plaintext); + Ok(plaintext) +} + +/// An asymmetric private key that can sign messages. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum PrivateKey { + Ecdsa(ecdsa::SecKey), + #[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(rng: &mut impl Rng256, alg: SignatureAlgorithm) -> Self { + match alg { + SignatureAlgorithm::ES256 => PrivateKey::Ecdsa(crypto::ecdsa::SecKey::gensk(rng)), + #[cfg(feature = "ed25519")] + SignatureAlgorithm::EDDSA => { + let bytes = rng.gen_uniform_u8x32(); + Self::new_ed25519_from_bytes(&bytes).unwrap() + } + SignatureAlgorithm::Unknown => unreachable!(), + } + } + + /// 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; + } + ecdsa::SecKey::from_bytes(array_ref!(bytes, 0, 32)).map(PrivateKey::from) + } + + #[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 corresponding public key. + pub fn get_pub_key(&self) -> CoseKey { + match self { + PrivateKey::Ecdsa(ecdsa_key) => CoseKey::from(ecdsa_key.genpk()), + #[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, message: &[u8]) -> Vec { + match self { + PrivateKey::Ecdsa(ecdsa_key) => ecdsa_key.sign_rfc6979::(message).to_asn1_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) -> Vec { + match self { + PrivateKey::Ecdsa(ecdsa_key) => { + let mut key_bytes = vec![0u8; 32]; + ecdsa_key.to_bytes(array_mut_ref!(key_bytes, 0, 32)); + key_bytes + } + #[cfg(feature = "ed25519")] + PrivateKey::Ed25519(ed25519_key) => ed25519_key.seed().to_vec(), + } + } +} + +impl From for cbor::Value { + fn from(private_key: PrivateKey) -> Self { + cbor_array![ + cbor_int!(private_key.signature_algorithm() as i64), + cbor_bytes!(private_key.to_bytes()), + ] + } +} + +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), + } + } +} + +impl From for PrivateKey { + fn from(ecdsa_key: ecdsa::SecKey) -> Self { + PrivateKey::Ecdsa(ecdsa_key) + } +} + +/// Encrypts the given private key and relying party ID hash 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). +/// +/// Also, by limiting ourselves to private key and RP ID hash, we are compatible with U2F for +/// ECDSA private keys. +/// +/// For v1 we write the following data for ECDSA (algorithm -7): +/// - 1 byte : version number +/// - 16 bytes: initialization vector for AES-256, +/// - 32 bytes: ECDSA private key for the credential, +/// - 32 bytes: relying party ID hashed with SHA256, +/// - 32 bytes: HMAC-SHA256 over everything else. +/// +/// For v2 we write the following data for EdDSA over curve Ed25519 (algorithm -8, curve 6): +/// - 1 byte : version number +/// - 16 bytes: initialization vector for AES-256, +/// - 32 bytes: Ed25519 private key for the credential, +/// - 32 bytes: relying party ID hashed with SHA256, +/// - 32 bytes: HMAC-SHA256 over everything else. +pub fn encrypt_key_handle( + env: &mut impl Env, + private_key: &PrivateKey, + application: &[u8; 32], +) -> Result, Ctap2StatusCode> { + let master_keys = storage::master_keys(env)?; + let aes_enc_key = crypto::aes256::EncryptionKey::new(&master_keys.encryption); + + let mut plaintext = [0; 64]; + let version = match private_key { + PrivateKey::Ecdsa(ecdsa_key) => { + ecdsa_key.to_bytes(array_mut_ref!(plaintext, 0, 32)); + ECDSA_CREDENTIAL_ID_VERSION + } + #[cfg(feature = "ed25519")] + PrivateKey::Ed25519(ed25519_key) => { + let sk_bytes = *ed25519_key.seed(); + plaintext[0..32].copy_from_slice(&sk_bytes); + ED25519_CREDENTIAL_ID_VERSION + } + }; + plaintext[32..64].copy_from_slice(application); + let mut encrypted_id = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true)?; + encrypted_id.insert(0, version); + + let id_hmac = hmac_256::(&master_keys.hmac, &encrypted_id[..]); + encrypted_id.extend(&id_hmac); + Ok(encrypted_id) +} + +/// Decrypts a credential ID and writes the private key into a PublicKeyCredentialSource. +/// +/// Returns None if +/// - the format does not match any known versions, +/// - the HMAC test fails or +/// - the relying party does not match the decrypted relying party ID hash. +/// +/// This functions reads: +/// - legacy credentials (no version number), +/// - v1 (ECDSA) +/// - v2 (EdDSA over curve Ed25519) +pub fn decrypt_credential_source( + env: &mut impl Env, + credential_id: Vec, + rp_id_hash: &[u8], +) -> Result, Ctap2StatusCode> { + if credential_id.len() < LEGACY_CREDENTIAL_ID_SIZE { + return Ok(None); + } + let master_keys = storage::master_keys(env)?; + let hmac_message_size = credential_id.len() - 32; + if !verify_hmac_256::( + &master_keys.hmac, + &credential_id[..hmac_message_size], + array_ref![credential_id, hmac_message_size, 32], + ) { + return Ok(None); + } + + let (payload, algorithm) = if credential_id.len() == LEGACY_CREDENTIAL_ID_SIZE { + (&credential_id[..hmac_message_size], ES256_ALGORITHM) + } else { + // Version number check + let algorithm = match credential_id[0] { + ECDSA_CREDENTIAL_ID_VERSION => ES256_ALGORITHM, + #[cfg(feature = "ed25519")] + ED25519_CREDENTIAL_ID_VERSION => EDDSA_ALGORITHM, + _ => return Ok(None), + }; + (&credential_id[1..hmac_message_size], algorithm) + }; + if payload.len() != 80 { + // We shouldn't have HMAC'ed anything of different length. The check is cheap though. + return Ok(None); + } + + let aes_enc_key = crypto::aes256::EncryptionKey::new(&master_keys.encryption); + let decrypted_id = aes256_cbc_decrypt(&aes_enc_key, payload, true)?; + + if rp_id_hash != &decrypted_id[32..] { + return Ok(None); + } + let sk_option = match algorithm { + ES256_ALGORITHM => PrivateKey::new_ecdsa_from_bytes(&decrypted_id[..32]), + #[cfg(feature = "ed25519")] + EDDSA_ALGORITHM => PrivateKey::new_ed25519_from_bytes(&decrypted_id[..32]), + _ => return Ok(None), + }; + + Ok(sk_option.map(|sk| PublicKeyCredentialSource { + key_type: PublicKeyCredentialType::PublicKey, + credential_id, + private_key: sk, + rp_id: String::from(""), + user_handle: vec![], + user_display_name: None, + cred_protect_policy: None, + creation_order: 0, + user_name: None, + user_icon: None, + cred_blob: None, + large_blob_key: None, + })) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::env::test::TestEnv; + + #[test] + fn test_encrypt_decrypt_with_iv() { + let mut env = TestEnv::new(); + let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]); + let plaintext = vec![0xAA; 64]; + let ciphertext = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true).unwrap(); + let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext, true).unwrap(); + assert_eq!(decrypted, plaintext); + } + + #[test] + fn test_encrypt_decrypt_without_iv() { + let mut env = TestEnv::new(); + let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]); + let plaintext = vec![0xAA; 64]; + let ciphertext = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, false).unwrap(); + let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext, false).unwrap(); + assert_eq!(decrypted, plaintext); + } + + #[test] + fn test_correct_iv_usage() { + let mut env = TestEnv::new(); + let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]); + let plaintext = vec![0xAA; 64]; + let mut ciphertext_no_iv = + aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, false).unwrap(); + let mut ciphertext_with_iv = vec![0u8; 16]; + ciphertext_with_iv.append(&mut ciphertext_no_iv); + let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext_with_iv, true).unwrap(); + assert_eq!(decrypted, plaintext); + } + + #[test] + fn test_iv_manipulation_property() { + let mut env = TestEnv::new(); + let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]); + let plaintext = vec![0xAA; 64]; + let mut ciphertext = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true).unwrap(); + let mut expected_plaintext = plaintext; + for i in 0..16 { + ciphertext[i] ^= 0xBB; + expected_plaintext[i] ^= 0xBB; + } + let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext, true).unwrap(); + assert_eq!(decrypted, expected_plaintext); + } + + #[test] + fn test_chaining() { + let mut env = TestEnv::new(); + let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]); + let plaintext = vec![0xAA; 64]; + let ciphertext1 = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true).unwrap(); + let ciphertext2 = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true).unwrap(); + assert_eq!(ciphertext1.len(), 80); + assert_eq!(ciphertext2.len(), 80); + // The ciphertext should mutate in all blocks with a different IV. + let block_iter1 = ciphertext1.chunks_exact(16); + let block_iter2 = ciphertext2.chunks_exact(16); + for (block1, block2) in block_iter1.zip(block_iter2) { + assert_ne!(block1, block2); + } + } + + #[test] + fn test_new_ecdsa_from_bytes() { + let mut env = TestEnv::new(); + let private_key = PrivateKey::new(env.rng(), 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::new(); + let private_key = PrivateKey::new(env.rng(), 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::new(); + let ecdsa_key = crypto::ecdsa::SecKey::gensk(env.rng()); + let public_key = ecdsa_key.genpk(); + let private_key = PrivateKey::from(ecdsa_key); + assert_eq!(private_key.get_pub_key(), CoseKey::from(public_key)); + } + + #[test] + fn test_private_key_sign_and_encode() { + let mut env = TestEnv::new(); + let message = [0x5A; 32]; + let ecdsa_key = crypto::ecdsa::SecKey::gensk(env.rng()); + let signature = ecdsa_key.sign_rfc6979::(&message).to_asn1_der(); + let private_key = PrivateKey::from(ecdsa_key); + assert_eq!(private_key.sign_and_encode(&message), signature); + } + + fn test_private_key_signature_algorithm(signature_algorithm: SignatureAlgorithm) { + let mut env = TestEnv::new(); + let private_key = PrivateKey::new(env.rng(), 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::new(); + let private_key = PrivateKey::new(env.rng(), signature_algorithm); + let cbor = cbor::Value::from(private_key.clone()); + 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), + ); + } + + fn test_encrypt_decrypt_credential(signature_algorithm: SignatureAlgorithm) { + let mut env = TestEnv::new(); + storage::init(&mut env).ok().unwrap(); + let private_key = PrivateKey::new(env.rng(), signature_algorithm); + + let rp_id_hash = [0x55; 32]; + let encrypted_id = encrypt_key_handle(&mut env, &private_key, &rp_id_hash).unwrap(); + let decrypted_source = decrypt_credential_source(&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::new(); + storage::init(&mut env).ok().unwrap(); + let private_key = PrivateKey::new(env.rng(), SignatureAlgorithm::ES256); + + let rp_id_hash = [0x55; 32]; + let mut encrypted_id = encrypt_key_handle(&mut env, &private_key, &rp_id_hash).unwrap(); + // Version 2 does not exist yet. + encrypted_id[0] = UNSUPPORTED_CREDENTIAL_ID_VERSION; + // Override the HMAC to pass the check. + encrypted_id.truncate(&encrypted_id.len() - 32); + let master_keys = storage::master_keys(&mut env).unwrap(); + let id_hmac = hmac_256::(&master_keys.hmac, &encrypted_id[..]); + encrypted_id.extend(&id_hmac); + + assert_eq!( + decrypt_credential_source(&mut env, encrypted_id, &rp_id_hash), + Ok(None) + ); + } + + fn test_encrypt_decrypt_bad_hmac(signature_algorithm: SignatureAlgorithm) { + let mut env = TestEnv::new(); + storage::init(&mut env).ok().unwrap(); + let private_key = PrivateKey::new(env.rng(), signature_algorithm); + + let rp_id_hash = [0x55; 32]; + let encrypted_id = encrypt_key_handle(&mut env, &private_key, &rp_id_hash).unwrap(); + for i in 0..encrypted_id.len() { + let mut modified_id = encrypted_id.clone(); + modified_id[i] ^= 0x01; + assert_eq!( + decrypt_credential_source(&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::new(); + storage::init(&mut env).ok().unwrap(); + let private_key = PrivateKey::new(env.rng(), signature_algorithm); + + let rp_id_hash = [0x55; 32]; + let encrypted_id = encrypt_key_handle(&mut env, &private_key, &rp_id_hash).unwrap(); + + for length in (1..ECDSA_CREDENTIAL_ID_SIZE).step_by(16) { + assert_eq!( + decrypt_credential_source(&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_key_handle( + env: &mut impl Env, + private_key: crypto::ecdsa::SecKey, + application: &[u8; 32], + ) -> Result, Ctap2StatusCode> { + let master_keys = storage::master_keys(env)?; + let aes_enc_key = crypto::aes256::EncryptionKey::new(&master_keys.encryption); + let mut plaintext = [0; 64]; + private_key.to_bytes(array_mut_ref!(plaintext, 0, 32)); + plaintext[32..64].copy_from_slice(application); + + let mut encrypted_id = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true)?; + let id_hmac = hmac_256::(&master_keys.hmac, &encrypted_id[..]); + encrypted_id.extend(&id_hmac); + Ok(encrypted_id) + } + + #[test] + fn test_encrypt_decrypt_credential_legacy() { + let mut env = TestEnv::new(); + storage::init(&mut env).ok().unwrap(); + let ecdsa_key = crypto::ecdsa::SecKey::gensk(env.rng()); + let private_key = PrivateKey::from(ecdsa_key.clone()); + + let rp_id_hash = [0x55; 32]; + let encrypted_id = legacy_encrypt_key_handle(&mut env, ecdsa_key, &rp_id_hash).unwrap(); + let decrypted_source = decrypt_credential_source(&mut env, encrypted_id, &rp_id_hash) + .unwrap() + .unwrap(); + + assert_eq!(private_key, decrypted_source.private_key); + } + + #[test] + fn test_encrypt_credential_size() { + let mut env = TestEnv::new(); + storage::init(&mut env).ok().unwrap(); + let private_key = PrivateKey::new(env.rng(), SignatureAlgorithm::ES256); + + let rp_id_hash = [0x55; 32]; + let encrypted_id = encrypt_key_handle(&mut env, &private_key, &rp_id_hash).unwrap(); + assert_eq!(encrypted_id.len(), ECDSA_CREDENTIAL_ID_SIZE); + } +} From 5aac730f93b92a1c53442a53abecea4c5848b8f6 Mon Sep 17 00:00:00 2001 From: Egor Duda Date: Thu, 19 May 2022 08:50:47 +0300 Subject: [PATCH 19/20] Delete spurious file --- src/ctap/crypto_wrapper.rs.orig | 693 -------------------------------- 1 file changed, 693 deletions(-) delete mode 100644 src/ctap/crypto_wrapper.rs.orig diff --git a/src/ctap/crypto_wrapper.rs.orig b/src/ctap/crypto_wrapper.rs.orig deleted file mode 100644 index f661f77..0000000 --- a/src/ctap/crypto_wrapper.rs.orig +++ /dev/null @@ -1,693 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#[cfg(feature = "ed25519")] -use crate::ctap::data_formats::EDDSA_ALGORITHM; -use crate::ctap::data_formats::{ - extract_array, extract_byte_string, CoseKey, PublicKeyCredentialSource, - PublicKeyCredentialType, SignatureAlgorithm, ES256_ALGORITHM, -}; -use crate::ctap::status_code::Ctap2StatusCode; -use crate::ctap::storage; -use crate::env::Env; -use alloc::string::String; -use alloc::vec; -use alloc::vec::Vec; -use core::convert::TryFrom; -use crypto::cbc::{cbc_decrypt, cbc_encrypt}; -use crypto::ecdsa; -use crypto::hmac::{hmac_256, verify_hmac_256}; -use crypto::sha256::Sha256; -use rng256::Rng256; -use sk_cbor as cbor; -use sk_cbor::{cbor_array, cbor_bytes, cbor_int}; - -// Legacy credential IDs consist of -// - 16 bytes: initialization vector for AES-256, -// - 32 bytes: ECDSA private key for the credential, -// - 32 bytes: relying party ID hashed with SHA256, -// - 32 bytes: HMAC-SHA256 over everything else. -pub const LEGACY_CREDENTIAL_ID_SIZE: usize = 112; -#[cfg(test)] -pub const ECDSA_CREDENTIAL_ID_SIZE: usize = 113; -// See encrypt_key_handle v1 documentation. -pub const MAX_CREDENTIAL_ID_SIZE: usize = 113; - -const ECDSA_CREDENTIAL_ID_VERSION: u8 = 0x01; -#[allow(dead_code)] -const ED25519_CREDENTIAL_ID_VERSION: u8 = 0x02; -#[cfg(test)] -const UNSUPPORTED_CREDENTIAL_ID_VERSION: u8 = 0x80; - -/// Wraps the AES256-CBC encryption to match what we need in CTAP. -pub fn aes256_cbc_encrypt( - rng: &mut dyn Rng256, - aes_enc_key: &crypto::aes256::EncryptionKey, - plaintext: &[u8], - embeds_iv: bool, -) -> Result, Ctap2StatusCode> { - if plaintext.len() % 16 != 0 { - return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); - } - // The extra 1 capacity is because encrypt_key_handle adds a version number. - let mut ciphertext = Vec::with_capacity(plaintext.len() + 16 * embeds_iv as usize + 1); - let iv = if embeds_iv { - let random_bytes = rng.gen_uniform_u8x32(); - ciphertext.extend_from_slice(&random_bytes[..16]); - *array_ref!(ciphertext, 0, 16) - } else { - [0u8; 16] - }; - let start = ciphertext.len(); - ciphertext.extend_from_slice(plaintext); - cbc_encrypt(aes_enc_key, iv, &mut ciphertext[start..]); - Ok(ciphertext) -} - -/// Wraps the AES256-CBC decryption to match what we need in CTAP. -pub fn aes256_cbc_decrypt( - aes_enc_key: &crypto::aes256::EncryptionKey, - ciphertext: &[u8], - embeds_iv: bool, -) -> Result, Ctap2StatusCode> { - if ciphertext.len() % 16 != 0 || (embeds_iv && ciphertext.is_empty()) { - return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); - } - let (iv, ciphertext) = if embeds_iv { - let (iv, ciphertext) = ciphertext.split_at(16); - (*array_ref!(iv, 0, 16), ciphertext) - } else { - ([0u8; 16], ciphertext) - }; - let mut plaintext = ciphertext.to_vec(); - let aes_dec_key = crypto::aes256::DecryptionKey::new(aes_enc_key); - cbc_decrypt(&aes_dec_key, iv, &mut plaintext); - Ok(plaintext) -} - -/// An asymmetric private key that can sign messages. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum PrivateKey { - Ecdsa(ecdsa::SecKey), - #[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(rng: &mut impl Rng256, alg: SignatureAlgorithm) -> Self { - match alg { - SignatureAlgorithm::ES256 => PrivateKey::Ecdsa(crypto::ecdsa::SecKey::gensk(rng)), - #[cfg(feature = "ed25519")] - SignatureAlgorithm::EDDSA => { - let bytes = rng.gen_uniform_u8x32(); - Self::new_ed25519_from_bytes(&bytes).unwrap() - } - SignatureAlgorithm::Unknown => unreachable!(), - } - } - - /// 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; - } - ecdsa::SecKey::from_bytes(array_ref!(bytes, 0, 32)).map(PrivateKey::from) - } - - #[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 corresponding public key. - pub fn get_pub_key(&self) -> CoseKey { - match self { - PrivateKey::Ecdsa(ecdsa_key) => CoseKey::from(ecdsa_key.genpk()), - #[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, message: &[u8]) -> Vec { - match self { - PrivateKey::Ecdsa(ecdsa_key) => ecdsa_key.sign_rfc6979::(message).to_asn1_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) -> Vec { - match self { - PrivateKey::Ecdsa(ecdsa_key) => { - let mut key_bytes = vec![0u8; 32]; - ecdsa_key.to_bytes(array_mut_ref!(key_bytes, 0, 32)); - key_bytes - } - #[cfg(feature = "ed25519")] - PrivateKey::Ed25519(ed25519_key) => ed25519_key.seed().to_vec(), - } - } -} - -impl From for cbor::Value { - fn from(private_key: PrivateKey) -> Self { - cbor_array![ - cbor_int!(private_key.signature_algorithm() as i64), - cbor_bytes!(private_key.to_bytes()), - ] - } -} - -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), - } - } -} - -impl From for PrivateKey { - fn from(ecdsa_key: ecdsa::SecKey) -> Self { - PrivateKey::Ecdsa(ecdsa_key) - } -} - -/// Encrypts the given private key and relying party ID hash 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). -/// -/// Also, by limiting ourselves to private key and RP ID hash, we are compatible with U2F for -/// ECDSA private keys. -/// -/// For v1 we write the following data for ECDSA (algorithm -7): -/// - 1 byte : version number -/// - 16 bytes: initialization vector for AES-256, -/// - 32 bytes: ECDSA private key for the credential, -/// - 32 bytes: relying party ID hashed with SHA256, -/// - 32 bytes: HMAC-SHA256 over everything else. -/// -/// For v2 we write the following data for EdDSA over curve Ed25519 (algorithm -8, curve 6): -/// - 1 byte : version number -/// - 16 bytes: initialization vector for AES-256, -/// - 32 bytes: Ed25519 private key for the credential, -/// - 32 bytes: relying party ID hashed with SHA256, -/// - 32 bytes: HMAC-SHA256 over everything else. -pub fn encrypt_key_handle( - env: &mut impl Env, - private_key: &PrivateKey, - application: &[u8; 32], -) -> Result, Ctap2StatusCode> { - let master_keys = storage::master_keys(env)?; - let aes_enc_key = crypto::aes256::EncryptionKey::new(&master_keys.encryption); - - let mut plaintext = [0; 64]; - let version = match private_key { - PrivateKey::Ecdsa(ecdsa_key) => { - ecdsa_key.to_bytes(array_mut_ref!(plaintext, 0, 32)); - ECDSA_CREDENTIAL_ID_VERSION - } - #[cfg(feature = "ed25519")] - PrivateKey::Ed25519(ed25519_key) => { - let sk_bytes = *ed25519_key.seed(); - plaintext[0..32].copy_from_slice(&sk_bytes); - ED25519_CREDENTIAL_ID_VERSION - } - }; - plaintext[32..64].copy_from_slice(application); - let mut encrypted_id = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true)?; - encrypted_id.insert(0, version); - - let id_hmac = hmac_256::(&master_keys.hmac, &encrypted_id[..]); - encrypted_id.extend(&id_hmac); - Ok(encrypted_id) -} - -/// Decrypts a credential ID and writes the private key into a PublicKeyCredentialSource. -/// -/// Returns None if -/// - the format does not match any known versions, -/// - the HMAC test fails or -/// - the relying party does not match the decrypted relying party ID hash. -/// -/// This functions reads: -/// - legacy credentials (no version number), -/// - v1 (ECDSA) -/// - v2 (EdDSA over curve Ed25519) -pub fn decrypt_credential_source( - env: &mut impl Env, - credential_id: Vec, - rp_id_hash: &[u8], -) -> Result, Ctap2StatusCode> { - if credential_id.len() < LEGACY_CREDENTIAL_ID_SIZE { - return Ok(None); - } - let master_keys = storage::master_keys(env)?; - let hmac_message_size = credential_id.len() - 32; - if !verify_hmac_256::( - &master_keys.hmac, - &credential_id[..hmac_message_size], - array_ref![credential_id, hmac_message_size, 32], - ) { - return Ok(None); - } - - let (payload, algorithm) = if credential_id.len() == LEGACY_CREDENTIAL_ID_SIZE { - (&credential_id[..hmac_message_size], ES256_ALGORITHM) - } else { - // Version number check - let algorithm = match credential_id[0] { - ECDSA_CREDENTIAL_ID_VERSION => ES256_ALGORITHM, - #[cfg(feature = "ed25519")] - ED25519_CREDENTIAL_ID_VERSION => EDDSA_ALGORITHM, - _ => return Ok(None), - }; - (&credential_id[1..hmac_message_size], algorithm) - }; - if payload.len() != 80 { - // We shouldn't have HMAC'ed anything of different length. The check is cheap though. - return Ok(None); - } - - let aes_enc_key = crypto::aes256::EncryptionKey::new(&master_keys.encryption); - let decrypted_id = aes256_cbc_decrypt(&aes_enc_key, payload, true)?; - - if rp_id_hash != &decrypted_id[32..] { - return Ok(None); - } - let sk_option = match algorithm { - ES256_ALGORITHM => PrivateKey::new_ecdsa_from_bytes(&decrypted_id[..32]), - #[cfg(feature = "ed25519")] - EDDSA_ALGORITHM => PrivateKey::new_ed25519_from_bytes(&decrypted_id[..32]), - _ => return Ok(None), - }; - - Ok(sk_option.map(|sk| PublicKeyCredentialSource { - key_type: PublicKeyCredentialType::PublicKey, - credential_id, - private_key: sk, - rp_id: String::from(""), - user_handle: vec![], - user_display_name: None, - cred_protect_policy: None, - creation_order: 0, - user_name: None, - user_icon: None, - cred_blob: None, - large_blob_key: None, - })) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::env::test::TestEnv; - - #[test] - fn test_encrypt_decrypt_with_iv() { - let mut env = TestEnv::new(); - let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]); - let plaintext = vec![0xAA; 64]; - let ciphertext = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true).unwrap(); - let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext, true).unwrap(); - assert_eq!(decrypted, plaintext); - } - - #[test] - fn test_encrypt_decrypt_without_iv() { - let mut env = TestEnv::new(); - let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]); - let plaintext = vec![0xAA; 64]; - let ciphertext = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, false).unwrap(); - let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext, false).unwrap(); - assert_eq!(decrypted, plaintext); - } - - #[test] - fn test_correct_iv_usage() { - let mut env = TestEnv::new(); - let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]); - let plaintext = vec![0xAA; 64]; - let mut ciphertext_no_iv = - aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, false).unwrap(); - let mut ciphertext_with_iv = vec![0u8; 16]; - ciphertext_with_iv.append(&mut ciphertext_no_iv); - let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext_with_iv, true).unwrap(); - assert_eq!(decrypted, plaintext); - } - - #[test] - fn test_iv_manipulation_property() { - let mut env = TestEnv::new(); - let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]); - let plaintext = vec![0xAA; 64]; - let mut ciphertext = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true).unwrap(); - let mut expected_plaintext = plaintext; - for i in 0..16 { - ciphertext[i] ^= 0xBB; - expected_plaintext[i] ^= 0xBB; - } - let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext, true).unwrap(); - assert_eq!(decrypted, expected_plaintext); - } - - #[test] - fn test_chaining() { - let mut env = TestEnv::new(); - let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]); - let plaintext = vec![0xAA; 64]; - let ciphertext1 = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true).unwrap(); - let ciphertext2 = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true).unwrap(); - assert_eq!(ciphertext1.len(), 80); - assert_eq!(ciphertext2.len(), 80); - // The ciphertext should mutate in all blocks with a different IV. - let block_iter1 = ciphertext1.chunks_exact(16); - let block_iter2 = ciphertext2.chunks_exact(16); - for (block1, block2) in block_iter1.zip(block_iter2) { - assert_ne!(block1, block2); - } - } - - #[test] - fn test_new_ecdsa_from_bytes() { - let mut env = TestEnv::new(); - let private_key = PrivateKey::new(env.rng(), 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::new(); - let private_key = PrivateKey::new(env.rng(), 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::new(); - let ecdsa_key = crypto::ecdsa::SecKey::gensk(env.rng()); - let public_key = ecdsa_key.genpk(); - let private_key = PrivateKey::from(ecdsa_key); - assert_eq!(private_key.get_pub_key(), CoseKey::from(public_key)); - } - - #[test] - fn test_private_key_sign_and_encode() { - let mut env = TestEnv::new(); - let message = [0x5A; 32]; - let ecdsa_key = crypto::ecdsa::SecKey::gensk(env.rng()); - let signature = ecdsa_key.sign_rfc6979::(&message).to_asn1_der(); - let private_key = PrivateKey::from(ecdsa_key); - assert_eq!(private_key.sign_and_encode(&message), signature); - } - - fn test_private_key_signature_algorithm(signature_algorithm: SignatureAlgorithm) { - let mut env = TestEnv::new(); - let private_key = PrivateKey::new(env.rng(), 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::new(); - let private_key = PrivateKey::new(env.rng(), signature_algorithm); - let cbor = cbor::Value::from(private_key.clone()); - 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), - ); - } - - fn test_encrypt_decrypt_credential(signature_algorithm: SignatureAlgorithm) { - let mut env = TestEnv::new(); - storage::init(&mut env).ok().unwrap(); - let private_key = PrivateKey::new(env.rng(), signature_algorithm); - - let rp_id_hash = [0x55; 32]; - let encrypted_id = encrypt_key_handle(&mut env, &private_key, &rp_id_hash).unwrap(); - let decrypted_source = decrypt_credential_source(&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::new(); - storage::init(&mut env).ok().unwrap(); - let private_key = PrivateKey::new(env.rng(), SignatureAlgorithm::ES256); - - let rp_id_hash = [0x55; 32]; - let mut encrypted_id = encrypt_key_handle(&mut env, &private_key, &rp_id_hash).unwrap(); - // Version 2 does not exist yet. - encrypted_id[0] = UNSUPPORTED_CREDENTIAL_ID_VERSION; - // Override the HMAC to pass the check. - encrypted_id.truncate(&encrypted_id.len() - 32); - let master_keys = storage::master_keys(&mut env).unwrap(); - let id_hmac = hmac_256::(&master_keys.hmac, &encrypted_id[..]); - encrypted_id.extend(&id_hmac); - - assert_eq!( - decrypt_credential_source(&mut env, encrypted_id, &rp_id_hash), - Ok(None) - ); - } - - fn test_encrypt_decrypt_bad_hmac(signature_algorithm: SignatureAlgorithm) { - let mut env = TestEnv::new(); - storage::init(&mut env).ok().unwrap(); - let private_key = PrivateKey::new(env.rng(), signature_algorithm); - - let rp_id_hash = [0x55; 32]; - let encrypted_id = encrypt_key_handle(&mut env, &private_key, &rp_id_hash).unwrap(); - for i in 0..encrypted_id.len() { - let mut modified_id = encrypted_id.clone(); - modified_id[i] ^= 0x01; - assert_eq!( - decrypt_credential_source(&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::new(); - storage::init(&mut env).ok().unwrap(); - let private_key = PrivateKey::new(env.rng(), signature_algorithm); - - let rp_id_hash = [0x55; 32]; - let encrypted_id = encrypt_key_handle(&mut env, &private_key, &rp_id_hash).unwrap(); - - for length in (1..ECDSA_CREDENTIAL_ID_SIZE).step_by(16) { - assert_eq!( - decrypt_credential_source(&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_key_handle( - env: &mut impl Env, - private_key: crypto::ecdsa::SecKey, - application: &[u8; 32], - ) -> Result, Ctap2StatusCode> { - let master_keys = storage::master_keys(env)?; - let aes_enc_key = crypto::aes256::EncryptionKey::new(&master_keys.encryption); - let mut plaintext = [0; 64]; - private_key.to_bytes(array_mut_ref!(plaintext, 0, 32)); - plaintext[32..64].copy_from_slice(application); - - let mut encrypted_id = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true)?; - let id_hmac = hmac_256::(&master_keys.hmac, &encrypted_id[..]); - encrypted_id.extend(&id_hmac); - Ok(encrypted_id) - } - - #[test] - fn test_encrypt_decrypt_credential_legacy() { - let mut env = TestEnv::new(); - storage::init(&mut env).ok().unwrap(); - let ecdsa_key = crypto::ecdsa::SecKey::gensk(env.rng()); - let private_key = PrivateKey::from(ecdsa_key.clone()); - - let rp_id_hash = [0x55; 32]; - let encrypted_id = legacy_encrypt_key_handle(&mut env, ecdsa_key, &rp_id_hash).unwrap(); - let decrypted_source = decrypt_credential_source(&mut env, encrypted_id, &rp_id_hash) - .unwrap() - .unwrap(); - - assert_eq!(private_key, decrypted_source.private_key); - } - - #[test] - fn test_encrypt_credential_size() { - let mut env = TestEnv::new(); - storage::init(&mut env).ok().unwrap(); - let private_key = PrivateKey::new(env.rng(), SignatureAlgorithm::ES256); - - let rp_id_hash = [0x55; 32]; - let encrypted_id = encrypt_key_handle(&mut env, &private_key, &rp_id_hash).unwrap(); - assert_eq!(encrypted_id.len(), ECDSA_CREDENTIAL_ID_SIZE); - } -} From 9d36da16c7c03dc18dd191ef8e6960370105b9ca Mon Sep 17 00:00:00 2001 From: Egor Duda Date: Thu, 19 May 2022 12:00:28 +0300 Subject: [PATCH 20/20] More detailed description for "ed25519" option --- deploy.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/deploy.py b/deploy.py index 96d867a..301eea5 100755 --- a/deploy.py +++ b/deploy.py @@ -1096,7 +1096,10 @@ if __name__ == "__main__": action="append_const", const="ed25519", dest="features", - help=("Enable Ed25519 support"), + help=("Adds support for credentials that use EdDSA algorithm over " + "curve Ed25519. " + "Current implementation is not side-channel resilient due to use " + "of variable-time arithmetic for computations over secret key."), ) main_parser.set_defaults(features=["with_ctap1"])