From 8eaa0fd940e6896682ee68750469259736ccb2e5 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus <> Date: Wed, 21 Feb 2024 13:53:54 +0100 Subject: [PATCH] New struct members added in CTAP 2.2 --- src/ctap2/commands/credential_management.rs | 13 ++++- src/ctap2/commands/get_assertion.rs | 61 ++++++++++++++++++++- src/ctap2/commands/get_info.rs | 30 +++++++++- src/ctap2/commands/make_credentials.rs | 37 +++++++++++-- src/ctap2/server.rs | 2 + src/util.rs | 10 ++++ 6 files changed, 145 insertions(+), 8 deletions(-) diff --git a/src/ctap2/commands/credential_management.rs b/src/ctap2/commands/credential_management.rs index c555c3fb..a363cb2c 100644 --- a/src/ctap2/commands/credential_management.rs +++ b/src/ctap2/commands/credential_management.rs @@ -136,6 +136,10 @@ pub struct CredentialManagementResponse { pub cred_protect: Option, /// Large blob encryption key. pub large_blob_key: Option>, + + // CTAP 2.2 + /// Whether the credential is third-party payment enabled, if supported by the authenticator. + pub third_party_payment: Option, } impl CtapResponse for CredentialManagementResponse {} @@ -215,6 +219,7 @@ impl<'de> Deserialize<'de> for CredentialManagementResponse { let mut total_credentials = None; // (0x09) Unsigned Integer Total number of credentials present on the authenticator for the RP in question let mut cred_protect = None; // (0x0A) Unsigned Integer Credential protection policy. let mut large_blob_key = None; // (0x0B) Byte string Large blob encryption key. + let mut third_party_payment = None; // (0x0C) bool while let Some(key) = map.next_key()? { match key { @@ -294,7 +299,12 @@ impl<'de> Deserialize<'de> for CredentialManagementResponse { // Using into_vec, to avoid any copy of large_blob_key large_blob_key = Some(map.next_value::()?.into_vec()); } - + 0x0C => { + if third_party_payment.is_some() { + return Err(SerdeError::duplicate_field("third_party_payment")); + } + third_party_payment = Some(map.next_value()?); + } k => { warn!("ClientPinResponse: unexpected key: {:?}", k); let _ = map.next_value::()?; @@ -315,6 +325,7 @@ impl<'de> Deserialize<'de> for CredentialManagementResponse { total_credentials, cred_protect, large_blob_key, + third_party_payment, }) } } diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 2a9823cc..39f1c02f 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -31,6 +31,7 @@ use serde::{ }; use serde_bytes::ByteBuf; use serde_cbor::{de::from_slice, ser, Value}; +use std::collections::HashMap; use std::convert::TryFrom; use std::fmt; use std::io::Cursor; @@ -258,6 +259,8 @@ pub struct GetAssertionExtensions { pub cred_blob: Option, #[serde(rename = "largeBlobKey", skip_serializing_if = "Option::is_none")] pub large_blob_key: Option, + #[serde(rename = "thirdPartyPayment", skip_serializing_if = "Option::is_none")] + pub third_party_payment: Option, } impl From for GetAssertionExtensions { @@ -281,13 +284,17 @@ impl From for GetAssertionExtensions { _ => None, }, large_blob_key: input.large_blob_key, + third_party_payment: input.third_party_payment, } } } impl GetAssertionExtensions { fn has_content(&self) -> bool { - self.hmac_secret.is_some() || self.cred_blob.is_some() || self.large_blob_key.is_some() + self.hmac_secret.is_some() + || self.cred_blob.is_some() + || self.large_blob_key.is_some() + || self.third_party_payment.is_some() } } @@ -306,6 +313,10 @@ pub struct GetAssertion { pub extensions: GetAssertionExtensions, pub options: GetAssertionOptions, pub pin_uv_auth_param: Option, + + // CTAP 2.2: + pub enterprise_attestation: Option, + pub attestation_formats_preference: Option>, } impl GetAssertion { @@ -323,6 +334,8 @@ impl GetAssertion { extensions, options, pin_uv_auth_param: None, + enterprise_attestation: None, + attestation_formats_preference: None, } } @@ -508,6 +521,8 @@ impl Serialize for GetAssertion { &5 => self.options.has_some().then_some(&self.options), &6 => &self.pin_uv_auth_param, &7 => self.pin_uv_auth_param.as_ref().map(|p| p.pin_protocol.id()), + &8 => &self.enterprise_attestation, + &9 => &self.attestation_formats_preference, } } } @@ -755,6 +770,9 @@ pub struct GetAssertionResponse { pub number_of_credentials: Option, pub user_selected: Option, pub large_blob_key: Option>, + pub unsigned_extension_outputs: Option>, + pub ep_attestation: Option, + pub att_stmt: Option>, } impl CtapResponse for GetAssertionResponse {} @@ -784,6 +802,9 @@ impl<'de> Deserialize<'de> for GetAssertionResponse { let mut number_of_credentials = None; let mut user_selected = None; let mut large_blob_key = None; + let mut unsigned_extension_outputs = None; + let mut ep_attestation = None; + let mut att_stmt = None; while let Some(key) = map.next_key()? { match key { @@ -831,6 +852,26 @@ impl<'de> Deserialize<'de> for GetAssertionResponse { let large_blob_key_bytes: ByteBuf = map.next_value()?; large_blob_key = Some(large_blob_key_bytes.into_vec()); } + 0x08 => { + if unsigned_extension_outputs.is_some() { + return Err(M::Error::duplicate_field( + "unsigned_extension_outputs", + )); + } + unsigned_extension_outputs = Some(map.next_value()?); + } + 0x09 => { + if ep_attestation.is_some() { + return Err(M::Error::duplicate_field("ep_attestation")); + } + ep_attestation = Some(map.next_value()?); + } + 0x0A => { + if att_stmt.is_some() { + return Err(M::Error::duplicate_field("att_stmt")); + } + att_stmt = Some(map.next_value()?); + } k => return Err(M::Error::custom(format!("unexpected key: {k:?}"))), } } @@ -846,6 +887,9 @@ impl<'de> Deserialize<'de> for GetAssertionResponse { number_of_credentials, user_selected, large_blob_key, + unsigned_extension_outputs, + ep_attestation, + att_stmt, }) } } @@ -1101,12 +1145,15 @@ pub mod test { )), cred_blob: None, large_blob_key: None, + third_party_payment: None, }, options: GetAssertionOptions { user_presence: Some(true), user_verification: None, }, pin_uv_auth_param: Some(PinUvAuthParam::create_empty()), + enterprise_attestation: None, + attestation_formats_preference: None, }; let req_serialized = assertion .wire_format() @@ -1160,6 +1207,7 @@ pub mod test { )), cred_blob: None, large_blob_key: None, + third_party_payment: None, }, options: GetAssertionOptions { user_presence: None, @@ -1170,6 +1218,8 @@ pub mod test { vec![9; 4], PinUvAuthTokenPermission::GetAssertion, )), + enterprise_attestation: None, + attestation_formats_preference: None, }; let req_serialized = assertion .wire_format() @@ -1207,12 +1257,15 @@ pub mod test { )), cred_blob: None, large_blob_key: None, + third_party_payment: None, }, options: GetAssertionOptions { user_presence: None, user_verification: None, }, pin_uv_auth_param: None, + enterprise_attestation: None, + attestation_formats_preference: None, }; assertion .wire_format() @@ -1230,12 +1283,15 @@ pub mod test { hmac_secret: Some(HmacGetSecretOrPrf::PrfUnmatched), cred_blob: None, large_blob_key: None, + third_party_payment: None, }, options: GetAssertionOptions { user_presence: None, user_verification: None, }, pin_uv_auth_param: None, + enterprise_attestation: None, + attestation_formats_preference: None, }; let req_serialized = assertion .wire_format() @@ -1636,6 +1692,9 @@ pub mod test { certifications: None, remaining_discoverable_credentials: None, vendor_prototype_config_commands: None, + attestation_formats: None, + uv_count_since_last_pin_entry: None, + long_touch_for_reset: None, }); // Sending first GetAssertion with first allow_list-entry, that will return an error diff --git a/src/ctap2/commands/get_info.rs b/src/ctap2/commands/get_info.rs index f676605a..27ad96b9 100644 --- a/src/ctap2/commands/get_info.rs +++ b/src/ctap2/commands/get_info.rs @@ -337,6 +337,10 @@ pub struct AuthenticatorInfo { pub certifications: Option>, pub remaining_discoverable_credentials: Option, pub vendor_prototype_config_commands: Option>, + // CTAP 2.2 + pub attestation_formats: Option>, + pub uv_count_since_last_pin_entry: Option, + pub long_touch_for_reset: Option, } impl AuthenticatorInfo { @@ -418,6 +422,9 @@ impl<'de> Deserialize<'de> for AuthenticatorInfo { let mut certifications = None; let mut remaining_discoverable_credentials = None; let mut vendor_prototype_config_commands = None; + let mut attestation_formats = None; + let mut uv_count_since_last_pin_entry = None; + let mut long_touch_for_reset = None; while let Some(key) = map.next_key()? { match key { 0x01 => { @@ -489,6 +496,15 @@ impl<'de> Deserialize<'de> for AuthenticatorInfo { 0x15 => { parse_next_optional_value!(vendor_prototype_config_commands, map); } + 0x16 => { + parse_next_optional_value!(attestation_formats, map); + } + 0x17 => { + parse_next_optional_value!(uv_count_since_last_pin_entry, map); + } + 0x18 => { + parse_next_optional_value!(long_touch_for_reset, map); + } k => { warn!("GetInfo: unexpected key: {:?}", k); let _ = map.next_value::()?; @@ -535,6 +551,9 @@ impl<'de> Deserialize<'de> for AuthenticatorInfo { certifications, remaining_discoverable_credentials, vendor_prototype_config_commands, + attestation_formats, + uv_count_since_last_pin_entry, + long_touch_for_reset, }) } else { Err(M::Error::custom("No AAGuid specified".to_string())) @@ -776,6 +795,9 @@ pub mod tests { certifications: None, remaining_discoverable_credentials: None, vendor_prototype_config_commands: None, + attestation_formats: None, + uv_count_since_last_pin_entry: None, + long_touch_for_reset: None, }; assert_eq!(authenticator_info, expected); @@ -786,7 +808,7 @@ pub mod tests { broken_payload[0] += 1; // Add the additional entry at the back with an invalid key broken_payload.extend_from_slice(&[ - 0x17, // unsigned(23) -> invalid key-number. CTAP2.1 goes only to 0x15 + 0x27, // unsigned(39) -> invalid key-number. CTAP2.2 goes only to 0x18 0x6B, // text(11) 0x69, 0x6E, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x5F, 0x6B, 0x65, 0x79, // "invalid_key" ]); @@ -863,6 +885,9 @@ pub mod tests { certifications: None, remaining_discoverable_credentials: Some(24), vendor_prototype_config_commands: None, + attestation_formats: None, + uv_count_since_last_pin_entry: None, + long_touch_for_reset: None, }; assert_eq!(authenticator_info, expected); @@ -956,6 +981,9 @@ pub mod tests { certifications: None, remaining_discoverable_credentials: None, vendor_prototype_config_commands: None, + attestation_formats: None, + uv_count_since_last_pin_entry: None, + long_touch_for_reset: None, }; assert_eq!(result, &expected); diff --git a/src/ctap2/commands/make_credentials.rs b/src/ctap2/commands/make_credentials.rs index 760aaf31..431629f3 100644 --- a/src/ctap2/commands/make_credentials.rs +++ b/src/ctap2/commands/make_credentials.rs @@ -30,6 +30,7 @@ use serde::{ }; use serde_bytes::ByteBuf; use serde_cbor::{self, de::from_slice, ser, Value}; +use std::collections::HashMap; use std::fmt; use std::io::{Cursor, Read}; @@ -40,6 +41,7 @@ pub struct MakeCredentialsResult { pub extensions: AuthenticationExtensionsClientOutputs, pub ep_attestation: Option, pub large_blob_key: Option>, + pub unsigned_extension_outputs: Option>, } impl MakeCredentialsResult { @@ -111,6 +113,7 @@ impl MakeCredentialsResult { extensions: Default::default(), ep_attestation: None, large_blob_key: None, + unsigned_extension_outputs: None, }) } } @@ -138,22 +141,24 @@ impl<'de> Deserialize<'de> for MakeCredentialsResult { let mut att_stmt: Option = None; let mut ep_attestation: Option = None; let mut large_blob_key: Option> = None; + let mut unsigned_extension_outputs: Option> = + None; while let Some(key) = map.next_key()? { match key { - 1 => { + 0x01 => { if format.is_some() { return Err(DesError::duplicate_field("fmt (0x01)")); } format = Some(map.next_value()?); } - 2 => { + 0x02 => { if auth_data.is_some() { return Err(DesError::duplicate_field("authData (0x02)")); } auth_data = Some(map.next_value()?); } - 3 => { + 0x03 => { let format = format.ok_or_else(|| DesError::missing_field("fmt (0x01)"))?; if att_stmt.is_some() { @@ -182,20 +187,28 @@ impl<'de> Deserialize<'de> for MakeCredentialsResult { } } } - 4 => { + 0x04 => { if ep_attestation.is_some() { return Err(M::Error::duplicate_field("ep_attestation")); } let ep_attestation_val: bool = map.next_value()?; ep_attestation = Some(ep_attestation_val); } - 5 => { + 0x05 => { if large_blob_key.is_some() { return Err(M::Error::duplicate_field("large_blob_key")); } let large_blob_key_bytes: ByteBuf = map.next_value()?; large_blob_key = Some(large_blob_key_bytes.into_vec()); } + 0x06 => { + if unsigned_extension_outputs.is_some() { + return Err(M::Error::duplicate_field( + "unsigned_extension_outputs", + )); + } + unsigned_extension_outputs = Some(map.next_value()?); + } _ => continue, } } @@ -214,6 +227,7 @@ impl<'de> Deserialize<'de> for MakeCredentialsResult { extensions: Default::default(), ep_attestation, large_blob_key, + unsigned_extension_outputs, }) } } @@ -269,6 +283,8 @@ pub struct MakeCredentialsExtensions { pub cred_blob: Option, #[serde(rename = "largeBlobKey", skip_serializing_if = "Option::is_none")] pub large_blob_key: Option, + #[serde(rename = "thirdPartyPayment", skip_serializing_if = "Option::is_none")] + pub third_party_payment: Option, } #[derive(Debug, Clone)] @@ -296,6 +312,7 @@ impl MakeCredentialsExtensions { || self.min_pin_length.is_some() || self.cred_blob.is_some() || self.large_blob_key.is_some() + || self.third_party_payment.is_some() } } @@ -314,6 +331,7 @@ impl From for MakeCredentialsExtensions { min_pin_length: input.min_pin_length, cred_blob: input.cred_blob, large_blob_key: input.large_blob_key, + third_party_payment: input.third_party_payment, } } } @@ -337,6 +355,9 @@ pub struct MakeCredentials { pub options: MakeCredentialsOptions, pub pin_uv_auth_param: Option, pub enterprise_attestation: Option, + + // CTAP 2.2 + pub attestation_formats_preference: Option>, } impl MakeCredentials { @@ -360,6 +381,7 @@ impl MakeCredentials { options, pin_uv_auth_param: None, enterprise_attestation: None, + attestation_formats_preference: None, } } @@ -526,6 +548,7 @@ impl Serialize for MakeCredentials { &0x08 => &self.pin_uv_auth_param, &0x09 => self.pin_uv_auth_param.as_ref().map(|p| p.pin_protocol.id()), &0x0a => &self.enterprise_attestation, + &0x0b => &self.attestation_formats_preference, ) } } @@ -748,6 +771,7 @@ pub mod test { extensions: Default::default(), ep_attestation: None, large_blob_key: None, + unsigned_extension_outputs: None, }; assert_eq!(make_cred_result, expected); @@ -797,6 +821,7 @@ pub mod test { min_pin_length: Some(true), cred_blob: None, large_blob_key: None, + third_party_payment: None, }, options: MakeCredentialsOptions { resident_key: Some(true), @@ -812,6 +837,7 @@ pub mod test { p }), enterprise_attestation: Some(7), + attestation_formats_preference: None, }; let req_serialized = req @@ -999,6 +1025,7 @@ pub mod test { extensions: Default::default(), ep_attestation: None, large_blob_key: None, + unsigned_extension_outputs: None, }; assert_eq!(make_cred_result, expected); diff --git a/src/ctap2/server.rs b/src/ctap2/server.rs index ff75f592..9b1de8d2 100644 --- a/src/ctap2/server.rs +++ b/src/ctap2/server.rs @@ -397,6 +397,8 @@ pub struct AuthenticationExtensionsClientInputs { /// GetAssertion-requests use AsBool pub cred_blob: Option, pub large_blob_key: Option, + // CTAP 2.2 + pub third_party_payment: Option, } #[derive(Clone, Debug, Default, Eq, PartialEq)] diff --git a/src/util.rs b/src/util.rs index 509f4aa9..ddf51c2c 100644 --- a/src/util.rs +++ b/src/util.rs @@ -166,6 +166,16 @@ macro_rules! serialize_map_optional { $( $value_ident : $key => $value , )* ) }; + ($s:expr, $k1:expr => $v1:expr, $k2:expr => $v2:expr, $k3:expr => $v3:expr, $k4:expr => $v4:expr, $k5:expr => $v5:expr, + $k6:expr => $v6:expr, $k7:expr => $v7:expr, $k8:expr => $v8:expr, $k9:expr => $v9:expr, $ka:expr => $va:expr, $kb:expr => $vb:expr, + $( $value_ident:ident : $key:expr => $value:expr , )*) => { + serialize_map_optional!( + @internal $s, + v1: $k1 => $v1, v2: $k2 => $v2, v3: $k3 => $v3, v4: $k4 => $v4, v5: $k5 => $v5, + v6: $k6 => $v6, v7: $k7 => $v7, v8: $k8 => $v8, v9: $k9 => $v9, va: $ka => $va, + vb: $kb => $vb, $( $value_ident : $key => $value , )* + ) + }; (@internal $serializer:expr, $( $value_ident:ident : $key:expr => $value:expr , )*) => { {