diff --git a/libwebauthn/src/ops/webauthn/make_credential.rs b/libwebauthn/src/ops/webauthn/make_credential.rs index 2655bc9..f1d5867 100644 --- a/libwebauthn/src/ops/webauthn/make_credential.rs +++ b/libwebauthn/src/ops/webauthn/make_credential.rs @@ -90,7 +90,7 @@ impl WebAuthnIDLResponse for MakeCredentialResponse { .attested_credential .as_ref() .map(|cred| Self::get_public_key_algorithm(&cred.credential_public_key)) - .unwrap_or(Ctap2COSEAlgorithmIdentifier::ES256 as i64); + .unwrap_or_else(|| i64::from(Ctap2COSEAlgorithmIdentifier::ES256)); // Serialize public key to COSE key format let public_key = self @@ -135,9 +135,9 @@ impl MakeCredentialResponse { /// Get the COSE algorithm identifier from the public key variant fn get_public_key_algorithm(key: &cosey::PublicKey) -> i64 { match key { - cosey::PublicKey::P256Key(_) => Ctap2COSEAlgorithmIdentifier::ES256 as i64, + cosey::PublicKey::P256Key(_) => i64::from(Ctap2COSEAlgorithmIdentifier::ES256), cosey::PublicKey::EcdhEsHkdf256Key(_) => -25, // ECDH-ES + HKDF-256 - cosey::PublicKey::Ed25519Key(_) => Ctap2COSEAlgorithmIdentifier::EDDSA as i64, + cosey::PublicKey::Ed25519Key(_) => i64::from(Ctap2COSEAlgorithmIdentifier::EDDSA), cosey::PublicKey::TotpKey(_) => 0, // No standard algorithm for TOTP } } @@ -918,7 +918,7 @@ mod tests { assert_eq!( req.algorithms, vec![Ctap2CredentialType { - algorithm: Ctap2COSEAlgorithmIdentifier::Unknown, // FIXME(#148): Passhtrough unknown algorithms + algorithm: Ctap2COSEAlgorithmIdentifier(-12345), public_key_type: Ctap2PublicKeyCredentialType::Unknown, }] ); @@ -1156,7 +1156,7 @@ mod tests { // Verify algorithm is ES256 (-7) for P256 key assert_eq!( response_obj.get("publicKeyAlgorithm").unwrap(), - Ctap2COSEAlgorithmIdentifier::ES256 as i64 + i64::from(Ctap2COSEAlgorithmIdentifier::ES256) ); } @@ -1173,7 +1173,7 @@ mod tests { // Verify attestation response assert_eq!( model.response.public_key_algorithm, - Ctap2COSEAlgorithmIdentifier::ES256 as i64 + i64::from(Ctap2COSEAlgorithmIdentifier::ES256) ); assert!(model.response.transports.is_empty()); } diff --git a/libwebauthn/src/proto/ctap2/model.rs b/libwebauthn/src/proto/ctap2/model.rs index 980facf..bc4f8a6 100644 --- a/libwebauthn/src/proto/ctap2/model.rs +++ b/libwebauthn/src/proto/ctap2/model.rs @@ -7,7 +7,7 @@ use crate::{ use num_enum::{IntoPrimitive, TryFromPrimitive}; use serde_bytes::ByteBuf; use serde_derive::{Deserialize, Serialize}; -use serde_repr::{Deserialize_repr, Serialize_repr}; +use serde_repr::Serialize_repr; mod get_info; pub use get_info::Ctap2GetInfoResponse; @@ -172,14 +172,92 @@ pub struct Ctap2PublicKeyCredentialDescriptor { pub transports: Option>, } -#[repr(i32)] -#[derive(Debug, Clone, Copy, FromPrimitive, PartialEq, Serialize_repr, Deserialize_repr)] -pub enum Ctap2COSEAlgorithmIdentifier { - ES256 = -7, - EDDSA = -8, - TOPT = -9, - #[serde(other)] - Unknown = -999, +/// COSE algorithm identifier from the IANA COSE Algorithms registry. +/// +/// Stored as a transparent `i32` so registered and unregistered values both +/// flow through unchanged. Use the associated constants for known values and +/// [`is_known`](Self::is_known) to test recognition. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(transparent)] +pub struct Ctap2COSEAlgorithmIdentifier(pub i32); + +impl Ctap2COSEAlgorithmIdentifier { + pub const ES256: Self = Self(-7); + pub const EDDSA: Self = Self(-8); + /// ESP256 (RFC 9864), equivalent to ES256 with explicit hash binding. + pub const ESP256: Self = Self(-9); + pub const ES384: Self = Self(-35); + pub const ES512: Self = Self(-36); + pub const PS256: Self = Self(-37); + pub const PS384: Self = Self(-38); + pub const PS512: Self = Self(-39); + pub const ES256K: Self = Self(-47); + pub const RS256: Self = Self(-257); + pub const RS384: Self = Self(-258); + pub const RS512: Self = Self(-259); + pub const RS1: Self = Self(-65535); + + pub fn is_known(self) -> bool { + matches!( + self, + Self::ES256 + | Self::EDDSA + | Self::ESP256 + | Self::ES384 + | Self::ES512 + | Self::PS256 + | Self::PS384 + | Self::PS512 + | Self::ES256K + | Self::RS256 + | Self::RS384 + | Self::RS512 + | Self::RS1 + ) + } +} + +impl From for Ctap2COSEAlgorithmIdentifier { + fn from(value: i32) -> Self { + Self(value) + } +} + +impl From for i32 { + fn from(value: Ctap2COSEAlgorithmIdentifier) -> Self { + value.0 + } +} + +impl From for i64 { + fn from(value: Ctap2COSEAlgorithmIdentifier) -> Self { + i64::from(value.0) + } +} + +impl std::fmt::Debug for Ctap2COSEAlgorithmIdentifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let name = match *self { + Self::ES256 => Some("ES256"), + Self::EDDSA => Some("EDDSA"), + Self::ESP256 => Some("ESP256"), + Self::ES384 => Some("ES384"), + Self::ES512 => Some("ES512"), + Self::PS256 => Some("PS256"), + Self::PS384 => Some("PS384"), + Self::PS512 => Some("PS512"), + Self::ES256K => Some("ES256K"), + Self::RS256 => Some("RS256"), + Self::RS384 => Some("RS384"), + Self::RS512 => Some("RS512"), + Self::RS1 => Some("RS1"), + _ => None, + }; + match name { + Some(n) => write!(f, "Ctap2COSEAlgorithmIdentifier::{}({})", n, self.0), + None => write!(f, "Ctap2COSEAlgorithmIdentifier({})", self.0), + } + } } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] @@ -212,8 +290,7 @@ impl Ctap2CredentialType { } pub fn is_known(&self) -> bool { - self.algorithm != Ctap2COSEAlgorithmIdentifier::Unknown - && self.public_key_type != Ctap2PublicKeyCredentialType::Unknown + self.algorithm.is_known() && self.public_key_type != Ctap2PublicKeyCredentialType::Unknown } } @@ -331,7 +408,7 @@ mod tests { } #[test] - pub fn deserialize_unknown_credential_type_algorithm() { + pub fn deserialize_preserves_unrecognised_algorithm() { // python $ cbor2.dumps({"alg":-42,"type":"public-key"}).hex() let serialized: Vec = hex::decode("a263616c67382964747970656a7075626c69632d6b6579").unwrap(); @@ -339,7 +416,7 @@ mod tests { assert_eq!( credential_type, Ctap2CredentialType { - algorithm: Ctap2COSEAlgorithmIdentifier::Unknown, + algorithm: Ctap2COSEAlgorithmIdentifier(-42), public_key_type: Ctap2PublicKeyCredentialType::PublicKey, } ); @@ -353,4 +430,38 @@ mod tests { let credential_type: Ctap2CredentialType = serde_cbor::from_slice(&serialized).unwrap(); assert!(!credential_type.is_known()); } + + #[test] + pub fn unrecognised_algorithm_roundtrips_through_cbor() { + // -12345 has no IANA assignment; the wire value must survive + // CBOR encode/decode without being rewritten to a sentinel. + let credential_type = Ctap2CredentialType { + algorithm: Ctap2COSEAlgorithmIdentifier(-12345), + public_key_type: Ctap2PublicKeyCredentialType::PublicKey, + }; + let serialized = cbor::to_vec(&credential_type).unwrap(); + let back: Ctap2CredentialType = serde_cbor::from_slice(&serialized).unwrap(); + assert_eq!(back.algorithm, Ctap2COSEAlgorithmIdentifier(-12345)); + assert!(!back.algorithm.is_known()); + } + + #[test] + pub fn rs256_constant_matches_iana_value() { + assert_eq!( + Ctap2COSEAlgorithmIdentifier::RS256, + Ctap2COSEAlgorithmIdentifier(-257) + ); + assert!(Ctap2COSEAlgorithmIdentifier::RS256.is_known()); + } + + #[test] + pub fn esp256_is_recognised() { + // -9 is ESP256 per RFC 9864; libwebauthn previously mis-named this + // codepoint TOPT. + assert_eq!( + Ctap2COSEAlgorithmIdentifier::ESP256, + Ctap2COSEAlgorithmIdentifier(-9) + ); + assert!(Ctap2COSEAlgorithmIdentifier::ESP256.is_known()); + } }