diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e53702..725e0bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [Unreleased]: https://github.com/trussed-dev/ctap-types/compare/0.6.0-rc.1...HEAD - Rename `authenticator_config` to `config`. +- Add `platform-serde` feature for additional `Serialize` and `Deserialize` implementations not required by authenticators. ## [0.6.0-rc.1] 2026-05-21 diff --git a/Cargo.toml b/Cargo.toml index 46dc8b8..c9149b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,9 @@ serde_test = "1.0.176" [features] std = [] +# adds additional serde::{Deserialize, Serialize} implementations required by platforms +platform-serde = [] + # implements arbitrary::Arbitrary for requests arbitrary = ["dep:arbitrary", "std"] # enables all fields for ctap2::get_info diff --git a/src/ctap2.rs b/src/ctap2.rs index ae1fbf1..d75a7b8 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -267,6 +267,61 @@ pub enum AttestationStatement { Packed(PackedAttestationStatement), } +// Hand-rolled Deserialize avoids serde's `#[serde(untagged)]` machinery, +// which pulls in `serde::private::de::Content` (alloc-dependent) and +// would force `serde/alloc` on the whole ctap-types crate. Distinguishes +// variants by structural shape: `None` is an empty CBOR map; `Packed` +// has `alg` + `sig` (+ optional `x5c`). +#[cfg(feature = "platform-serde")] +impl<'de> Deserialize<'de> for AttestationStatement { + fn deserialize(deserializer: D) -> core::result::Result + where + D: serde::Deserializer<'de>, + { + struct V; + impl<'de> serde::de::Visitor<'de> for V { + type Value = AttestationStatement; + fn expecting(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.write_str("a CBOR map for an attestation statement") + } + fn visit_map(self, mut map: A) -> core::result::Result + where + A: serde::de::MapAccess<'de>, + { + use serde::de::Error as _; + + let mut alg: Option = None; + let mut sig: Option> = None; + let mut x5c: Option, 1>> = None; + let mut has_values = false; + while let Some(key) = map.next_key::<&str>()? { + has_values = true; + match key { + "alg" => alg = Some(map.next_value()?), + "sig" => sig = Some(map.next_value()?), + "x5c" => x5c = Some(map.next_value()?), + _ => { + let _: serde::de::IgnoredAny = map.next_value()?; + } + } + } + if let Some((alg, sig)) = alg.zip(sig) { + Ok(AttestationStatement::Packed(PackedAttestationStatement { + alg, + sig, + x5c, + })) + } else if !has_values { + Ok(AttestationStatement::None(NoneAttestationStatement {})) + } else { + Err(A::Error::custom("unsupported attestation statement format")) + } + } + } + deserializer.deserialize_map(V) + } +} + #[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[non_exhaustive] @@ -303,9 +358,11 @@ impl TryFrom<&str> for AttestationStatementFormat { } #[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[cfg_attr(feature = "platform-serde", derive(Deserialize))] pub struct NoneAttestationStatement {} #[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[cfg_attr(feature = "platform-serde", derive(Deserialize))] pub struct PackedAttestationStatement { pub alg: i32, pub sig: Bytes, @@ -329,6 +386,27 @@ impl AttestationFormatsPreference { } } +#[cfg(feature = "platform-serde")] +impl Serialize for AttestationFormatsPreference { + fn serialize(&self, serializer: S) -> core::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeSeq; + // Wire format mirrors the Deserialize impl below: a CBOR array of + // attestation-format strings. The `unknown` flag is a parse-time + // marker — Deserialize sets it when it sees a string it doesn't + // recognize. That source string isn't retained, so we can't round- + // trip it; on the way out we only emit the recognized entries. + let mut seq = serializer.serialize_seq(Some(self.known_formats.len()))?; + for format in &self.known_formats { + let s: &str = (*format).into(); + seq.serialize_element(s)?; + } + seq.end() + } +} + impl<'de> Deserialize<'de> for AttestationFormatsPreference { fn deserialize(deserializer: D) -> core::result::Result where diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index 491fde2..268d872 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -64,6 +64,7 @@ pub struct Request<'a> { } #[derive(Clone, Debug, Default, Eq, PartialEq, SerializeIndexed)] +#[cfg_attr(feature = "platform-serde", derive(DeserializeIndexed))] #[non_exhaustive] #[serde_indexed(offset = 1)] pub struct Response { diff --git a/src/ctap2/get_assertion.rs b/src/ctap2/get_assertion.rs index 1507c85..ba1e046 100644 --- a/src/ctap2/get_assertion.rs +++ b/src/ctap2/get_assertion.rs @@ -103,6 +103,7 @@ pub type AuthenticatorData<'a> = pub type AllowList<'a> = Vec, MAX_CREDENTIAL_COUNT_IN_LIST>; #[derive(Clone, Debug, Eq, PartialEq, DeserializeIndexed)] +#[cfg_attr(feature = "platform-serde", derive(SerializeIndexed))] #[non_exhaustive] #[serde_indexed(offset = 1)] pub struct Request<'a> { @@ -128,6 +129,7 @@ pub struct Request<'a> { // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorMakeCredential // does not coincide with what python-fido2 expects in AttestationObject.__init__ *at all* :'-) #[derive(Clone, Debug, Eq, PartialEq, SerializeIndexed)] +#[cfg_attr(feature = "platform-serde", derive(DeserializeIndexed))] #[non_exhaustive] #[serde_indexed(offset = 1)] pub struct Response { diff --git a/src/ctap2/make_credential.rs b/src/ctap2/make_credential.rs index daeabc2..f6aac4f 100644 --- a/src/ctap2/make_credential.rs +++ b/src/ctap2/make_credential.rs @@ -98,6 +98,7 @@ pub struct ExtensionsOutput { } #[derive(Clone, Debug, Eq, PartialEq, DeserializeIndexed)] +#[cfg_attr(feature = "platform-serde", derive(SerializeIndexed))] #[non_exhaustive] #[serde_indexed(offset = 1)] pub struct Request<'a> { @@ -162,6 +163,7 @@ impl super::SerializeAttestedCredentialData for AttestedCredentialData<'_> { } #[derive(Clone, Debug, Eq, PartialEq, SerializeIndexed)] +#[cfg_attr(feature = "platform-serde", derive(DeserializeIndexed))] #[non_exhaustive] #[serde_indexed(offset = 1)] pub struct Response { @@ -198,6 +200,7 @@ impl ResponseBuilder { } #[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[cfg_attr(feature = "platform-serde", derive(Deserialize))] #[non_exhaustive] pub struct UnsignedExtensionOutputs {}