Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions libwebauthn/src/ops/webauthn/make_credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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,
}]
);
Expand Down Expand Up @@ -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)
);
}

Expand All @@ -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());
}
Expand Down
137 changes: 124 additions & 13 deletions libwebauthn/src/proto/ctap2/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -172,14 +172,92 @@ pub struct Ctap2PublicKeyCredentialDescriptor {
pub transports: Option<Vec<Ctap2Transport>>,
}

#[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<i32> for Ctap2COSEAlgorithmIdentifier {
fn from(value: i32) -> Self {
Self(value)
}
}

impl From<Ctap2COSEAlgorithmIdentifier> for i32 {
fn from(value: Ctap2COSEAlgorithmIdentifier) -> Self {
value.0
}
}

impl From<Ctap2COSEAlgorithmIdentifier> 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)]
Expand Down Expand Up @@ -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
}
}

Expand Down Expand Up @@ -331,15 +408,15 @@ 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<u8> =
hex::decode("a263616c67382964747970656a7075626c69632d6b6579").unwrap();
let credential_type: Ctap2CredentialType = serde_cbor::from_slice(&serialized).unwrap();
assert_eq!(
credential_type,
Ctap2CredentialType {
algorithm: Ctap2COSEAlgorithmIdentifier::Unknown,
algorithm: Ctap2COSEAlgorithmIdentifier(-42),
public_key_type: Ctap2PublicKeyCredentialType::PublicKey,
}
);
Expand All @@ -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());
}
}
Loading