diff --git a/Cargo.toml b/Cargo.toml index 29527e7..b33cc1a 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-compact = { version = "1", default-features = false, 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"] +ed25519 = ["ed25519-compact"] [dev-dependencies] enum-iterator = "0.6.0" diff --git a/deploy.py b/deploy.py index 9b92fd4..301eea5 100755 --- a/deploy.py +++ b/deploy.py @@ -1091,6 +1091,17 @@ if __name__ == "__main__": help=("When set, the output of elf2tab is appended to this file."), ) + main_parser.add_argument( + "--ed25519", + action="append_const", + const="ed25519", + dest="features", + 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"]) # 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 cd59d65..dc0af0c 100644 --- a/src/ctap/crypto_wrapper.rs +++ b/src/ctap/crypto_wrapper.rs @@ -12,9 +12,11 @@ // 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, + PublicKeyCredentialType, SignatureAlgorithm, ES256_ALGORITHM, }; use crate::ctap::status_code::Ctap2StatusCode; use crate::ctap::storage; @@ -42,6 +44,12 @@ 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, @@ -94,6 +102,8 @@ pub fn aes256_cbc_decrypt( #[cfg_attr(test, derive(PartialEq, Eq))] pub enum PrivateKey { Ecdsa(ecdsa::SecKey), + #[cfg(feature = "ed25519")] + Ed25519(ed25519_compact::SecretKey), } impl PrivateKey { @@ -105,6 +115,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 = "ed25519")] + SignatureAlgorithm::EDDSA => { + let bytes = rng.gen_uniform_u8x32(); + Self::new_ed25519_from_bytes(&bytes).unwrap() + } SignatureAlgorithm::Unknown => unreachable!(), } } @@ -119,10 +134,21 @@ impl PrivateKey { 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()), } } @@ -130,6 +156,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 = "ed25519")] + PrivateKey::Ed25519(ed25519_key) => ed25519_key.sign(message, None).to_vec(), } } @@ -137,6 +165,8 @@ impl PrivateKey { pub fn signature_algorithm(&self) -> SignatureAlgorithm { match self { PrivateKey::Ecdsa(_) => SignatureAlgorithm::ES256, + #[cfg(feature = "ed25519")] + PrivateKey::Ed25519(_) => SignatureAlgorithm::EDDSA, } } @@ -148,6 +178,8 @@ impl PrivateKey { 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(), } } } @@ -173,6 +205,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 = "ed25519")] + SignatureAlgorithm::EDDSA => PrivateKey::new_ed25519_from_bytes(&key_bytes) + .ok_or(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR), _ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR), } } @@ -192,12 +227,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, @@ -206,17 +248,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 + 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); @@ -233,6 +280,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, @@ -251,14 +299,17 @@ pub fn decrypt_credential_source( return Ok(None); } - let payload = if credential_id.len() == LEGACY_CREDENTIAL_ID_SIZE { - &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 - if credential_id[0] != 1 { - return Ok(None); - } - &credential_id[1..hmac_message_size] + 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. @@ -271,7 +322,12 @@ 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 => 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, @@ -370,6 +426,18 @@ mod test { ); } + #[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); @@ -378,6 +446,15 @@ mod test { 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(); @@ -397,26 +474,44 @@ 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 = "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_private_key_from_bad_cbor() { + 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!(SignatureAlgorithm::ES256 as i64), + cbor_int!(signature_algorithm as i64), cbor_bytes!(vec![0x88; 32]), // The array is too long. cbor_int!(0), @@ -425,7 +520,21 @@ mod test { 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), @@ -437,11 +546,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(); @@ -452,6 +560,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 = "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(); @@ -460,8 +579,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(); @@ -474,11 +592,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(); @@ -493,10 +610,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 = "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(); @@ -509,6 +636,17 @@ mod test { } } + #[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, diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 658ba58..6de8c17 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -27,7 +27,9 @@ 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; +#[cfg(feature = "ed25519")] +pub const EDDSA_ALGORITHM: i64 = -8; // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrpentity #[derive(Clone, Debug, PartialEq, Eq)] @@ -503,6 +505,8 @@ impl From for cbor::Value { #[cfg_attr(feature = "fuzz", derive(Arbitrary))] pub enum SignatureAlgorithm { ES256 = ES256_ALGORITHM as isize, + #[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. Unknown = 0, @@ -518,6 +522,8 @@ impl From for SignatureAlgorithm { fn from(int: i64) -> Self { match int { ES256_ALGORITHM => SignatureAlgorithm::ES256, + #[cfg(feature = "ed25519")] + EDDSA_ALGORITHM => SignatureAlgorithm::EDDSA, _ => SignatureAlgorithm::Unknown, } } @@ -721,6 +727,8 @@ pub struct CoseKey { x_bytes: [u8; ecdh::NBYTES], y_bytes: [u8; ecdh::NBYTES], algorithm: i64, + key_type: i64, + curve: i64, } impl CoseKey { @@ -730,8 +738,12 @@ impl CoseKey { const ECDH_ALGORITHM: i64 = -25; // The parameter behind map key 1. const EC2_KEY_TYPE: i64 = 2; + #[cfg(feature = "ed25519")] + const OKP_KEY_TYPE: i64 = 1; // The parameter behind map key -1. const P_256_CURVE: i64 = 1; + #[cfg(feature = "ed25519")] + const ED25519_CURVE: i64 = 6; } // This conversion accepts both ECDH and ECDSA. @@ -777,6 +789,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, }) } } @@ -787,12 +801,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, } @@ -808,6 +824,8 @@ impl From for CoseKey { x_bytes, y_bytes, algorithm: CoseKey::ECDH_ALGORITHM, + key_type: CoseKey::EC2_KEY_TYPE, + curve: CoseKey::P_256_CURVE, } } } @@ -821,6 +839,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 = "ed25519")] +impl From for CoseKey { + fn from(pk: ed25519_compact::PublicKey) -> Self { + CoseKey { + x_bytes: *pk, + y_bytes: [0u8; 32], + key_type: CoseKey::OKP_KEY_TYPE, + curve: CoseKey::ED25519_CURVE, + algorithm: EDDSA_ALGORITHM, } } } @@ -833,6 +866,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 @@ -842,6 +877,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) } @@ -855,9 +893,14 @@ 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) @@ -905,6 +948,8 @@ 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 = "ed25519")] + SignatureAlgorithm::EDDSA => Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM), SignatureAlgorithm::Unknown => Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM), } } @@ -1565,6 +1610,13 @@ mod test { let signature_algorithm = SignatureAlgorithm::from(alg_int); assert_eq!(signature_algorithm, SignatureAlgorithm::ES256); + #[cfg(feature = "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); @@ -1579,6 +1631,17 @@ mod test { let created_cbor: cbor::Value = signature_algorithm.unwrap().into(); assert_eq!(created_cbor, cbor_signature_algorithm); + #[cfg(feature = "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; @@ -1642,23 +1705,36 @@ 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 = "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..4fbc14e 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -117,6 +117,26 @@ pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa alg: SignatureAlgorithm::ES256, }; +#[cfg(feature = "ed25519")] +pub const EDDSA_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialParameter { + cred_type: PublicKeyCredentialType::PublicKey, + alg: SignatureAlgorithm::EDDSA, +}; + +const SUPPORTED_CRED_PARAMS: &[PublicKeyCredentialParameter] = &[ + ES256_CRED_PARAM, + #[cfg(feature = "ed25519")] + EDDSA_CRED_PARAM, +]; + +fn get_preferred_cred_param( + params: &[PublicKeyCredentialParameter], +) -> Option<&PublicKeyCredentialParameter> { + params + .iter() + .find(|¶m| SUPPORTED_CRED_PARAMS.contains(param)) +} + /// Transports supported by OpenSK. /// /// An OpenSK library user annotates incoming data with this data type. @@ -604,10 +624,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 +1165,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(SUPPORTED_CRED_PARAMS.to_vec()), max_serialized_large_blob_array: Some( env.customization().max_large_blob_array_size() as u64, ), @@ -1461,7 +1480,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!(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,