From d285dc066e6f01832ffd8b3303eacff9a1b200bc Mon Sep 17 00:00:00 2001 From: Alfie Fresta Date: Sun, 7 Jun 2026 12:19:17 +0100 Subject: [PATCH] fix(ctap2): decode unsignedExtensionOutputs at GetAssertion 0x08 GetAssertion response index 0x08 is unsignedExtensionOutputs, a CBOR map, not enterprise attestation. Decoding it as Option made the whole response fail to parse. Decode it as an optional map and remove the speculative enterprise_attestation and attestation_statement fields, which no spec defines for GetAssertion. --- libwebauthn/src/ops/u2f.rs | 3 +- libwebauthn/src/ops/webauthn/get_assertion.rs | 8 ++--- .../src/proto/ctap2/model/get_assertion.rs | 35 ++++++++++++++----- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/libwebauthn/src/ops/u2f.rs b/libwebauthn/src/ops/u2f.rs index 1f38aba5..cb45c78d 100644 --- a/libwebauthn/src/ops/u2f.rs +++ b/libwebauthn/src/ops/u2f.rs @@ -216,8 +216,7 @@ impl UpgradableResponse for SignResponse { credentials_count: None, user_selected: None, large_blob_key: None, - enterprise_attestation: None, - attestation_statement: None, + unsigned_extension_outputs: None, }; // This isn't great, but we have no access to the original request, and need to construct diff --git a/libwebauthn/src/ops/webauthn/get_assertion.rs b/libwebauthn/src/ops/webauthn/get_assertion.rs index 77b1bd9b..49923763 100644 --- a/libwebauthn/src/ops/webauthn/get_assertion.rs +++ b/libwebauthn/src/ops/webauthn/get_assertion.rs @@ -25,8 +25,8 @@ use crate::{ }, pin::PinUvAuthProtocol, proto::ctap2::{ - Ctap2AttestationStatement, Ctap2GetAssertionResponseExtensions, - Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialUserEntity, + Ctap2GetAssertionResponseExtensions, Ctap2PublicKeyCredentialDescriptor, + Ctap2PublicKeyCredentialUserEntity, }, webauthn::CtapError, }; @@ -452,8 +452,6 @@ pub struct Assertion { pub credentials_count: Option, pub user_selected: Option, pub unsigned_extensions_output: Option, - pub enterprise_attestation: Option, - pub attestation_statement: Option, } impl WebAuthnIDLResponse for Assertion { @@ -1229,8 +1227,6 @@ mod tests { credentials_count: None, user_selected: None, unsigned_extensions_output: None, - enterprise_attestation: None, - attestation_statement: None, } } diff --git a/libwebauthn/src/proto/ctap2/model/get_assertion.rs b/libwebauthn/src/proto/ctap2/model/get_assertion.rs index ce396175..f7039bc4 100644 --- a/libwebauthn/src/proto/ctap2/model/get_assertion.rs +++ b/libwebauthn/src/proto/ctap2/model/get_assertion.rs @@ -370,11 +370,7 @@ pub struct Ctap2GetAssertionResponse { #[serde(skip_serializing_if = "Option::is_none")] #[serde(index = 0x08)] - pub enterprise_attestation: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - #[serde(index = 0x09)] - pub attestation_statement: Option, + pub unsigned_extension_outputs: Option>, } impl Ctap2UserVerifiableRequest for Ctap2GetAssertionRequest { @@ -462,8 +458,6 @@ impl Ctap2GetAssertionResponse { credentials_count: self.credentials_count, user_selected: self.user_selected, unsigned_extensions_output, - enterprise_attestation: self.enterprise_attestation, - attestation_statement: self.attestation_statement, } } } @@ -575,8 +569,7 @@ mod tests { credentials_count: None, user_selected: None, large_blob_key: None, - enterprise_attestation: None, - attestation_statement: None, + unsigned_extension_outputs: None, } } @@ -660,4 +653,28 @@ mod tests { assert!(large_blob.blob.is_none()); } + + #[test] + fn decodes_unsigned_extension_outputs_at_index_0x08() { + // 0x08 is unsignedExtensionOutputs (a CBOR map), not enterprise attestation. + let mut auth_data = vec![0u8; 37]; + auth_data[32] = AuthenticatorDataFlags::USER_PRESENT.bits(); + + let mut ueo = BTreeMap::new(); + ueo.insert( + Value::Text("thirdPartyPayment".to_string()), + Value::Bool(true), + ); + + let mut response: BTreeMap = BTreeMap::new(); + response.insert(0x02, Value::Bytes(auth_data)); + response.insert(0x03, Value::Bytes(vec![0xAAu8; 64])); + response.insert(0x08, Value::Map(ueo.clone())); + + let bytes = crate::proto::ctap2::cbor::to_vec(&response).unwrap(); + let parsed: Ctap2GetAssertionResponse = + crate::proto::ctap2::cbor::from_slice(&bytes).unwrap(); + + assert_eq!(parsed.unsigned_extension_outputs, Some(ueo)); + } }