diff --git a/CHANGELOG.md b/CHANGELOG.md index cfd2f7c..4615c50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `AuthenticatorConfig` command. - Add `credBlob` extension and split `make_credential::Extensions` into `ExtensionsInput` and `ExtensionsOutput`. - Add `minPinLength` extension. +- Add support for missing CTAP 2.2 features: + - Add `hmac-secret-mc` extension. ## [0.5.0] 2026-03-23 diff --git a/src/arbitrary.rs b/src/arbitrary.rs index 4bf3180..ae04757 100644 --- a/src/arbitrary.rs +++ b/src/arbitrary.rs @@ -308,6 +308,7 @@ impl<'a> Arbitrary<'a> for ctap2::make_credential::ExtensionsInput<'a> { } else { None }; + let hmac_secret_mc = u.arbitrary()?; Ok(Self { cred_protect, hmac_secret, @@ -315,6 +316,7 @@ impl<'a> Arbitrary<'a> for ctap2::make_credential::ExtensionsInput<'a> { #[cfg(feature = "third-party-payment")] third_party_payment, cred_blob, + hmac_secret_mc, }) } } diff --git a/src/ctap2/get_info.rs b/src/ctap2/get_info.rs index 2736791..00d6814 100644 --- a/src/ctap2/get_info.rs +++ b/src/ctap2/get_info.rs @@ -14,7 +14,7 @@ pub struct Response { // 0x02 #[serde(skip_serializing_if = "Option::is_none")] - pub extensions: Option>, + pub extensions: Option>, // 0x03 pub aaguid: Bytes<16>, @@ -252,6 +252,7 @@ pub enum Extension { CredProtect, CredBlob, HmacSecret, + HmacSecretMc, LargeBlobKey, MinPinLength, ThirdPartyPayment, @@ -261,6 +262,7 @@ impl Extension { const CRED_PROTECT: &'static str = "credProtect"; const CRED_BLOB: &'static str = "credBlob"; const HMAC_SECRET: &'static str = "hmac-secret"; + const HMAC_SECRET_MC: &'static str = "hmac-secret-mc"; const LARGE_BLOB_KEY: &'static str = "largeBlobKey"; const MIN_PIN_LENGTH: &'static str = "minPinLength"; const THIRD_PARTY_PAYMENT: &'static str = "thirdPartyPayment"; @@ -272,6 +274,7 @@ impl From for &str { Extension::CredProtect => Extension::CRED_PROTECT, Extension::CredBlob => Extension::CRED_BLOB, Extension::HmacSecret => Extension::HMAC_SECRET, + Extension::HmacSecretMc => Extension::HMAC_SECRET_MC, Extension::LargeBlobKey => Extension::LARGE_BLOB_KEY, Extension::MinPinLength => Extension::MIN_PIN_LENGTH, Extension::ThirdPartyPayment => Extension::THIRD_PARTY_PAYMENT, @@ -287,6 +290,7 @@ impl TryFrom<&str> for Extension { Self::CRED_PROTECT => Ok(Self::CredProtect), Self::CRED_BLOB => Ok(Self::CredBlob), Self::HMAC_SECRET => Ok(Self::HmacSecret), + Self::HMAC_SECRET_MC => Ok(Self::HmacSecretMc), Self::LARGE_BLOB_KEY => Ok(Self::LargeBlobKey), Self::MIN_PIN_LENGTH => Ok(Self::MinPinLength), Self::THIRD_PARTY_PAYMENT => Ok(Self::ThirdPartyPayment), @@ -475,6 +479,7 @@ mod tests { (Extension::CredProtect, "credProtect"), (Extension::CredBlob, "credBlob"), (Extension::HmacSecret, "hmac-secret"), + (Extension::HmacSecretMc, "hmac-secret-mc"), (Extension::LargeBlobKey, "largeBlobKey"), (Extension::MinPinLength, "minPinLength"), (Extension::ThirdPartyPayment, "thirdPartyPayment"), diff --git a/src/ctap2/make_credential.rs b/src/ctap2/make_credential.rs index 534a13a..daeabc2 100644 --- a/src/ctap2/make_credential.rs +++ b/src/ctap2/make_credential.rs @@ -51,6 +51,13 @@ pub struct ExtensionsInput<'a> { #[serde(skip_serializing_if = "Option::is_none")] pub large_blob_key: Option, + /// `hmac-secret-mc` (CTAP 2.2 §11.4.5 / WebAuthn L3): platform-supplied + /// hmac-secret request evaluated at MakeCredential time, returning + /// hmac-secret outputs alongside the freshly-minted credential. + #[serde(rename = "hmac-secret-mc")] + #[serde(skip_serializing_if = "Option::is_none")] + pub hmac_secret_mc: Option, + #[cfg(feature = "third-party-payment")] #[serde(rename = "thirdPartyPayment")] #[serde(skip_serializing_if = "Option::is_none")] @@ -77,6 +84,13 @@ pub struct ExtensionsOutput { #[serde(skip_serializing_if = "Option::is_none")] pub hmac_secret: Option, + /// `hmac-secret-mc` (CTAP 2.2): encrypted hmac-secret outputs produced at + /// MakeCredential time. Wire format mirrors GetAssertion's `hmac-secret` + /// output — `enc(output1)` or `enc(output1 || output2)`, up to 80 bytes. + #[serde(rename = "hmac-secret-mc")] + #[serde(skip_serializing_if = "Option::is_none")] + pub hmac_secret_mc: Option>, + #[cfg(feature = "third-party-payment")] #[serde(rename = "thirdPartyPayment")] #[serde(skip_serializing_if = "Option::is_none")] @@ -190,6 +204,8 @@ pub struct UnsignedExtensionOutputs {} #[cfg(test)] mod tests { use super::*; + use crate::ctap2::get_assertion::HmacSecretInput; + use cosey::EcdhEsHkdf256PublicKey; use serde_test::{assert_ser_tokens, Token}; #[test] @@ -223,6 +239,15 @@ mod tests { #[cfg(feature = "third-party-payment")] third_party_payment: Some(true), cred_blob: Some(serde_bytes::Bytes::new(b"1234")), + hmac_secret_mc: Some(HmacSecretInput { + key_agreement: EcdhEsHkdf256PublicKey { + x: [0xff; 32].try_into().unwrap(), + y: [0xff; 32].try_into().unwrap(), + }, + salt_enc: [0xff; 80].try_into().unwrap(), + salt_auth: [0xff; 32].try_into().unwrap(), + pin_protocol: Some(1), + }), }; crate::test::assert_canonical_cbor(&extensions); } @@ -235,6 +260,7 @@ mod tests { #[cfg(feature = "third-party-payment")] third_party_payment: Some(true), cred_blob: Some(true), + hmac_secret_mc: Some([0xff; 80].try_into().unwrap()), }; crate::test::assert_canonical_cbor(&extensions); }