From 30b511c6323f652e12445d98a05df37edd98a9b5 Mon Sep 17 00:00:00 2001 From: Rene Leveille Date: Mon, 19 Jan 2026 11:30:33 -0500 Subject: [PATCH 01/10] add taplo config --- taplo.toml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 taplo.toml diff --git a/taplo.toml b/taplo.toml new file mode 100644 index 0000000..21e8f63 --- /dev/null +++ b/taplo.toml @@ -0,0 +1,3 @@ +[formatting] +reorder_inline_tables = false +reorder_keys = true From edd95509b087730f02bbfb56fcc605ad27098dae Mon Sep 17 00:00:00 2001 From: Rene Leveille Date: Mon, 19 Jan 2026 11:30:56 -0500 Subject: [PATCH 02/10] Add passkey-crypto crate --- Cargo.toml | 1 + passkey-crypto/Cargo.toml | 18 ++++++++++++++++++ passkey-crypto/README.md | 0 passkey-crypto/src/lib.rs | 14 ++++++++++++++ 4 files changed, 33 insertions(+) create mode 100644 passkey-crypto/Cargo.toml create mode 100644 passkey-crypto/README.md create mode 100644 passkey-crypto/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 017b009..a663c96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "passkey", "passkey-authenticator", "passkey-client", + "passkey-crypto", "passkey-transports", "passkey-types", "public-suffix", diff --git a/passkey-crypto/Cargo.toml b/passkey-crypto/Cargo.toml new file mode 100644 index 0000000..1a84727 --- /dev/null +++ b/passkey-crypto/Cargo.toml @@ -0,0 +1,18 @@ +[package] +authors.workspace = true +categories.workspace = true +description = "Generic cryptographic backend for the passkey-rs crates" +edition.workspace = true +include = ["../LICENSE-APACHE", "../LICENSE-MIT", "src/"] +keywords.workspace = true +license.workspace = true +name = "passkey-crypto" +readme = "README.md" +repository.workspace = true +rust-version.workspace = true +version = "0.1.0" + +[dependencies] + +[lints] +workspace = true diff --git a/passkey-crypto/README.md b/passkey-crypto/README.md new file mode 100644 index 0000000..e69de29 diff --git a/passkey-crypto/src/lib.rs b/passkey-crypto/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/passkey-crypto/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} From 0d169491c97d833961cb1e8094c017c04e1d5302 Mon Sep 17 00:00:00 2001 From: Rene Leveille Date: Mon, 19 Jan 2026 12:44:31 -0500 Subject: [PATCH 03/10] Add an RngBackend trait with a default feature for the rand crate --- passkey-authenticator/Cargo.toml | 1 + passkey-client/Cargo.toml | 1 + passkey-crypto/Cargo.toml | 9 +++++++++ passkey-crypto/src/lib.rs | 15 ++++----------- passkey-crypto/src/rng/mod.rs | 29 +++++++++++++++++++++++++++++ passkey-crypto/src/rng/rand.rs | 28 ++++++++++++++++++++++++++++ passkey-types/Cargo.toml | 1 + passkey/Cargo.toml | 4 ++++ 8 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 passkey-crypto/src/rng/mod.rs create mode 100644 passkey-crypto/src/rng/rand.rs diff --git a/passkey-authenticator/Cargo.toml b/passkey-authenticator/Cargo.toml index 6bda8b1..5450a47 100644 --- a/passkey-authenticator/Cargo.toml +++ b/passkey-authenticator/Cargo.toml @@ -17,6 +17,7 @@ workspace = true [features] default = [] +js = ["passkey-crypto/js"] testable = ["dep:mockall", "passkey-types/testable"] tokio = ["dep:tokio"] diff --git a/passkey-client/Cargo.toml b/passkey-client/Cargo.toml index bd6b818..5236660 100644 --- a/passkey-client/Cargo.toml +++ b/passkey-client/Cargo.toml @@ -17,6 +17,7 @@ workspace = true [features] android-asset-validation = ["dep:nom"] +js = ["passkey-crypto/js"] testable = ["dep:mockall"] tokio = ["dep:tokio"] typeshare = ["passkey-types/typeshare", "dep:typeshare"] diff --git a/passkey-crypto/Cargo.toml b/passkey-crypto/Cargo.toml index 1a84727..e9e284a 100644 --- a/passkey-crypto/Cargo.toml +++ b/passkey-crypto/Cargo.toml @@ -12,7 +12,16 @@ repository.workspace = true rust-version.workspace = true version = "0.1.0" +[features] +default = ["rand"] +rand = ["dep:rand", "dep:getrandom"] +js = ["getrandom/js"] + [dependencies] +rand = { version = "0.8", optional = true , features = ["min_const_gen"]} + +[target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = { version = "0.2", optional = true } [lints] workspace = true diff --git a/passkey-crypto/src/lib.rs b/passkey-crypto/src/lib.rs index b93cf3f..0ab5a0e 100644 --- a/passkey-crypto/src/lib.rs +++ b/passkey-crypto/src/lib.rs @@ -1,14 +1,7 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +//! -#[cfg(test)] -mod tests { - use super::*; +pub mod rng; - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } +pub trait CryptoBackend { + type Rng: rng::RngBackend; } diff --git a/passkey-crypto/src/rng/mod.rs b/passkey-crypto/src/rng/mod.rs new file mode 100644 index 0000000..c7681b2 --- /dev/null +++ b/passkey-crypto/src/rng/mod.rs @@ -0,0 +1,29 @@ +//! Implements the various number generator backends. +//! +//! Each backend is mutually exclusive, therefore only one backend may be used at a time. +//! Currently the implemented backends are: +//! * `rand`: Using `::rand::thread_rng` + +#[cfg(feature = "rand")] +mod rand; + +use std::ops::RangeInclusive; + +#[cfg(feature = "rand")] +use rand::*; + +/// The methods that all Random Number Generator backends must implement for use accross the passkey +/// crates. +pub trait RngBackend: Sized { + /// TEMP: until key creation is self contained in CryptoBackend + fn new() -> Self; + /// Generate random data of specific length. + fn random_vec(len: usize) -> Vec; + /// Generate a fixed size array of random bytes + fn random_array() -> [u8; N]; + /// Randomly select a number from a given range + fn from_range(range: RangeInclusive) -> u8; +} + +/// The enabled Random Number Generator backend +pub type Rng = _Rng; diff --git a/passkey-crypto/src/rng/rand.rs b/passkey-crypto/src/rng/rand.rs new file mode 100644 index 0000000..709a8b8 --- /dev/null +++ b/passkey-crypto/src/rng/rand.rs @@ -0,0 +1,28 @@ +use ::rand::{Rng, RngCore, rngs::ThreadRng}; + +use super::RngBackend; + +pub(super) type _Rng = ThreadRng; + +impl RngBackend for ThreadRng { + fn new() -> Self { + ::rand::thread_rng() + } + fn random_vec(len: usize) -> Vec { + let mut data = vec![0u8; len]; + let mut rng = ::rand::thread_rng(); + rng.fill_bytes(&mut data); + data + } + fn random_array() -> [u8; N] { + let mut rng = ::rand::thread_rng(); + // The rand crate has updated this wording in version 0.9 and above. + // Unfortunately, the RustCrypto ecosystem hasn't updated yet so here we are. + rng.r#gen() + } + + fn from_range(range: std::ops::RangeInclusive) -> u8 { + let mut rng = ::rand::thread_rng(); + rng.gen_range(range) + } +} diff --git a/passkey-types/Cargo.toml b/passkey-types/Cargo.toml index ef669d2..db0c1f9 100644 --- a/passkey-types/Cargo.toml +++ b/passkey-types/Cargo.toml @@ -17,6 +17,7 @@ workspace = true [features] default = [] +js = ["passkey-crypto/js"] serialize_bytes_as_base64_string = [] testable = ["dep:p256"] typeshare = ["dep:typeshare"] diff --git a/passkey/Cargo.toml b/passkey/Cargo.toml index 0d6e807..2a2f964 100644 --- a/passkey/Cargo.toml +++ b/passkey/Cargo.toml @@ -15,6 +15,10 @@ version = "0.5.0" [lints] workspace = true +[features] +default = [] +js = ["passkey-crypto/js"] + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [package.metadata.cargo-udeps.ignore] From b914290378fcd7ba66eb9cf1283250b5ef6792ea Mon Sep 17 00:00:00 2001 From: Rene Leveille Date: Mon, 19 Jan 2026 12:45:15 -0500 Subject: [PATCH 04/10] Apply the use of the new swappabled Rng Backend to all the crates --- passkey-authenticator/Cargo.toml | 2 +- passkey-authenticator/src/authenticator.rs | 5 +-- .../authenticator/extensions/hmac_secret.rs | 6 ++-- .../extensions/hmac_secret/tests.rs | 10 +++--- .../src/authenticator/get_assertion/tests.rs | 12 +++---- .../src/authenticator/make_credential.rs | 5 +-- .../authenticator/make_credential/tests.rs | 22 ++++++------- .../src/authenticator/tests.rs | 4 +-- passkey-authenticator/src/tests.rs | 7 +++-- passkey-authenticator/src/u2f.rs | 3 +- passkey-authenticator/src/u2f/tests.rs | 9 +++--- passkey-client/Cargo.toml | 1 + passkey-client/src/tests/ext_prf.rs | 28 ++++++++--------- passkey-client/src/tests/mod.rs | 13 ++++---- passkey-types/Cargo.toml | 5 +-- .../src/ctap2/attestation_fmt/test.rs | 8 ++--- .../src/ctap2/extensions/hmac_secret/tests.rs | 31 +++++++++---------- passkey-types/src/lib.rs | 2 +- passkey-types/src/passkey/mock.rs | 11 ++++--- passkey-types/src/utils.rs | 1 - passkey-types/src/utils/rand.rs | 15 --------- passkey/Cargo.toml | 4 +++ passkey/examples/usage.rs | 9 +++--- passkey/src/lib.rs | 15 ++++----- 24 files changed, 110 insertions(+), 118 deletions(-) delete mode 100644 passkey-types/src/utils/rand.rs diff --git a/passkey-authenticator/Cargo.toml b/passkey-authenticator/Cargo.toml index 5450a47..a707cb1 100644 --- a/passkey-authenticator/Cargo.toml +++ b/passkey-authenticator/Cargo.toml @@ -27,8 +27,8 @@ coset = { workspace = true } log = "0.4" mockall = { version = "0.11", optional = true } p256 = { version = "0.13", features = ["arithmetic", "jwk", "pem"] } +passkey-crypto = { path = "../passkey-crypto", version = "0.1" } passkey-types = { path = "../passkey-types", version = "0.5" } -rand = "0.8" tokio = { version = "1", features = ["sync"], optional = true } [dev-dependencies] diff --git a/passkey-authenticator/src/authenticator.rs b/passkey-authenticator/src/authenticator.rs index c071449..22cc1ee 100644 --- a/passkey-authenticator/src/authenticator.rs +++ b/passkey-authenticator/src/authenticator.rs @@ -1,4 +1,5 @@ use coset::iana; +use passkey_crypto::rng::RngBackend; use passkey_types::{ ctap2::{Aaguid, Ctap2Error, Flags}, webauthn, @@ -38,8 +39,8 @@ impl CredentialIdLength { const MAX: u8 = 64; /// Generates and returns a uniformly random [CredentialIdLength]. - pub fn randomized(rng: &mut impl rand::Rng) -> Self { - let length = rng.gen_range(Self::MIN..=Self::MAX); + pub fn randomized() -> Self { + let length = Rng::from_range(Self::MIN..=Self::MAX); Self(length) } } diff --git a/passkey-authenticator/src/authenticator/extensions/hmac_secret.rs b/passkey-authenticator/src/authenticator/extensions/hmac_secret.rs index 68f7de3..81cbd74 100644 --- a/passkey-authenticator/src/authenticator/extensions/hmac_secret.rs +++ b/passkey-authenticator/src/authenticator/extensions/hmac_secret.rs @@ -1,5 +1,6 @@ use std::ops::Not; +use passkey_crypto::rng::{Rng, RngBackend}; use passkey_types::{ crypto::hmac_sha256, ctap2::{ @@ -9,7 +10,6 @@ use passkey_types::{ AuthenticatorPrfValues, HmacSecretSaltOrOutput, }, }, - rand::random_vec, }; use crate::Authenticator; @@ -96,8 +96,8 @@ impl Authenticator { } Some(passkey_types::StoredHmacSecret { - cred_with_uv: random_vec(32), - cred_without_uv: config.credentials.without_uv().then(|| random_vec(32)), + cred_with_uv: Rng::random_vec(32), + cred_without_uv: config.credentials.without_uv().then(|| Rng::random_vec(32)), }) } diff --git a/passkey-authenticator/src/authenticator/extensions/hmac_secret/tests.rs b/passkey-authenticator/src/authenticator/extensions/hmac_secret/tests.rs index 81bc29a..5144a87 100644 --- a/passkey-authenticator/src/authenticator/extensions/hmac_secret/tests.rs +++ b/passkey-authenticator/src/authenticator/extensions/hmac_secret/tests.rs @@ -31,7 +31,7 @@ fn hmac_secret_cycle_works() { .hmac_secret(ext) .build(); - let request = prf_eval_request(Some(random_vec(64))); + let request = prf_eval_request(Some(Rng::random_vec(64))); let res = auth .get_prf( @@ -66,7 +66,7 @@ fn hmac_secret_cycle_works() { .get_prf( &passkey.credential_id, passkey.extensions.hmac_secret.as_ref(), - prf_eval_request(Some(random_vec(64))), + prf_eval_request(Some(Rng::random_vec(64))), true, ) .expect("Changing input should still succeed") @@ -110,7 +110,7 @@ fn hmac_secret_cycle_works_with_one_cred() { .hmac_secret(ext) .build(); - let request = prf_eval_request(Some(random_vec(64))); + let request = prf_eval_request(Some(Rng::random_vec(64))); let res = auth .get_prf( @@ -142,7 +142,7 @@ fn hmac_secret_cycle_works_with_one_cred() { .get_prf( &passkey.credential_id, passkey.extensions.hmac_secret.as_ref(), - prf_eval_request(Some(random_vec(64))), + prf_eval_request(Some(Rng::random_vec(64))), true, ) .expect("Changing input should still succeed") @@ -167,7 +167,7 @@ fn hmac_secret_cycle_works_with_one_salt() { .hmac_secret(ext) .build(); - let mut request = prf_eval_request(Some(random_vec(64))); + let mut request = prf_eval_request(Some(Rng::random_vec(64))); request.eval = request.eval.map(|e| AuthenticatorPrfValues { first: e.first, second: None, diff --git a/passkey-authenticator/src/authenticator/get_assertion/tests.rs b/passkey-authenticator/src/authenticator/get_assertion/tests.rs index 4386e6c..90aeaed 100644 --- a/passkey-authenticator/src/authenticator/get_assertion/tests.rs +++ b/passkey-authenticator/src/authenticator/get_assertion/tests.rs @@ -1,10 +1,10 @@ +use passkey_crypto::rng::{Rng, RngBackend}; use passkey_types::{ Passkey, StoredHmacSecret, ctap2::{ Aaguid, Ctap2Error, get_assertion::{ExtensionInputs, Options, Request}, }, - rand::random_vec, }; use crate::{ @@ -104,7 +104,7 @@ async fn unsupported_extension_with_request_gives_no_ext_output() { let request = Request { extensions: Some(ExtensionInputs { - prf: Some(prf_eval_request(Some(random_vec(32)))), + prf: Some(prf_eval_request(Some(Rng::random_vec(32)))), ..Default::default() }), ..good_request() @@ -143,7 +143,7 @@ async fn unsupported_extension_with_empty_request_gives_no_ext_output() { #[tokio::test] async fn supported_extension_with_empty_request_gives_no_ext_output() { - let shared_store = Some(create_passkey(Some(random_vec(32)))); + let shared_store = Some(create_passkey(Some(Rng::random_vec(32)))); let user_mock = MockUserValidationMethod::verified_user(1); let mut authenticator = @@ -166,7 +166,7 @@ async fn supported_extension_with_empty_request_gives_no_ext_output() { #[tokio::test] async fn supported_extension_without_extension_request_gives_no_ext_output() { - let shared_store = Some(create_passkey(Some(random_vec(32)))); + let shared_store = Some(create_passkey(Some(Rng::random_vec(32)))); let user_mock = MockUserValidationMethod::verified_user(1); let mut authenticator = @@ -186,7 +186,7 @@ async fn supported_extension_without_extension_request_gives_no_ext_output() { #[tokio::test] async fn supported_extension_with_request_gives_output() { - let shared_store = Some(create_passkey(Some(random_vec(32)))); + let shared_store = Some(create_passkey(Some(Rng::random_vec(32)))); let user_mock = MockUserValidationMethod::verified_user(1); let mut authenticator = @@ -195,7 +195,7 @@ async fn supported_extension_with_request_gives_output() { let request = Request { extensions: Some(ExtensionInputs { - prf: Some(prf_eval_request(Some(random_vec(32)))), + prf: Some(prf_eval_request(Some(Rng::random_vec(32)))), ..Default::default() }), ..good_request() diff --git a/passkey-authenticator/src/authenticator/make_credential.rs b/passkey-authenticator/src/authenticator/make_credential.rs index 0edb65b..0072764 100644 --- a/passkey-authenticator/src/authenticator/make_credential.rs +++ b/passkey-authenticator/src/authenticator/make_credential.rs @@ -1,4 +1,5 @@ use p256::SecretKey; +use passkey_crypto::rng::{Rng, RngBackend}; use passkey_types::{ Passkey, ctap2::{ @@ -111,10 +112,10 @@ where .await?; // 9. Generate a new credential key pair for the algorithm specified. - let credential_id = passkey_types::rand::random_vec(self.credential_id_length.into()); + let credential_id = Rng::random_vec(self.credential_id_length.into()); let private_key = { - let mut rng = rand::thread_rng(); + let mut rng = Rng::new(); SecretKey::random(&mut rng) }; diff --git a/passkey-authenticator/src/authenticator/make_credential/tests.rs b/passkey-authenticator/src/authenticator/make_credential/tests.rs index eddcff5..b2d0229 100644 --- a/passkey-authenticator/src/authenticator/make_credential/tests.rs +++ b/passkey-authenticator/src/authenticator/make_credential/tests.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use coset::iana; +use passkey_crypto::rng::{Rng, RngBackend}; use passkey_types::{ Bytes, ctap2::{ @@ -10,7 +11,6 @@ use passkey_types::{ ExtensionInputs, Options, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }, }, - rand::random_vec, webauthn, }; @@ -26,13 +26,13 @@ use crate::{ fn good_request() -> Request { Request { - client_data_hash: random_vec(32).into(), + client_data_hash: Rng::random_vec(32).into(), rp: PublicKeyCredentialRpEntity { id: "future.1password.com".into(), name: Some("1password".into()), }, user: webauthn::PublicKeyCredentialUserEntity { - id: random_vec(16).into(), + id: Rng::random_vec(16).into(), display_name: "wendy".into(), name: "Appleseed".into(), }, @@ -77,7 +77,7 @@ async fn assert_storage_on_success() { #[tokio::test] async fn assert_excluded_credentials() { - let cred_id: Bytes = random_vec(16).into(); + let cred_id: Bytes = Rng::random_vec(16).into(); let response = Request { exclude_list: Some(vec![webauthn::PublicKeyCredentialDescriptor { ty: webauthn::PublicKeyCredentialType::PublicKey, @@ -314,8 +314,8 @@ async fn hmac_secret_mc_happy_path() { extensions: Some(ExtensionInputs { prf: Some(AuthenticatorPrfInputs { eval: Some(AuthenticatorPrfValues { - first: random_vec(32).try_into().unwrap(), - second: Some(random_vec(32).try_into().unwrap()), + first: Rng::random_vec(32).try_into().unwrap(), + second: Some(Rng::random_vec(32).try_into().unwrap()), }), eval_by_credential: None, }), @@ -360,7 +360,7 @@ async fn hmac_secret_mc_without_hmac_secret_support() { extensions: Some(ExtensionInputs { prf: Some(AuthenticatorPrfInputs { eval: Some(AuthenticatorPrfValues { - first: random_vec(32).try_into().unwrap(), + first: Rng::random_vec(32).try_into().unwrap(), second: None, }), eval_by_credential: None, @@ -447,7 +447,7 @@ async fn empty_store_with_exclude_credentials_succeeds() { // would return NoCredentials error when checking excludeCredentials, // causing credential creation to fail incorrectly. - let cred_id: Bytes = random_vec(16).into(); + let cred_id: Bytes = Rng::random_vec(16).into(); let request = Request { exclude_list: Some(vec![webauthn::PublicKeyCredentialDescriptor { ty: webauthn::PublicKeyCredentialType::PublicKey, @@ -508,15 +508,15 @@ async fn store_with_credentials_not_in_exclude_list_succeeds() { // but none of them are in the excludeCredentials list, // credential creation should succeed. - let stored_cred_id: Bytes = random_vec(16).into(); - let excluded_cred_id: Bytes = random_vec(16).into(); + let stored_cred_id: Bytes = Rng::random_vec(16).into(); + let excluded_cred_id: Bytes = Rng::random_vec(16).into(); // Create a passkey that will be stored (with different ID than excluded) let passkey = Passkey { key: Default::default(), rp_id: "future.1password.com".into(), credential_id: stored_cred_id.clone(), - user_handle: Some(random_vec(16).into()), + user_handle: Some(Rng::random_vec(16).into()), username: Some("Appleseed".into()), user_display_name: Some("wendy".into()), counter: None, diff --git a/passkey-authenticator/src/authenticator/tests.rs b/passkey-authenticator/src/authenticator/tests.rs index 98954f0..a493294 100644 --- a/passkey-authenticator/src/authenticator/tests.rs +++ b/passkey-authenticator/src/authenticator/tests.rs @@ -1,3 +1,4 @@ +use passkey_crypto::rng::Rng; use passkey_types::ctap2::{Aaguid, Flags}; use crate::{ @@ -298,10 +299,9 @@ fn credential_id_lengths_validate() { #[test] fn credential_id_generation() { - let mut rng = rand::thread_rng(); let valid_range = 0..=64; for _ in 0..=100 { - let length = CredentialIdLength::randomized(&mut rng).0; + let length = CredentialIdLength::randomized::().0; assert!(valid_range.contains(&length)); } } diff --git a/passkey-authenticator/src/tests.rs b/passkey-authenticator/src/tests.rs index ab0b30a..b824347 100644 --- a/passkey-authenticator/src/tests.rs +++ b/passkey-authenticator/src/tests.rs @@ -6,14 +6,15 @@ use p256::{ signature::{Signer, Verifier}, }, }; -use passkey_types::{ctap2::AuthenticatorData, rand::random_vec}; +use passkey_crypto::rng::{Rng, RngBackend}; +use passkey_types::ctap2::AuthenticatorData; use super::{CoseKeyPair, private_key_from_cose_key}; #[test] fn private_key_cose_round_trip_sanity_check() { let private_key = { - let mut rng = rand::thread_rng(); + let mut rng = Rng::new(); SecretKey::random(&mut rng) }; let CoseKeyPair { @@ -25,7 +26,7 @@ fn private_key_cose_round_trip_sanity_check() { let auth_data = AuthenticatorData::new("future.1password.com", None); let mut signature_target = auth_data.to_vec(); - signature_target.extend(random_vec(32)); + signature_target.extend(Rng::random_vec(32)); let secret_key = private_key_from_cose_key(&private_cose).expect("to get a private key"); diff --git a/passkey-authenticator/src/u2f.rs b/passkey-authenticator/src/u2f.rs index 9a728b7..0f717e8 100644 --- a/passkey-authenticator/src/u2f.rs +++ b/passkey-authenticator/src/u2f.rs @@ -8,6 +8,7 @@ use p256::{ SecretKey, ecdsa::{SigningKey, signature::Signer}, }; +use passkey_crypto::rng::{Rng, RngBackend}; use passkey_types::{ Bytes, Passkey, ctap2::{Flags, U2FError}, @@ -53,7 +54,7 @@ impl U2 ) -> Result { // Create Keypair on P256 curve let private_key = { - let mut rng = rand::thread_rng(); + let mut rng = Rng::new(); SecretKey::random(&mut rng) }; diff --git a/passkey-authenticator/src/u2f/tests.rs b/passkey-authenticator/src/u2f/tests.rs index 62ce27b..bbc788c 100644 --- a/passkey-authenticator/src/u2f/tests.rs +++ b/passkey-authenticator/src/u2f/tests.rs @@ -5,6 +5,7 @@ use p256::{ EncodedPoint, ecdsa::{Signature, VerifyingKey, signature::Verifier}, }; +use passkey_crypto::rng::{Rng, RngBackend}; use passkey_types::{ctap2::Aaguid, *}; #[tokio::test] @@ -16,8 +17,8 @@ async fn test_save_u2f_passkey() { MockUserValidationMethod::verified_user(0), ); - let challenge: [u8; 32] = ::rand::random(); - let application: [u8; 32] = ::rand::random(); + let challenge: [u8; 32] = Rng::random_array(); + let application: [u8; 32] = Rng::random_array(); // Create a U2F request let reg_request = RegisterRequest { @@ -25,7 +26,7 @@ async fn test_save_u2f_passkey() { application, }; - let handle: [u8; 16] = ::rand::random(); + let handle: [u8; 16] = Rng::random_array(); // Register the request and assert that it worked. let store_result = authenticator.register(reg_request, &handle[..]).await; @@ -34,7 +35,7 @@ async fn test_save_u2f_passkey() { let public_key = response.public_key; // Now generate an authentication challenge using the original application - let challenge: [u8; 32] = ::rand::random(); + let challenge: [u8; 32] = Rng::random_array(); let auth_req = AuthenticationRequest { parameter: u2f::AuthenticationParameter::CheckOnly, application, diff --git a/passkey-client/Cargo.toml b/passkey-client/Cargo.toml index 5236660..b6f7670 100644 --- a/passkey-client/Cargo.toml +++ b/passkey-client/Cargo.toml @@ -32,6 +32,7 @@ itertools = "0.14" mockall = { version = "0.11", optional = true } nom = { version = "7", features = ["alloc"], optional = true } passkey-authenticator = { path = "../passkey-authenticator", version = "0.5" } +passkey-crypto = { path = "../passkey-crypto", version = "0.1" } passkey-types = { path = "../passkey-types", version = "0.5" } public-suffix = { path = "../public-suffix", version = "0.1" } reqwest = { version = "0.12", default-features = false, optional = true } diff --git a/passkey-client/src/tests/ext_prf.rs b/passkey-client/src/tests/ext_prf.rs index 0db99b9..04152b0 100644 --- a/passkey-client/src/tests/ext_prf.rs +++ b/passkey-client/src/tests/ext_prf.rs @@ -176,12 +176,12 @@ impl PrfValuesConfig { match self { PrfValuesConfig::None => None, PrfValuesConfig::One => Some(webauthn::AuthenticationExtensionsPrfValues { - first: Bytes::from(random_vec(128)), + first: Bytes::from(Rng::random_vec(128)), second: None, }), PrfValuesConfig::Two => Some(webauthn::AuthenticationExtensionsPrfValues { - first: Bytes::from(random_vec(128)), - second: Some(Bytes::from(random_vec(128))), + first: Bytes::from(Rng::random_vec(128)), + second: Some(Bytes::from(Rng::random_vec(128))), }), } } @@ -316,7 +316,7 @@ async fn auth_empty_allow_credentials() { let origin = Url::parse("https://future.1password.com").unwrap(); let eval_by_cred = webauthn::AuthenticationExtensionsPrfValues { - first: Bytes::from(random_vec(128)), + first: Bytes::from(Rng::random_vec(128)), second: None, }; let options = good_credential_creation_options_with_prf(Some(eval_by_cred.clone())); @@ -369,7 +369,7 @@ macro_rules! invalid_eval_by_credential_in_authentication { let mut client = Client::new(auth); let eval_by_cred = webauthn::AuthenticationExtensionsPrfValues { - first: Bytes::from(random_vec(128)), + first: Bytes::from(Rng::random_vec(128)), second: None, }; @@ -417,7 +417,7 @@ macro_rules! invalid_eval_by_credential_in_authentication { invalid_eval_by_credential_in_authentication! { auth_empty_key_in_eval_by_credential: String::from(""), auth_invalid_base64url_key_in_eval_by_credential: String::from("xyz"), - auth_no_matching_credential_id_in_allow_credentials: String::from(Bytes::from(random_vec(64))) + auth_no_matching_credential_id_in_allow_credentials: String::from(Bytes::from(Rng::random_vec(64))) } #[cfg(test)] @@ -441,8 +441,8 @@ macro_rules! compare_auth_calls { .hmac_secret(HmacSecretConfig::new_without_uv()); let mut client = Client::new(auth); - let mut first = Bytes::from(random_vec(128)); - let mut second = Some(Bytes::from(random_vec(128))); + let mut first = Bytes::from(Rng::random_vec(128)); + let mut second = Some(Bytes::from(Rng::random_vec(128))); let eval_by_cred = webauthn::AuthenticationExtensionsPrfValues { first: first.clone(), @@ -486,8 +486,8 @@ macro_rules! compare_auth_calls { .expect("failed to authenticate with PRF input"); if $same_inputs == SameInputs::No { - first = Bytes::from(random_vec(128)); - second = Some(Bytes::from(random_vec(128))); + first = Bytes::from(Rng::random_vec(128)); + second = Some(Bytes::from(Rng::random_vec(128))); } let auth_options = webauthn::CredentialRequestOptions { @@ -702,8 +702,8 @@ async fn two_eval_by_credential_entries() { let mut client = Client::new(auth); let eval_values = webauthn::AuthenticationExtensionsPrfValues { - first: Bytes::from(random_vec(128)), - second: Some(Bytes::from(random_vec(128))), + first: Bytes::from(Rng::random_vec(128)), + second: Some(Bytes::from(Rng::random_vec(128))), }; let origin = Url::parse("https://future.1password.com").unwrap(); @@ -739,8 +739,8 @@ async fn two_eval_by_credential_entries() { .expect("failed to authenticate with PRF input"); let eval_values_2 = webauthn::AuthenticationExtensionsPrfValues { - first: Bytes::from(random_vec(128)), - second: Some(Bytes::from(random_vec(128))), + first: Bytes::from(Rng::random_vec(128)), + second: Some(Bytes::from(Rng::random_vec(128))), }; let mut cred_id_2 = cred_id.clone(); diff --git a/passkey-client/src/tests/mod.rs b/passkey-client/src/tests/mod.rs index c2c5b10..5059646 100644 --- a/passkey-client/src/tests/mod.rs +++ b/passkey-client/src/tests/mod.rs @@ -3,9 +3,8 @@ use crate::rp_id_verifier::tests::TestFetcher; use super::*; use coset::iana; use passkey_authenticator::{MemoryStore, MockUserValidationMethod, UserCheck}; -use passkey_types::{ - Bytes, ctap2, encoding::try_from_base64url, rand::random_vec, webauthn::CollectedClientData, -}; +use passkey_crypto::rng::{Rng, RngBackend}; +use passkey_types::{Bytes, ctap2, encoding::try_from_base64url, webauthn::CollectedClientData}; use serde::Deserialize; use url::{ParseError, Url}; @@ -18,11 +17,11 @@ fn good_credential_creation_options() -> webauthn::PublicKeyCredentialCreationOp name: "future.1password.com".into(), }, user: webauthn::PublicKeyCredentialUserEntity { - id: random_vec(16).into(), + id: Rng::random_vec(16).into(), display_name: "wendy".into(), name: "wendy".into(), }, - challenge: random_vec(32).into(), + challenge: Rng::random_vec(32).into(), pub_key_cred_params: vec![webauthn::PublicKeyCredentialParameters { ty: webauthn::PublicKeyCredentialType::PublicKey, alg: iana::Algorithm::ES256, @@ -41,7 +40,7 @@ fn good_credential_request_options( credential_id: impl Into, ) -> webauthn::PublicKeyCredentialRequestOptions { webauthn::PublicKeyCredentialRequestOptions { - challenge: random_vec(32).into(), + challenge: Rng::random_vec(32).into(), timeout: None, rp_id: Some("future.1password.com".into()), allow_credentials: Some(vec![webauthn::PublicKeyCredentialDescriptor { @@ -616,7 +615,7 @@ async fn fail_to_create_with_unrelated_origin() { // We can call authenticate without a passkey in the store here // since RP validation is before we fetch anything from the store let auth_options = webauthn::CredentialRequestOptions { - public_key: good_credential_request_options(random_vec(32)), + public_key: good_credential_request_options(Rng::random_vec(32)), }; let res = client .authenticate(&origin, auth_options, DefaultClientData) diff --git a/passkey-types/Cargo.toml b/passkey-types/Cargo.toml index db0c1f9..3de769e 100644 --- a/passkey-types/Cargo.toml +++ b/passkey-types/Cargo.toml @@ -28,7 +28,7 @@ ciborium = "0.2" data-encoding = "2" hmac = "0.12" indexmap = { version = "2", features = ["serde"] } -rand = "0.8" +passkey-crypto = { path = "../passkey-crypto", version = "0.1"} serde = { version = "1", features = ["derive"] } serde_json = { version = "1", features = ["preserve_order"] } sha2 = "0.10" @@ -43,6 +43,3 @@ p256 = { version = "0.13", features = [ "jwk", "pem", ], optional = true } - -[target.'cfg(target_arch = "wasm32")'.dependencies] -getrandom = { version = "0.2", features = ["js"] } diff --git a/passkey-types/src/ctap2/attestation_fmt/test.rs b/passkey-types/src/ctap2/attestation_fmt/test.rs index 1208090..01d80eb 100644 --- a/passkey-types/src/ctap2/attestation_fmt/test.rs +++ b/passkey-types/src/ctap2/attestation_fmt/test.rs @@ -1,8 +1,8 @@ use ciborium::cbor; use coset::CoseKeyBuilder; +use passkey_crypto::rng::{Rng, RngBackend}; use super::*; -use crate::utils::rand::random_vec; #[test] fn deserialize_authenticator_data_with_at_and_ed() { @@ -164,12 +164,12 @@ fn round_trip_deserialization() { let expected = AuthenticatorData::new("future.1password.com", Some(0)) .set_attested_credential_data(AttestedCredentialData { aaguid: Aaguid::new_empty(), - credential_id: random_vec(16), + credential_id: Rng::random_vec(16), key: CoseKeyBuilder::new_ec2_pub_key( coset::iana::EllipticCurve::P_256, // seeing as these are random, it is not a valid key, so don't use this. - random_vec(32), - random_vec(32), + Rng::random_vec(32), + Rng::random_vec(32), ) .algorithm(coset::iana::Algorithm::ES256) .build(), diff --git a/passkey-types/src/ctap2/extensions/hmac_secret/tests.rs b/passkey-types/src/ctap2/extensions/hmac_secret/tests.rs index 839d61c..b0715ee 100644 --- a/passkey-types/src/ctap2/extensions/hmac_secret/tests.rs +++ b/passkey-types/src/ctap2/extensions/hmac_secret/tests.rs @@ -1,7 +1,6 @@ use ciborium::{cbor, value::Value}; use coset::AsCborValue; - -use crate::rand::random_vec; +use passkey_crypto::rng::{Rng, RngBackend}; use super::*; @@ -85,9 +84,9 @@ fn from_64_byte_slice() { #[test] fn from_incorrectly_sized_byte_slice() { - let too_short = random_vec(31); - let between = random_vec(33); - let too_long = random_vec(65); + let too_short = Rng::random_vec(31); + let between = Rng::random_vec(33); + let too_long = Rng::random_vec(65); HmacSecretSaltOrOutput::try_from(too_short.as_slice()) .expect_err("Failed to detect salt1 is too short"); @@ -111,7 +110,7 @@ fn from_incorrectly_sized_byte_slice() { HmacSecretSaltOrOutput::try_new(&between, Some(&too_short)) .expect_err("Failed to detect salt1 is long and salt2 is short"); - let correct = random_vec(32); + let correct = Rng::random_vec(32); HmacSecretSaltOrOutput::try_new(&correct, Some(&too_short)) .expect_err("Failed to detect salt1 is good but salt2 is short"); @@ -127,8 +126,8 @@ fn from_incorrectly_sized_byte_slice() { fn from_correct_cbor() { let key = coset::CoseKeyBuilder::new_ec2_pub_key( coset::iana::EllipticCurve::P_256, - random_vec(32), - random_vec(32), + Rng::random_vec(32), + Rng::random_vec(32), ) .build() .to_cbor_value() @@ -137,14 +136,14 @@ fn from_correct_cbor() { 0x01 => key, 0x02 => Value::Bytes(GOOD_SALT1.to_vec()), // should be a HMAC other salt with the key - 0x03 => Value::Bytes(random_vec(32)) + 0x03 => Value::Bytes(Rng::random_vec(32)) }) .unwrap(); let remote_two_salts = cbor!({ 0x01 => key, 0x02 => Value::Bytes(GOOD_SALT1_AND_2.to_vec()), // should be a HMAC other salt with the key - 0x03 => Value::Bytes(random_vec(32)) + 0x03 => Value::Bytes(Rng::random_vec(32)) }) .unwrap(); @@ -175,8 +174,8 @@ fn from_correct_cbor() { fn cbor_round_trip_one_salt() { let key = coset::CoseKeyBuilder::new_ec2_pub_key( coset::iana::EllipticCurve::P_256, - random_vec(32), - random_vec(32), + Rng::random_vec(32), + Rng::random_vec(32), ) .build() .to_cbor_value() @@ -184,7 +183,7 @@ fn cbor_round_trip_one_salt() { let one_salt = HmacGetSecretInput { key_agreement: key, salt_enc: Bytes::from(GOOD_SALT1.as_slice()), - salt_auth: random_vec(32).into(), + salt_auth: Rng::random_vec(32).into(), pin_uv_auth_protocol: None, }; let mut buf = Vec::with_capacity(128); @@ -214,8 +213,8 @@ fn cbor_round_trip_one_salt() { fn cbor_round_trip_both_salts() { let key = coset::CoseKeyBuilder::new_ec2_pub_key( coset::iana::EllipticCurve::P_256, - random_vec(32), - random_vec(32), + Rng::random_vec(32), + Rng::random_vec(32), ) .build() .to_cbor_value() @@ -223,7 +222,7 @@ fn cbor_round_trip_both_salts() { let one_salt = HmacGetSecretInput { key_agreement: key, salt_enc: Bytes::from(GOOD_SALT1_AND_2.as_slice()), - salt_auth: random_vec(32).into(), + salt_auth: Rng::random_vec(32).into(), pin_uv_auth_protocol: None, }; let mut buf = Vec::with_capacity(128); diff --git a/passkey-types/src/lib.rs b/passkey-types/src/lib.rs index 2fb26f7..857475f 100644 --- a/passkey-types/src/lib.rs +++ b/passkey-types/src/lib.rs @@ -42,7 +42,7 @@ pub use self::{ passkey::{CredentialExtensions, Passkey, StoredHmacSecret}, utils::{ bytes::{Bytes, NotBase64Encoded}, - crypto, encoding, rand, + crypto, encoding, }, }; diff --git a/passkey-types/src/passkey/mock.rs b/passkey-types/src/passkey/mock.rs index a3e8696..f36fd47 100644 --- a/passkey-types/src/passkey/mock.rs +++ b/passkey-types/src/passkey/mock.rs @@ -1,7 +1,8 @@ use coset::{CoseKeyBuilder, iana}; use p256::{SecretKey, ecdsa::SigningKey}; +use passkey_crypto::rng::{Rng, RngBackend}; -use crate::{Passkey, StoredHmacSecret, rand::random_vec}; +use crate::{Passkey, StoredHmacSecret}; /// A builder for the [`Passkey`] type which should be used as a mock for testing. pub struct PasskeyBuilder { @@ -12,7 +13,7 @@ impl PasskeyBuilder { /// Create a new pub(super) fn new(rp_id: String) -> Self { let private_key = { - let mut rng = rand::thread_rng(); + let mut rng = Rng::new(); SecretKey::random(&mut rng) }; @@ -37,7 +38,7 @@ impl PasskeyBuilder { Self { inner: Passkey { key: private, - credential_id: random_vec(16).into(), + credential_id: Rng::random_vec(16).into(), rp_id, user_handle: None, username: None, @@ -50,13 +51,13 @@ impl PasskeyBuilder { /// Regenerate the credential ID with a different size than the default 16 bytes pub fn credential_id(mut self, len: usize) -> Self { - self.inner.credential_id = random_vec(len).into(); + self.inner.credential_id = Rng::random_vec(len).into(); self } /// Generate the user handle with an optional custom size. The default is 16 bytes. pub fn user_handle(mut self, len: Option) -> Self { - self.inner.user_handle = Some(random_vec(len.unwrap_or(16)).into()); + self.inner.user_handle = Some(Rng::random_vec(len.unwrap_or(16)).into()); self } diff --git a/passkey-types/src/utils.rs b/passkey-types/src/utils.rs index c2cc17a..aa5a726 100644 --- a/passkey-types/src/utils.rs +++ b/passkey-types/src/utils.rs @@ -8,4 +8,3 @@ pub(crate) mod serde_workaround; pub mod crypto; pub mod encoding; -pub mod rand; diff --git a/passkey-types/src/utils/rand.rs b/passkey-types/src/utils/rand.rs deleted file mode 100644 index d254f47..0000000 --- a/passkey-types/src/utils/rand.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! Random number generator utilities used for tests - -use rand::RngCore; - -fn random_fill(buffer: &mut [u8]) { - let mut random = rand::thread_rng(); - random.fill_bytes(buffer); -} - -/// Generate random data of specific length. -pub fn random_vec(len: usize) -> Vec { - let mut data = vec![0u8; len]; - random_fill(&mut data); - data -} diff --git a/passkey/Cargo.toml b/passkey/Cargo.toml index 2a2f964..fb70559 100644 --- a/passkey/Cargo.toml +++ b/passkey/Cargo.toml @@ -31,6 +31,7 @@ passkey-authenticator = { path = "../passkey-authenticator", version = "0.5" } passkey-client = { path = "../passkey-client", version = "0.5" } passkey-transports = { path = "../passkey-transports", version = "0.1" } passkey-types = { path = "../passkey-types", version = "0.5" } +passkey-crypto = { path = "../passkey-crypto", version = "0.1" } [dev-dependencies] async-trait = "0.1" @@ -43,6 +44,9 @@ passkey-client = { path = "../passkey-client", version = "0.5", features = [ "testable", "tokio", ] } +passkey-crypto = { path = "../passkey-crypto", version = "0.1", features = [ + "rand" +]} tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] } tokio-test = "0.4" url = "2" diff --git a/passkey/examples/usage.rs b/passkey/examples/usage.rs index 7d14ba0..327c8bd 100644 --- a/passkey/examples/usage.rs +++ b/passkey/examples/usage.rs @@ -2,7 +2,8 @@ use passkey::{ authenticator::{Authenticator, UiHint, UserCheck, UserValidationMethod}, client::{Client, WebauthnError}, - types::{Bytes, Passkey, crypto::sha256, ctap2::*, rand::random_vec, webauthn::*}, + crypto::rng::{Rng, RngBackend}, + types::{Bytes, Passkey, crypto::sha256, ctap2::*, webauthn::*}, }; use coset::iana; @@ -83,7 +84,7 @@ async fn client_setup( // Let's try and authenticate. // Create a challenge that would usually come from the RP. - let challenge_bytes_from_rp: Bytes = random_vec(32).into(); + let challenge_bytes_from_rp: Bytes = Rng::random_vec(32).into(); // Now try and authenticate let credential_request = CredentialRequestOptions { public_key: PublicKeyCredentialRequestOptions { @@ -173,14 +174,14 @@ fn ctap2_other_error(code: StatusCode) { async fn main() -> Result<(), WebauthnError> { let rp_url = Url::parse("https://future.1password.com").expect("Should Parse"); let user_entity = PublicKeyCredentialUserEntity { - id: random_vec(32).into(), + id: Rng::random_vec(32).into(), display_name: "Johnny Passkey".into(), name: "jpasskey@example.org".into(), }; // Set up a client, create and authenticate a credential, then report results. let (created_cred, authed_cred) = client_setup( - random_vec(32).into(), // challenge_bytes_from_rp + Rng::random_vec(32).into(), // challenge_bytes_from_rp PublicKeyCredentialParameters { ty: PublicKeyCredentialType::PublicKey, alg: iana::Algorithm::ES256, diff --git a/passkey/src/lib.rs b/passkey/src/lib.rs index 7ec1cfe..d900a72 100644 --- a/passkey/src/lib.rs +++ b/passkey/src/lib.rs @@ -66,7 +66,7 @@ //! use passkey::{ //! authenticator::{Authenticator, UiHint, UserValidationMethod, UserCheck}, //! client::{Client, DefaultClientData, WebauthnError}, -//! types::{ctap2::*, rand::random_vec, crypto::sha256, webauthn::*, Bytes, Passkey}, +//! types::{ctap2::*, crypto::sha256, webauthn::*, Bytes, Passkey}, //! }; //! //! use coset::iana; @@ -98,14 +98,14 @@ //! //! // Example of how to set up, register and authenticate with a `Client`. //! # tokio_test::block_on(async { -//! let challenge_bytes_from_rp: Bytes = random_vec(32).into(); +//! let challenge_bytes_from_rp: Bytes = Rng::random_vec(32).into(); //! let parameters_from_rp = PublicKeyCredentialParameters { //! ty: PublicKeyCredentialType::PublicKey, //! alg: iana::Algorithm::ES256, //! }; //! let origin = Url::parse("https://future.1password.com").expect("Should parse"); //! let user_entity = PublicKeyCredentialUserEntity { -//! id: random_vec(32).into(), +//! id: Rng::random_vec(32).into(), //! display_name: "Johnny Passkey".into(), //! name: "jpasskey@example.org".into(), //! }; @@ -150,7 +150,7 @@ //! //! // Let's try and authenticate. //! // Create a challenge that would usually come from the RP. -//! let challenge_bytes_from_rp: Bytes = random_vec(32).into(); +//! let challenge_bytes_from_rp: Bytes = Rng::random_vec(32).into(); //! // Now try and authenticate //! let credential_request = CredentialRequestOptions { //! public_key: PublicKeyCredentialRequestOptions { @@ -180,7 +180,7 @@ //! # use passkey::{ //! # authenticator::{Authenticator, UiHint, UserValidationMethod, UserCheck}, //! # client::{Client, WebauthnError}, -//! # types::{ctap2::*, rand::random_vec, crypto::sha256, webauthn::*, Bytes, Passkey}, +//! # types::{ctap2::*, crypto::sha256, webauthn::*, Bytes, Passkey}, //! # }; //! # //! # use coset::iana; @@ -213,9 +213,9 @@ //! # tokio_test::block_on(async { //! // Note: this isn't really how you generate `client_data_hash` but it simplifies the example. //! // See usage.rs for actual technique. -//! let client_data_hash: Bytes = random_vec(32).into(); +//! let client_data_hash: Bytes = Rng::random_vec(32).into(); //! let user_entity = PublicKeyCredentialUserEntity { -//! id: random_vec(32).into(), +//! id: Rng::random_vec(32).into(), //! display_name: "Johnny Passkey".into(), //! name: "jpasskey@example.org".into(), //! }; @@ -265,5 +265,6 @@ pub use passkey_authenticator as authenticator; pub use passkey_client as client; +pub use passkey_crypto as crypto; pub use passkey_transports as transports; pub use passkey_types as types; From 1abd7ebc3191be6fa6bfdd3d01d1ee47f1397fc3 Mon Sep 17 00:00:00 2001 From: Rene Leveille Date: Mon, 19 Jan 2026 12:52:19 -0500 Subject: [PATCH 05/10] Changelog the new rngBackend --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a4054a..8fe6d86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ ## Unreleased +### passkey-crypto v0.1.0 + +A new crate! This crate houses the swappable cryptographic backends for different libraries should you +wish/need to use a different set of libraries than the default RustCrypto libraries. As always PRs are +accepted to add new backends should you wish to not use plenty of newtypes to get around the orphan +rules. + +- New `RngBackend` trait which replaces the pre-existing `passkey-types::rand::random_vec` function. + Use this new method as `passkey-crypto::rng::Rng::random_vec`. + +### passkey-types + +- ⚠ BREAKING: The `passkey-types::rand` module no longer exists and is instead replaced by `passkey-crypto::rng`. + ## Passkey v0.5.0 - Migrate project to Rust 2024 edition From 700ea7d7c5a407f05e6c72e18d3f7480334f61c6 Mon Sep 17 00:00:00 2001 From: Rene Leveille Date: Mon, 19 Jan 2026 16:19:15 -0500 Subject: [PATCH 06/10] Add RustCrypto cryptographic backend started a trait tree with associated types for all of the keys and signatures --- passkey-crypto/Cargo.toml | 9 +++++++- passkey-crypto/src/lib.rs | 29 +++++++++++++++++++++++ passkey-crypto/src/rust_crypto/mod.rs | 33 +++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 passkey-crypto/src/rust_crypto/mod.rs diff --git a/passkey-crypto/Cargo.toml b/passkey-crypto/Cargo.toml index e9e284a..b8b7fbc 100644 --- a/passkey-crypto/Cargo.toml +++ b/passkey-crypto/Cargo.toml @@ -13,11 +13,18 @@ rust-version.workspace = true version = "0.1.0" [features] -default = ["rand"] +default = ["rust-crypto"] rand = ["dep:rand", "dep:getrandom"] js = ["getrandom/js"] +rust-crypto = ["dep:p256", "rand"] [dependencies] +coset.workspace = true +p256 = { version = "0.13", optional = true, features = [ + "arithmetic", + "jwk", + "pem" +] } rand = { version = "0.8", optional = true , features = ["min_const_gen"]} [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/passkey-crypto/src/lib.rs b/passkey-crypto/src/lib.rs index 0ab5a0e..3c98216 100644 --- a/passkey-crypto/src/lib.rs +++ b/passkey-crypto/src/lib.rs @@ -1,7 +1,36 @@ //! +use coset::{CoseKey, iana}; + pub mod rng; +#[cfg(feature = "rust-crypto")] +pub mod rust_crypto; + pub trait CryptoBackend { type Rng: rng::RngBackend; + type SecretKey: SecretKeyT; + + fn generate_key(&self, algorithm: iana::Algorithm) -> Result; + // fn parse_private_cose_key(cose_key: CoseKey) -> Self::PrivateKey; +} + +pub trait SecretKeyT { + type Signature: SignatureT; + type PublicKey: PublicKeyT; + // fn sign(&mut self, target: &[u8]) -> Self::Signature; + // fn public_key(&self) -> Self::PublicKey; + // fn to_cose_key(&self) -> CoseKey; } + +pub trait PublicKeyT { + type Signature: SignatureT; + // fn verify(&self, target: &[u8], signature: &Self::Signature) -> Result<(), Error>; + // fn to_cose_key(&self) -> CoseKey; +} + +pub trait SignatureT { + // fn to_bytes(&self) -> Vec; +} + +pub type Error = Box; diff --git a/passkey-crypto/src/rust_crypto/mod.rs b/passkey-crypto/src/rust_crypto/mod.rs new file mode 100644 index 0000000..4a316c7 --- /dev/null +++ b/passkey-crypto/src/rust_crypto/mod.rs @@ -0,0 +1,33 @@ +use coset::iana; + +use crate::{CryptoBackend, PublicKeyT, SecretKeyT, SignatureT}; + +pub struct RustCryptoBackend {} + +impl CryptoBackend for RustCryptoBackend { + type Rng = ::rand::rngs::ThreadRng; + + type SecretKey = p256::SecretKey; + + fn generate_key(&self, algorithm: iana::Algorithm) -> Result { + let mut rng = ::rand::thread_rng(); + match algorithm { + iana::Algorithm::ES256 | iana::Algorithm::PS256 => { + Ok(p256::SecretKey::random(&mut rng)) + } + _ => Err("Algorithm is unsupported".to_string().into()), + } + } +} + +impl SecretKeyT for p256::SecretKey { + type Signature = p256::ecdsa::Signature; + + type PublicKey = p256::PublicKey; +} + +impl PublicKeyT for p256::PublicKey { + type Signature = p256::ecdsa::Signature; +} + +impl SignatureT for p256::ecdsa::Signature {} From be119f826a70a204a199bf243b7c0ea7bb85cf24 Mon Sep 17 00:00:00 2001 From: Rene Leveille Date: Mon, 19 Jan 2026 16:44:13 -0500 Subject: [PATCH 07/10] move cose encoding of the private key to the privatekey trait --- .../extensions/hmac_secret/tests.rs | 7 ++-- .../src/authenticator/get_assertion/tests.rs | 3 +- passkey-authenticator/src/tests.rs | 29 +++++++-------- passkey-crypto/src/lib.rs | 2 +- passkey-crypto/src/rust_crypto/mod.rs | 19 +++++++++- passkey-types/src/passkey.rs | 4 +- passkey-types/src/passkey/mock.rs | 37 ++++++------------- 7 files changed, 50 insertions(+), 51 deletions(-) diff --git a/passkey-authenticator/src/authenticator/extensions/hmac_secret/tests.rs b/passkey-authenticator/src/authenticator/extensions/hmac_secret/tests.rs index 5144a87..0334001 100644 --- a/passkey-authenticator/src/authenticator/extensions/hmac_secret/tests.rs +++ b/passkey-authenticator/src/authenticator/extensions/hmac_secret/tests.rs @@ -1,3 +1,4 @@ +use passkey_crypto::rust_crypto::RustCryptoBackend; use passkey_types::{Passkey, ctap2::Aaguid}; use crate::{Authenticator, MockUserValidationMethod}; @@ -27,7 +28,7 @@ fn hmac_secret_cycle_works() { .expect("There should be passkey extensions"); assert!(ext.cred_without_uv.is_some()); - let passkey = Passkey::mock("sneakernetsend.com".into()) + let passkey = Passkey::mock("sneakernetsend.com".into(), RustCryptoBackend) .hmac_secret(ext) .build(); @@ -106,7 +107,7 @@ fn hmac_secret_cycle_works_with_one_cred() { .expect("There should be passkey extensions"); assert!(ext.cred_without_uv.is_none()); - let passkey = Passkey::mock("sneakernetsend.com".into()) + let passkey = Passkey::mock("sneakernetsend.com".into(), RustCryptoBackend) .hmac_secret(ext) .build(); @@ -163,7 +164,7 @@ fn hmac_secret_cycle_works_with_one_salt() { .expect("There should be passkey extensions"); assert!(ext.cred_without_uv.is_none()); - let passkey = Passkey::mock("sneakernetsend.com".into()) + let passkey = Passkey::mock("sneakernetsend.com".into(), RustCryptoBackend) .hmac_secret(ext) .build(); diff --git a/passkey-authenticator/src/authenticator/get_assertion/tests.rs b/passkey-authenticator/src/authenticator/get_assertion/tests.rs index 90aeaed..63a12a6 100644 --- a/passkey-authenticator/src/authenticator/get_assertion/tests.rs +++ b/passkey-authenticator/src/authenticator/get_assertion/tests.rs @@ -1,4 +1,5 @@ use passkey_crypto::rng::{Rng, RngBackend}; +use passkey_crypto::rust_crypto::RustCryptoBackend; use passkey_types::{ Passkey, StoredHmacSecret, ctap2::{ @@ -14,7 +15,7 @@ use crate::{ }; fn create_passkey(hmac_secret: Option>) -> Passkey { - let builder = Passkey::mock("example.com".into()); + let builder = Passkey::mock("example.com".into(), RustCryptoBackend); if let Some(hs) = hmac_secret { builder.hmac_secret(StoredHmacSecret { diff --git a/passkey-authenticator/src/tests.rs b/passkey-authenticator/src/tests.rs index b824347..9db2acb 100644 --- a/passkey-authenticator/src/tests.rs +++ b/passkey-authenticator/src/tests.rs @@ -1,26 +1,23 @@ use coset::iana; -use p256::{ - SecretKey, - ecdsa::{ - SigningKey, - signature::{Signer, Verifier}, - }, +use p256::ecdsa::{ + SigningKey, + signature::{Signer, Verifier}, +}; +use passkey_crypto::{ + CryptoBackend, SecretKeyT, + rng::{Rng, RngBackend}, + rust_crypto::RustCryptoBackend, }; -use passkey_crypto::rng::{Rng, RngBackend}; use passkey_types::ctap2::AuthenticatorData; -use super::{CoseKeyPair, private_key_from_cose_key}; +use super::private_key_from_cose_key; #[test] fn private_key_cose_round_trip_sanity_check() { - let private_key = { - let mut rng = Rng::new(); - SecretKey::random(&mut rng) - }; - let CoseKeyPair { - private: private_cose, - .. - } = CoseKeyPair::from_secret_key(&private_key, iana::Algorithm::ES256); + let private_key = RustCryptoBackend + .generate_key(iana::Algorithm::ES256) + .expect("Backend does not support ES256"); + let private_cose = private_key.to_cose_key(); let public_signing_key = SigningKey::from(&private_key); let public_key = public_signing_key.verifying_key(); diff --git a/passkey-crypto/src/lib.rs b/passkey-crypto/src/lib.rs index 3c98216..aca4e1a 100644 --- a/passkey-crypto/src/lib.rs +++ b/passkey-crypto/src/lib.rs @@ -20,7 +20,7 @@ pub trait SecretKeyT { type PublicKey: PublicKeyT; // fn sign(&mut self, target: &[u8]) -> Self::Signature; // fn public_key(&self) -> Self::PublicKey; - // fn to_cose_key(&self) -> CoseKey; + fn to_cose_key(&self) -> CoseKey; } pub trait PublicKeyT { diff --git a/passkey-crypto/src/rust_crypto/mod.rs b/passkey-crypto/src/rust_crypto/mod.rs index 4a316c7..5683928 100644 --- a/passkey-crypto/src/rust_crypto/mod.rs +++ b/passkey-crypto/src/rust_crypto/mod.rs @@ -1,8 +1,8 @@ -use coset::iana; +use coset::{CoseKey, CoseKeyBuilder, iana}; use crate::{CryptoBackend, PublicKeyT, SecretKeyT, SignatureT}; -pub struct RustCryptoBackend {} +pub struct RustCryptoBackend; impl CryptoBackend for RustCryptoBackend { type Rng = ::rand::rngs::ThreadRng; @@ -24,6 +24,21 @@ impl SecretKeyT for p256::SecretKey { type Signature = p256::ecdsa::Signature; type PublicKey = p256::PublicKey; + + fn to_cose_key(&self) -> CoseKey { + let public_key = p256::ecdsa::SigningKey::from(self) + .verifying_key() + .to_encoded_point(false); + // SAFETY: These unwraps are safe because the public_key above is not compressed (false + // parameter) therefore x and y are guaranteed to contain values. + #[allow(deprecated)] + let x = public_key.x().unwrap().as_slice().to_vec(); + #[allow(deprecated)] + let y = public_key.y().unwrap().as_slice().to_vec(); + CoseKeyBuilder::new_ec2_priv_key(iana::EllipticCurve::P_256, x, y, self.to_bytes().to_vec()) + .algorithm(iana::Algorithm::ES256) + .build() + } } impl PublicKeyT for p256::PublicKey { diff --git a/passkey-types/src/passkey.rs b/passkey-types/src/passkey.rs index e50aa11..82e6126 100644 --- a/passkey-types/src/passkey.rs +++ b/passkey-types/src/passkey.rs @@ -186,8 +186,8 @@ impl Passkey { /// The default credential Id length is 16, change it with the [`PasskeyBuilder::credential_id`] /// method. #[cfg(feature = "testable")] - pub fn mock(rp_id: String) -> PasskeyBuilder { - PasskeyBuilder::new(rp_id) + pub fn mock(rp_id: String, crypto: C) -> PasskeyBuilder { + PasskeyBuilder::new(rp_id, crypto) } } diff --git a/passkey-types/src/passkey/mock.rs b/passkey-types/src/passkey/mock.rs index f36fd47..a1aeba3 100644 --- a/passkey-types/src/passkey/mock.rs +++ b/passkey-types/src/passkey/mock.rs @@ -1,6 +1,8 @@ -use coset::{CoseKeyBuilder, iana}; -use p256::{SecretKey, ecdsa::SigningKey}; -use passkey_crypto::rng::{Rng, RngBackend}; +use coset::iana; +use passkey_crypto::{ + CryptoBackend, SecretKeyT, + rng::{Rng, RngBackend}, +}; use crate::{Passkey, StoredHmacSecret}; @@ -11,30 +13,13 @@ pub struct PasskeyBuilder { impl PasskeyBuilder { /// Create a new - pub(super) fn new(rp_id: String) -> Self { - let private_key = { - let mut rng = Rng::new(); - SecretKey::random(&mut rng) - }; - - let public_key = SigningKey::from(&private_key) - .verifying_key() - .to_encoded_point(false); - // SAFETY: These unwraps are safe because the public_key above is not compressed (false - // parameter) therefore x and y are guaranteed to contain values. - #[allow(deprecated)] - let x = public_key.x().unwrap().as_slice().to_vec(); - #[allow(deprecated)] - let y = public_key.y().unwrap().as_slice().to_vec(); - let private = CoseKeyBuilder::new_ec2_priv_key( - iana::EllipticCurve::P_256, - x, - y, - private_key.to_bytes().to_vec(), - ) - .algorithm(iana::Algorithm::ES256) - .build(); + pub(super) fn new(rp_id: String, crypto: C) -> Self { + // This expect is safe since this is test code and should never be used in production. + let private_key = crypto + .generate_key(iana::Algorithm::ES256) + .expect("The crypto backend does not support ES256"); + let private = private_key.to_cose_key(); Self { inner: Passkey { key: private, From 0e90306183ae176e9413294fe71a90b386d72b73 Mon Sep 17 00:00:00 2001 From: Rene Leveille Date: Mon, 19 Jan 2026 17:32:56 -0500 Subject: [PATCH 08/10] move public key encoding into rust crypto backend --- .../src/authenticator/make_credential.rs | 2 +- passkey-authenticator/src/lib.rs | 27 +++++-------------- passkey-authenticator/src/u2f.rs | 4 +-- passkey-crypto/src/lib.rs | 4 +-- passkey-crypto/src/rust_crypto/mod.rs | 18 +++++++++++++ 5 files changed, 29 insertions(+), 26 deletions(-) diff --git a/passkey-authenticator/src/authenticator/make_credential.rs b/passkey-authenticator/src/authenticator/make_credential.rs index 0072764..4130f23 100644 --- a/passkey-authenticator/src/authenticator/make_credential.rs +++ b/passkey-authenticator/src/authenticator/make_credential.rs @@ -124,7 +124,7 @@ where // Encoding of the key pair into their CoseKey representation before moving the private CoseKey // into the passkey. Keeping the public key ready for step 11 below and returning the attested // credential. - let CoseKeyPair { public, private } = CoseKeyPair::from_secret_key(&private_key, algorithm); + let CoseKeyPair { public, private } = CoseKeyPair::from_secret_key(&private_key); let store_info = self.store.get_info().await; diff --git a/passkey-authenticator/src/lib.rs b/passkey-authenticator/src/lib.rs index a9381e6..ccf1078 100644 --- a/passkey-authenticator/src/lib.rs +++ b/passkey-authenticator/src/lib.rs @@ -40,6 +40,7 @@ use p256::{ elliptic_curve::{generic_array::GenericArray, sec1::FromEncodedPoint}, pkcs8::EncodePublicKey, }; +use passkey_crypto::PublicKeyT; use passkey_types::{Bytes, ctap2::Ctap2Error}; pub use self::{ @@ -155,27 +156,13 @@ pub struct CoseKeyPair { impl CoseKeyPair { /// Create a new COSE key pair from a secret key and algorithm. - pub fn from_secret_key(private_key: &SecretKey, algorithm: Algorithm) -> Self { - let public_key = SigningKey::from(private_key) - .verifying_key() - .to_encoded_point(false); - // SAFETY: These unwraps are safe because the public_key above is not compressed (false - // parameter) therefore x and y are guarateed to contain values. - let x = public_key.x().unwrap().as_slice().to_vec(); - let y = public_key.y().unwrap().as_slice().to_vec(); - let private = CoseKeyBuilder::new_ec2_priv_key( - iana::EllipticCurve::P_256, - x.clone(), - y.clone(), - private_key.to_bytes().to_vec(), - ) - .algorithm(algorithm) - .build(); - let public = CoseKeyBuilder::new_ec2_pub_key(iana::EllipticCurve::P_256, x, y) - .algorithm(algorithm) - .build(); + pub fn from_secret_key(private_key: &SK) -> Self { + let public_key = private_key.public_key(); - Self { public, private } + Self { + public: public_key.to_cose_key(), + private: private_key.to_cose_key(), + } } } diff --git a/passkey-authenticator/src/u2f.rs b/passkey-authenticator/src/u2f.rs index 0f717e8..2f3a807 100644 --- a/passkey-authenticator/src/u2f.rs +++ b/passkey-authenticator/src/u2f.rs @@ -3,7 +3,6 @@ use crate::{ Authenticator, CoseKeyPair, CredentialStore, UserValidationMethod, passkey::PasskeyAccessor, }; -use coset::iana; use p256::{ SecretKey, ecdsa::{SigningKey, signature::Signer}, @@ -59,8 +58,7 @@ impl U2 }; // SAFETY: Can only fail if key is malformed - let CoseKeyPair { public: _, private } = - CoseKeyPair::from_secret_key(&private_key, iana::Algorithm::ES256); + let CoseKeyPair { public: _, private } = CoseKeyPair::from_secret_key(&private_key); let signing_key = SigningKey::from(private_key); let public_key = signing_key.verifying_key(); let pub_key_encoded = public_key.to_encoded_point(false); diff --git a/passkey-crypto/src/lib.rs b/passkey-crypto/src/lib.rs index aca4e1a..1aea3fc 100644 --- a/passkey-crypto/src/lib.rs +++ b/passkey-crypto/src/lib.rs @@ -19,14 +19,14 @@ pub trait SecretKeyT { type Signature: SignatureT; type PublicKey: PublicKeyT; // fn sign(&mut self, target: &[u8]) -> Self::Signature; - // fn public_key(&self) -> Self::PublicKey; + fn public_key(&self) -> Self::PublicKey; fn to_cose_key(&self) -> CoseKey; } pub trait PublicKeyT { type Signature: SignatureT; // fn verify(&self, target: &[u8], signature: &Self::Signature) -> Result<(), Error>; - // fn to_cose_key(&self) -> CoseKey; + fn to_cose_key(&self) -> CoseKey; } pub trait SignatureT { diff --git a/passkey-crypto/src/rust_crypto/mod.rs b/passkey-crypto/src/rust_crypto/mod.rs index 5683928..981841f 100644 --- a/passkey-crypto/src/rust_crypto/mod.rs +++ b/passkey-crypto/src/rust_crypto/mod.rs @@ -39,10 +39,28 @@ impl SecretKeyT for p256::SecretKey { .algorithm(iana::Algorithm::ES256) .build() } + + fn public_key(&self) -> Self::PublicKey { + self.public_key() + } } impl PublicKeyT for p256::PublicKey { type Signature = p256::ecdsa::Signature; + + fn to_cose_key(&self) -> CoseKey { + let encoded_public_key = p256::ecdsa::VerifyingKey::from(self).to_encoded_point(false); + + // SAFETY: These unwraps are safe because the public_key above is not compressed (false + // parameter) therefore x and y are guarateed to contain values. + #[allow(deprecated)] + let x = encoded_public_key.x().unwrap().as_slice().to_vec(); + #[allow(deprecated)] + let y = encoded_public_key.y().unwrap().as_slice().to_vec(); + CoseKeyBuilder::new_ec2_pub_key(iana::EllipticCurve::P_256, x, y) + .algorithm(iana::Algorithm::ES256) + .build() + } } impl SignatureT for p256::ecdsa::Signature {} From e59e2ecc72fb569782cfd81571157c6493a5ce8d Mon Sep 17 00:00:00 2001 From: Rene Leveille Date: Mon, 19 Jan 2026 17:59:53 -0500 Subject: [PATCH 09/10] Make Authenticator store its cryptographic backend --- passkey-authenticator/src/authenticator.rs | 13 +- .../src/authenticator/extensions.rs | 2 +- .../authenticator/extensions/hmac_secret.rs | 2 +- .../extensions/hmac_secret/tests.rs | 27 +++- .../src/authenticator/get_assertion.rs | 4 +- .../src/authenticator/get_assertion/tests.rs | 48 +++++-- .../src/authenticator/get_info.rs | 8 +- .../src/authenticator/make_credential.rs | 8 +- .../authenticator/make_credential/tests.rs | 132 +++++++++++++----- .../src/authenticator/tests.rs | 23 +-- passkey-authenticator/src/ctap2.rs | 11 +- passkey-authenticator/src/lib.rs | 3 +- passkey-authenticator/src/u2f.rs | 23 ++- passkey-authenticator/src/u2f/tests.rs | 6 +- passkey-client/src/extensions.rs | 4 +- passkey-client/src/lib.rs | 20 +-- passkey-client/src/tests/ext_prf.rs | 30 +++- passkey-client/src/tests/mod.rs | 15 +- passkey/examples/usage.rs | 11 +- passkey/src/lib.rs | 22 ++- 20 files changed, 309 insertions(+), 103 deletions(-) diff --git a/passkey-authenticator/src/authenticator.rs b/passkey-authenticator/src/authenticator.rs index 22cc1ee..25f907f 100644 --- a/passkey-authenticator/src/authenticator.rs +++ b/passkey-authenticator/src/authenticator.rs @@ -1,5 +1,5 @@ use coset::iana; -use passkey_crypto::rng::RngBackend; +use passkey_crypto::{CryptoBackend, rng::RngBackend}; use passkey_types::{ ctap2::{Aaguid, Ctap2Error, Flags}, webauthn, @@ -68,7 +68,7 @@ impl From for usize { } /// A virtual authenticator with all the necessary state and information. -pub struct Authenticator { +pub struct Authenticator { /// The authenticator's AAGUID aaguid: Aaguid, /// Provides credential storage capabilities @@ -95,15 +95,19 @@ pub struct Authenticator { /// Supported authenticator extensions extensions: Extensions, + + /// The cryptographic backend of the Authenticator + crypto: C, } -impl Authenticator +impl Authenticator where S: CredentialStore, U: UserValidationMethod, + C: CryptoBackend, { /// Create an authenticator with a known aaguid, a backing storage and a User verification system. - pub fn new(aaguid: Aaguid, store: S, user: U) -> Self { + pub fn new(aaguid: Aaguid, store: S, user: U, crypto: C) -> Self { Self { aaguid, store, @@ -117,6 +121,7 @@ where make_credentials_with_signature_counter: false, credential_id_length: CredentialIdLength::default(), extensions: Extensions::default(), + crypto, } } diff --git a/passkey-authenticator/src/authenticator/extensions.rs b/passkey-authenticator/src/authenticator/extensions.rs index 9d8edea..331d6c7 100644 --- a/passkey-authenticator/src/authenticator/extensions.rs +++ b/passkey-authenticator/src/authenticator/extensions.rs @@ -54,7 +54,7 @@ pub(super) struct GetExtensionOutputs { pub unsigned: Option, } -impl Authenticator { +impl Authenticator { pub(super) fn make_extensions( &self, request: Option, diff --git a/passkey-authenticator/src/authenticator/extensions/hmac_secret.rs b/passkey-authenticator/src/authenticator/extensions/hmac_secret.rs index 81cbd74..0313a9d 100644 --- a/passkey-authenticator/src/authenticator/extensions/hmac_secret.rs +++ b/passkey-authenticator/src/authenticator/extensions/hmac_secret.rs @@ -81,7 +81,7 @@ impl HmacSecretCredentialSupport { } } -impl Authenticator { +impl Authenticator { pub(super) fn make_hmac_secret( &self, hmac_secret_request: Option, diff --git a/passkey-authenticator/src/authenticator/extensions/hmac_secret/tests.rs b/passkey-authenticator/src/authenticator/extensions/hmac_secret/tests.rs index 0334001..5b99853 100644 --- a/passkey-authenticator/src/authenticator/extensions/hmac_secret/tests.rs +++ b/passkey-authenticator/src/authenticator/extensions/hmac_secret/tests.rs @@ -20,8 +20,13 @@ pub(crate) fn prf_eval_request(eval: Option>) -> AuthenticatorPrfInputs #[test] fn hmac_secret_cycle_works() { - let auth = Authenticator::new(Aaguid::new_empty(), None, MockUserValidationMethod::new()) - .hmac_secret(HmacSecretConfig::new_without_uv()); + let auth = Authenticator::new( + Aaguid::new_empty(), + None, + MockUserValidationMethod::new(), + RustCryptoBackend, + ) + .hmac_secret(HmacSecretConfig::new_without_uv()); let ext = auth .make_hmac_secret(Some(true)) @@ -99,8 +104,13 @@ fn hmac_secret_cycle_works() { #[test] fn hmac_secret_cycle_works_with_one_cred() { - let auth = Authenticator::new(Aaguid::new_empty(), None, MockUserValidationMethod::new()) - .hmac_secret(HmacSecretConfig::new_with_uv_only()); + let auth = Authenticator::new( + Aaguid::new_empty(), + None, + MockUserValidationMethod::new(), + RustCryptoBackend, + ) + .hmac_secret(HmacSecretConfig::new_with_uv_only()); let ext = auth .make_hmac_secret(Some(true)) @@ -156,8 +166,13 @@ fn hmac_secret_cycle_works_with_one_cred() { #[test] fn hmac_secret_cycle_works_with_one_salt() { - let auth = Authenticator::new(Aaguid::new_empty(), None, MockUserValidationMethod::new()) - .hmac_secret(HmacSecretConfig::new_with_uv_only()); + let auth = Authenticator::new( + Aaguid::new_empty(), + None, + MockUserValidationMethod::new(), + RustCryptoBackend, + ) + .hmac_secret(HmacSecretConfig::new_with_uv_only()); let ext = auth .make_hmac_secret(Some(true)) diff --git a/passkey-authenticator/src/authenticator/get_assertion.rs b/passkey-authenticator/src/authenticator/get_assertion.rs index b3f51bd..b2307ad 100644 --- a/passkey-authenticator/src/authenticator/get_assertion.rs +++ b/passkey-authenticator/src/authenticator/get_assertion.rs @@ -1,4 +1,5 @@ use p256::ecdsa::{SigningKey, signature::SignerMut}; +use passkey_crypto::CryptoBackend; use passkey_types::{ Bytes, ctap2::{ @@ -15,10 +16,11 @@ use crate::{ user_validation::UiHint, }; -impl Authenticator +impl Authenticator where S: CredentialStore + Sync, U: UserValidationMethod::PasskeyItem> + Sync, + C: CryptoBackend, { /// This method is used by a host to request cryptographic proof of user authentication as well /// as user consent to a given transaction, using a previously generated credential that is diff --git a/passkey-authenticator/src/authenticator/get_assertion/tests.rs b/passkey-authenticator/src/authenticator/get_assertion/tests.rs index 63a12a6..ee13761 100644 --- a/passkey-authenticator/src/authenticator/get_assertion/tests.rs +++ b/passkey-authenticator/src/authenticator/get_assertion/tests.rs @@ -53,6 +53,7 @@ async fn get_assertion_returns_no_credentials_found() { Aaguid::new_empty(), store, MockUserValidationMethod::verified_user_with_hint(1, MockUiHint::InformNoCredentialsFound), + RustCryptoBackend, ); // Act @@ -78,6 +79,7 @@ async fn get_assertion_increments_signature_counter_when_counter_is_some() { 1, MockUiHint::RequestExistingCredential(passkey), ), + RustCryptoBackend, ); // Act @@ -100,8 +102,12 @@ async fn unsupported_extension_with_request_gives_no_ext_output() { let shared_store = Some(create_passkey(None)); let user_mock = MockUserValidationMethod::verified_user(1); - let mut authenticator = - Authenticator::new(Aaguid::new_empty(), shared_store.clone(), user_mock); + let mut authenticator = Authenticator::new( + Aaguid::new_empty(), + shared_store.clone(), + user_mock, + RustCryptoBackend, + ); let request = Request { extensions: Some(ExtensionInputs { @@ -125,8 +131,12 @@ async fn unsupported_extension_with_empty_request_gives_no_ext_output() { let shared_store = Some(create_passkey(None)); let user_mock = MockUserValidationMethod::verified_user(1); - let mut authenticator = - Authenticator::new(Aaguid::new_empty(), shared_store.clone(), user_mock); + let mut authenticator = Authenticator::new( + Aaguid::new_empty(), + shared_store.clone(), + user_mock, + RustCryptoBackend, + ); let request = Request { extensions: Some(ExtensionInputs::default()), @@ -147,9 +157,13 @@ async fn supported_extension_with_empty_request_gives_no_ext_output() { let shared_store = Some(create_passkey(Some(Rng::random_vec(32)))); let user_mock = MockUserValidationMethod::verified_user(1); - let mut authenticator = - Authenticator::new(Aaguid::new_empty(), shared_store.clone(), user_mock) - .hmac_secret(extensions::HmacSecretConfig::new_with_uv_only()); + let mut authenticator = Authenticator::new( + Aaguid::new_empty(), + shared_store.clone(), + user_mock, + RustCryptoBackend, + ) + .hmac_secret(extensions::HmacSecretConfig::new_with_uv_only()); let request = Request { extensions: Some(ExtensionInputs::default()), @@ -170,9 +184,13 @@ async fn supported_extension_without_extension_request_gives_no_ext_output() { let shared_store = Some(create_passkey(Some(Rng::random_vec(32)))); let user_mock = MockUserValidationMethod::verified_user(1); - let mut authenticator = - Authenticator::new(Aaguid::new_empty(), shared_store.clone(), user_mock) - .hmac_secret(extensions::HmacSecretConfig::new_with_uv_only()); + let mut authenticator = Authenticator::new( + Aaguid::new_empty(), + shared_store.clone(), + user_mock, + RustCryptoBackend, + ) + .hmac_secret(extensions::HmacSecretConfig::new_with_uv_only()); let request = good_request(); @@ -190,9 +208,13 @@ async fn supported_extension_with_request_gives_output() { let shared_store = Some(create_passkey(Some(Rng::random_vec(32)))); let user_mock = MockUserValidationMethod::verified_user(1); - let mut authenticator = - Authenticator::new(Aaguid::new_empty(), shared_store.clone(), user_mock) - .hmac_secret(extensions::HmacSecretConfig::new_with_uv_only()); + let mut authenticator = Authenticator::new( + Aaguid::new_empty(), + shared_store.clone(), + user_mock, + RustCryptoBackend, + ) + .hmac_secret(extensions::HmacSecretConfig::new_with_uv_only()); let request = Request { extensions: Some(ExtensionInputs { diff --git a/passkey-authenticator/src/authenticator/get_info.rs b/passkey-authenticator/src/authenticator/get_info.rs index f893762..87de8ed 100644 --- a/passkey-authenticator/src/authenticator/get_info.rs +++ b/passkey-authenticator/src/authenticator/get_info.rs @@ -1,3 +1,4 @@ +use passkey_crypto::CryptoBackend; use passkey_types::{ ctap2::get_info::{Options, Response, Version}, webauthn::PublicKeyCredentialParameters, @@ -7,7 +8,12 @@ use crate::{ Authenticator, CredentialStore, UserValidationMethod, credential_store::DiscoverabilitySupport, }; -impl Authenticator { +impl Authenticator +where + S: CredentialStore, + U: UserValidationMethod, + C: CryptoBackend, +{ /// 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 async fn get_info(&self) -> Box { diff --git a/passkey-authenticator/src/authenticator/make_credential.rs b/passkey-authenticator/src/authenticator/make_credential.rs index 4130f23..9c28d15 100644 --- a/passkey-authenticator/src/authenticator/make_credential.rs +++ b/passkey-authenticator/src/authenticator/make_credential.rs @@ -1,5 +1,8 @@ use p256::SecretKey; -use passkey_crypto::rng::{Rng, RngBackend}; +use passkey_crypto::{ + CryptoBackend, + rng::{Rng, RngBackend}, +}; use passkey_types::{ Passkey, ctap2::{ @@ -10,10 +13,11 @@ use passkey_types::{ use crate::{Authenticator, CoseKeyPair, CredentialStore, UiHint, UserValidationMethod}; -impl Authenticator +impl Authenticator where S: CredentialStore + Sync, U: UserValidationMethod::PasskeyItem> + Sync, + C: CryptoBackend, { /// This method is invoked by the host to request generation of a new credential in the authenticator. pub async fn make_credential(&mut self, input: Request) -> Result { diff --git a/passkey-authenticator/src/authenticator/make_credential/tests.rs b/passkey-authenticator/src/authenticator/make_credential/tests.rs index b2d0229..4d8a67f 100644 --- a/passkey-authenticator/src/authenticator/make_credential/tests.rs +++ b/passkey-authenticator/src/authenticator/make_credential/tests.rs @@ -1,7 +1,10 @@ use std::sync::Arc; use coset::iana; -use passkey_crypto::rng::{Rng, RngBackend}; +use passkey_crypto::{ + rng::{Rng, RngBackend}, + rust_crypto::RustCryptoBackend, +}; use passkey_types::{ Bytes, ctap2::{ @@ -62,8 +65,12 @@ async fn assert_storage_on_success() { MockUiHint::RequestNewCredential(request.user.clone().into(), request.rp.clone()), ); - let mut authenticator = - Authenticator::new(Aaguid::new_empty(), shared_store.clone(), user_mock); + let mut authenticator = Authenticator::new( + Aaguid::new_empty(), + shared_store.clone(), + user_mock, + RustCryptoBackend, + ); authenticator .make_credential(request) @@ -122,8 +129,12 @@ async fn assert_excluded_credentials() { shared_store.lock().await.insert(cred_id.into(), passkey); - let mut authenticator = - Authenticator::new(Aaguid::new_empty(), shared_store.clone(), user_mock); + let mut authenticator = Authenticator::new( + Aaguid::new_empty(), + shared_store.clone(), + user_mock, + RustCryptoBackend, + ); authenticator .make_credential(response) @@ -138,7 +149,12 @@ async fn assert_excluded_credentials() { #[tokio::test] async fn assert_unsupported_algorithm() { let user_mock = MockUserValidationMethod::verified_user(0); - let mut authenticator = Authenticator::new(Aaguid::new_empty(), MemoryStore::new(), user_mock); + let mut authenticator = Authenticator::new( + Aaguid::new_empty(), + MemoryStore::new(), + user_mock, + RustCryptoBackend, + ); let request = Request { pub_key_cred_params: vec![webauthn::PublicKeyCredentialParameters { @@ -162,8 +178,12 @@ async fn make_credential_counter_is_some_0_when_counters_are_enabled() { let shared_store = Arc::new(Mutex::new(None)); let user_mock = MockUserValidationMethod::verified_user(1); let request = good_request(); - let mut authenticator = - Authenticator::new(Aaguid::new_empty(), shared_store.clone(), user_mock); + let mut authenticator = Authenticator::new( + Aaguid::new_empty(), + shared_store.clone(), + user_mock, + RustCryptoBackend, + ); authenticator.set_make_credentials_with_signature_counter(true); // Act @@ -179,8 +199,12 @@ async fn unsupported_extension_with_request_gives_no_ext_output() { let shared_store = Arc::new(Mutex::new(MemoryStore::new())); let user_mock = MockUserValidationMethod::verified_user(1); - let mut authenticator = - Authenticator::new(Aaguid::new_empty(), shared_store.clone(), user_mock); + let mut authenticator = Authenticator::new( + Aaguid::new_empty(), + shared_store.clone(), + user_mock, + RustCryptoBackend, + ); let request = Request { extensions: Some(ExtensionInputs { @@ -206,8 +230,12 @@ async fn unsupported_extension_with_request_gives_no_ext_output() { async fn unsupported_extension_with_empty_request_gives_no_ext_output() { let shared_store = Arc::new(Mutex::new(MemoryStore::new())); let user_mock = MockUserValidationMethod::verified_user(1); - let mut authenticator = - Authenticator::new(Aaguid::new_empty(), shared_store.clone(), user_mock); + let mut authenticator = Authenticator::new( + Aaguid::new_empty(), + shared_store.clone(), + user_mock, + RustCryptoBackend, + ); let request = Request { extensions: Some(ExtensionInputs::default()), @@ -228,9 +256,13 @@ async fn supported_extension_with_empty_request_gives_no_ext_output() { let shared_store = Arc::new(Mutex::new(MemoryStore::new())); let user_mock = MockUserValidationMethod::verified_user(1); - let mut authenticator = - Authenticator::new(Aaguid::new_empty(), shared_store.clone(), user_mock) - .hmac_secret(extensions::HmacSecretConfig::new_with_uv_only()); + let mut authenticator = Authenticator::new( + Aaguid::new_empty(), + shared_store.clone(), + user_mock, + RustCryptoBackend, + ) + .hmac_secret(extensions::HmacSecretConfig::new_with_uv_only()); let request = Request { extensions: Some(ExtensionInputs::default()), @@ -251,9 +283,13 @@ async fn supported_extension_without_extension_request_gives_no_ext_output() { let shared_store = Arc::new(Mutex::new(MemoryStore::new())); let user_mock = MockUserValidationMethod::verified_user(1); - let mut authenticator = - Authenticator::new(Aaguid::new_empty(), shared_store.clone(), user_mock) - .hmac_secret(extensions::HmacSecretConfig::new_with_uv_only()); + let mut authenticator = Authenticator::new( + Aaguid::new_empty(), + shared_store.clone(), + user_mock, + RustCryptoBackend, + ) + .hmac_secret(extensions::HmacSecretConfig::new_with_uv_only()); let request = good_request(); @@ -271,9 +307,13 @@ async fn supported_extension_with_request_gives_output() { let shared_store = Arc::new(Mutex::new(MemoryStore::new())); let user_mock = MockUserValidationMethod::verified_user(1); - let mut authenticator = - Authenticator::new(Aaguid::new_empty(), shared_store.clone(), user_mock) - .hmac_secret(extensions::HmacSecretConfig::new_with_uv_only()); + let mut authenticator = Authenticator::new( + Aaguid::new_empty(), + shared_store.clone(), + user_mock, + RustCryptoBackend, + ) + .hmac_secret(extensions::HmacSecretConfig::new_with_uv_only()); let request = Request { extensions: Some(ExtensionInputs { @@ -305,10 +345,13 @@ async fn hmac_secret_mc_happy_path() { let shared_store = Arc::new(Mutex::new(MemoryStore::new())); let user_mock = MockUserValidationMethod::verified_user(1); - let mut authenticator = - Authenticator::new(Aaguid::new_empty(), shared_store.clone(), user_mock).hmac_secret( - extensions::HmacSecretConfig::new_with_uv_only().enable_on_make_credential(), - ); + let mut authenticator = Authenticator::new( + Aaguid::new_empty(), + shared_store.clone(), + user_mock, + RustCryptoBackend, + ) + .hmac_secret(extensions::HmacSecretConfig::new_with_uv_only().enable_on_make_credential()); let request = Request { extensions: Some(ExtensionInputs { @@ -351,10 +394,14 @@ async fn hmac_secret_mc_without_hmac_secret_support() { let shared_store = Arc::new(Mutex::new(MemoryStore::new())); let user_mock = MockUserValidationMethod::verified_user(1); - let mut authenticator = - Authenticator::new(Aaguid::new_empty(), shared_store.clone(), user_mock) - //support on make credential is not set. - .hmac_secret(extensions::HmacSecretConfig::new_with_uv_only()); + let mut authenticator = Authenticator::new( + Aaguid::new_empty(), + shared_store.clone(), + user_mock, + RustCryptoBackend, + ) + //support on make credential is not set. + .hmac_secret(extensions::HmacSecretConfig::new_with_uv_only()); let request = Request { extensions: Some(ExtensionInputs { @@ -428,7 +475,8 @@ async fn make_credential_returns_err_when_rk_is_requested_but_not_supported() { let store = StoreWithoutDiscoverableSupport; let user_mock = MockUserValidationMethod::verified_user(0); let request = good_request(); - let mut authenticator = Authenticator::new(Aaguid::new_empty(), store, user_mock); + let mut authenticator = + Authenticator::new(Aaguid::new_empty(), store, user_mock, RustCryptoBackend); authenticator.set_make_credentials_with_signature_counter(true); // Act @@ -464,8 +512,12 @@ async fn empty_store_with_exclude_credentials_succeeds() { MockUiHint::RequestNewCredential(request.user.clone().into(), request.rp.clone()), ); - let mut authenticator = - Authenticator::new(Aaguid::new_empty(), shared_store.clone(), user_mock); + let mut authenticator = Authenticator::new( + Aaguid::new_empty(), + shared_store.clone(), + user_mock, + RustCryptoBackend, + ); // This should succeed - an empty store means no credentials to exclude authenticator @@ -491,8 +543,12 @@ async fn empty_exclude_credentials_with_empty_store_succeeds() { MockUiHint::RequestNewCredential(request.user.clone().into(), request.rp.clone()), ); - let mut authenticator = - Authenticator::new(Aaguid::new_empty(), shared_store.clone(), user_mock); + let mut authenticator = Authenticator::new( + Aaguid::new_empty(), + shared_store.clone(), + user_mock, + RustCryptoBackend, + ); authenticator .make_credential(request) @@ -545,8 +601,12 @@ async fn store_with_credentials_not_in_exclude_list_succeeds() { MockUiHint::RequestNewCredential(request.user.clone().into(), request.rp.clone()), ); - let mut authenticator = - Authenticator::new(Aaguid::new_empty(), shared_store.clone(), user_mock); + let mut authenticator = Authenticator::new( + Aaguid::new_empty(), + shared_store.clone(), + user_mock, + RustCryptoBackend, + ); // This should succeed - the store contains credentials, but not the excluded one authenticator diff --git a/passkey-authenticator/src/authenticator/tests.rs b/passkey-authenticator/src/authenticator/tests.rs index a493294..55ae4cc 100644 --- a/passkey-authenticator/src/authenticator/tests.rs +++ b/passkey-authenticator/src/authenticator/tests.rs @@ -1,4 +1,4 @@ -use passkey_crypto::rng::Rng; +use passkey_crypto::{rng::Rng, rust_crypto::RustCryptoBackend}; use passkey_types::ctap2::{Aaguid, Flags}; use crate::{ @@ -26,7 +26,8 @@ async fn check_user_does_not_check_up_or_uv_when_not_requested() { // Arrange let store = None; - let authenticator = Authenticator::new(Aaguid::new_empty(), store, user_mock); + let authenticator = + Authenticator::new(Aaguid::new_empty(), store, user_mock, RustCryptoBackend); let options = passkey_types::ctap2::make_credential::Options { up: false, uv: false, @@ -64,7 +65,8 @@ async fn check_user_checks_up_when_requested() { // Arrange let store = None; - let authenticator = Authenticator::new(Aaguid::new_empty(), store, user_mock); + let authenticator = + Authenticator::new(Aaguid::new_empty(), store, user_mock, RustCryptoBackend); let options = passkey_types::ctap2::make_credential::Options { up: true, uv: false, @@ -105,7 +107,8 @@ async fn check_user_checks_uv_when_requested() { // Arrange let store = None; - let authenticator = Authenticator::new(Aaguid::new_empty(), store, user_mock); + let authenticator = + Authenticator::new(Aaguid::new_empty(), store, user_mock, RustCryptoBackend); let options = passkey_types::ctap2::make_credential::Options { up: true, uv: true, @@ -143,7 +146,8 @@ async fn check_user_returns_operation_denied_when_up_was_requested_but_not_retur // Arrange let store = None; - let authenticator = Authenticator::new(Aaguid::new_empty(), store, user_mock); + let authenticator = + Authenticator::new(Aaguid::new_empty(), store, user_mock, RustCryptoBackend); let options = passkey_types::ctap2::make_credential::Options { up: true, uv: false, @@ -186,7 +190,8 @@ async fn check_user_returns_operation_denied_when_uv_was_requested_but_not_retur // Arrange let store = None; - let authenticator = Authenticator::new(Aaguid::new_empty(), store, user_mock); + let authenticator = + Authenticator::new(Aaguid::new_empty(), store, user_mock, RustCryptoBackend); let options = passkey_types::ctap2::make_credential::Options { up: true, uv: true, @@ -215,7 +220,8 @@ async fn check_user_returns_unsupported_option_when_uv_was_requested_but_is_not_ // Arrange let store = None; - let authenticator = Authenticator::new(Aaguid::new_empty(), store, user_mock); + let authenticator = + Authenticator::new(Aaguid::new_empty(), store, user_mock, RustCryptoBackend); let options = passkey_types::ctap2::make_credential::Options { up: true, uv: true, @@ -259,7 +265,8 @@ async fn check_user_returns_up_and_uv_flags_when_neither_up_or_uv_was_requested_ // Arrange let store = None; - let authenticator = Authenticator::new(Aaguid::new_empty(), store, user_mock); + let authenticator = + Authenticator::new(Aaguid::new_empty(), store, user_mock, RustCryptoBackend); let options = passkey_types::ctap2::make_credential::Options { up: false, uv: false, diff --git a/passkey-authenticator/src/ctap2.rs b/passkey-authenticator/src/ctap2.rs index 0016490..a8553b7 100644 --- a/passkey-authenticator/src/ctap2.rs +++ b/passkey-authenticator/src/ctap2.rs @@ -5,16 +5,20 @@ //! //! +use passkey_crypto::CryptoBackend; use passkey_types::ctap2::{StatusCode, get_assertion, get_info, make_credential}; use crate::{Authenticator, CredentialStore, UserValidationMethod}; mod sealed { - use crate::{Authenticator, CredentialStore, UserValidationMethod}; + use super::{Authenticator, CredentialStore, CryptoBackend, UserValidationMethod}; pub trait Sealed {} - impl Sealed for Authenticator {} + impl Sealed + for Authenticator + { + } } /// Methods defined as being required for a [CTAP 2.0] compliant authenticator to implement. @@ -46,10 +50,11 @@ pub trait Ctap2Api: sealed::Sealed { } #[async_trait::async_trait] -impl Ctap2Api for Authenticator +impl Ctap2Api for Authenticator where S: CredentialStore + Sync + Send, U: UserValidationMethod::PasskeyItem> + Sync + Send, + C: CryptoBackend + Sync + Send, { async fn get_info(&self) -> Box { Authenticator::get_info(self).await diff --git a/passkey-authenticator/src/lib.rs b/passkey-authenticator/src/lib.rs index ccf1078..e9c43eb 100644 --- a/passkey-authenticator/src/lib.rs +++ b/passkey-authenticator/src/lib.rs @@ -31,12 +31,11 @@ mod u2f; mod user_validation; use coset::{ - CoseKey, CoseKeyBuilder, + CoseKey, iana::{self, Algorithm, EnumI64}, }; use p256::{ EncodedPoint, PublicKey, SecretKey, - ecdsa::SigningKey, elliptic_curve::{generic_array::GenericArray, sec1::FromEncodedPoint}, pkcs8::EncodePublicKey, }; diff --git a/passkey-authenticator/src/u2f.rs b/passkey-authenticator/src/u2f.rs index 2f3a807..36d14ad 100644 --- a/passkey-authenticator/src/u2f.rs +++ b/passkey-authenticator/src/u2f.rs @@ -7,7 +7,10 @@ use p256::{ SecretKey, ecdsa::{SigningKey, signature::Signer}, }; -use passkey_crypto::rng::{Rng, RngBackend}; +use passkey_crypto::{ + CryptoBackend, + rng::{Rng, RngBackend}, +}; use passkey_types::{ Bytes, Passkey, ctap2::{Flags, U2FError}, @@ -15,11 +18,18 @@ use passkey_types::{ AuthenticationRequest, AuthenticationResponse, PublicKey, RegisterRequest, RegisterResponse, }, }; + mod sealed { - use crate::{Authenticator, CredentialStore, UserValidationMethod}; + use super::{Authenticator, CredentialStore, CryptoBackend, UserValidationMethod}; pub trait Sealed {} - impl Sealed for Authenticator {} + impl Sealed for Authenticator + where + S: CredentialStore, + U: UserValidationMethod, + C: CryptoBackend, + { + } } /// Provides the U2F Authenticator API @@ -42,8 +52,11 @@ pub trait U2fApi: sealed::Sealed { } #[async_trait::async_trait] -impl U2fApi - for Authenticator +impl U2fApi for Authenticator +where + S: CredentialStore + Sync + Send, + U: UserValidationMethod + Sync + Send, + C: CryptoBackend + Sync + Send, { /// Apply a register request and create a credential and respond with the public key of said credential. async fn register( diff --git a/passkey-authenticator/src/u2f/tests.rs b/passkey-authenticator/src/u2f/tests.rs index bbc788c..1b31d75 100644 --- a/passkey-authenticator/src/u2f/tests.rs +++ b/passkey-authenticator/src/u2f/tests.rs @@ -5,7 +5,10 @@ use p256::{ EncodedPoint, ecdsa::{Signature, VerifyingKey, signature::Verifier}, }; -use passkey_crypto::rng::{Rng, RngBackend}; +use passkey_crypto::{ + rng::{Rng, RngBackend}, + rust_crypto::RustCryptoBackend, +}; use passkey_types::{ctap2::Aaguid, *}; #[tokio::test] @@ -15,6 +18,7 @@ async fn test_save_u2f_passkey() { Aaguid::new_empty(), credstore, MockUserValidationMethod::verified_user(0), + RustCryptoBackend, ); let challenge: [u8; 32] = Rng::random_array(); diff --git a/passkey-client/src/extensions.rs b/passkey-client/src/extensions.rs index e5738e9..327a8fa 100644 --- a/passkey-client/src/extensions.rs +++ b/passkey-client/src/extensions.rs @@ -11,6 +11,7 @@ //! [prf]: https://w3c.github.io/webauthn/#prf-extension use passkey_authenticator::{CredentialStore, StoreInfo, UserValidationMethod}; +use passkey_crypto::CryptoBackend; use passkey_types::{ ctap2::{get_assertion, get_info, make_credential}, webauthn::{ @@ -23,10 +24,11 @@ use crate::{Client, WebauthnError}; mod prf; -impl Client +impl Client where S: CredentialStore + Sync, U: UserValidationMethod + Sync, + C: CryptoBackend, P: public_suffix::EffectiveTLDProvider + Sync + 'static, { /// Create the extension inputs to be passed to an authenticator over CTAP2 diff --git a/passkey-client/src/lib.rs b/passkey-client/src/lib.rs index c3c4d32..ba8ebe7 100644 --- a/passkey-client/src/lib.rs +++ b/passkey-client/src/lib.rs @@ -16,6 +16,7 @@ //! [Webauthn]: https://w3c.github.io/webauthn/ mod client_data; pub use client_data::*; +use passkey_crypto::CryptoBackend; use std::{borrow::Cow, fmt::Display}; @@ -152,25 +153,27 @@ impl Display for Origin<'_> { /// default provider implementation. Use `new_with_custom_tld_provider()` to provide a custom /// `EffectiveTLDProvider` if your application needs to interpret eTLDs differently from the Mozilla /// Public Suffix List. -pub struct Client +pub struct Client where S: CredentialStore + Sync, U: UserValidationMethod + Sync, + C: CryptoBackend, P: public_suffix::EffectiveTLDProvider + Sync + 'static, { - authenticator: Authenticator, + authenticator: Authenticator, rp_id_verifier: RpIdVerifier, } -impl Client +impl Client where S: CredentialStore + Sync, U: UserValidationMethod + Sync, + C: CryptoBackend + Sync, Passkey: TryFrom<::PasskeyItem>, { /// Create a `Client` with a given `Authenticator` that uses the default /// TLD verifier provided by `[public_suffix]`. - pub fn new(authenticator: Authenticator) -> Self { + pub fn new(authenticator: Authenticator) -> Self { Self { authenticator, rp_id_verifier: RpIdVerifier::new(public_suffix::DEFAULT_PROVIDER, None), @@ -178,17 +181,18 @@ where } } -impl Client +impl Client where S: CredentialStore + Sync, U: UserValidationMethod::PasskeyItem> + Sync, + C: CryptoBackend, P: public_suffix::EffectiveTLDProvider + Sync + 'static, F: Fetcher + Sync, { /// Create a `Client` with a given `Authenticator` and a custom TLD provider /// that implements `[public_suffix::EffectiveTLDProvider]`. pub fn new_with_custom_tld_provider( - authenticator: Authenticator, + authenticator: Authenticator, custom_provider: P, fetcher: Option, ) -> Self { @@ -205,12 +209,12 @@ where } /// Read access to the Client's `Authenticator`. - pub fn authenticator(&self) -> &Authenticator { + pub fn authenticator(&self) -> &Authenticator { &self.authenticator } /// Write access to the Client's `Authenticator`. - pub fn authenticator_mut(&mut self) -> &mut Authenticator { + pub fn authenticator_mut(&mut self) -> &mut Authenticator { &mut self.authenticator } diff --git a/passkey-client/src/tests/ext_prf.rs b/passkey-client/src/tests/ext_prf.rs index 04152b0..5b58f32 100644 --- a/passkey-client/src/tests/ext_prf.rs +++ b/passkey-client/src/tests/ext_prf.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use passkey_authenticator::extensions::HmacSecretConfig; +use passkey_crypto::rust_crypto::RustCryptoBackend; use passkey_types::{ crypto::hmac_sha256, ctap2::{AuthenticatorData, Flags}, @@ -31,6 +32,7 @@ async fn registration_without_eval() { ctap2::Aaguid::new_empty(), MemoryStore::new(), uv_mock_with_creation(1), + RustCryptoBackend, ) .hmac_secret(HmacSecretConfig::new_without_uv()); @@ -69,6 +71,7 @@ async fn registration_with_single_input_eval() { ctap2::Aaguid::new_empty(), MemoryStore::new(), uv_mock_with_creation(1), + RustCryptoBackend, ) .hmac_secret(HmacSecretConfig::new_without_uv().enable_on_make_credential()); let mut client = Client::new(auth); @@ -138,6 +141,7 @@ async fn registration_with_eval_by_credential() { ctap2::Aaguid::new_empty(), MemoryStore::new(), uv_mock_user_check_skip(1), + RustCryptoBackend, ) .hmac_secret(HmacSecretConfig::new_without_uv()); let mut client = Client::new(auth); @@ -197,6 +201,7 @@ macro_rules! valid_authentication_with_prf { ctap2::Aaguid::new_empty(), MemoryStore::new(), uv_mock_with_creation(2), + RustCryptoBackend ) .hmac_secret(HmacSecretConfig::new_without_uv()); let mut client = Client::new(auth); @@ -310,6 +315,7 @@ async fn auth_empty_allow_credentials() { ctap2::Aaguid::new_empty(), MemoryStore::new(), uv_mock_user_check_skip(2), + RustCryptoBackend, ) .hmac_secret(HmacSecretConfig::new_without_uv()); let mut client = Client::new(auth); @@ -364,6 +370,7 @@ macro_rules! invalid_eval_by_credential_in_authentication { ctap2::Aaguid::new_empty(), MemoryStore::new(), uv_mock_user_check_skip(2), + RustCryptoBackend ) .hmac_secret(HmacSecretConfig::new_without_uv()); let mut client = Client::new(auth); @@ -437,6 +444,7 @@ macro_rules! compare_auth_calls { ctap2::Aaguid::new_empty(), MemoryStore::new(), uv_mock_with_creation(3), + RustCryptoBackend ) .hmac_secret(HmacSecretConfig::new_without_uv()); let mut client = Client::new(auth); @@ -554,6 +562,7 @@ async fn registration_and_authentication_with_unsupported_authenticator_ignores_ ctap2::Aaguid::new_empty(), MemoryStore::new(), uv_mock_with_creation(2), + RustCryptoBackend, ); let mut client = Client::new(auth); @@ -602,6 +611,7 @@ async fn empty_extension_and_no_hmac_secret_support() { ctap2::Aaguid::new_empty(), MemoryStore::new(), uv_mock_with_creation(2), + RustCryptoBackend, ); let mut client = Client::new(auth); @@ -648,6 +658,7 @@ async fn empty_extension_with_hmac_secret_support() { ctap2::Aaguid::new_empty(), MemoryStore::new(), uv_mock_with_creation(2), + RustCryptoBackend, ) .hmac_secret(HmacSecretConfig::new_without_uv()); let mut client = Client::new(auth); @@ -697,6 +708,7 @@ async fn two_eval_by_credential_entries() { ctap2::Aaguid::new_empty(), MemoryStore::new(), uv_mock_with_creation(3), + RustCryptoBackend, ) .hmac_secret(HmacSecretConfig::new_without_uv()); let mut client = Client::new(auth); @@ -812,8 +824,13 @@ async fn prf_already_hashed_does_not_hash_again() { let origin = Url::parse("https://future.1password.com").unwrap(); - let auth = Authenticator::new(ctap2::Aaguid::new_empty(), None, uv_mock_with_creation(2)) - .hmac_secret(HmacSecretConfig::new_without_uv().enable_on_make_credential()); + let auth = Authenticator::new( + ctap2::Aaguid::new_empty(), + None, + uv_mock_with_creation(2), + RustCryptoBackend, + ) + .hmac_secret(HmacSecretConfig::new_without_uv().enable_on_make_credential()); let mut client = Client::new(auth); let create_request = webauthn::CredentialCreationOptions { public_key: webauthn::PublicKeyCredentialCreationOptions { @@ -901,8 +918,13 @@ async fn prf_takes_precedence_over_prf_already_hashed() { let origin = Url::parse("https://future.1password.com").unwrap(); - let auth = Authenticator::new(ctap2::Aaguid::new_empty(), None, uv_mock_with_creation(2)) - .hmac_secret(HmacSecretConfig::new_without_uv().enable_on_make_credential()); + let auth = Authenticator::new( + ctap2::Aaguid::new_empty(), + None, + uv_mock_with_creation(2), + RustCryptoBackend, + ) + .hmac_secret(HmacSecretConfig::new_without_uv().enable_on_make_credential()); let mut client = Client::new(auth); let create_request = webauthn::CredentialCreationOptions { public_key: webauthn::PublicKeyCredentialCreationOptions { diff --git a/passkey-client/src/tests/mod.rs b/passkey-client/src/tests/mod.rs index 5059646..b8f5d7d 100644 --- a/passkey-client/src/tests/mod.rs +++ b/passkey-client/src/tests/mod.rs @@ -3,7 +3,10 @@ use crate::rp_id_verifier::tests::TestFetcher; use super::*; use coset::iana; use passkey_authenticator::{MemoryStore, MockUserValidationMethod, UserCheck}; -use passkey_crypto::rng::{Rng, RngBackend}; +use passkey_crypto::{ + rng::{Rng, RngBackend}, + rust_crypto::RustCryptoBackend, +}; use passkey_types::{Bytes, ctap2, encoding::try_from_base64url, webauthn::CollectedClientData}; use serde::Deserialize; use url::{ParseError, Url}; @@ -85,6 +88,7 @@ async fn create_and_authenticate() { ctap2::Aaguid::new_empty(), MemoryStore::new(), uv_mock_with_creation(2), + RustCryptoBackend, ); let mut client = Client::new(auth); @@ -118,6 +122,7 @@ async fn create_and_authenticate_with_extra_client_data() { ctap2::Aaguid::new_empty(), MemoryStore::new(), uv_mock_with_creation(2), + RustCryptoBackend, ); let mut client = Client::new(auth); @@ -182,6 +187,7 @@ async fn create_and_authenticate_with_origin_subdomain() { ctap2::Aaguid::new_empty(), MemoryStore::new(), uv_mock_with_creation(2), + RustCryptoBackend, ); let mut client = Client::new(auth); @@ -220,6 +226,7 @@ async fn create_and_authenticate_without_rp_id() { ctap2::Aaguid::new_empty(), MemoryStore::new(), uv_mock_with_creation(2), + RustCryptoBackend, ); let mut client = Client::new(auth); @@ -267,6 +274,7 @@ async fn create_and_authenticate_without_cred_params() { ctap2::Aaguid::new_empty(), MemoryStore::new(), uv_mock_with_creation(2), + RustCryptoBackend, ); let mut client = Client::new(auth); @@ -425,6 +433,7 @@ async fn client_register_triggers_uv_when_uv_is_required() { ctap2::Aaguid::new_empty(), MemoryStore::new(), user_mock_with_uv(), + RustCryptoBackend, ); let mut client = Client::new(auth); let origin = Url::parse("https://future.1password.com").unwrap(); @@ -452,6 +461,7 @@ async fn client_register_does_not_trigger_uv_when_uv_is_discouraged() { ctap2::Aaguid::new_empty(), MemoryStore::new(), user_mock_without_uv(), + RustCryptoBackend, ); let mut client = Client::new(auth); let origin = Url::parse("https://future.1password.com").unwrap(); @@ -545,6 +555,7 @@ fn map_rk_maps_criteria_to_rk_bool() { ctap2::Aaguid::new_empty(), MemoryStore::new(), MockUserValidationMethod::verified_user(0), + RustCryptoBackend, )); let result = client.map_rk(&Some(criteria), &auth_info); @@ -559,6 +570,7 @@ async fn create_and_authenticate_with_related_origins() { ctap2::Aaguid::new_empty(), MemoryStore::new(), uv_mock_with_creation(2), + RustCryptoBackend, ); let mut client = Client::new_with_custom_tld_provider( @@ -593,6 +605,7 @@ async fn fail_to_create_with_unrelated_origin() { ctap2::Aaguid::new_empty(), MemoryStore::new(), uv_mock_with_creation(0), + RustCryptoBackend, ); let mut client = Client::new_with_custom_tld_provider( diff --git a/passkey/examples/usage.rs b/passkey/examples/usage.rs index 327c8bd..fdd1cce 100644 --- a/passkey/examples/usage.rs +++ b/passkey/examples/usage.rs @@ -2,7 +2,10 @@ use passkey::{ authenticator::{Authenticator, UiHint, UserCheck, UserValidationMethod}, client::{Client, WebauthnError}, - crypto::rng::{Rng, RngBackend}, + crypto::{ + rng::{Rng, RngBackend}, + rust_crypto::RustCryptoBackend, + }, types::{Bytes, Passkey, crypto::sha256, ctap2::*, webauthn::*}, }; @@ -50,7 +53,8 @@ async fn client_setup( // Create the CredentialStore for the Authenticator. // Option is the simplest possible implementation of CredentialStore let store: Option = None; - let my_authenticator = Authenticator::new(my_aaguid, store, user_validation_method); + let my_authenticator = + Authenticator::new(my_aaguid, store, user_validation_method, RustCryptoBackend); // Create the Client // If you are creating credentials, you need to declare the Client as mut @@ -117,7 +121,8 @@ async fn authenticator_setup( let user_validation_method = MyUserValidationMethod {}; let my_aaguid = Aaguid::new_empty(); - let mut my_authenticator = Authenticator::new(my_aaguid, store, user_validation_method); + let mut my_authenticator = + Authenticator::new(my_aaguid, store, user_validation_method, RustCryptoBackend); let reg_request = make_credential::Request { client_data_hash: client_data_hash.clone(), diff --git a/passkey/src/lib.rs b/passkey/src/lib.rs index d900a72..311ddc7 100644 --- a/passkey/src/lib.rs +++ b/passkey/src/lib.rs @@ -66,6 +66,10 @@ //! use passkey::{ //! authenticator::{Authenticator, UiHint, UserValidationMethod, UserCheck}, //! client::{Client, DefaultClientData, WebauthnError}, +//! crypto::{ +//! rng::{Rng, RngBackend}, +//! rust_crypto::RustCryptoBackend, +//! }, //! types::{ctap2::*, crypto::sha256, webauthn::*, Bytes, Passkey}, //! }; //! @@ -115,7 +119,12 @@ //! // Create the CredentialStore for the Authenticator. //! // Option is the simplest possible implementation of CredentialStore //! let store: Option = None; -//! let my_authenticator = Authenticator::new(my_aaguid, store, user_validation_method); +//! let mut my_authenticator = Authenticator::new( +//! my_aaguid, +//! store, +//! user_validation_method, +//! RustCryptoBackend, +//! ); //! //! // Create the Client //! // If you are creating credentials, you need to declare the Client as mut @@ -180,6 +189,10 @@ //! # use passkey::{ //! # authenticator::{Authenticator, UiHint, UserValidationMethod, UserCheck}, //! # client::{Client, WebauthnError}, +//! # crypto::{ +//! # rng::{Rng, RngBackend}, +//! # rust_crypto::RustCryptoBackend, +//! # }, //! # types::{ctap2::*, crypto::sha256, webauthn::*, Bytes, Passkey}, //! # }; //! # @@ -228,7 +241,12 @@ //! let user_validation_method = MyUserValidationMethod {}; //! let my_aaguid = Aaguid::new_empty(); //! -//! let mut my_authenticator = Authenticator::new(my_aaguid, store, user_validation_method); +//! let mut my_authenticator = Authenticator::new( +//! my_aaguid, +//! store, +//! user_validation_method, +//! RustCryptoBackend, +//! ); //! //! let reg_request = make_credential::Request { //! client_data_hash: client_data_hash.clone(), From 674d2d0fa6ac4f9a08de5990e60c278eba9493a3 Mon Sep 17 00:00:00 2001 From: Rene Leveille Date: Mon, 19 Jan 2026 18:06:47 -0500 Subject: [PATCH 10/10] migrate make-credential entirely over to the crypto backend --- .../src/authenticator/make_credential.rs | 9 ++++----- passkey-authenticator/src/ctap2.rs | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/passkey-authenticator/src/authenticator/make_credential.rs b/passkey-authenticator/src/authenticator/make_credential.rs index 9c28d15..a4da440 100644 --- a/passkey-authenticator/src/authenticator/make_credential.rs +++ b/passkey-authenticator/src/authenticator/make_credential.rs @@ -1,4 +1,3 @@ -use p256::SecretKey; use passkey_crypto::{ CryptoBackend, rng::{Rng, RngBackend}, @@ -118,10 +117,10 @@ where // 9. Generate a new credential key pair for the algorithm specified. let credential_id = Rng::random_vec(self.credential_id_length.into()); - let private_key = { - let mut rng = Rng::new(); - SecretKey::random(&mut rng) - }; + let private_key = self + .crypto + .generate_key(algorithm) + .map_err(|_| Ctap2Error::UnsupportedAlgorithm)?; let extensions = self.make_extensions(input.extensions, flags.contains(Flags::UV))?; diff --git a/passkey-authenticator/src/ctap2.rs b/passkey-authenticator/src/ctap2.rs index a8553b7..3b65282 100644 --- a/passkey-authenticator/src/ctap2.rs +++ b/passkey-authenticator/src/ctap2.rs @@ -54,7 +54,7 @@ impl Ctap2Api for Authenticator where S: CredentialStore + Sync + Send, U: UserValidationMethod::PasskeyItem> + Sync + Send, - C: CryptoBackend + Sync + Send, + C: CryptoBackend + Sync + Send, { async fn get_info(&self) -> Box { Authenticator::get_info(self).await