├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── passkey-authenticator ├── Cargo.toml ├── README.md └── src │ ├── authenticator.rs │ ├── authenticator │ ├── extensions.rs │ ├── extensions │ │ └── hmac_secret.rs │ ├── get_assertion.rs │ ├── get_info.rs │ └── make_credential.rs │ ├── credential_store.rs │ ├── ctap2.rs │ ├── lib.rs │ ├── u2f.rs │ └── user_validation.rs ├── passkey-client ├── Cargo.toml ├── README.md └── src │ ├── android.rs │ ├── client_data.rs │ ├── extensions.rs │ ├── extensions │ └── prf.rs │ ├── lib.rs │ ├── quirks.rs │ └── tests │ ├── ext_prf.rs │ └── mod.rs ├── passkey-transports ├── Cargo.toml ├── README.md └── src │ ├── hid.rs │ └── lib.rs ├── passkey-types ├── Cargo.toml ├── README.md └── src │ ├── ctap2.rs │ ├── ctap2 │ ├── aaguid.rs │ ├── attestation_fmt.rs │ ├── error.rs │ ├── extensions │ │ ├── hmac_secret.rs │ │ ├── mod.rs │ │ └── prf.rs │ ├── flags.rs │ ├── get_assertion.rs │ ├── get_info.rs │ └── make_credential.rs │ ├── lib.rs │ ├── passkey.rs │ ├── passkey │ └── mock.rs │ ├── u2f.rs │ ├── u2f │ ├── authenticate.rs │ ├── commands.rs │ ├── register.rs │ └── version.rs │ ├── utils.rs │ ├── utils │ ├── bytes.rs │ ├── crypto.rs │ ├── encoding.rs │ ├── rand.rs │ ├── repr_enum.rs │ ├── serde.rs │ └── serde_workaround.rs │ ├── webauthn.rs │ └── webauthn │ ├── assertion.rs │ ├── attestation.rs │ ├── common.rs │ └── extensions │ ├── credential_properties.rs │ ├── mod.rs │ └── pseudo_random_function.rs ├── passkey ├── Cargo.toml ├── examples │ └── usage.rs └── src │ └── lib.rs └── public-suffix ├── .gitignore ├── Cargo.toml ├── README.md ├── gen.sh ├── generator ├── AUTHORS ├── CONTRIBUTORS ├── LICENSE ├── go.mod ├── go.sum └── main.go ├── public_suffix_list.dat ├── src ├── lib.rs ├── tld_list.rs ├── tld_list_test.rs └── types.rs └── tests └── tests.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | branches: 7 | - main 8 | 9 | name: CI 10 | 11 | jobs: 12 | clippy: 13 | name: Clippy 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | rust: 18 | - stable 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions-rs/toolchain@v1 22 | with: 23 | profile: minimal 24 | toolchain: ${{ matrix.rust }} 25 | components: clippy 26 | - run: rustup run ${{ matrix.rust }} cargo clippy --all --all-targets --all-features -- -D warnings 27 | 28 | fmt: 29 | name: Rustfmt 30 | runs-on: ubuntu-latest 31 | strategy: 32 | matrix: 33 | rust: 34 | # Once we settle on a MSRV we should add that here. 35 | - stable 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: actions-rs/toolchain@v1 39 | with: 40 | profile: minimal 41 | toolchain: ${{ matrix.rust }} 42 | components: rustfmt 43 | - run: rustup run ${{ matrix.rust }} cargo fmt --all -- --check 44 | 45 | semver-checks: 46 | runs-on: ubuntu-latest 47 | steps: 48 | - name: Checkout 49 | uses: actions/checkout@v4 50 | - name: Check semver 51 | uses: obi1kenobi/cargo-semver-checks-action@v2 52 | 53 | docs: 54 | name: Documentation 55 | runs-on: ubuntu-latest 56 | strategy: 57 | matrix: 58 | rust: 59 | - stable 60 | steps: 61 | - uses: actions/checkout@v4 62 | - uses: actions-rs/toolchain@v1 63 | with: 64 | profile: minimal 65 | toolchain: ${{ matrix.rust }} 66 | - run: RUSTDOCFLAGS="-D warnings " rustup run ${{ matrix.rust }} cargo doc --workspace --no-deps --all-features 67 | 68 | test: 69 | name: Test 70 | runs-on: ubuntu-latest 71 | strategy: 72 | matrix: 73 | rust: 74 | # Once we settle on a MSRV we should add that here. 75 | - stable 76 | steps: 77 | - uses: actions/checkout@v4 78 | - uses: actions-rs/toolchain@v1 79 | with: 80 | profile: minimal 81 | toolchain: ${{ matrix.rust }} 82 | - run: rustup run ${{ matrix.rust }} cargo test 83 | 84 | typeshare: 85 | name: Typeshare 86 | runs-on: ubuntu-latest 87 | strategy: 88 | matrix: 89 | rust: 90 | - stable 91 | lang: 92 | - typescript 93 | - kotlin 94 | - swift 95 | include: 96 | - fs: ts 97 | lang: typescript 98 | - fs: kt 99 | lang: kotlin 100 | - fs: swift 101 | lang: swift 102 | steps: 103 | - uses: actions/checkout@v4 104 | - uses: actions-rs/install@v0.1 105 | with: 106 | crate: typeshare-cli 107 | version: "1.11.0" 108 | - run: typeshare --lang=${{ matrix.lang }} . -o test.${{ matrix.fs }} 109 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | .idea -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | ## Passkey v0.4.0 6 | ### passkey-authenticator v0.4.0 7 | 8 | - Added: support for controlling generated credential's ID length to Authenticator ([#49](https://github.com/1Password/passkey-rs/pull/49)) 9 | - ⚠ BREAKING: Removal of `Authenticator::set_display_name` and `Authenticator::display_name` methods ([#51](https://github.com/1Password/passkey-rs/pull/51)) 10 | 11 | ### passkey-client v0.4.0 12 | - ⚠ BREAKING: Update android asset link verification ([#51](https://github.com/1Password/passkey-rs/pull/51)) 13 | - Change `asset_link_url` parameter in `UnverifiedAssetLink::new` to be required rather than optional. 14 | - Remove internal string in `ValidationError::InvalidAssetLinkUrl` variant. 15 | - Remove special casing of responses for specific RPs ([#51](https://github.com/1Password/passkey-rs/pull/51)) 16 | - Added `RpIdValidator::is_valid_rp_id` to verify that an rp_id is valid to be used as such ([#51](https://github.com/1Password/passkey-rs/pull/51)) 17 | 18 | ### passkey-types v0.4.0 19 | - ⚠ BREAKING: Removal of `CredentialPropertiesOutput::authenticator_display_name` ([#51](https://github.com/1Password/passkey-rs/pull/51)) 20 | 21 | 22 | ## Passkey v0.3.0 23 | ### passkey-authenticator v0.3.0 24 | 25 | - Added: support for signature counters 26 | - ⚠ BREAKING: Add `update_credential` function to `CredentialStore` ([#23](https://github.com/1Password/passkey-rs/pull/23)). 27 | - Add `make_credentials_with_signature_counter` to `Authenticator`. 28 | - ⚠ BREAKING: Merge functions in `UserValidationMethod` ([#24](https://github.com/1Password/passkey-rs/pull/24)) 29 | - Removed: `UserValidationMethod::check_user_presence` 30 | - Removed: `UserValidationMethod::check_user_verification` 31 | - Added: `UserValidationMethod::check_user`. This function now performs both user presence and user verification checks. 32 | The function now also returns which validations were performed, even if they were not requested. 33 | - Added: Support for discoverable credentials 34 | - ⚠ BREAKING: Added: `CredentialStore::get_info` which returns `StoreInfo` containing `DiscoverabilitySupport`. 35 | - ⚠ BREAKING: Changed: `CredentialStore::save_credential` now also takes `Options`. 36 | - Changed: `Authenticator::make_credentials` now returns an error if a discoverable credential was requested but not supported by the store. 37 | 38 | ### passkey-client v0.3.0 39 | 40 | - Changed: The `Client` no longer hardcodes the UV value sent to the `Authenticator` ([#22](https://github.com/1Password/passkey-rs/pull/22)). 41 | - Changed: The `Client` no longer hardcodes the RK value sent to the `Authenticator` ([#27](https://github.com/1Password/passkey-rs/pull/27)). 42 | - The client now supports additional user-defined properties in the client data, while also clarifying how the client 43 | handles client data and its hash. 44 | - ⚠ BREAKING: Changed: `register` and `authenticate` take `ClientData` instead of `Option>`. 45 | - ⚠ BREAKING: Changed: Custom client data hashes are now specified using `DefaultClientDataWithCustomHash(Vec)` instead of 46 | `Some(Vec)`. 47 | - Added: Additional fields can be added to the client data using `DefaultClientDataWithExtra(ExtraData)`. 48 | - Added: The `Client` now has the ability to adjust the response for quirky relying parties 49 | when a fully featured response would break their server side validation. ([#31](https://github.com/1Password/passkey-rs/pull/31)) 50 | - ⚠ BREAKING: Added the `Origin` enum which is now the origin parameter for the following methods ([#32](https://github.com/1Password/passkey-rs/pull/27)): 51 | - `Client::register` takes an `impl Into` instead of a `&Url` 52 | - `Client::authenticate` takes an `impl Into` instead of a `&Url` 53 | - `RpIdValidator::assert_domain` takes an `&Origin` instead of a `&Url` 54 | - ⚠ BREAKING: The collected client data will now have the android app signature as the origin when a request comes from an app directly. ([#32](https://github.com/1Password/passkey-rs/pull/27)) 55 | 56 | ## passkey-types v0.3.0 57 | 58 | - `CollectedClientData` is now generic and supports additional strongly typed fields. ([#28](https://github.com/1Password/passkey-rs/pull/28)) 59 | - Changed: `CollectedClientData` has changed to `CollectedClientData` 60 | - The `Client` now returns `CredProps::rk` depending on the authenticator's capabilities. ([#29](https://github.com/1Password/passkey-rs/pull/29)) 61 | - ⚠ BREAKING: Rename webauthn extension outputs to be consistent with inputs. ([#33](https://github.com/1Password/passkey-rs/pull/33)) 62 | - ⚠ BREAKING: Create new extension inputs for the CTAP authenticator inputs. ([#33](https://github.com/1Password/passkey-rs/pull/33)) 63 | - ⚠ BREAKING: Add unsigned extension outputs for the CTAP authenticator outputs. ([#34](https://github.com/1Password/passkey-rs/pull/33)) 64 | - ⚠ BREAKING: Add ability for `Passkey` to store associated extension data. ([#36](https://github.com/1Password/passkey-rs/pull/36)) 65 | - ⚠ BREAKING: Change version and extension information in `ctap2::get_info` from strings to enums. ([#39](https://github.com/1Password/passkey-rs/pull/39)) 66 | - ⚠ BREAKING: Add missing CTAP2.1 fields to `make_credential::Response` and `get_assertion::Response`. ([#39](https://github.com/1Password/passkey-rs/pull/39)) 67 | - Make the `PublicKeyCredential` outputs equatable in swift. ([#39](https://github.com/1Password/passkey-rs/pull/39)) 68 | 69 | ## Passkey v0.2.0 70 | ### passkey-types v0.2.0 71 | 72 | Most of these changes are adding fields to structs which are breaking changes due to the current lack of builder methods for these types. Due to this, additions of fields to structs or variants to enums won't be marked as breaking in this release's notes. Other types of breaking changes will be explicitly called out. 73 | 74 | - ⚠ BREAKING: Update `bitflags` from v1 to v2. This means `ctap2::Flags` no longer implement `PartialOrd`, `Ord` and `Hash` as those traits aren't applicable. 75 | - Added a `transports` field to `ctap2::get_info::Response` 76 | - Changes in `webauthn::PublicKeyCredential`: 77 | - ⚠ BREAKING: `authenticator_attachment` is now optional 78 | - ⚠ BREAKING: `client_extension_results`'s type has been renamed from `AuthenticationExtensionsClientOutputs` to `AuthenticatorExtensionsClientOutputs` 79 | - Changes for `webauthn::PublicKeyCredentialRequestOptions`: 80 | - `timeout` now supports deserializing from a stringified number 81 | - `user_verification` will now ignore unknown values instead of returning an error on deserialization 82 | - Add `hints` field (#9) 83 | - Add `attestation` and `attestation_formats` fields 84 | - Changes for `webauthn::AuthenticatorAssertionResponse` 85 | - Add `attestation_object` field 86 | - Changes for `webauthn::PublicKeyCredentialCreationOptions`: 87 | - `timeout` now supports deserializing from a stringified number 88 | - Add `hints` field (#9) 89 | - Add `attestation_formats` field 90 | - Fix `webauthn::CollectedClientData` JSON serialization to correctly follow the spec. (#6) 91 | - Add `unknown_keys` field 92 | - Always serializes `cross_origin` with a boolean even if it is set to `None` 93 | - ⚠ BREAKING: Remove from `#[typeshare]` generation as `#[serde(flatten)]` on `unknown_keys` is not supported. 94 | - Add `webauthn::ClientDataType::PaymentGet` variant. 95 | - Make all enums with unit variants `Clone`, `Copy`, `PartialEq` and `Eq` 96 | - Add support for the `CredProps` extension with `authenticatorDisplayName` 97 | 98 | ### passkey-authenticator v0.2.0 99 | 100 | - Add `Authenticator::transports(Vec)` builder method for customizing the transports during credential creation. The default is `internal` and `hybrid`. 101 | - Add `Authenticator:{set_display_name, display_name}` methods for setting a display name for the `CredProps` extension's `authenticatorDisplayName`. 102 | - Update `p256` to version `0.13` 103 | - Update `signature` to version `2` 104 | 105 | ### passkey-client v0.2.0 106 | 107 | - Add `WebauthnError::is_vendor_error()` for verifying if the internal CTAP error was in the range of `passkey_types::ctap2::VendorError` 108 | - Break out Rp Id verification from the `Client` into its own `RpIdVerifier` which it now uses internally. This allows the use of `RpIdVerifier::assert_domain` publicly now instead of it being a private method to client without the need for everything else the client needs. 109 | - `Client::register` now handles `CredProps` extension requests. 110 | - Update `idna` to version `0.5` 111 | 112 | ### public-suffix v0.1.1 113 | 114 | - Update the public suffix list 115 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "passkey", 5 | "passkey-authenticator", 6 | "passkey-client", 7 | "passkey-transports", 8 | "passkey-types", 9 | "public-suffix", 10 | ] 11 | 12 | [workspace.package] 13 | authors = ["1Password"] 14 | repository = "https://github.com/1Password/passkey-rs" 15 | edition = "2021" 16 | license = "MIT OR Apache-2.0" 17 | keywords = ["passkey", "webauthn", "fido2", "passwordless", "ctap"] 18 | categories = ["authentication"] 19 | 20 | [workspace.lints.rust] 21 | missing_docs = "warn" 22 | unused_must_use = "forbid" 23 | unused-qualifications = "deny" 24 | 25 | [workspace.lints.rustdoc] 26 | broken_intra_doc_links = "deny" 27 | 28 | [workspace.lints.clippy] 29 | dbg_macro = "deny" 30 | unimplemented = "deny" 31 | todo = "deny" 32 | unused_async = "deny" 33 | undocumented_unsafe_blocks = "deny" 34 | as_conversions = "deny" 35 | result_unit_err = "deny" -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 1Password 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /passkey-authenticator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "passkey-authenticator" 3 | version = "0.4.0" 4 | description = "A webauthn authenticator supporting passkeys." 5 | include = ["src/", "../LICENSE-APACHE", "../LICENSE-MIT"] 6 | readme = "README.md" 7 | authors.workspace = true 8 | repository.workspace = true 9 | edition.workspace = true 10 | license.workspace = true 11 | keywords.workspace = true 12 | categories.workspace = true 13 | 14 | [lints] 15 | workspace = true 16 | 17 | [features] 18 | default = [] 19 | tokio = ["dep:tokio"] 20 | testable = ["dep:mockall", "passkey-types/testable"] 21 | 22 | [dependencies] 23 | async-trait = "0.1" 24 | coset = "0.3" 25 | log = "0.4" 26 | mockall = { version = "0.11", optional = true } 27 | p256 = { version = "0.13", features = ["pem", "arithmetic", "jwk"] } 28 | passkey-types = { path = "../passkey-types", version = "0.4" } 29 | rand = "0.8" 30 | tokio = { version = "1", features = ["sync"], optional = true } 31 | 32 | [dev-dependencies] 33 | mockall = { version = "0.11" } 34 | passkey-types = { path = "../passkey-types", version = "0.4", features = [ 35 | "testable", 36 | ] } 37 | tokio = { version = "1", features = ["sync", "macros", "rt"] } 38 | generic-array = { version = "0.14", default-features = false } 39 | signature = { version = "2", features = ["rand_core"] } 40 | -------------------------------------------------------------------------------- /passkey-authenticator/README.md: -------------------------------------------------------------------------------- 1 | # Passkey Authenticator 2 | 3 | [![github]](https://github.com/1Password/passkey-rs/tree/main/passkey-authenticator) 4 | [![version]](https://crates.io/crates/passkey-authenticator) 5 | [![documentation]](https://docs.rs/passkey-authenticator/) 6 | 7 | This crate defines an Authenticator type along with a basic implementation of the [CTAP 2.0] specification. The `Authenticator` struct is designed in such a way that storage and user interaction are defined through traits, allowing only the parts that vary between vendors, but keeping the specification compliant implementation regardless of vendor. This is why the `Ctap2Api` trait is sealed, to prevent external implementations. 8 | 9 | [github]: https://img.shields.io/badge/GitHub-1Password%2Fpasskey--rs%2Fpasskey--authenticator-informational?logo=github&style=flat 10 | [version]: https://img.shields.io/crates/v/passkey-authenticator?logo=rust&style=flat 11 | [documentation]: https://img.shields.io/docsrs/passkey-authenticator/latest?logo=docs.rs&style=flat 12 | [CTAP 2.0]: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html 13 | 14 | ## Why RustCrypto? 15 | 16 | For targeting WASM, yes there are other cryptographic libraries out there that allow targeting WASM, but none of them are as easy to compile to wasm than the pure rust implementations of the [RustCrypto] libraries. Now this does come with limitations, so there are plans to provide a similar backing trait to "plug-in" the desired cryptography from a vendor. Work is ongoing for this. 17 | 18 | [RustCrypto]: https://github.com/RustCrypto 19 | -------------------------------------------------------------------------------- /passkey-authenticator/src/authenticator/extensions.rs: -------------------------------------------------------------------------------- 1 | //! The authenticator extensions as defined in [CTAP2 Defined Extensions][ctap2] or in 2 | //! [WebAuthn Defined Extensions][webauthn]. 3 | //! 4 | //! The currently supported extensions are: 5 | //! * [`HmacSecret`][HmacSecretConfig] 6 | //! * [AuthenticatorDisplayName] 7 | //! 8 | //! [ctap2]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#sctn-defined-extensions 9 | //! [webauthn]: https://w3c.github.io/webauthn/#sctn-defined-extensions 10 | //! [AuthenticatorDisplayName]: https://w3c.github.io/webauthn/#dom-credentialpropertiesoutput-authenticatordisplayname 11 | 12 | use passkey_types::{ 13 | ctap2::{get_assertion, get_info, make_credential, StatusCode}, 14 | Passkey, 15 | }; 16 | 17 | mod hmac_secret; 18 | pub use hmac_secret::{HmacSecretConfig, HmacSecretCredentialSupport}; 19 | 20 | #[cfg(test)] 21 | pub(crate) use hmac_secret::tests::prf_eval_request; 22 | 23 | #[cfg(doc)] 24 | use passkey_types::webauthn; 25 | 26 | use crate::Authenticator; 27 | 28 | #[derive(Debug, Default)] 29 | #[non_exhaustive] 30 | pub(super) struct Extensions { 31 | /// Extension to retrieve a symmetric secret from the authenticator. 32 | pub hmac_secret: Option, 33 | } 34 | 35 | impl Extensions { 36 | /// Get a list of extensions that are currently supported by this instance. 37 | pub fn list_extensions(&self) -> Option> { 38 | // We don't support Pin UV auth yet so we will only support the unsigned prf extension 39 | let prf = self 40 | .hmac_secret 41 | .is_some() 42 | .then_some(get_info::Extension::Prf); 43 | 44 | prf.map(|ext| vec![ext]) 45 | } 46 | } 47 | 48 | pub(super) struct MakeExtensionOutputs { 49 | pub signed: Option, 50 | pub unsigned: Option, 51 | pub credential: passkey_types::CredentialExtensions, 52 | } 53 | 54 | #[derive(Default)] 55 | pub(super) struct GetExtensionOutputs { 56 | pub signed: Option, 57 | pub unsigned: Option, 58 | } 59 | 60 | impl Authenticator { 61 | pub(super) fn make_extensions( 62 | &self, 63 | request: Option, 64 | uv: bool, 65 | ) -> Result { 66 | let request = request.and_then(|r| r.zip_contents()); 67 | let pk_extensions = self.make_passkey_extensions(request.as_ref()); 68 | 69 | let prf = request 70 | .and_then(|ext| { 71 | ext.prf.and_then(|input| { 72 | self.make_prf(pk_extensions.hmac_secret.as_ref(), input, uv) 73 | .transpose() 74 | }) 75 | }) 76 | .transpose()?; 77 | 78 | Ok(MakeExtensionOutputs { 79 | signed: None, 80 | unsigned: make_credential::UnsignedExtensionOutputs { prf }.zip_contents(), 81 | credential: pk_extensions, 82 | }) 83 | } 84 | 85 | fn make_passkey_extensions( 86 | &self, 87 | request: Option<&make_credential::ExtensionInputs>, 88 | ) -> passkey_types::CredentialExtensions { 89 | let should_build_hmac_secret = 90 | request.and_then(|r| r.hmac_secret.or(Some(r.prf.is_some()))); 91 | let hmac_secret = self.make_hmac_secret(should_build_hmac_secret); 92 | 93 | passkey_types::CredentialExtensions { hmac_secret } 94 | } 95 | 96 | pub(super) fn get_extensions( 97 | &self, 98 | passkey: &Passkey, 99 | request: Option, 100 | uv: bool, 101 | ) -> Result { 102 | let Some(ext) = request.and_then(get_assertion::ExtensionInputs::zip_contents) else { 103 | return Ok(Default::default()); 104 | }; 105 | 106 | let prf = ext 107 | .prf 108 | .and_then(|salts| { 109 | self.get_prf( 110 | &passkey.credential_id, 111 | passkey.extensions.hmac_secret.as_ref(), 112 | salts, 113 | uv, 114 | ) 115 | .transpose() 116 | }) 117 | .transpose()?; 118 | 119 | Ok(GetExtensionOutputs { 120 | signed: None, 121 | unsigned: get_assertion::UnsignedExtensionOutputs { prf }.zip_contents(), 122 | }) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /passkey-authenticator/src/authenticator/get_info.rs: -------------------------------------------------------------------------------- 1 | use passkey_types::ctap2::get_info::{Options, Response, Version}; 2 | 3 | use crate::{ 4 | credential_store::DiscoverabilitySupport, Authenticator, CredentialStore, UserValidationMethod, 5 | }; 6 | 7 | impl Authenticator { 8 | /// Using this method, the host can request that the authenticator report a list of all 9 | /// supported protocol versions, supported extensions, AAGUID of the device, and its capabilities. 10 | pub async fn get_info(&self) -> Response { 11 | Response { 12 | versions: vec![Version::FIDO_2_0, Version::U2F_V2], 13 | extensions: self.extensions.list_extensions(), 14 | aaguid: *self.aaguid(), 15 | options: Some(Options { 16 | rk: self.store.get_info().await.discoverability 17 | != DiscoverabilitySupport::OnlyNonDiscoverable, 18 | uv: self.user_validation.is_verification_enabled(), 19 | up: self.user_validation.is_presence_enabled(), 20 | ..Default::default() 21 | }), 22 | max_msg_size: None, 23 | pin_protocols: None, 24 | transports: Some(self.transports.clone()), 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /passkey-authenticator/src/credential_store.rs: -------------------------------------------------------------------------------- 1 | #[cfg(any(feature = "tokio", test))] 2 | use std::sync::Arc; 3 | 4 | use passkey_types::{ 5 | ctap2::{ 6 | get_assertion::Options, 7 | make_credential::{PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity}, 8 | Ctap2Error, StatusCode, 9 | }, 10 | webauthn::PublicKeyCredentialDescriptor, 11 | Passkey, 12 | }; 13 | 14 | /// A struct that defines the capabilities of a store. 15 | pub struct StoreInfo { 16 | /// How the store handles discoverability. 17 | pub discoverability: DiscoverabilitySupport, 18 | } 19 | 20 | /// Enum to define how the store handles discoverability. 21 | /// Note that this does not say anything about which storage mode will be used. 22 | #[derive(PartialEq)] 23 | pub enum DiscoverabilitySupport { 24 | /// The store supports both discoverable and non-credentials. 25 | Full, 26 | 27 | /// The store only supports non-discoverable credentials. 28 | /// An error will be returned if a discoverable credential is requested. 29 | OnlyNonDiscoverable, 30 | 31 | /// The store only supports discoverable credential. 32 | /// No error will be returned if a non-discoverable credential is requested. 33 | ForcedDiscoverable, 34 | } 35 | 36 | impl DiscoverabilitySupport { 37 | /// Helper method to determine if the store created a discoverable credential or not. 38 | pub fn is_passkey_discoverable(&self, rk_input: bool) -> bool { 39 | match self { 40 | DiscoverabilitySupport::Full => rk_input, 41 | DiscoverabilitySupport::OnlyNonDiscoverable => false, 42 | DiscoverabilitySupport::ForcedDiscoverable => true, 43 | } 44 | } 45 | } 46 | 47 | /// Use this on a type that enables storage and fetching of credentials 48 | #[async_trait::async_trait] 49 | pub trait CredentialStore { 50 | /// Defines the return type of find_credentials(...) 51 | type PasskeyItem: TryInto + Send + Sync; 52 | 53 | /// Find all credentials matching the given `ids` and `rp_id`. 54 | /// 55 | /// If multiple are found, it is recommended to sort the credentials using their creation date 56 | /// before returning as the algorithm will take the first credential from the list for assertions. 57 | async fn find_credentials( 58 | &self, 59 | ids: Option<&[PublicKeyCredentialDescriptor]>, 60 | rp_id: &str, 61 | ) -> Result, StatusCode>; 62 | 63 | /// Save the new credential into your store 64 | async fn save_credential( 65 | &mut self, 66 | cred: Passkey, 67 | user: PublicKeyCredentialUserEntity, 68 | rp: PublicKeyCredentialRpEntity, 69 | options: Options, 70 | ) -> Result<(), StatusCode>; 71 | 72 | /// Update the credential in your store 73 | async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode>; 74 | 75 | /// Get information about the store 76 | async fn get_info(&self) -> StoreInfo; 77 | } 78 | 79 | /// In-memory store for Passkeys 80 | /// 81 | /// Useful for tests. 82 | pub type MemoryStore = std::collections::HashMap, Passkey>; 83 | 84 | #[async_trait::async_trait] 85 | impl CredentialStore for MemoryStore { 86 | type PasskeyItem = Passkey; 87 | 88 | async fn find_credentials( 89 | &self, 90 | allow_credentials: Option<&[PublicKeyCredentialDescriptor]>, 91 | _rp_id: &str, 92 | ) -> Result, StatusCode> { 93 | let creds: Vec = allow_credentials 94 | .into_iter() 95 | .flatten() 96 | .filter_map(|id| self.get(&*id.id)) 97 | .cloned() 98 | .collect(); 99 | if creds.is_empty() { 100 | Err(Ctap2Error::NoCredentials.into()) 101 | } else { 102 | Ok(creds) 103 | } 104 | } 105 | 106 | async fn save_credential( 107 | &mut self, 108 | cred: Passkey, 109 | _user: PublicKeyCredentialUserEntity, 110 | _rp: PublicKeyCredentialRpEntity, 111 | _options: Options, 112 | ) -> Result<(), StatusCode> { 113 | self.insert(cred.credential_id.clone().into(), cred); 114 | Ok(()) 115 | } 116 | 117 | async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> { 118 | self.insert(cred.credential_id.clone().into(), cred); 119 | Ok(()) 120 | } 121 | 122 | async fn get_info(&self) -> StoreInfo { 123 | StoreInfo { 124 | discoverability: DiscoverabilitySupport::ForcedDiscoverable, 125 | } 126 | } 127 | } 128 | 129 | #[async_trait::async_trait] 130 | impl CredentialStore for Option { 131 | type PasskeyItem = Passkey; 132 | 133 | async fn find_credentials( 134 | &self, 135 | id: Option<&[PublicKeyCredentialDescriptor]>, 136 | _rp_id: &str, 137 | ) -> Result, StatusCode> { 138 | if let Some(id) = id { 139 | id.iter().find_map(|id| { 140 | // TODO: && pk.rp_id == rp_id) need rp_id on typeshared passkey 141 | self.clone().filter(|pk| pk.credential_id == id.id) 142 | }) 143 | } else { 144 | self.clone() // TODO: .filter(|pk| pk.rp_id == rp_id) need rp_id on typeshared passkey 145 | } 146 | .map(|pk| vec![pk]) 147 | .ok_or(Ctap2Error::NoCredentials.into()) 148 | } 149 | 150 | async fn save_credential( 151 | &mut self, 152 | cred: Passkey, 153 | _user: PublicKeyCredentialUserEntity, 154 | _rp: PublicKeyCredentialRpEntity, 155 | _options: Options, 156 | ) -> Result<(), StatusCode> { 157 | self.replace(cred); 158 | Ok(()) 159 | } 160 | 161 | async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> { 162 | self.replace(cred); 163 | Ok(()) 164 | } 165 | 166 | async fn get_info(&self) -> StoreInfo { 167 | StoreInfo { 168 | discoverability: DiscoverabilitySupport::ForcedDiscoverable, 169 | } 170 | } 171 | } 172 | 173 | #[cfg(any(feature = "tokio", test))] 174 | #[async_trait::async_trait] 175 | impl + Send + Sync> CredentialStore 176 | for Arc> 177 | { 178 | type PasskeyItem = Passkey; 179 | 180 | async fn find_credentials( 181 | &self, 182 | ids: Option<&[PublicKeyCredentialDescriptor]>, 183 | rp_id: &str, 184 | ) -> Result, StatusCode> { 185 | self.lock().await.find_credentials(ids, rp_id).await 186 | } 187 | 188 | async fn save_credential( 189 | &mut self, 190 | cred: Passkey, 191 | user: PublicKeyCredentialUserEntity, 192 | rp: PublicKeyCredentialRpEntity, 193 | options: Options, 194 | ) -> Result<(), StatusCode> { 195 | self.lock() 196 | .await 197 | .save_credential(cred, user, rp, options) 198 | .await 199 | } 200 | 201 | async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> { 202 | self.lock().await.update_credential(cred).await 203 | } 204 | 205 | async fn get_info(&self) -> StoreInfo { 206 | self.lock().await.get_info().await 207 | } 208 | } 209 | 210 | #[cfg(any(feature = "tokio", test))] 211 | #[async_trait::async_trait] 212 | impl + Send + Sync> CredentialStore 213 | for Arc> 214 | { 215 | type PasskeyItem = Passkey; 216 | 217 | async fn find_credentials( 218 | &self, 219 | ids: Option<&[PublicKeyCredentialDescriptor]>, 220 | rp_id: &str, 221 | ) -> Result, StatusCode> { 222 | self.read().await.find_credentials(ids, rp_id).await 223 | } 224 | 225 | async fn save_credential( 226 | &mut self, 227 | cred: Passkey, 228 | user: PublicKeyCredentialUserEntity, 229 | rp: PublicKeyCredentialRpEntity, 230 | options: Options, 231 | ) -> Result<(), StatusCode> { 232 | self.write() 233 | .await 234 | .save_credential(cred, user, rp, options) 235 | .await 236 | } 237 | 238 | async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> { 239 | self.write().await.update_credential(cred).await 240 | } 241 | 242 | async fn get_info(&self) -> StoreInfo { 243 | self.read().await.get_info().await 244 | } 245 | } 246 | 247 | #[cfg(any(feature = "tokio", test))] 248 | #[async_trait::async_trait] 249 | impl + Send + Sync> CredentialStore 250 | for tokio::sync::Mutex 251 | { 252 | type PasskeyItem = Passkey; 253 | 254 | async fn find_credentials( 255 | &self, 256 | ids: Option<&[PublicKeyCredentialDescriptor]>, 257 | rp_id: &str, 258 | ) -> Result, StatusCode> { 259 | self.lock().await.find_credentials(ids, rp_id).await 260 | } 261 | 262 | async fn save_credential( 263 | &mut self, 264 | cred: Passkey, 265 | user: PublicKeyCredentialUserEntity, 266 | rp: PublicKeyCredentialRpEntity, 267 | options: Options, 268 | ) -> Result<(), StatusCode> { 269 | self.lock() 270 | .await 271 | .save_credential(cred, user, rp, options) 272 | .await 273 | } 274 | 275 | async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> { 276 | self.lock().await.update_credential(cred).await 277 | } 278 | 279 | async fn get_info(&self) -> StoreInfo { 280 | self.lock().await.get_info().await 281 | } 282 | } 283 | 284 | #[cfg(any(feature = "tokio", test))] 285 | #[async_trait::async_trait] 286 | impl + Send + Sync> CredentialStore 287 | for tokio::sync::RwLock 288 | { 289 | type PasskeyItem = Passkey; 290 | 291 | async fn find_credentials( 292 | &self, 293 | ids: Option<&[PublicKeyCredentialDescriptor]>, 294 | rp_id: &str, 295 | ) -> Result, StatusCode> { 296 | self.read().await.find_credentials(ids, rp_id).await 297 | } 298 | 299 | async fn save_credential( 300 | &mut self, 301 | cred: Passkey, 302 | user: PublicKeyCredentialUserEntity, 303 | rp: PublicKeyCredentialRpEntity, 304 | options: Options, 305 | ) -> Result<(), StatusCode> { 306 | self.write() 307 | .await 308 | .save_credential(cred, user, rp, options) 309 | .await 310 | } 311 | 312 | async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> { 313 | self.write().await.update_credential(cred).await 314 | } 315 | 316 | async fn get_info(&self) -> StoreInfo { 317 | self.read().await.get_info().await 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /passkey-authenticator/src/ctap2.rs: -------------------------------------------------------------------------------- 1 | //! Ctap 2.0 Authenticator API 2 | //! 3 | //! This module defines the [`Ctap2Api`] trait which is sealed to the [`Authenticator`] type and a 4 | //! future `RemoteAuthenticator` type wich will implement the different transports. 5 | //! 6 | //! 7 | 8 | use passkey_types::ctap2::{get_assertion, get_info, make_credential, StatusCode}; 9 | 10 | use crate::{Authenticator, CredentialStore, UserValidationMethod}; 11 | 12 | mod sealed { 13 | use crate::{Authenticator, CredentialStore, UserValidationMethod}; 14 | 15 | pub trait Sealed {} 16 | 17 | impl Sealed for Authenticator {} 18 | } 19 | 20 | /// Methods defined as being required for a [CTAP 2.0] compliant authenticator to implement. 21 | /// 22 | /// This trait is sealed to prevent missuse and to prevent incorrect implementations in the wild. 23 | /// If you need to define an authenticator please use the [`Authenticator`] struct which provides 24 | /// the necessary generics to customize storage and UI interactions. 25 | /// 26 | /// These methods are provided as traits in order to have a remotely connected authenticators through 27 | /// the different transports defined in [CTAP 2.0]. 28 | /// 29 | /// [CTAP 2.0]: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html 30 | #[async_trait::async_trait] 31 | pub trait Ctap2Api: sealed::Sealed { 32 | /// Request to get the information of the authenticator and see what it supports. 33 | async fn get_info(&self) -> get_info::Response; 34 | 35 | /// Request to create and save a new credential in the authenticator. 36 | async fn make_credential( 37 | &mut self, 38 | request: make_credential::Request, 39 | ) -> Result; 40 | 41 | /// Request to assert a user's existing credential that might exist in the authenticator. 42 | async fn get_assertion( 43 | &self, 44 | request: get_assertion::Request, 45 | ) -> Result; 46 | } 47 | 48 | #[async_trait::async_trait] 49 | impl Ctap2Api for Authenticator 50 | where 51 | S: CredentialStore + Sync + Send, 52 | U: UserValidationMethod + Sync + Send, 53 | { 54 | async fn get_info(&self) -> get_info::Response { 55 | self.get_info().await 56 | } 57 | 58 | async fn make_credential( 59 | &mut self, 60 | request: make_credential::Request, 61 | ) -> Result { 62 | self.make_credential(request).await 63 | } 64 | 65 | async fn get_assertion( 66 | &self, 67 | request: get_assertion::Request, 68 | ) -> Result { 69 | self.get_assertion(request).await 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /passkey-authenticator/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Passkey Authenticator 2 | //! 3 | //! [![github]](https://github.com/1Password/passkey-rs/tree/main/passkey-authenticator) 4 | //! [![version]](https://crates.io/crates/passkey-authenticator) 5 | //! [![documentation]](https://docs.rs/passkey-authenticator/) 6 | //! 7 | //! This crate defines an [`Authenticator`] type along with a basic implementation of the [CTAP 2.0] 8 | //! specification. The [`Authenticator`] struct is designed in such a way that storage and user 9 | //! interaction are defined through traits, allowing only the parts that vary between vendors, 10 | //! but keeping the specification compliant implementation regardless of vendor. This is why the 11 | //! [`Ctap2Api`] trait is sealed, to prevent external implementations. 12 | //! 13 | //! ## Why RustCrypto? 14 | //! 15 | //! For targeting WASM, yes there are other cryptographic libraries out there that allow targeting 16 | //! WASM, but none of them are as easy to compile to wasm than the pure rust implementations of the 17 | //! [RustCrypto] libraries. Now this does come with limitations, so there are plans to provide a 18 | //! similar backing trait to "plug-in" the desired cryptography from a vendor. Work is ongoing for this. 19 | //! 20 | //! [github]: https://img.shields.io/badge/GitHub-1Password%2Fpasskey--rs%2Fpasskey--authenticator-informational?logo=github&style=flat 21 | //! [version]: https://img.shields.io/crates/v/passkey-authenticator?logo=rust&style=flat 22 | //! [documentation]: https://img.shields.io/docsrs/passkey-authenticator/latest?logo=docs.rs&style=flat 23 | //! [CTAP 2.0]: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html 24 | //! [RustCrypto]: https://github.com/RustCrypto 25 | 26 | mod authenticator; 27 | mod credential_store; 28 | mod ctap2; 29 | mod u2f; 30 | mod user_validation; 31 | 32 | use coset::{ 33 | iana::{self, Algorithm, EnumI64}, 34 | CoseKey, CoseKeyBuilder, 35 | }; 36 | use p256::{ 37 | ecdsa::SigningKey, 38 | elliptic_curve::{generic_array::GenericArray, sec1::FromEncodedPoint}, 39 | pkcs8::EncodePublicKey, 40 | EncodedPoint, PublicKey, SecretKey, 41 | }; 42 | use passkey_types::{ctap2::Ctap2Error, Bytes}; 43 | 44 | pub use self::{ 45 | authenticator::{extensions, Authenticator, CredentialIdLength}, 46 | credential_store::{CredentialStore, DiscoverabilitySupport, MemoryStore, StoreInfo}, 47 | ctap2::Ctap2Api, 48 | u2f::U2fApi, 49 | user_validation::{UserCheck, UserValidationMethod}, 50 | }; 51 | 52 | #[cfg(any(test, feature = "testable"))] 53 | pub use self::user_validation::MockUserValidationMethod; 54 | 55 | /// Extract a cryptographic secret key from a [`CoseKey`]. 56 | // possible candidate for a `passkey-crypto` crate? 57 | fn private_key_from_cose_key(key: &CoseKey) -> Result { 58 | if !matches!( 59 | key.alg, 60 | Some(coset::RegisteredLabelWithPrivate::Assigned( 61 | Algorithm::ES256 62 | )) 63 | ) { 64 | return Err(Ctap2Error::UnsupportedAlgorithm); 65 | } 66 | if !matches!( 67 | key.kty, 68 | coset::RegisteredLabel::Assigned(iana::KeyType::EC2) 69 | ) { 70 | return Err(Ctap2Error::InvalidCredential); 71 | } 72 | 73 | key.params 74 | .iter() 75 | .find_map(|(k, v)| { 76 | if let coset::Label::Int(i) = k { 77 | iana::Ec2KeyParameter::from_i64(*i) 78 | .filter(|p| p == &iana::Ec2KeyParameter::D) 79 | .and_then(|_| v.as_bytes()) 80 | .and_then(|b| SecretKey::from_slice(b).ok()) 81 | } else { 82 | None 83 | } 84 | }) 85 | .ok_or(Ctap2Error::InvalidCredential) 86 | } 87 | 88 | /// Convert a Cose Key to a X.509 SubjectPublicKeyInfo formatted byte array. 89 | /// 90 | /// This should be used by the client when creating the [Easy Credential Data Accessors][ez] 91 | /// 92 | /// [ez]: https://w3c.github.io/webauthn/#sctn-public-key-easy 93 | pub fn public_key_der_from_cose_key(key: &CoseKey) -> Result { 94 | if !matches!( 95 | key.alg, 96 | Some(coset::RegisteredLabelWithPrivate::Assigned( 97 | Algorithm::ES256 98 | )) 99 | ) { 100 | return Err(Ctap2Error::UnsupportedAlgorithm); 101 | } 102 | if !matches!( 103 | key.kty, 104 | coset::RegisteredLabel::Assigned(iana::KeyType::EC2) 105 | ) { 106 | return Err(Ctap2Error::InvalidCredential); 107 | } 108 | 109 | let (mut x, mut y) = (None, None); 110 | for (key, value) in &key.params { 111 | if let coset::Label::Int(i) = key { 112 | let key = iana::Ec2KeyParameter::from_i64(*i).ok_or(Ctap2Error::InvalidCbor)?; 113 | match key { 114 | iana::Ec2KeyParameter::X => { 115 | if value.as_bytes().and_then(|v| x.replace(v)).is_some() { 116 | log::warn!("Cose key has multiple entries for X coordinate"); 117 | } 118 | } 119 | iana::Ec2KeyParameter::Y => { 120 | if value.as_bytes().and_then(|v| y.replace(v)).is_some() { 121 | log::warn!("Cose key has multiple entries for Y coordinate"); 122 | } 123 | } 124 | _ => (), 125 | } 126 | } 127 | } 128 | let (Some(x), Some(y)) = (x, y) else { 129 | return Err(Ctap2Error::CborUnexpectedType); 130 | }; 131 | 132 | let point = EncodedPoint::from_affine_coordinates( 133 | GenericArray::from_slice(x.as_slice()), 134 | GenericArray::from_slice(y.as_slice()), 135 | false, 136 | ); 137 | let Some(pub_key): Option = PublicKey::from_encoded_point(&point).into() else { 138 | return Err(Ctap2Error::InvalidCredential); 139 | }; 140 | pub_key 141 | .to_public_key_der() 142 | .map_err(|_| Ctap2Error::InvalidCredential) 143 | .map(|pk| pk.as_ref().to_vec().into()) 144 | } 145 | 146 | pub(crate) struct CoseKeyPair { 147 | public: CoseKey, 148 | private: CoseKey, 149 | } 150 | 151 | impl CoseKeyPair { 152 | fn from_secret_key(private_key: &SecretKey, algorithm: Algorithm) -> Self { 153 | let public_key = SigningKey::from(private_key) 154 | .verifying_key() 155 | .to_encoded_point(false); 156 | // SAFETY: These unwraps are safe because the public_key above is not compressed (false 157 | // parameter) therefore x and y are guarateed to contain values. 158 | let x = public_key.x().unwrap().as_slice().to_vec(); 159 | let y = public_key.y().unwrap().as_slice().to_vec(); 160 | let private = CoseKeyBuilder::new_ec2_priv_key( 161 | iana::EllipticCurve::P_256, 162 | x.clone(), 163 | y.clone(), 164 | private_key.to_bytes().to_vec(), 165 | ) 166 | .algorithm(algorithm) 167 | .build(); 168 | let public = CoseKeyBuilder::new_ec2_pub_key(iana::EllipticCurve::P_256, x, y) 169 | .algorithm(algorithm) 170 | .build(); 171 | 172 | Self { public, private } 173 | } 174 | } 175 | 176 | #[cfg(test)] 177 | mod tests { 178 | use coset::iana; 179 | use p256::{ 180 | ecdsa::{ 181 | signature::{Signer, Verifier}, 182 | SigningKey, 183 | }, 184 | SecretKey, 185 | }; 186 | use passkey_types::{ctap2::AuthenticatorData, rand::random_vec}; 187 | 188 | use super::{private_key_from_cose_key, CoseKeyPair}; 189 | 190 | #[test] 191 | fn private_key_cose_round_trip_sanity_check() { 192 | let private_key = { 193 | let mut rng = rand::thread_rng(); 194 | SecretKey::random(&mut rng) 195 | }; 196 | let CoseKeyPair { 197 | private: private_cose, 198 | .. 199 | } = CoseKeyPair::from_secret_key(&private_key, iana::Algorithm::ES256); 200 | let public_signing_key = SigningKey::from(&private_key); 201 | let public_key = public_signing_key.verifying_key(); 202 | 203 | let auth_data = AuthenticatorData::new("future.1password.com", None); 204 | let mut signature_target = auth_data.to_vec(); 205 | signature_target.extend(random_vec(32)); 206 | 207 | let secret_key = private_key_from_cose_key(&private_cose).expect("to get a private key"); 208 | 209 | let private_key = SigningKey::from(secret_key); 210 | let signature: p256::ecdsa::Signature = private_key.sign(&signature_target); 211 | 212 | public_key 213 | .verify(&signature_target, &signature) 214 | .expect("failed to verify signature") 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /passkey-authenticator/src/u2f.rs: -------------------------------------------------------------------------------- 1 | //! Follows U2F 1.2 2 | 3 | use crate::{Authenticator, CoseKeyPair, CredentialStore, UserValidationMethod}; 4 | use coset::iana; 5 | use p256::{ 6 | ecdsa::{signature::Signer, SigningKey}, 7 | SecretKey, 8 | }; 9 | use passkey_types::{ 10 | ctap2::{Flags, U2FError}, 11 | u2f::{ 12 | AuthenticationRequest, AuthenticationResponse, PublicKey, RegisterRequest, RegisterResponse, 13 | }, 14 | Bytes, Passkey, 15 | }; 16 | mod sealed { 17 | use crate::{Authenticator, CredentialStore, UserValidationMethod}; 18 | 19 | pub trait Sealed {} 20 | impl Sealed for Authenticator {} 21 | } 22 | 23 | /// Provides the U2F Authenticator API 24 | #[async_trait::async_trait] 25 | pub trait U2fApi: sealed::Sealed { 26 | /// from: RegisterRequest::register() (u2f/register.rs) 27 | async fn register( 28 | &mut self, 29 | request: RegisterRequest, 30 | handle: &[u8], 31 | ) -> Result; 32 | 33 | /// from AuthenticationRequest::authenticate() (u2f/authenticate.rs) 34 | async fn authenticate( 35 | &self, 36 | request: AuthenticationRequest, 37 | counter: u32, 38 | user_presence: Flags, 39 | ) -> Result; 40 | } 41 | 42 | #[async_trait::async_trait] 43 | impl U2fApi 44 | for Authenticator 45 | { 46 | /// Apply a register request and create a credential and respond with the public key of said credential. 47 | async fn register( 48 | &mut self, 49 | request: RegisterRequest, 50 | handle: &[u8], 51 | ) -> Result { 52 | // Create Keypair on P256 curve 53 | let private_key = { 54 | let mut rng = rand::thread_rng(); 55 | SecretKey::random(&mut rng) 56 | }; 57 | 58 | // SAFETY: Can only fail if key is malformed 59 | let CoseKeyPair { public: _, private } = 60 | CoseKeyPair::from_secret_key(&private_key, iana::Algorithm::ES256); 61 | let signing_key = SigningKey::from(private_key); 62 | let public_key = signing_key.verifying_key(); 63 | let pub_key_encoded = public_key.to_encoded_point(false); 64 | 65 | // SAFETY: These unwraps are safe due to the encoding not having any compression (false above) 66 | // this makes sure that both x and y points are present in the encoded and are of 32 bytes 67 | // in size. 68 | let public_key = PublicKey { 69 | x: pub_key_encoded.x().unwrap().as_slice().try_into().unwrap(), 70 | y: pub_key_encoded.y().unwrap().as_slice().try_into().unwrap(), 71 | }; 72 | 73 | // create signature, see [`RegisterResponse::signature`]'s documentation for more information 74 | let signature_target = [0x00] // 1. reserved byte 75 | .into_iter() 76 | .chain(request.application) // 2. application parameter 77 | .chain(request.challenge) // 3. challenge parameter 78 | .chain(handle.iter().copied()) // 4. Key handle 79 | .chain(public_key.encode()) // 5. public key 80 | .collect::>(); 81 | let signature_singleton: p256::ecdsa::Signature = signing_key.sign(&signature_target); 82 | let signature = signature_singleton.to_vec(); 83 | 84 | let attestation_certificate = Vec::new(); 85 | 86 | let response = RegisterResponse { 87 | public_key, 88 | key_handle: handle.into(), 89 | attestation_certificate, 90 | signature, 91 | }; 92 | 93 | let (passkey, user, rp) = 94 | Passkey::wrap_u2f_registration_request(&request, &response, handle, &private); 95 | 96 | // U2F registration does not use rk, uv, or up 97 | let options = passkey_types::ctap2::get_assertion::Options { 98 | rk: false, 99 | uv: false, 100 | up: false, 101 | }; 102 | let result = self 103 | .store_mut() 104 | .save_credential(passkey, user, rp, options) 105 | .await; 106 | 107 | match result { 108 | Ok(_) => Ok(response), 109 | _ => Err(U2FError::Other), 110 | } 111 | } 112 | 113 | /// Apply an authentication request with the appropriate response 114 | async fn authenticate( 115 | &self, 116 | request: AuthenticationRequest, 117 | counter: u32, 118 | user_presence: Flags, 119 | ) -> Result { 120 | // Turn the Authentication Request into a PublicKeyCredentialDescriptor and 121 | // an rp_id in order to find the secret key in our store 122 | 123 | let pk_descriptor = passkey_types::webauthn::PublicKeyCredentialDescriptor { 124 | ty: passkey_types::webauthn::PublicKeyCredentialType::PublicKey, 125 | id: request.key_handle.into(), 126 | transports: None, 127 | }; 128 | let id_bytes: Bytes = request.application.to_vec().into(); 129 | let maybe_credential = self 130 | .store() 131 | .find_credentials(Some(&[pk_descriptor]), String::from(id_bytes).as_str()) 132 | .await 133 | .map_err(|_| U2FError::Other); 134 | 135 | let credential: Passkey = maybe_credential? 136 | .into_iter() 137 | .next() 138 | .ok_or(U2FError::Other)? 139 | .try_into() 140 | .map_err(|_| U2FError::Other)?; 141 | 142 | let secret_key = 143 | super::private_key_from_cose_key(&credential.key).map_err(|_| U2FError::Other)?; 144 | let signing_key = SigningKey::from(secret_key); 145 | 146 | // The following signature_target is specified in the U2F Raw Message Formats spec: 147 | // https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#authentication-response-message-success 148 | // [A signature] is [an] ECDSA signature (on P-256) over the following byte string: 149 | let signature_target = request 150 | .application // 1. The application parameter [32 bytes] from the authentication request message. 151 | .into_iter() 152 | .chain(std::iter::once(user_presence.into())) // 2. The ... user presence byte [1 byte]. 153 | .chain(counter.to_be_bytes()) // 3. The ... counter [4 bytes]. 154 | .chain(request.challenge) // 4. The challenge parameter [32 bytes] from the authentication request message. 155 | .collect::>(); 156 | 157 | let signature: p256::ecdsa::Signature = signing_key.sign(&signature_target); 158 | let signature_bytes = signature.to_der().as_bytes().to_vec(); 159 | 160 | Ok(AuthenticationResponse { 161 | user_presence, 162 | counter, 163 | signature: signature_bytes, 164 | }) 165 | } 166 | } 167 | 168 | #[cfg(test)] 169 | mod tests { 170 | use super::{AuthenticationRequest, Authenticator, RegisterRequest}; 171 | use crate::{u2f::U2fApi, user_validation::MockUserValidationMethod}; 172 | use generic_array::GenericArray; 173 | use p256::{ 174 | ecdsa::{signature::Verifier, Signature, VerifyingKey}, 175 | EncodedPoint, 176 | }; 177 | use passkey_types::{ctap2::Aaguid, *}; 178 | 179 | #[tokio::test] 180 | async fn test_save_u2f_passkey() { 181 | let credstore: Option = None; 182 | let mut authenticator = Authenticator::new( 183 | Aaguid::new_empty(), 184 | credstore, 185 | MockUserValidationMethod::verified_user(0), 186 | ); 187 | 188 | let challenge: [u8; 32] = ::rand::random(); 189 | let application: [u8; 32] = ::rand::random(); 190 | 191 | // Create a U2F request 192 | let reg_request = RegisterRequest { 193 | challenge, 194 | application, 195 | }; 196 | 197 | let handle: [u8; 16] = ::rand::random(); 198 | 199 | // Register the request and assert that it worked. 200 | let store_result = authenticator.register(reg_request, &handle[..]).await; 201 | assert!(store_result.is_ok()); 202 | let response = store_result.unwrap(); 203 | let public_key = response.public_key; 204 | 205 | // Now generate an authentication challenge using the original application 206 | let challenge: [u8; 32] = ::rand::random(); 207 | let auth_req = AuthenticationRequest { 208 | parameter: u2f::AuthenticationParameter::CheckOnly, 209 | application, 210 | challenge, 211 | key_handle: handle.to_vec(), 212 | }; 213 | 214 | // Try to authenticate. 215 | let counter = 181; 216 | let auth_result = authenticator 217 | .authenticate(auth_req, counter, ctap2::Flags::UV) 218 | .await; 219 | assert!(auth_result.is_ok()); 220 | let auth_result = auth_result.unwrap(); 221 | assert_eq!(auth_result.counter, counter); 222 | assert_eq!(auth_result.user_presence, ctap2::Flags::UV); 223 | 224 | // Now can we verify the signature from the Authenticator using the 225 | // public key we received above? 226 | 227 | // Recover the VerifyingKey from the uncompressed X, Y points for the public key 228 | let ep = EncodedPoint::from_affine_coordinates( 229 | &GenericArray::clone_from_slice(&public_key.x), 230 | &GenericArray::clone_from_slice(&public_key.y), 231 | false, 232 | ); 233 | let verifying_key = VerifyingKey::from_encoded_point(&ep).unwrap(); 234 | let sig = Signature::from_der(&auth_result.signature).unwrap(); 235 | 236 | // Generate the expected challenge message that 237 | // the authenticator should have signed. 238 | // See docs for AuthenticationResponse for explanation. 239 | let signature_target = application 240 | .into_iter() 241 | .chain(std::iter::once(auth_result.user_presence.into())) 242 | .chain(auth_result.counter.to_be_bytes()) 243 | .chain(challenge) 244 | .collect::>(); 245 | 246 | // Verify that the given signature is correct for the given message. 247 | assert!(verifying_key.verify(&signature_target, &sig).is_ok()); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /passkey-authenticator/src/user_validation.rs: -------------------------------------------------------------------------------- 1 | use passkey_types::{ctap2::Ctap2Error, Passkey}; 2 | 3 | #[cfg(doc)] 4 | use crate::Authenticator; 5 | 6 | /// The result of a user validation check. 7 | #[derive(Clone, Copy, PartialEq)] 8 | pub struct UserCheck { 9 | /// Indicates whether the user was present. 10 | pub presence: bool, 11 | 12 | /// Indicates whether the user was verified. 13 | pub verification: bool, 14 | } 15 | 16 | /// Pluggable trait for the [`Authenticator`] to do user interaction and verification. 17 | #[cfg_attr(any(test, feature = "testable"), mockall::automock(type PasskeyItem = Passkey;))] 18 | #[async_trait::async_trait] 19 | pub trait UserValidationMethod { 20 | /// The type of the passkey item that can be used to display additional information about the operation to the user. 21 | type PasskeyItem: TryInto + Send + Sync; 22 | 23 | /// Check for the user's presence and obtain consent for the operation. The operation may 24 | /// also require the user to be verified. 25 | /// 26 | /// * `crdential` - Can be used to display additional information about the operation to the user. 27 | /// * `presence` - Indicates whether the user's presence is required. 28 | /// * `verification` - Indicates whether the user should be verified. 29 | async fn check_user<'a>( 30 | &self, 31 | credential: Option<&'a Self::PasskeyItem>, 32 | presence: bool, 33 | verification: bool, 34 | ) -> Result; 35 | 36 | /// Indicates whether this type is capable of testing user presence. 37 | fn is_presence_enabled(&self) -> bool; 38 | 39 | /// Indicates that this type is capable of verifying the user within itself. 40 | /// For example, devices with UI, biometrics fall into this category. 41 | /// 42 | /// If `Some(true)`, it indicates that the device is capable of user verification 43 | /// within itself and has been configured. 44 | /// 45 | /// If Some(false), it indicates that the device is capable of user verification 46 | /// within itself and has not been yet configured. For example, a biometric device that has not 47 | /// yet been configured will return this parameter set to false. 48 | /// 49 | /// If `None`, it indicates that the device is not capable of user verification within itself. 50 | /// 51 | /// A device that can only do Client PIN will set this to `None`. 52 | /// 53 | /// If a device is capable of verifying the user within itself as well as able to do Client PIN, 54 | /// it will return both `Some` and the Client PIN option. 55 | fn is_verification_enabled(&self) -> Option; 56 | } 57 | 58 | #[cfg(any(test, feature = "testable"))] 59 | impl MockUserValidationMethod { 60 | /// Sets up the mock for returning true for the verification. 61 | pub fn verified_user(times: usize) -> Self { 62 | let mut user_mock = MockUserValidationMethod::new(); 63 | user_mock.expect_is_presence_enabled().returning(|| true); 64 | user_mock 65 | .expect_is_verification_enabled() 66 | .returning(|| Some(true)) 67 | .times(..); 68 | user_mock.expect_is_presence_enabled().returning(|| true); 69 | user_mock 70 | .expect_check_user() 71 | .with( 72 | mockall::predicate::always(), 73 | mockall::predicate::eq(true), 74 | mockall::predicate::eq(true), 75 | ) 76 | .returning(|_, _, _| { 77 | Ok(UserCheck { 78 | presence: true, 79 | verification: true, 80 | }) 81 | }) 82 | .times(times); 83 | user_mock 84 | } 85 | 86 | /// Sets up the mock for returning true for the verification. 87 | pub fn verified_user_with_credential(times: usize, credential: Passkey) -> Self { 88 | let mut user_mock = MockUserValidationMethod::new(); 89 | user_mock 90 | .expect_is_verification_enabled() 91 | .returning(|| Some(true)); 92 | user_mock 93 | .expect_check_user() 94 | .withf(move |cred, up, uv| cred == &Some(&credential) && *up && *uv) 95 | .returning(|_, _, _| { 96 | Ok(UserCheck { 97 | presence: true, 98 | verification: true, 99 | }) 100 | }) 101 | .times(times); 102 | user_mock 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /passkey-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "passkey-client" 3 | version = "0.4.0" 4 | description = "Webauthn client in Rust." 5 | include = ["src/", "../LICENSE-APACHE", "../LICENSE-MIT"] 6 | readme = "README.md" 7 | authors.workspace = true 8 | repository.workspace = true 9 | edition.workspace = true 10 | license.workspace = true 11 | keywords.workspace = true 12 | categories.workspace = true 13 | 14 | [lints] 15 | workspace = true 16 | 17 | [features] 18 | tokio = ["dep:tokio"] 19 | testable = ["dep:mockall"] 20 | android-asset-validation = ["dep:nom"] 21 | 22 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 23 | 24 | [dependencies] 25 | passkey-authenticator = { path = "../passkey-authenticator", version = "0.4" } 26 | passkey-types = { path = "../passkey-types", version = "0.4" } 27 | public-suffix = { path = "../public-suffix", version = "0.1" } 28 | serde = { version = "1", features = ["derive"] } 29 | serde_json = "1" 30 | ciborium = "0.2" 31 | mockall = { version = "0.11", optional = true } 32 | typeshare = "1" 33 | idna = "1" 34 | url = "2" 35 | coset = "0.3" 36 | tokio = { version = "1", features = ["sync"], optional = true } 37 | nom = { version = "7", features = ["alloc"], optional = true } 38 | 39 | [dev-dependencies] 40 | coset = "0.3" 41 | mockall = { version = "0.11" } 42 | passkey-authenticator = { path = "../passkey-authenticator", version = "0.4", features = [ 43 | "tokio", 44 | "testable", 45 | ] } 46 | tokio = { version = "1", features = ["macros", "rt"] } 47 | -------------------------------------------------------------------------------- /passkey-client/README.md: -------------------------------------------------------------------------------- 1 | # Passkey Client 2 | 3 | [![github]](https://github.com/1Password/passkey-rs/tree/main/passkey-client) 4 | [![version]](https://crates.io/crates/passkey-client) 5 | [![documentation]](https://docs.rs/passkey-client/) 6 | 7 | This crate defines a `Client` type along with a basic implementation of the [Webauthn] 8 | specification. The `Client` uses an `Authenticator` to perform the actual cryptographic 9 | operations, while the Client itself marshals data to and from the structs received from the Relying Party. 10 | 11 | This crate does not provide any code to perform networking requests to and from Relying Parties. 12 | 13 | [github]: https://img.shields.io/badge/GitHub-1Password%2Fpasskey--rs%2Fpasskey--client-informational?logo=github&style=flat 14 | [version]: https://img.shields.io/crates/v/passkey-client?logo=rust&style=flat 15 | [documentation]: https://img.shields.io/docsrs/passkey-client/latest?logo=docs.rs&style=flat 16 | [Webauthn]: https://w3c.github.io/webauthn/ -------------------------------------------------------------------------------- /passkey-client/src/android.rs: -------------------------------------------------------------------------------- 1 | use nom::{ 2 | bytes::complete::{tag, take_while_m_n}, 3 | character::is_hex_digit, 4 | combinator::map_res, 5 | multi::separated_list1, 6 | IResult, 7 | }; 8 | use std::{borrow::Cow, fmt::Debug, str::from_utf8}; 9 | use url::Url; 10 | 11 | #[derive(Debug, Clone)] 12 | /// An Unverified asset link. 13 | pub struct UnverifiedAssetLink<'a> { 14 | /// Application package name. 15 | package_name: Cow<'a, str>, 16 | /// Fingerprint to compare. 17 | sha256_cert_fingerprint: Vec, 18 | /// Host to lookup the well known asset link. 19 | host: Cow<'a, str>, 20 | /// When sourced from the application statement list or parsed from host for passkeys. 21 | asset_link_url: Url, 22 | } 23 | 24 | impl<'a> UnverifiedAssetLink<'a> { 25 | /// Create a new [`UnverifiedAssetLink`]. 26 | pub fn new( 27 | package_name: impl Into>, 28 | sha256_cert_fingerprint: &str, 29 | host: impl Into>, 30 | asset_link_url: Url, 31 | ) -> Result { 32 | // Is this correct? 33 | // It looks like you can set your own url path. 34 | // https://developers.google.com/digital-asset-links/v1/statements#scaling-to-dozens-of-statements-or-more 35 | if !valid_asset_link_url(&asset_link_url) { 36 | return Err(ValidationError::InvalidAssetLinkUrl); 37 | } 38 | let host = host.into(); 39 | 40 | valid_fingerprint(sha256_cert_fingerprint).map(|sha256_cert_fingerprint| Self { 41 | package_name: package_name.into(), 42 | sha256_cert_fingerprint, 43 | host, 44 | asset_link_url, 45 | }) 46 | } 47 | 48 | /// Fingerprint of the application's signing certificate 49 | pub fn sha256_cert_fingerprint(&self) -> &[u8] { 50 | self.sha256_cert_fingerprint.as_slice() 51 | } 52 | 53 | /// The application's package name 54 | pub fn package_name(&self) -> &str { 55 | &self.package_name 56 | } 57 | 58 | /// The host to lookup the well-known assetlinks 59 | pub fn host(&self) -> &str { 60 | &self.host 61 | } 62 | 63 | /// Get the digital asset Url for validation 64 | pub fn asset_link_url(&self) -> Url { 65 | self.asset_link_url.clone() 66 | } 67 | } 68 | 69 | /// Digital asset fingerprint validation error. 70 | #[derive(Debug)] 71 | pub enum ValidationError { 72 | /// The fingerprint could not be parsed. 73 | ParseFailed(String), 74 | /// The fingerprint had an invalid length. 75 | InvalidLength, 76 | /// The asset link url is not secure or incorrect path. 77 | InvalidAssetLinkUrl, 78 | } 79 | 80 | impl From>> for ValidationError { 81 | fn from(value: nom::Err>) -> Self { 82 | let code_msg = value.map(|err| format!("{:?}", err.code)); 83 | let message = match code_msg { 84 | nom::Err::Incomplete(_) => "Parsing incomplete".to_owned(), 85 | nom::Err::Error(msg) => format!("Parsing error: {msg}"), 86 | nom::Err::Failure(msg) => format!("Parsing failure: {msg}"), 87 | }; 88 | 89 | ValidationError::ParseFailed(message) 90 | } 91 | } 92 | 93 | /// Make sure we have an expected fingerprint. Characters have to be uppercase. 94 | /// 95 | /// 96 | /// * Having a lower case signature in assetlinks.json. The signature should be 97 | /// in upper case. 98 | pub fn valid_fingerprint(fingerprint: &str) -> Result, ValidationError> { 99 | #[derive(Debug)] 100 | enum HexError { 101 | Utf8, 102 | ParseInt, 103 | } 104 | 105 | fn parse_fingerprint(input: &[u8]) -> IResult<&[u8], Vec> { 106 | separated_list1( 107 | tag(":"), 108 | map_res( 109 | take_while_m_n(2, 2, |c| is_hex_digit(c) && !c.is_ascii_lowercase()), 110 | |hex| { 111 | u8::from_str_radix(from_utf8(hex).map_err(|_| HexError::Utf8)?, 16) 112 | .map_err(|_| HexError::ParseInt) 113 | }, 114 | ), 115 | )(input) 116 | } 117 | 118 | let (left, parsed) = parse_fingerprint(fingerprint.as_bytes())?; 119 | 120 | (left.is_empty() && parsed.len() == 32) 121 | .then_some(parsed) 122 | .ok_or(ValidationError::InvalidLength) 123 | } 124 | 125 | /// Check for secure and expected path. 126 | fn valid_asset_link_url(url: &Url) -> bool { 127 | url.scheme() == "https" && url.path() == "/.well-known/assetlinks.json" 128 | } 129 | 130 | #[cfg(test)] 131 | mod test { 132 | use super::{valid_asset_link_url, valid_fingerprint, ValidationError}; 133 | use url::Url; 134 | 135 | #[test] 136 | fn check_valid_fingerprint() { 137 | assert!( 138 | valid_fingerprint("B3:5B:68:D5:CE:84:50:55:7C:6A:55:FD:64:B5:1F:EA:C1:10:CB:36:D6:A3:52:1C:59:48:DB:3A:38:0A:34:A9").is_ok(), 139 | "Should be valid fingerprint" 140 | ); 141 | } 142 | 143 | #[test] 144 | fn check_invalid_fingerprint_lowercase() { 145 | let result = valid_fingerprint("b3:5b:68:d5:ce:84:50:55:7c:6a:55:fd:64:b5:1f:ea:c1:10:cb:36:d6:a3:52:1c:59:48:db:3a:38:0a:34:a9"); 146 | assert!(result.is_err(), "Should be invalid fingerprint"); 147 | assert!(matches!(result, Err(ValidationError::ParseFailed(..)))) 148 | } 149 | 150 | #[test] 151 | fn check_invalid_fingerprint_length() { 152 | let result = valid_fingerprint("B3:5B:68:D5:CE:84:50:55:7C:6A:55"); 153 | assert!(result.is_err(), "Should be invalid fingerprint"); 154 | assert!(matches!(result, Err(ValidationError::InvalidLength))) 155 | } 156 | 157 | #[test] 158 | fn check_invalid_fingerprint_non_hex() { 159 | assert!( 160 | valid_fingerprint("B3:5B:68:X5:CE:84:50:55:7C:6A:55:FD:64:B5:1F:EA:C1:10:CB:36:D6:A3:52:1C:59:48:DB:3A:38:0A:34:A9").is_err(), 161 | "Should be valid fingerprint" 162 | ); 163 | } 164 | 165 | #[test] 166 | fn asset_link_url_ok() { 167 | let url = Url::parse("https://www.facebook.com/.well-known/assetlinks.json").unwrap(); 168 | assert!(valid_asset_link_url(&url)); 169 | } 170 | 171 | #[test] 172 | fn asset_link_url_not_secure() { 173 | let url = Url::parse("http://www.facebook.com/.well-known/assetlinks.json").unwrap(); 174 | assert!(!valid_asset_link_url(&url)); 175 | } 176 | 177 | #[test] 178 | fn asset_link_url_unexpected_path() { 179 | let url = Url::parse("https://www.facebook.com/assetlinks.json").unwrap(); 180 | assert!(!valid_asset_link_url(&url)); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /passkey-client/src/client_data.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | /// A trait describing how client data should be generated during a WebAuthn operation. 4 | pub trait ClientData { 5 | /// Extra client data to be appended to the automatically generated client data. 6 | fn extra_client_data(&self) -> E; 7 | 8 | /// The hash of the client data to be used in the WebAuthn operation. 9 | fn client_data_hash(&self) -> Option>; 10 | } 11 | 12 | /// The client data and its hash will be automatically generated from the request 13 | /// according to the WebAuthn specification. 14 | pub struct DefaultClientData; 15 | impl ClientData<()> for DefaultClientData { 16 | fn extra_client_data(&self) {} 17 | 18 | fn client_data_hash(&self) -> Option> { 19 | None 20 | } 21 | } 22 | 23 | /// The extra client data will be appended to the automatically generated client data. 24 | /// The hash will be automatically generated from the result client data according to the WebAuthn specification. 25 | pub struct DefaultClientDataWithExtra(pub E); 26 | impl ClientData for DefaultClientDataWithExtra { 27 | fn extra_client_data(&self) -> E { 28 | self.0.clone() 29 | } 30 | fn client_data_hash(&self) -> Option> { 31 | None 32 | } 33 | } 34 | 35 | /// The client data will be automatically generated from the request according to the WebAuthn specification 36 | /// but it will not be used as a base for the hash. The client data hash will instead be provided by the caller. 37 | pub struct DefaultClientDataWithCustomHash(pub Vec); 38 | impl ClientData<()> for DefaultClientDataWithCustomHash { 39 | fn extra_client_data(&self) {} 40 | 41 | fn client_data_hash(&self) -> Option> { 42 | Some(self.0.clone()) 43 | } 44 | } 45 | 46 | /// Backwards compatibility with the previous `register` and `authenticate` functions 47 | /// which only took `Option>` as a client data hash. 48 | impl ClientData<()> for Option> { 49 | fn extra_client_data(&self) {} 50 | 51 | fn client_data_hash(&self) -> Option> { 52 | self.clone() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /passkey-client/src/extensions.rs: -------------------------------------------------------------------------------- 1 | //! WebAuthn extensions as defined in [WebAuthn Defined Extensions][webauthn] 2 | //! and [CTAP2 Defined Extensions][ctap2]. 3 | //! 4 | //! The currently supported extensions are: 5 | //! * [`Credential Properties`][credprops] 6 | //! * [`Pseudo-random function`][prf] 7 | //! 8 | //! [ctap2]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#sctn-defined-extensions 9 | //! [webauthn]: https://w3c.github.io/webauthn/#sctn-defined-extensions 10 | //! [credprops]: https://w3c.github.io/webauthn/#sctn-authenticator-credential-properties-extension 11 | //! [prf]: https://w3c.github.io/webauthn/#prf-extension 12 | 13 | use passkey_authenticator::{CredentialStore, StoreInfo, UserValidationMethod}; 14 | use passkey_types::{ 15 | ctap2::{get_assertion, get_info, make_credential}, 16 | webauthn::{ 17 | AuthenticationExtensionsClientInputs, AuthenticationExtensionsClientOutputs, 18 | CredentialPropertiesOutput, PublicKeyCredentialRequestOptions, 19 | }, 20 | }; 21 | 22 | use crate::{Client, WebauthnError}; 23 | 24 | mod prf; 25 | 26 | impl Client 27 | where 28 | S: CredentialStore + Sync, 29 | U: UserValidationMethod + Sync, 30 | P: public_suffix::EffectiveTLDProvider + Sync + 'static, 31 | { 32 | /// Create the extension inputs to be passed to an authenticator over CTAP2 33 | /// during a registration request. 34 | pub(super) fn registration_extension_ctap2_input( 35 | &self, 36 | request: Option<&AuthenticationExtensionsClientInputs>, 37 | supported_extensions: &[get_info::Extension], 38 | ) -> Result, WebauthnError> { 39 | prf::registration_prf_to_ctap2_input(request, supported_extensions) 40 | } 41 | 42 | /// Build the extension outputs for the WebAuthn client in a registration request. 43 | pub(super) fn registration_extension_outputs( 44 | &self, 45 | request: Option<&AuthenticationExtensionsClientInputs>, 46 | store_info: StoreInfo, 47 | rk: bool, 48 | authenticator_response: Option, 49 | ) -> AuthenticationExtensionsClientOutputs { 50 | let cred_props_requested = request.and_then(|ext| ext.cred_props) == Some(true); 51 | let cred_props = if cred_props_requested { 52 | let discoverable = store_info.discoverability.is_passkey_discoverable(rk); 53 | 54 | Some(CredentialPropertiesOutput { 55 | discoverable: Some(discoverable), 56 | }) 57 | } else { 58 | None 59 | }; 60 | 61 | // Handling the prf extension outputs. 62 | let prf = authenticator_response 63 | .and_then(|ext_out| ext_out.prf) 64 | .map(Into::into); 65 | 66 | AuthenticationExtensionsClientOutputs { cred_props, prf } 67 | } 68 | 69 | /// Create the extension inputs to be passed to an authenticator over CTAP2 70 | /// during an authentication request. 71 | pub(super) fn auth_extension_ctap2_input( 72 | &self, 73 | request: &PublicKeyCredentialRequestOptions, 74 | supported_extensions: &[get_info::Extension], 75 | ) -> Result, WebauthnError> { 76 | prf::auth_prf_to_ctap2_input(request, supported_extensions) 77 | } 78 | 79 | /// Build the extension outputs for the WebAuthn client in an authentication request. 80 | pub(super) fn auth_extension_outputs( 81 | &self, 82 | authenticator_response: Option, 83 | ) -> AuthenticationExtensionsClientOutputs { 84 | // Handling the prf extension output. 85 | // NOTE: currently very simple because prf is the only 86 | // extension that we support for ctap2. When adding new extensions, 87 | // take care to properly unpack all outputs to Options. 88 | let prf = authenticator_response 89 | .and_then(|ext_out| ext_out.prf) 90 | .map(Into::into); 91 | 92 | AuthenticationExtensionsClientOutputs { 93 | prf, 94 | ..Default::default() 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /passkey-client/src/extensions/prf.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use passkey_types::{ 4 | crypto::sha256, 5 | ctap2::{ 6 | extensions::{AuthenticatorPrfInputs, AuthenticatorPrfValues}, 7 | get_assertion, get_info, make_credential, 8 | }, 9 | webauthn::{ 10 | AuthenticationExtensionsClientInputs, AuthenticationExtensionsPrfInputs, 11 | AuthenticationExtensionsPrfValues, PublicKeyCredentialDescriptor, 12 | PublicKeyCredentialRequestOptions, 13 | }, 14 | Bytes, 15 | }; 16 | 17 | use crate::WebauthnError; 18 | 19 | type Result = ::std::result::Result; 20 | 21 | pub(super) fn registration_prf_to_ctap2_input( 22 | request: Option<&AuthenticationExtensionsClientInputs>, 23 | supported_extensions: &[get_info::Extension], 24 | ) -> Result> { 25 | let maybe_prf = make_ctap_extension( 26 | request.and_then(|r| r.prf.as_ref()), 27 | supported_extensions, 28 | true, 29 | )?; 30 | 31 | if maybe_prf.is_none() { 32 | // Then try prfAlreadyHashed 33 | make_ctap_extension( 34 | request.and_then(|r| r.prf_already_hashed.as_ref()), 35 | supported_extensions, 36 | false, 37 | ) 38 | } else { 39 | Ok(maybe_prf) 40 | } 41 | } 42 | 43 | fn validate_no_eval_by_cred( 44 | prf_input: Option<&AuthenticationExtensionsPrfInputs>, 45 | ) -> Result> { 46 | Ok(match prf_input { 47 | Some(prf) if prf.eval_by_credential.is_some() => { 48 | return Err(WebauthnError::NotSupportedError); 49 | } 50 | Some(prf) => Some(prf), 51 | None => None, 52 | }) 53 | } 54 | 55 | fn convert_eval_to_ctap( 56 | eval: &AuthenticationExtensionsPrfValues, 57 | should_hash: bool, 58 | ) -> Result { 59 | let (first, second) = if should_hash { 60 | let salt1 = make_salt(&eval.first); 61 | let salt2 = eval.second.as_ref().map(make_salt); 62 | (salt1, salt2) 63 | } else { 64 | let salt1 = eval 65 | .first 66 | .as_slice() 67 | .try_into() 68 | .map_err(|_| WebauthnError::ValidationError)?; 69 | let salt2 = eval 70 | .second 71 | .as_ref() 72 | .map(|b| { 73 | b.as_slice() 74 | .try_into() 75 | .map_err(|_| WebauthnError::ValidationError) 76 | }) 77 | .transpose()?; 78 | (salt1, salt2) 79 | }; 80 | 81 | Ok(AuthenticatorPrfValues { first, second }) 82 | } 83 | 84 | fn make_ctap_extension( 85 | prf: Option<&AuthenticationExtensionsPrfInputs>, 86 | supported_extensions: &[get_info::Extension], 87 | should_hash: bool, 88 | ) -> Result> { 89 | // Check if PRF extension input is provided and process it. 90 | // 91 | // Should return a "NotSupportedError" if `evalByCredential` is present 92 | // in this registration request. 93 | let prf = validate_no_eval_by_cred(prf)?; 94 | 95 | // Only request hmac-secret extension input if it's enabled on the authenticator and prf is requested. 96 | let hmac_secret = prf.and_then(|_| { 97 | supported_extensions 98 | .contains(&get_info::Extension::HmacSecret) 99 | .then_some(true) 100 | }); 101 | 102 | let prf = prf 103 | .filter(|_| supported_extensions.contains(&get_info::Extension::Prf)) 104 | .map(|prf| { 105 | // Only create prf extension input if it's enabled on the authenticator. 106 | prf.eval 107 | .as_ref() 108 | .map(|values| convert_eval_to_ctap(values, should_hash)) 109 | .transpose() 110 | .map(|eval| AuthenticatorPrfInputs { 111 | eval, 112 | eval_by_credential: None, 113 | }) 114 | }) 115 | .transpose()?; 116 | 117 | // If any of the input fields are Some, only then should this pass 118 | // a Some(ExtensionInputs) to authenticator. Otherwise, it should 119 | // forward a None. 120 | Ok(make_credential::ExtensionInputs { 121 | hmac_secret, 122 | hmac_secret_mc: None, 123 | prf, 124 | } 125 | .zip_contents()) 126 | } 127 | 128 | pub(super) fn auth_prf_to_ctap2_input( 129 | request: &PublicKeyCredentialRequestOptions, 130 | supported_extensions: &[get_info::Extension], 131 | ) -> Result> { 132 | let maybe_prf = get_ctap_extension( 133 | request.allow_credentials.as_deref(), 134 | request.extensions.as_ref().and_then(|ext| ext.prf.as_ref()), 135 | supported_extensions, 136 | true, 137 | )?; 138 | 139 | if maybe_prf.is_none() { 140 | // Then try prfAlreadyHashed 141 | get_ctap_extension( 142 | request.allow_credentials.as_deref(), 143 | request 144 | .extensions 145 | .as_ref() 146 | .and_then(|ext| ext.prf_already_hashed.as_ref()), 147 | supported_extensions, 148 | false, 149 | ) 150 | } else { 151 | Ok(maybe_prf) 152 | } 153 | } 154 | 155 | fn get_ctap_extension( 156 | allow_credentials: Option<&[PublicKeyCredentialDescriptor]>, 157 | prf_input: Option<&AuthenticationExtensionsPrfInputs>, 158 | supported_extensions: &[get_info::Extension], 159 | should_hash: bool, 160 | ) -> Result> { 161 | // Check if the authenticator supports prf before continuing 162 | if !supported_extensions.contains(&get_info::Extension::Prf) { 163 | return Ok(None); 164 | } 165 | // Check if PRF extension input is provided and process it. 166 | let eval_by_credential = prf_input 167 | .as_ref() 168 | .and_then(|prf| prf.eval_by_credential.as_ref()); 169 | 170 | // If evalByCredential is not empty but allowCredentials is empty, 171 | // return a DOMException whose name is “NotSupportedError”. 172 | if eval_by_credential.is_some_and(|record| !record.is_empty()) 173 | && (allow_credentials.is_none() 174 | || allow_credentials 175 | .as_ref() 176 | .is_some_and(|allow| allow.is_empty())) 177 | { 178 | return Err(WebauthnError::NotSupportedError); 179 | } 180 | 181 | // Pre-compute the parsed values of the base64url-encoded key s.t. we 182 | // can speed up our logic later on instead of having the re-compute 183 | // these values there again. 184 | // TODO: consolidate with authenticator logic 185 | let precomputed_eval_cred = eval_by_credential 186 | .map(|record| { 187 | record 188 | .iter() 189 | .map(|(key, val)| { 190 | Bytes::try_from(key.as_str()) 191 | .map(|k| (k, val)) 192 | .map_err(|_| WebauthnError::SyntaxError) 193 | }) 194 | .collect::>>() 195 | }) 196 | .transpose()?; 197 | 198 | // If any key in evalByCredential is the empty string, or is not a valid 199 | // base64url encoding, or does not equal the id of some element of 200 | // allowCredentials after performing base64url decoding, then return a 201 | // DOMException whose name is “SyntaxError”. 202 | if let Some(record) = precomputed_eval_cred.as_ref() { 203 | if record.iter().any(|(k_bytes, _)| { 204 | k_bytes.is_empty() 205 | || allow_credentials 206 | .as_ref() 207 | .is_some_and(|allow| !allow.iter().any(|cred| cred.id == *k_bytes)) 208 | }) { 209 | return Err(WebauthnError::SyntaxError); 210 | } 211 | } 212 | 213 | let new_eval_by_cred = precomputed_eval_cred 214 | .map(|map| { 215 | map.into_iter() 216 | .map(|(k, values)| convert_eval_to_ctap(values, should_hash).map(|v| (k, v))) 217 | .collect::>>() 218 | }) 219 | .transpose()?; 220 | 221 | let eval = prf_input 222 | .and_then(|prf| { 223 | prf.eval 224 | .as_ref() 225 | .map(|prf_values| convert_eval_to_ctap(prf_values, should_hash)) 226 | }) 227 | .transpose()?; 228 | 229 | let prf = prf_input.map(|_| AuthenticatorPrfInputs { 230 | eval, 231 | eval_by_credential: new_eval_by_cred, 232 | }); 233 | 234 | let extension_inputs = get_assertion::ExtensionInputs { 235 | hmac_secret: None, 236 | prf, 237 | } 238 | .zip_contents(); 239 | 240 | Ok(extension_inputs) 241 | } 242 | 243 | // Build the value that's used as salt by the CTAP2 hmac-secret extension. 244 | fn make_salt(prf_value: &Bytes) -> [u8; 32] { 245 | sha256( 246 | &b"WebAuthn PRF" 247 | .iter() 248 | .chain(std::iter::once(&0x0)) 249 | .chain(prf_value) 250 | .cloned() 251 | .collect::>(), 252 | ) 253 | } 254 | -------------------------------------------------------------------------------- /passkey-client/src/quirks.rs: -------------------------------------------------------------------------------- 1 | //! The goal of this module is to address quirks with RP's different implementations. 2 | //! We don't want to limit this library's functionality for all RPs because of only 3 | //! a few RPs misbehave. 4 | 5 | use passkey_types::webauthn::CreatedPublicKeyCredential; 6 | 7 | /// List of quirky RPs, the default is [`Self::NotQuirky`] which maps to being a no-op 8 | #[derive(Default)] 9 | pub(crate) enum QuirkyRp { 10 | /// The RP is not known to be quirky, thus the mapping methods will be no-ops. 11 | #[default] 12 | NotQuirky, 13 | 14 | /// Adobe crashes on their server when they encounter the key 15 | /// [credProps.authenticatorDisplayName][adn] during key creation. 16 | /// 17 | /// RP_IDs: 18 | /// * `adobe.com` 19 | /// 20 | /// [adn]: https://w3c.github.io/webauthn/#dom-credentialpropertiesoutput-authenticatordisplayname 21 | Adobe, 22 | 23 | /// Hyatt returns an "invalid request" error when they encounter the key 24 | /// [credProps.authenticatorDisplayName][adn] during key creation. 25 | /// 26 | /// RP_IDs: 27 | /// * `hyatt.com` 28 | /// 29 | /// [adn]: https://w3c.github.io/webauthn/#dom-credentialpropertiesoutput-authenticatordisplayname 30 | Hyatt, 31 | } 32 | 33 | impl QuirkyRp { 34 | pub fn from_rp_id(rp_id: &str) -> Self { 35 | match rp_id { 36 | "adobe.com" => QuirkyRp::Adobe, 37 | "hyatt.com" => QuirkyRp::Hyatt, 38 | _ => QuirkyRp::NotQuirky, 39 | } 40 | } 41 | 42 | /// Use this after creating the response but before returning it to the function caller 43 | #[inline] 44 | pub fn map_create_credential( 45 | &self, 46 | response: CreatedPublicKeyCredential, 47 | ) -> CreatedPublicKeyCredential { 48 | match self { 49 | // no-op 50 | Self::NotQuirky => response, 51 | Self::Adobe | Self::Hyatt => remove_authenticator_display_name(response), 52 | } 53 | } 54 | } 55 | 56 | #[inline] 57 | fn remove_authenticator_display_name( 58 | mut response: CreatedPublicKeyCredential, 59 | ) -> CreatedPublicKeyCredential { 60 | if let Some(cp) = response.client_extension_results.cred_props.as_mut() { 61 | cp.authenticator_display_name = None; 62 | } 63 | response 64 | } 65 | -------------------------------------------------------------------------------- /passkey-transports/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "passkey-transports" 3 | description = "A crate managing CTAP2 transport-specific bindings." 4 | version = "0.1.0" 5 | include = ["src/", "../LICENSE-APACHE", "../LICENSE-MIT"] 6 | readme = "README.md" 7 | authors.workspace = true 8 | repository.workspace = true 9 | edition.workspace = true 10 | license.workspace = true 11 | keywords.workspace = true 12 | categories.workspace = true 13 | 14 | 15 | [lints] 16 | workspace = true 17 | 18 | [dependencies] 19 | 20 | [dev-dependencies] -------------------------------------------------------------------------------- /passkey-transports/README.md: -------------------------------------------------------------------------------- 1 | # Passkey Transports 2 | 3 | [![github]](https://github.com/1Password/passkey-rs/tree/main/passkey-transports) 4 | [![version]](https://crates.io/crates/passkey-transports) 5 | [![documentation]](https://docs.rs/passkey-transports/) 6 | 7 | This crate implements the CTAP2 transports between the client and the authenticator. The direction is from the perspective of the client. This is used in the case when the authenticator is not internal to the client's program but accessible as another process or device. 8 | 9 | Currently only the USB HID transport is implemented with plans to support the other specified transports as well as the different platform authenticators. 10 | 11 | 12 | [github]: https://img.shields.io/badge/GitHub-1Password%2Fpasskey--rs%2Fpasskey--transports-informational?logo=github&style=flat 13 | [version]: https://img.shields.io/crates/v/passkey-transports?logo=rust&style=flat 14 | [documentation]: https://img.shields.io/docsrs/passkey-transports/latest?logo=docs.rs&style=flat 15 | [Webauthn]: https://w3c.github.io/webauthn/ -------------------------------------------------------------------------------- /passkey-transports/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Passkey Transports 2 | //! 3 | //! [![github]](https://github.com/1Password/passkey-rs/tree/main/passkey-transports) 4 | //! [![version]](https://crates.io/crates/passkey-transports) 5 | //! [![documentation]](https://docs.rs/passkey-transports/) 6 | //! 7 | //! This crate implements the CTAP2 transports between the client and the authenticator. The 8 | //! direction is from the perspective of the client. This is used in the case when the authenticator 9 | //! is not internal to the client's program but accessible as another process or device. 10 | //! 11 | //! Currently only the USB [`hid`] transport is implemented with plans to support the other specified 12 | //! transports as well as the different platform authenticators. 13 | //! 14 | //! 15 | //! [github]: https://img.shields.io/badge/GitHub-1Password%2Fpasskey--rs%2Fpasskey--transports-informational?logo=github&style=flat 16 | //! [version]: https://img.shields.io/crates/v/passkey-transports?logo=rust&style=flat 17 | //! [documentation]: https://img.shields.io/docsrs/passkey-transports/latest?logo=docs.rs&style=flat 18 | 19 | pub mod hid; 20 | -------------------------------------------------------------------------------- /passkey-types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "passkey-types" 3 | description = "Rust type definitions for the webauthn and CTAP specifications" 4 | include = ["src/", "../LICENSE-APACHE", "../LICENSE-MIT"] 5 | readme = "README.md" 6 | version = "0.4.0" 7 | authors.workspace = true 8 | repository.workspace = true 9 | edition.workspace = true 10 | license.workspace = true 11 | keywords.workspace = true 12 | categories.workspace = true 13 | 14 | [lints] 15 | workspace = true 16 | 17 | [features] 18 | default = [] 19 | serialize_bytes_as_base64_string = [] 20 | testable = ["dep:p256"] 21 | 22 | [dependencies] 23 | bitflags = "2" 24 | ciborium = "0.2" 25 | data-encoding = "2" 26 | indexmap = { version = "2", features = ["serde"] } 27 | hmac = "0.12" 28 | rand = "0.8" 29 | serde = { version = "1", features = ["derive"] } 30 | serde_json = { version = "1", features = ["preserve_order"] } 31 | sha2 = "0.10" 32 | strum = { version = "0.25", features = ["derive"] } 33 | typeshare = "1" 34 | zeroize = { version = "1", features = ["zeroize_derive"] } 35 | # TODO: investigate rolling our own IANA listings and COSE keys 36 | coset = "0.3" 37 | p256 = { version = "0.13", features = [ 38 | "pem", 39 | "arithmetic", 40 | "jwk", 41 | ], optional = true } 42 | 43 | [target.'cfg(target_arch = "wasm32")'.dependencies] 44 | getrandom = { version = "0.2", features = ["js"] } 45 | -------------------------------------------------------------------------------- /passkey-types/README.md: -------------------------------------------------------------------------------- 1 | # Passkey Types 2 | 3 | [![github path](https://img.shields.io/badge/GitHub-1Password%2Fpasskey--rs%2Fpasskey--types-informational?logo=github&style=flat)](https://github.com/1Password/passkey-rs/tree/main/passkey-types) 4 | [![Crates.io version](https://img.shields.io/crates/v/passkey-types?logo=rust&style=flat)](https://crates.io/crates/passkey-types) 5 | [![crate documentation](https://img.shields.io/docsrs/passkey-types/latest?logo=docs.rs&style=flat)](https://docs.rs/passkey-types/) 6 | 7 | This crate contains the types defined in both the [WebAuthn Level 3] and [CTAP 2.0] specifications for the operations they define. They are each separated in their own modules. 8 | 9 | ## Webauthn 10 | 11 | In this module the type names mirror exactly those in the specifications for ease of navigation. They are defined in a way that allows interoperability with the web types directly as well as the [JSON encoding] for over network communication. 12 | 13 | ### Bytes Serialization 14 | 15 | By default, the [`Bytes`] type serializes to an array of numbers for easy conversion to array buffers on the JavaScript side. However, if you are interacting with a server directly or wish to use this crate with Android's [credential-manager] library, you may wish this type to serialize to Base64Url. To do so, simply enable the crate feature `serialize_bytes_as_base64_string`. In the future we will work on changing this behavior dynamically. 16 | 17 | ## CTAP 2 18 | 19 | In this module, seeing as the method inputs are not given explicit names, the `Request` and `Response` types are defined in separate modules for each operation. These types make use of the same data structures from the [WebAuthn](#webauthn) module. In some cases though, the types have different constraits regarding required and optional fields, in which case it is re-defined in the [CTAP](#ctap-2) module along with a `TryFrom` implementation in either direction. 20 | 21 | [WebAuthn Level 3]: https://w3c.github.io/webauthn/ 22 | [CTAP 2.0]: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html 23 | [JSON encoding]: https://w3c.github.io/webauthn/#typedefdef-publickeycredentialjson 24 | [`Bytes`]: https://docs.rs/passkey-types/latest/passkey_types/struct.Bytes.html 25 | [credential-manager]: https://developer.android.com/reference/android/credentials/package-summary 26 | -------------------------------------------------------------------------------- /passkey-types/src/ctap2.rs: -------------------------------------------------------------------------------- 1 | //! The types defined here are a representation of types defined in the [CTAP 2.0] specification along 2 | //! with authenticator specific types from the [WebAuthn Level 3] specification. 3 | //! 4 | //! [CTAP 2.0]: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html 5 | //! [WebAuthn Level 3]: https://w3c.github.io/webauthn 6 | 7 | mod aaguid; 8 | mod attestation_fmt; 9 | mod error; 10 | mod flags; 11 | 12 | pub mod extensions; 13 | pub mod get_assertion; 14 | pub mod get_info; 15 | pub mod make_credential; 16 | 17 | pub use self::{aaguid::*, attestation_fmt::*, error::*, flags::*}; 18 | -------------------------------------------------------------------------------- /passkey-types/src/ctap2/aaguid.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// An Authenticator Attestation GUID is a 128-bit identifier. 4 | /// 5 | /// This should be used to indicate the type (e.g. make and model) of an Authenticator. The [spec] 6 | /// recommends this to be identical accross all substantially identical authenticators made by the 7 | /// same manufacturer so that Relying Parties may use it to infer properties of the authenticator. 8 | /// 9 | /// For privacy reasons we do not recomend this as it can be used for PII, therefore we provide a 10 | /// way to generate an empty AAGUID where it is only `0`s. This the typical AAGUID used when doing 11 | /// self or no attestation. 12 | /// 13 | /// [spec]: https://w3c.github.io/webauthn/#sctn-authenticator-model 14 | /// [RFC4122]: https://www.rfc-editor.org/rfc/rfc4122 15 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 16 | pub struct Aaguid(pub [u8; Self::LEN]); 17 | 18 | impl Aaguid { 19 | const LEN: usize = 16; 20 | 21 | /// Generate empty AAGUID 22 | pub const fn new_empty() -> Self { 23 | Self([0; 16]) 24 | } 25 | } 26 | 27 | impl Default for Aaguid { 28 | fn default() -> Self { 29 | Self::new_empty() 30 | } 31 | } 32 | 33 | impl From<[u8; 16]> for Aaguid { 34 | fn from(inner: [u8; 16]) -> Self { 35 | Aaguid(inner) 36 | } 37 | } 38 | 39 | impl Serialize for Aaguid { 40 | fn serialize(&self, serializer: S) -> Result 41 | where 42 | S: serde::Serializer, 43 | { 44 | serializer.serialize_bytes(&self.0) 45 | } 46 | } 47 | 48 | impl<'de> Deserialize<'de> for Aaguid { 49 | fn deserialize(deserializer: D) -> Result 50 | where 51 | D: serde::Deserializer<'de>, 52 | { 53 | struct AaguidVisitior; 54 | impl serde::de::Visitor<'_> for AaguidVisitior { 55 | type Value = Aaguid; 56 | 57 | fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 58 | write!(f, "A byte string of {} bytes long", Aaguid::LEN) 59 | } 60 | 61 | fn visit_bytes(self, v: &[u8]) -> Result 62 | where 63 | E: serde::de::Error, 64 | { 65 | v.try_into().map(Aaguid).map_err(|_| { 66 | E::custom(format!("Byte string of len {}, is not of len 16", v.len())) 67 | }) 68 | } 69 | } 70 | deserializer.deserialize_bytes(AaguidVisitior) 71 | } 72 | } 73 | 74 | #[cfg(test)] 75 | mod tests { 76 | use super::Aaguid; 77 | 78 | #[test] 79 | fn deserialize_byte_str_to_aaguid() { 80 | let cbor_bytes = [ 81 | 0x50, // bytes(16) 82 | 0x02, 0x2b, 0xeb, 0xfd, 0x62, 0x3c, 0xac, 0x25, // data 83 | 0xce, 0xe4, 0xd0, 0x90, 0xb9, 0xf8, 0xb5, 0xaf, 84 | ]; 85 | 86 | let aaguid: Aaguid = ciborium::de::from_reader(cbor_bytes.as_slice()) 87 | .expect("could not deserialize from byte string"); 88 | assert_eq!( 89 | aaguid, 90 | Aaguid([ 91 | 0x02, 0x2b, 0xeb, 0xfd, 0x62, 0x3c, 0xac, 0x25, 0xce, 0xe4, 0xd0, 0x90, 0xb9, 0xf8, 92 | 0xb5, 0xaf, 93 | ]) 94 | ); 95 | } 96 | 97 | #[test] 98 | fn new_empty_truly_zero() { 99 | assert_eq!(Aaguid::new_empty().0, [0; 16]); 100 | } 101 | 102 | #[test] 103 | fn aaguid_serialization_round_trip() { 104 | let expected = Aaguid::new_empty(); 105 | let mut aaguid_bytes = Vec::with_capacity(17); 106 | ciborium::ser::into_writer(&expected, &mut aaguid_bytes) 107 | .expect("could not serialize aaguid"); 108 | 109 | let result = ciborium::de::from_reader(aaguid_bytes.as_slice()) 110 | .expect("could not deserialized aaguid"); 111 | 112 | assert_eq!(expected, result); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /passkey-types/src/ctap2/extensions/mod.rs: -------------------------------------------------------------------------------- 1 | //! Types for the CTAP2 authenticator extensions. 2 | //! 3 | //! 4 | mod hmac_secret; 5 | pub(super) mod prf; 6 | 7 | pub use hmac_secret::{HmacGetSecretInput, HmacSecretSaltOrOutput, TryFromSliceError}; 8 | pub use prf::{ 9 | AuthenticatorPrfGetOutputs, AuthenticatorPrfInputs, AuthenticatorPrfMakeOutputs, 10 | AuthenticatorPrfValues, 11 | }; 12 | -------------------------------------------------------------------------------- /passkey-types/src/ctap2/extensions/prf.rs: -------------------------------------------------------------------------------- 1 | //! While this is not an official CTAP extension, 2 | //! it is used on Windows directly and it allows an in-memory authenticator 3 | //! to handle the prf extension in a more efficient manor. 4 | 5 | use std::collections::HashMap; 6 | 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use crate::{webauthn, Bytes}; 10 | 11 | #[cfg(doc)] 12 | use crate::ctap2::{get_assertion, make_credential}; 13 | 14 | /// This struct is a more opiniated mirror of [`webauthn::AuthenticationExtensionsPrfInputs`]. 15 | #[derive(Debug, Serialize, Deserialize, Clone)] 16 | pub struct AuthenticatorPrfInputs { 17 | /// See [`webauthn::AuthenticationExtensionsPrfInputs::eval`]. 18 | #[serde(default, skip_serializing_if = "Option::is_none")] 19 | pub eval: Option, 20 | 21 | /// See [`webauthn::AuthenticationExtensionsPrfInputs::eval_by_credential`]. 22 | #[serde(default, skip_serializing_if = "Option::is_none")] 23 | pub eval_by_credential: Option>, 24 | } 25 | 26 | /// This struct is a more opiniated mirror of [`webauthn::AuthenticationExtensionsPrfValues`]. 27 | #[derive(Debug, Serialize, Deserialize, Clone)] 28 | pub struct AuthenticatorPrfValues { 29 | /// This is the already hashed values of [`webauthn::AuthenticationExtensionsPrfValues::first`]. 30 | pub first: [u8; 32], 31 | 32 | /// This is the already hashed values of [`webauthn::AuthenticationExtensionsPrfValues::second`]. 33 | #[serde(default, skip_serializing_if = "Option::is_none")] 34 | pub second: Option<[u8; 32]>, 35 | } 36 | 37 | impl From for webauthn::AuthenticationExtensionsPrfValues { 38 | fn from(value: AuthenticatorPrfValues) -> Self { 39 | Self { 40 | first: value.first.to_vec().into(), 41 | second: value.second.map(|b| b.to_vec().into()), 42 | } 43 | } 44 | } 45 | 46 | /// This struct is a more opiniated mirror of [`webauthn::AuthenticationExtensionsPrfOutputs`] 47 | /// specifically for [`make_credential`]. 48 | #[derive(Debug, Serialize, Deserialize, Clone)] 49 | pub struct AuthenticatorPrfMakeOutputs { 50 | /// See [`webauthn::AuthenticationExtensionsPrfOutputs::enabled`]. 51 | pub enabled: bool, 52 | 53 | /// See [`webauthn::AuthenticationExtensionsPrfOutputs::results`]. 54 | #[serde(default, skip_serializing_if = "Option::is_none")] 55 | pub results: Option, 56 | } 57 | 58 | impl From for webauthn::AuthenticationExtensionsPrfOutputs { 59 | fn from(value: AuthenticatorPrfMakeOutputs) -> Self { 60 | Self { 61 | enabled: Some(value.enabled), 62 | results: value.results.map(Into::into), 63 | } 64 | } 65 | } 66 | 67 | /// This struct is a more opiniated mirror of [`webauthn::AuthenticationExtensionsPrfOutputs`] 68 | /// specifically for [`get_assertion`]. 69 | #[derive(Debug, Serialize, Deserialize, Clone)] 70 | pub struct AuthenticatorPrfGetOutputs { 71 | /// See [`webauthn::AuthenticationExtensionsPrfOutputs::results`]. 72 | pub results: AuthenticatorPrfValues, 73 | } 74 | 75 | impl From for webauthn::AuthenticationExtensionsPrfOutputs { 76 | fn from(value: AuthenticatorPrfGetOutputs) -> Self { 77 | Self { 78 | enabled: None, 79 | results: Some(value.results.into()), 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /passkey-types/src/ctap2/flags.rs: -------------------------------------------------------------------------------- 1 | use bitflags::bitflags; 2 | 3 | bitflags! { 4 | /// Flags for authenticator Data 5 | /// 6 | /// 7 | #[repr(transparent)] 8 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 9 | pub struct Flags: u8 { 10 | /// User Present, bit 0 11 | const UP = 1 << 0; 12 | /// User Verified, bit 2 13 | const UV = 1 << 2; 14 | /// Backup Eligibility, bit 3 15 | const BE = 1 << 3; 16 | /// Backup state, bit 4 17 | const BS = 1 << 4; 18 | /// Attested Credential Data, bit 6 19 | const AT = 1 << 6; 20 | /// Extension Data Included, bit 7 21 | const ED = 1 << 7; 22 | } 23 | } 24 | 25 | impl Default for Flags { 26 | fn default() -> Self { 27 | Flags::BE | Flags::BS 28 | } 29 | } 30 | 31 | impl From for u8 { 32 | fn from(src: Flags) -> Self { 33 | src.bits() 34 | } 35 | } 36 | 37 | impl TryFrom for Flags { 38 | type Error = (); 39 | 40 | fn try_from(value: u8) -> Result { 41 | Flags::from_bits(value).ok_or(()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /passkey-types/src/ctap2/get_assertion.rs: -------------------------------------------------------------------------------- 1 | //! 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::{ 5 | ctap2::AuthenticatorData, 6 | webauthn::{PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity}, 7 | Bytes, 8 | }; 9 | 10 | pub use crate::ctap2::make_credential::Options; 11 | 12 | #[cfg(doc)] 13 | use { 14 | crate::webauthn::{CollectedClientData, PublicKeyCredentialRequestOptions}, 15 | ciborium::value::Value, 16 | }; 17 | 18 | use super::extensions::{AuthenticatorPrfGetOutputs, AuthenticatorPrfInputs, HmacGetSecretInput}; 19 | 20 | serde_workaround! { 21 | /// While similar in structure to [`PublicKeyCredentialRequestOptions`], 22 | /// it is not completely identical, namely the presence of the `options` key. 23 | #[derive(Debug)] 24 | pub struct Request { 25 | /// Relying Party Identifier 26 | #[serde(rename = 0x01)] 27 | pub rp_id: String, 28 | 29 | /// Hash of the serialized client data collected by the host. 30 | /// See [`CollectedClientData`] 31 | #[serde(rename = 0x02)] 32 | pub client_data_hash: Bytes, 33 | 34 | /// A sequence of PublicKeyCredentialDescriptor structures, each denoting a credential. If 35 | /// this parameter is present and has 1 or more entries, the authenticator MUST only 36 | /// generate an assertion using one of the denoted credentials. 37 | #[serde(rename = 0x03, default, skip_serializing_if = Option::is_none)] 38 | pub allow_list: Option>, 39 | 40 | /// Parameters to influence authenticator operation. These parameters might be authenticator 41 | /// specific. 42 | #[serde(rename = 0x04, default, skip_serializing_if = Option::is_none)] 43 | pub extensions: Option, 44 | 45 | /// Parameters to influence authenticator operation, see [`Options`] for more details. 46 | #[serde(rename = 0x05, default)] 47 | pub options: Options, 48 | 49 | /// First 16 bytes of HMAC-SHA-256 of clientDataHash using pinToken which platform got from 50 | /// the authenticator: HMAC-SHA-256(pinToken, clientDataHash). (NOT YET SUPPORTED) 51 | #[serde(rename = 0x06, default, skip_serializing_if = Option::is_none)] 52 | pub pin_auth: Option, 53 | 54 | /// PIN protocol version chosen by the client 55 | #[serde(rename = 0x07, default, skip_serializing_if = Option::is_none)] 56 | pub pin_protocol: Option, 57 | } 58 | } 59 | 60 | /// All supported Authenticator extensions inputs during credential assertion 61 | #[derive(Debug, Serialize, Deserialize, Default)] 62 | pub struct ExtensionInputs { 63 | /// The input salts for fetching and deriving a symmetric secret. 64 | /// 65 | /// 66 | #[serde( 67 | rename = "hmac-secret", 68 | default, 69 | skip_serializing_if = "Option::is_none" 70 | )] 71 | pub hmac_secret: Option, 72 | 73 | /// The direct input from a on-system client for the prf extension. 74 | /// 75 | /// The output from a request using the `prf` extension will not be signed 76 | /// and will be un-encrypted. 77 | /// This input should already be hashed by the client. 78 | #[serde(default, skip_serializing_if = "Option::is_none")] 79 | pub prf: Option, 80 | } 81 | 82 | impl ExtensionInputs { 83 | /// Validates that there is at least one extension field that is `Some`. 84 | /// If all fields are `None` then this returns `None` as well. 85 | pub fn zip_contents(self) -> Option { 86 | let Self { hmac_secret, prf } = &self; 87 | 88 | let has_hmac_secret = hmac_secret.is_some(); 89 | let has_prf = prf.is_some(); 90 | 91 | (has_hmac_secret || has_prf).then_some(self) 92 | } 93 | } 94 | 95 | serde_workaround! { 96 | /// Type returned from `Authenticator::get_assertion` on success. 97 | #[derive(Debug)] 98 | pub struct Response { 99 | /// PublicKeyCredentialDescriptor structure containing the credential identifier whose 100 | /// private key was used to generate the assertion. May be omitted if the allowList has 101 | /// exactly one Credential. 102 | #[serde(rename = 0x01, default, skip_serializing_if = Option::is_none)] 103 | pub credential: Option, 104 | 105 | /// The signed-over contextual bindings made by the authenticator 106 | #[serde(rename = 0x02)] 107 | pub auth_data: AuthenticatorData, 108 | 109 | /// The assertion signature produced by the authenticator 110 | #[serde(rename = 0x03)] 111 | pub signature: Bytes, 112 | 113 | /// [`PublicKeyCredentialUserEntity`] structure containing the user account information. 114 | /// User identifiable information (name, DisplayName, icon) MUST not be returned if user 115 | /// verification is not done by the authenticator. 116 | /// 117 | /// ## U2F Devices: 118 | /// For U2F devices, this parameter is not returned as this user information is not present 119 | /// for U2F credentials. 120 | /// 121 | /// ## FIDO Devices - server resident credentials: 122 | /// For server resident credentials on FIDO devices, this parameter is optional as server 123 | /// resident credentials behave same as U2F credentials where they are discovered given the 124 | /// user information on the RP. Authenticators optionally MAY store user information inside 125 | /// the credential ID. 126 | /// 127 | /// ## FIDO devices - device resident credentials: 128 | /// For device resident keys on FIDO devices, at least user "id" is mandatory. 129 | /// 130 | /// For single account per RP case, authenticator returns "id" field to the platform which 131 | /// will be returned to the WebAuthn layer. 132 | /// 133 | /// For multiple accounts per RP case, where the authenticator does not have a display, 134 | /// authenticator returns "id" as well as other fields to the platform. Platform will use 135 | /// this information to show the account selection UX to the user and for the user selected 136 | /// account, it will ONLY return "id" back to the WebAuthn layer and discard other user details. 137 | #[serde(rename = 0x04, default, skip_serializing_if = Option::is_none)] 138 | pub user: Option, 139 | 140 | /// Total number of account credentials for the RP. This member is required when more than 141 | /// one account for the RP and the authenticator does not have a display. Omitted when 142 | /// returned for the authenticatorGetNextAssertion method. 143 | /// 144 | /// It seems unlikely that more than 256 credentials would be needed for any given RP. Please 145 | /// file an enhancement request if this limit impacts your application. 146 | #[serde(rename = 0x05, default, skip_serializing_if = Option::is_none)] 147 | pub number_of_credentials: Option, 148 | 149 | /// Indicates that a credential was selected by the user via interaction directly with the authenticator, 150 | /// and thus the platform does not need to confirm the credential. 151 | /// Optional; defaults to false. 152 | /// MUST NOT be present in response to a request where an [`Request::allow_list`] was given, 153 | /// where [`Self::number_of_credentials`] is greater than one, 154 | /// nor in response to an `authenticatorGetNextAssertion` request. 155 | #[serde(rename = 0x06, default, skip_serializing_if = Option::is_none)] 156 | pub user_selected: Option, 157 | 158 | /// The contents of the associated `largeBlobKey` if present for the asserted credential, 159 | /// and if [largeBlobKey[] was true in the extensions input. 160 | /// 161 | /// This extension is currently un-supported by this library. 162 | #[serde(rename = 0x07, default, skip_serializing_if = Option::is_none)] 163 | pub large_blob_key: Option, 164 | 165 | /// A map, keyed by extension identifiers, to unsigned outputs of extensions, if any. 166 | /// Authenticators SHOULD omit this field if no processed extensions define unsigned outputs. 167 | /// Clients MUST treat an empty map the same as an omitted field. 168 | #[serde(rename = 0x08, default, skip_serializing_if = Option::is_none)] 169 | pub unsigned_extension_outputs: Option, 170 | } 171 | } 172 | 173 | /// All supported Authenticator extensions outputs during credential assertion 174 | /// 175 | /// This is to be serialized to [`Value`] in [`AuthenticatorData::extensions`] 176 | #[derive(Debug, Serialize, Deserialize)] 177 | pub struct SignedExtensionOutputs { 178 | /// Outputs the symmetric secrets after successfull processing. The output MUST be encrypted. 179 | /// 180 | /// 181 | #[serde( 182 | rename = "hmac-secret", 183 | default, 184 | skip_serializing_if = "Option::is_none" 185 | )] 186 | pub hmac_secret: Option, 187 | } 188 | 189 | impl SignedExtensionOutputs { 190 | /// Validates that there is at least one extension field that is `Some`. 191 | /// If all fields are `None` then this returns `None` as well. 192 | pub fn zip_contents(self) -> Option { 193 | let Self { hmac_secret } = &self; 194 | hmac_secret.is_some().then_some(self) 195 | } 196 | } 197 | 198 | /// A map, keyed by extension identifiers, to unsigned outputs of extensions, if any. 199 | /// Authenticators SHOULD omit this field if no processed extensions define unsigned outputs. 200 | /// Clients MUST treat an empty map the same as an omitted field. 201 | #[derive(Debug, Serialize, Deserialize, Default)] 202 | #[serde(rename_all = "camelCase")] 203 | pub struct UnsignedExtensionOutputs { 204 | /// This output is supported in the Webauthn specification and will be used when the authenticator 205 | /// and the client are in memory or communicating through an internal channel. 206 | /// 207 | /// If you are using transports where this needs to pass through a wire, use hmac-secret instead. 208 | pub prf: Option, 209 | } 210 | 211 | impl UnsignedExtensionOutputs { 212 | /// Validates that there is at least one extension field that is `Some`. 213 | /// If all fields are `None` then this returns `None` as well. 214 | pub fn zip_contents(self) -> Option { 215 | let Self { prf } = &self; 216 | prf.is_some().then_some(self) 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /passkey-types/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Passkey Types 2 | //! 3 | //! [![github]](https://github.com/1Password/passkey-rs/tree/main/passkey-types) 4 | //! [![version]](https://crates.io/crates/passkey-types) 5 | //! [![documentation]](crate) 6 | //! 7 | //! This crate contains the types defined in both the [WebAuthn Level 3] and [CTAP 2.0] 8 | //! specifications for the operations they define. They are each separated in their own modules. 9 | //! 10 | //! ## Webauthn 11 | //! 12 | //! In [this](webauthn) module the type names mirror exactly those in the specifications for ease of 13 | //! navigation. They are defined in a way that allows interoperability with the web types directly 14 | //! as well as the [JSON encoding] for over network communication. 15 | //! 16 | //! ## CTAP 2 17 | //! 18 | //! In [this](ctap2) module, since the method inputs are not given explicit names, the `Request` and 19 | //! `Response` types are defined in separate modules for each operation. These types make use of the 20 | //! same data structures from the [`webauthn`] module. In some cases though, the types have 21 | //! different constraits regarding required and optional fields, in which case it is re-defined in 22 | //! the [`ctap2`] module along with a [`TryFrom`] implementation in either direction. 23 | //! 24 | //! [github]: https://img.shields.io/badge/GitHub-1Password%2Fpasskey--rs%2Fpasskey--types-informational?logo=github&style=flat 25 | //! [version]: https://img.shields.io/crates/v/passkey-types?logo=rust&style=flat 26 | //! [documentation]: https://img.shields.io/docsrs/passkey-types/latest?logo=docs.rs&style=flat 27 | //! [WebAuthn Level 3]: https://w3c.github.io/webauthn/ 28 | //! [CTAP 2.0]: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html 29 | //! [JSON encoding]: https://w3c.github.io/webauthn/#typedefdef-publickeycredentialjson 30 | 31 | #[macro_use] 32 | mod utils; 33 | 34 | mod passkey; 35 | 36 | pub mod ctap2; 37 | pub mod u2f; 38 | pub mod webauthn; 39 | 40 | // Re-exports 41 | pub use self::{ 42 | passkey::{CredentialExtensions, Passkey, StoredHmacSecret}, 43 | utils::{ 44 | bytes::{Bytes, NotBase64Encoded}, 45 | crypto, encoding, rand, 46 | }, 47 | }; 48 | 49 | #[cfg(feature = "testable")] 50 | pub use self::passkey::PasskeyBuilder; 51 | -------------------------------------------------------------------------------- /passkey-types/src/passkey.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use super::u2f::{AuthenticationRequest, RegisterRequest, RegisterResponse}; 4 | use crate::{ctap2::make_credential as ctap2, webauthn, Bytes}; 5 | use coset::CoseKey; 6 | use zeroize::{Zeroize, ZeroizeOnDrop}; 7 | 8 | #[cfg(feature = "testable")] 9 | mod mock; 10 | 11 | #[cfg(feature = "testable")] 12 | pub use self::mock::PasskeyBuilder; 13 | 14 | /// The private WebAuthn credential containing all relevant required and optional information for an 15 | /// authentication ceremony. 16 | /// 17 | /// The WebAuthn term for this is a [Public Key Credential Source][cred-src]. 18 | /// 19 | /// # Personally Identifying Information (PII) considerations 20 | /// While this struct implements [`Debug`], it only prints the following fields: 21 | /// * [`CoseKey::kty`] enum from the [`Self::key`] field, 22 | /// * [`Self::counter`] which is the number of times this was used to authenticate. 23 | /// 24 | /// The rest of this struct should be considered secret, either for cryptographic security, or because 25 | /// its value could be used as PII. 26 | /// 27 | /// [cred-src]: https://w3c.github.io/webauthn/#public-key-credential-source 28 | // TODO: Implement Zeroize on this if/when rolling our own CoseKey type 29 | // TODO: use `#[non_exhaustive]` here with a builder pattern for building new passkeys 30 | #[derive(Clone)] 31 | #[cfg_attr(any(test, feature = "testable"), derive(PartialEq))] 32 | pub struct Passkey { 33 | /// The private key in COSE key format. 34 | /// 35 | /// # PII considerations 36 | /// This value should be considered secret and never printed out as it is a secret cryptographic 37 | /// key. The only thing that get printed in the `Debug` implementation is the key type, 38 | /// e.g: EC2, RSA, etc. 39 | pub key: CoseKey, 40 | 41 | /// A probabilistically-unique byte sequence identifying this [`Passkey`]. It must be at most 1023 42 | /// bytes long. 43 | /// 44 | /// Credential IDs are generated by authenticators in two forms: 45 | /// 1. At least 16 bytes that include at least 100 bits of entropy, or 46 | /// 2. The [`Passkey`] item, without its `credential_id`, encrypted so only its managing 47 | /// authenticator can decrypt it. This form allows the authenticator to be nearly stateless, by 48 | /// having the Relying Party store any necessary state. 49 | /// 50 | /// Relying Parties do not need to distinguish these two `credential id` forms. 51 | /// 52 | /// 53 | /// # PII considerations 54 | /// This value should be considered secret as it is the user's credential ID for the associated 55 | /// Relying Party. See [Privacy leak via credential IDs][privacy] for more information. 56 | /// 57 | /// [privacy]: https://w3c.github.io/webauthn/#sctn-credential-id-privacy-leak 58 | pub credential_id: Bytes, 59 | 60 | /// The [Relying Party ID][RP_ID] for which the [`Passkey`] is associated. This value mirrors the 61 | /// [`webauthn::PublicKeyCredentialRpEntity::id`] value passed during the creation of this credential. 62 | /// 63 | /// # PII considerations 64 | /// This should be handled similarly to a URL. Since this is a user credential for a Relying 65 | /// Party, this would leak the fact that a user has an account for this particular Relying Party. 66 | /// 67 | /// [RP_ID]: https://w3c.github.io/webauthn/#relying-party-identifier 68 | pub rp_id: String, 69 | 70 | /// This is the [`webauthn::PublicKeyCredentialUserEntity::id`] passed in during the creation of 71 | /// this credential. An Authenticator can choose to store this or not. If it stores this value, 72 | /// this [`Passkey`] will become a [Discoverable Credential] and will be returned during authentication 73 | /// Ceremonies. 74 | /// 75 | /// # PII Considerations 76 | /// This is the identifier a Relying party uses on their side to personally identify a user. This 77 | /// value is analogous to a username. 78 | /// 79 | /// [Discoverable Credential]: https://w3c.github.io/webauthn/#client-side-discoverable-credential 80 | pub user_handle: Option, 81 | 82 | /// Value tracks the number of times an authentication ceremony has been successfully completed. 83 | /// If the value is `None` then it will be sent as the constant `0`. 84 | /// See [Signature counter considerations][signCount] for more information. 85 | /// 86 | /// # PII considerations 87 | /// This value, if populated, is used by the Relying Party as an indicator of a cloned 88 | /// authenticator. If this [`Passkey`] is to be synced, consider leaving this value empty unless 89 | /// you can guarantee the value to always be increased for every use of this passkey across its 90 | /// distribution. 91 | /// 92 | /// [signCount]: https://w3c.github.io/webauthn/#signature-counter 93 | pub counter: Option, 94 | 95 | /// Authenticator extensions that need data associated to passkey secrets 96 | pub extensions: CredentialExtensions, 97 | } 98 | 99 | impl Passkey { 100 | /// Standardized way to "upgrade" a U2F register request into a passkey 101 | pub fn from_u2f_register_response( 102 | request: &RegisterRequest, 103 | response: &RegisterResponse, 104 | private_key: &CoseKey, 105 | ) -> Self { 106 | let app_id: Bytes = request.application.to_vec().into(); 107 | Self { 108 | key: private_key.clone(), 109 | credential_id: response.key_handle.clone().to_vec().into(), 110 | rp_id: app_id.into(), 111 | user_handle: None, 112 | counter: Some(0), 113 | extensions: Default::default(), 114 | } 115 | } 116 | 117 | /// Upgrade a U2F Authentication Request into a Passkey 118 | pub fn from_u2f_auth_request( 119 | request: &AuthenticationRequest, 120 | counter: u32, 121 | private_key: &CoseKey, 122 | ) -> Self { 123 | let app_id: Bytes = request.application.to_vec().into(); 124 | Self { 125 | key: private_key.clone(), 126 | credential_id: request.key_handle.clone().to_vec().into(), 127 | rp_id: app_id.into(), 128 | user_handle: None, 129 | counter: Some(counter), 130 | extensions: Default::default(), 131 | } 132 | } 133 | 134 | /// This function wraps up a U2F registration request as a Passkey for storing 135 | /// in a CredentialStore. 136 | pub fn wrap_u2f_registration_request( 137 | request: &RegisterRequest, 138 | response: &RegisterResponse, 139 | key_handle: &[u8], 140 | private_key: &CoseKey, 141 | ) -> ( 142 | Passkey, 143 | ctap2::PublicKeyCredentialUserEntity, 144 | ctap2::PublicKeyCredentialRpEntity, 145 | ) { 146 | let passkey = Passkey::from_u2f_register_response(request, response, private_key); 147 | 148 | let user_entity = ctap2::PublicKeyCredentialUserEntity { 149 | id: key_handle.to_vec().into(), 150 | display_name: None, 151 | name: None, 152 | icon_url: None, 153 | }; 154 | 155 | let app_id: Bytes = request.application.to_vec().into(); 156 | let rp = ctap2::PublicKeyCredentialRpEntity { 157 | id: app_id.into(), 158 | name: None, 159 | }; 160 | 161 | (passkey, user_entity, rp) 162 | } 163 | 164 | /// Create a passkey mock builder. 165 | /// 166 | /// The default credential Id length is 16, change it with the [`PasskeyBuilder::credential_id`] 167 | /// method. 168 | #[cfg(feature = "testable")] 169 | pub fn mock(rp_id: String) -> PasskeyBuilder { 170 | PasskeyBuilder::new(rp_id) 171 | } 172 | } 173 | 174 | impl From for webauthn::PublicKeyCredentialDescriptor { 175 | fn from(value: Passkey) -> Self { 176 | Self { 177 | ty: webauthn::PublicKeyCredentialType::PublicKey, 178 | id: value.credential_id, 179 | transports: None, 180 | } 181 | } 182 | } 183 | 184 | impl From<&Passkey> for webauthn::PublicKeyCredentialDescriptor { 185 | fn from(value: &Passkey) -> Self { 186 | Self { 187 | ty: webauthn::PublicKeyCredentialType::PublicKey, 188 | id: value.credential_id.clone(), 189 | transports: None, 190 | } 191 | } 192 | } 193 | 194 | impl Debug for Passkey { 195 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 196 | f.debug_struct("Passkey") 197 | .field("key_type", &self.key.kty) 198 | .field("counter", &self.counter) 199 | .finish() 200 | } 201 | } 202 | 203 | /// Supported extensions on a [`Passkey`] 204 | #[derive(Default, Clone, Zeroize, ZeroizeOnDrop)] 205 | #[cfg_attr(any(test, feature = "testable"), derive(PartialEq))] 206 | pub struct CredentialExtensions { 207 | /// Whether the passkey has hmac-secret credentials associated to it 208 | pub hmac_secret: Option, 209 | } 210 | 211 | /// The stored hmac-secret credentials associated to a [`Passkey`] 212 | #[derive(Clone, Zeroize, ZeroizeOnDrop)] 213 | #[cfg_attr(any(test, feature = "testable"), derive(PartialEq))] 214 | pub struct StoredHmacSecret { 215 | /// The credential that must be gated behind user verification 216 | pub cred_with_uv: Vec, 217 | /// The credential that is not gated behind user verification, but is gated behind user presence 218 | pub cred_without_uv: Option>, 219 | } 220 | -------------------------------------------------------------------------------- /passkey-types/src/passkey/mock.rs: -------------------------------------------------------------------------------- 1 | use coset::{iana, CoseKeyBuilder}; 2 | use p256::{ecdsa::SigningKey, SecretKey}; 3 | 4 | use crate::{rand::random_vec, Passkey, StoredHmacSecret}; 5 | 6 | /// A builder for the [`Passkey`] type which should be used as a mock for testing. 7 | pub struct PasskeyBuilder { 8 | inner: Passkey, 9 | } 10 | 11 | impl PasskeyBuilder { 12 | /// Create a new 13 | pub(super) fn new(rp_id: String) -> Self { 14 | let private_key = { 15 | let mut rng = rand::thread_rng(); 16 | SecretKey::random(&mut rng) 17 | }; 18 | 19 | let public_key = SigningKey::from(&private_key) 20 | .verifying_key() 21 | .to_encoded_point(false); 22 | // SAFETY: These unwraps are safe because the public_key above is not compressed (false 23 | // parameter) therefore x and y are guaranteed to contain values. 24 | let x = public_key.x().unwrap().as_slice().to_vec(); 25 | let y = public_key.y().unwrap().as_slice().to_vec(); 26 | let private = CoseKeyBuilder::new_ec2_priv_key( 27 | iana::EllipticCurve::P_256, 28 | x, 29 | y, 30 | private_key.to_bytes().to_vec(), 31 | ) 32 | .algorithm(iana::Algorithm::ES256) 33 | .build(); 34 | 35 | Self { 36 | inner: Passkey { 37 | key: private, 38 | credential_id: random_vec(16).into(), 39 | rp_id, 40 | user_handle: None, 41 | counter: None, 42 | extensions: Default::default(), 43 | }, 44 | } 45 | } 46 | 47 | /// Regenerate the credential ID with a different size than the default 16 bytes 48 | pub fn credential_id(mut self, len: usize) -> Self { 49 | self.inner.credential_id = random_vec(len).into(); 50 | self 51 | } 52 | 53 | /// Generate the user handle with an optional custom size. The default is 16 bytes. 54 | pub fn user_handle(mut self, len: Option) -> Self { 55 | self.inner.user_handle = Some(random_vec(len.unwrap_or(16)).into()); 56 | self 57 | } 58 | 59 | /// Add a counter to the passkey. The default is None 60 | pub fn counter(mut self, counter: u32) -> Self { 61 | self.inner.counter = Some(counter); 62 | self 63 | } 64 | 65 | /// Add hmac-secret extension data associated to the passkey. The default is none 66 | pub fn hmac_secret(mut self, hmac_secret: StoredHmacSecret) -> Self { 67 | self.inner.extensions.hmac_secret = Some(hmac_secret); 68 | self 69 | } 70 | 71 | /// Get the built passkey 72 | pub fn build(self) -> Passkey { 73 | self.inner 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /passkey-types/src/u2f.rs: -------------------------------------------------------------------------------- 1 | //! U2F Authenticator API 2 | mod authenticate; 3 | mod commands; 4 | mod register; 5 | mod version; 6 | 7 | pub use {authenticate::*, commands::*, register::*, version::*}; 8 | 9 | /// ISO 7816-4 Status Words (`SW_*`) 10 | /// 11 | /// Values are taken from 12 | #[repr(u16)] 13 | #[derive(Debug)] 14 | pub enum ResponseStatusWords { 15 | /// The command completed successfully without error 16 | NoError = 0x9000, 17 | /// The request was rejected due to test-of-user-presence being required. 18 | ConditionsNotSatisfied = 0x6985, 19 | /// The request was rejected due to an invalid key handle. 20 | WrongData = 0x6A80, 21 | /// The length of the request was invalid. 22 | WrongLength = 0x6700, 23 | /// The Class byte of the request is not supported. (i.e. CLA != 0) 24 | ClaNotSupported = 0x6E00, 25 | /// The Instruction of the request is not supported. 26 | InsNotSupported = 0x6D00, 27 | } 28 | 29 | impl From for u16 { 30 | #[expect(clippy::as_conversions)] 31 | fn from(sw: ResponseStatusWords) -> Self { 32 | sw as u16 33 | } 34 | } 35 | 36 | impl ResponseStatusWords { 37 | /// Transform a `ResponseStatusWords` to a `u16` as postfix without needing to specify the type. 38 | pub fn as_primitive(self) -> u16 { 39 | self.into() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /passkey-types/src/u2f/authenticate.rs: -------------------------------------------------------------------------------- 1 | use crate::ctap2::Flags; 2 | use std::array::TryFromSliceError; 3 | 4 | use super::ResponseStatusWords; 5 | 6 | /// The authentication Request MUST come with a parameter to determine it's use 7 | #[repr(u8)] 8 | #[derive(Debug)] 9 | pub enum AuthenticationParameter { 10 | /// If the control byte is set to 0x07 by the FIDO Client, the U2F token is supposed to simply 11 | /// check whether the provided key handle was originally created by this token, and whether it 12 | /// was created for the provided application parameter. If so, the U2F token MUST respond with 13 | /// an authentication response message:error:test-of-user-presence-required (note that despite 14 | /// the name this signals a success condition). If the key handle was not created by this U2F 15 | /// token, or if it was created for a different application parameter, the token MUST respond 16 | /// with an authentication response message:error:bad-key-handle. 17 | CheckOnly = 0x07, 18 | 19 | /// If the FIDO client sets the control byte to 0x03, then the U2F token is supposed to perform 20 | /// a real signature and respond with either an authentication response message:success or an 21 | /// appropriate error response (see below). The signature SHOULD only be provided if user 22 | /// presence could be validated. 23 | EnforceUserPresence = 0x03, 24 | 25 | /// If the FIDO client sets the control byte to 0x08, then the U2F token is supposed to perform 26 | /// a real signature and respond with either an authentication response message:success or an 27 | /// appropriate error response (see below). The signature MAY be provided without validating 28 | /// user presence. 29 | DontEnforceUserPresence = 0x08, 30 | } 31 | 32 | impl From for u8 { 33 | #[expect(clippy::as_conversions)] 34 | fn from(src: AuthenticationParameter) -> Self { 35 | src as u8 36 | } 37 | } 38 | 39 | impl From for AuthenticationParameter { 40 | fn from(src: u8) -> Self { 41 | match src { 42 | 0x07 => AuthenticationParameter::CheckOnly, 43 | 0x03 => AuthenticationParameter::EnforceUserPresence, 44 | 0x08 => AuthenticationParameter::DontEnforceUserPresence, 45 | _ => unreachable!("U2F Authentication parameter which is not in the spec"), 46 | } 47 | } 48 | } 49 | 50 | /// This message is used to initiate a U2F token authentication. The FIDO Client first contacts the 51 | /// relying party to obtain a challenge, and then constructs the authentication request message. 52 | #[derive(Debug)] 53 | pub struct AuthenticationRequest { 54 | /// During registration, the FIDO Client MAY send authentication request messages to the U2F 55 | /// token to figure out whether the U2F token has already been registered. In this case, the 56 | /// FIDO client will use the [`AuthenticationParameter::CheckOnly`] value for the control byte. 57 | /// In all other cases (i.e., during authentication), the FIDO Client MUST use the 58 | /// [`AuthenticationParameter::EnforceUserPresence`] or 59 | /// [`AuthenticationParameter::DontEnforceUserPresence`] 60 | pub parameter: AuthenticationParameter, 61 | /// The challenge parameter is the SHA-256 hash of the Client Data, a stringified JSON data 62 | /// structure that the FIDO Client prepares. Among other things, the Client Data contains the 63 | /// challenge from the relying party (hence the name of the parameter). 64 | pub challenge: [u8; 32], 65 | /// The application parameter is the SHA-256 hash of the UTF-8 encoding of the application 66 | /// identity of the application requesting the authentication as provided by the relying party. 67 | pub application: [u8; 32], 68 | /// This is provided by the relying party, and was obtained by the relying party during registration. 69 | pub key_handle: Vec, 70 | } 71 | 72 | impl AuthenticationRequest { 73 | /// Try parsing a data payload into an authentication request with the given parameter taken from 74 | /// the u2f message frame. 75 | #[expect(clippy::as_conversions)] 76 | pub fn try_from( 77 | data: &[u8], 78 | parameter: impl Into, 79 | ) -> Result { 80 | let (challenge, data) = data.split_at(32); 81 | let (application, data) = data.split_at(32); 82 | let (handle_len, data) = data.split_at(1); 83 | let key_handle = data[..handle_len[0] as usize].to_vec(); 84 | Ok(Self { 85 | parameter: parameter.into(), 86 | challenge: challenge.try_into()?, 87 | application: application.try_into()?, 88 | key_handle, 89 | }) 90 | } 91 | } 92 | 93 | /// This message is output by the U2F token after processing/signing the [`AuthenticationRequest`] 94 | /// message. Its raw representation is the concatenation of its fields. 95 | pub struct AuthenticationResponse { 96 | /// Whether user presence was verified or not 97 | pub user_presence: Flags, 98 | /// This a counter value that the U2F token increments every time it performs an authentication 99 | /// operation. It must be transported as big endian representation. 100 | pub counter: u32, 101 | /// This is a ECDSA signature (on P-256) over the following byte string. 102 | /// 1. The application parameter [32 bytes] from the authentication request message. 103 | /// 2. The above user presence byte [1 byte]. 104 | /// 3. The above counter [4 bytes]. 105 | /// 4. The challenge parameter [32 bytes] from the authentication request message. 106 | /// 107 | /// The signature is encoded in ANSI X9.62 format (see [ECDSA-ANSI] in bibliography). The 108 | /// signature is to be verified by the relying party using the public key obtained during 109 | /// registration. 110 | pub signature: Vec, 111 | } 112 | 113 | impl AuthenticationResponse { 114 | /// Encode the response to its successfull binary representation 115 | pub fn encode(self) -> Vec { 116 | [self.user_presence.into()] 117 | .into_iter() 118 | .chain(self.counter.to_be_bytes()) 119 | .chain(self.signature) 120 | .chain(u16::from(ResponseStatusWords::NoError).to_be_bytes()) // NoError indicates success 121 | .collect() 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /passkey-types/src/u2f/commands.rs: -------------------------------------------------------------------------------- 1 | use super::{authenticate::AuthenticationRequest, register::RegisterRequest, ResponseStatusWords}; 2 | 3 | /// U2F command, determined at the INS position, 4 | /// 5 | /// Anything between the values `0x40-0xbf` are vendor specific and therefore unsupported. 6 | /// 7 | /// This Enum does not `#[repr(u8)]` with discriminant values since we cannot currently have tuple 8 | /// variants in those enums until stabilizes. 9 | #[derive(Debug)] 10 | pub enum Command { 11 | /// Value of `0x01` with parameters of `P1 = 0x00`, `P2 = 0x00` 12 | Register, 13 | /// Value of `0x02` with parameters of `P1 = 0x03|0x07|0x08`, `P2 = 0x00` 14 | Authenticate, 15 | /// Value of `0x03` with parameters of `P1 = 0x00`, `P2 = 0x00` 16 | Version, 17 | /// Unsupported command value 18 | Unsuported(u8), 19 | } 20 | 21 | impl From for u8 { 22 | fn from(src: Command) -> Self { 23 | match src { 24 | Command::Register => 0x01, 25 | Command::Authenticate => 0x02, 26 | Command::Version => 0x03, 27 | Command::Unsuported(cmd) => cmd, 28 | } 29 | } 30 | } 31 | 32 | impl From for Command { 33 | fn from(src: u8) -> Self { 34 | match src { 35 | 0x01 => Command::Register, 36 | 0x02 => Command::Authenticate, 37 | 0x03 => Command::Version, 38 | cmd => Command::Unsuported(cmd), 39 | } 40 | } 41 | } 42 | 43 | /// Data payload of a U2F Request 44 | #[derive(Debug)] 45 | pub enum RequestPayload { 46 | /// Register command payload 47 | Register(RegisterRequest), 48 | /// Authentication command payload 49 | Authenticate(AuthenticationRequest), 50 | /// Version command payload 51 | Version, 52 | } 53 | 54 | /// U2F request frame 55 | #[derive(Debug)] 56 | pub struct Request { 57 | /// Must be of value 0 58 | pub cla: u8, 59 | /// Command byte 60 | pub ins: Command, 61 | /// Parameter byte, only used during authentication 62 | pub p1: u8, 63 | /// Length of the data payload 64 | pub data_len: usize, 65 | /// Data payload 66 | pub data: RequestPayload, 67 | } 68 | 69 | const REQUEST_HEADER_LEN: usize = 6; 70 | 71 | impl TryFrom<&[u8]> for Request { 72 | type Error = ResponseStatusWords; 73 | 74 | #[expect(clippy::as_conversions)] 75 | fn try_from(value: &[u8]) -> Result { 76 | if value.len() < REQUEST_HEADER_LEN { 77 | return Err(ResponseStatusWords::WrongLength); 78 | } 79 | 80 | let cla = value[0]; 81 | if cla != 0 { 82 | return Err(ResponseStatusWords::WrongData); 83 | } 84 | let ins = Command::from(value[1]); 85 | let p1 = value[2]; 86 | let data_start = REQUEST_HEADER_LEN + 1; 87 | // SAFETY: This unwrap is safe since 3..7 gives 4 bytes which is a safe conversion to an 88 | // array of len 4. Technically the first of these bytes is `p2` the second parameter, 89 | // but in the base U2F spec this will always be 0. So this length is safe. 90 | let data_len = u32::from_be_bytes(value[3..data_start].try_into().unwrap()) as usize; 91 | let data_end = data_start + data_len; 92 | let payload = &value[data_start..data_end]; 93 | 94 | let data = match ins { 95 | Command::Register => RequestPayload::Register( 96 | payload 97 | .try_into() 98 | // Wrong length because it must be two SHA256's which are 32 bytes each 99 | .map_err(|_| ResponseStatusWords::WrongLength)?, 100 | ), 101 | Command::Authenticate => RequestPayload::Authenticate( 102 | AuthenticationRequest::try_from(payload, p1) 103 | .map_err(|_| ResponseStatusWords::WrongLength)?, 104 | ), 105 | Command::Version => RequestPayload::Version, 106 | Command::Unsuported(_) => return Err(ResponseStatusWords::InsNotSupported), 107 | }; 108 | 109 | Ok(Request { 110 | cla, 111 | ins, 112 | p1, 113 | data_len, 114 | data, 115 | }) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /passkey-types/src/u2f/register.rs: -------------------------------------------------------------------------------- 1 | use std::array::TryFromSliceError; 2 | 3 | use super::ResponseStatusWords; 4 | 5 | /// Request payload to register a new user 6 | #[derive(Debug)] 7 | pub struct RegisterRequest { 8 | /// SHA256 hash challenge issued by the relying party 9 | pub challenge: [u8; 32], 10 | /// SHA256 of the application identity 11 | pub application: [u8; 32], 12 | } 13 | 14 | impl TryFrom<&[u8]> for RegisterRequest { 15 | type Error = TryFromSliceError; 16 | 17 | fn try_from(data: &[u8]) -> Result { 18 | Ok(Self { 19 | challenge: data[..32].try_into()?, 20 | application: data[32..].try_into()?, 21 | }) 22 | } 23 | } 24 | 25 | /// Register response payload 26 | /// 27 | /// This message is output by the U2F token once it created a new keypair in response to the 28 | /// registration request message. Note that U2F tokens SHOULD verify user presence before returning 29 | /// a registration response success message (otherwise they SHOULD return a 30 | /// test-of-user-presence-required message - see above). 31 | pub struct RegisterResponse { 32 | // Reserved byte, value 0x05 which is added in the `encode` method 33 | /// This is the (uncompressed) x,y-representation of a curve point on the P-256 NIST elliptic 34 | /// curve. User's new public key 35 | pub public_key: PublicKey, 36 | 37 | // Key handle length byte which specifies the length of the key handle (see below). The value is 38 | // unsigned (range 0-255) 39 | /// This a handle that allows the U2F token to identify the generated key pair. U2F tokens MAY 40 | /// wrap the generated private key and the application id it was generated for, and output that 41 | /// as the key handle. 42 | pub key_handle: Vec, 43 | 44 | /// This is a certificate in X.509 DER format. Parsing of the X.509 certificate unambiguously 45 | /// establishes its ending. 46 | pub attestation_certificate: Vec, 47 | 48 | /// This is a ECDSA signature (on P-256) over the following byte string: 49 | /// 1. A byte reserved for future use [1 byte] with the value 0x00. 50 | /// 2. The application parameter [32 bytes] from the registration request message. 51 | /// 3. The challenge parameter [32 bytes] from the registration request message. 52 | /// 4. The above key handle [variable length]. (Note that the key handle length is not included in the signature base string. 53 | /// This doesn't cause confusion in the signature base string, since all other parameters in the signature base string are fixed-length.) 54 | /// 5. The above user public key [65 bytes]. 55 | pub signature: Vec, 56 | } 57 | 58 | /// U2F public key is the concatenation of `0x04 | x | y` where `0x04` signifies ecc uncompressed. 59 | #[derive(Clone, Copy)] 60 | pub struct PublicKey { 61 | // magic 0x04 byte which is added in the `encode` method 62 | /// X coordinate of the ECC public key 63 | pub x: [u8; 32], 64 | /// Y coordinate of the ECC public key 65 | pub y: [u8; 32], 66 | } 67 | 68 | impl RegisterResponse { 69 | /// Encode the Response to it's binary format for a successfull response 70 | #[expect(clippy::as_conversions)] 71 | pub fn encode(self) -> Vec { 72 | [0x05] // Reserved magic byte 73 | .into_iter() 74 | .chain(self.public_key.encode()) 75 | .chain([self.key_handle.len() as u8]) 76 | .chain(self.key_handle) 77 | .chain(self.attestation_certificate) 78 | .chain(self.signature) 79 | .chain(ResponseStatusWords::NoError.as_primitive().to_be_bytes()) // NoError indicates success 80 | .collect() 81 | } 82 | } 83 | 84 | impl PublicKey { 85 | /// Encode a Public key into an iterator 86 | pub fn encode(self) -> impl Iterator { 87 | [0x04].into_iter().chain(self.x).chain(self.y) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /passkey-types/src/u2f/version.rs: -------------------------------------------------------------------------------- 1 | use super::ResponseStatusWords; 2 | 3 | /// The u2f version representation 4 | pub struct Version; 5 | 6 | impl Version { 7 | /// Encode this version into its byte representation. 8 | pub fn encode(self) -> Vec { 9 | b"U2F_V2" 10 | .iter() 11 | .copied() 12 | .chain(ResponseStatusWords::NoError.as_primitive().to_be_bytes()) 13 | .collect() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /passkey-types/src/utils.rs: -------------------------------------------------------------------------------- 1 | //! Utils is a module providing utility functions used by various parts of passkey-rs. 2 | pub(crate) mod bytes; 3 | #[macro_use] 4 | pub(crate) mod repr_enum; 5 | pub(crate) mod serde; 6 | #[macro_use] 7 | pub(crate) mod serde_workaround; 8 | 9 | pub mod crypto; 10 | pub mod encoding; 11 | pub mod rand; 12 | -------------------------------------------------------------------------------- /passkey-types/src/utils/bytes.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Deref, DerefMut}; 2 | 3 | use serde::{de::Visitor, Deserialize, Deserializer, Serialize}; 4 | use typeshare::typeshare; 5 | 6 | use super::encoding; 7 | 8 | /// A newtype around `Vec` which serializes using the transport format's byte representation. 9 | /// 10 | /// When feature `serialize_bytes_as_base64_string` is set, this type will be serialized into a 11 | /// `base64url` representation instead. Note that this type should not be used externally when this 12 | /// feature is set, such as in Kotlin, to avoid a serialization errors. In the future, this feature 13 | /// flag can be removed when typeshare supports target/language specific serialization: 14 | /// 15 | /// 16 | /// This will use an array of numbers for JSON, and a byte string in CBOR for example. 17 | /// 18 | /// It also supports deserializing from `base64` and `base64url` formatted strings. 19 | #[typeshare(transparent)] 20 | #[derive(Debug, Default, PartialEq, Eq, Clone, Hash)] 21 | #[repr(transparent)] 22 | pub struct Bytes(Vec); 23 | 24 | impl Deref for Bytes { 25 | type Target = Vec; 26 | 27 | fn deref(&self) -> &Self::Target { 28 | &self.0 29 | } 30 | } 31 | 32 | impl DerefMut for Bytes { 33 | fn deref_mut(&mut self) -> &mut Self::Target { 34 | &mut self.0 35 | } 36 | } 37 | 38 | impl From> for Bytes { 39 | fn from(inner: Vec) -> Self { 40 | Bytes(inner) 41 | } 42 | } 43 | 44 | impl From<&[u8]> for Bytes { 45 | fn from(value: &[u8]) -> Self { 46 | Bytes(value.to_vec()) 47 | } 48 | } 49 | 50 | impl From for Vec { 51 | fn from(src: Bytes) -> Self { 52 | src.0 53 | } 54 | } 55 | 56 | impl From for String { 57 | fn from(src: Bytes) -> Self { 58 | encoding::base64url(&src) 59 | } 60 | } 61 | 62 | /// The string given for decoding is not `base64url` nor `base64` encoded data. 63 | #[derive(Debug)] 64 | pub struct NotBase64Encoded; 65 | 66 | impl TryFrom<&str> for Bytes { 67 | type Error = NotBase64Encoded; 68 | 69 | fn try_from(value: &str) -> Result { 70 | encoding::try_from_base64url(value) 71 | .or_else(|| encoding::try_from_base64(value)) 72 | .ok_or(NotBase64Encoded) 73 | .map(Self) 74 | } 75 | } 76 | 77 | impl FromIterator for Bytes { 78 | fn from_iter>(iter: T) -> Self { 79 | Bytes(iter.into_iter().collect()) 80 | } 81 | } 82 | 83 | impl IntoIterator for Bytes { 84 | type Item = u8; 85 | 86 | type IntoIter = std::vec::IntoIter; 87 | 88 | fn into_iter(self) -> Self::IntoIter { 89 | self.0.into_iter() 90 | } 91 | } 92 | 93 | impl<'a> IntoIterator for &'a Bytes { 94 | type Item = &'a u8; 95 | 96 | type IntoIter = std::slice::Iter<'a, u8>; 97 | 98 | fn into_iter(self) -> Self::IntoIter { 99 | self.0.iter() 100 | } 101 | } 102 | 103 | impl Serialize for Bytes { 104 | fn serialize(&self, serializer: S) -> Result 105 | where 106 | S: serde::Serializer, 107 | { 108 | if cfg!(feature = "serialize_bytes_as_base64_string") { 109 | serializer.serialize_str(&encoding::base64url(&self.0)) 110 | } else { 111 | serializer.serialize_bytes(&self.0) 112 | } 113 | } 114 | } 115 | 116 | impl<'de> Deserialize<'de> for Bytes { 117 | fn deserialize(deserializer: D) -> Result 118 | where 119 | D: Deserializer<'de>, 120 | { 121 | struct Base64Visitor; 122 | 123 | impl<'de> Visitor<'de> for Base64Visitor { 124 | type Value = Bytes; 125 | 126 | fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 127 | write!(f, "A vector of bytes or a base46(url) encoded string") 128 | } 129 | fn visit_borrowed_str(self, v: &'de str) -> Result 130 | where 131 | E: serde::de::Error, 132 | { 133 | self.visit_str(v) 134 | } 135 | fn visit_string(self, v: String) -> Result 136 | where 137 | E: serde::de::Error, 138 | { 139 | self.visit_str(&v) 140 | } 141 | fn visit_str(self, v: &str) -> Result 142 | where 143 | E: serde::de::Error, 144 | { 145 | v.try_into().map_err(|_| { 146 | E::invalid_value( 147 | serde::de::Unexpected::Str(v), 148 | &"A base64(url) encoded string", 149 | ) 150 | }) 151 | } 152 | fn visit_seq(self, mut seq: A) -> Result 153 | where 154 | A: serde::de::SeqAccess<'de>, 155 | { 156 | let mut buf = Vec::with_capacity(seq.size_hint().unwrap_or_default()); 157 | while let Some(byte) = seq.next_element()? { 158 | buf.push(byte); 159 | } 160 | Ok(Bytes(buf)) 161 | } 162 | fn visit_bytes(self, v: &[u8]) -> Result 163 | where 164 | E: serde::de::Error, 165 | { 166 | Ok(Bytes(v.to_vec())) 167 | } 168 | } 169 | deserializer.deserialize_any(Base64Visitor) 170 | } 171 | } 172 | 173 | #[cfg(test)] 174 | mod tests { 175 | use super::*; 176 | use std::collections::HashMap; 177 | #[test] 178 | fn deserialize_many_formats_into_base64url_vec() { 179 | let json = r#"{ 180 | "array": [101,195,212,161,191,112,75,189,152,52,121,17,62,113,114,164], 181 | "base64url": "ZcPUob9wS72YNHkRPnFypA", 182 | "base64": "ZcPUob9wS72YNHkRPnFypA==" 183 | }"#; 184 | 185 | let deserialized: HashMap<&str, Bytes> = 186 | serde_json::from_str(json).expect("failed to deserialize"); 187 | 188 | assert_eq!(deserialized["array"], deserialized["base64url"]); 189 | assert_eq!(deserialized["base64url"], deserialized["base64"]); 190 | } 191 | 192 | #[test] 193 | fn deserialization_should_fail() { 194 | let json = r#"{ 195 | "array": ["ZcPUob9wS72YNHkRPnFypA","ZcPUob9wS72YNHkRPnFypA=="], 196 | }"#; 197 | 198 | serde_json::from_str::>(json) 199 | .expect_err("did not give an error as expected."); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /passkey-types/src/utils/crypto.rs: -------------------------------------------------------------------------------- 1 | //! Collection of common cryptography primitives used in serialization of types. 2 | 3 | use hmac::{Hmac, Mac}; 4 | use sha2::{Digest, Sha256}; 5 | 6 | /// Compute the SHA-256 of the given `data`. 7 | pub fn sha256(data: &[u8]) -> [u8; 32] { 8 | // SAFETY: sha256 always gives a 32 byte array 9 | Sha256::digest(data).into() 10 | } 11 | 12 | /// Compute the HMAC of the given data with the given key 13 | pub fn hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32] { 14 | let mut mac = Hmac::::new_from_slice(key).expect("hmac can take key of any size"); 15 | mac.update(data); 16 | 17 | mac.finalize().into_bytes().into() 18 | } 19 | -------------------------------------------------------------------------------- /passkey-types/src/utils/encoding.rs: -------------------------------------------------------------------------------- 1 | //! Utilitie functions for encoding datatypes in a consistent way accross the `passkey` libraries 2 | //! with a mind on global webauthn ecosystem support. 3 | 4 | use data_encoding::{Specification, BASE64, BASE64URL, BASE64URL_NOPAD, BASE64_NOPAD}; 5 | 6 | /// Convert bytes to base64 without padding 7 | pub fn base64(data: &[u8]) -> String { 8 | BASE64_NOPAD.encode(data) 9 | } 10 | 11 | /// Convert bytes to base64url without padding 12 | pub fn base64url(data: &[u8]) -> String { 13 | BASE64URL_NOPAD.encode(data) 14 | } 15 | 16 | /// Try parsing from base64 with or without padding 17 | pub(crate) fn try_from_base64(input: &str) -> Option> { 18 | let padding = BASE64.specification().padding.unwrap(); 19 | let sane_string = input.trim_end_matches(padding); 20 | BASE64_NOPAD.decode(sane_string.as_bytes()).ok() 21 | } 22 | 23 | /// Try parsing from base64url with or without padding 24 | pub fn try_from_base64url(input: &str) -> Option> { 25 | let specs = BASE64URL.specification(); 26 | let padding = specs.padding.unwrap(); 27 | let specs = Specification { 28 | check_trailing_bits: false, 29 | padding: None, 30 | ..specs 31 | }; 32 | let encoding = specs.encoding().unwrap(); 33 | let sane_string = input.trim_end_matches(padding); 34 | encoding.decode(sane_string.as_bytes()).ok() 35 | } 36 | -------------------------------------------------------------------------------- /passkey-types/src/utils/rand.rs: -------------------------------------------------------------------------------- 1 | //! Random number generator utilities used for tests 2 | 3 | use rand::RngCore; 4 | 5 | fn random_fill(buffer: &mut [u8]) { 6 | let mut random = rand::thread_rng(); 7 | random.fill_bytes(buffer); 8 | } 9 | 10 | /// Generate random data of specific length. 11 | pub fn random_vec(len: usize) -> Vec { 12 | let mut data = vec![0u8; len]; 13 | random_fill(&mut data); 14 | data 15 | } 16 | -------------------------------------------------------------------------------- /passkey-types/src/utils/repr_enum.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | /// Error converting an integer code into an enum variant. The integer is not within the range of values 4 | /// in the known error type. 5 | #[derive(Debug)] 6 | pub struct CodeOutOfRange(pub I); 7 | 8 | impl Display for CodeOutOfRange { 9 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 10 | write!(f, "Value {} is out of range", self.0) 11 | } 12 | } 13 | 14 | /// Generate an enum with associated values, plus conversion methods 15 | macro_rules! repr_enum { 16 | ( $(#[$attr:meta])* $enum_name:ident: $repr:ident {$($(#[$fattr:meta])* $name:ident: $val:expr,)* } ) => { 17 | #[allow(clippy::allow_attributes, reason = "the macro doesn't always play nicely with expect()")] 18 | #[allow(non_camel_case_types)] 19 | $(#[$attr])* 20 | #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] 21 | #[non_exhaustive] 22 | #[repr($repr)] 23 | pub enum $enum_name { 24 | $($(#[$fattr])* $name = $val,)* 25 | } 26 | impl TryFrom<$repr> for $enum_name { 27 | type Error = $crate::utils::repr_enum::CodeOutOfRange<$repr>; 28 | 29 | fn try_from(value: $repr) -> Result { 30 | Ok(match value { 31 | $($val => Self::$name,)* 32 | _ => return Err($crate::utils::repr_enum::CodeOutOfRange(value)) 33 | }) 34 | } 35 | } 36 | impl From<$enum_name> for $repr { 37 | #[expect(clippy::as_conversions)] 38 | fn from(src: $enum_name) -> Self { 39 | src as $repr 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /passkey-types/src/utils/serde.rs: -------------------------------------------------------------------------------- 1 | //! Utilities to be used in serde derives for more robust (de)serializations. 2 | 3 | use std::str::FromStr; 4 | 5 | use serde::{ 6 | de::{Error, Visitor}, 7 | Deserialize, Deserializer, 8 | }; 9 | 10 | /// Many fields in the webauthn spec have the following wording. 11 | /// 12 | /// > The values SHOULD be members of `T` but client platforms MUST ignore unknown values. 13 | /// 14 | /// This method is a simple way of ignoring unknown values without failing deserialization. 15 | pub(crate) fn ignore_unknown<'de, D, T>(de: D) -> Result 16 | where 17 | D: Deserializer<'de>, 18 | T: Deserialize<'de> + Default, 19 | { 20 | Ok(T::deserialize(de).unwrap_or_default()) 21 | } 22 | 23 | #[derive(Debug, Default)] 24 | enum PossiblyUnknown { 25 | Some(T), 26 | #[default] 27 | None, 28 | } 29 | 30 | impl<'de, T> Deserialize<'de> for PossiblyUnknown 31 | where 32 | T: Deserialize<'de>, 33 | { 34 | fn deserialize(de: D) -> Result 35 | where 36 | D: Deserializer<'de>, 37 | { 38 | Ok(match T::deserialize(de) { 39 | Ok(val) => Self::Some(val), 40 | Err(_) => Self::None, 41 | }) 42 | } 43 | } 44 | 45 | pub(crate) fn ignore_unknown_opt_vec<'de, D, T>(de: D) -> Result>, D::Error> 46 | where 47 | D: Deserializer<'de>, 48 | T: Deserialize<'de> + std::fmt::Debug, 49 | { 50 | struct IgnoreUnknown(std::marker::PhantomData); 51 | 52 | impl<'d, T> Visitor<'d> for IgnoreUnknown 53 | where 54 | T: Deserialize<'d> + std::fmt::Debug, 55 | { 56 | type Value = Option>; 57 | 58 | fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 59 | write!(f, "a list of types") 60 | } 61 | 62 | fn visit_seq(self, mut seq: A) -> Result 63 | where 64 | A: serde::de::SeqAccess<'d>, 65 | { 66 | let mut array = Vec::with_capacity(seq.size_hint().unwrap_or_default()); 67 | while let Some(elem) = seq.next_element::>()? { 68 | if let PossiblyUnknown::Some(elem) = elem { 69 | array.push(elem) 70 | } 71 | } 72 | Ok(Some(array)) 73 | } 74 | fn visit_none(self) -> Result 75 | where 76 | E: Error, 77 | { 78 | Ok(None) 79 | } 80 | } 81 | 82 | de.deserialize_seq(IgnoreUnknown(std::marker::PhantomData)) 83 | } 84 | 85 | pub(crate) fn ignore_unknown_vec<'de, D, T>(de: D) -> Result, D::Error> 86 | where 87 | D: Deserializer<'de>, 88 | T: Deserialize<'de> + std::fmt::Debug, 89 | { 90 | ignore_unknown_opt_vec(de) 91 | .and_then(|opt| opt.ok_or_else(|| D::Error::custom("Expected a list of types"))) 92 | } 93 | 94 | pub mod i64_to_iana { 95 | use super::StringOrNum; 96 | use std::marker::PhantomData; 97 | 98 | use coset::iana::EnumI64; 99 | 100 | pub fn serialize(value: &T, ser: S) -> Result 101 | where 102 | S: serde::Serializer, 103 | T: EnumI64, 104 | { 105 | ser.serialize_i64(value.to_i64()) 106 | } 107 | 108 | pub fn deserialize<'de, D, T>(de: D) -> Result 109 | where 110 | D: serde::Deserializer<'de>, 111 | T: EnumI64, 112 | { 113 | let value: i64 = de.deserialize_any(StringOrNum(PhantomData))?; 114 | 115 | T::from_i64(value).ok_or_else(|| { 116 | ::invalid_value( 117 | serde::de::Unexpected::Signed(value), 118 | &"An iana::Algorithm value", 119 | ) 120 | }) 121 | } 122 | } 123 | 124 | struct StringOrNum(pub std::marker::PhantomData); 125 | 126 | impl Visitor<'_> for StringOrNum 127 | where 128 | T: FromStr + TryFrom + TryFrom, 129 | { 130 | type Value = T; 131 | 132 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 133 | formatter.write_str("A number or a stringified number") 134 | } 135 | 136 | fn visit_str(self, v: &str) -> Result 137 | where 138 | E: Error, 139 | { 140 | if let Ok(v) = FromStr::from_str(v) { 141 | Ok(v) 142 | } else if let Ok(v) = f64::from_str(v) { 143 | self.visit_f64(v) 144 | } else { 145 | Err(E::custom("Was not a stringified number")) 146 | } 147 | } 148 | 149 | fn visit_string(self, v: String) -> Result 150 | where 151 | E: Error, 152 | { 153 | self.visit_str(&v) 154 | } 155 | 156 | fn visit_i64(self, v: i64) -> Result 157 | where 158 | E: Error, 159 | { 160 | TryFrom::try_from(v).map_err(|_| E::custom("out of range")) 161 | } 162 | 163 | fn visit_i32(self, v: i32) -> Result 164 | where 165 | E: Error, 166 | { 167 | self.visit_i64(v.into()) 168 | } 169 | 170 | fn visit_i16(self, v: i16) -> Result 171 | where 172 | E: Error, 173 | { 174 | self.visit_i64(v.into()) 175 | } 176 | 177 | fn visit_i8(self, v: i8) -> Result 178 | where 179 | E: Error, 180 | { 181 | self.visit_i64(v.into()) 182 | } 183 | 184 | fn visit_u64(self, v: u64) -> Result 185 | where 186 | E: Error, 187 | { 188 | TryFrom::try_from(v).map_err(|_| E::custom("out of range")) 189 | } 190 | 191 | fn visit_u32(self, v: u32) -> Result 192 | where 193 | E: Error, 194 | { 195 | self.visit_u64(v.into()) 196 | } 197 | 198 | fn visit_u16(self, v: u16) -> Result 199 | where 200 | E: Error, 201 | { 202 | self.visit_u64(v.into()) 203 | } 204 | 205 | fn visit_u8(self, v: u8) -> Result 206 | where 207 | E: Error, 208 | { 209 | self.visit_u64(v.into()) 210 | } 211 | 212 | fn visit_f32(self, v: f32) -> Result 213 | where 214 | E: Error, 215 | { 216 | self.visit_f64(v.into()) 217 | } 218 | 219 | fn visit_f64(self, v: f64) -> Result 220 | where 221 | E: Error, 222 | { 223 | #[expect(clippy::as_conversions)] 224 | // Ensure the float has an integer representation, 225 | // or be 0 if it is a non-integer number 226 | self.visit_i64(if v.is_normal() { v as i64 } else { 0 }) 227 | } 228 | } 229 | 230 | pub(crate) fn maybe_stringified<'de, D>(de: D) -> Result, D::Error> 231 | where 232 | D: Deserializer<'de>, 233 | { 234 | de.deserialize_any(StringOrNum(std::marker::PhantomData)) 235 | .map(Some) 236 | } 237 | 238 | #[cfg(test)] 239 | mod tests { 240 | use super::*; 241 | #[test] 242 | fn from_float_representations() { 243 | #[derive(Deserialize)] 244 | struct FromFloat { 245 | #[serde(deserialize_with = "maybe_stringified")] 246 | num: Option, 247 | } 248 | 249 | let float_with_0 = r#"{"num": 0.0}"#; 250 | let result: FromFloat = 251 | serde_json::from_str(float_with_0).expect("failed to parse from 0.0"); 252 | assert_eq!(result.num, Some(0)); 253 | 254 | let float_ends_with_0 = r#"{"num": 1800.0}"#; 255 | let result: FromFloat = 256 | serde_json::from_str(float_ends_with_0).expect("failed to parse from 1800.0"); 257 | assert_eq!(result.num, Some(1800)); 258 | 259 | let float_ends_with_num = r#"{"num": 1800.1234}"#; 260 | let result: FromFloat = 261 | serde_json::from_str(float_ends_with_num).expect("failed to parse from 1800.1234"); 262 | assert_eq!(result.num, Some(1800)); 263 | 264 | let sub_zero = r#"{"num": 0.1234}"#; 265 | let result: FromFloat = 266 | serde_json::from_str(sub_zero).expect("failed to parse from 0.1234"); 267 | assert_eq!(result.num, Some(0)); 268 | 269 | let scientific = r#"{"num": 1.0e-308}"#; 270 | let result: FromFloat = 271 | serde_json::from_str(scientific).expect("failed to parse from 1.0e-308"); 272 | assert_eq!(result.num, Some(0)); 273 | 274 | // Ignoring these cases because `serde_json` will fail to deserialize these values 275 | // https://github.com/serde-rs/json/issues/842 276 | 277 | // let nan = r#"{"num": NaN}"#; 278 | // let result: FromFloat = serde_json::from_str(nan).expect("failed to parse from NaN"); 279 | // assert_eq!(result.num, Some(0)); 280 | 281 | // let inf = r#"{"num": Infinity}"#; 282 | // let result: FromFloat = serde_json::from_str(inf).expect("failed to parse from Infinity"); 283 | // assert_eq!(result.num, Some(0)); 284 | 285 | // let neg_inf = r#"{"num": -Infinity}"#; 286 | // let result: FromFloat = 287 | // serde_json::from_str(neg_inf).expect("failed to parse from -Infinity"); 288 | // assert_eq!(result.num, Some(0)); 289 | 290 | let float_with_0_str = r#"{"num": "0.0"}"#; 291 | let result: FromFloat = 292 | serde_json::from_str(float_with_0_str).expect("failed to parse from stringified 0.0"); 293 | assert_eq!(result.num, Some(0)); 294 | 295 | let float_ends_with_0_str = r#"{"num": "1800.0"}"#; 296 | let result: FromFloat = serde_json::from_str(float_ends_with_0_str) 297 | .expect("failed to parse from stringified 1800.0"); 298 | assert_eq!(result.num, Some(1800)); 299 | 300 | let float_ends_with_num_str = r#"{"num": "1800.1234"}"#; 301 | let result: FromFloat = serde_json::from_str(float_ends_with_num_str) 302 | .expect("failed to parse from stringified 1800.1234"); 303 | assert_eq!(result.num, Some(1800)); 304 | 305 | let sub_zero_str = r#"{"num": "0.1234"}"#; 306 | let result: FromFloat = 307 | serde_json::from_str(sub_zero_str).expect("failed to parse from stringified 0.1234"); 308 | assert_eq!(result.num, Some(0)); 309 | 310 | let scientific_str = r#"{"num": "1.0e-308"}"#; 311 | let result: FromFloat = serde_json::from_str(scientific_str) 312 | .expect("failed to parse from stringified 1.0e-308"); 313 | assert_eq!(result.num, Some(0)); 314 | 315 | let nan_str = r#"{"num": "NaN"}"#; 316 | let result: FromFloat = 317 | serde_json::from_str(nan_str).expect("failed to parse from stringified NaN"); 318 | assert_eq!(result.num, Some(0)); 319 | 320 | let inf_str = r#"{"num": "Infinity"}"#; 321 | let result: FromFloat = 322 | serde_json::from_str(inf_str).expect("failed to parse from stringified Infinity"); 323 | assert_eq!(result.num, Some(0)); 324 | 325 | let neg_inf_str = r#"{"num": "-Infinity"}"#; 326 | let result: FromFloat = 327 | serde_json::from_str(neg_inf_str).expect("failed to parse from stringified -Infinity"); 328 | assert_eq!(result.num, Some(0)); 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /passkey-types/src/utils/serde_workaround.rs: -------------------------------------------------------------------------------- 1 | /// This is a workaround to deriving [`serde::Deserialize`] and [`serde::Serialize`] but where the 2 | /// field identifiers are serialized as integers rather than strings. This is due to the fact that 3 | /// serde can only serialize struct fields as strings, including when using the `#[serde(rename)]` 4 | /// attribute. 5 | /// 6 | /// Issues to keep an eye on for this workaround to no longer be relevant: 7 | /// * rename for enum variants: 8 | /// * rename for struct fields: 9 | macro_rules! serde_workaround { 10 | ( 11 | $(#[$attr:meta])* 12 | pub struct $name:ident {$( 13 | $(#[doc=$doc:literal])* 14 | #[serde(rename = $discriminant:literal$(,$default:ident)?$(,skip_serializing_if = $method:path)?$(,deserialize_with = $de:path)?)] 15 | $vis:vis $field:ident: $ty:ty, 16 | )*} 17 | ) => { 18 | $(#[$attr])* 19 | pub struct $name {$( 20 | $(#[doc=$doc])* 21 | $vis $field: $ty, 22 | )*} 23 | 24 | #[doc(hidden)] 25 | const _: () = { 26 | use serde::{de::MapAccess, ser::SerializeMap, Deserialize, Serialize}; 27 | use strum::{EnumString, FromRepr, IntoStaticStr}; 28 | 29 | fn struct_len(_instance: &$name) -> usize { 30 | 0 $(+ serde_workaround_struct_len!(_instance.$field $(;$method)?))* 31 | } 32 | 33 | 34 | #[allow(clippy::allow_attributes, reason = "the macro doesn't always play nicely with expect()")] 35 | #[allow(non_camel_case_types)] 36 | #[derive(FromRepr, IntoStaticStr, EnumString, Clone, Copy)] 37 | #[strum(serialize_all = "camelCase")] 38 | #[repr(u8)] 39 | enum Ident { 40 | $($field = $discriminant,)* 41 | #[strum(disabled)] 42 | Unknown 43 | } 44 | 45 | impl Serialize for Ident { 46 | #[expect(clippy::as_conversions)] 47 | fn serialize(&self, serializer: S) -> Result 48 | where 49 | S: serde::Serializer, 50 | { 51 | serializer.serialize_u8(*self as u8) 52 | } 53 | } 54 | 55 | impl Serialize for $name { 56 | fn serialize(&self, serializer: S) -> Result 57 | where 58 | S: serde::Serializer 59 | { 60 | let mut serde_state = serde::Serializer::serialize_map(serializer, Some(struct_len(&self)))?; 61 | $( 62 | serde_serialize_entry!{serde_state; self.$field $(;$method)?} 63 | )* 64 | serde_state.end() 65 | } 66 | } 67 | 68 | struct FieldVisitor; 69 | impl<'de> serde::de::Visitor<'de> for FieldVisitor { 70 | type Value = Ident; 71 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 72 | formatter.write_str("field identifier") 73 | } 74 | fn visit_u128(self, value: u128) -> Result 75 | where 76 | E: serde::de::Error, 77 | { 78 | let repr: u8 = value.try_into().map_err(|_| { 79 | E::invalid_value(serde::de::Unexpected::Bytes(value.to_ne_bytes().as_slice()), &format!( 80 | "Descriminant of value {} too big to be an identifier", 81 | value 82 | ).as_str()) 83 | })?; 84 | self.visit_u8(repr) 85 | } 86 | delegate_visit_to_u8!{ 87 | visit_u64: u64, 88 | visit_u32: u32, 89 | visit_u16: u16, 90 | } 91 | fn visit_u8(self, value: u8) -> Result 92 | where 93 | E: serde::de::Error, 94 | { 95 | Ok(Ident::from_repr(value).unwrap_or(Ident::Unknown)) 96 | } 97 | fn visit_str(self, value: &str) -> Result 98 | where 99 | E: serde::de::Error, 100 | { 101 | Ok(Ident::try_from(value).unwrap_or(Ident::Unknown)) 102 | } 103 | fn visit_bytes(self, value: &[u8]) -> Result 104 | where 105 | E: serde::de::Error, 106 | { 107 | let ident = if let Ok(value) = std::str::from_utf8(value) { 108 | Ident::try_from(value).unwrap_or(Ident::Unknown) 109 | } else { 110 | Ident::Unknown 111 | }; 112 | Ok(ident) 113 | } 114 | } 115 | impl<'de> Deserialize<'de> for Ident { 116 | #[inline] 117 | fn deserialize(deserializer: D) -> Result 118 | where 119 | D: serde::Deserializer<'de>, 120 | { 121 | serde::Deserializer::deserialize_any(deserializer, FieldVisitor) 122 | } 123 | } 124 | 125 | struct Visitor; 126 | 127 | impl<'de> serde::de::Visitor<'de> for Visitor { 128 | type Value = $name; 129 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 130 | formatter.write_str(concat!("struct ", stringify!($name))) 131 | } 132 | 133 | #[inline] 134 | fn visit_map(self, mut map: A) -> Result 135 | where 136 | A: MapAccess<'de>, 137 | { 138 | $( 139 | let mut $field: Option<$ty> = None; 140 | )* 141 | 142 | while let Some(key) = map.next_key::()? { 143 | match key { 144 | $( 145 | Ident::$field => serde_deserialize_with!(key; $field; map$(; $ty; $de; $name)?), 146 | )* 147 | Ident::Unknown => { 148 | let _ = map.next_value::()?; 149 | } 150 | } 151 | } 152 | $( 153 | let $field = serde_visit_map!($field $(;$default)?); 154 | )* 155 | Ok($name { 156 | $($field,)* 157 | }) 158 | } 159 | } 160 | impl<'de> Deserialize<'de> for $name { 161 | #[inline] 162 | fn deserialize(deserializer: D) -> Result 163 | where 164 | D: serde::Deserializer<'de>, 165 | { 166 | serde::Deserializer::deserialize_map(deserializer, Visitor) 167 | } 168 | } 169 | }; 170 | }; 171 | } 172 | 173 | macro_rules! serde_workaround_struct_len { 174 | ($field:expr; $skip_if:path) => { 175 | if $skip_if(&$field) { 176 | 0 177 | } else { 178 | 1 179 | } 180 | }; 181 | ($field:expr ) => { 182 | 1 183 | }; 184 | } 185 | 186 | macro_rules! serde_serialize_entry { 187 | ($state:ident; $self:ident.$field:ident; $skip_if:path) => { 188 | if !$skip_if(&$self.$field) { 189 | serde_serialize_entry!($state; $self.$field) 190 | } 191 | }; 192 | ($state:ident; $self:ident.$field:ident) => { 193 | $state.serialize_entry(&Ident::$field, &$self.$field)? 194 | }; 195 | } 196 | 197 | macro_rules! serde_visit_map { 198 | ($field:ident; default) => { 199 | $field.unwrap_or_default() 200 | }; 201 | ($field:ident) => { 202 | $field.ok_or_else(|| ::missing_field(Ident::$field.into()))? 203 | }; 204 | } 205 | 206 | macro_rules! serde_deserialize_with { 207 | ($key:ident; $field:ident; $map:ident; $ty:ty; $de_with:path; $name:ident) => {{ 208 | struct __DeserializeWith<'de> { 209 | value: $ty, 210 | phantom: ::std::marker::PhantomData<$name>, 211 | lifetime: ::std::marker::PhantomData<&'de ()>, 212 | } 213 | impl<'de> ::serde::Deserialize<'de> for __DeserializeWith<'de> { 214 | fn deserialize(deserializer: D) -> Result 215 | where 216 | D: ::serde::Deserializer<'de>, 217 | { 218 | $de_with(deserializer).map(|value| __DeserializeWith { 219 | value, 220 | phantom: ::std::marker::PhantomData, 221 | lifetime: ::std::marker::PhantomData, 222 | }) 223 | } 224 | } 225 | $crate::utils::serde_workaround::check_is_already_set($key, &$field, &$map)?; 226 | $field = Some($map.next_value::<__DeserializeWith<'de>>()?.value); 227 | }}; 228 | ($key:ident; $field:ident; $map:ident) => { 229 | $crate::utils::serde_workaround::set_if_none($key, &mut $field, &mut $map)? 230 | }; 231 | } 232 | 233 | macro_rules! delegate_visit_to_u8 { 234 | ($($fn:ident: $int:ty,)+) => { 235 | $( 236 | fn $fn(self, value: $int) -> Result 237 | where 238 | E: serde::de::Error, 239 | { 240 | let repr: u8 = value.try_into().map_err(|_| { 241 | E::invalid_value(serde::de::Unexpected::Unsigned(value.into()), &format!( 242 | "Descriminant of value {} too big to be an identifier", 243 | value 244 | ).as_str()) 245 | })?; 246 | self.visit_u8(repr) 247 | } 248 | )* 249 | }; 250 | } 251 | 252 | pub(crate) fn set_if_none<'de, E, K, T, M>( 253 | key: K, 254 | val: &mut Option, 255 | map: &mut M, 256 | ) -> Result<(), E> 257 | where 258 | E: serde::de::Error, 259 | K: Into<&'static str>, 260 | T: serde::Deserialize<'de>, 261 | M: serde::de::MapAccess<'de, Error = E>, 262 | { 263 | check_is_already_set(key, val, map)?; 264 | *val = Some(map.next_value()?); 265 | Ok(()) 266 | } 267 | 268 | pub(crate) fn check_is_already_set<'de, E, K, T, M>( 269 | key: K, 270 | val: &Option, 271 | _map: &M, 272 | ) -> Result<(), E> 273 | where 274 | E: serde::de::Error, 275 | K: Into<&'static str>, 276 | M: serde::de::MapAccess<'de, Error = E>, 277 | { 278 | if val.is_some() { 279 | return Err(E::duplicate_field(key.into())); 280 | } 281 | Ok(()) 282 | } 283 | -------------------------------------------------------------------------------- /passkey-types/src/webauthn.rs: -------------------------------------------------------------------------------- 1 | //! Implementation of the types defined in [WebAuthn Level 3] 2 | //! 3 | //! [WebAuthn Level 3]: https://w3c.github.io/webauthn 4 | 5 | use serde::{Deserialize, Serialize}; 6 | use typeshare::typeshare; 7 | 8 | use crate::{utils::serde::ignore_unknown, Bytes}; 9 | 10 | mod assertion; 11 | mod attestation; 12 | mod common; 13 | mod extensions; 14 | 15 | // re-export types 16 | pub use self::{assertion::*, attestation::*, common::*, extensions::*}; 17 | 18 | mod sealed { 19 | pub trait Sealed {} 20 | 21 | impl Sealed for super::AuthenticatorAssertionResponse {} 22 | impl Sealed for super::AuthenticatorAttestationResponse {} 23 | } 24 | 25 | /// Marker trait for response types 26 | pub trait AuthenticatorResponse: sealed::Sealed {} 27 | 28 | impl AuthenticatorResponse for AuthenticatorAssertionResponse {} 29 | impl AuthenticatorResponse for AuthenticatorAttestationResponse {} 30 | 31 | /// This is the response from a successful creation or assertion of a credential. 32 | /// 33 | /// It is recommended to use the type aliases depending on which response you are expecting: 34 | /// * Credential Creation: [CreatedPublicKeyCredential] 35 | /// * Credential assertion: [AuthenticatedPublicKeyCredential] 36 | /// 37 | /// 38 | #[derive(Debug, Deserialize, Serialize)] 39 | #[serde(rename_all = "camelCase")] 40 | #[typeshare(swift = "Equatable", swiftGenericConstraints = "R: Equatable")] 41 | pub struct PublicKeyCredential { 42 | /// The id contains the credential ID, chosen by the authenticator. This is usually the base64url 43 | /// encoded data of [Self::raw_id] 44 | /// 45 | /// The credential ID is used to look up credentials for use and is therefore expected to be 46 | /// globally unique with high probability across all credentials of the same type across all 47 | /// authenticators. 48 | /// 49 | /// > NOTE: This API does not constrain the format or length of this identifier, except that it 50 | /// MUST be sufficient for the authenticator to uniquely select a key. 51 | pub id: String, 52 | 53 | /// The raw byte containing the credential ID, see [Self::id] for more information. 54 | pub raw_id: Bytes, 55 | 56 | /// Always [PublicKeyCredentialType] 57 | #[serde(rename = "type")] 58 | pub ty: PublicKeyCredentialType, 59 | 60 | /// This contains the authenticator's response to the client's request to either: 61 | /// * create a public key in which case it is of type [AuthenticatorAttestationResponse] or 62 | /// * generate an authentication assertion in which case it is of type [AuthenticatorAssertionResponse] 63 | pub response: R, 64 | 65 | /// This reports the modality of the communication between the client and authenticator. 66 | #[serde( 67 | default, 68 | skip_serializing_if = "Option::is_none", 69 | deserialize_with = "ignore_unknown" 70 | )] 71 | pub authenticator_attachment: Option, 72 | 73 | /// This object is a map containing extension identifier → client extension output entries 74 | /// produced by the extension’s client extension processing. 75 | #[serde(default)] 76 | pub client_extension_results: AuthenticationExtensionsClientOutputs, 77 | } 78 | -------------------------------------------------------------------------------- /passkey-types/src/webauthn/extensions/credential_properties.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use typeshare::typeshare; 3 | 4 | #[cfg(doc)] 5 | use crate::webauthn::PublicKeyCredential; 6 | 7 | /// This client registration extension facilitates reporting certain credential properties known by 8 | /// the client to the requesting WebAuthn [Relying Party] upon creation of a [`PublicKeyCredential`] 9 | /// source as a result of a registration ceremony. 10 | /// 11 | /// 12 | /// 13 | /// [Relying Party]: https://w3c.github.io/webauthn/#relying-party 14 | #[derive(Debug, Deserialize, Serialize)] 15 | #[serde(rename_all = "camelCase")] 16 | #[typeshare(swift = "Equatable")] 17 | pub struct CredentialPropertiesOutput { 18 | /// This OPTIONAL property, known abstractly as the resident key credential property 19 | /// (i.e., client-side [discoverable credential] property), is a Boolean value indicating whether 20 | /// the [`PublicKeyCredential`] returned as a result of a registration ceremony is a client-side 21 | /// [discoverable credential]. 22 | /// * If `rk` is true, the credential is a [discoverable credential]. 23 | /// * If `rk` is false, the credential is a [server-side credential]. 24 | /// * If `rk` is not present, it is not known whether the credential is a [discoverable credential] 25 | /// or a [server-side credential]. 26 | /// 27 | /// [discoverable credential]: https://w3c.github.io/webauthn/#discoverable-credential 28 | /// [server-side credential]: https://w3c.github.io/webauthn/#server-side-public-key-credential-source 29 | #[serde(rename = "rk", default, skip_serializing_if = "Option::is_none")] 30 | pub discoverable: Option, 31 | } 32 | -------------------------------------------------------------------------------- /passkey-types/src/webauthn/extensions/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use typeshare::typeshare; 3 | 4 | mod credential_properties; 5 | mod pseudo_random_function; 6 | 7 | pub use credential_properties::*; 8 | pub use pseudo_random_function::*; 9 | 10 | #[cfg(doc)] 11 | use crate::webauthn::PublicKeyCredential; 12 | 13 | /// This is a dictionary containing the client extension input values for zero or more 14 | /// [WebAuthn Extensions]. There are currently none supported. 15 | /// 16 | /// 17 | /// 18 | /// [WebAuthn Extensions]: https://w3c.github.io/webauthn/#webauthn-extensions 19 | #[derive(Debug, Default, Deserialize, Serialize)] 20 | #[serde(rename_all = "camelCase")] 21 | #[typeshare] 22 | pub struct AuthenticationExtensionsClientInputs { 23 | /// Boolean to indicate that this extension is requested by the relying party. 24 | /// 25 | /// See [`CredentialPropertiesOutput`] for more information. 26 | #[serde(default, skip_serializing_if = "Option::is_none")] 27 | pub cred_props: Option, 28 | 29 | /// Inputs for the pseudo-random function extensions. 30 | /// 31 | /// See [`AuthenticationExtensionsPrfInputs`] for more information. 32 | #[serde(default, skip_serializing_if = "Option::is_none")] 33 | pub prf: Option, 34 | 35 | /// Inputs for the pseudo-random function extension where the inputs are already hashed 36 | /// by another client following the `sha256("WebAuthn PRF" || salt)` format. 37 | /// 38 | /// This is not an official extension, rather a field that occurs in some cases on Android 39 | /// as well as the field that MUST be used when mapping from Apple's Authentication Services 40 | /// [`ASAuthorizationPublicKeyCredentialPRFAssertionInput`]. 41 | /// 42 | /// This field SHOULD NOT be present alongside the [`Self::prf`] field as that field will take precedence. 43 | /// 44 | /// [`ASAuthorizationPublicKeyCredentialPRFAssertionInput`]: https://developer.apple.com/documentation/authenticationservices/asauthorizationpublickeycredentialprfassertioninput-swift.struct 45 | #[serde(default, skip_serializing_if = "Option::is_none")] 46 | pub prf_already_hashed: Option, 47 | } 48 | 49 | impl AuthenticationExtensionsClientInputs { 50 | /// Validates that there is at least one extension field that is `Some` 51 | /// and that they are in turn not empty. If all fields are `None` 52 | /// then this returns `None` as well. 53 | pub fn zip_contents(self) -> Option { 54 | let Self { 55 | cred_props, 56 | prf, 57 | prf_already_hashed, 58 | } = &self; 59 | let has_cred_props = cred_props.is_some(); 60 | 61 | let has_prf = prf.is_some(); 62 | let has_prf_already_hashed = prf_already_hashed.is_some(); 63 | 64 | (has_cred_props || has_prf || has_prf_already_hashed).then_some(self) 65 | } 66 | } 67 | 68 | /// This is a dictionary containing the client extension output values for zero or more 69 | /// [WebAuthn Extensions]. 70 | /// 71 | /// 72 | /// 73 | /// [WebAuthn Extensions]: https://w3c.github.io/webauthn/#webauthn-extensions 74 | #[derive(Debug, Default, Deserialize, Serialize)] 75 | #[serde(rename_all = "camelCase")] 76 | #[typeshare(swift = "Equatable")] 77 | pub struct AuthenticationExtensionsClientOutputs { 78 | /// Contains properties of the given [`PublicKeyCredential`] when it is included. 79 | /// 80 | /// See [`CredentialPropertiesOutput`] for more information 81 | #[serde(default, skip_serializing_if = "Option::is_none")] 82 | pub cred_props: Option, 83 | 84 | /// Contains the results of evaluating the PRF. 85 | /// 86 | /// See [`AuthenticationExtensionsPrfOutputs`] for more information. 87 | #[serde(default, skip_serializing_if = "Option::is_none")] 88 | pub prf: Option, 89 | } 90 | -------------------------------------------------------------------------------- /passkey-types/src/webauthn/extensions/pseudo_random_function.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use typeshare::typeshare; 3 | 4 | use std::collections::HashMap; 5 | 6 | use crate::Bytes; 7 | 8 | /// Pseudo-random function values. 9 | /// 10 | /// This is used for both PRF inputs and outputs. 11 | /// 12 | /// When used as inputs to the PRF evaluation, these values will be included 13 | /// in the calculation of the salts that are sent as parameters in the 14 | /// `hmac-secret` extension process to the authenticator. 15 | /// 16 | /// When used as outputs, the fields will contain the results of evaluating 17 | /// the PRF. 18 | #[derive(Debug, Deserialize, Serialize, Clone)] 19 | #[serde(rename_all = "camelCase")] 20 | #[typeshare(swift = "Equatable")] 21 | pub struct AuthenticationExtensionsPrfValues { 22 | /// The first PRF value. 23 | pub first: Bytes, 24 | 25 | /// The second PRF value. 26 | #[serde(default, skip_serializing_if = "Option::is_none")] 27 | pub second: Option, 28 | } 29 | 30 | /// Inputs for the pseudo-random function extension. 31 | /// 32 | /// This client registration extension and authentication extension allows a 33 | /// Relying Party to evaluate outputs from a pseudo-random function (PRF) 34 | /// associated with a credential. The PRFs provided by this extension map from 35 | /// BufferSources of any length to 32-byte BufferSources. 36 | /// 37 | /// 38 | #[derive(Debug, Default, Deserialize, Serialize, Clone)] 39 | #[serde(rename_all = "camelCase")] 40 | #[typeshare] 41 | pub struct AuthenticationExtensionsPrfInputs { 42 | /// One or two inputs on which to evaluate PRF. Not all authenticators 43 | /// support evaluating the PRFs during credential creation so outputs may, 44 | /// or may not, be provided. If not, then an assertion is needed in order 45 | /// to obtain the outputs. 46 | #[serde(default, skip_serializing_if = "Option::is_none")] 47 | pub eval: Option, 48 | 49 | /// A record mapping base64url encoded [credential IDs] to PRF inputs to 50 | /// evaluate for that credential. Only applicable during assertions when 51 | /// [allowCredentials] is not empty. 52 | /// 53 | /// [credential IDs]: https://w3c.github.io/webauthn/#credential-id 54 | /// [allowCredentials]: https://w3c.github.io/webauthn/#dom-publickeycredentialrequestoptions-allowcredentials 55 | #[serde(default, skip_serializing_if = "Option::is_none")] 56 | pub eval_by_credential: Option>, 57 | } 58 | 59 | /// Outputs from the pseudo-random function extension. 60 | /// 61 | /// See [`AuthenticationExtensionsPrfInputs`] for details. 62 | #[derive(Debug, Deserialize, Serialize, Clone)] 63 | #[serde(rename_all = "camelCase")] 64 | #[typeshare(swift = "Equatable")] 65 | pub struct AuthenticationExtensionsPrfOutputs { 66 | /// True if, and only if, the one or two PRFs are available for use with 67 | /// the created credential. This is only reported during registration and 68 | /// is not present in the case of authentication. 69 | #[serde(default, skip_serializing_if = "Option::is_none")] 70 | pub enabled: Option, 71 | 72 | /// The results of evaluating the PRF for the inputs given in `eval` or 73 | /// `evalByCredential` in [`AuthenticationExtensionsPrfInputs`]. Outputs 74 | /// may not be available during registration; see comments in `eval`. 75 | #[serde(default, skip_serializing_if = "Option::is_none")] 76 | pub results: Option, 77 | } 78 | -------------------------------------------------------------------------------- /passkey/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "passkey" 3 | version = "0.4.0" 4 | description = "A one stop library to implement a passkey client and authenticator" 5 | include = ["src/", "../LICENSE-APACHE", "../LICENSE-MIT"] 6 | readme = "../README.md" 7 | authors.workspace = true 8 | repository.workspace = true 9 | edition.workspace = true 10 | license.workspace = true 11 | keywords.workspace = true 12 | categories.workspace = true 13 | 14 | [lints] 15 | workspace = true 16 | 17 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 18 | 19 | [package.metadata.cargo-udeps.ignore] 20 | development = [ 21 | "tokio-test", 22 | ] # Only used for async doctests. Cargo udeps can't check.: 23 | 24 | [dependencies] 25 | passkey-authenticator = { path = "../passkey-authenticator", version = "0.4" } 26 | passkey-types = { path = "../passkey-types", version = "0.4" } 27 | passkey-client = { path = "../passkey-client", version = "0.4" } 28 | passkey-transports = { path = "../passkey-transports", version = "0.1" } 29 | 30 | [dev-dependencies] 31 | coset = "0.3" 32 | url = "2" 33 | tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] } 34 | tokio-test = "0.4" 35 | async-trait = "0.1" 36 | passkey-client = { path = "../passkey-client", version = "0.4", features = [ 37 | "tokio", 38 | "testable", 39 | ] } 40 | passkey-authenticator = { path = "../passkey-authenticator", version = "0.4", features = [ 41 | "tokio", 42 | "testable", 43 | ] } 44 | -------------------------------------------------------------------------------- /passkey/examples/usage.rs: -------------------------------------------------------------------------------- 1 | //! Sample App for Passkeys 2 | use passkey::{ 3 | authenticator::{Authenticator, UserCheck, UserValidationMethod}, 4 | client::{Client, WebauthnError}, 5 | types::{crypto::sha256, ctap2::*, rand::random_vec, webauthn::*, Bytes, Passkey}, 6 | }; 7 | 8 | use coset::iana; 9 | use passkey_client::DefaultClientData; 10 | use url::Url; 11 | 12 | // MyUserValidationMethod is a stub impl of the UserValidationMethod trait, used later. 13 | struct MyUserValidationMethod {} 14 | #[async_trait::async_trait] 15 | impl UserValidationMethod for MyUserValidationMethod { 16 | type PasskeyItem = Passkey; 17 | 18 | async fn check_user<'a>( 19 | &self, 20 | _credential: Option<&'a Passkey>, 21 | presence: bool, 22 | verification: bool, 23 | ) -> Result { 24 | Ok(UserCheck { 25 | presence, 26 | verification, 27 | }) 28 | } 29 | 30 | fn is_verification_enabled(&self) -> Option { 31 | Some(true) 32 | } 33 | 34 | fn is_presence_enabled(&self) -> bool { 35 | true 36 | } 37 | } 38 | 39 | // Example of how to set up, register and authenticate with a `Client`. 40 | async fn client_setup( 41 | challenge_bytes_from_rp: Bytes, 42 | parameters_from_rp: PublicKeyCredentialParameters, 43 | origin: &Url, 44 | user_entity: PublicKeyCredentialUserEntity, 45 | ) -> Result<(CreatedPublicKeyCredential, AuthenticatedPublicKeyCredential), WebauthnError> { 46 | // First create an Authenticator for the Client to use. 47 | let my_aaguid = Aaguid::new_empty(); 48 | let user_validation_method = MyUserValidationMethod {}; 49 | // Create the CredentialStore for the Authenticator. 50 | // Option is the simplest possible implementation of CredentialStore 51 | let store: Option = None; 52 | let my_authenticator = Authenticator::new(my_aaguid, store, user_validation_method); 53 | 54 | // Create the Client 55 | // If you are creating credentials, you need to declare the Client as mut 56 | let mut my_client = Client::new(my_authenticator); 57 | 58 | // The following values, provided as parameters to this function would usually be 59 | // retrieved from a Relying Party according to the context of the application. 60 | let request = CredentialCreationOptions { 61 | public_key: PublicKeyCredentialCreationOptions { 62 | rp: PublicKeyCredentialRpEntity { 63 | id: None, // Leaving the ID as None means use the effective domain 64 | name: origin.domain().unwrap().into(), 65 | }, 66 | user: user_entity, 67 | challenge: challenge_bytes_from_rp, 68 | pub_key_cred_params: vec![parameters_from_rp], 69 | timeout: None, 70 | exclude_credentials: None, 71 | authenticator_selection: None, 72 | hints: None, 73 | attestation: AttestationConveyancePreference::None, 74 | attestation_formats: None, 75 | extensions: None, 76 | }, 77 | }; 78 | 79 | // Now create the credential. 80 | let my_webauthn_credential = my_client 81 | .register(origin, request, DefaultClientData) 82 | .await?; 83 | 84 | // Let's try and authenticate. 85 | // Create a challenge that would usually come from the RP. 86 | let challenge_bytes_from_rp: Bytes = random_vec(32).into(); 87 | // Now try and authenticate 88 | let credential_request = CredentialRequestOptions { 89 | public_key: PublicKeyCredentialRequestOptions { 90 | challenge: challenge_bytes_from_rp, 91 | timeout: None, 92 | rp_id: Some(String::from(origin.domain().unwrap())), 93 | allow_credentials: None, 94 | user_verification: UserVerificationRequirement::default(), 95 | hints: None, 96 | attestation: AttestationConveyancePreference::None, 97 | attestation_formats: None, 98 | extensions: None, 99 | }, 100 | }; 101 | 102 | let authenticated_cred = my_client 103 | .authenticate(origin, credential_request, DefaultClientData) 104 | .await?; 105 | 106 | Ok((my_webauthn_credential, authenticated_cred)) 107 | } 108 | 109 | async fn authenticator_setup( 110 | user_entity: PublicKeyCredentialUserEntity, 111 | client_data_hash: Bytes, 112 | algorithms_from_rp: PublicKeyCredentialParameters, 113 | rp_id: String, 114 | ) -> Result { 115 | let store: Option = None; 116 | let user_validation_method = MyUserValidationMethod {}; 117 | let my_aaguid = Aaguid::new_empty(); 118 | 119 | let mut my_authenticator = Authenticator::new(my_aaguid, store, user_validation_method); 120 | 121 | let reg_request = make_credential::Request { 122 | client_data_hash: client_data_hash.clone(), 123 | rp: make_credential::PublicKeyCredentialRpEntity { 124 | id: rp_id.clone(), 125 | name: None, 126 | }, 127 | user: user_entity, 128 | pub_key_cred_params: vec![algorithms_from_rp], 129 | exclude_list: None, 130 | extensions: None, 131 | options: make_credential::Options::default(), 132 | pin_auth: None, 133 | pin_protocol: None, 134 | }; 135 | 136 | let credential: make_credential::Response = 137 | my_authenticator.make_credential(reg_request).await?; 138 | 139 | ctap2_creation_success(credential); 140 | 141 | let auth_request = get_assertion::Request { 142 | rp_id, 143 | client_data_hash, 144 | allow_list: None, 145 | extensions: None, 146 | options: make_credential::Options::default(), 147 | pin_auth: None, 148 | pin_protocol: None, 149 | }; 150 | 151 | let response = my_authenticator.get_assertion(auth_request).await?; 152 | 153 | Ok(response) 154 | } 155 | 156 | fn ctap2_creation_success(credential: make_credential::Response) { 157 | println!( 158 | "CTAP2 credential creation succeeded:\n\n{:?}\n\n", 159 | credential 160 | ); 161 | } 162 | 163 | fn ctap2_auth_success(credential: get_assertion::Response) { 164 | println!( 165 | "CTAP2 credential authentication succeeded:\n\n{:?}\n\n", 166 | credential 167 | ); 168 | } 169 | 170 | fn ctap2_credential_not_found() { 171 | println!("CTAP2 error: Credential not found."); 172 | } 173 | 174 | fn ctap2_other_error(code: StatusCode) { 175 | println!("CTAP2 error: Other Status Code: {:?}", code); 176 | } 177 | 178 | #[tokio::main] 179 | async fn main() -> Result<(), WebauthnError> { 180 | let rp_url = Url::parse("https://future.1password.com").expect("Should Parse"); 181 | let user_entity = PublicKeyCredentialUserEntity { 182 | id: random_vec(32).into(), 183 | display_name: "Johnny Passkey".into(), 184 | name: "jpasskey@example.org".into(), 185 | }; 186 | 187 | // Set up a client, create and authenticate a credential, then report results. 188 | let (created_cred, authed_cred) = client_setup( 189 | random_vec(32).into(), // challenge_bytes_from_rp 190 | PublicKeyCredentialParameters { 191 | ty: PublicKeyCredentialType::PublicKey, 192 | alg: iana::Algorithm::ES256, 193 | }, 194 | &rp_url, // origin 195 | user_entity.clone(), 196 | ) 197 | .await?; 198 | 199 | println!("Webauthn credential created:\n\n{:?}\n\n", created_cred); 200 | println!("Webauthn credential auth'ed:\n\n{:?}\n\n", authed_cred); 201 | 202 | // Generate the client_data_hash from the created_cred response 203 | let client_data_hash = sha256(&created_cred.response.client_data_json).to_vec(); 204 | 205 | // Authenticator Version 206 | let authenticator_result = authenticator_setup( 207 | user_entity, 208 | client_data_hash.into(), 209 | PublicKeyCredentialParameters { 210 | ty: PublicKeyCredentialType::PublicKey, 211 | alg: iana::Algorithm::ES256, 212 | }, 213 | rp_url 214 | .domain() 215 | .expect("Our example should unwrap.") 216 | .to_string(), // tld_from_rp 217 | ) 218 | .await; 219 | 220 | match authenticator_result { 221 | Ok(authresponse) => { 222 | ctap2_auth_success(authresponse); 223 | } 224 | Err(StatusCode::Ctap2(Ctap2Code::Known(Ctap2Error::NoCredentials))) => { 225 | ctap2_credential_not_found() 226 | } 227 | Err(status_code) => ctap2_other_error(status_code), 228 | }; 229 | 230 | Ok(()) 231 | } 232 | -------------------------------------------------------------------------------- /public-suffix/.gitignore: -------------------------------------------------------------------------------- 1 | pkg/ 2 | src/golang.org/ 3 | -------------------------------------------------------------------------------- /public-suffix/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "public-suffix" 3 | description = "Crate for efficient determination of eTLD+1 based on the Mozilla Public Suffix List." 4 | version = "0.1.3" 5 | include = ["src/", "../LICENSE-APACHE", "../LICENSE-MIT"] 6 | readme = "README.md" 7 | keywords = ["tld", "etld", "domain", "publicsuffix"] 8 | authors.workspace = true 9 | repository.workspace = true 10 | edition.workspace = true 11 | license.workspace = true 12 | 13 | [lints] 14 | workspace = true 15 | 16 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 17 | [features] 18 | default = ["default_provider"] 19 | default_provider = [] 20 | 21 | [dependencies] 22 | -------------------------------------------------------------------------------- /public-suffix/README.md: -------------------------------------------------------------------------------- 1 | # public-suffix by 1Password 2 | 3 | [![github]](https://github.com/1Password/passkey-rs/tree/main/public-suffix) 4 | [![version]](https://crates.io/crates/public-suffix/) 5 | [![documentation]](https://docs.rs/public-suffix/) 6 | 7 | The `public-suffix` crate provides a compact and efficient way to determine the effective TLD-plus-one of any given domain. 8 | 9 | This crate is driven by a data file, held in `public-suffix-list.dat` and contains code to generate Rust structures to represent this information in code. 10 | 11 | ## Using this Crate 12 | 13 | You can use this crate directly, using the included `DEFAULT_PROVIDER` list as follows: 14 | 15 | ``` 16 | let domain = "sainsburys.co.uk"; 17 | let etld = DEFAULT_PROVIDER.effective_tld_plus_one(domain); 18 | ``` 19 | 20 | ## Generating a Custom Public Suffix List 21 | 22 | It may be that users of this crate wish to compute eTLD+1 differently for certain domains according to the needs of their particular application. 23 | 24 | To do this, provide your own version of `public_suffix_list.dat` and run the included generator script (`gen.sh`) with the contents of your custom TLD file. 25 | 26 | This will regenerate the Rust representations of that data for inclusion in your own crate. The `main.go` program called by `gen.sh` supports various arguments to control its output. The main arguments you may wish to use are: 27 | 28 | - `--output-path` - directory in which to place the generated files. 29 | - `--base-name` - the base name of the generated files. The generator will create `${base-name}.rs` and `${base-name}_test.rs` in the directory specified by `output-path`. 30 | - `--struct` - the name of the Rust struct that will be generated to represent your custom TLD data. 31 | - `--crate` - a boolean controlling whether the struct will be created as `public_suffix::StructName` (if true) or `crate::StructName` (if false). When you are creating your own structs, always set this to false. 32 | 33 | ## Using Your Custom Public Suffix List 34 | 35 | Next, in your `Cargo.toml`, disable the `default-provider` feature in this crate: `default-features = false`. Doing so will remove the built-in implementation of the public suffix list structure and instead you can use your own: 36 | 37 | ``` 38 | type PublicSuffixList = ListProvider; 39 | 40 | pub const MY_CUSTOM_TLD_LIST: PublicSuffixList = PublicSuffixList::new(); 41 | ``` 42 | 43 | ...then you can call the same functions on `MY_CUSTOM_TLD_LIST`: 44 | 45 | ``` 46 | let domain = "sainsburys.co.uk"; 47 | let etld = MY_CUSTOM_TLD_LIST.effective_tld_plus_one(domain); 48 | ``` 49 | 50 | ## Contributing and feedback 51 | 52 | `public-suffix` is an [open source project](https://github.com/1Password/public-suffix). 53 | 54 | 🐛 If you find an issue you'd like to report, or otherwise have feedback, please [file a new Issue](https://github.com/1Password/public-suffix/issues/new). 55 | 56 | 🧑‍💻 If you'd like to contribute to the code please start by filing or commenting on an [Issue](https://github.com/1Password/public-suffix/issues) so we can track the work. 57 | 58 | ## Credits 59 | 60 | Made with ❤️ and ☕ by the [1Password](https://1password.com/) team. 61 | 62 | ### Get a free 1Password account for your open source project 63 | 64 | Does your team need a secure way to manage passwords and other credentials for your open source project? Head on over to our [other repository](https://github.com/1Password/1password-teams-open-source) to get a 1Password Teams account on us: 65 | 66 | ✨[1Password for Open Source Projects](https://github.com/1Password/1password-teams-open-source) 67 | 68 | #### License 69 | 70 | 71 | Licensed under either of Apache License, Version 72 | 2.0 or MIT license at your option. 73 | 74 | 75 |
76 | 77 | 78 | Unless you explicitly state otherwise, any contribution intentionally submitted 79 | for inclusion in this crate by you, as defined in the Apache-2.0 license, shall 80 | be dual licensed as above, without any additional terms or conditions. 81 | 82 | 83 | 84 | [github]: https://img.shields.io/badge/GitHub-1Password%2Fpasskey--rs%2Fpublic--suffix-informational?logo=github&style=flat 85 | [version]: https://img.shields.io/crates/v/public-suffix?logo=rust&style=flat 86 | [documentation]: https://img.shields.io/docsrs/public-suffix/latest?logo=docs.rs&style=flat -------------------------------------------------------------------------------- /public-suffix/gen.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | set -ex 3 | 4 | current="$(realpath "$(dirname "$0")")" 5 | 6 | export GOPATH=$PWD 7 | (cd "${current}/generator" && cat "${current}/public_suffix_list.dat" | \ 8 | go run main.go --output-path "${current}/src/" --base-name tld_list 9 | ) 10 | cargo fmt 11 | -------------------------------------------------------------------------------- /public-suffix/generator/AUTHORS: -------------------------------------------------------------------------------- 1 | # This source code refers to The Go Authors for copyright purposes. 2 | # The master list of authors is in the main Go distribution, 3 | # visible at http://tip.golang.org/AUTHORS. 4 | -------------------------------------------------------------------------------- /public-suffix/generator/CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # This source code was written by the Go contributors. 2 | # The master list of contributors is in the main Go distribution, 3 | # visible at http://tip.golang.org/CONTRIBUTORS. 4 | -------------------------------------------------------------------------------- /public-suffix/generator/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /public-suffix/generator/go.mod: -------------------------------------------------------------------------------- 1 | module go.1password.io/generator 2 | 3 | require golang.org/x/net v0.0.0-20220708220712-1185a9018129 4 | 5 | require golang.org/x/text v0.3.7 // indirect 6 | 7 | go 1.17 8 | -------------------------------------------------------------------------------- /public-suffix/generator/go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/net v0.0.0-20220708220712-1185a9018129 h1:vucSRfWwTsoXro7P+3Cjlr6flUMtzCwzlvkxEQtHHB0= 2 | golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 3 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 4 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 5 | -------------------------------------------------------------------------------- /public-suffix/src/types.rs: -------------------------------------------------------------------------------- 1 | /// Implementation to allow the use of custom tables. 2 | /// 3 | /// DO NOT IMPLEMENT THIS MANUALLY. This should only be implemented by the code generator where you 4 | /// feed it the list you want. This list should be formatted in the same way as 5 | /// where a single line is either: 6 | /// * A domain public suffix, 7 | /// * A comment which starts with `//`, 8 | /// * or empty. 9 | #[allow(missing_docs)] 10 | pub trait Table { 11 | const NODES_BITS_CHILDREN: u32; 12 | const NODES_BITS_ICANN: u32; 13 | const NODES_BITS_TEXT_OFFSET: u32; 14 | const NODES_BITS_TEXT_LENGTH: u32; 15 | 16 | const CHILDREN_BITS_WILDCARD: u32; 17 | const CHILDREN_BITS_NODE_TYPE: u32; 18 | const CHILDREN_BITS_HI: u32; 19 | const CHILDREN_BITS_LO: u32; 20 | 21 | const NODE_TYPE_NORMAL: u32; 22 | const NODE_TYPE_EXCEPTION: u32; 23 | 24 | /// numTLD is the number of top level domains. 25 | const NUM_TLD: u32; 26 | 27 | /// The resulting string is the combined text of all labels concatenated together. 28 | const TEXT: &'static str; 29 | 30 | /// NODES is the list of nodes. Each node is represented as a uint32, which 31 | /// encodes the node's children, wildcard bit and node type (as an index into 32 | /// the children array), ICANN bit and text. 33 | /// 34 | /// If the table was generated with the -comments flag, there is a //-comment 35 | /// after each node's data. In it is the nodes-array indexes of the children, 36 | /// formatted as (n0x1234-n0x1256), with * denoting the wildcard bit. The 37 | /// nodeType is printed as + for normal, ! for exception, and o for parent-only 38 | /// nodes that have children but don't match a domain label in their own right. 39 | /// An I denotes an ICANN domain. 40 | const NODES: &'static [u32]; 41 | 42 | /// children is the list of nodes' children, the parent's wildcard bit and the 43 | /// parent's node type. If a node has no children then their children index 44 | /// will be in the range [0, 6), depending on the wildcard bit and node type. 45 | const CHILDREN: &'static [u32]; 46 | } 47 | -------------------------------------------------------------------------------- /public-suffix/tests/tests.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //! These tests are transliterated from list_test.go. Some of the tests from 6 | //! list_test.go were made unit tests inside lib.rs. 7 | 8 | use public_suffix::*; 9 | 10 | static TEST_CASES: &[(&str, &str)] = &[ 11 | // Empty string. 12 | ("", ""), 13 | // The .ao rules are: 14 | // ao 15 | // ed.ao 16 | // gv.ao 17 | // og.ao 18 | // co.ao 19 | // pb.ao 20 | // it.ao 21 | ("ao", "ao"), 22 | ("www.ao", "ao"), 23 | ("pb.ao", "pb.ao"), 24 | ("www.pb.ao", "pb.ao"), 25 | ("www.xxx.yyy.zzz.pb.ao", "pb.ao"), 26 | // The .ar rules are: 27 | // ar 28 | // com.ar 29 | // edu.ar 30 | // gob.ar 31 | // gov.ar 32 | // int.ar 33 | // mil.ar 34 | // net.ar 35 | // org.ar 36 | // tur.ar 37 | ("ar", "ar"), 38 | ("www.ar", "ar"), 39 | ("nic.ar", "ar"), 40 | ("www.nic.ar", "ar"), 41 | ("com.ar", "com.ar"), 42 | ("www.com.ar", "com.ar"), 43 | ("logspot.com.ar", "com.ar"), 44 | ("zlogspot.com.ar", "com.ar"), 45 | ("zblogspot.com.ar", "com.ar"), 46 | // The .arpa rules are: 47 | // arpa 48 | // e164.arpa 49 | // in-addr.arpa 50 | // ip6.arpa 51 | // iris.arpa 52 | // uri.arpa 53 | // urn.arpa 54 | ("arpa", "arpa"), 55 | ("www.arpa", "arpa"), 56 | ("urn.arpa", "urn.arpa"), 57 | ("www.urn.arpa", "urn.arpa"), 58 | ("www.xxx.yyy.zzz.urn.arpa", "urn.arpa"), 59 | // The relevant {kobe,kyoto}.jp rules are: 60 | // jp 61 | // *.kobe.jp 62 | // !city.kobe.jp 63 | // kyoto.jp 64 | // ide.kyoto.jp 65 | ("jp", "jp"), 66 | ("kobe.jp", "jp"), 67 | ("c.kobe.jp", "c.kobe.jp"), 68 | ("b.c.kobe.jp", "c.kobe.jp"), 69 | ("a.b.c.kobe.jp", "c.kobe.jp"), 70 | ("city.kobe.jp", "kobe.jp"), 71 | ("www.city.kobe.jp", "kobe.jp"), 72 | ("kyoto.jp", "kyoto.jp"), 73 | ("test.kyoto.jp", "kyoto.jp"), 74 | ("ide.kyoto.jp", "ide.kyoto.jp"), 75 | ("b.ide.kyoto.jp", "ide.kyoto.jp"), 76 | ("a.b.ide.kyoto.jp", "ide.kyoto.jp"), 77 | // The .tw rules are: 78 | // tw 79 | // edu.tw 80 | // gov.tw 81 | // mil.tw 82 | // com.tw 83 | // net.tw 84 | // org.tw 85 | // idv.tw 86 | // game.tw 87 | // ebiz.tw 88 | // club.tw 89 | ("tw", "tw"), 90 | ("aaa.tw", "tw"), 91 | ("www.aaa.tw", "tw"), 92 | ("xn--czrw28b.aaa.tw", "tw"), 93 | ("edu.tw", "edu.tw"), 94 | ("www.edu.tw", "edu.tw"), 95 | ("xn--czrw28b.edu.tw", "edu.tw"), 96 | ("xn--kpry57d.tw", "tw"), 97 | // The .uk rules are: 98 | // uk 99 | // ac.uk 100 | // co.uk 101 | // gov.uk 102 | // ltd.uk 103 | // me.uk 104 | // net.uk 105 | // nhs.uk 106 | // org.uk 107 | // plc.uk 108 | // police.uk 109 | // *.sch.uk 110 | ("uk", "uk"), 111 | ("aaa.uk", "uk"), 112 | ("www.aaa.uk", "uk"), 113 | ("mod.uk", "uk"), 114 | ("www.mod.uk", "uk"), 115 | ("sch.uk", "uk"), 116 | ("mod.sch.uk", "mod.sch.uk"), 117 | ("www.sch.uk", "www.sch.uk"), 118 | ("co.uk", "co.uk"), 119 | ("www.co.uk", "co.uk"), 120 | ("blogspot.nic.uk", "uk"), 121 | ("blogspot.sch.uk", "blogspot.sch.uk"), 122 | // The .рф rules are 123 | // рф (xn--p1ai) 124 | ("xn--p1ai", "xn--p1ai"), 125 | ("aaa.xn--p1ai", "xn--p1ai"), 126 | ("www.xxx.yyy.xn--p1ai", "xn--p1ai"), 127 | // The .bd rules are: 128 | // *.bd 129 | ("bd", "bd"), // The catch-all "*" rule is not in the ICANN DOMAIN section. See footnote (†). 130 | ("www.bd", "www.bd"), 131 | ("xxx.www.bd", "www.bd"), 132 | ("zzz.bd", "zzz.bd"), 133 | ("www.zzz.bd", "zzz.bd"), 134 | ("www.xxx.yyy.zzz.bd", "zzz.bd"), 135 | // The .ck rules are: 136 | // *.ck 137 | // !www.ck 138 | ("ck", "ck"), // The catch-all "*" rule is not in the ICANN DOMAIN section. See footnote (†). 139 | ("www.ck", "ck"), 140 | ("xxx.www.ck", "ck"), 141 | ("zzz.ck", "zzz.ck"), 142 | ("www.zzz.ck", "zzz.ck"), 143 | ("www.xxx.yyy.zzz.ck", "zzz.ck"), 144 | // The .myjino.ru rules (in the PRIVATE DOMAIN section) are: 145 | // myjino.ru 146 | // *.hosting.myjino.ru 147 | // *.landing.myjino.ru 148 | // *.spectrum.myjino.ru 149 | // *.vps.myjino.ru 150 | ("myjino.ru", "myjino.ru"), 151 | ("aaa.myjino.ru", "myjino.ru"), 152 | ("bbb.ccc.myjino.ru", "myjino.ru"), 153 | ("hosting.ddd.myjino.ru", "myjino.ru"), 154 | ("landing.myjino.ru", "myjino.ru"), 155 | ("www.landing.myjino.ru", "www.landing.myjino.ru"), 156 | ("spectrum.vps.myjino.ru", "spectrum.vps.myjino.ru"), 157 | // The .uberspace.de rules (in the PRIVATE DOMAIN section) are: 158 | // *.uberspace.de 159 | ("uberspace.de", "de"), // "de" is in the ICANN DOMAIN section. See footnote (†). 160 | ("aaa.uberspace.de", "aaa.uberspace.de"), 161 | ("bbb.ccc.uberspace.de", "ccc.uberspace.de"), 162 | // There are no .nosuchtld rules. 163 | ("nosuchtld", "nosuchtld"), 164 | ("foo.nosuchtld", "nosuchtld"), 165 | ("bar.foo.nosuchtld", "nosuchtld"), 166 | // (†) There is some disagreement on how wildcards behave: what should the 167 | // public suffix of "platform.sh" be when both "*.platform.sh" and "sh" is 168 | // in the PSL, but "platform.sh" is not? Two possible answers are 169 | // "platform.sh" and "sh", there are valid arguments for either behavior, 170 | // and different browsers have implemented different behaviors. 171 | // 172 | // This implementation, Go's golang.org/x/net/publicsuffix, returns "sh", 173 | // the same as a literal interpretation of the "Formal Algorithm" section 174 | // of https://publicsuffix.org/list/ 175 | // 176 | // Together, the TestPublicSuffix and TestSlowPublicSuffix tests check that 177 | // the Go implementation (func PublicSuffix in list.go) and the literal 178 | // interpretation (func slowPublicSuffix in list_test.go) produce the same 179 | // (golden) results on every test case in this publicSuffixTestCases slice, 180 | // including some "platform.sh" style cases. 181 | // 182 | // More discussion of "the platform.sh problem" is at: 183 | // - https://github.com/publicsuffix/list/issues/694 184 | // - https://bugzilla.mozilla.org/show_bug.cgi?id=1124625#c6 185 | // - https://wiki.mozilla.org/Public_Suffix_List/platform.sh_Problem 186 | ]; 187 | 188 | #[test] 189 | fn test_public_suffix() { 190 | for &(domain, want_ps) in TEST_CASES.iter() { 191 | assert_eq!( 192 | DEFAULT_PROVIDER.public_suffix(domain), 193 | want_ps, 194 | "{domain:?} -> {want_ps:?}" 195 | ); 196 | } 197 | } 198 | 199 | // TODO: PORT TestSlowPublicSuffix 200 | 201 | // from 202 | // https://github.com/publicsuffix/list/blob/master/tests/test_psl.txt 203 | static ETLD_PLUS_ONE_TEST_CASES: &[(&str, Result<&'static str, Error>)] = &[ 204 | // Empty input. 205 | ("", Err(Error::CannotDeriveETldPlus1)), 206 | // Unlisted TLD. 207 | ("example", Err(Error::CannotDeriveETldPlus1)), 208 | ("example.example", Ok("example.example")), 209 | ("b.example.example", Ok("example.example")), 210 | ("a.b.example.example", Ok("example.example")), 211 | // TLD with only 1 rule. 212 | ("biz", Err(Error::CannotDeriveETldPlus1)), 213 | ("domain.biz", Ok("domain.biz")), 214 | ("b.domain.biz", Ok("domain.biz")), 215 | ("a.b.domain.biz", Ok("domain.biz")), 216 | // TLD with some 2-level rules. 217 | ("com", Err(Error::CannotDeriveETldPlus1)), 218 | ("example.com", Ok("example.com")), 219 | ("b.example.com", Ok("example.com")), 220 | ("a.b.example.com", Ok("example.com")), 221 | ("uk.com", Err(Error::CannotDeriveETldPlus1)), 222 | ("example.uk.com", Ok("example.uk.com")), 223 | ("b.example.uk.com", Ok("example.uk.com")), 224 | ("a.b.example.uk.com", Ok("example.uk.com")), 225 | ("test.ac", Ok("test.ac")), 226 | // TLD with only 1 (wildcard) rule. 227 | ("mm", Err(Error::CannotDeriveETldPlus1)), 228 | ("c.mm", Err(Error::CannotDeriveETldPlus1)), 229 | ("b.c.mm", Ok("b.c.mm")), 230 | ("a.b.c.mm", Ok("b.c.mm")), 231 | // More complex TLD. 232 | ("jp", Err(Error::CannotDeriveETldPlus1)), 233 | ("test.jp", Ok("test.jp")), 234 | ("www.test.jp", Ok("test.jp")), 235 | ("ac.jp", Err(Error::CannotDeriveETldPlus1)), 236 | ("test.ac.jp", Ok("test.ac.jp")), 237 | ("www.test.ac.jp", Ok("test.ac.jp")), 238 | ("kyoto.jp", Err(Error::CannotDeriveETldPlus1)), 239 | ("test.kyoto.jp", Ok("test.kyoto.jp")), 240 | ("ide.kyoto.jp", Err(Error::CannotDeriveETldPlus1)), 241 | ("b.ide.kyoto.jp", Ok("b.ide.kyoto.jp")), 242 | ("a.b.ide.kyoto.jp", Ok("b.ide.kyoto.jp")), 243 | ("c.kobe.jp", Err(Error::CannotDeriveETldPlus1)), 244 | ("b.c.kobe.jp", Ok("b.c.kobe.jp")), 245 | ("a.b.c.kobe.jp", Ok("b.c.kobe.jp")), 246 | ("city.kobe.jp", Ok("city.kobe.jp")), 247 | ("www.city.kobe.jp", Ok("city.kobe.jp")), 248 | // TLD with a wildcard rule and exceptions. 249 | ("ck", Err(Error::CannotDeriveETldPlus1)), 250 | ("test.ck", Err(Error::CannotDeriveETldPlus1)), 251 | ("b.test.ck", Ok("b.test.ck")), 252 | ("a.b.test.ck", Ok("b.test.ck")), 253 | ("www.ck", Ok("www.ck")), 254 | ("www.www.ck", Ok("www.ck")), 255 | // US K12. 256 | ("us", Err(Error::CannotDeriveETldPlus1)), 257 | ("test.us", Ok("test.us")), 258 | ("www.test.us", Ok("test.us")), 259 | ("ak.us", Err(Error::CannotDeriveETldPlus1)), 260 | ("test.ak.us", Ok("test.ak.us")), 261 | ("www.test.ak.us", Ok("test.ak.us")), 262 | ("k12.ak.us", Err(Error::CannotDeriveETldPlus1)), 263 | ("test.k12.ak.us", Ok("test.k12.ak.us")), 264 | ("www.test.k12.ak.us", Ok("test.k12.ak.us")), 265 | // Punycoded IDN labels 266 | ("xn--85x722f.com.cn", Ok("xn--85x722f.com.cn")), 267 | ("xn--85x722f.xn--55qx5d.cn", Ok("xn--85x722f.xn--55qx5d.cn")), 268 | ( 269 | "www.xn--85x722f.xn--55qx5d.cn", 270 | Ok("xn--85x722f.xn--55qx5d.cn"), 271 | ), 272 | ("shishi.xn--55qx5d.cn", Ok("shishi.xn--55qx5d.cn")), 273 | ("xn--55qx5d.cn", Err(Error::CannotDeriveETldPlus1)), 274 | ("xn--85x722f.xn--fiqs8s", Ok("xn--85x722f.xn--fiqs8s")), 275 | ("www.xn--85x722f.xn--fiqs8s", Ok("xn--85x722f.xn--fiqs8s")), 276 | ("shishi.xn--fiqs8s", Ok("shishi.xn--fiqs8s")), 277 | ("xn--fiqs8s", Err(Error::CannotDeriveETldPlus1)), 278 | // Invalid input 279 | (".", Err(Error::EmptyLabel)), 280 | ("de.", Err(Error::EmptyLabel)), 281 | (".de", Err(Error::EmptyLabel)), 282 | (".com.au", Err(Error::EmptyLabel)), 283 | ("com.au.", Err(Error::EmptyLabel)), 284 | ("com..au", Err(Error::EmptyLabel)), 285 | ]; 286 | 287 | #[test] 288 | fn effective_tld_plus_one_test() { 289 | for &(domain, want) in ETLD_PLUS_ONE_TEST_CASES { 290 | assert_eq!( 291 | DEFAULT_PROVIDER.effective_tld_plus_one(domain), 292 | want, 293 | "{domain:?} -> {want:?}" 294 | ); 295 | } 296 | } 297 | --------------------------------------------------------------------------------