diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48688b1..1c4d2db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,6 +94,8 @@ jobs: - name: Check semver uses: obi1kenobi/cargo-semver-checks-action@6b69fcf40e9b5fb17adeb57e4b6ecd020649a239 # v2.9 + with: + exclude: passkey-crypto docs: name: Documentation diff --git a/CHANGELOG.md b/CHANGELOG.md index 05eddff..164ea81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,16 @@ - Fix RP ID validation to require dot boundary ([#92](https://github.com/1Password/passkey-rs/pull/92)) +### 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-transports - ⚠ BREAKING: Remove `hid::Command::Msg` variant as that is U2F only and U2F support is now being removed. @@ -20,6 +30,7 @@ - ⚠ BREAKING: Remove U2F support ([#105](https://github.com/1Password/passkey-rs/pull/105)) - ⚠ BREAKING: Migrate `U2FError` variant into `Ctap2Error`, rename `Ctap2Code` to `StatusCode`, and finaly remove the old `StatusCode`. ([#105](https://github.com/1Password/passkey-rs/pull/105)) +- ⚠ BREAKING: The `passkey-types::rand` module no longer exists and is instead replaced by `passkey-crypto::rng`. ## Passkey v0.5.0 diff --git a/Cargo.toml b/Cargo.toml index b3d353f..34b1632 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-authenticator/Cargo.toml b/passkey-authenticator/Cargo.toml index b5a32cf..385bbd2 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"] linux = ["dep:ciborium", "dep:tokio", "dep:passkey-transports", "tokio/macros", "passkey-transports/linux"] @@ -27,10 +28,9 @@ ciborium = { version = "0.2", optional = true } 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-transports = { path = "../passkey-transports", version = "0.2", optional = true } passkey-types = { path = "../passkey-types", version = "0.6" } -rand = "0.8" tokio = { version = "1", features = ["sync", "macros"], optional = true } [dev-dependencies] diff --git a/passkey-authenticator/src/authenticator.rs b/passkey-authenticator/src/authenticator.rs index 3be6ee2..88fe919 100644 --- a/passkey-authenticator/src/authenticator.rs +++ b/passkey-authenticator/src/authenticator.rs @@ -1,4 +1,5 @@ use coset::iana; +use passkey_crypto::{CryptoBackend, 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) } } @@ -92,7 +93,7 @@ impl ValidationOptions for passkey_types::ctap2::get_assertion::Options { } /// 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 @@ -119,20 +120,23 @@ 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, - // TODO: Change this to a method on the cryptographic backend - algs: vec![iana::Algorithm::ES256], + algs: crypto.enumerate_algorithms(), transports: vec![ webauthn::AuthenticatorTransport::Internal, webauthn::AuthenticatorTransport::Hybrid, @@ -141,6 +145,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 68f7de3..8e945f9 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::RngBackend, rust_crypto::RustCryptoRng}; use passkey_types::{ crypto::hmac_sha256, ctap2::{ @@ -9,7 +10,6 @@ use passkey_types::{ AuthenticatorPrfValues, HmacSecretSaltOrOutput, }, }, - rand::random_vec, }; use crate::Authenticator; @@ -81,7 +81,7 @@ impl HmacSecretCredentialSupport { } } -impl Authenticator { +impl Authenticator { pub(super) fn make_hmac_secret( &self, hmac_secret_request: Option, @@ -96,8 +96,11 @@ 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: RustCryptoRng::random_vec(32), + cred_without_uv: config + .credentials + .without_uv() + .then(|| RustCryptoRng::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..eb567bc 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, RustCryptoRng}; use passkey_types::{Passkey, ctap2::Aaguid}; use crate::{Authenticator, MockUserValidationMethod}; @@ -19,19 +20,24 @@ 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)) .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(); - let request = prf_eval_request(Some(random_vec(64))); + let request = prf_eval_request(Some(RustCryptoRng::random_vec(64))); let res = auth .get_prf( @@ -66,7 +72,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(RustCryptoRng::random_vec(64))), true, ) .expect("Changing input should still succeed") @@ -98,19 +104,24 @@ 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)) .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(); - let request = prf_eval_request(Some(random_vec(64))); + let request = prf_eval_request(Some(RustCryptoRng::random_vec(64))); let res = auth .get_prf( @@ -142,7 +153,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(RustCryptoRng::random_vec(64))), true, ) .expect("Changing input should still succeed") @@ -155,19 +166,24 @@ 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)) .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(); - let mut request = prf_eval_request(Some(random_vec(64))); + let mut request = prf_eval_request(Some(RustCryptoRng::random_vec(64))); request.eval = request.eval.map(|e| AuthenticatorPrfValues { first: e.first, second: None, diff --git a/passkey-authenticator/src/authenticator/get_assertion.rs b/passkey-authenticator/src/authenticator/get_assertion.rs index cabb883..5a82d21 100644 --- a/passkey-authenticator/src/authenticator/get_assertion.rs +++ b/passkey-authenticator/src/authenticator/get_assertion.rs @@ -1,4 +1,4 @@ -use p256::ecdsa::{SigningKey, signature::SignerMut}; +use passkey_crypto::{CryptoBackend, SecretKeyT}; use passkey_types::{ Bytes, ctap2::{ @@ -11,14 +11,14 @@ use passkey_types::{ use crate::{ Authenticator, CredentialStore, UserValidationMethod, passkey::{AsCredentialDescriptor, PasskeyAccessor}, - private_key_from_cose_key, 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 @@ -136,12 +136,9 @@ where let mut signature_target = auth_data.to_vec(); signature_target.extend(input.client_data_hash); - let secret_key = private_key_from_cose_key(&credential.key())?; + let mut private_key = C::SecretKey::from_cose_key(&credential.key())?; - let mut private_key = SigningKey::from(secret_key); - - let signature: p256::ecdsa::Signature = private_key.sign(&signature_target); - let signature_bytes = signature.to_der().to_bytes().to_vec().into(); + let signature_bytes = private_key.sign(&signature_target).into(); let user_handle = credential.user_handle().map(Bytes::from); diff --git a/passkey-authenticator/src/authenticator/get_assertion/tests.rs b/passkey-authenticator/src/authenticator/get_assertion/tests.rs index 5779fd7..507aa8a 100644 --- a/passkey-authenticator/src/authenticator/get_assertion/tests.rs +++ b/passkey-authenticator/src/authenticator/get_assertion/tests.rs @@ -1,10 +1,11 @@ +use passkey_crypto::rng::RngBackend; +use passkey_crypto::rust_crypto::{RustCryptoBackend, RustCryptoRng}; use passkey_types::{ Passkey, StoredHmacSecret, ctap2::{ Aaguid, Ctap2Error, get_assertion::{ExtensionInputs, Options, Request}, }, - rand::random_vec, }; use crate::{ @@ -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 { @@ -48,6 +49,7 @@ async fn get_assertion_returns_no_credentials_found() { Aaguid::new_empty(), store, MockUserValidationMethod::verified_user_with_hint(1, MockUiHint::InformNoCredentialsFound), + RustCryptoBackend, ); // Act @@ -73,6 +75,7 @@ async fn get_assertion_increments_signature_counter_when_counter_is_some() { 1, MockUiHint::RequestExistingCredential(passkey), ), + RustCryptoBackend, ); // Act @@ -95,12 +98,16 @@ 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 { - prf: Some(prf_eval_request(Some(random_vec(32)))), + prf: Some(prf_eval_request(Some(RustCryptoRng::random_vec(32)))), ..Default::default() }), ..good_request() @@ -120,8 +127,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()), @@ -139,12 +150,16 @@ 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(RustCryptoRng::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()), @@ -162,12 +177,16 @@ 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(RustCryptoRng::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(); @@ -182,16 +201,20 @@ 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(RustCryptoRng::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 { - prf: Some(prf_eval_request(Some(random_vec(32)))), + prf: Some(prf_eval_request(Some(RustCryptoRng::random_vec(32)))), ..Default::default() }), ..good_request() 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 785c62e..3ee23bc 100644 --- a/passkey-authenticator/src/authenticator/make_credential.rs +++ b/passkey-authenticator/src/authenticator/make_credential.rs @@ -1,4 +1,4 @@ -use p256::SecretKey; +use passkey_crypto::{CryptoBackend, rng::RngBackend, rust_crypto::RustCryptoRng}; use passkey_types::{ Passkey, ctap2::{ @@ -9,10 +9,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 { @@ -111,19 +112,19 @@ 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 = RustCryptoRng::random_vec(self.credential_id_length.into()); - let private_key = { - let mut rng = rand::thread_rng(); - 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))?; // 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/authenticator/make_credential/tests.rs b/passkey-authenticator/src/authenticator/make_credential/tests.rs index eddcff5..ab18e21 100644 --- a/passkey-authenticator/src/authenticator/make_credential/tests.rs +++ b/passkey-authenticator/src/authenticator/make_credential/tests.rs @@ -1,6 +1,10 @@ use std::sync::Arc; use coset::iana; +use passkey_crypto::{ + rng::RngBackend, + rust_crypto::{RustCryptoBackend, RustCryptoRng}, +}; use passkey_types::{ Bytes, ctap2::{ @@ -10,7 +14,6 @@ use passkey_types::{ ExtensionInputs, Options, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }, }, - rand::random_vec, webauthn, }; @@ -26,13 +29,13 @@ use crate::{ fn good_request() -> Request { Request { - client_data_hash: random_vec(32).into(), + client_data_hash: RustCryptoRng::random_vec(32).into(), rp: PublicKeyCredentialRpEntity { id: "future.1password.com".into(), name: Some("1password".into()), }, user: webauthn::PublicKeyCredentialUserEntity { - id: random_vec(16).into(), + id: RustCryptoRng::random_vec(16).into(), display_name: "wendy".into(), name: "Appleseed".into(), }, @@ -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) @@ -77,7 +84,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 = RustCryptoRng::random_vec(16).into(); let response = Request { exclude_list: Some(vec![webauthn::PublicKeyCredentialDescriptor { ty: webauthn::PublicKeyCredentialType::PublicKey, @@ -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,17 +345,20 @@ 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 { prf: Some(AuthenticatorPrfInputs { eval: Some(AuthenticatorPrfValues { - first: random_vec(32).try_into().unwrap(), - second: Some(random_vec(32).try_into().unwrap()), + first: RustCryptoRng::random_vec(32).try_into().unwrap(), + second: Some(RustCryptoRng::random_vec(32).try_into().unwrap()), }), eval_by_credential: None, }), @@ -351,16 +394,20 @@ 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 { prf: Some(AuthenticatorPrfInputs { eval: Some(AuthenticatorPrfValues { - first: random_vec(32).try_into().unwrap(), + first: RustCryptoRng::random_vec(32).try_into().unwrap(), second: None, }), eval_by_credential: None, @@ -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 @@ -447,7 +495,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 = RustCryptoRng::random_vec(16).into(); let request = Request { exclude_list: Some(vec![webauthn::PublicKeyCredentialDescriptor { ty: webauthn::PublicKeyCredentialType::PublicKey, @@ -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) @@ -508,15 +564,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 = RustCryptoRng::random_vec(16).into(); + let excluded_cred_id: Bytes = RustCryptoRng::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(RustCryptoRng::random_vec(16).into()), username: Some("Appleseed".into()), user_display_name: Some("wendy".into()), counter: None, @@ -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 98954f0..ee99c76 100644 --- a/passkey-authenticator/src/authenticator/tests.rs +++ b/passkey-authenticator/src/authenticator/tests.rs @@ -1,3 +1,4 @@ +use passkey_crypto::{CryptoBackend, rust_crypto::RustCryptoBackend}; use passkey_types::ctap2::{Aaguid, Flags}; use crate::{ @@ -25,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, @@ -63,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, @@ -104,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, @@ -142,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, @@ -185,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, @@ -214,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, @@ -258,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, @@ -298,10 +306,10 @@ 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::<::Rng>().0; assert!(valid_range.contains(&length)); } } diff --git a/passkey-authenticator/src/ctap2.rs b/passkey-authenticator/src/ctap2.rs index 7751003..0f72783 100644 --- a/passkey-authenticator/src/ctap2.rs +++ b/passkey-authenticator/src/ctap2.rs @@ -5,19 +5,22 @@ //! //! +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 {} - #[cfg(all(feature = "linux", target_os = "linux"))] impl Sealed for crate::linux::LinuxAuthenticator {} + impl Sealed + for Authenticator + { + } } /// Methods defined as being required for a [CTAP 2.0] compliant authenticator to implement. @@ -49,10 +52,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 c35a6b5..193c09f 100644 --- a/passkey-authenticator/src/lib.rs +++ b/passkey-authenticator/src/lib.rs @@ -32,17 +32,8 @@ mod user_validation; #[cfg(all(feature = "linux", target_os = "linux"))] pub mod linux; -use coset::{ - CoseKey, CoseKeyBuilder, - iana::{self, Algorithm, EnumI64}, -}; -use p256::{ - EncodedPoint, PublicKey, SecretKey, - ecdsa::SigningKey, - elliptic_curve::{generic_array::GenericArray, sec1::FromEncodedPoint}, - pkcs8::EncodePublicKey, -}; -use passkey_types::{Bytes, ctap2::Ctap2Error}; +use coset::CoseKey; +use passkey_crypto::PublicKeyT; pub use self::{ authenticator::{Authenticator, CredentialIdLength, extensions}, @@ -55,97 +46,6 @@ pub use self::{ #[cfg(any(test, feature = "testable"))] pub use self::user_validation::MockUserValidationMethod; -/// Extract a cryptographic secret key from a [`CoseKey`]. -// possible candidate for a `passkey-crypto` crate? -pub fn private_key_from_cose_key(key: &CoseKey) -> Result { - if !matches!( - key.alg, - Some(coset::RegisteredLabelWithPrivate::Assigned( - Algorithm::ES256 - )) - ) { - return Err(Ctap2Error::UnsupportedAlgorithm); - } - if !matches!( - key.kty, - coset::RegisteredLabel::Assigned(iana::KeyType::EC2) - ) { - return Err(Ctap2Error::InvalidCredential); - } - - key.params - .iter() - .find_map(|(k, v)| { - if let coset::Label::Int(i) = k { - iana::Ec2KeyParameter::from_i64(*i) - .filter(|p| p == &iana::Ec2KeyParameter::D) - .and_then(|_| v.as_bytes()) - .and_then(|b| SecretKey::from_slice(b).ok()) - } else { - None - } - }) - .ok_or(Ctap2Error::InvalidCredential) -} - -/// Convert a Cose Key to a X.509 SubjectPublicKeyInfo formatted byte array. -/// -/// This should be used by the client when creating the [Easy Credential Data Accessors][ez] -/// -/// [ez]: https://w3c.github.io/webauthn/#sctn-public-key-easy -pub fn public_key_der_from_cose_key(key: &CoseKey) -> Result { - if !matches!( - key.alg, - Some(coset::RegisteredLabelWithPrivate::Assigned( - Algorithm::ES256 - )) - ) { - return Err(Ctap2Error::UnsupportedAlgorithm); - } - if !matches!( - key.kty, - coset::RegisteredLabel::Assigned(iana::KeyType::EC2) - ) { - return Err(Ctap2Error::InvalidCredential); - } - - let (mut x, mut y) = (None, None); - for (key, value) in &key.params { - if let coset::Label::Int(i) = key { - let key = iana::Ec2KeyParameter::from_i64(*i).ok_or(Ctap2Error::InvalidCbor)?; - match key { - iana::Ec2KeyParameter::X => { - if value.as_bytes().and_then(|v| x.replace(v)).is_some() { - log::warn!("Cose key has multiple entries for X coordinate"); - } - } - iana::Ec2KeyParameter::Y => { - if value.as_bytes().and_then(|v| y.replace(v)).is_some() { - log::warn!("Cose key has multiple entries for Y coordinate"); - } - } - _ => (), - } - } - } - let (Some(x), Some(y)) = (x, y) else { - return Err(Ctap2Error::CborUnexpectedType); - }; - - let point = EncodedPoint::from_affine_coordinates( - GenericArray::from_slice(x.as_slice()), - GenericArray::from_slice(y.as_slice()), - false, - ); - let Some(pub_key): Option = PublicKey::from_encoded_point(&point).into() else { - return Err(Ctap2Error::InvalidCredential); - }; - pub_key - .to_public_key_der() - .map_err(|_| Ctap2Error::InvalidCredential) - .map(|pk| pk.as_ref().to_vec().into()) -} - /// A COSE key pair, containing both the public and private keys. pub struct CoseKeyPair { /// The public key. @@ -156,27 +56,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/tests.rs b/passkey-authenticator/src/tests.rs index ab0b30a..7cdf4b0 100644 --- a/passkey-authenticator/src/tests.rs +++ b/passkey-authenticator/src/tests.rs @@ -1,37 +1,31 @@ use coset::iana; -use p256::{ - SecretKey, - ecdsa::{ - SigningKey, - signature::{Signer, Verifier}, - }, +use passkey_crypto::{ + CryptoBackend, PublicKeyT, SecretKeyT, + rng::RngBackend, + rust_crypto::{RustCryptoBackend, RustCryptoRng}, }; -use passkey_types::{ctap2::AuthenticatorData, rand::random_vec}; - -use super::{CoseKeyPair, private_key_from_cose_key}; +use passkey_types::ctap2::AuthenticatorData; #[test] fn private_key_cose_round_trip_sanity_check() { - let private_key = { - let mut rng = rand::thread_rng(); - SecretKey::random(&mut rng) - }; - let CoseKeyPair { - private: private_cose, - .. - } = CoseKeyPair::from_secret_key(&private_key, iana::Algorithm::ES256); - let public_signing_key = SigningKey::from(&private_key); - let public_key = public_signing_key.verifying_key(); + let original_private_key = RustCryptoBackend + .generate_key(iana::Algorithm::ES256) + .expect("Backend does not support ES256"); + let private_cose = original_private_key.to_cose_key(); + let public_key = original_private_key.public_key(); 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(RustCryptoRng::random_vec(32)); - let secret_key = private_key_from_cose_key(&private_cose).expect("to get a private key"); + let mut reconstructed_private_key = + ::SecretKey::from_cose_key(&private_cose) + .expect("to get a private key"); - let private_key = SigningKey::from(secret_key); - let signature: p256::ecdsa::Signature = private_key.sign(&signature_target); + let signature = reconstructed_private_key.sign(&signature_target); + // TODO: this currently fails because private_key.sign() performs DER encoding for P256 + // signatures. Should we have a separate "sign_with_der" method? public_key .verify(&signature_target, &signature) .expect("failed to verify signature") diff --git a/passkey-authenticator/tests/authenticator-rs-compat.rs b/passkey-authenticator/tests/authenticator-rs-compat.rs index d5262db..f49ceb2 100644 --- a/passkey-authenticator/tests/authenticator-rs-compat.rs +++ b/passkey-authenticator/tests/authenticator-rs-compat.rs @@ -3,10 +3,14 @@ use authenticator::MakeCredentialsResult; use coset::iana; use passkey_authenticator::{Authenticator, UiHint, UserCheck, UserValidationMethod}; +use passkey_crypto::{ + rng::RngBackend, + rust_crypto::{RustCryptoBackend, RustCryptoRng}, +}; use passkey_types::{ Passkey, ctap2::{Ctap2Error, make_credential}, - rand, webauthn, + webauthn, }; struct MockUV; @@ -38,16 +42,16 @@ impl UserValidationMethod for MockUV { #[tokio::test] async fn ensure_attestation_object_compatibility() { - let mut auth = Authenticator::new([0; 16].into(), None::, MockUV); + let mut auth = Authenticator::new([0; 16].into(), None::, MockUV, RustCryptoBackend); let cred_response = auth .make_credential(make_credential::Request { - client_data_hash: rand::random_vec(32).into(), + client_data_hash: RustCryptoRng::random_vec(32).into(), rp: make_credential::PublicKeyCredentialRpEntity { id: "webauth.io".to_string(), name: Some("webauthn.io".to_string()), }, user: webauthn::PublicKeyCredentialUserEntity { - id: rand::random_vec(16).into(), + id: RustCryptoRng::random_vec(16).into(), name: "wendy".to_string(), display_name: "wendy".to_string(), }, diff --git a/passkey-client/Cargo.toml b/passkey-client/Cargo.toml index 090ba98..da38313 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"] @@ -32,6 +33,7 @@ itertools = "0.14" mockall = { version = "0.11", optional = true } nom = { version = "7", features = ["alloc"], optional = true } passkey-authenticator = { path = "../passkey-authenticator", version = "0.6" } +passkey-crypto = { path = "../passkey-crypto", version = "0.1" } passkey-types = { path = "../passkey-types", version = "0.6" } public-suffix = { path = "../public-suffix", version = "0.1" } reqwest = { version = "0.12", default-features = false, optional = true } 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 885be0a..5a3aae9 100644 --- a/passkey-client/src/lib.rs +++ b/passkey-client/src/lib.rs @@ -16,13 +16,14 @@ //! [Webauthn]: https://w3c.github.io/webauthn/ mod client_data; pub use client_data::*; +use passkey_crypto::{CryptoBackend, PublicKeyT, SecretKeyT}; use std::{borrow::Cow, fmt::Display}; use coset::{Algorithm, iana::EnumI64}; use passkey_authenticator::{Authenticator, CredentialStore, UserValidationMethod}; use passkey_types::{ - Passkey, + Bytes, Passkey, crypto::sha256, ctap2, encoding, webauthn::{ @@ -245,27 +246,29 @@ 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, /// When the RP sends [`UserVerificationRequirement::Preferred`], whether to set CTAP `uv` to true. uv_when_preferred: bool, } -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), @@ -274,17 +277,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 { @@ -302,12 +306,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 } @@ -432,8 +436,11 @@ where } }; let public_key = Some( - passkey_authenticator::public_key_der_from_cose_key(&credential_id.key) - .map_err(|e| WebauthnError::AuthenticatorError(e.into()))?, + <::SecretKey as SecretKeyT>::PublicKey::bytes_from_cose_key( + &credential_id.key, + ) + .map(Into::::into) + .map_err(|e| WebauthnError::AuthenticatorError(ctap2::Ctap2Error::from(e).into()))?, ); let attestation_object = ctap2_response.as_webauthn_bytes(); diff --git a/passkey-client/src/linux.rs b/passkey-client/src/linux.rs index dc0c076..f287949 100644 --- a/passkey-client/src/linux.rs +++ b/passkey-client/src/linux.rs @@ -23,9 +23,9 @@ use std::future::Future; use coset::{Algorithm, iana::EnumI64}; use passkey_authenticator::linux::{LinuxAuthenticator, OpenError}; -use passkey_authenticator::public_key_der_from_cose_key; +use passkey_crypto::{CryptoBackend, PublicKeyT, SecretKeyT, rust_crypto::RustCryptoBackend}; use passkey_types::{ - ctap2, encoding, + Bytes, ctap2, encoding, webauthn::{ self, AuthenticatedPublicKeyCredential, AuthenticatorAssertionResponse, AuthenticatorAttachment, AuthenticatorAttestationResponse, ClientDataType, @@ -239,9 +239,12 @@ where // In the case that the algorithm is unknown, default to 0 (Reserved) _ => 0, }; + // TODO: we handle this conversion through the RustCryptoBackend. + // We may want to change this in the future. let public_key = Some( - public_key_der_from_cose_key(&credential_id.key) - .map_err(|e| WebauthnError::AuthenticatorError(e.into()))?, + <::SecretKey as SecretKeyT>::PublicKey::bytes_from_cose_key(&credential_id.key) + .map(Into::::into) + .map_err(|e| WebauthnError::AuthenticatorError(ctap2::Ctap2Error::from(e).into()))?, ); let attestation_object = ctap2_response.as_webauthn_bytes(); diff --git a/passkey-client/src/tests/ext_prf.rs b/passkey-client/src/tests/ext_prf.rs index 0db99b9..6d9d016 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); @@ -176,12 +180,12 @@ impl PrfValuesConfig { match self { PrfValuesConfig::None => None, PrfValuesConfig::One => Some(webauthn::AuthenticationExtensionsPrfValues { - first: Bytes::from(random_vec(128)), + first: Bytes::from(RustCryptoRng::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(RustCryptoRng::random_vec(128)), + second: Some(Bytes::from(RustCryptoRng::random_vec(128))), }), } } @@ -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,13 +315,14 @@ 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); let origin = Url::parse("https://future.1password.com").unwrap(); let eval_by_cred = webauthn::AuthenticationExtensionsPrfValues { - first: Bytes::from(random_vec(128)), + first: Bytes::from(RustCryptoRng::random_vec(128)), second: None, }; let options = good_credential_creation_options_with_prf(Some(eval_by_cred.clone())); @@ -364,12 +370,13 @@ 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); let eval_by_cred = webauthn::AuthenticationExtensionsPrfValues { - first: Bytes::from(random_vec(128)), + first: Bytes::from(RustCryptoRng::random_vec(128)), second: None, }; @@ -417,7 +424,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(RustCryptoRng::random_vec(64))) } #[cfg(test)] @@ -437,12 +444,13 @@ 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); - let mut first = Bytes::from(random_vec(128)); - let mut second = Some(Bytes::from(random_vec(128))); + let mut first = Bytes::from(RustCryptoRng::random_vec(128)); + let mut second = Some(Bytes::from(RustCryptoRng::random_vec(128))); let eval_by_cred = webauthn::AuthenticationExtensionsPrfValues { first: first.clone(), @@ -486,8 +494,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(RustCryptoRng::random_vec(128)); + second = Some(Bytes::from(RustCryptoRng::random_vec(128))); } let auth_options = webauthn::CredentialRequestOptions { @@ -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,13 +708,14 @@ 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); let eval_values = webauthn::AuthenticationExtensionsPrfValues { - first: Bytes::from(random_vec(128)), - second: Some(Bytes::from(random_vec(128))), + first: Bytes::from(RustCryptoRng::random_vec(128)), + second: Some(Bytes::from(RustCryptoRng::random_vec(128))), }; let origin = Url::parse("https://future.1password.com").unwrap(); @@ -739,8 +751,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(RustCryptoRng::random_vec(128)), + second: Some(Bytes::from(RustCryptoRng::random_vec(128))), }; let mut cred_id_2 = cred_id.clone(); @@ -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 320bc18..14a3987 100644 --- a/passkey-client/src/tests/mod.rs +++ b/passkey-client/src/tests/mod.rs @@ -3,9 +3,11 @@ 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::RngBackend, + rust_crypto::{RustCryptoBackend, RustCryptoRng}, }; +use passkey_types::{Bytes, ctap2, encoding::try_from_base64url, webauthn::CollectedClientData}; use serde::Deserialize; use url::{ParseError, Url}; @@ -18,11 +20,11 @@ fn good_credential_creation_options() -> webauthn::PublicKeyCredentialCreationOp name: "future.1password.com".into(), }, user: webauthn::PublicKeyCredentialUserEntity { - id: random_vec(16).into(), + id: RustCryptoRng::random_vec(16).into(), display_name: "wendy".into(), name: "wendy".into(), }, - challenge: random_vec(32).into(), + challenge: RustCryptoRng::random_vec(32).into(), pub_key_cred_params: vec![webauthn::PublicKeyCredentialParameters { ty: webauthn::PublicKeyCredentialType::PublicKey, alg: iana::Algorithm::ES256, @@ -41,7 +43,7 @@ fn good_credential_request_options( credential_id: impl Into, ) -> webauthn::PublicKeyCredentialRequestOptions { webauthn::PublicKeyCredentialRequestOptions { - challenge: random_vec(32).into(), + challenge: RustCryptoRng::random_vec(32).into(), timeout: None, rp_id: Some("future.1password.com".into()), allow_credentials: Some(vec![webauthn::PublicKeyCredentialDescriptor { @@ -86,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); @@ -119,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); @@ -183,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); @@ -221,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); @@ -268,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); @@ -426,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(); @@ -453,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(); @@ -555,6 +564,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( @@ -589,6 +599,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( @@ -611,7 +622,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(RustCryptoRng::random_vec(32)), }; let res = client .authenticate(&origin, auth_options, DefaultClientData) diff --git a/passkey-crypto/Cargo.toml b/passkey-crypto/Cargo.toml new file mode 100644 index 0000000..d56d718 --- /dev/null +++ b/passkey-crypto/Cargo.toml @@ -0,0 +1,36 @@ +[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" + +[features] +default = ["rust-crypto"] +rand = ["dep:rand", "dep:getrandom"] +js = ["getrandom/js"] +rust-crypto = ["dep:p256", "dep:ed25519-dalek", "dep:signature", "rand"] + +[dependencies] +coset.workspace = true +ed25519-dalek = { version = "3.0", optional = true, features = [ "rand_core" ] } +log = "0.4" +p256 = { version = "0.14", optional = true, features = [ + "arithmetic", + "pem" +] } +rand = { version = "0.10", optional = true} +signature = { version = "3.0", optional = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = { version = "0.2", optional = true } + +[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..90110f8 --- /dev/null +++ b/passkey-crypto/src/lib.rs @@ -0,0 +1,46 @@ +//! Swappable crypto backends for passkey operations. + +// TODO: remove this +#![allow(missing_docs)] + +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 enumerate_algorithms(&self) -> Vec; + + fn generate_key(&self, algorithm: iana::Algorithm) -> Result; +} + +pub trait SecretKeyT { + type PublicKey: PublicKeyT; + fn from_cose_key(cose_key: &CoseKey) -> Result + where + Self: Sized; + fn sign(&mut self, target: &[u8]) -> Vec; + fn public_key(&self) -> Self::PublicKey; + fn to_cose_key(&self) -> CoseKey; +} + +// TODO: is this conformant with CTAP2? Do we need to expose other error variants? +#[derive(Debug)] +pub enum CoseKeyConversionError { + UnsupportedAlgorithm, + InvalidCredential, + Other(Box), +} + +pub trait PublicKeyT { + fn verify(&self, target: &[u8], signature: &[u8]) -> Result<(), Error>; + fn bytes_from_cose_key(cose_key: &CoseKey) -> Result, CoseKeyConversionError>; + fn to_cose_key(&self) -> CoseKey; +} + +pub type Error = Box; diff --git a/passkey-crypto/src/rng/mod.rs b/passkey-crypto/src/rng/mod.rs new file mode 100644 index 0000000..7c2480d --- /dev/null +++ b/passkey-crypto/src/rng/mod.rs @@ -0,0 +1,21 @@ +//! 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; + +/// The methods that all Random Number Generator backends must implement for use accross the passkey +/// crates. +pub trait RngBackend: Sized { + /// 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; +} diff --git a/passkey-crypto/src/rng/rand.rs b/passkey-crypto/src/rng/rand.rs new file mode 100644 index 0000000..532b407 --- /dev/null +++ b/passkey-crypto/src/rng/rand.rs @@ -0,0 +1,24 @@ +use ::rand::{Rng, rngs::ThreadRng}; +use rand::RngExt; + +use super::RngBackend; + +impl RngBackend for ThreadRng { + fn random_vec(len: usize) -> Vec { + let mut data = vec![0u8; len]; + let mut rng = ::rand::rng(); + rng.fill_bytes(&mut data); + data + } + fn random_array() -> [u8; N] { + let mut rng = ::rand::rng(); + let mut bytes = [0u8; N]; + rng.fill(&mut bytes); + bytes + } + + fn from_range(range: std::ops::RangeInclusive) -> u8 { + let mut rng = ::rand::rng(); + rng.random_range(range) + } +} diff --git a/passkey-crypto/src/rust_crypto/mod.rs b/passkey-crypto/src/rust_crypto/mod.rs new file mode 100644 index 0000000..dae4d9d --- /dev/null +++ b/passkey-crypto/src/rust_crypto/mod.rs @@ -0,0 +1,296 @@ +use coset::{ + CoseKey, CoseKeyBuilder, + cbor::Value, + iana::{self, EnumI64}, +}; + +use crate::{CoseKeyConversionError, CryptoBackend, PublicKeyT, SecretKeyT}; +use ed25519_dalek::{Signer, ed25519::SignatureEncoding}; +use p256::{ + Sec1Point, + elliptic_curve::{Generate, array::Array}, + pkcs8::EncodePublicKey, +}; +use signature::Verifier; + +pub enum RustCryptoSecretKey { + P256(p256::ecdsa::SigningKey), + Ed25519(ed25519_dalek::SigningKey), +} + +pub enum RustCryptoPublicKey { + P256(p256::ecdsa::VerifyingKey), + Ed25519(ed25519_dalek::VerifyingKey), +} + +impl PublicKeyT for RustCryptoPublicKey { + fn verify(&self, target: &[u8], signature: &[u8]) -> Result<(), crate::Error> { + match self { + Self::P256(public_key) => { + let signature = p256::ecdsa::Signature::from_slice(signature)?; + public_key.verify(target, &signature)?; + } + Self::Ed25519(public_key) => { + let signature = ed25519_dalek::ed25519::Signature::from_slice(signature)?; + public_key.verify(target, &signature)?; + } + } + Ok(()) + } + + fn bytes_from_cose_key(cose_key: &CoseKey) -> Result, CoseKeyConversionError> { + let Some(coset::RegisteredLabelWithPrivate::Assigned(alg)) = cose_key.alg else { + return Err(CoseKeyConversionError::UnsupportedAlgorithm); + }; + if !matches!(alg, iana::Algorithm::ES256 | iana::Algorithm::Ed25519) { + return Err(CoseKeyConversionError::UnsupportedAlgorithm); + } + match alg { + iana::Algorithm::ES256 => { + if !matches!( + cose_key.kty, + coset::RegisteredLabel::Assigned(iana::KeyType::EC2) + ) { + return Err(CoseKeyConversionError::InvalidCredential); + } + let (mut x, mut y) = (None, None); + for (key, value) in &cose_key.params { + if let coset::Label::Int(i) = key { + let key = iana::Ec2KeyParameter::from_i64(*i) + .ok_or(CoseKeyConversionError::InvalidCredential)?; + match key { + iana::Ec2KeyParameter::X => { + if value.as_bytes().and_then(|v| x.replace(v)).is_some() { + log::warn!("Cose key has multiple entries for X coordinate"); + } + } + iana::Ec2KeyParameter::Y => { + if value.as_bytes().and_then(|v| y.replace(v)).is_some() { + log::warn!("Cose key has multiple entries for Y coordinate"); + } + } + _ => (), + } + } + } + let (Some(x), Some(y)) = (x, y) else { + return Err(CoseKeyConversionError::InvalidCredential); + }; + let point = Sec1Point::from_affine_coordinates( + &Array::try_from(x.as_slice()) + .map_err(|_| CoseKeyConversionError::InvalidCredential)?, + &Array::try_from(y.as_slice()) + .map_err(|_| CoseKeyConversionError::InvalidCredential)?, + false, + ); + let Ok(pub_key) = p256::ecdsa::VerifyingKey::from_sec1_point(&point) else { + return Err(CoseKeyConversionError::InvalidCredential); + }; + pub_key + .to_public_key_der() + .map_err(|_| CoseKeyConversionError::InvalidCredential) + .map(|pk| pk.as_ref().to_vec()) + } + iana::Algorithm::Ed25519 => { + if !matches!( + cose_key.kty, + coset::RegisteredLabel::Assigned(iana::KeyType::OKP) + ) { + return Err(CoseKeyConversionError::InvalidCredential); + } + let mut x = None; + for (key, value) in &cose_key.params { + if let coset::Label::Int(i) = key { + let key = iana::Ec2KeyParameter::from_i64(*i) + .ok_or(CoseKeyConversionError::InvalidCredential)?; + if key == iana::Ec2KeyParameter::X + && value.as_bytes().and_then(|v| x.replace(v)).is_some() + { + log::warn!("Cose key has multiple entries for X coordinate"); + } + } + } + let Some(x) = x else { + return Err(CoseKeyConversionError::InvalidCredential); + }; + Ok(x.to_owned()) + } + _ => Err(CoseKeyConversionError::UnsupportedAlgorithm), + } + } + + fn to_cose_key(&self) -> CoseKey { + match self { + Self::P256(public_key) => { + let encoded_public_key = public_key.to_sec1_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() + } + Self::Ed25519(public_key) => CoseKeyBuilder::new_okp_key() + .algorithm(iana::Algorithm::Ed25519) + .param( + iana::OkpKeyParameter::Crv.to_i64(), + Value::from(iana::EllipticCurve::Ed25519.to_i64()), + ) + .param( + iana::OkpKeyParameter::X.to_i64(), + Value::from(public_key.to_bytes().as_slice()), + ) + .build(), + } + } +} + +impl SecretKeyT for RustCryptoSecretKey { + type PublicKey = RustCryptoPublicKey; + + fn from_cose_key(cose_key: &CoseKey) -> Result + where + Self: Sized, + { + let Some(coset::RegisteredLabelWithPrivate::Assigned(alg)) = cose_key.alg else { + return Err(CoseKeyConversionError::UnsupportedAlgorithm); + }; + if !matches!(alg, iana::Algorithm::ES256 | iana::Algorithm::Ed25519) { + return Err(CoseKeyConversionError::UnsupportedAlgorithm); + } + let bytes = cose_key + .params + .iter() + .find_map(|(k, v)| { + if let coset::Label::Int(i) = k { + iana::Ec2KeyParameter::from_i64(*i) + .filter(|p| p == &iana::Ec2KeyParameter::D) + .and_then(|_| v.as_bytes()) + } else { + None + } + }) + .ok_or(CoseKeyConversionError::InvalidCredential)?; + match alg { + iana::Algorithm::ES256 => { + if !matches!( + cose_key.kty, + coset::RegisteredLabel::Assigned(iana::KeyType::EC2) + ) { + return Err(CoseKeyConversionError::InvalidCredential); + } + Ok(Self::P256( + p256::ecdsa::SigningKey::from_slice(bytes) + .map_err(|_| CoseKeyConversionError::InvalidCredential)?, + )) + } + iana::Algorithm::Ed25519 => { + if !matches!( + cose_key.kty, + coset::RegisteredLabel::Assigned(iana::KeyType::OKP) + ) { + return Err(CoseKeyConversionError::InvalidCredential); + } + Ok(Self::Ed25519(ed25519_dalek::SigningKey::from_bytes( + bytes + .as_slice() + .try_into() + .map_err(|_| CoseKeyConversionError::InvalidCredential)?, + ))) + } + _ => Err(CoseKeyConversionError::UnsupportedAlgorithm), + } + } + + fn sign(&mut self, target: &[u8]) -> Vec { + match self { + Self::P256(secret_key) => { + let signature: p256::ecdsa::Signature = secret_key.sign(target); + signature.to_der().to_vec() + } + Self::Ed25519(secret_key) => secret_key.sign(target).to_vec(), + } + } + + fn public_key(&self) -> Self::PublicKey { + match self { + Self::P256(secret_key) => RustCryptoPublicKey::P256(*secret_key.verifying_key()), + Self::Ed25519(secret_key) => RustCryptoPublicKey::Ed25519(secret_key.verifying_key()), + } + } + + fn to_cose_key(&self) -> CoseKey { + match self { + Self::P256(secret_key) => { + let public_key = secret_key.verifying_key().to_sec1_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, + secret_key.to_bytes().to_vec(), + ) + .algorithm(iana::Algorithm::ES256) + .build() + } + Self::Ed25519(secret_key) => CoseKeyBuilder::new_okp_key() + .algorithm(iana::Algorithm::Ed25519) + .param( + iana::OkpKeyParameter::Crv.to_i64(), + Value::from(iana::EllipticCurve::Ed25519.to_i64()), + ) + .param( + iana::OkpKeyParameter::X.to_i64(), + Value::from(secret_key.verifying_key().to_bytes().as_slice()), + ) + .param( + iana::OkpKeyParameter::D.to_i64(), + Value::from(&secret_key.to_bytes()[..]), + ) + .build(), + } + } +} + +pub struct RustCryptoBackend; + +impl CryptoBackend for RustCryptoBackend { + type Rng = ::rand::rngs::ThreadRng; + + type SecretKey = RustCryptoSecretKey; + + fn enumerate_algorithms(&self) -> Vec { + vec![ + iana::Algorithm::ES256, + iana::Algorithm::PS256, + iana::Algorithm::EdDSA, + iana::Algorithm::Ed25519, + ] + } + + fn generate_key(&self, algorithm: iana::Algorithm) -> Result { + match algorithm { + iana::Algorithm::ES256 | iana::Algorithm::PS256 => Ok(RustCryptoSecretKey::P256( + p256::ecdsa::SigningKey::generate(), + )), + iana::Algorithm::EdDSA | iana::Algorithm::Ed25519 => { + let mut rng = ::rand::rng(); + Ok(RustCryptoSecretKey::Ed25519( + ed25519_dalek::SigningKey::generate(&mut rng), + )) + } + _ => Err("Algorithm is unsupported".to_string().into()), + } + } +} + +pub type RustCryptoRng = ::Rng; diff --git a/passkey-types/Cargo.toml b/passkey-types/Cargo.toml index f6d1e34..6247ec0 100644 --- a/passkey-types/Cargo.toml +++ b/passkey-types/Cargo.toml @@ -17,8 +17,9 @@ workspace = true [features] default = [] +js = ["passkey-crypto/js"] serialize_bytes_as_base64_string = [] -testable = ["dep:p256"] +testable = [] typeshare = ["dep:typeshare"] [dependencies] @@ -27,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" @@ -37,11 +38,3 @@ url = { version = "2", features = ["serde"] } zeroize = { version = "1", features = ["zeroize_derive"] } # TODO: investigate rolling our own IANA listings and COSE keys coset = { workspace = true } -p256 = { version = "0.13", features = [ - "arithmetic", - "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..06c5cf5 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::RngBackend, rust_crypto::RustCryptoRng}; 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: RustCryptoRng::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), + RustCryptoRng::random_vec(32), + RustCryptoRng::random_vec(32), ) .algorithm(coset::iana::Algorithm::ES256) .build(), diff --git a/passkey-types/src/ctap2/error.rs b/passkey-types/src/ctap2/error.rs index 3fe6dd2..7bb961f 100644 --- a/passkey-types/src/ctap2/error.rs +++ b/passkey-types/src/ctap2/error.rs @@ -1,5 +1,7 @@ //! Error responses +use passkey_crypto::CoseKeyConversionError; + use crate::utils::repr_enum::CodeOutOfRange; /// Ctap2 error which may or may not be explicitly defined @@ -158,6 +160,22 @@ repr_enum! { } } +impl From for Ctap2Error { + fn from(value: CoseKeyConversionError) -> Self { + match value { + CoseKeyConversionError::UnsupportedAlgorithm => Ctap2Error::UnsupportedAlgorithm, + CoseKeyConversionError::InvalidCredential => Ctap2Error::InvalidCredential, + CoseKeyConversionError::Other(_) => Ctap2Error::Other, + } + } +} + +impl From for StatusCode { + fn from(value: CoseKeyConversionError) -> Self { + StatusCode::Known(Ctap2Error::from(value)) + } +} + impl From for StatusCode { fn from(src: Ctap2Error) -> Self { StatusCode::Known(src) diff --git a/passkey-types/src/ctap2/extensions/hmac_secret/tests.rs b/passkey-types/src/ctap2/extensions/hmac_secret/tests.rs index f316261..4ec8d4c 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::RngBackend, rust_crypto::RustCryptoRng}; 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 = RustCryptoRng::random_vec(31); + let between = RustCryptoRng::random_vec(33); + let too_long = RustCryptoRng::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 = RustCryptoRng::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), + RustCryptoRng::random_vec(32), + RustCryptoRng::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(RustCryptoRng::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(RustCryptoRng::random_vec(32)) }) .unwrap(); @@ -176,8 +175,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), + RustCryptoRng::random_vec(32), + RustCryptoRng::random_vec(32), ) .build() .to_cbor_value() @@ -185,7 +184,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: RustCryptoRng::random_vec(32).into(), pin_uv_auth_protocol: None, }; let mut buf = Vec::with_capacity(128); @@ -216,8 +215,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), + RustCryptoRng::random_vec(32), + RustCryptoRng::random_vec(32), ) .build() .to_cbor_value() @@ -225,7 +224,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: RustCryptoRng::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 2f610f8..09a64e1 100644 --- a/passkey-types/src/lib.rs +++ b/passkey-types/src/lib.rs @@ -41,7 +41,7 @@ pub use self::{ passkey::{CredentialExtensions, Passkey, StoredHmacSecret}, utils::{ bytes::{Bytes, NotBase64Encoded}, - crypto, encoding, rand, + crypto, encoding, }, }; diff --git a/passkey-types/src/passkey.rs b/passkey-types/src/passkey.rs index d14cab9..54483f6 100644 --- a/passkey-types/src/passkey.rs +++ b/passkey-types/src/passkey.rs @@ -117,8 +117,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 a3e8696..c4284bb 100644 --- a/passkey-types/src/passkey/mock.rs +++ b/passkey-types/src/passkey/mock.rs @@ -1,7 +1,7 @@ -use coset::{CoseKeyBuilder, iana}; -use p256::{SecretKey, ecdsa::SigningKey}; +use coset::iana; +use passkey_crypto::{CryptoBackend, SecretKeyT, rng::RngBackend, rust_crypto::RustCryptoRng}; -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 { @@ -10,34 +10,17 @@ pub struct PasskeyBuilder { impl PasskeyBuilder { /// Create a new - pub(super) fn new(rp_id: String) -> Self { - let private_key = { - let mut rng = rand::thread_rng(); - 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, - credential_id: random_vec(16).into(), + credential_id: RustCryptoRng::random_vec(16).into(), rp_id, user_handle: None, username: None, @@ -50,13 +33,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 = RustCryptoRng::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(RustCryptoRng::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 180f026..290b090 100644 --- a/passkey/Cargo.toml +++ b/passkey/Cargo.toml @@ -18,6 +18,7 @@ workspace = true [features] default = [] linux = ["passkey-authenticator/linux", "passkey-client/linux"] +js = ["passkey-crypto/js"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -31,6 +32,7 @@ passkey-authenticator = { path = "../passkey-authenticator", version = "0.6" } passkey-client = { path = "../passkey-client", version = "0.6" } passkey-transports = { path = "../passkey-transports", version = "0.2" } passkey-types = { path = "../passkey-types", version = "0.6" } +passkey-crypto = { path = "../passkey-crypto", version = "0.1" } [dev-dependencies] async-trait = "0.1" @@ -43,6 +45,9 @@ passkey-client = { path = "../passkey-client", version = "0.6", 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/linux.rs b/passkey/examples/linux.rs index e1c41f5..1706926 100644 --- a/passkey/examples/linux.rs +++ b/passkey/examples/linux.rs @@ -2,7 +2,8 @@ #[cfg(all(feature = "linux", target_os = "linux"))] use passkey::{ client::{DefaultClientData, WebauthnError, linux::LinuxClient}, - types::{Bytes, rand::random_vec, webauthn::*}, + crypto::{rng::RngBackend, rust_crypto::RustCryptoRng}, + types::{Bytes, webauthn::*}, }; #[cfg(all(feature = "linux", target_os = "linux"))] @@ -53,7 +54,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 = RustCryptoRng::random_vec(32).into(); // Now try and authenticate let credential_request = CredentialRequestOptions { public_key: PublicKeyCredentialRequestOptions { @@ -81,14 +82,14 @@ async fn client_setup( 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: RustCryptoRng::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 + RustCryptoRng::random_vec(32).into(), // challenge_bytes_from_rp PublicKeyCredentialParameters { ty: PublicKeyCredentialType::PublicKey, alg: iana::Algorithm::ES256, diff --git a/passkey/examples/usage.rs b/passkey/examples/usage.rs index ec3f1d8..046fb23 100644 --- a/passkey/examples/usage.rs +++ b/passkey/examples/usage.rs @@ -2,7 +2,11 @@ use passkey::{ authenticator::{Authenticator, UiHint, UserCheck, UserValidationMethod}, client::{Client, WebauthnError}, - types::{Bytes, Passkey, crypto::sha256, ctap2::*, rand::random_vec, webauthn::*}, + crypto::{ + rng::RngBackend, + rust_crypto::{RustCryptoBackend, RustCryptoRng}, + }, + types::{Bytes, Passkey, crypto::sha256, ctap2::*, webauthn::*}, }; use coset::iana; @@ -49,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 @@ -83,7 +88,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 = RustCryptoRng::random_vec(32).into(); // Now try and authenticate let credential_request = CredentialRequestOptions { public_key: PublicKeyCredentialRequestOptions { @@ -116,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(), @@ -173,14 +179,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: RustCryptoRng::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 + RustCryptoRng::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 fed7f35..11d7151 100644 --- a/passkey/src/lib.rs +++ b/passkey/src/lib.rs @@ -66,7 +66,11 @@ //! use passkey::{ //! authenticator::{Authenticator, UiHint, UserValidationMethod, UserCheck}, //! client::{Client, DefaultClientData, WebauthnError}, -//! types::{ctap2::*, rand::random_vec, crypto::sha256, webauthn::*, Bytes, Passkey}, +//! crypto::{ +//! rng::RngBackend, +//! rust_crypto::{RustCryptoBackend, RustCryptoRng}, +//! }, +//! types::{ctap2::*, crypto::sha256, webauthn::*, Bytes, Passkey}, //! }; //! //! use coset::iana; @@ -98,14 +102,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 = RustCryptoRng::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: RustCryptoRng::random_vec(32).into(), //! display_name: "Johnny Passkey".into(), //! name: "jpasskey@example.org".into(), //! }; @@ -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 @@ -150,7 +159,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 = RustCryptoRng::random_vec(32).into(); //! // Now try and authenticate //! let credential_request = CredentialRequestOptions { //! public_key: PublicKeyCredentialRequestOptions { @@ -180,7 +189,11 @@ //! # use passkey::{ //! # authenticator::{Authenticator, UiHint, UserValidationMethod, UserCheck}, //! # client::{Client, WebauthnError}, -//! # types::{ctap2::*, rand::random_vec, crypto::sha256, webauthn::*, Bytes, Passkey}, +//! # crypto::{ +//! # rng::RngBackend, +//! # rust_crypto::{RustCryptoBackend, RustCryptoRng}, +//! # }, +//! # types::{ctap2::*, crypto::sha256, webauthn::*, Bytes, Passkey}, //! # }; //! # //! # use coset::iana; @@ -213,9 +226,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 = RustCryptoRng::random_vec(32).into(); //! let user_entity = PublicKeyCredentialUserEntity { -//! id: random_vec(32).into(), +//! id: RustCryptoRng::random_vec(32).into(), //! display_name: "Johnny Passkey".into(), //! name: "jpasskey@example.org".into(), //! }; @@ -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(), @@ -265,5 +283,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; 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