webauthn-rs-proto

Patched webauthn-rs-proto (https://crates.io/crates/webauthn-rs-proto) that adds support for Ed25519.
git clone https://git.philomathiclife.com/repos/webauthn-rs-proto
Log | Files | Refs | README | LICENSE

commit e925eae63471f8a8a4e94d5145c346152e54371d
Author: Zack Newman <zack@philomathiclife.com>
Date:   Sat, 30 Dec 2023 10:17:08 -0700

init

Diffstat:
A.gitignore | 2++
ACargo.toml | 36++++++++++++++++++++++++++++++++++++
ALICENSE.md | 374+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AREADME.md | 35+++++++++++++++++++++++++++++++++++
Asrc/attest.rs | 208+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/auth.rs | 223+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/cose.rs | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/extensions.rs | 412+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib.rs | 18++++++++++++++++++
Asrc/options.rs | 301+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
10 files changed, 1700 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,2 @@ +Cargo.lock +target/** diff --git a/Cargo.toml b/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "webauthn-rs-proto" +version = "0.4.10" +authors = ["William Brown <william@blackhats.net.au>", "Zack Newman <zack@philomathiclife.com>"] +edition = "2021" +description = "Webauthn Specification Bindings" +repository = "https://git.philomathiclife.com/repos/webauthn-rs-proto" +readme = "README.md" +keywords = ["webauthn", "authentication"] +categories = ["authentication", "web-programming"] +license = "MPL-2.0" + +[features] +wasm = ["wasm-bindgen", "web-sys", "js-sys", "serde-wasm-bindgen"] + +[dependencies] +base64urlsafedata = { version = "0.1.2" } +serde = { version = "1", features = ["derive"] } +serde_json = "1.0" +url = { version = "2", features = ["serde"] } +wasm-bindgen = { version = "0.2", features = ["serde-serialize"], optional = true } +serde-wasm-bindgen = { version = "0.4", optional = true } +js-sys = { version = "0.3", optional = true } + +[dependencies.web-sys] +version = "0.3" +optional = true +features = [ + "CredentialCreationOptions", + "CredentialRequestOptions", + "PublicKeyCredential", + "PublicKeyCredentialCreationOptions", + "AuthenticationExtensionsClientInputs", + "AuthenticationExtensionsClientOutputs", + "console", +] diff --git a/LICENSE.md b/LICENSE.md @@ -0,0 +1,374 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. + diff --git a/README.md b/README.md @@ -0,0 +1,35 @@ +# webauthn-rs-proto + +`webauthn-rs-proto` is a patched version of [`webauthn-rs-proto`](https://crates.io/crates/webauthn-rs-proto) that +adds support for Ed25519. + +## Why not upstream the patch? + +There are philosophical differences between upstream and me. Additionally upstream misunderstands the benefits of Ed25519 and is convinced +there is no benefit to it over ES256 despite _actual_ cryptographers disagreeing. + +## How to use this crate? + +This crate will hopefully require minimal changes to upstream; as a result, it will not be published to `crates.io`. To use +this crate, configure `Cargo.toml` and `.cargo/config.toml` like below: + +```toml +[patch.crates-io] +webauthn-rs-proto = { git = "https://git.philomathiclife.com/repos/webauthn-rs-proto", tag = "v0.4.10" } +``` + +```toml +[net] +git-fetch-with-cli = true +``` + +### Status + +This package will be actively maintained to stay in-sync with the latest version of `webauthn-rs-proto`. The crate +is only tested on the `x86_64-unknown-linux-gnu` and `x86_64-unknown-openbsd` targets, but +it should work on any [Tier 1 with Host Tools](https://doc.rust-lang.org/beta/rustc/platform-support.html) +target. + +Due to other philosophical differences and coding differences including but not limited to licensing; adherence to government +standards that are not actually cryptographically relevant; and use of OpenSSL 3.x.y, this crate will be abandoned +when [`webauthn_rp`](https://docs.rs/webauthn_rp/latest/webauthn_rp/) becomes available. diff --git a/src/attest.rs b/src/attest.rs @@ -0,0 +1,208 @@ +//! Types related to attestation (Registration) + +use base64urlsafedata::Base64UrlSafeData; +use serde::{Deserialize, Serialize}; + +use crate::extensions::{RegistrationExtensionsClientOutputs, RequestRegistrationExtensions}; +use crate::options::*; + +/// <https://w3c.github.io/webauthn/#dictionary-makecredentialoptions> +#[derive(Debug, Serialize, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PublicKeyCredentialCreationOptions { + /// The relying party + pub rp: RelyingParty, + /// The user. + pub user: User, + /// The one-time challenge for the credential to sign. + pub challenge: Base64UrlSafeData, + /// The set of cryptographic types allowed by this server. + pub pub_key_cred_params: Vec<PubKeyCredParams>, + + /// The timeout for the authenticator to stop accepting the operation + #[serde(skip_serializing_if = "Option::is_none")] + pub timeout: Option<u32>, + + /// The requested attestation level from the device. + #[serde(skip_serializing_if = "Option::is_none")] + pub attestation: Option<AttestationConveyancePreference>, + + /// Credential ID's that are excluded from being able to be registered. + #[serde(skip_serializing_if = "Option::is_none")] + pub exclude_credentials: Option<Vec<PublicKeyCredentialDescriptor>>, + + /// Criteria defining which authenticators may be used in this operation. + #[serde(skip_serializing_if = "Option::is_none")] + pub authenticator_selection: Option<AuthenticatorSelectionCriteria>, + + /// Non-standard extensions that may be used by the browser/authenticator. + #[serde(skip_serializing_if = "Option::is_none")] + pub extensions: Option<RequestRegistrationExtensions>, +} + +/// A JSON serializable challenge which is issued to the user's webbrowser +/// for handling. This is meant to be opaque, that is, you should not need +/// to inspect or alter the content of the struct - you should serialise it +/// and transmit it to the client only. +#[derive(Debug, Serialize, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreationChallengeResponse { + /// The options. + pub public_key: PublicKeyCredentialCreationOptions, +} + +#[cfg(feature = "wasm")] +impl From<CreationChallengeResponse> for web_sys::CredentialCreationOptions { + fn from(ccr: CreationChallengeResponse) -> Self { + use js_sys::{Array, Object, Uint8Array}; + use wasm_bindgen::JsValue; + + let chal = Uint8Array::from(ccr.public_key.challenge.0.as_slice()); + let userid = Uint8Array::from(ccr.public_key.user.id.0.as_slice()); + + let jsv = serde_wasm_bindgen::to_value(&ccr).unwrap(); + + let pkcco = js_sys::Reflect::get(&jsv, &"publicKey".into()).unwrap(); + js_sys::Reflect::set(&pkcco, &"challenge".into(), &chal).unwrap(); + + let user = js_sys::Reflect::get(&pkcco, &"user".into()).unwrap(); + js_sys::Reflect::set(&user, &"id".into(), &userid).unwrap(); + + if let Some(extensions) = ccr.public_key.extensions { + let obj: Object = (&extensions).into(); + js_sys::Reflect::set(&pkcco, &"extensions".into(), &obj).unwrap(); + } + + if let Some(exclude_credentials) = ccr.public_key.exclude_credentials { + // There must be an array of these in the jsv ... + let exclude_creds: Array = exclude_credentials + .iter() + .map(|ac| { + let obj = Object::new(); + js_sys::Reflect::set( + &obj, + &"type".into(), + &JsValue::from_str(ac.type_.as_str()), + ) + .unwrap(); + + js_sys::Reflect::set(&obj, &"id".into(), &Uint8Array::from(ac.id.0.as_slice())) + .unwrap(); + + if let Some(transports) = &ac.transports { + let tarray: Array = transports + .iter() + .map(|trs| serde_wasm_bindgen::to_value(trs).unwrap()) + .collect(); + + js_sys::Reflect::set(&obj, &"transports".into(), &tarray).unwrap(); + } + + obj + }) + .collect(); + + js_sys::Reflect::set(&pkcco, &"excludeCredentials".into(), &exclude_creds).unwrap(); + } + + web_sys::CredentialCreationOptions::from(jsv) + } +} + +/// <https://w3c.github.io/webauthn/#authenticatorattestationresponse> +#[derive(Debug, Serialize, Clone, Deserialize)] +pub struct AuthenticatorAttestationResponseRaw { + /// <https://w3c.github.io/webauthn/#dom-authenticatorattestationresponse-attestationobject> + #[serde(rename = "attestationObject")] + pub attestation_object: Base64UrlSafeData, + + /// <https://w3c.github.io/webauthn/#dom-authenticatorresponse-clientdatajson> + #[serde(rename = "clientDataJSON")] + pub client_data_json: Base64UrlSafeData, + + /// <https://w3c.github.io/webauthn/#dom-authenticatorattestationresponse-gettransports> + #[serde(default)] + pub transports: Option<Vec<AuthenticatorTransport>>, +} + +/// A client response to a registration challenge. This contains all required +/// information to asses and assert trust in a credentials legitimacy, followed +/// by registration to a user. +/// +/// You should not need to handle the inner content of this structure - you should +/// provide this to the correctly handling function of Webauthn only. +/// <https://w3c.github.io/webauthn/#iface-pkcredential> +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct RegisterPublicKeyCredential { + /// The id of the PublicKey credential, likely in base64. + /// + /// This is NEVER actually + /// used in a real registration, because the true credential ID is taken from the + /// attestation data. + pub id: String, + /// The id of the credential, as binary. + /// + /// This is NEVER actually + /// used in a real registration, because the true credential ID is taken from the + /// attestation data. + #[serde(rename = "rawId")] + pub raw_id: Base64UrlSafeData, + /// <https://w3c.github.io/webauthn/#dom-publickeycredential-response> + pub response: AuthenticatorAttestationResponseRaw, + /// The type of credential. + #[serde(rename = "type")] + pub type_: String, + /// Unsigned Client processed extensions. + #[serde(default)] + pub extensions: RegistrationExtensionsClientOutputs, +} + +#[cfg(feature = "wasm")] +impl From<web_sys::PublicKeyCredential> for RegisterPublicKeyCredential { + fn from(data: web_sys::PublicKeyCredential) -> RegisterPublicKeyCredential { + use js_sys::Uint8Array; + + // is_user_verifying_platform_authenticator_available + + // AuthenticatorAttestationResponse has getTransports but web_sys isn't exposing it? + let transports = None; + + // First, we have to b64 some data here. + // data.raw_id + let data_raw_id = + Uint8Array::new(&js_sys::Reflect::get(&data, &"rawId".into()).unwrap()).to_vec(); + + let data_response = js_sys::Reflect::get(&data, &"response".into()).unwrap(); + let data_response_attestation_object = Uint8Array::new( + &js_sys::Reflect::get(&data_response, &"attestationObject".into()).unwrap(), + ) + .to_vec(); + + let data_response_client_data_json = Uint8Array::new( + &js_sys::Reflect::get(&data_response, &"clientDataJSON".into()).unwrap(), + ) + .to_vec(); + + let data_extensions = data.get_client_extension_results(); + + // Now we can convert to the base64 values for json. + let data_raw_id_b64 = Base64UrlSafeData(data_raw_id); + + let data_response_attestation_object_b64 = + Base64UrlSafeData(data_response_attestation_object); + + let data_response_client_data_json_b64 = Base64UrlSafeData(data_response_client_data_json); + + RegisterPublicKeyCredential { + id: format!("{}", data_raw_id_b64), + raw_id: data_raw_id_b64, + response: AuthenticatorAttestationResponseRaw { + attestation_object: data_response_attestation_object_b64, + client_data_json: data_response_client_data_json_b64, + transports, + }, + type_: "public-key".to_string(), + extensions: data_extensions.into(), + } + } +} diff --git a/src/auth.rs b/src/auth.rs @@ -0,0 +1,223 @@ +//! Types related to authentication (Assertion) + +use base64urlsafedata::Base64UrlSafeData; +use serde::{Deserialize, Serialize}; + +use crate::extensions::{AuthenticationExtensionsClientOutputs, RequestAuthenticationExtensions}; +use crate::options::*; + +/// The requested options for the authentication +#[derive(Debug, Serialize, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PublicKeyCredentialRequestOptions { + /// The challenge that should be signed by the authenticator. + pub challenge: Base64UrlSafeData, + /// The timeout for the authenticator in case of no interaction. + #[serde(skip_serializing_if = "Option::is_none")] + pub timeout: Option<u32>, + /// The relying party ID. + pub rp_id: String, + /// The set of credentials that are allowed to sign this challenge. + pub allow_credentials: Vec<AllowCredentials>, + /// The verification policy the browser will request. + pub user_verification: UserVerificationPolicy, + /// extensions. + #[serde(skip_serializing_if = "Option::is_none")] + pub extensions: Option<RequestAuthenticationExtensions>, +} + +/// Request in residentkey workflows that conditional mediation should be used +/// in the UI, or not. +#[derive(Debug, Serialize, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum Mediation { + // /// No mediation is provided - This is represented by "None" on the Option + // below. We can't use None here as a variant because it confuses serde-wasm-bindgen :( + // None, + // /// Silent, try to do things without the user being involved. Probably a bad idea. + // Silent, + // /// If we can get creds without the user having to do anything, great, other wise ask the user. Probably a bad idea. + // Optional, + /// Discovered credentials are presented to the user in a dialog. + /// Conditional UI is used. See <https://github.com/w3c/webauthn/wiki/Explainer:-WebAuthn-Conditional-UI> + /// <https://w3c.github.io/webappsec-credential-management/#enumdef-credentialmediationrequirement> + Conditional, + // /// The user needs to do something. + // Required +} + +/// A JSON serializable challenge which is issued to the user's webbrowser +/// for handling. This is meant to be opaque, that is, you should not need +/// to inspect or alter the content of the struct - you should serialise it +/// and transmit it to the client only. +#[derive(Debug, Serialize, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RequestChallengeResponse { + /// The options. + pub public_key: PublicKeyCredentialRequestOptions, + #[serde(default, skip_serializing_if = "Option::is_none")] + /// The mediation requested + pub mediation: Option<Mediation>, +} + +#[cfg(feature = "wasm")] +impl From<RequestChallengeResponse> for web_sys::CredentialRequestOptions { + fn from(rcr: RequestChallengeResponse) -> Self { + use js_sys::{Array, Object, Uint8Array}; + use wasm_bindgen::JsValue; + + let jsv = serde_wasm_bindgen::to_value(&rcr).unwrap(); + let pkcco = js_sys::Reflect::get(&jsv, &"publicKey".into()).unwrap(); + + let chal = Uint8Array::from(rcr.public_key.challenge.0.as_slice()); + js_sys::Reflect::set(&pkcco, &"challenge".into(), &chal).unwrap(); + + if let Some(extensions) = rcr.public_key.extensions { + let obj: Object = (&extensions).into(); + js_sys::Reflect::set(&pkcco, &"extensions".into(), &obj).unwrap(); + } + + let allow_creds: Array = rcr + .public_key + .allow_credentials + .iter() + .map(|ac| { + let obj = Object::new(); + js_sys::Reflect::set(&obj, &"type".into(), &JsValue::from_str(ac.type_.as_str())) + .unwrap(); + + js_sys::Reflect::set(&obj, &"id".into(), &Uint8Array::from(ac.id.0.as_slice())) + .unwrap(); + + if let Some(transports) = &ac.transports { + let tarray: Array = transports + .iter() + .map(|trs| serde_wasm_bindgen::to_value(trs).unwrap()) + .collect(); + + js_sys::Reflect::set(&obj, &"transports".into(), &tarray).unwrap(); + } + + obj + }) + .collect(); + js_sys::Reflect::set(&pkcco, &"allowCredentials".into(), &allow_creds).unwrap(); + + web_sys::CredentialRequestOptions::from(jsv) + } +} + +/// <https://w3c.github.io/webauthn/#authenticatorassertionresponse> +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct AuthenticatorAssertionResponseRaw { + /// Raw authenticator data. + #[serde(rename = "authenticatorData")] + pub authenticator_data: Base64UrlSafeData, + + /// Signed client data. + #[serde(rename = "clientDataJSON")] + pub client_data_json: Base64UrlSafeData, + + /// Signature + pub signature: Base64UrlSafeData, + + /// Optional userhandle. + #[serde(rename = "userHandle")] + pub user_handle: Option<Base64UrlSafeData>, +} + +/// A client response to an authentication challenge. This contains all required +/// information to asses and assert trust in a credentials legitimacy, followed +/// by authentication to a user. +/// +/// You should not need to handle the inner content of this structure - you should +/// provide this to the correctly handling function of Webauthn only. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct PublicKeyCredential { + /// The credential Id, likely base64 + pub id: String, + /// The binary of the credential id. + #[serde(rename = "rawId")] + pub raw_id: Base64UrlSafeData, + /// The authenticator response. + pub response: AuthenticatorAssertionResponseRaw, + /// Unsigned Client processed extensions. + #[serde(default)] + pub extensions: AuthenticationExtensionsClientOutputs, + /// The authenticator type. + #[serde(rename = "type")] + pub type_: String, +} + +impl PublicKeyCredential { + /// Retrieve the user uniqueid that *may* have been provided by the authenticator during this + /// authentication. + pub fn get_user_unique_id(&self) -> Option<&[u8]> { + self.response.user_handle.as_ref().map(|b| b.as_ref()) + } + + /// Retrieve the credential id that was provided in this authentication + pub fn get_credential_id(&self) -> &[u8] { + self.raw_id.0.as_slice() + } +} + +#[cfg(feature = "wasm")] +impl From<web_sys::PublicKeyCredential> for PublicKeyCredential { + fn from(data: web_sys::PublicKeyCredential) -> PublicKeyCredential { + use js_sys::Uint8Array; + + let data_raw_id = + Uint8Array::new(&js_sys::Reflect::get(&data, &"rawId".into()).unwrap()).to_vec(); + + let data_response = js_sys::Reflect::get(&data, &"response".into()).unwrap(); + + let data_response_authenticator_data = Uint8Array::new( + &js_sys::Reflect::get(&data_response, &"authenticatorData".into()).unwrap(), + ) + .to_vec(); + + let data_response_signature = + Uint8Array::new(&js_sys::Reflect::get(&data_response, &"signature".into()).unwrap()) + .to_vec(); + + let data_response_user_handle = + &js_sys::Reflect::get(&data_response, &"userHandle".into()).unwrap(); + let data_response_user_handle = if data_response_user_handle.is_undefined() { + None + } else { + Some(Uint8Array::new(data_response_user_handle).to_vec()) + }; + + let data_response_client_data_json = Uint8Array::new( + &js_sys::Reflect::get(&data_response, &"clientDataJSON".into()).unwrap(), + ) + .to_vec(); + + let data_extensions = data.get_client_extension_results(); + web_sys::console::log_1(&data_extensions); + + // Base64 it + + let data_raw_id_b64 = Base64UrlSafeData(data_raw_id); + let data_response_client_data_json_b64 = Base64UrlSafeData(data_response_client_data_json); + let data_response_authenticator_data_b64 = + Base64UrlSafeData(data_response_authenticator_data); + let data_response_signature_b64 = Base64UrlSafeData(data_response_signature); + + let data_response_user_handle_b64 = data_response_user_handle.map(Base64UrlSafeData); + + PublicKeyCredential { + id: format!("{}", data_raw_id_b64), + raw_id: data_raw_id_b64, + response: AuthenticatorAssertionResponseRaw { + authenticator_data: data_response_authenticator_data_b64, + client_data_json: data_response_client_data_json_b64, + signature: data_response_signature_b64, + user_handle: data_response_user_handle_b64, + }, + extensions: data_extensions.into(), + type_: "public-key".to_string(), + } + } +} diff --git a/src/cose.rs b/src/cose.rs @@ -0,0 +1,91 @@ +//! Types related to CBOR Object Signing and Encryption (COSE) + +use serde::{Deserialize, Serialize}; + +/// A COSE signature algorithm, indicating the type of key and hash type +/// that should be used. You shouldn't need to alter or use this value. +#[allow(non_camel_case_types)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[repr(i32)] +pub enum COSEAlgorithm { + #[serde(alias = "Ed25519")] + /// Identifies this key as EdDSA (likely curve Ed25519). + EdDSA = -8, + /// Identifies this key as ECDSA (recommended SECP256R1) with SHA256 hashing + #[serde(alias = "ECDSA_SHA256")] + ES256 = -7, // recommends curve SECP256R1 + /// Identifies this key as ECDSA (recommended SECP384R1) with SHA384 hashing + #[serde(alias = "ECDSA_SHA384")] + ES384 = -35, // recommends curve SECP384R1 + /// Identifies this key as ECDSA (recommended SECP521R1) with SHA512 hashing + #[serde(alias = "ECDSA_SHA512")] + ES512 = -36, // recommends curve SECP521R1 + /// Identifies this key as RS256 aka RSASSA-PKCS1-v1_5 w/ SHA-256 + RS256 = -257, + /// Identifies this key as RS384 aka RSASSA-PKCS1-v1_5 w/ SHA-384 + RS384 = -258, + /// Identifies this key as RS512 aka RSASSA-PKCS1-v1_5 w/ SHA-512 + RS512 = -259, + /// Identifies this key as PS256 aka RSASSA-PSS w/ SHA-256 + PS256 = -37, + /// Identifies this key as PS384 aka RSASSA-PSS w/ SHA-384 + PS384 = -38, + /// Identifies this key as PS512 aka RSASSA-PSS w/ SHA-512 + PS512 = -39, + /// Identifies this as an INSECURE RS1 aka RSASSA-PKCS1-v1_5 using SHA-1. This is not + /// used by validators, but can exist in some windows hello tpm's + INSECURE_RS1 = -65535, + /// Identifies this key as the protocol used for [PIN/UV Auth Protocol One](https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#pinProto1) + /// + /// This reports as algorithm `-25`, but it is a lie. Don't include this in any algorithm lists. + PinUvProtocol, +} + +impl COSEAlgorithm { + /// Return the set of secure recommended COSEAlgorithm's + pub fn secure_algs() -> Vec<Self> { + vec![ + COSEAlgorithm::EdDSA, + COSEAlgorithm::ES256, + COSEAlgorithm::RS256, + ] + } + + /// Return the set of all possible algorithms that may exist as a COSEAlgorithm + pub fn all_possible_algs() -> Vec<Self> { + vec![ + COSEAlgorithm::EdDSA, + COSEAlgorithm::ES256, + COSEAlgorithm::ES384, + COSEAlgorithm::ES512, + COSEAlgorithm::RS256, + COSEAlgorithm::RS384, + COSEAlgorithm::RS512, + COSEAlgorithm::PS256, + COSEAlgorithm::PS384, + COSEAlgorithm::PS512, + COSEAlgorithm::INSECURE_RS1, + ] + } +} + +impl TryFrom<i128> for COSEAlgorithm { + type Error = (); + + fn try_from(i: i128) -> Result<Self, Self::Error> { + match i { + -8 => Ok(COSEAlgorithm::EdDSA), + -7 => Ok(COSEAlgorithm::ES256), + -35 => Ok(COSEAlgorithm::ES384), + -36 => Ok(COSEAlgorithm::ES512), + -257 => Ok(COSEAlgorithm::RS256), + -258 => Ok(COSEAlgorithm::RS384), + -259 => Ok(COSEAlgorithm::RS512), + -37 => Ok(COSEAlgorithm::PS256), + -38 => Ok(COSEAlgorithm::PS384), + -39 => Ok(COSEAlgorithm::PS512), + -65535 => Ok(COSEAlgorithm::INSECURE_RS1), + _ => Err(()), + } + } +} diff --git a/src/extensions.rs b/src/extensions.rs @@ -0,0 +1,412 @@ +//! Extensions allowing certain types of authenticators to provide supplemental information. + +use base64urlsafedata::Base64UrlSafeData; +use serde::{Deserialize, Serialize}; + +/// Valid credential protection policies +#[derive(Debug, Serialize, Clone, Copy, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[repr(u8)] +pub enum CredentialProtectionPolicy { + /// This reflects "FIDO_2_0" semantics. In this configuration, performing + /// some form of user verification is optional with or without credentialID + /// list. This is the default state of the credential if the extension is + /// not specified. + UserVerificationOptional = 0x1, + /// In this configuration, credential is discovered only when its + /// credentialID is provided by the platform or when some form of user + /// verification is performed. + UserVerificationOptionalWithCredentialIDList = 0x2, + /// This reflects that discovery and usage of the credential MUST be + /// preceded by some form of user verification. + UserVerificationRequired = 0x3, +} + +impl TryFrom<u8> for CredentialProtectionPolicy { + type Error = &'static str; + + fn try_from(v: u8) -> Result<Self, Self::Error> { + use CredentialProtectionPolicy::*; + match v { + 0x1 => Ok(UserVerificationOptional), + 0x2 => Ok(UserVerificationOptionalWithCredentialIDList), + 0x3 => Ok(UserVerificationRequired), + _ => Err("Invalid policy number"), + } + } +} + +/// The desired options for the client's use of the `credProtect` extension +/// +/// <https://fidoalliance.org/specs/fido-v2.1-rd-20210309/fido-client-to-authenticator-protocol-v2.1-rd-20210309.html#sctn-credProtect-extension> +#[derive(Debug, Serialize, Clone, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct CredProtect { + /// The credential policy to enact + pub credential_protection_policy: CredentialProtectionPolicy, + /// Whether it is better for the authenticator to fail to create a + /// credential rather than ignore the protection policy + /// If no value is provided, the client treats it as `false`. + #[serde(skip_serializing_if = "Option::is_none")] + pub enforce_credential_protection_policy: Option<bool>, +} + +/// Extension option inputs for PublicKeyCredentialCreationOptions. +/// +/// Implements \[AuthenticatorExtensionsClientInputs\] from the spec. +#[derive(Debug, Serialize, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RequestRegistrationExtensions { + /// The `credProtect` extension options + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub cred_protect: Option<CredProtect>, + + /// ⚠️ - Browsers do not support this! + /// Uvm + #[serde(skip_serializing_if = "Option::is_none")] + pub uvm: Option<bool>, + + /// ⚠️ - This extension result is always unsigned, and only indicates if the + /// browser *requests* a residentKey to be created. It has no bearing on the + /// true rk state of the credential. + #[serde(skip_serializing_if = "Option::is_none")] + pub cred_props: Option<bool>, + + /// CTAP2.1 Minumum pin length + #[serde(skip_serializing_if = "Option::is_none")] + pub min_pin_length: Option<bool>, + + /// ⚠️ - Browsers support the *creation* of the secret, but not the retrieval of it. + /// CTAP2.1 create hmac secret + #[serde(skip_serializing_if = "Option::is_none")] + pub hmac_create_secret: Option<bool>, +} + +impl Default for RequestRegistrationExtensions { + fn default() -> Self { + RequestRegistrationExtensions { + cred_protect: None, + uvm: Some(true), + cred_props: Some(true), + min_pin_length: None, + hmac_create_secret: None, + } + } +} + +// Unable to create from, because it's an out of crate struct +#[allow(clippy::from_over_into)] +#[cfg(feature = "wasm")] +impl Into<js_sys::Object> for &RequestRegistrationExtensions { + fn into(self) -> js_sys::Object { + use js_sys::Object; + use wasm_bindgen::JsValue; + + let RequestRegistrationExtensions { + cred_protect, + uvm, + cred_props, + min_pin_length, + hmac_create_secret, + } = self; + + let obj = Object::new(); + + if let Some(cred_protect) = cred_protect { + let jsv = serde_wasm_bindgen::to_value(&cred_protect).unwrap(); + js_sys::Reflect::set(&obj, &"credProtect".into(), &jsv).unwrap(); + } + + if let Some(uvm) = uvm { + js_sys::Reflect::set(&obj, &"uvm".into(), &JsValue::from_bool(*uvm)).unwrap(); + } + + if let Some(cred_props) = cred_props { + js_sys::Reflect::set(&obj, &"credProps".into(), &JsValue::from_bool(*cred_props)) + .unwrap(); + } + + if let Some(min_pin_length) = min_pin_length { + js_sys::Reflect::set( + &obj, + &"minPinLength".into(), + &JsValue::from_bool(*min_pin_length), + ) + .unwrap(); + } + + if let Some(hmac_create_secret) = hmac_create_secret { + js_sys::Reflect::set( + &obj, + &"hmacCreateSecret".into(), + &JsValue::from_bool(*hmac_create_secret), + ) + .unwrap(); + } + + obj + } +} + +// ========== Auth exten ============ + +/// The inputs to the hmac secret if it was created during registration. +/// +/// <https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#sctn-hmac-secret-extension> +#[derive(Debug, Serialize, Clone, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct HmacGetSecretInput { + /// Retrieve a symmetric secrets from the authenticator with this input. + pub output1: Base64UrlSafeData, + /// Rotate the secret in the same operation. + pub output2: Option<Base64UrlSafeData>, +} + +/// Extension option inputs for PublicKeyCredentialRequestOptions +/// +/// Implements \[AuthenticatorExtensionsClientInputs\] from the spec +#[derive(Debug, Serialize, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RequestAuthenticationExtensions { + /// The `appid` extension options + #[serde(skip_serializing_if = "Option::is_none")] + pub appid: Option<String>, + + /// ⚠️ - Browsers do not support this! + /// Uvm + #[serde(skip_serializing_if = "Option::is_none")] + pub uvm: Option<bool>, + + /// ⚠️ - Browsers do not support this! + /// <https://bugs.chromium.org/p/chromium/issues/detail?id=1023225> + /// Hmac get secret + #[serde(skip_serializing_if = "Option::is_none")] + pub hmac_get_secret: Option<HmacGetSecretInput>, +} + +// Unable to create from, because it's an out of crate struct +#[allow(clippy::from_over_into)] +#[cfg(feature = "wasm")] +impl Into<js_sys::Object> for &RequestAuthenticationExtensions { + fn into(self) -> js_sys::Object { + use js_sys::{Object, Uint8Array}; + use wasm_bindgen::JsValue; + + let RequestAuthenticationExtensions { + // I don't think we care? + appid: _, + uvm, + hmac_get_secret, + } = self; + + let obj = Object::new(); + + if let Some(uvm) = uvm { + js_sys::Reflect::set(&obj, &"uvm".into(), &JsValue::from_bool(*uvm)).unwrap(); + } + + if let Some(HmacGetSecretInput { output1, output2 }) = hmac_get_secret { + let hmac = Object::new(); + + let o1 = Uint8Array::from(output1.0.as_slice()); + js_sys::Reflect::set(&hmac, &"output1".into(), &o1).unwrap(); + + if let Some(output2) = output2 { + let o2 = Uint8Array::from(output2.0.as_slice()); + js_sys::Reflect::set(&hmac, &"output2".into(), &o2).unwrap(); + } + + js_sys::Reflect::set(&obj, &"hmacGetSecret".into(), &hmac).unwrap(); + } + + obj + } +} + +/// The response to a hmac get secret request. +#[derive(Debug, Serialize, Clone, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct HmacGetSecretOutput { + /// Output of HMAC(Salt 1 || Client Secret) + pub output1: Base64UrlSafeData, + /// Output of HMAC(Salt 2 || Client Secret) + pub output2: Option<Base64UrlSafeData>, +} + +/// <https://w3c.github.io/webauthn/#dictdef-authenticationextensionsclientoutputs> +/// The default option here for Options are None, so it can be derived +#[derive(Debug, Deserialize, Serialize, Clone, Default)] +pub struct AuthenticationExtensionsClientOutputs { + /// Indicates whether the client used the provided appid extension + #[serde(default)] + pub appid: Option<bool>, + /// The response to a hmac get secret request. + #[serde(default)] + pub hmac_get_secret: Option<HmacGetSecretOutput>, +} + +#[cfg(feature = "wasm")] +impl From<web_sys::AuthenticationExtensionsClientOutputs> + for AuthenticationExtensionsClientOutputs +{ + fn from( + ext: web_sys::AuthenticationExtensionsClientOutputs, + ) -> AuthenticationExtensionsClientOutputs { + use js_sys::Uint8Array; + + let appid = js_sys::Reflect::get(&ext, &"appid".into()) + .ok() + .and_then(|jv| jv.as_bool()); + + let hmac_get_secret = js_sys::Reflect::get(&ext, &"hmacGetSecret".into()) + .ok() + .and_then(|jv| { + let output2 = js_sys::Reflect::get(&jv, &"output2".into()) + .map(|v| Uint8Array::new(&v).to_vec()) + .map(Base64UrlSafeData) + .ok(); + + let output1 = js_sys::Reflect::get(&jv, &"output1".into()) + .map(|v| Uint8Array::new(&v).to_vec()) + .map(Base64UrlSafeData) + .ok(); + + output1.map(|output1| HmacGetSecretOutput { output1, output2 }) + }); + + AuthenticationExtensionsClientOutputs { + appid, + hmac_get_secret, + } + } +} + +/// <https://www.w3.org/TR/webauthn-3/#sctn-authenticator-credential-properties-extension> +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct CredProps { + rk: bool, +} + +/// <https://w3c.github.io/webauthn/#dictdef-authenticationextensionsclientoutputs> +/// The default option here for Options are None, so it can be derived +#[derive(Debug, Deserialize, Serialize, Clone, Default)] +pub struct RegistrationExtensionsClientOutputs { + /// Indicates whether the client used the provided appid extension + #[serde(default, skip_serializing_if = "Option::is_none")] + pub appid: Option<bool>, + + /// Indicates if the client believes it created a resident key. This + /// property is managed by the webbrowser, and is NOT SIGNED and CAN NOT be trusted! + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cred_props: Option<CredProps>, + + /// Indicates if the client successfully applied a HMAC Secret + #[serde(default, skip_serializing_if = "Option::is_none")] + pub hmac_secret: Option<bool>, + + /// Indicates if the client successfully applied a credential protection policy. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cred_protect: Option<CredentialProtectionPolicy>, + + /// Indicates the current minimum PIN length + #[serde(default, skip_serializing_if = "Option::is_none")] + pub min_pin_length: Option<u32>, +} + +#[cfg(feature = "wasm")] +impl From<web_sys::AuthenticationExtensionsClientOutputs> for RegistrationExtensionsClientOutputs { + fn from( + ext: web_sys::AuthenticationExtensionsClientOutputs, + ) -> RegistrationExtensionsClientOutputs { + let appid = js_sys::Reflect::get(&ext, &"appid".into()) + .ok() + .and_then(|jv| jv.as_bool()); + + // Destructure "credProps":{"rk":false} from within a map. + let cred_props = js_sys::Reflect::get(&ext, &"credProps".into()) + .ok() + .and_then(|cred_props_struct| { + js_sys::Reflect::get(&cred_props_struct, &"rk".into()) + .ok() + .and_then(|jv| jv.as_bool()) + .map(|rk| CredProps { rk }) + }); + + let hmac_secret = js_sys::Reflect::get(&ext, &"hmac-secret".into()) + .ok() + .and_then(|jv| jv.as_bool()); + + let cred_protect = js_sys::Reflect::get(&ext, &"credProtect".into()) + .ok() + .and_then(|jv| jv.as_f64()) + .and_then(|f| CredentialProtectionPolicy::try_from(f as u8).ok()); + + let min_pin_length = js_sys::Reflect::get(&ext, &"minPinLength".into()) + .ok() + .and_then(|jv| jv.as_f64()) + .map(|f| f as u32); + + RegistrationExtensionsClientOutputs { + appid, + cred_props, + hmac_secret, + cred_protect, + min_pin_length, + } + } +} + +/// The result state of an extension as returned from the authenticator. +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub enum ExtnState<T> +where + T: Clone + core::fmt::Debug, +{ + /// This extension was not requested, and so no result was provided. + #[default] + NotRequested, + /// The extension was requested, and the authenticator did NOT act on it. + Ignored, + /// The extension was requested, and the authenticator correctly responded. + Set(T), + /// The extension was not requested, and the authenticator sent an unsolicited extension value. + Unsolicited(T), + /// ⚠️ WARNING: The data in this extension is not signed cryptographically, and can not be + /// trusted for security assertions. It MAY be used for UI/UX hints. + Unsigned(T), +} + +/// The set of extensions that were registered by this credential. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RegisteredExtensions { + // ⚠️ It's critical we place serde default here so that we + // can deserialise in the future as we add new types! + /// The state of the cred_protect extension + #[serde(default)] + pub cred_protect: ExtnState<CredentialProtectionPolicy>, + /// The state of the hmac-secret extension, if it was created + #[serde(default)] + pub hmac_create_secret: ExtnState<bool>, + /// The state of the client appid extensions + #[serde(default)] + pub appid: ExtnState<bool>, + /// The state of the client credential properties extension + #[serde(default)] + pub cred_props: ExtnState<CredProps>, +} + +impl RegisteredExtensions { + /// Yield an empty set of registered extensions + pub fn none() -> Self { + RegisteredExtensions { + cred_protect: ExtnState::NotRequested, + hmac_create_secret: ExtnState::NotRequested, + appid: ExtnState::NotRequested, + cred_props: ExtnState::NotRequested, + } + } +} + +/// The set of extensions that were provided by the client during authentication +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AuthenticationExtensions {} diff --git a/src/lib.rs b/src/lib.rs @@ -0,0 +1,18 @@ +//! JSON Protocol Structs and representations for communication with authenticators +//! and clients. + +#![deny(warnings)] +#![warn(unused_extern_crates)] +#![warn(missing_docs)] + +pub mod attest; +pub mod auth; +pub mod cose; +pub mod extensions; +pub mod options; + +pub use attest::*; +pub use auth::*; +pub use cose::*; +pub use extensions::*; +pub use options::*; diff --git a/src/options.rs b/src/options.rs @@ -0,0 +1,301 @@ +//! Types that define options as to how an authenticator may interact with +//! with the server. + +use base64urlsafedata::Base64UrlSafeData; +use serde::{Deserialize, Serialize}; +use std::{collections::BTreeMap, str::FromStr}; + +/// A credential ID type. At the moment this is a vector of bytes, but +/// it could also be a future change for this to be base64 string instead. +/// +/// If changed, this would likely be a major library version change. +pub type CredentialID = Base64UrlSafeData; + +/// Defines the User Authenticator Verification policy. This is documented +/// <https://w3c.github.io/webauthn/#enumdef-userverificationrequirement>, and each +/// variant lists it's effects. +/// +/// To be clear, Verification means that the Authenticator perform extra or supplementary +/// interaction with the user to verify who they are. An example of this is Apple Touch Id +/// required a fingerprint to be verified, or a yubico device requiring a pin in addition to +/// a touch event. +/// +/// An example of a non-verified interaction is a yubico device with no pin where touch is +/// the only interaction - we only verify a user is present, but we don't have extra details +/// to the legitimacy of that user. +/// +/// As UserVerificationPolicy is *only* used in credential registration, this stores the +/// verification state of the credential in the persisted credential. These persisted +/// credentials define which UserVerificationPolicy is issued during authentications. +/// +/// ⚠️ WARNING - discouraged is marked with a warning, as in some cases, some authenticators +/// will FORCE verification during registration but NOT during authentication. This means +/// that is is NOT possible assert verification has been bypassed or not from the server +/// viewpoint, and to the user it may create confusion about when verification is or is +/// not required. +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[allow(non_camel_case_types)] +#[serde(rename_all = "lowercase")] +pub enum UserVerificationPolicy { + /// Require User Verification bit to be set, and fail the registration or authentication + /// if false. If the authenticator is not able to perform verification, it may not be + /// usable with this policy. + Required, + /// TO FILL IN + #[default] + #[serde(rename = "preferred")] + Preferred, + /// TO FILL IN + #[serde(rename = "discouraged")] + Discouraged_DO_NOT_USE, +} + +/// Relying Party Entity +#[derive(Debug, Serialize, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RelyingParty { + /// The name of the relying party. + pub name: String, + /// The id of the relying party. + pub id: String, + // Note: "icon" is deprecated: https://github.com/w3c/webauthn/pull/1337 +} + +/// User Entity +#[derive(Debug, Serialize, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct User { + /// The user's id in base64 form. This MUST be a unique id, and + /// must NOT contain personally identifying information, as this value can NEVER + /// be changed. If in doubt, use a UUID. + pub id: Base64UrlSafeData, + /// A detailed name for the account, such as an email address. This value + /// **can** change, so **must not** be used as a primary key. + pub name: String, + /// The user's preferred name for display. This value **can** change, so + /// **must not** be used as a primary key. + pub display_name: String, + // Note: "icon" is deprecated: https://github.com/w3c/webauthn/pull/1337 +} + +/// Public key cryptographic parameters +#[derive(Debug, Serialize, Clone, Deserialize)] +pub struct PubKeyCredParams { + /// The type of public-key credential. + #[serde(rename = "type")] + pub type_: String, + /// The algorithm in use defined by COSE. + pub alg: i64, +} + +/// <https://www.w3.org/TR/webauthn/#enumdef-attestationconveyancepreference> +#[derive(Debug, Serialize, Clone, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum AttestationConveyancePreference { + /// Do not request attestation. + /// <https://www.w3.org/TR/webauthn/#dom-attestationconveyancepreference-none> + None, + + /// Request attestation in a semi-anonymized form. + /// <https://www.w3.org/TR/webauthn/#dom-attestationconveyancepreference-indirect> + Indirect, + + /// Request attestation in a direct form. + /// <https://www.w3.org/TR/webauthn/#dom-attestationconveyancepreference-direct> + Direct, +} + +/// <https://www.w3.org/TR/webauthn/#enumdef-authenticatortransport> +#[derive(Debug, Serialize, Clone, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +#[allow(unused)] +pub enum AuthenticatorTransport { + /// <https://www.w3.org/TR/webauthn/#dom-authenticatortransport-usb> + Usb, + /// <https://www.w3.org/TR/webauthn/#dom-authenticatortransport-nfc> + Nfc, + /// <https://www.w3.org/TR/webauthn/#dom-authenticatortransport-ble> + Ble, + /// <https://www.w3.org/TR/webauthn/#dom-authenticatortransport-internal> + Internal, + /// Hybrid transport, formerly caBLE. Part of the level 3 draft specification. + /// <https://w3c.github.io/webauthn/#dom-authenticatortransport-hybrid> + Hybrid, + /// Test transport; used for Windows 10. + Test, +} + +impl FromStr for AuthenticatorTransport { + type Err = (); + fn from_str(s: &str) -> Result<Self, Self::Err> { + use AuthenticatorTransport::*; + + // "internal" is longest (8 chars) + if s.len() > 8 { + return Err(()); + } + + Ok(match s.to_ascii_lowercase().as_str() { + "usb" => Usb, + "nfc" => Nfc, + "ble" => Ble, + "internal" => Internal, + "test" => Test, + "hybrid" => Hybrid, + &_ => return Err(()), + }) + } +} + +impl ToString for AuthenticatorTransport { + fn to_string(&self) -> String { + use AuthenticatorTransport::*; + match self { + Usb => "usb", + Nfc => "nfc", + Ble => "ble", + Internal => "internal", + Test => "test", + Hybrid => "hybrid", + } + .to_string() + } +} + +/// <https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialdescriptor> +#[derive(Debug, Serialize, Clone, Deserialize)] +pub struct PublicKeyCredentialDescriptor { + /// The type of credential + #[serde(rename = "type")] + pub type_: String, + /// The credential id. + pub id: Base64UrlSafeData, + /// The allowed transports for this credential. Note this is a hint, and is NOT + /// enforced. + #[serde(skip_serializing_if = "Option::is_none")] + pub transports: Option<Vec<AuthenticatorTransport>>, +} + +/// The authenticator attachment hint. This is NOT enforced, and is only used +/// to help a user select a relevant authenticator type. +/// +/// <https://www.w3.org/TR/webauthn/#attachment> +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum AuthenticatorAttachment { + /// Request a device that is part of the machine aka inseperable. + /// <https://www.w3.org/TR/webauthn/#attachment> + #[serde(rename = "platform")] + Platform, + /// Request a device that can be seperated from the machine aka an external token. + /// <https://www.w3.org/TR/webauthn/#attachment> + #[serde(rename = "cross-platform")] + CrossPlatform, +} + +/// <https://www.w3.org/TR/webauthn/#dictdef-authenticatorselectioncriteria> +#[derive(Debug, Serialize, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthenticatorSelectionCriteria { + /// How the authenticator should be attached to the client machine. + /// Note this is only a hint. It is not enforced in anyway shape or form. + /// <https://www.w3.org/TR/webauthn/#attachment> + #[serde(skip_serializing_if = "Option::is_none")] + pub authenticator_attachment: Option<AuthenticatorAttachment>, + + /// Hint to the credential to create a resident key. Note this can not be enforced + /// or validated, so the authenticator may choose to ignore this parameter. + /// <https://www.w3.org/TR/webauthn/#resident-credential> + pub require_resident_key: bool, + + /// The user verification level to request during registration. Depending on if this + /// authenticator provides verification may affect future interactions as this is + /// associated to the credential during registration. + pub user_verification: UserVerificationPolicy, +} + +/// A descriptor of a credential that can be used. +#[derive(Debug, Serialize, Clone, Deserialize)] +pub struct AllowCredentials { + #[serde(rename = "type")] + /// The type of credential. + pub type_: String, + /// The id of the credential. + pub id: Base64UrlSafeData, + /// <https://www.w3.org/TR/webauthn/#transport> + /// may be usb, nfc, ble, internal + #[serde(skip_serializing_if = "Option::is_none")] + pub transports: Option<Vec<AuthenticatorTransport>>, +} + +/// The data collected and hashed in the operation. +/// <https://www.w3.org/TR/webauthn-2/#dictdef-collectedclientdata> +#[derive(Debug, Serialize, Clone, Deserialize)] +pub struct CollectedClientData { + /// The credential type + #[serde(rename = "type")] + pub type_: String, + /// The challenge. + pub challenge: Base64UrlSafeData, + /// The rp origin as the browser understood it. + pub origin: url::Url, + /// The inverse of the sameOriginWithAncestors argument value that was + /// passed into the internal method. + #[serde(rename = "crossOrigin", skip_serializing_if = "Option::is_none")] + pub cross_origin: Option<bool>, + /// tokenBinding. + #[serde(rename = "tokenBinding")] + pub token_binding: Option<TokenBinding>, + /// This struct be extended, so it's important to be tolerant of unknown + /// keys. + #[serde(flatten)] + pub unknown_keys: BTreeMap<String, serde_json::value::Value>, +} + +/* +impl TryFrom<&[u8]> for CollectedClientData { + type Error = WebauthnError; + fn try_from(data: &[u8]) -> Result<CollectedClientData, WebauthnError> { + let ccd: CollectedClientData = + serde_json::from_slice(data).map_err(WebauthnError::ParseJSONFailure)?; + Ok(ccd) + } +} +*/ + +/// Token binding +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct TokenBinding { + /// status + pub status: String, + /// id + pub id: Option<String>, +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use crate::AuthenticatorTransport; + + #[test] + fn test_authenticator_transports() { + let cases: [(&str, AuthenticatorTransport); 6] = [ + ("ble", AuthenticatorTransport::Ble), + ("internal", AuthenticatorTransport::Internal), + ("nfc", AuthenticatorTransport::Nfc), + ("usb", AuthenticatorTransport::Usb), + ("test", AuthenticatorTransport::Test), + ("hybrid", AuthenticatorTransport::Hybrid), + ]; + + for (s, t) in cases { + assert_eq!( + t, + AuthenticatorTransport::from_str(s).expect("unknown authenticatorTransport") + ); + assert_eq!(s, AuthenticatorTransport::to_string(&t)); + } + + assert!(AuthenticatorTransport::from_str("fake fake").is_err()); + } +}