diff --git a/CHANGELOG.md b/CHANGELOG.md index a7b8793..faa1d15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Use enums instead of string constants - Introduce `Version`, `Extension` and `Transport` enums and use them in `ctap2::get_info` - Fix serialization of the `AttestationStatementFormat` enum and use it in `ctap2::make_credential` +- Remove `Deserialize` implementation for `ctap2::get_assertion::Response` +- Remove `Serialize` implementation for `ctap2::{get_assertion, make_credential}::Request` +- Move `AttestationStatement`, `AttestationStatementFormat`, `NoneAttestationStatement`, `PackedAttestationStatement` from `ctap2::make_credential` into the `ctap2` module ### Added @@ -23,6 +26,9 @@ 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 +- Derive `Copy` for `ctap2::AttestationStatementFormat` ## [0.2.0] - 2024-06-21 diff --git a/src/arbitrary.rs b/src/arbitrary.rs index 1731dc4..8d55dce 100644 --- a/src/arbitrary.rs +++ b/src/arbitrary.rs @@ -33,6 +33,18 @@ impl<'a> Arbitrary<'a> for ctap1::register::Request<'a> { } } +// cannot be derived because of missing impl for Vec<_> +impl<'a> Arbitrary<'a> for ctap2::AttestationFormatsPreference { + fn arbitrary(u: &mut Unstructured<'a>) -> Result { + let known_formats = arbitrary_vec(u)?; + let unknown = u.arbitrary()?; + Ok(Self { + known_formats, + unknown, + }) + } +} + // cannot be derived because of missing impl for serde_bytes::Bytes, EcdhEsHkdf256PublicKey impl<'a> Arbitrary<'a> for ctap2::client_pin::Request<'a> { fn arbitrary(u: &mut Unstructured<'a>) -> Result { @@ -138,6 +150,7 @@ impl<'a> Arbitrary<'a> for ctap2::get_assertion::Request<'a> { }; let pin_protocol = u.arbitrary()?; let enterprise_attestation = u.arbitrary()?; + let attestation_formats_preference = u.arbitrary()?; Ok(Self { rp_id, client_data_hash, @@ -147,6 +160,7 @@ impl<'a> Arbitrary<'a> for ctap2::get_assertion::Request<'a> { pin_auth, pin_protocol, enterprise_attestation, + attestation_formats_preference, }) } } @@ -196,6 +210,7 @@ impl<'a> Arbitrary<'a> for ctap2::make_credential::Request<'a> { }; let pin_protocol = u.arbitrary()?; let enterprise_attestation = u.arbitrary()?; + let attestation_formats_preference = u.arbitrary()?; Ok(Self { client_data_hash, rp, @@ -207,6 +222,7 @@ impl<'a> Arbitrary<'a> for ctap2::make_credential::Request<'a> { pin_auth, pin_protocol, enterprise_attestation, + attestation_formats_preference, }) } } diff --git a/src/ctap2.rs b/src/ctap2.rs index 67e1e87..717af21 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -6,7 +6,7 @@ use bitflags::bitflags; use cbor_smol::cbor_deserialize; use serde::{Deserialize, Serialize}; -use crate::{sizes::*, Bytes, Vec}; +use crate::{sizes::*, Bytes, TryFromStrError, Vec}; pub use crate::operation::{Operation, VendorOperation}; @@ -250,6 +250,111 @@ 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, Serialize, Deserialize)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[non_exhaustive] +#[serde(into = "&str", try_from = "&str")] +pub enum AttestationStatementFormat { + None, + Packed, +} + +impl AttestationStatementFormat { + const NONE: &'static str = "none"; + const PACKED: &'static str = "packed"; +} + +impl From for &str { + fn from(format: AttestationStatementFormat) -> Self { + match format { + AttestationStatementFormat::None => AttestationStatementFormat::NONE, + AttestationStatementFormat::Packed => AttestationStatementFormat::PACKED, + } + } +} + +impl TryFrom<&str> for AttestationStatementFormat { + type Error = TryFromStrError; + + fn try_from(s: &str) -> core::result::Result { + match s { + Self::NONE => Ok(Self::None), + Self::PACKED => Ok(Self::Packed), + _ => Err(TryFromStrError), + } + } +} + +#[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 { + pub(crate) known_formats: Vec, + pub(crate) 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 Ok(format) = AttestationStatementFormat::try_from(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 c31d587..5b243db 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::*; @@ -85,7 +85,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> { @@ -103,12 +103,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 { @@ -129,6 +131,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)] @@ -151,6 +155,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 2630ad3..945f7a3 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 a77bc7d..0d6f992 100644 --- a/src/ctap2/make_credential.rs +++ b/src/ctap2/make_credential.rs @@ -1,12 +1,14 @@ -use crate::{Bytes, TryFromStrError, 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 { @@ -45,7 +47,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> { @@ -65,6 +67,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; @@ -143,60 +147,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, Serialize, Deserialize)] -#[non_exhaustive] -#[serde(into = "&str", try_from = "&str")] -pub enum AttestationStatementFormat { - None, - Packed, -} - -impl AttestationStatementFormat { - const NONE: &'static str = "none"; - const PACKED: &'static str = "packed"; -} - -impl From for &str { - fn from(format: AttestationStatementFormat) -> Self { - match format { - AttestationStatementFormat::None => AttestationStatementFormat::NONE, - AttestationStatementFormat::Packed => AttestationStatementFormat::PACKED, - } - } -} - -impl TryFrom<&str> for AttestationStatementFormat { - type Error = TryFromStrError; - - fn try_from(s: &str) -> Result { - match s { - Self::NONE => Ok(Self::None), - Self::PACKED => Ok(Self::Packed), - _ => Err(TryFromStrError), - } - } -} - -#[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 {}