diff --git a/CHANGELOG.md b/CHANGELOG.md index 39c1e90..faa1d15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,11 +13,22 @@ 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 - Add a `std` feature (disabled by default) - Add `arbitrary::Arbitrary` implementations for all requests behind an `arbitrary` feature (disabled by default) +- Add support for CTAP 2.2 ([#38](https://github.com/trussed-dev/ctap-types/issues/38)) + - Add support for the `thirdPartyPayment` extension behind a `third-party-payment` feature (disabled by default) + - 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/Cargo.toml b/Cargo.toml index d50a748..06432c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ arbitrary = ["dep:arbitrary", "std"] get-info-full = [] # enables support for implementing the large-blobs extension, see src/sizes.rs large-blobs = [] +third-party-payment = [] log-all = [] log-none = [] diff --git a/src/arbitrary.rs b/src/arbitrary.rs index fe22654..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 { @@ -137,6 +149,8 @@ impl<'a> Arbitrary<'a> for ctap2::get_assertion::Request<'a> { None }; let pin_protocol = u.arbitrary()?; + let enterprise_attestation = u.arbitrary()?; + let attestation_formats_preference = u.arbitrary()?; Ok(Self { rp_id, client_data_hash, @@ -145,6 +159,8 @@ impl<'a> Arbitrary<'a> for ctap2::get_assertion::Request<'a> { options, pin_auth, pin_protocol, + enterprise_attestation, + attestation_formats_preference, }) } } @@ -194,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, @@ -205,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/credential_management.rs b/src/ctap2/credential_management.rs index 2f9e082..491fde2 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -108,4 +108,8 @@ pub struct Response { // 0x0B #[serde(skip_serializing_if = "Option::is_none")] pub large_blob_key: Option>, + // 0x0C + #[cfg(feature = "third-party-payment")] + #[serde(skip_serializing_if = "Option::is_none")] + pub third_party_payment: Option, } diff --git a/src/ctap2/get_assertion.rs b/src/ctap2/get_assertion.rs index d6373ba..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::*; @@ -27,10 +27,16 @@ pub struct ExtensionsInput { #[serde(rename = "hmac-secret")] #[serde(skip_serializing_if = "Option::is_none")] pub hmac_secret: Option, + /// Whether a large blob key is requested. #[serde(rename = "largeBlobKey")] #[serde(skip_serializing_if = "Option::is_none")] pub large_blob_key: Option, + + #[cfg(feature = "third-party-payment")] + #[serde(rename = "thirdPartyPayment")] + #[serde(skip_serializing_if = "Option::is_none")] + pub third_party_payment: Option, } #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] @@ -40,6 +46,30 @@ pub struct ExtensionsOutput { #[serde(skip_serializing_if = "Option::is_none")] // *either* enc(output1) *or* enc(output1 || output2) pub hmac_secret: Option>, + + #[cfg(feature = "third-party-payment")] + #[serde(rename = "thirdPartyPayment")] + #[serde(skip_serializing_if = "Option::is_none")] + pub third_party_payment: Option, +} + +impl ExtensionsOutput { + #[inline] + pub fn is_set(&self) -> bool { + let Self { + hmac_secret, + #[cfg(feature = "third-party-payment")] + third_party_payment, + } = self; + if hmac_secret.is_some() { + return true; + } + #[cfg(feature = "third-party-payment")] + if third_party_payment.is_some() { + return true; + } + false + } } pub struct NoAttestedCredentialData; @@ -55,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> { @@ -71,12 +101,16 @@ pub struct Request<'a> { pub pin_auth: Option<&'a serde_bytes::Bytes>, #[serde(skip_serializing_if = "Option::is_none")] 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 { @@ -93,6 +127,12 @@ pub struct Response { /// See https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#sctn-getAssert-authnr-alg #[serde(skip_serializing_if = "Option::is_none")] pub large_blob_key: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + 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)] @@ -113,6 +153,13 @@ impl ResponseBuilder { number_of_credentials: None, user_selected: None, large_blob_key: None, + unsigned_extension_outputs: None, + ep_att: None, + att_stmt: None, } } } + +#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +pub struct UnsignedExtensionOutputs {} diff --git a/src/ctap2/get_info.rs b/src/ctap2/get_info.rs index e625c96..945f7a3 100644 --- a/src/ctap2/get_info.rs +++ b/src/ctap2/get_info.rs @@ -115,6 +115,24 @@ pub struct Response { #[cfg(feature = "get-info-full")] #[serde(skip_serializing_if = "Option::is_none")] pub vendor_prototype_config_commands: Option, + + // 0x16 + // FIDO_2_2 + #[cfg(feature = "get-info-full")] + #[serde(skip_serializing_if = "Option::is_none")] + pub attestation_formats: Option>, + + // 0x17 + // FIDO_2_2 + #[cfg(feature = "get-info-full")] + #[serde(skip_serializing_if = "Option::is_none")] + pub uv_count_since_last_pin_entry: Option, + + // 0x18 + // FIDO_2_2 + #[cfg(feature = "get-info-full")] + #[serde(skip_serializing_if = "Option::is_none")] + pub long_touch_for_reset: Option, } impl Default for Response { @@ -174,6 +192,12 @@ impl ResponseBuilder { remaining_discoverable_credentials: None, #[cfg(feature = "get-info-full")] vendor_prototype_config_commands: None, + #[cfg(feature = "get-info-full")] + attestation_formats: None, + #[cfg(feature = "get-info-full")] + uv_count_since_last_pin_entry: None, + #[cfg(feature = "get-info-full")] + long_touch_for_reset: None, } } } @@ -227,12 +251,14 @@ pub enum Extension { CredProtect, HmacSecret, LargeBlobKey, + ThirdPartyPayment, } impl Extension { const CRED_PROTECT: &'static str = "credProtect"; const HMAC_SECRET: &'static str = "hmac-secret"; const LARGE_BLOB_KEY: &'static str = "largeBlobKey"; + const THIRD_PARTY_PAYMENT: &'static str = "thirdPartyPayment"; } impl From for &str { @@ -241,6 +267,7 @@ impl From for &str { Extension::CredProtect => Extension::CRED_PROTECT, Extension::HmacSecret => Extension::HMAC_SECRET, Extension::LargeBlobKey => Extension::LARGE_BLOB_KEY, + Extension::ThirdPartyPayment => Extension::THIRD_PARTY_PAYMENT, } } } @@ -253,6 +280,7 @@ impl TryFrom<&str> for Extension { Self::CRED_PROTECT => Ok(Self::CredProtect), Self::HMAC_SECRET => Ok(Self::HmacSecret), Self::LARGE_BLOB_KEY => Ok(Self::LargeBlobKey), + Self::THIRD_PARTY_PAYMENT => Ok(Self::ThirdPartyPayment), _ => Err(TryFromStrError), } } diff --git a/src/ctap2/make_credential.rs b/src/ctap2/make_credential.rs index 087e663..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 { @@ -29,16 +31,23 @@ pub struct Extensions { #[serde(rename = "credProtect")] #[serde(skip_serializing_if = "Option::is_none")] pub cred_protect: Option, + #[serde(rename = "hmac-secret")] #[serde(skip_serializing_if = "Option::is_none")] pub hmac_secret: Option, + // See https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#sctn-largeBlobKey-extension #[serde(rename = "largeBlobKey")] #[serde(skip_serializing_if = "Option::is_none")] pub large_blob_key: Option, + + #[cfg(feature = "third-party-payment")] + #[serde(rename = "thirdPartyPayment")] + #[serde(skip_serializing_if = "Option::is_none")] + 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> { @@ -58,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; @@ -112,6 +123,8 @@ pub struct Response { pub ep_att: Option, #[serde(skip_serializing_if = "Option::is_none")] pub large_blob_key: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub unsigned_extension_outputs: Option, } #[derive(Debug)] @@ -129,63 +142,14 @@ impl ResponseBuilder { att_stmt: None, ep_att: None, large_blob_key: None, + unsigned_extension_outputs: None, } } } #[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)] -#[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>>, -} +pub struct UnsignedExtensionOutputs {} #[cfg(test)] mod tests {