From fab5f59ef953b121eeb22182cb0bbe0d09596862 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Fri, 26 Apr 2024 13:56:03 +0200 Subject: [PATCH 01/25] [PM-7720] feat: use new `ClientData` trait instead of optional `client_data_hash` --- README.md | 2 +- passkey-client/src/lib.rs | 67 +++++++++++++++++++++++++++++---- passkey-client/src/tests/mod.rs | 10 ++--- passkey/examples/usage.rs | 7 +++- passkey/src/lib.rs | 4 +- 5 files changed, 72 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 812e2c0..55a9e08 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ let request = CredentialCreationOptions { }; // Now create the credential. -let my_webauthn_credential: CreatedPublicKeyCredential = my_client.register(origin, request).await?; +let my_webauthn_credential: CreatedPublicKeyCredential = my_client.register(origin, request, DefaultClientData).await?; ``` diff --git a/passkey-client/src/lib.rs b/passkey-client/src/lib.rs index a73c19d..e4813c9 100644 --- a/passkey-client/src/lib.rs +++ b/passkey-client/src/lib.rs @@ -28,6 +28,7 @@ use passkey_types::{ }, Passkey, }; +use serde::Serialize; use typeshare::typeshare; use url::Url; @@ -78,6 +79,54 @@ impl From for WebauthnError { } } +/// A trait describing how client data should be generated during a WebAuthn operation. +pub trait ClientData { + /// Extra client data to be appended to the automatically generated client data. + fn extra_client_data(&self) -> Option; + + /// The hash of the client data to be used in the WebAuthn operation. + fn client_data_hash(&self) -> Option>; +} + +/// The client data and its hash will be automatically generated from the request +/// according to the WebAuthn specification. +pub struct DefaultClientData; +impl ClientData<()> for DefaultClientData { + fn extra_client_data(&self) -> Option<()> { + None + } + fn client_data_hash(&self) -> Option> { + None + } +} + +/// The extra client data will be appended to the automatically generated client data. +/// The hash will be automatically generated from the result client data according to the WebAuthn specification. +pub struct DefaultClientDataWithExtra(E); + +/// The client data will be automatically generated from the request according to the WebAuthn specification +/// but it will not be used as a base for the hash. The client data hash will instead be provided by the caller. +pub struct DefaultClientDataWithCustomHash(Vec); +impl ClientData<()> for DefaultClientDataWithCustomHash { + fn extra_client_data(&self) -> Option<()> { + None + } + fn client_data_hash(&self) -> Option> { + Some(self.0.clone()) + } +} + +/// Backwards compatibility with the previous `register` and `authenticate` functions +/// which only took `Option>` as a client data hash. +impl ClientData<()> for Option> { + fn extra_client_data(&self) -> Option<()> { + None + } + fn client_data_hash(&self) -> Option> { + self.clone() + } +} + /// Returns a decoded [String] if the domain name is punycode otherwise /// the original string reference [str] is returned. fn decode_host(host: &str) -> Option> { @@ -163,11 +212,11 @@ where /// Register a webauthn `request` from the given `origin`. /// /// Returns either a [`webauthn::CreatedPublicKeyCredential`] on success or some [`WebauthnError`] - pub async fn register( + pub async fn register, E: Serialize>( &mut self, origin: &Url, request: webauthn::CredentialCreationOptions, - client_data_hash: Option>, + client_data: D, ) -> Result { // extract inner value of request as there is nothing else of value directly in CredentialCreationOptions let request = request.public_key; @@ -199,8 +248,9 @@ where // SAFETY: it is a developer error if serializing this struct fails. let client_data_json = serde_json::to_string(&collected_client_data).unwrap(); - let client_data_json_hash = - client_data_hash.unwrap_or_else(|| sha256(client_data_json.as_bytes()).to_vec()); + let client_data_json_hash = client_data + .client_data_hash() + .unwrap_or_else(|| sha256(client_data_json.as_bytes()).to_vec()); let cred_props = if let Some(true) = request.extensions.as_ref().and_then(|ext| ext.cred_props) { @@ -293,11 +343,11 @@ where /// Authenticate a Webauthn request. /// /// Returns either an [`webauthn::AuthenticatedPublicKeyCredential`] on success or some [`WebauthnError`]. - pub async fn authenticate( + pub async fn authenticate, E: Serialize>( &mut self, origin: &Url, request: webauthn::CredentialRequestOptions, - client_data_hash: Option>, + client_data: D, ) -> Result { // extract inner value of request as there is nothing else of value directly in CredentialRequestOptions let request = request.public_key; @@ -323,8 +373,9 @@ where // SAFETY: it is a developer error if serializing this struct fails. let client_data_json = serde_json::to_string(&collected_client_data).unwrap(); - let client_data_json_hash = - client_data_hash.unwrap_or_else(|| sha256(client_data_json.as_bytes()).to_vec()); + let client_data_json_hash = client_data + .client_data_hash() + .unwrap_or_else(|| sha256(client_data_json.as_bytes()).to_vec()); let ctap2_response = self .authenticator diff --git a/passkey-client/src/tests/mod.rs b/passkey-client/src/tests/mod.rs index d27ac8a..7fbfb66 100644 --- a/passkey-client/src/tests/mod.rs +++ b/passkey-client/src/tests/mod.rs @@ -120,7 +120,7 @@ async fn create_and_authenticate_with_origin_subdomain() { public_key: good_credential_creation_options(), }; let cred = client - .register(&origin, options, None) + .register(&origin, options, DefaultClientData) .await .expect("failed to register with options"); @@ -164,7 +164,7 @@ async fn create_and_authenticate_without_rp_id() { }, }; let cred = client - .register(&origin, options, None) + .register(&origin, options, DefaultClientData) .await .expect("failed to register with options"); @@ -208,7 +208,7 @@ async fn create_and_authenticate_without_cred_params() { }, }; let cred = client - .register(&origin, options, None) + .register(&origin, options, DefaultClientData) .await .expect("failed to register with options"); @@ -355,7 +355,7 @@ async fn client_register_triggers_uv_when_uv_is_required() { // Act & Assert client - .register(&origin, options, None) + .register(&origin, options, DefaultClientData) .await .expect("failed to register with options"); } @@ -382,7 +382,7 @@ async fn client_register_does_not_trigger_uv_when_uv_is_discouraged() { // Act & Assert client - .register(&origin, options, None) + .register(&origin, options, DefaultClientData) .await .expect("failed to register with options"); } diff --git a/passkey/examples/usage.rs b/passkey/examples/usage.rs index a3b85b3..0814d06 100644 --- a/passkey/examples/usage.rs +++ b/passkey/examples/usage.rs @@ -6,6 +6,7 @@ use passkey::{ }; use coset::iana; +use passkey_client::DefaultClientData; use url::Url; // MyUserValidationMethod is a stub impl of the UserValidationMethod trait, used later. @@ -76,7 +77,9 @@ async fn client_setup( }; // Now create the credential. - let my_webauthn_credential = my_client.register(origin, request, None).await?; + let my_webauthn_credential = my_client + .register(origin, request, DefaultClientData) + .await?; // Let's try and authenticate. // Create a challenge that would usually come from the RP. @@ -97,7 +100,7 @@ async fn client_setup( }; let authenticated_cred = my_client - .authenticate(origin, credential_request, None) + .authenticate(origin, credential_request, DefaultClientData) .await?; Ok((my_webauthn_credential, authenticated_cred)) diff --git a/passkey/src/lib.rs b/passkey/src/lib.rs index ff5daf6..54fff47 100644 --- a/passkey/src/lib.rs +++ b/passkey/src/lib.rs @@ -65,7 +65,7 @@ //! ``` //! use passkey::{ //! authenticator::{Authenticator, UserValidationMethod, UserCheck}, -//! client::{Client, WebauthnError}, +//! client::{Client, DefaultClientData, WebauthnError}, //! types::{ctap2::*, rand::random_vec, crypto::sha256, webauthn::*, Bytes, Passkey}, //! }; //! @@ -143,7 +143,7 @@ //! }; //! //! // Now create the credential. -//! let my_webauthn_credential = my_client.register(&origin, request, None).await.unwrap(); +//! let my_webauthn_credential = my_client.register(&origin, request, DefaultClientData).await.unwrap(); //! //! // Let's try and authenticate. //! // Create a challenge that would usually come from the RP. From cff1d0a44aa2fa2dacfb884630af2789e59bf59b Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Mon, 29 Apr 2024 09:38:54 +0200 Subject: [PATCH 02/25] [PM-7720] feat: implement known generic `extra_data` in `client_data` --- passkey-client/src/client_data.rs | 57 ++++++++++++++++++ passkey-client/src/lib.rs | 59 +++--------------- passkey-client/src/tests/mod.rs | 54 ++++++++++++++++- passkey-types/src/utils/encoding.rs | 2 +- passkey-types/src/webauthn/attestation.rs | 73 ++++++++++++++++++++++- 5 files changed, 191 insertions(+), 54 deletions(-) create mode 100644 passkey-client/src/client_data.rs diff --git a/passkey-client/src/client_data.rs b/passkey-client/src/client_data.rs new file mode 100644 index 0000000..073ede1 --- /dev/null +++ b/passkey-client/src/client_data.rs @@ -0,0 +1,57 @@ +use serde::Serialize; + +/// A trait describing how client data should be generated during a WebAuthn operation. +pub trait ClientData { + /// Extra client data to be appended to the automatically generated client data. + fn extra_client_data(&self) -> E; + + /// The hash of the client data to be used in the WebAuthn operation. + fn client_data_hash(&self) -> Option>; +} + +/// The client data and its hash will be automatically generated from the request +/// according to the WebAuthn specification. +pub struct DefaultClientData; +impl ClientData<()> for DefaultClientData { + fn extra_client_data(&self) -> () { + () + } + fn client_data_hash(&self) -> Option> { + None + } +} + +/// The extra client data will be appended to the automatically generated client data. +/// The hash will be automatically generated from the result client data according to the WebAuthn specification. +pub struct DefaultClientDataWithExtra(pub E); +impl ClientData for DefaultClientDataWithExtra { + fn extra_client_data(&self) -> E { + self.0.clone() + } + fn client_data_hash(&self) -> Option> { + None + } +} + +/// The client data will be automatically generated from the request according to the WebAuthn specification +/// but it will not be used as a base for the hash. The client data hash will instead be provided by the caller. +pub struct DefaultClientDataWithCustomHash(pub Vec); +impl ClientData<()> for DefaultClientDataWithCustomHash { + fn extra_client_data(&self) -> () { + () + } + fn client_data_hash(&self) -> Option> { + Some(self.0.clone()) + } +} + +/// Backwards compatibility with the previous `register` and `authenticate` functions +/// which only took `Option>` as a client data hash. +impl ClientData<()> for Option> { + fn extra_client_data(&self) -> () { + () + } + fn client_data_hash(&self) -> Option> { + self.clone() + } +} diff --git a/passkey-client/src/lib.rs b/passkey-client/src/lib.rs index e4813c9..385e84e 100644 --- a/passkey-client/src/lib.rs +++ b/passkey-client/src/lib.rs @@ -14,6 +14,9 @@ //! [version]: https://img.shields.io/crates/v/passkey-client?logo=rust&style=flat //! [documentation]: https://img.shields.io/docsrs/passkey-client/latest?logo=docs.rs&style=flat //! [Webauthn]: https://w3c.github.io/webauthn/ +mod client_data; +pub use client_data::*; + use std::borrow::Cow; use ciborium::{cbor, value::Value}; @@ -79,54 +82,6 @@ impl From for WebauthnError { } } -/// A trait describing how client data should be generated during a WebAuthn operation. -pub trait ClientData { - /// Extra client data to be appended to the automatically generated client data. - fn extra_client_data(&self) -> Option; - - /// The hash of the client data to be used in the WebAuthn operation. - fn client_data_hash(&self) -> Option>; -} - -/// The client data and its hash will be automatically generated from the request -/// according to the WebAuthn specification. -pub struct DefaultClientData; -impl ClientData<()> for DefaultClientData { - fn extra_client_data(&self) -> Option<()> { - None - } - fn client_data_hash(&self) -> Option> { - None - } -} - -/// The extra client data will be appended to the automatically generated client data. -/// The hash will be automatically generated from the result client data according to the WebAuthn specification. -pub struct DefaultClientDataWithExtra(E); - -/// The client data will be automatically generated from the request according to the WebAuthn specification -/// but it will not be used as a base for the hash. The client data hash will instead be provided by the caller. -pub struct DefaultClientDataWithCustomHash(Vec); -impl ClientData<()> for DefaultClientDataWithCustomHash { - fn extra_client_data(&self) -> Option<()> { - None - } - fn client_data_hash(&self) -> Option> { - Some(self.0.clone()) - } -} - -/// Backwards compatibility with the previous `register` and `authenticate` functions -/// which only took `Option>` as a client data hash. -impl ClientData<()> for Option> { - fn extra_client_data(&self) -> Option<()> { - None - } - fn client_data_hash(&self) -> Option> { - self.clone() - } -} - /// Returns a decoded [String] if the domain name is punycode otherwise /// the original string reference [str] is returned. fn decode_host(host: &str) -> Option> { @@ -212,7 +167,7 @@ where /// Register a webauthn `request` from the given `origin`. /// /// Returns either a [`webauthn::CreatedPublicKeyCredential`] on success or some [`WebauthnError`] - pub async fn register, E: Serialize>( + pub async fn register, E: Serialize + Clone>( &mut self, origin: &Url, request: webauthn::CredentialCreationOptions, @@ -238,11 +193,12 @@ where .rp_id_verifier .assert_domain(origin, request.rp.id.as_deref())?; - let collected_client_data = webauthn::CollectedClientData { + let collected_client_data = webauthn::CollectedClientData:: { ty: webauthn::ClientDataType::Create, challenge: encoding::base64url(&request.challenge), origin: origin.as_str().trim_end_matches('/').to_owned(), cross_origin: None, + extra_data: client_data.extra_client_data(), unknown_keys: Default::default(), }; @@ -363,11 +319,12 @@ where .rp_id_verifier .assert_domain(origin, request.rp_id.as_deref())?; - let collected_client_data = webauthn::CollectedClientData { + let collected_client_data = webauthn::CollectedClientData::<()> { ty: webauthn::ClientDataType::Get, challenge: encoding::base64url(&request.challenge), origin: origin.as_str().trim_end_matches('/').to_owned(), cross_origin: None, //Some(false), + extra_data: (), unknown_keys: Default::default(), }; diff --git a/passkey-client/src/tests/mod.rs b/passkey-client/src/tests/mod.rs index 7fbfb66..2a0a3ac 100644 --- a/passkey-client/src/tests/mod.rs +++ b/passkey-client/src/tests/mod.rs @@ -1,7 +1,10 @@ use super::*; use coset::iana; use passkey_authenticator::{MemoryStore, MockUserValidationMethod, UserCheck}; -use passkey_types::{ctap2, rand::random_vec, Bytes}; +use passkey_types::{ + ctap2, encoding::try_from_base64url, rand::random_vec, webauthn::CollectedClientData, Bytes, +}; +use serde::Deserialize; use url::{ParseError, Url}; fn good_credential_creation_options() -> webauthn::PublicKeyCredentialCreationOptions { @@ -106,6 +109,55 @@ async fn create_and_authenticate() { .expect("failed to authenticate with freshly created credential"); } +#[tokio::test] +async fn create_and_authenticate_with_extra_client_data() { + #[derive(Clone, Serialize, Deserialize)] + struct AndroidClientData { + android_package_name: String, + } + let auth = Authenticator::new( + ctap2::Aaguid::new_empty(), + MemoryStore::new(), + uv_mock_with_creation(2), + ); + let mut client = Client::new(auth); + + let origin = Url::parse("https://future.1password.com").unwrap(); + let options = webauthn::CredentialCreationOptions { + public_key: good_credential_creation_options(), + }; + let extra_data = AndroidClientData { + android_package_name: "com.example.app".to_owned(), + }; + let cred = client + .register(&origin, options, DefaultClientDataWithExtra(extra_data)) + .await + .expect("failed to register with options"); + + let returned_base64url_client_data_json: String = cred.response.client_data_json.into(); + println!("client_data_json: {}", returned_base64url_client_data_json); + let returned_client_data_json = + try_from_base64url(returned_base64url_client_data_json.as_str()) + .expect("could not base64url decode client data"); + let returned_client_data: CollectedClientData = + serde_json::from_slice(&returned_client_data_json) + .expect("could not json deserialize client data"); + assert_eq!( + returned_client_data.extra_data.android_package_name, + "com.example.app" + ); + + let credential_id = cred.raw_id; + + let auth_options = webauthn::CredentialRequestOptions { + public_key: good_credential_request_options(credential_id), + }; + client + .authenticate(&origin, auth_options, None) + .await + .expect("failed to authenticate with freshly created credential"); +} + #[tokio::test] async fn create_and_authenticate_with_origin_subdomain() { let auth = Authenticator::new( diff --git a/passkey-types/src/utils/encoding.rs b/passkey-types/src/utils/encoding.rs index 2007853..e93d9e9 100644 --- a/passkey-types/src/utils/encoding.rs +++ b/passkey-types/src/utils/encoding.rs @@ -22,7 +22,7 @@ pub(crate) fn try_from_base64(input: &str) -> Option> { /// Try parsing from base64url with or without padding pub fn try_from_base64url(input: &str) -> Option> { - let specs = BASE64URL.specification(); + let specs: Specification = BASE64URL.specification(); let padding = specs.padding.unwrap(); let specs = Specification { check_trailing_bits: false, diff --git a/passkey-types/src/webauthn/attestation.rs b/passkey-types/src/webauthn/attestation.rs index 1946843..54d0e70 100644 --- a/passkey-types/src/webauthn/attestation.rs +++ b/passkey-types/src/webauthn/attestation.rs @@ -578,7 +578,10 @@ pub struct AuthenticatorAttestationResponse { /// [5.8.1.1 Serialization]: https://w3c.github.io/webauthn/#clientdatajson-serialization #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct CollectedClientData { +pub struct CollectedClientData +where + E: Clone + Serialize, +{ /// This member contains the value [`ClientDataType::Create`] when creating new credentials, and /// [`ClientDataType::Get`] when getting an assertion from an existing credential. The purpose /// of this member is to prevent certain types of signature confusion attacks (where an attacker @@ -603,6 +606,11 @@ pub struct CollectedClientData { #[serde(default, serialize_with = "truthiness")] pub cross_origin: Option, + /// CollectedClientData can be extended by the user of this library, this accounts for + /// keys that are unknown to the library, but may be known to the user. + #[serde(flatten)] + pub extra_data: E, + /// CollectedClientData can be extended in the future, this accounts for unknown keys /// Uses an IndexMap to preserve order of keys for JSON byte serialization #[serde(flatten)] @@ -645,6 +653,8 @@ impl fmt::Display for ClientDataType { #[cfg(test)] mod tests { + use serde::{Deserialize, Serialize}; + use super::CredentialCreationOptions; use crate::webauthn::{ClientDataType, CollectedClientData}; @@ -656,6 +666,14 @@ mod tests { "crossOrigin":false }"#; + const ANDROID_CLIENT_DATA_JSON_STRING: &str = r#"{ + "type": "webauthn.get", + "challenge": "ZEvMflZDcwQJmarInnYi88px-6HZcv2Uoxw7-_JOOTg", + "origin": "http://localhost:4000", + "crossOrigin": false, + "androidPackageName": "com.android.chrome" + }"#; + /// This is a Secure Payment Confirmation (SPC) response. SPC assertion responses /// extend the `CollectedClientData` struct by adding a "payment" field that /// normally does not exist on `CollectedClientData` @@ -795,6 +813,59 @@ mod tests { ) } + #[test] + fn test_client_data_deserialization_with_extra_data() { + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] + #[serde(rename_all = "camelCase")] + struct AndroidExtraData { + android_package_name: String, + } + + let expected_collected_client_data = CollectedClientData { + ty: ClientDataType::Get, + challenge: "ZEvMflZDcwQJmarInnYi88px-6HZcv2Uoxw7-_JOOTg".to_string(), + origin: "http://localhost:4000".to_owned(), + cross_origin: Some(false), + extra_data: AndroidExtraData { + android_package_name: "com.android.chrome".to_string(), + }, + unknown_keys: Default::default(), + }; + + // Deserialize CollectedClientData from JSON string + let actual_collected_client_data: CollectedClientData = + serde_json::from_str(ANDROID_CLIENT_DATA_JSON_STRING).unwrap(); + + // Check that serde_json byte serialization is also equivalent + assert_eq!( + actual_collected_client_data.extra_data, + expected_collected_client_data.extra_data, + ); + } + + #[test] + fn test_client_data_serialization_with_extra_data() { + #[derive(Debug, Clone, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + struct AndroidExtraData { + android_package_name: String, + } + + // This is the raw client data json byte buffer returned by an Android webauthn assertion + let expected_client_data_bytes = r#"{"type":"webauthn.get","challenge":"ZEvMflZDcwQJmarInnYi88px-6HZcv2Uoxw7-_JOOTg","origin":"http://localhost:4000","crossOrigin":false,"androidPackageName":"com.android.chrome"}"#.as_bytes(); + + // Deserialize CollectedClientData from JSON string + let actual_collected_client_data: CollectedClientData = + serde_json::from_str(ANDROID_CLIENT_DATA_JSON_STRING).unwrap(); + + // Check that serde_json byte serialization is also equivalent + let actual_client_data_bytes = serde_json::to_vec(&actual_collected_client_data).unwrap(); + assert_eq!( + actual_client_data_bytes.as_slice(), + expected_client_data_bytes + ) + } + #[test] fn test_extended_client_data_serialization() { // This is the raw client data json byte buffer returned by an SPC webauthn assertion From 86bd4c26d2d476a315b069a70eb6300518cae3bd Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Mon, 29 Apr 2024 10:05:08 +0200 Subject: [PATCH 03/25] [PM-7720] feat: add to assertions --- passkey-client/src/lib.rs | 6 +++--- passkey-client/src/tests/mod.rs | 27 +++++++++++++++++++++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/passkey-client/src/lib.rs b/passkey-client/src/lib.rs index 385e84e..39958e9 100644 --- a/passkey-client/src/lib.rs +++ b/passkey-client/src/lib.rs @@ -299,7 +299,7 @@ where /// Authenticate a Webauthn request. /// /// Returns either an [`webauthn::AuthenticatedPublicKeyCredential`] on success or some [`WebauthnError`]. - pub async fn authenticate, E: Serialize>( + pub async fn authenticate, E: Serialize + Clone>( &mut self, origin: &Url, request: webauthn::CredentialRequestOptions, @@ -319,12 +319,12 @@ where .rp_id_verifier .assert_domain(origin, request.rp_id.as_deref())?; - let collected_client_data = webauthn::CollectedClientData::<()> { + let collected_client_data = webauthn::CollectedClientData:: { ty: webauthn::ClientDataType::Get, challenge: encoding::base64url(&request.challenge), origin: origin.as_str().trim_end_matches('/').to_owned(), cross_origin: None, //Some(false), - extra_data: (), + extra_data: client_data.extra_client_data(), unknown_keys: Default::default(), }; diff --git a/passkey-client/src/tests/mod.rs b/passkey-client/src/tests/mod.rs index 2a0a3ac..791ed35 100644 --- a/passkey-client/src/tests/mod.rs +++ b/passkey-client/src/tests/mod.rs @@ -130,12 +130,15 @@ async fn create_and_authenticate_with_extra_client_data() { android_package_name: "com.example.app".to_owned(), }; let cred = client - .register(&origin, options, DefaultClientDataWithExtra(extra_data)) + .register( + &origin, + options, + DefaultClientDataWithExtra(extra_data.clone()), + ) .await .expect("failed to register with options"); let returned_base64url_client_data_json: String = cred.response.client_data_json.into(); - println!("client_data_json: {}", returned_base64url_client_data_json); let returned_client_data_json = try_from_base64url(returned_base64url_client_data_json.as_str()) .expect("could not base64url decode client data"); @@ -152,10 +155,26 @@ async fn create_and_authenticate_with_extra_client_data() { let auth_options = webauthn::CredentialRequestOptions { public_key: good_credential_request_options(credential_id), }; - client - .authenticate(&origin, auth_options, None) + let result = client + .authenticate( + &origin, + auth_options, + DefaultClientDataWithExtra(extra_data.clone()), + ) .await .expect("failed to authenticate with freshly created credential"); + + let returned_base64url_client_data_json: String = result.response.client_data_json.into(); + let returned_client_data_json = + try_from_base64url(returned_base64url_client_data_json.as_str()) + .expect("could not base64url decode client data"); + let returned_client_data: CollectedClientData = + serde_json::from_slice(&returned_client_data_json) + .expect("could not json deserialize client data"); + assert_eq!( + returned_client_data.extra_data.android_package_name, + "com.example.app" + ); } #[tokio::test] From bc2c039486ff409301d5064b6fe10aa2bcfe1881 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Mon, 29 Apr 2024 10:21:15 +0200 Subject: [PATCH 04/25] [PM-7720] chore: refactor all code to use `DefaultClientData` instead of backwards compatible `Option` implementation --- passkey-client/src/tests/mod.rs | 10 +++++----- passkey/src/lib.rs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/passkey-client/src/tests/mod.rs b/passkey-client/src/tests/mod.rs index 791ed35..371f0ff 100644 --- a/passkey-client/src/tests/mod.rs +++ b/passkey-client/src/tests/mod.rs @@ -94,7 +94,7 @@ async fn create_and_authenticate() { public_key: good_credential_creation_options(), }; let cred = client - .register(&origin, options, None) + .register(&origin, options, DefaultClientData) .await .expect("failed to register with options"); @@ -104,7 +104,7 @@ async fn create_and_authenticate() { public_key: good_credential_request_options(credential_id), }; client - .authenticate(&origin, auth_options, None) + .authenticate(&origin, auth_options, DefaultClientData) .await .expect("failed to authenticate with freshly created credential"); } @@ -207,7 +207,7 @@ async fn create_and_authenticate_with_origin_subdomain() { public_key: good_credential_request_options(cred.raw_id), }; let res = client - .authenticate(&origin, auth_options, None) + .authenticate(&origin, auth_options, DefaultClientData) .await .expect("failed to authenticate with freshly created credential"); let att_obj = ctap2::AuthenticatorData::from_slice(&res.response.authenticator_data) @@ -254,7 +254,7 @@ async fn create_and_authenticate_without_rp_id() { }, }; let res = client - .authenticate(&origin, auth_options, None) + .authenticate(&origin, auth_options, DefaultClientData) .await .expect("failed to authenticate with freshly created credential"); let att_obj = ctap2::AuthenticatorData::from_slice(&res.response.authenticator_data) @@ -289,7 +289,7 @@ async fn create_and_authenticate_without_cred_params() { public_key: good_credential_request_options(credential_id), }; client - .authenticate(&origin, auth_options, None) + .authenticate(&origin, auth_options, DefaultClientData) .await .expect("failed to authenticate with freshly created credential"); } diff --git a/passkey/src/lib.rs b/passkey/src/lib.rs index 54fff47..1e95c9b 100644 --- a/passkey/src/lib.rs +++ b/passkey/src/lib.rs @@ -164,7 +164,7 @@ //! }; //! //! let authenticated_cred = my_client -//! .authenticate(&origin, credential_request, None) +//! .authenticate(&origin, credential_request, DefaultClientData) //! .await //! .unwrap(); //! # }) From 06e4696fd3d33d277cff046acbbcfea60821b697 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Mon, 29 Apr 2024 10:45:34 +0200 Subject: [PATCH 05/25] [PM-7720] fix: double generic trait --- passkey-types/src/webauthn/attestation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/passkey-types/src/webauthn/attestation.rs b/passkey-types/src/webauthn/attestation.rs index 54d0e70..e58afdb 100644 --- a/passkey-types/src/webauthn/attestation.rs +++ b/passkey-types/src/webauthn/attestation.rs @@ -578,7 +578,7 @@ pub struct AuthenticatorAttestationResponse { /// [5.8.1.1 Serialization]: https://w3c.github.io/webauthn/#clientdatajson-serialization #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct CollectedClientData +pub struct CollectedClientData where E: Clone + Serialize, { From 7f2adc51eca086aefde1f3615b366cce95d22590 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Mon, 29 Apr 2024 10:46:01 +0200 Subject: [PATCH 06/25] [PM-7720] fix: remove unintentional change --- passkey-types/src/utils/encoding.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/passkey-types/src/utils/encoding.rs b/passkey-types/src/utils/encoding.rs index e93d9e9..2007853 100644 --- a/passkey-types/src/utils/encoding.rs +++ b/passkey-types/src/utils/encoding.rs @@ -22,7 +22,7 @@ pub(crate) fn try_from_base64(input: &str) -> Option> { /// Try parsing from base64url with or without padding pub fn try_from_base64url(input: &str) -> Option> { - let specs: Specification = BASE64URL.specification(); + let specs = BASE64URL.specification(); let padding = specs.padding.unwrap(); let specs = Specification { check_trailing_bits: false, From f1ca241370cabf52acee735d6f4eb35156f3db52 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Mon, 29 Apr 2024 10:46:18 +0200 Subject: [PATCH 07/25] [PM-7720] chore: update version --- CHANGELOG.md | 17 +++++++++++++++++ passkey-client/Cargo.toml | 2 +- passkey-types/Cargo.toml | 2 +- passkey/Cargo.toml | 2 +- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index def5ef1..400c1fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## Paskey v0.3.1 +### passkey-client v0.3.1 + +The client now supports additional user-defined properties in the client data, while also clarifying how the client +handles client data and its hash. + +- Change `register` and `authenticate` to take a `ClientData` instead of `Option>`. +- Custom client data hashes are now specified using `DefaultClientDataWithCustomHash(Vec)` instead of + `Some(Vec)`. +- Additional fields can be aedded to the client data using `DefaultClientDataWithExtra(ExtraData)`. + +### passkey-client v0.2.1 + +`CollectedClientData` is now generic and supports additional strongly typed fields. + +- `CollectedClientData` has changed to `CollectedClientData` + ## Passkey v0.3.0 ### passkey-authenticator v0.3.0 diff --git a/passkey-client/Cargo.toml b/passkey-client/Cargo.toml index f9289ee..cce2c40 100644 --- a/passkey-client/Cargo.toml +++ b/passkey-client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "passkey-client" -version = "0.3.0" +version = "0.3.1" description = "Webauthn client in Rust." include = ["src/", "../LICENSE-APACHE", "../LICENSE-MIT"] readme = "README.md" diff --git a/passkey-types/Cargo.toml b/passkey-types/Cargo.toml index 61cc9d9..dce682c 100644 --- a/passkey-types/Cargo.toml +++ b/passkey-types/Cargo.toml @@ -3,7 +3,7 @@ name = "passkey-types" description = "Rust type definitions for the webauthn and CTAP specifications" include = ["src/", "../LICENSE-APACHE", "../LICENSE-MIT"] readme = "README.md" -version = "0.2.0" +version = "0.2.1" authors.workspace = true repository.workspace = true edition.workspace = true diff --git a/passkey/Cargo.toml b/passkey/Cargo.toml index f15040b..5fb9c6c 100644 --- a/passkey/Cargo.toml +++ b/passkey/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "passkey" -version = "0.3.0" +version = "0.3.1" description = "A one stop library to implement a passkey client and authenticator" include = ["src/", "../LICENSE-APACHE", "../LICENSE-MIT"] readme = "../README.md" From 3cb11a6f5214fef579cf78ed06a0f4d98bc9b95e Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Mon, 29 Apr 2024 10:58:27 +0200 Subject: [PATCH 08/25] [PM-7720] lint: remove unit types --- passkey-client/src/client_data.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/passkey-client/src/client_data.rs b/passkey-client/src/client_data.rs index 073ede1..b385b48 100644 --- a/passkey-client/src/client_data.rs +++ b/passkey-client/src/client_data.rs @@ -13,9 +13,8 @@ pub trait ClientData { /// according to the WebAuthn specification. pub struct DefaultClientData; impl ClientData<()> for DefaultClientData { - fn extra_client_data(&self) -> () { - () - } + fn extra_client_data(&self) {} + fn client_data_hash(&self) -> Option> { None } @@ -37,9 +36,8 @@ impl ClientData for DefaultClientDataWithExtra { /// but it will not be used as a base for the hash. The client data hash will instead be provided by the caller. pub struct DefaultClientDataWithCustomHash(pub Vec); impl ClientData<()> for DefaultClientDataWithCustomHash { - fn extra_client_data(&self) -> () { - () - } + fn extra_client_data(&self) {} + fn client_data_hash(&self) -> Option> { Some(self.0.clone()) } @@ -48,9 +46,8 @@ impl ClientData<()> for DefaultClientDataWithCustomHash { /// Backwards compatibility with the previous `register` and `authenticate` functions /// which only took `Option>` as a client data hash. impl ClientData<()> for Option> { - fn extra_client_data(&self) -> () { - () - } + fn extra_client_data(&self) {} + fn client_data_hash(&self) -> Option> { self.clone() } From 50e8b87fef2f93ec66af95cb77dc46ce9b3b4ff1 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Mon, 29 Apr 2024 13:33:34 +0200 Subject: [PATCH 09/25] [PM-7720] feat: test unknown data with extra_data --- passkey-types/src/webauthn/attestation.rs | 37 +++++++++++++++-------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/passkey-types/src/webauthn/attestation.rs b/passkey-types/src/webauthn/attestation.rs index e58afdb..f34b8c2 100644 --- a/passkey-types/src/webauthn/attestation.rs +++ b/passkey-types/src/webauthn/attestation.rs @@ -666,12 +666,13 @@ mod tests { "crossOrigin":false }"#; - const ANDROID_CLIENT_DATA_JSON_STRING: &str = r#"{ - "type": "webauthn.get", - "challenge": "ZEvMflZDcwQJmarInnYi88px-6HZcv2Uoxw7-_JOOTg", - "origin": "http://localhost:4000", - "crossOrigin": false, - "androidPackageName": "com.android.chrome" + const EXTENDED_ANDROID_CLIENT_DATA_JSON_STRING: &str = r#"{ + "type": "webauthn.get", + "challenge": "ZEvMflZDcwQJmarInnYi88px-6HZcv2Uoxw7-_JOOTg", + "origin": "http://localhost:4000", + "crossOrigin": false, + "androidPackageName": "com.android.chrome", + "other_keys_can_be_added_here": "do not compare clientDataJSON against a template. See https://goo.gl/yabPex" }"#; /// This is a Secure Payment Confirmation (SPC) response. SPC assertion responses @@ -814,7 +815,7 @@ mod tests { } #[test] - fn test_client_data_deserialization_with_extra_data() { + fn test_client_data_deserialization_with_extra_and_unknown_data() { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] struct AndroidExtraData { @@ -829,22 +830,34 @@ mod tests { extra_data: AndroidExtraData { android_package_name: "com.android.chrome".to_string(), }, - unknown_keys: Default::default(), + unknown_keys: [( + "other_keys_can_be_added_here".to_string(), + serde_json::json!( + "do not compare clientDataJSON against a template. See https://goo.gl/yabPex" + ), + )] + .iter() + .cloned() + .collect(), }; // Deserialize CollectedClientData from JSON string let actual_collected_client_data: CollectedClientData = - serde_json::from_str(ANDROID_CLIENT_DATA_JSON_STRING).unwrap(); + serde_json::from_str(EXTENDED_ANDROID_CLIENT_DATA_JSON_STRING).unwrap(); // Check that serde_json byte serialization is also equivalent assert_eq!( actual_collected_client_data.extra_data, expected_collected_client_data.extra_data, ); + assert_eq!( + actual_collected_client_data.unknown_keys, + expected_collected_client_data.unknown_keys, + ); } #[test] - fn test_client_data_serialization_with_extra_data() { + fn test_client_data_serialization_with_extra_and_unknown_data() { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct AndroidExtraData { @@ -852,11 +865,11 @@ mod tests { } // This is the raw client data json byte buffer returned by an Android webauthn assertion - let expected_client_data_bytes = r#"{"type":"webauthn.get","challenge":"ZEvMflZDcwQJmarInnYi88px-6HZcv2Uoxw7-_JOOTg","origin":"http://localhost:4000","crossOrigin":false,"androidPackageName":"com.android.chrome"}"#.as_bytes(); + let expected_client_data_bytes = r#"{"type":"webauthn.get","challenge":"ZEvMflZDcwQJmarInnYi88px-6HZcv2Uoxw7-_JOOTg","origin":"http://localhost:4000","crossOrigin":false,"androidPackageName":"com.android.chrome","other_keys_can_be_added_here":"do not compare clientDataJSON against a template. See https://goo.gl/yabPex"}"#.as_bytes(); // Deserialize CollectedClientData from JSON string let actual_collected_client_data: CollectedClientData = - serde_json::from_str(ANDROID_CLIENT_DATA_JSON_STRING).unwrap(); + serde_json::from_str(EXTENDED_ANDROID_CLIENT_DATA_JSON_STRING).unwrap(); // Check that serde_json byte serialization is also equivalent let actual_client_data_bytes = serde_json::to_vec(&actual_collected_client_data).unwrap(); From 240f889e88d44e96708d26b4a5b90f11607ca416 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Mon, 29 Apr 2024 13:36:03 +0200 Subject: [PATCH 10/25] [PM-7720] fix: typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 400c1fa..3581903 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ handles client data and its hash. - Change `register` and `authenticate` to take a `ClientData` instead of `Option>`. - Custom client data hashes are now specified using `DefaultClientDataWithCustomHash(Vec)` instead of `Some(Vec)`. -- Additional fields can be aedded to the client data using `DefaultClientDataWithExtra(ExtraData)`. +- Additional fields can be added to the client data using `DefaultClientDataWithExtra(ExtraData)`. ### passkey-client v0.2.1 From 2df7838b8832322d309c399a4c0f6ca2979af12d Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Thu, 2 May 2024 14:57:35 +0200 Subject: [PATCH 11/25] [PM-7148] feat: send options to store when saving cred --- .../src/authenticator/make_credential.rs | 2 +- passkey-authenticator/src/credential_store.rs | 32 +++++++++++++++---- passkey-authenticator/src/u2f.rs | 13 ++++++-- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/passkey-authenticator/src/authenticator/make_credential.rs b/passkey-authenticator/src/authenticator/make_credential.rs index 7c6eb44..a787fc0 100644 --- a/passkey-authenticator/src/authenticator/make_credential.rs +++ b/passkey-authenticator/src/authenticator/make_credential.rs @@ -144,7 +144,7 @@ where // 10 self.store_mut() - .save_credential(passkey, input.user.into(), input.rp) + .save_credential(passkey, input.user.into(), input.rp, input.options) .await?; Ok(response) diff --git a/passkey-authenticator/src/credential_store.rs b/passkey-authenticator/src/credential_store.rs index 02fbbe7..5f67538 100644 --- a/passkey-authenticator/src/credential_store.rs +++ b/passkey-authenticator/src/credential_store.rs @@ -3,8 +3,9 @@ use std::sync::Arc; use passkey_types::{ ctap2::{ - make_credential::PublicKeyCredentialRpEntity, - make_credential::PublicKeyCredentialUserEntity, Ctap2Error, StatusCode, + get_assertion::Options, + make_credential::{PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity}, + Ctap2Error, StatusCode, }, webauthn::PublicKeyCredentialDescriptor, Passkey, @@ -32,6 +33,7 @@ pub trait CredentialStore { cred: Passkey, user: PublicKeyCredentialUserEntity, rp: PublicKeyCredentialRpEntity, + options: Options, ) -> Result<(), StatusCode>; /// Update the credential in your store @@ -70,6 +72,7 @@ impl CredentialStore for MemoryStore { cred: Passkey, _user: PublicKeyCredentialUserEntity, _rp: PublicKeyCredentialRpEntity, + _options: Options, ) -> Result<(), StatusCode> { self.insert(cred.credential_id.clone().into(), cred); Ok(()) @@ -107,6 +110,7 @@ impl CredentialStore for Option { cred: Passkey, _user: PublicKeyCredentialUserEntity, _rp: PublicKeyCredentialRpEntity, + _options: Options, ) -> Result<(), StatusCode> { self.replace(cred); Ok(()) @@ -138,8 +142,12 @@ impl + Send + Sync> CredentialStore cred: Passkey, user: PublicKeyCredentialUserEntity, rp: PublicKeyCredentialRpEntity, + options: Options, ) -> Result<(), StatusCode> { - self.lock().await.save_credential(cred, user, rp).await + self.lock() + .await + .save_credential(cred, user, rp, options) + .await } async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> { @@ -167,8 +175,12 @@ impl + Send + Sync> CredentialStore cred: Passkey, user: PublicKeyCredentialUserEntity, rp: PublicKeyCredentialRpEntity, + options: Options, ) -> Result<(), StatusCode> { - self.write().await.save_credential(cred, user, rp).await + self.write() + .await + .save_credential(cred, user, rp, options) + .await } async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> { @@ -196,8 +208,12 @@ impl + Send + Sync> CredentialStore cred: Passkey, user: PublicKeyCredentialUserEntity, rp: PublicKeyCredentialRpEntity, + options: Options, ) -> Result<(), StatusCode> { - self.lock().await.save_credential(cred, user, rp).await + self.lock() + .await + .save_credential(cred, user, rp, options) + .await } async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> { @@ -225,8 +241,12 @@ impl + Send + Sync> CredentialStore cred: Passkey, user: PublicKeyCredentialUserEntity, rp: PublicKeyCredentialRpEntity, + options: Options, ) -> Result<(), StatusCode> { - self.write().await.save_credential(cred, user, rp).await + self.write() + .await + .save_credential(cred, user, rp, options) + .await } async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> { diff --git a/passkey-authenticator/src/u2f.rs b/passkey-authenticator/src/u2f.rs index f558457..1d70808 100644 --- a/passkey-authenticator/src/u2f.rs +++ b/passkey-authenticator/src/u2f.rs @@ -7,7 +7,7 @@ use p256::{ SecretKey, }; use passkey_types::{ - ctap2::{Flags, U2FError}, + ctap2::{get_assertion::Options, Flags, U2FError}, u2f::{ AuthenticationRequest, AuthenticationResponse, PublicKey, RegisterRequest, RegisterResponse, }, @@ -94,7 +94,16 @@ impl U2 &request, &response, handle, &private, ); - let result = self.store_mut().save_credential(passkey, user, rp).await; + // U2F registration does not use rk, uv, or up + let options = Options { + rk: false, + uv: false, + up: false, + }; + let result = self + .store_mut() + .save_credential(passkey, user, rp, options) + .await; match result { Ok(_) => Ok(response), From 9df89a26731c6a46f45b552a9ed335f16e0915c1 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Thu, 2 May 2024 15:22:52 +0200 Subject: [PATCH 12/25] [PM-7149] feat: add discoverability info to store --- passkey-authenticator/src/credential_store.rs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/passkey-authenticator/src/credential_store.rs b/passkey-authenticator/src/credential_store.rs index 5f67538..d0e3efe 100644 --- a/passkey-authenticator/src/credential_store.rs +++ b/passkey-authenticator/src/credential_store.rs @@ -11,6 +11,27 @@ use passkey_types::{ Passkey, }; +/// A struct that defines the capabilities of a store. +pub struct StoreInfo { + /// How the store handles discoverability. + pub discoverability: DiscoverabilitySupport, +} + +/// Enum to define how the store handles discoverability. +/// Note that this is does not say anything about which storage mode will be used. +pub enum DiscoverabilitySupport { + /// The store supports both discoverable and non-credentials. + Full, + + /// The store only supports non-discoverable credentials. + /// An error will be returned if a discoverable credential is requested. + OnlyNonDiscoverable, + + /// The store only supports discoverable credential. + /// No error will be returned if a non-discoverable credential is requested. + ForcedDiscoverable, +} + /// Use this on a type that enables storage and fetching of credentials #[async_trait::async_trait] pub trait CredentialStore { @@ -38,6 +59,9 @@ pub trait CredentialStore { /// Update the credential in your store async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode>; + + /// Get information about the store + async fn get_info(&self) -> StoreInfo; } /// In-memory store for Passkeys @@ -82,6 +106,12 @@ impl CredentialStore for MemoryStore { self.insert(cred.credential_id.clone().into(), cred); Ok(()) } + + async fn get_info(&self) -> StoreInfo { + StoreInfo { + discoverability: DiscoverabilitySupport::ForcedDiscoverable, + } + } } #[async_trait::async_trait] @@ -120,6 +150,12 @@ impl CredentialStore for Option { self.replace(cred); Ok(()) } + + async fn get_info(&self) -> StoreInfo { + StoreInfo { + discoverability: DiscoverabilitySupport::ForcedDiscoverable, + } + } } #[cfg(any(feature = "tokio", test))] @@ -153,6 +189,10 @@ impl + Send + Sync> CredentialStore async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> { self.lock().await.update_credential(cred).await } + + async fn get_info(&self) -> StoreInfo { + self.lock().await.get_info().await + } } #[cfg(any(feature = "tokio", test))] @@ -186,6 +226,10 @@ impl + Send + Sync> CredentialStore async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> { self.write().await.update_credential(cred).await } + + async fn get_info(&self) -> StoreInfo { + self.read().await.get_info().await + } } #[cfg(any(feature = "tokio", test))] @@ -219,6 +263,10 @@ impl + Send + Sync> CredentialStore async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> { self.lock().await.update_credential(cred).await } + + async fn get_info(&self) -> StoreInfo { + self.lock().await.get_info().await + } } #[cfg(any(feature = "tokio", test))] @@ -252,4 +300,8 @@ impl + Send + Sync> CredentialStore async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> { self.write().await.update_credential(cred).await } + + async fn get_info(&self) -> StoreInfo { + self.read().await.get_info().await + } } From 3f118279c8e9bbb863d4a465fee6f87e0520a3d1 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Thu, 2 May 2024 16:07:53 +0200 Subject: [PATCH 13/25] [PM-7149] feat: add rk support info to authenticator --- .../src/authenticator/get_info.rs | 9 ++- .../src/authenticator/make_credential.rs | 75 ++++++++++++++++++- passkey-authenticator/src/credential_store.rs | 1 + passkey-authenticator/src/ctap2.rs | 2 +- passkey-authenticator/src/user_validation.rs | 9 ++- passkey-client/src/lib.rs | 2 +- 6 files changed, 86 insertions(+), 12 deletions(-) diff --git a/passkey-authenticator/src/authenticator/get_info.rs b/passkey-authenticator/src/authenticator/get_info.rs index 1059a14..4229cb1 100644 --- a/passkey-authenticator/src/authenticator/get_info.rs +++ b/passkey-authenticator/src/authenticator/get_info.rs @@ -1,17 +1,20 @@ use passkey_types::ctap2::get_info::{Options, Response}; -use crate::{Authenticator, CredentialStore, UserValidationMethod}; +use crate::{ + credential_store::DiscoverabilitySupport, Authenticator, CredentialStore, UserValidationMethod, +}; impl Authenticator { /// Using this method, the host can request that the authenticator report a list of all /// supported protocol versions, supported extensions, AAGUID of the device, and its capabilities. - pub fn get_info(&self) -> Response { + pub async fn get_info(&self) -> Response { Response { versions: vec!["FIDO_2_0".into(), "U2F_V2".into()], extensions: None, aaguid: *self.aaguid(), options: Some(Options { - rk: true, + rk: self.store.get_info().await.discoverability + != DiscoverabilitySupport::OnlyNonDiscoverable, uv: self.user_validation.is_verification_enabled(), up: self.user_validation.is_presence_enabled(), ..Default::default() diff --git a/passkey-authenticator/src/authenticator/make_credential.rs b/passkey-authenticator/src/authenticator/make_credential.rs index a787fc0..74d7df4 100644 --- a/passkey-authenticator/src/authenticator/make_credential.rs +++ b/passkey-authenticator/src/authenticator/make_credential.rs @@ -55,7 +55,13 @@ where // return CTAP2_ERR_INVALID_OPTION. Ignore any options that are not understood. // Note that because this specification defines normative behaviors for them, all // authenticators MUST understand the "rk", "up", and "uv" options. - // NB: this is handled at the very begining of the method + // NOTE: Some of this step is handled at the very begining of the method + + // 4. If the "rk" option is present then: + // 1. If the rk option ID is not present in authenticatorGetInfo response, end the operation by returning CTAP2_ERR_UNSUPPORTED_OPTION. + if input.options.rk && !self.get_info().await.options.unwrap_or_default().rk { + return Err(Ctap2Error::UnsupportedOption.into()); + } // 4. TODO, if the extensions parameter is present, process any extensions that this // authenticator supports. Authenticator extension outputs generated by the authenticator @@ -157,8 +163,12 @@ mod tests { use coset::iana; use passkey_types::{ - ctap2::make_credential::{Options, PublicKeyCredentialRpEntity}, - ctap2::Aaguid, + ctap2::{ + make_credential::{ + Options, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, + }, + Aaguid, + }, rand::random_vec, webauthn, Bytes, }; @@ -166,7 +176,11 @@ mod tests { use tokio::sync::Mutex; use super::*; - use crate::{user_validation::MockUserValidationMethod, MemoryStore}; + use crate::{ + credential_store::{DiscoverabilitySupport, StoreInfo}, + user_validation::MockUserValidationMethod, + MemoryStore, + }; fn good_request() -> Request { Request { @@ -291,4 +305,57 @@ mod tests { let store = shared_store.lock().await; assert_eq!(store.as_ref().and_then(|c| c.counter).unwrap(), 0); } + + #[tokio::test] + async fn make_credential_returns_err_when_rk_is_requested_but_not_supported() { + struct StoreWithoutDiscoverableSupport; + #[async_trait::async_trait] + impl CredentialStore for StoreWithoutDiscoverableSupport { + type PasskeyItem = Passkey; + + async fn find_credentials( + &self, + _id: Option<&[webauthn::PublicKeyCredentialDescriptor]>, + _rp_id: &str, + ) -> Result, StatusCode> { + unimplemented!("The test should not call find_credentials") + } + + async fn save_credential( + &mut self, + _cred: Passkey, + _user: PublicKeyCredentialUserEntity, + _rp: PublicKeyCredentialRpEntity, + _options: Options, + ) -> Result<(), StatusCode> { + unimplemented!("The test should not call save_credential") + } + + async fn update_credential(&mut self, _cred: Passkey) -> Result<(), StatusCode> { + unimplemented!("The test should not call update_credential") + } + + async fn get_info(&self) -> StoreInfo { + StoreInfo { + discoverability: DiscoverabilitySupport::OnlyNonDiscoverable, + } + } + } + + // Arrange + let store = StoreWithoutDiscoverableSupport; + let user_mock = MockUserValidationMethod::verified_user(1); + let request = good_request(); + let mut authenticator = Authenticator::new(Aaguid::new_empty(), store, user_mock); + authenticator.set_make_credentials_with_signature_counter(true); + + // Act + let err = authenticator + .make_credential(request) + .await + .expect_err("Succeeded with unsupported rk"); + + // Assert + assert_eq!(err, Ctap2Error::UnsupportedOption.into()); + } } diff --git a/passkey-authenticator/src/credential_store.rs b/passkey-authenticator/src/credential_store.rs index d0e3efe..674ba4c 100644 --- a/passkey-authenticator/src/credential_store.rs +++ b/passkey-authenticator/src/credential_store.rs @@ -19,6 +19,7 @@ pub struct StoreInfo { /// Enum to define how the store handles discoverability. /// Note that this is does not say anything about which storage mode will be used. +#[derive(PartialEq)] pub enum DiscoverabilitySupport { /// The store supports both discoverable and non-credentials. Full, diff --git a/passkey-authenticator/src/ctap2.rs b/passkey-authenticator/src/ctap2.rs index 1bfb317..0dcb324 100644 --- a/passkey-authenticator/src/ctap2.rs +++ b/passkey-authenticator/src/ctap2.rs @@ -52,7 +52,7 @@ where U: UserValidationMethod + Sync + Send, { async fn get_info(&self) -> get_info::Response { - self.get_info() + self.get_info().await } async fn make_credential( diff --git a/passkey-authenticator/src/user_validation.rs b/passkey-authenticator/src/user_validation.rs index 9f3f059..26a45de 100644 --- a/passkey-authenticator/src/user_validation.rs +++ b/passkey-authenticator/src/user_validation.rs @@ -64,7 +64,11 @@ impl MockUserValidationMethod { user_mock .expect_is_verification_enabled() .returning(|| Some(true)) - .times(times); + .times(..); + user_mock + .expect_is_presence_enabled() + .returning(|| true) + .times(..); user_mock .expect_check_user() .with( @@ -87,8 +91,7 @@ impl MockUserValidationMethod { let mut user_mock = MockUserValidationMethod::new(); user_mock .expect_is_verification_enabled() - .returning(|| Some(true)) - .times(times); + .returning(|| Some(true)); user_mock .expect_check_user() .with( diff --git a/passkey-client/src/lib.rs b/passkey-client/src/lib.rs index 39958e9..c523b08 100644 --- a/passkey-client/src/lib.rs +++ b/passkey-client/src/lib.rs @@ -175,7 +175,7 @@ where ) -> Result { // extract inner value of request as there is nothing else of value directly in CredentialCreationOptions let request = request.public_key; - let auth_info = self.authenticator.get_info(); + let auth_info = self.authenticator.get_info().await; let pub_key_cred_params = if request.pub_key_cred_params.is_empty() { webauthn::PublicKeyCredentialParameters::default_algorithms() From f1d4287a1b2f8710dc7b15ce1f4ba69bbb7dae13 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Thu, 2 May 2024 16:16:53 +0200 Subject: [PATCH 14/25] [PM-7149] fix: get_assertion should throw is rk is true --- passkey-authenticator/src/authenticator/get_assertion.rs | 8 +++++++- passkey-client/src/lib.rs | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/passkey-authenticator/src/authenticator/get_assertion.rs b/passkey-authenticator/src/authenticator/get_assertion.rs index 069ab0c..8f33d8b 100644 --- a/passkey-authenticator/src/authenticator/get_assertion.rs +++ b/passkey-authenticator/src/authenticator/get_assertion.rs @@ -63,6 +63,12 @@ where // Note that because this specification defines normative behaviors for them, all // authenticators MUST understand the "rk", "up", and "uv" options. + // 4. If the "rk" option is present then: + // 1. Return CTAP2_ERR_UNSUPPORTED_OPTION. + if input.options.rk { + return Err(Ctap2Error::UnsupportedOption.into()); + } + // 6. TODO, if the extensions parameter is present, process any extensions that this // authenticator supports. Authenticator extension outputs generated by the authenticator // extension processing are returned in the authenticator data. @@ -187,7 +193,7 @@ mod tests { options: Options { up: true, uv: true, - rk: true, + rk: false, }, } } diff --git a/passkey-client/src/lib.rs b/passkey-client/src/lib.rs index c523b08..9c8ae43 100644 --- a/passkey-client/src/lib.rs +++ b/passkey-client/src/lib.rs @@ -342,7 +342,7 @@ where allow_list: request.allow_credentials, extensions: request.extensions, options: ctap2::get_assertion::Options { - rk: true, + rk: false, up: true, uv: true, }, From 8d33d5816394adfb5a0c99db387a6c0758151770 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Thu, 2 May 2024 16:18:15 +0200 Subject: [PATCH 15/25] [PM-7149] fix: hardcoded UV in assertions --- passkey-client/src/lib.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/passkey-client/src/lib.rs b/passkey-client/src/lib.rs index 9c8ae43..69924b9 100644 --- a/passkey-client/src/lib.rs +++ b/passkey-client/src/lib.rs @@ -334,6 +334,9 @@ where .client_data_hash() .unwrap_or_else(|| sha256(client_data_json.as_bytes()).to_vec()); + let rk = false; + let uv = request.user_verification != UserVerificationRequirement::Discouraged; + let ctap2_response = self .authenticator .get_assertion(ctap2::get_assertion::Request { @@ -341,11 +344,7 @@ where client_data_hash: client_data_json_hash.into(), allow_list: request.allow_credentials, extensions: request.extensions, - options: ctap2::get_assertion::Options { - rk: false, - up: true, - uv: true, - }, + options: ctap2::get_assertion::Options { rk, up: true, uv }, pin_auth: None, pin_protocol: None, }) From f079e5efafdcb00da7a826db4dc3bf8ee8bc0db1 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Thu, 2 May 2024 16:31:56 +0200 Subject: [PATCH 16/25] [PM-7149] feat: return real rk value in credProps --- passkey-authenticator/src/lib.rs | 2 +- passkey-client/src/lib.rs | 31 +++++++++++++++++++++---------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/passkey-authenticator/src/lib.rs b/passkey-authenticator/src/lib.rs index f3c9d6a..834e9b9 100644 --- a/passkey-authenticator/src/lib.rs +++ b/passkey-authenticator/src/lib.rs @@ -43,7 +43,7 @@ use passkey_types::{ctap2::Ctap2Error, Bytes}; pub use self::{ authenticator::Authenticator, - credential_store::{CredentialStore, MemoryStore}, + credential_store::{CredentialStore, DiscoverabilitySupport, MemoryStore}, ctap2::Ctap2Api, u2f::U2fApi, user_validation::{UserCheck, UserValidationMethod}, diff --git a/passkey-client/src/lib.rs b/passkey-client/src/lib.rs index 69924b9..d88cbde 100644 --- a/passkey-client/src/lib.rs +++ b/passkey-client/src/lib.rs @@ -21,7 +21,9 @@ use std::borrow::Cow; use ciborium::{cbor, value::Value}; use coset::{iana::EnumI64, Algorithm}; -use passkey_authenticator::{Authenticator, CredentialStore, UserValidationMethod}; +use passkey_authenticator::{ + Authenticator, CredentialStore, DiscoverabilitySupport, UserValidationMethod, +}; use passkey_types::{ crypto::sha256, ctap2, encoding, @@ -208,15 +210,8 @@ where .client_data_hash() .unwrap_or_else(|| sha256(client_data_json.as_bytes()).to_vec()); - let cred_props = - if let Some(true) = request.extensions.as_ref().and_then(|ext| ext.cred_props) { - Some(CredentialPropertiesOutput { - discoverable: Some(true), // Set to true because it is set in the Options of make_credential. - authenticator_display_name: self.authenticator.display_name().cloned(), - }) - } else { - None - }; + let cred_props_requested = + request.extensions.as_ref().and_then(|ext| ext.cred_props) == Some(true); let rk = self.map_rk(&request.authenticator_selection); let uv = request.authenticator_selection.map(|s| s.user_verification) @@ -277,6 +272,22 @@ where .map_err(|e| WebauthnError::AuthenticatorError(e.into()))?, ); + let cred_props = if cred_props_requested { + let auth_discoverability = self.authenticator.store().get_info().await.discoverability; + let discoverable = match auth_discoverability { + DiscoverabilitySupport::Full => rk, + DiscoverabilitySupport::OnlyNonDiscoverable => false, + DiscoverabilitySupport::ForcedDiscoverable => true, + }; + + Some(CredentialPropertiesOutput { + discoverable: Some(discoverable), + authenticator_display_name: self.authenticator.display_name().cloned(), + }) + } else { + None + }; + let response = webauthn::CreatedPublicKeyCredential { id: encoding::base64url(credential_id.credential_id()), raw_id: credential_id.credential_id().to_vec().into(), From 99850a061064f8bfbf50031443e64c992e85c748 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Thu, 2 May 2024 16:51:10 +0200 Subject: [PATCH 17/25] [PM-7149] lint: fix --- passkey-types/src/utils/bytes.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/passkey-types/src/utils/bytes.rs b/passkey-types/src/utils/bytes.rs index aae7c46..6c884ab 100644 --- a/passkey-types/src/utils/bytes.rs +++ b/passkey-types/src/utils/bytes.rs @@ -100,7 +100,7 @@ impl Serialize for Bytes { S: serde::Serializer, { if cfg!(feature = "serialize_bytes_as_base64_string") { - serializer.serialize_str(&crate::encoding::base64url(&self.0)) + serializer.serialize_str(&encoding::base64url(&self.0)) } else { serializer.serialize_bytes(&self.0) } From 7adfa1151b86fb87b703a93cab02786920baa251 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Mon, 13 May 2024 14:03:37 +0200 Subject: [PATCH 18/25] [PM-7149] fix: lint --- passkey-authenticator/src/authenticator.rs | 2 +- .../src/authenticator/make_credential.rs | 3 +++ passkey-authenticator/src/lib.rs | 4 ++-- passkey-authenticator/src/u2f.rs | 9 ++++----- passkey-client/src/tests/mod.rs | 4 ++-- public-suffix/src/lib.rs | 6 ++++++ 6 files changed, 18 insertions(+), 10 deletions(-) diff --git a/passkey-authenticator/src/authenticator.rs b/passkey-authenticator/src/authenticator.rs index 053c7e0..f4c7b7a 100644 --- a/passkey-authenticator/src/authenticator.rs +++ b/passkey-authenticator/src/authenticator.rs @@ -204,7 +204,7 @@ mod tests { let result = authenticator.check_user(&options, None).await.unwrap(); // Assert - assert_eq!(result, passkey_types::ctap2::Flags::empty()); + assert_eq!(result, Flags::empty()); } #[tokio::test] diff --git a/passkey-authenticator/src/authenticator/make_credential.rs b/passkey-authenticator/src/authenticator/make_credential.rs index 74d7df4..d89ee23 100644 --- a/passkey-authenticator/src/authenticator/make_credential.rs +++ b/passkey-authenticator/src/authenticator/make_credential.rs @@ -318,6 +318,7 @@ mod tests { _id: Option<&[webauthn::PublicKeyCredentialDescriptor]>, _rp_id: &str, ) -> Result, StatusCode> { + #![allow(clippy::unimplemented)] unimplemented!("The test should not call find_credentials") } @@ -328,10 +329,12 @@ mod tests { _rp: PublicKeyCredentialRpEntity, _options: Options, ) -> Result<(), StatusCode> { + #![allow(clippy::unimplemented)] unimplemented!("The test should not call save_credential") } async fn update_credential(&mut self, _cred: Passkey) -> Result<(), StatusCode> { + #![allow(clippy::unimplemented)] unimplemented!("The test should not call update_credential") } diff --git a/passkey-authenticator/src/lib.rs b/passkey-authenticator/src/lib.rs index 834e9b9..a440f28 100644 --- a/passkey-authenticator/src/lib.rs +++ b/passkey-authenticator/src/lib.rs @@ -58,7 +58,7 @@ fn private_key_from_cose_key(key: &CoseKey) -> Result { if !matches!( key.alg, Some(coset::RegisteredLabelWithPrivate::Assigned( - iana::Algorithm::ES256 + Algorithm::ES256 )) ) { return Err(Ctap2Error::UnsupportedAlgorithm); @@ -94,7 +94,7 @@ pub fn public_key_der_from_cose_key(key: &CoseKey) -> Result if !matches!( key.alg, Some(coset::RegisteredLabelWithPrivate::Assigned( - iana::Algorithm::ES256 + Algorithm::ES256 )) ) { return Err(Ctap2Error::UnsupportedAlgorithm); diff --git a/passkey-authenticator/src/u2f.rs b/passkey-authenticator/src/u2f.rs index 1d70808..1ed0580 100644 --- a/passkey-authenticator/src/u2f.rs +++ b/passkey-authenticator/src/u2f.rs @@ -7,7 +7,7 @@ use p256::{ SecretKey, }; use passkey_types::{ - ctap2::{get_assertion::Options, Flags, U2FError}, + ctap2::{Flags, U2FError}, u2f::{ AuthenticationRequest, AuthenticationResponse, PublicKey, RegisterRequest, RegisterResponse, }, @@ -90,12 +90,11 @@ impl U2 signature, }; - let (passkey, user, rp) = passkey_types::Passkey::wrap_u2f_registration_request( - &request, &response, handle, &private, - ); + let (passkey, user, rp) = + Passkey::wrap_u2f_registration_request(&request, &response, handle, &private); // U2F registration does not use rk, uv, or up - let options = Options { + let options = passkey_types::ctap2::get_assertion::Options { rk: false, uv: false, up: false, diff --git a/passkey-client/src/tests/mod.rs b/passkey-client/src/tests/mod.rs index 371f0ff..393ace8 100644 --- a/passkey-client/src/tests/mod.rs +++ b/passkey-client/src/tests/mod.rs @@ -418,7 +418,7 @@ async fn client_register_triggers_uv_when_uv_is_required() { public_key: good_credential_creation_options(), }; options.public_key.authenticator_selection = Some(AuthenticatorSelectionCriteria { - user_verification: webauthn::UserVerificationRequirement::Required, + user_verification: UserVerificationRequirement::Required, authenticator_attachment: Default::default(), resident_key: Default::default(), require_resident_key: Default::default(), @@ -445,7 +445,7 @@ async fn client_register_does_not_trigger_uv_when_uv_is_discouraged() { public_key: good_credential_creation_options(), }; options.public_key.authenticator_selection = Some(AuthenticatorSelectionCriteria { - user_verification: webauthn::UserVerificationRequirement::Discouraged, + user_verification: UserVerificationRequirement::Discouraged, authenticator_attachment: Default::default(), resident_key: Default::default(), require_resident_key: Default::default(), diff --git a/public-suffix/src/lib.rs b/public-suffix/src/lib.rs index 0a1aa99..30e04c8 100644 --- a/public-suffix/src/lib.rs +++ b/public-suffix/src/lib.rs @@ -167,6 +167,12 @@ impl EffectiveTLDProvider for ListProvider { } } +impl Default for ListProvider { + fn default() -> Self { + Self::new() + } +} + impl ListProvider { /// Create a new ListProvider. pub const fn new() -> Self { From da23fa68d3790b466e1b3f1cbb55803917f2c220 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Wed, 26 Jun 2024 11:22:26 +0200 Subject: [PATCH 19/25] fix: verified_user_with_credential comparison Re-added `PartialEq` to `Passkey`, but this time it is scoped to tests only --- passkey-authenticator/Cargo.toml | 3 +++ passkey-authenticator/src/user_validation.rs | 6 +----- passkey-types/Cargo.toml | 1 + passkey-types/src/passkey.rs | 1 + public-suffix/src/lib.rs | 6 ------ 5 files changed, 6 insertions(+), 11 deletions(-) diff --git a/passkey-authenticator/Cargo.toml b/passkey-authenticator/Cargo.toml index b78477f..2a09e61 100644 --- a/passkey-authenticator/Cargo.toml +++ b/passkey-authenticator/Cargo.toml @@ -31,6 +31,9 @@ tokio = { version = "1", features = ["sync"], optional = true } [dev-dependencies] mockall = { version = "0.11" } +passkey-types = { path = "../passkey-types", version = "0.2", features = [ + "testable", +] } tokio = { version = "1", features = ["sync", "macros", "rt"] } generic-array = { version = "0.14", default-features = false } signature = { version = "2", features = ["rand_core"] } diff --git a/passkey-authenticator/src/user_validation.rs b/passkey-authenticator/src/user_validation.rs index 0fd2db5..843a73f 100644 --- a/passkey-authenticator/src/user_validation.rs +++ b/passkey-authenticator/src/user_validation.rs @@ -94,11 +94,7 @@ impl MockUserValidationMethod { .returning(|| Some(true)); user_mock .expect_check_user() - .with( - mockall::predicate::eq(Some(credential)), - mockall::predicate::eq(true), - mockall::predicate::eq(true), - ) + .withf(move |cred, up, uv| cred == &Some(&credential) && *up && *uv) .returning(|_, _, _| { Ok(UserCheck { presence: true, diff --git a/passkey-types/Cargo.toml b/passkey-types/Cargo.toml index dce682c..d7cb2ee 100644 --- a/passkey-types/Cargo.toml +++ b/passkey-types/Cargo.toml @@ -17,6 +17,7 @@ workspace = true [features] default = [] serialize_bytes_as_base64_string = [] +testable = [] [dependencies] bitflags = "2" diff --git a/passkey-types/src/passkey.rs b/passkey-types/src/passkey.rs index 321e69e..d59b22f 100644 --- a/passkey-types/src/passkey.rs +++ b/passkey-types/src/passkey.rs @@ -21,6 +21,7 @@ use coset::CoseKey; // TODO: Implement Zeroize on this if/when rolling our own CoseKey type // TODO: use `#[non_exhaustive]` here with a builder pattern for building new passkeys #[derive(Clone)] +#[cfg_attr(any(test, feature = "testable"), derive(PartialEq))] pub struct Passkey { /// The private key in COSE key format. /// diff --git a/public-suffix/src/lib.rs b/public-suffix/src/lib.rs index f8915c9..30e04c8 100644 --- a/public-suffix/src/lib.rs +++ b/public-suffix/src/lib.rs @@ -291,12 +291,6 @@ impl ListProvider { } } -impl Default for ListProvider { - fn default() -> Self { - Self::new() - } -} - fn after_or_all(dot: Option) -> RangeFrom { match dot { Some(dot) => (dot + 1).., From 48a75bd0adc4c6f09a3a75cf03f1e4c0c53721a5 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Wed, 26 Jun 2024 11:31:49 +0200 Subject: [PATCH 20/25] docs: update CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74e6a72..ad819cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ - Removed: `UserValidationMethod::check_user_verification` - Added: `UserValidationMethod::check_user`. This function now performs both user presence and user verification checks. The function now also returns which validations were performed, even if they were not requested. +- Added: Support for discoverable credentials + - ⚠ BREAKING: Added: `CredentialStore::get_info` which returns `StoreInfo` containing `DiscoverabilitySupport`. + - ⚠ BREAKING: Changed: `CredentialStore::save_credential` now also takes `Options`. + - Changed: `Authenticator::make_credentials` now returns an error if a discoverable credential was requested but not supported by the store. ### passkey-client @@ -25,6 +29,7 @@ handles client data and its hash. - Added: Additional fields can be added to the client data using `DefaultClientDataWithExtra(ExtraData)`. - `CollectedClientData` is now generic and supports additional strongly typed fields. - Changed: `CollectedClientData` has changed to `CollectedClientData` +- The `Client` now returns `CredProps::rk` depending on the authenticator's capabilities. ## Passkey v0.2.0 ### passkey-types v0.2.0 From 55c04cfd699662b0f86d8089d029c7fe21850ef9 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Tue, 16 Jul 2024 10:09:00 +0200 Subject: [PATCH 21/25] fix: move `CollectedClientData` to correct section --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f62d03a..539c430 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,8 +23,6 @@ handles client data and its hash. - ⚠ BREAKING: Changed: Custom client data hashes are now specified using `DefaultClientDataWithCustomHash(Vec)` instead of `Some(Vec)`. - Added: Additional fields can be added to the client data using `DefaultClientDataWithExtra(ExtraData)`. -- `CollectedClientData` is now generic and supports additional strongly typed fields. - - Changed: `CollectedClientData` has changed to `CollectedClientData` - Added: The `Client` now has the ability to adjust the response for quirky relying parties when a fully featured response would break their server side validation. ([#31](https://github.com/1Password/passkey-rs/pull/31)) - ⚠ BREAKING: Added the `Origin` enum which is now the origin parameter for the following methods ([#32](https://github.com/1Password/passkey-rs/pull/27)): @@ -33,6 +31,11 @@ handles client data and its hash. - `RpIdValidator::assert_domain` takes an `&Origin` instead of a `&Url` - ⚠ BREAKING: The collected client data will now have the android app signature as the origin when a request comes from an app directly. ([#32](https://github.com/1Password/passkey-rs/pull/27)) +## passkey-types + +- `CollectedClientData` is now generic and supports additional strongly typed fields. + - Changed: `CollectedClientData` has changed to `CollectedClientData` + ## Passkey v0.2.0 ### passkey-types v0.2.0 From 6cb53c0d7fc904549fde022578a9e96b9cca01c2 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Tue, 16 Jul 2024 10:10:41 +0200 Subject: [PATCH 22/25] fix: remove unnecessary `clone()` --- passkey-client/src/tests/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/passkey-client/src/tests/mod.rs b/passkey-client/src/tests/mod.rs index ca9aa4d..9aa8d79 100644 --- a/passkey-client/src/tests/mod.rs +++ b/passkey-client/src/tests/mod.rs @@ -155,7 +155,7 @@ async fn create_and_authenticate_with_extra_client_data() { .authenticate( &origin, auth_options, - DefaultClientDataWithExtra(extra_data.clone()), + DefaultClientDataWithExtra(extra_data), ) .await .expect("failed to authenticate with freshly created credential"); From 551e52232df67ddae3542f09dcc6b5fe542ba8af Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Tue, 16 Jul 2024 10:11:34 +0200 Subject: [PATCH 23/25] chore: fix formatting according to suggestion --- passkey/src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/passkey/src/lib.rs b/passkey/src/lib.rs index d0bdad9..bb4e2c6 100644 --- a/passkey/src/lib.rs +++ b/passkey/src/lib.rs @@ -143,7 +143,10 @@ //! }; //! //! // Now create the credential. -//! let my_webauthn_credential = my_client.register(&origin, request, DefaultClientData).await.unwrap(); +//! let my_webauthn_credential = my_client +//! .register(&origin, request, DefaultClientData) +//! .await +//! .unwrap(); //! //! // Let's try and authenticate. //! // Create a challenge that would usually come from the RP. From bbea425ebabd52a2581d052f1f942253575da4eb Mon Sep 17 00:00:00 2001 From: Rene Leveille Date: Thu, 25 Jul 2024 13:12:36 -0400 Subject: [PATCH 24/25] Address documentation lints for rust 1.80 --- passkey-types/src/passkey.rs | 4 ++-- public-suffix/src/lib.rs | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/passkey-types/src/passkey.rs b/passkey-types/src/passkey.rs index d59b22f..0fe5d5f 100644 --- a/passkey-types/src/passkey.rs +++ b/passkey-types/src/passkey.rs @@ -37,8 +37,8 @@ pub struct Passkey { /// Credential IDs are generated by authenticators in two forms: /// 1. At least 16 bytes that include at least 100 bits of entropy, or /// 2. The [`Passkey`] item, without its `credential_id`, encrypted so only its managing - /// authenticator can decrypt it. This form allows the authenticator to be nearly stateless, by - /// having the Relying Party store any necessary state. + /// authenticator can decrypt it. This form allows the authenticator to be nearly stateless, by + /// having the Relying Party store any necessary state. /// /// Relying Parties do not need to distinguish these two `credential id` forms. /// diff --git a/public-suffix/src/lib.rs b/public-suffix/src/lib.rs index 30e04c8..bf0b279 100644 --- a/public-suffix/src/lib.rs +++ b/public-suffix/src/lib.rs @@ -41,6 +41,7 @@ //! - "www.books.amazon.co.uk" //! - "books.amazon.co.uk" //! - "amazon.co.uk" +//! //! Specifically, the eTLD+1 is "amazon.co.uk", because the eTLD is "co.uk". //! //! ``` @@ -86,12 +87,12 @@ //! 0. Make sure you have golang installed. //! 1. Make the public-suffix crate the current working directory. //! 2. `wget https://publicsuffix.org/list/public_suffix_list.dat`, which will -//! overwrite the old version of this file. +//! overwrite the old version of this file. //! 3. Run `./gen.sh` to regenerate the list from the updated `public_suffix_list.dat`. -//! The first time you run this, you'll need network connectivity to `go get` the -//! dependencies. +//! The first time you run this, you'll need network connectivity to `go get` the +//! dependencies. //! 4. Commit the changed generated source code and the updated -//! `public_suffix_list.dat`. +//! `public_suffix_list.dat`. //! //! We intentionally do not try to download the latest version of the public suffix //! list during the build to keep the build deterministic and networking-free. From b7b2772fa2e5883ee8736f822fbf77d061d978d4 Mon Sep 17 00:00:00 2001 From: Rene Leveille Date: Thu, 25 Jul 2024 15:20:07 -0400 Subject: [PATCH 25/25] fix doc cfg --- passkey-authenticator/src/authenticator/extensions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/passkey-authenticator/src/authenticator/extensions.rs b/passkey-authenticator/src/authenticator/extensions.rs index 42b05af..016efb1 100644 --- a/passkey-authenticator/src/authenticator/extensions.rs +++ b/passkey-authenticator/src/authenticator/extensions.rs @@ -17,7 +17,7 @@ use passkey_types::{ mod hmac_secret; pub use hmac_secret::{HmacSecretConfig, HmacSecretCredentialSupport}; -#[cfg(docs)] +#[cfg(doc)] use passkey_types::webauthn; use crate::Authenticator;