Skip to content

Commit af9e7db

Browse files
0x0ecerobin-nitrokey
authored andcommitted
ctap2: add platform-serde feature gating client-direction codec
1 parent 52da246 commit af9e7db

6 files changed

Lines changed: 88 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
[Unreleased]: https://github.com/trussed-dev/ctap-types/compare/0.6.0-rc.1...HEAD
1010

1111
- Rename `authenticator_config` to `config`.
12+
- Add `platform-serde` feature for additional `Serialize` and `Deserialize` implementations not required by authenticators.
1213

1314
## [0.6.0-rc.1] 2026-05-21
1415

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ serde_test = "1.0.176"
3131
[features]
3232
std = []
3333

34+
# adds additional serde::{Deserialize, Serialize} implementations required by platforms
35+
platform-serde = []
36+
3437
# implements arbitrary::Arbitrary for requests
3538
arbitrary = ["dep:arbitrary", "std"]
3639
# enables all fields for ctap2::get_info

src/ctap2.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,61 @@ pub enum AttestationStatement {
267267
Packed(PackedAttestationStatement),
268268
}
269269

270+
// Hand-rolled Deserialize avoids serde's `#[serde(untagged)]` machinery,
271+
// which pulls in `serde::private::de::Content` (alloc-dependent) and
272+
// would force `serde/alloc` on the whole ctap-types crate. Distinguishes
273+
// variants by structural shape: `None` is an empty CBOR map; `Packed`
274+
// has `alg` + `sig` (+ optional `x5c`).
275+
#[cfg(feature = "platform-serde")]
276+
impl<'de> Deserialize<'de> for AttestationStatement {
277+
fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
278+
where
279+
D: serde::Deserializer<'de>,
280+
{
281+
struct V;
282+
impl<'de> serde::de::Visitor<'de> for V {
283+
type Value = AttestationStatement;
284+
fn expecting(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
285+
f.write_str("a CBOR map for an attestation statement")
286+
}
287+
fn visit_map<A>(self, mut map: A) -> core::result::Result<Self::Value, A::Error>
288+
where
289+
A: serde::de::MapAccess<'de>,
290+
{
291+
use serde::de::Error as _;
292+
293+
let mut alg: Option<i32> = None;
294+
let mut sig: Option<Bytes<ASN1_SIGNATURE_LENGTH>> = None;
295+
let mut x5c: Option<Vec<Bytes<1024>, 1>> = None;
296+
let mut has_values = false;
297+
while let Some(key) = map.next_key::<&str>()? {
298+
has_values = true;
299+
match key {
300+
"alg" => alg = Some(map.next_value()?),
301+
"sig" => sig = Some(map.next_value()?),
302+
"x5c" => x5c = Some(map.next_value()?),
303+
_ => {
304+
let _: serde::de::IgnoredAny = map.next_value()?;
305+
}
306+
}
307+
}
308+
if let Some((alg, sig)) = alg.zip(sig) {
309+
Ok(AttestationStatement::Packed(PackedAttestationStatement {
310+
alg,
311+
sig,
312+
x5c,
313+
}))
314+
} else if !has_values {
315+
Ok(AttestationStatement::None(NoneAttestationStatement {}))
316+
} else {
317+
Err(A::Error::custom("unsupported attestation statement format"))
318+
}
319+
}
320+
}
321+
deserializer.deserialize_map(V)
322+
}
323+
}
324+
270325
#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
271326
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
272327
#[non_exhaustive]
@@ -303,9 +358,11 @@ impl TryFrom<&str> for AttestationStatementFormat {
303358
}
304359

305360
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
361+
#[cfg_attr(feature = "platform-serde", derive(Deserialize))]
306362
pub struct NoneAttestationStatement {}
307363

308364
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
365+
#[cfg_attr(feature = "platform-serde", derive(Deserialize))]
309366
pub struct PackedAttestationStatement {
310367
pub alg: i32,
311368
pub sig: Bytes<ASN1_SIGNATURE_LENGTH>,
@@ -329,6 +386,27 @@ impl AttestationFormatsPreference {
329386
}
330387
}
331388

389+
#[cfg(feature = "platform-serde")]
390+
impl Serialize for AttestationFormatsPreference {
391+
fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
392+
where
393+
S: serde::Serializer,
394+
{
395+
use serde::ser::SerializeSeq;
396+
// Wire format mirrors the Deserialize impl below: a CBOR array of
397+
// attestation-format strings. The `unknown` flag is a parse-time
398+
// marker — Deserialize sets it when it sees a string it doesn't
399+
// recognize. That source string isn't retained, so we can't round-
400+
// trip it; on the way out we only emit the recognized entries.
401+
let mut seq = serializer.serialize_seq(Some(self.known_formats.len()))?;
402+
for format in &self.known_formats {
403+
let s: &str = (*format).into();
404+
seq.serialize_element(s)?;
405+
}
406+
seq.end()
407+
}
408+
}
409+
332410
impl<'de> Deserialize<'de> for AttestationFormatsPreference {
333411
fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
334412
where

src/ctap2/credential_management.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ pub struct Request<'a> {
6464
}
6565

6666
#[derive(Clone, Debug, Default, Eq, PartialEq, SerializeIndexed)]
67+
#[cfg_attr(feature = "platform-serde", derive(DeserializeIndexed))]
6768
#[non_exhaustive]
6869
#[serde_indexed(offset = 1)]
6970
pub struct Response {

src/ctap2/get_assertion.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ pub type AuthenticatorData<'a> =
103103
pub type AllowList<'a> = Vec<PublicKeyCredentialDescriptorRef<'a>, MAX_CREDENTIAL_COUNT_IN_LIST>;
104104

105105
#[derive(Clone, Debug, Eq, PartialEq, DeserializeIndexed)]
106+
#[cfg_attr(feature = "platform-serde", derive(SerializeIndexed))]
106107
#[non_exhaustive]
107108
#[serde_indexed(offset = 1)]
108109
pub struct Request<'a> {
@@ -128,6 +129,7 @@ pub struct Request<'a> {
128129
// https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorMakeCredential
129130
// does not coincide with what python-fido2 expects in AttestationObject.__init__ *at all* :'-)
130131
#[derive(Clone, Debug, Eq, PartialEq, SerializeIndexed)]
132+
#[cfg_attr(feature = "platform-serde", derive(DeserializeIndexed))]
131133
#[non_exhaustive]
132134
#[serde_indexed(offset = 1)]
133135
pub struct Response {

src/ctap2/make_credential.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ pub struct ExtensionsOutput {
9898
}
9999

100100
#[derive(Clone, Debug, Eq, PartialEq, DeserializeIndexed)]
101+
#[cfg_attr(feature = "platform-serde", derive(SerializeIndexed))]
101102
#[non_exhaustive]
102103
#[serde_indexed(offset = 1)]
103104
pub struct Request<'a> {
@@ -162,6 +163,7 @@ impl super::SerializeAttestedCredentialData for AttestedCredentialData<'_> {
162163
}
163164

164165
#[derive(Clone, Debug, Eq, PartialEq, SerializeIndexed)]
166+
#[cfg_attr(feature = "platform-serde", derive(DeserializeIndexed))]
165167
#[non_exhaustive]
166168
#[serde_indexed(offset = 1)]
167169
pub struct Response {
@@ -198,6 +200,7 @@ impl ResponseBuilder {
198200
}
199201

200202
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
203+
#[cfg_attr(feature = "platform-serde", derive(Deserialize))]
201204
#[non_exhaustive]
202205
pub struct UnsignedExtensionOutputs {}
203206

0 commit comments

Comments
 (0)