diff --git a/Cargo.toml b/Cargo.toml index 0356b0b..4fd63af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ name = "usbip" required-features = ["dispatch"] [dependencies] -ctap-types = { version = "0.2.0", features = ["large-blobs"] } +ctap-types = { version = "0.2.0", features = ["large-blobs", "third-party-payment"] } cosey = "0.3" delog = "0.1.0" heapless = "0.7" @@ -49,6 +49,7 @@ log-error = [] aes = "0.8.4" cbc = { version = "0.1.2", features = ["alloc"] } ciborium = { version = "0.2.2" } +ciborium-io = "0.2.2" cipher = "0.4.4" ctaphid = { version = "0.3.1", default-features = false } delog = { version = "0.1.6", features = ["std-log"] } diff --git a/src/credential.rs b/src/credential.rs index bae32ef..b0219e8 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -196,6 +196,13 @@ impl Credential { Self::Stripped(credential) => &credential.key, } } + + pub fn third_party_payment(&self) -> Option { + match self { + Self::Full(credential) => credential.data.third_party_payment, + Self::Stripped(credential) => credential.third_party_payment, + } + } } /// The main content of a `FullCredential`. @@ -239,6 +246,9 @@ pub struct CredentialData { // extensions (cont. -- we can only append new options due to index-based deserialization) #[serde(skip_serializing_if = "Option::is_none")] pub large_blob_key: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub third_party_payment: Option, } // TODO: figure out sizes @@ -332,6 +342,7 @@ impl FullCredential { hmac_secret: Option, cred_protect: Option, large_blob_key: Option>, + third_party_payment: Option, nonce: [u8; 12], ) -> Self { info!("credential for algorithm {}", algorithm); @@ -347,6 +358,7 @@ impl FullCredential { hmac_secret, cred_protect, large_blob_key, + third_party_payment, use_short_id: Some(true), }; @@ -456,6 +468,8 @@ pub struct StrippedCredential { // TODO: HACK -- remove #[serde(skip_serializing_if = "Option::is_none")] pub large_blob_key: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub third_party_payment: Option, } impl StrippedCredential { @@ -491,6 +505,7 @@ impl From<&FullCredential> for StrippedCredential { hmac_secret: credential.data.hmac_secret, cred_protect: credential.data.cred_protect, large_blob_key: credential.data.large_blob_key, + third_party_payment: credential.data.third_party_payment, } } } @@ -525,6 +540,7 @@ mod test { cred_protect: None, use_short_id: Some(true), large_blob_key: Some(ByteArray::new([0xff; 32])), + third_party_payment: Some(true), } } @@ -611,6 +627,7 @@ mod test { cred_protect: None, use_short_id: Some(true), large_blob_key: Some(random_byte_array()), + third_party_payment: Some(false), } } @@ -693,6 +710,7 @@ mod test { hmac_secret: Some(true), cred_protect: Some(CredentialProtectionPolicy::Required), large_blob_key: Some(ByteArray::new([0xff; 32])), + third_party_payment: Some(true), }; trussed::virt::with_ram_client("fido", |mut client| { let kek = syscall!(client.generate_chacha8poly1305_key(Location::Internal)).key; @@ -702,7 +720,7 @@ mod test { .try_into() .unwrap(); let id = credential.id(&mut client, kek, &rp_id_hash).unwrap(); - assert_eq!(id.0.len(), 239); + assert_eq!(id.0.len(), 241); }); } diff --git a/src/ctap1.rs b/src/ctap1.rs index 3af469f..f6dd013 100644 --- a/src/ctap1.rs +++ b/src/ctap1.rs @@ -91,6 +91,7 @@ impl Authenticator for crate::Authenti hmac_secret: None, cred_protect: None, large_blob_key: None, + third_party_payment: None, }; // info!("made credential {:?}", &credential); diff --git a/src/ctap2.rs b/src/ctap2.rs index 6335631..6297069 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -56,6 +56,7 @@ impl Authenticator for crate::Authenti if self.config.supports_large_blobs() { extensions.push(Extension::LargeBlobKey).unwrap(); } + extensions.push(Extension::ThirdPartyPayment).unwrap(); let mut pin_protocols = Vec::new(); for pin_protocol in self.pin_protocols() { @@ -221,6 +222,7 @@ impl Authenticator for crate::Authenti // let mut cred_protect_requested = CredentialProtectionPolicy::Optional; let mut cred_protect_requested = None; let mut large_blob_key_requested = false; + let mut third_party_payment_requested = false; if let Some(extensions) = ¶meters.extensions { hmac_secret_requested = extensions.hmac_secret; @@ -243,6 +245,8 @@ impl Authenticator for crate::Authenti } } } + + third_party_payment_requested = extensions.third_party_payment.unwrap_or_default(); } // debug_now!("hmac-secret = {:?}, credProtect = {:?}", hmac_secret_requested, cred_protect_requested); @@ -360,6 +364,7 @@ impl Authenticator for crate::Authenti hmac_secret_requested, cred_protect_requested, large_blob_key, + third_party_payment_requested.then_some(true), nonce, ); @@ -1502,9 +1507,11 @@ impl crate::Authenticator { &mut self, get_assertion_state: &state::ActiveGetAssertionData, extensions: &ctap2::get_assertion::ExtensionsInput, - _credential: &Credential, + credential: &Credential, credential_key: KeyId, ) -> Result> { + let mut output = ctap2::get_assertion::ExtensionsOutput::default(); + if let Some(hmac_secret) = &extensions.hmac_secret { let pin_protocol = hmac_secret .pin_protocol @@ -1565,12 +1572,14 @@ impl crate::Authenticator { shared_secret.delete(&mut self.trussed); - let mut extensions = ctap2::get_assertion::ExtensionsOutput::default(); - extensions.hmac_secret = Some(Bytes::from_slice(&output_enc).unwrap()); - Ok(Some(extensions)) - } else { - Ok(None) + output.hmac_secret = Some(Bytes::from_slice(&output_enc).unwrap()); } + + if extensions.third_party_payment.unwrap_or_default() { + output.third_party_payment = Some(credential.third_party_payment().unwrap_or_default()); + } + + Ok(output.is_set().then_some(output)) } #[inline(never)] diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index 8df5e70..527fc52 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -453,6 +453,8 @@ where response.public_key = Some(cose_public_key); response.cred_protect = cred_protect; response.large_blob_key = credential.data.large_blob_key; + response.third_party_payment = + Some(credential.data.third_party_payment.unwrap_or_default()); Ok(response) } diff --git a/tests/basic.rs b/tests/basic.rs index 1e00a00..7e6a898 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -10,9 +10,9 @@ use hex_literal::hex; use virt::{Ctap2, Ctap2Error}; use webauthn::{ - ClientPin, CredentialManagement, CredentialManagementParams, GetAssertion, GetInfo, - KeyAgreementKey, MakeCredential, MakeCredentialOptions, PinToken, PubKeyCredDescriptor, - PubKeyCredParam, PublicKey, Rp, SharedSecret, User, + ClientPin, CredentialManagement, CredentialManagementParams, ExtensionsInput, GetAssertion, + GetInfo, KeyAgreementKey, MakeCredential, MakeCredentialOptions, PinToken, + PubKeyCredDescriptor, PubKeyCredParam, PublicKey, Rp, SharedSecret, User, }; #[test] @@ -215,38 +215,85 @@ fn test_make_credential() { } } +#[derive(Debug)] +struct TestGetAssertion { + mc_third_party_payment: Option, + ga_third_party_payment: Option, +} + +impl TestGetAssertion { + fn run(&self) { + println!("{}", "=".repeat(80)); + println!("Running test:"); + println!("{self:#?}"); + println!(); + + let rp_id = "example.com"; + // TODO: client data + let client_data_hash = &[0; 32]; + + virt::run_ctap2(|device| { + let rp = Rp::new(rp_id); + let user = User::new(b"id123") + .name("john.doe") + .display_name("John Doe"); + let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -7)]; + let mut request = MakeCredential::new(client_data_hash, rp, user, pub_key_cred_params); + if let Some(third_party_payment) = self.mc_third_party_payment { + request.extensions = Some(ExtensionsInput { + third_party_payment: Some(third_party_payment), + }); + } + let response = device.exec(request).unwrap(); + let credential = response.auth_data.credential.unwrap(); + + let mut request = GetAssertion::new(rp_id, client_data_hash); + request.allow_list = Some(vec![PubKeyCredDescriptor::new( + "public-key", + credential.id.clone(), + )]); + if let Some(third_party_payment) = self.ga_third_party_payment { + request.extensions = Some(ExtensionsInput { + third_party_payment: Some(third_party_payment), + }); + } + let response = device.exec(request).unwrap(); + assert_eq!(response.credential.ty, "public-key"); + assert_eq!(response.credential.id, credential.id); + assert_eq!(response.auth_data.credential, None); + credential.verify_assertion(&response.auth_data, client_data_hash, &response.signature); + if self.ga_third_party_payment.unwrap_or_default() { + let extensions = response.auth_data.extensions.unwrap(); + assert_eq!( + extensions.get("thirdPartyPayment"), + Some(&Value::from( + self.mc_third_party_payment.unwrap_or_default() + )) + ); + } else { + assert!(response.auth_data.extensions.is_none()); + } + }); + } +} + #[test] fn test_get_assertion() { - let rp_id = "example.com"; - // TODO: client data - let client_data_hash = &[0; 32]; - - virt::run_ctap2(|device| { - let rp = Rp::new(rp_id); - let user = User::new(b"id123") - .name("john.doe") - .display_name("John Doe"); - let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -7)]; - let request = MakeCredential::new(client_data_hash, rp, user, pub_key_cred_params); - let response = device.exec(request).unwrap(); - let credential = response.auth_data.credential.unwrap(); - - let mut request = GetAssertion::new(rp_id, client_data_hash); - request.allow_list = Some(vec![PubKeyCredDescriptor::new( - "public-key", - credential.id.clone(), - )]); - let response = device.exec(request).unwrap(); - assert_eq!(response.credential.ty, "public-key"); - assert_eq!(response.credential.id, credential.id); - assert_eq!(response.auth_data.credential, None); - credential.verify_assertion(&response.auth_data, client_data_hash, &response.signature); - }); + for mc_third_party_payment in [Some(false), Some(true), None] { + for ga_third_party_payment in [Some(false), Some(true), None] { + TestGetAssertion { + mc_third_party_payment, + ga_third_party_payment, + } + .run() + } + } } #[derive(Debug)] struct TestListCredentials { pin_token_rp_id: bool, + third_party_payment: Option, } impl TestListCredentials { @@ -272,6 +319,11 @@ impl TestListCredentials { request.options = Some(MakeCredentialOptions::default().rk(true)); request.pin_auth = Some(pin_auth); request.pin_protocol = Some(2); + if let Some(third_party_payment) = self.third_party_payment { + request.extensions = Some(ExtensionsInput { + third_party_payment: Some(third_party_payment), + }); + } let reply = device.exec(request).unwrap(); assert_eq!( reply.auth_data.flags & 0b1, @@ -327,6 +379,10 @@ impl TestListCredentials { let user: BTreeMap = reply.user.unwrap().deserialized().unwrap(); assert_eq!(reply.total_credentials, Some(1)); assert_eq!(user.get("id").unwrap(), &Value::from(user_id.as_slice())); + assert_eq!( + reply.third_party_payment, + Some(self.third_party_payment.unwrap_or_default()) + ); }); } } @@ -334,11 +390,16 @@ impl TestListCredentials { #[test] fn test_list_credentials() { for pin_token_rp_id in [false, true] { - let test = TestListCredentials { pin_token_rp_id }; - println!("{}", "=".repeat(80)); - println!("Running test:"); - println!("{test:#?}"); - println!(); - test.run(); + for third_party_payment in [Some(false), Some(true), None] { + let test = TestListCredentials { + pin_token_rp_id, + third_party_payment, + }; + println!("{}", "=".repeat(80)); + println!("Running test:"); + println!("{test:#?}"); + println!(); + test.run(); + } } } diff --git a/tests/webauthn/mod.rs b/tests/webauthn/mod.rs index 61ed577..b642195 100644 --- a/tests/webauthn/mod.rs +++ b/tests/webauthn/mod.rs @@ -301,6 +301,7 @@ pub struct MakeCredential { rp: Rp, user: User, pub_key_cred_params: Vec, + pub extensions: Option, pub options: Option, pub pin_auth: Option<[u8; 32]>, pub pin_protocol: Option, @@ -318,6 +319,7 @@ impl MakeCredential { rp, user, pub_key_cred_params: pub_key_cred_params.into(), + extensions: None, options: None, pin_auth: None, pin_protocol: None, @@ -339,6 +341,9 @@ impl From for Value { .map(Value::from) .collect::>(), ); + if let Some(extensions) = request.extensions { + map.push(6, extensions); + } if let Some(options) = request.options { map.push(7, options); } @@ -352,6 +357,21 @@ impl From for Value { } } +#[derive(Default)] +pub struct ExtensionsInput { + pub third_party_payment: Option, +} + +impl From for Value { + fn from(extensions: ExtensionsInput) -> Value { + let mut map = Map::default(); + if let Some(third_party_payment) = extensions.third_party_payment { + map.push("thirdPartyPayment", third_party_payment); + } + map.into() + } +} + #[derive(Default)] pub struct MakeCredentialOptions { rk: Option, @@ -476,6 +496,7 @@ pub struct GetAssertion { rp_id: String, client_data_hash: Vec, pub allow_list: Option>, + pub extensions: Option, } impl GetAssertion { @@ -484,6 +505,7 @@ impl GetAssertion { rp_id: rp_id.into(), client_data_hash: client_data_hash.into(), allow_list: None, + extensions: None, } } } @@ -497,6 +519,9 @@ impl From for Value { let values: Vec<_> = allow_list.into_iter().map(Value::from).collect(); map.push(0x03, values); } + if let Some(extensions) = request.extensions { + map.push(0x04, extensions); + } map.into() } } @@ -708,6 +733,7 @@ pub struct CredentialManagementReply { pub total_rps: Option, pub user: Option, pub total_credentials: Option, + pub third_party_payment: Option, } impl From for CredentialManagementReply { @@ -719,6 +745,7 @@ impl From for CredentialManagementReply { total_rps: map.remove(&5).map(|value| value.deserialized().unwrap()), user: map.remove(&6), total_credentials: map.remove(&9).map(|value| value.deserialized().unwrap()), + third_party_payment: map.remove(&0x0c).map(|value| value.deserialized().unwrap()), } } }