From be6b5931142c4c6136e6eff9d7c857c3bfb10c25 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Fri, 21 Jun 2024 19:16:05 +0200 Subject: [PATCH] Add support for attestation format preferences --- CHANGELOG.md | 9 ++++ src/ctap2.rs | 88 ++++++++++++++++++++++++++++++++++++ src/ctap2/get_assertion.rs | 11 +++-- src/ctap2/get_info.rs | 2 +- src/ctap2/make_credential.rs | 44 ++++-------------- 5 files changed, 116 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20b918b..8208b57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [Unreleased]: https://github.com/trussed-dev/ctap-types/compare/0.2.0...HEAD +### Breaking Changes + +- Remove `Deserialize` implementation for `ctap2::get_assertion::Response` +- Remove `Serialize` implementation for `ctap2::{get_assertion, make_credential}::Request` +- Use `AttestationStatementFormat` enum for `fmt` in `ctap2::make_credential::Response` +- Move `AttestationStatement`, `AttestationStatementFormat`, `NoneAttestationStatement`, `PackedAttestationStatement` from `ctap2::make_credential` into the `ctap2` module + ### Added - Add support for CTAP 2.2 ([#38](https://github.com/trussed-dev/ctap-types/issues/38)) @@ -15,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add new fields to `get_info` - Add unsigned extension outputs to `make_credential` and `get_assertion` - Add enterprise attestation support to `get_assertion` + - Add support for attestation statements in `get_assertion` + - Add support for attestation format preferences ## [0.2.0] - 2024-06-21 diff --git a/src/ctap2.rs b/src/ctap2.rs index 54e3c12..9597346 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -248,6 +248,94 @@ impl<'a, A: SerializeAttestedCredentialData, E: serde::Serialize> AuthenticatorD } } +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(untagged)] +#[allow(clippy::large_enum_variant)] +pub enum AttestationStatement { + None(NoneAttestationStatement), + Packed(PackedAttestationStatement), +} + +#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "lowercase")] +pub enum AttestationStatementFormat { + None, + Packed, +} + +impl AttestationStatementFormat { + fn from_str(s: &str) -> Option { + match s { + "none" => Some(Self::None), + "packed" => Some(Self::Packed), + _ => None, + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub struct NoneAttestationStatement {} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub struct PackedAttestationStatement { + pub alg: i32, + pub sig: Bytes, + #[serde(skip_serializing_if = "Option::is_none")] + pub x5c: Option, 1>>, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct AttestationFormatsPreference { + known_formats: Vec, + unknown: bool, +} + +impl AttestationFormatsPreference { + pub fn known_formats(&self) -> &[AttestationStatementFormat] { + &self.known_formats + } + + pub fn includes_unknown_formats(&self) -> bool { + self.unknown + } +} + +impl<'de> Deserialize<'de> for AttestationFormatsPreference { + fn deserialize(deserializer: D) -> core::result::Result + where + D: serde::Deserializer<'de>, + { + struct ValueVisitor; + + impl<'de> serde::de::Visitor<'de> for ValueVisitor { + type Value = AttestationFormatsPreference; + + fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { + formatter.write_str("a sequence") + } + + fn visit_seq(self, mut seq: A) -> core::result::Result + where + A: serde::de::SeqAccess<'de>, + { + let mut preference = AttestationFormatsPreference::default(); + while let Some(value) = seq.next_element::<&str>()? { + if let Some(format) = AttestationStatementFormat::from_str(value) { + preference.known_formats.push(format).ok(); + } else { + preference.unknown = true; + } + } + Ok(preference) + } + } + + deserializer.deserialize_seq(ValueVisitor) + } +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] pub enum Error { diff --git a/src/ctap2/get_assertion.rs b/src/ctap2/get_assertion.rs index 8464750..0b858c2 100644 --- a/src/ctap2/get_assertion.rs +++ b/src/ctap2/get_assertion.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use serde_bytes::ByteArray; use serde_indexed::{DeserializeIndexed, SerializeIndexed}; -use super::{AuthenticatorOptions, Result}; +use super::{AttestationFormatsPreference, AttestationStatement, AuthenticatorOptions, Result}; use crate::sizes::*; use crate::webauthn::*; @@ -84,7 +84,7 @@ pub type AuthenticatorData<'a> = pub type AllowList<'a> = Vec, MAX_CREDENTIAL_COUNT_IN_LIST>; -#[derive(Clone, Debug, Eq, PartialEq, SerializeIndexed, DeserializeIndexed)] +#[derive(Clone, Debug, Eq, PartialEq, DeserializeIndexed)] #[non_exhaustive] #[serde_indexed(offset = 1)] pub struct Request<'a> { @@ -102,12 +102,14 @@ pub struct Request<'a> { pub pin_protocol: Option, #[serde(skip_serializing_if = "Option::is_none")] pub enterprise_attestation: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub attestation_formats_preference: Option, } // NB: attn object definition / order at end of // 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, DeserializeIndexed)] +#[derive(Clone, Debug, Eq, PartialEq, SerializeIndexed)] #[non_exhaustive] #[serde_indexed(offset = 1)] pub struct Response { @@ -128,6 +130,8 @@ pub struct Response { pub unsigned_extension_outputs: Option, #[serde(skip_serializing_if = "Option::is_none")] pub ep_att: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub att_stmt: Option, } #[derive(Debug)] @@ -150,6 +154,7 @@ impl ResponseBuilder { large_blob_key: None, unsigned_extension_outputs: None, ep_att: None, + att_stmt: None, } } } diff --git a/src/ctap2/get_info.rs b/src/ctap2/get_info.rs index 7e30b77..05cd425 100644 --- a/src/ctap2/get_info.rs +++ b/src/ctap2/get_info.rs @@ -120,7 +120,7 @@ pub struct Response { // FIDO_2_2 #[cfg(feature = "get-info-full")] #[serde(skip_serializing_if = "Option::is_none")] - pub attestation_formats: Option>, + pub attestation_formats: Option>, // 0x17 // FIDO_2_2 diff --git a/src/ctap2/make_credential.rs b/src/ctap2/make_credential.rs index 4ef1441..3a4c76a 100644 --- a/src/ctap2/make_credential.rs +++ b/src/ctap2/make_credential.rs @@ -1,12 +1,14 @@ -use crate::{Bytes, String, Vec}; +use crate::Vec; use serde::{Deserialize, Serialize}; use serde_bytes::ByteArray; use serde_indexed::{DeserializeIndexed, SerializeIndexed}; -use super::{AuthenticatorOptions, Error}; +use super::{ + AttestationFormatsPreference, AttestationStatement, AttestationStatementFormat, + AuthenticatorOptions, Error, +}; use crate::ctap2::credential_management::CredentialProtectionPolicy; -use crate::sizes::*; use crate::webauthn::*; impl TryFrom for CredentialProtectionPolicy { @@ -44,7 +46,7 @@ pub struct Extensions { pub third_party_payment: Option, } -#[derive(Clone, Debug, Eq, PartialEq, SerializeIndexed, DeserializeIndexed)] +#[derive(Clone, Debug, Eq, PartialEq, DeserializeIndexed)] #[non_exhaustive] #[serde_indexed(offset = 1)] pub struct Request<'a> { @@ -64,6 +66,8 @@ pub struct Request<'a> { pub pin_protocol: Option, #[serde(skip_serializing_if = "Option::is_none")] pub enterprise_attestation: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub attestation_formats_preference: Option, } pub type AttestationObject = Response; @@ -110,7 +114,7 @@ impl<'a> super::SerializeAttestedCredentialData for AttestedCredentialData<'a> { #[non_exhaustive] #[serde_indexed(offset = 1)] pub struct Response { - pub fmt: String<32>, + pub fmt: AttestationStatementFormat, pub auth_data: super::SerializedAuthenticatorData, #[serde(skip_serializing_if = "Option::is_none")] pub att_stmt: Option, @@ -124,7 +128,7 @@ pub struct Response { #[derive(Debug)] pub struct ResponseBuilder { - pub fmt: String<32>, + pub fmt: AttestationStatementFormat, pub auth_data: super::SerializedAuthenticatorData, } @@ -142,34 +146,6 @@ impl ResponseBuilder { } } -#[derive(Clone, Debug, Eq, PartialEq, Serialize)] -#[non_exhaustive] -#[serde(untagged)] -#[allow(clippy::large_enum_variant)] -pub enum AttestationStatement { - None(NoneAttestationStatement), - Packed(PackedAttestationStatement), -} - -#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] -#[non_exhaustive] -#[serde(rename_all = "lowercase")] -pub enum AttestationStatementFormat { - None, - Packed, -} - -#[derive(Clone, Debug, Eq, PartialEq, Serialize)] -pub struct NoneAttestationStatement {} - -#[derive(Clone, Debug, Eq, PartialEq, Serialize)] -pub struct PackedAttestationStatement { - pub alg: i32, - pub sig: Bytes, - #[serde(skip_serializing_if = "Option::is_none")] - pub x5c: Option, 1>>, -} - #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[non_exhaustive] pub struct UnsignedExtensionOutputs {}