diff --git a/compat_tester/webauthn-rs-demo-shared/Cargo.toml b/compat_tester/webauthn-rs-demo-shared/Cargo.toml index 8cefbbcf..831e845e 100644 --- a/compat_tester/webauthn-rs-demo-shared/Cargo.toml +++ b/compat_tester/webauthn-rs-demo-shared/Cargo.toml @@ -8,6 +8,7 @@ rust-version = "1.70.0" core = ["webauthn-rs-core"] [dependencies] +base64urlsafedata.workspace = true serde.workspace = true webauthn-rs-core = { workspace = true, optional = true } diff --git a/compat_tester/webauthn-rs-demo-shared/src/lib.rs b/compat_tester/webauthn-rs-demo-shared/src/lib.rs index 2949d4a6..2a5b307d 100644 --- a/compat_tester/webauthn-rs-demo-shared/src/lib.rs +++ b/compat_tester/webauthn-rs-demo-shared/src/lib.rs @@ -1,11 +1,11 @@ #![deny(warnings)] #![warn(unused_extern_crates)] +use base64urlsafedata::HumanBinaryData; use serde::{Deserialize, Serialize}; #[cfg(feature = "core")] use webauthn_rs_core::error::WebauthnError; -pub use webauthn_rs_core::proto::CredentialID; pub use webauthn_rs_proto::{ AttestationConveyancePreference, AuthenticationExtensions, AuthenticatorAttachment, COSEAlgorithm, CreationChallengeResponse, CredProtect, CredentialProtectionPolicy, ExtnState, @@ -14,6 +14,8 @@ pub use webauthn_rs_proto::{ UserVerificationPolicy, }; +pub type CredentialID = HumanBinaryData; + #[derive(Serialize, Deserialize, Debug, Clone, Copy)] pub enum AttestationLevel { None, diff --git a/compat_tester/webauthn-rs-demo/pkg/webauthn_rs_demo_wasm.d.ts b/compat_tester/webauthn-rs-demo/pkg/webauthn_rs_demo_wasm.d.ts index 00c9d87d..ac219efa 100644 --- a/compat_tester/webauthn-rs-demo/pkg/webauthn_rs_demo_wasm.d.ts +++ b/compat_tester/webauthn-rs-demo/pkg/webauthn_rs_demo_wasm.d.ts @@ -12,9 +12,9 @@ export interface InitOutput { readonly __wbindgen_malloc: (a: number, b: number) => number; readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; readonly __wbindgen_export_2: WebAssembly.Table; - readonly wasm_bindgen__convert__closures__invoke1_mut_ref__h015a6d4beac911b9: (a: number, b: number, c: number) => void; - readonly wasm_bindgen__convert__closures__invoke1__h9385f9b96e74d99b: (a: number, b: number, c: number) => void; - readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hcb3dcc208685cd98: (a: number, b: number, c: number) => void; + readonly wasm_bindgen__convert__closures__invoke1_mut_ref__h0f42758389f9ef59: (a: number, b: number, c: number) => void; + readonly wasm_bindgen__convert__closures__invoke1__h3626c9e41e52d750: (a: number, b: number, c: number) => void; + readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h458d161319ed8eb6: (a: number, b: number, c: number) => void; readonly __wbindgen_add_to_stack_pointer: (a: number) => number; readonly __wbindgen_exn_store: (a: number) => void; readonly __wbindgen_free: (a: number, b: number, c: number) => void; diff --git a/compat_tester/webauthn-rs-demo/pkg/webauthn_rs_demo_wasm.js b/compat_tester/webauthn-rs-demo/pkg/webauthn_rs_demo_wasm.js index ac22140b..2fa04732 100644 --- a/compat_tester/webauthn-rs-demo/pkg/webauthn_rs_demo_wasm.js +++ b/compat_tester/webauthn-rs-demo/pkg/webauthn_rs_demo_wasm.js @@ -232,7 +232,7 @@ function addBorrowedObject(obj) { } function __wbg_adapter_48(arg0, arg1, arg2) { try { - wasm.wasm_bindgen__convert__closures__invoke1_mut_ref__h015a6d4beac911b9(arg0, arg1, addBorrowedObject(arg2)); + wasm.wasm_bindgen__convert__closures__invoke1_mut_ref__h0f42758389f9ef59(arg0, arg1, addBorrowedObject(arg2)); } finally { heap[stack_pointer++] = undefined; } @@ -260,11 +260,11 @@ function makeClosure(arg0, arg1, dtor, f) { return real; } function __wbg_adapter_51(arg0, arg1, arg2) { - wasm.wasm_bindgen__convert__closures__invoke1__h9385f9b96e74d99b(arg0, arg1, addHeapObject(arg2)); + wasm.wasm_bindgen__convert__closures__invoke1__h3626c9e41e52d750(arg0, arg1, addHeapObject(arg2)); } function __wbg_adapter_54(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hcb3dcc208685cd98(arg0, arg1, addHeapObject(arg2)); + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h458d161319ed8eb6(arg0, arg1, addHeapObject(arg2)); } /** @@ -944,16 +944,16 @@ function __wbg_get_imports() { const ret = wasm.memory; return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper1109 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 529, __wbg_adapter_48); + imports.wbg.__wbindgen_closure_wrapper1147 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 541, __wbg_adapter_48); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper1612 = function(arg0, arg1, arg2) { - const ret = makeClosure(arg0, arg1, 644, __wbg_adapter_51); + imports.wbg.__wbindgen_closure_wrapper1653 = function(arg0, arg1, arg2) { + const ret = makeClosure(arg0, arg1, 654, __wbg_adapter_51); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper1703 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 659, __wbg_adapter_54); + imports.wbg.__wbindgen_closure_wrapper1743 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 669, __wbg_adapter_54); return addHeapObject(ret); }; diff --git a/compat_tester/webauthn-rs-demo/pkg/webauthn_rs_demo_wasm_bg.wasm b/compat_tester/webauthn-rs-demo/pkg/webauthn_rs_demo_wasm_bg.wasm index b713c91a..0b490add 100644 Binary files a/compat_tester/webauthn-rs-demo/pkg/webauthn_rs_demo_wasm_bg.wasm and b/compat_tester/webauthn-rs-demo/pkg/webauthn_rs_demo_wasm_bg.wasm differ diff --git a/compat_tester/webauthn-rs-demo/pkg/webauthn_rs_demo_wasm_bg.wasm.d.ts b/compat_tester/webauthn-rs-demo/pkg/webauthn_rs_demo_wasm_bg.wasm.d.ts index 9d381aa5..06d0d10b 100644 --- a/compat_tester/webauthn-rs-demo/pkg/webauthn_rs_demo_wasm_bg.wasm.d.ts +++ b/compat_tester/webauthn-rs-demo/pkg/webauthn_rs_demo_wasm_bg.wasm.d.ts @@ -5,9 +5,9 @@ export function run_app(a: number): void; export function __wbindgen_malloc(a: number, b: number): number; export function __wbindgen_realloc(a: number, b: number, c: number, d: number): number; export const __wbindgen_export_2: WebAssembly.Table; -export function wasm_bindgen__convert__closures__invoke1_mut_ref__h015a6d4beac911b9(a: number, b: number, c: number): void; -export function wasm_bindgen__convert__closures__invoke1__h9385f9b96e74d99b(a: number, b: number, c: number): void; -export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hcb3dcc208685cd98(a: number, b: number, c: number): void; +export function wasm_bindgen__convert__closures__invoke1_mut_ref__h0f42758389f9ef59(a: number, b: number, c: number): void; +export function wasm_bindgen__convert__closures__invoke1__h3626c9e41e52d750(a: number, b: number, c: number): void; +export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h458d161319ed8eb6(a: number, b: number, c: number): void; export function __wbindgen_add_to_stack_pointer(a: number): number; export function __wbindgen_exn_store(a: number): void; export function __wbindgen_free(a: number, b: number, c: number): void; diff --git a/compat_tester/webauthn-rs-demo/src/actors.rs b/compat_tester/webauthn-rs-demo/src/actors.rs index 46a1e2d7..e37b6157 100644 --- a/compat_tester/webauthn-rs-demo/src/actors.rs +++ b/compat_tester/webauthn-rs-demo/src/actors.rs @@ -367,11 +367,15 @@ impl WebauthnActor { .ok_or(WebauthnError::CredentialNotFound)?; self.wan - .generate_challenge_authenticate(vec![cred], uv, extensions, None) + .new_challenge_authenticate_builder(vec![cred], uv) + .map(|builder| builder.extensions(extensions)) + .and_then(|b| self.wan.generate_challenge_authenticate(b)) } None => self .wan - .generate_challenge_authenticate(creds, None, extensions, None), + .new_challenge_authenticate_builder(creds, None) + .map(|builder| builder.extensions(extensions)) + .and_then(|b| self.wan.generate_challenge_authenticate(b)), }?; debug!("complete ChallengeAuthenticate -> {:?}", acr); diff --git a/sshkey-attest/src/lib.rs b/sshkey-attest/src/lib.rs index a4a27d3e..9ad21d90 100644 --- a/sshkey-attest/src/lib.rs +++ b/sshkey-attest/src/lib.rs @@ -25,15 +25,14 @@ use uuid::Uuid; pub use webauthn_rs_core::error::WebauthnError; use webauthn_rs_core::{ attestation::{ - assert_packed_attest_req, validate_extension, verify_attestation_ca_chain, - AttestationFormat, FidoGenCeAaguid, + assert_packed_attest_req, validate_extension, verify_attestation_ca_chain, FidoGenCeAaguid, }, crypto::{compute_sha256, verify_signature}, internals::AuthenticatorData, proto::{ - AttestationCaList, AttestationMetadata, COSEAlgorithm, COSEKey, COSEKeyType, - CredentialProtectionPolicy, ExtnState, ParsedAttestation, ParsedAttestationData, - RegisteredExtensions, Registration, + AttestationCaList, AttestationFormat, AttestationMetadata, COSEAlgorithm, COSEKey, + COSEKeyType, CredentialProtectionPolicy, ExtnState, ParsedAttestation, + ParsedAttestationData, RegisteredExtensions, Registration, }, }; diff --git a/sshkey-attest/src/proto.rs b/sshkey-attest/src/proto.rs index cf81c0a0..3927a8ab 100644 --- a/sshkey-attest/src/proto.rs +++ b/sshkey-attest/src/proto.rs @@ -1,8 +1,7 @@ //! Serialisable formats of attested ssh keys use serde::{Deserialize, Serialize}; -use webauthn_rs_core::attestation::AttestationFormat; -use webauthn_rs_core::proto::{ParsedAttestation, RegisteredExtensions}; +use webauthn_rs_core::proto::{AttestationFormat, ParsedAttestation, RegisteredExtensions}; pub use sshkeys::PublicKey; diff --git a/webauthn-authenticator-rs/examples/authenticate.rs b/webauthn-authenticator-rs/examples/authenticate.rs index efa9c89f..05587a8a 100644 --- a/webauthn-authenticator-rs/examples/authenticate.rs +++ b/webauthn-authenticator-rs/examples/authenticate.rs @@ -280,16 +280,15 @@ async fn main() { .connect_provider(CableRequestType::GetAssertion, &ui) .await; let (chal, auth_state) = wan - .generate_challenge_authenticate( - vec![cred.clone()], - None, - Some(RequestAuthenticationExtensions { + .new_challenge_authenticate_builder(vec![cred.clone()], None) + .map(|builder| { + builder.extensions(Some(RequestAuthenticationExtensions { appid: Some("example.app.id".to_string()), uvm: None, hmac_get_secret: None, - }), - None, - ) + })) + }) + .and_then(|b| wan.generate_challenge_authenticate(b)) .unwrap(); let r = u diff --git a/webauthn-authenticator-rs/src/authenticator_hashed.rs b/webauthn-authenticator-rs/src/authenticator_hashed.rs index 600d595e..30fa05a3 100644 --- a/webauthn-authenticator-rs/src/authenticator_hashed.rs +++ b/webauthn-authenticator-rs/src/authenticator_hashed.rs @@ -151,7 +151,9 @@ pub fn perform_register_with_request( timeout: Some(timeout_ms), exclude_credentials: Some(request.exclude_list), // TODO + hints: None, attestation: None, + attestation_formats: None, authenticator_selection: None, extensions: None, }; @@ -192,6 +194,7 @@ pub fn perform_auth_with_request( rp_id: request.rp_id, allow_credentials: request.allow_list, // TODO + hints: None, user_verification: webauthn_rs_proto::UserVerificationPolicy::Preferred, extensions: None, }; diff --git a/webauthn-authenticator-rs/src/softpasskey.rs b/webauthn-authenticator-rs/src/softpasskey.rs index 54d8c1f7..e1cac89a 100644 --- a/webauthn-authenticator-rs/src/softpasskey.rs +++ b/webauthn-authenticator-rs/src/softpasskey.rs @@ -568,7 +568,8 @@ mod tests { let cred = wan.register_credential(&r, ®_state, None).unwrap(); let (chal, auth_state) = wan - .generate_challenge_authenticate(vec![cred], None, None, None) + .new_challenge_authenticate_builder(vec![cred], None) + .and_then(|b| wan.generate_challenge_authenticate(b)) .unwrap(); let r = wa diff --git a/webauthn-authenticator-rs/src/softtoken.rs b/webauthn-authenticator-rs/src/softtoken.rs index 1262b4e4..8ede1129 100644 --- a/webauthn-authenticator-rs/src/softtoken.rs +++ b/webauthn-authenticator-rs/src/softtoken.rs @@ -928,7 +928,8 @@ mod tests { info!("Credential -> {:?}", cred); let (chal, auth_state) = wan - .generate_challenge_authenticate(vec![cred], None, None, None) + .new_challenge_authenticate_builder(vec![cred], None) + .and_then(|b| wan.generate_challenge_authenticate(b)) .unwrap(); let r = wa @@ -1014,7 +1015,8 @@ mod tests { let mut wa = WebauthnAuthenticator::new(soft_token); let (chal, auth_state) = wan - .generate_challenge_authenticate(vec![cred], None, None, None) + .new_challenge_authenticate_builder(vec![cred], None) + .and_then(|b| wan.generate_challenge_authenticate(b)) .unwrap(); let r = wa diff --git a/webauthn-rs-core/src/attestation.rs b/webauthn-rs-core/src/attestation.rs index 66254737..35445700 100644 --- a/webauthn-rs-core/src/attestation.rs +++ b/webauthn-rs-core/src/attestation.rs @@ -341,52 +341,6 @@ where }) } -/// The type of attestation on the credential -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash)] -pub enum AttestationFormat { - /// Packed attestation - Packed, - /// TPM attestation (like Micrsoft) - Tpm, - /// Android hardware attestation - AndroidKey, - /// Older Android Safety Net - AndroidSafetyNet, - /// Old U2F attestation type - FIDOU2F, - /// Apple touchID/faceID - AppleAnonymous, - /// No attestation - None, -} - -impl AttestationFormat { - /// Only a small number of devices correctly report their transports. These are - /// limited to attested devices, and exclusively packed (fido2) and tpms. Most - /// other devices/browsers will get this wrong, meaning that authentication will - /// fail or not offer the correct transports to the user. - pub(crate) fn transports_valid(&self) -> bool { - matches!(self, AttestationFormat::Packed | AttestationFormat::Tpm) - } -} - -impl TryFrom<&str> for AttestationFormat { - type Error = WebauthnError; - - fn try_from(a: &str) -> Result { - match a { - "packed" => Ok(AttestationFormat::Packed), - "tpm" => Ok(AttestationFormat::Tpm), - "android-key" => Ok(AttestationFormat::AndroidKey), - "android-safetynet" => Ok(AttestationFormat::AndroidSafetyNet), - "fido-u2f" => Ok(AttestationFormat::FIDOU2F), - "apple" => Ok(AttestationFormat::AppleAnonymous), - "none" => Ok(AttestationFormat::None), - _ => Err(WebauthnError::AttestationNotSupported), - } - } -} - // Perform the Verification procedure for 8.2. Packed Attestation Statement Format // https://w3c.github.io/webauthn/#sctn-packed-attestation pub(crate) fn verify_packed_attestation( @@ -1432,9 +1386,11 @@ pub fn verify_attestation_ca_chain<'a>( ParsedAttestationData::AnonCa(chain) => chain, ParsedAttestationData::Self_ | ParsedAttestationData::None => { // nothing to check + debug!("No attestation present"); return Ok(None); } ParsedAttestationData::ECDAA | ParsedAttestationData::Uncertain => { + debug!("attestation is an unsupported format"); return Err(WebauthnError::AttestationNotVerifiable); } }; diff --git a/webauthn-rs-core/src/core.rs b/webauthn-rs-core/src/core.rs index db7fa518..093c35b5 100644 --- a/webauthn-rs-core/src/core.rs +++ b/webauthn-rs-core/src/core.rs @@ -22,7 +22,7 @@ use url::Url; use crate::attestation::{ verify_android_key_attestation, verify_android_safetynet_attestation, verify_apple_anonymous_attestation, verify_attestation_ca_chain, verify_fidou2f_attestation, - verify_packed_attestation, verify_tpm_attestation, AttestationFormat, + verify_packed_attestation, verify_tpm_attestation, }; use crate::constants::CHALLENGE_SIZE_BYTES; use crate::crypto::compute_sha256; @@ -74,7 +74,19 @@ pub struct ChallengeRegisterBuilder { credential_algorithms: Vec, require_resident_key: bool, authenticator_attachment: Option, + attestation_formats: Option>, reject_synchronised_authenticators: bool, + hints: Option>, +} + +/// A builder allowing customisation of a client authentication challenge. +#[derive(Debug)] +pub struct ChallengeAuthenticateBuilder { + creds: Vec, + policy: UserVerificationPolicy, + extensions: Option, + allow_backup_eligible_upgrade: bool, + hints: Option>, } impl ChallengeRegisterBuilder { @@ -128,6 +140,44 @@ impl ChallengeRegisterBuilder { self.reject_synchronised_authenticators = value; self } + + /// Add the set of hints for which public keys may satisfy this request. + pub fn hints(mut self, hints: Option>) -> Self { + self.hints = hints; + self + } + + /// Add the set of attestation formats that will be accepted in this operation + pub fn attestation_formats( + mut self, + attestation_formats: Option>, + ) -> Self { + self.attestation_formats = attestation_formats; + self + } +} + +impl ChallengeAuthenticateBuilder { + /// Define extensions to be requested in the request. Defaults to None. + pub fn extensions(mut self, value: Option) -> Self { + self.extensions = value; + self + } + + /// Allow authenticators to modify their backup eligibility. This can occur + /// where some formerly hardware bound devices become roaming ones. If in + /// double leave this value as default (false, rejects credentials that + /// post-create move from single device to roaming). + pub fn allow_backup_eligible_upgrade(mut self, value: bool) -> Self { + self.allow_backup_eligible_upgrade = value; + self + } + + /// Add the set of hints for which public keys may satisfy this request. + pub fn hints(mut self, hints: Option>) -> Self { + self.hints = hints; + self + } } impl WebauthnCore { @@ -205,6 +255,8 @@ impl WebauthnCore { require_resident_key: Default::default(), authenticator_attachment: Default::default(), reject_synchronised_authenticators: Default::default(), + hints: Default::default(), + attestation_formats: Default::default(), }) } @@ -236,7 +288,9 @@ impl WebauthnCore { credential_algorithms, require_resident_key, authenticator_attachment, + attestation_formats, reject_synchronised_authenticators, + hints, } = challenge_builder; let challenge = self.generate_challenge(); @@ -270,6 +324,7 @@ impl WebauthnCore { }) .collect(), timeout: Some(timeout_millis), + hints, attestation: Some(attestation), exclude_credentials: exclude_credentials.as_ref().map(|creds| { creds @@ -289,6 +344,7 @@ impl WebauthnCore { user_verification: policy, }), extensions: extensions.clone(), + attestation_formats, }, }; @@ -529,7 +585,8 @@ impl WebauthnCore { // https://w3c.github.io/webauthn-3/#none-attestation // https://www.w3.org/TR/webauthn-3/#sctn-apple-anonymous-attestation // - let attest_format = AttestationFormat::try_from(data.attestation_object.fmt.as_str())?; + let attest_format = AttestationFormat::try_from(data.attestation_object.fmt.as_str()) + .map_err(|()| WebauthnError::AttestationNotSupported)?; // Verify that attStmt is a correct attestation statement, conveying a valid attestation // signature, by using the attestation statement format fmt’s verification procedure given @@ -626,7 +683,10 @@ impl WebauthnCore { // but in this case because we have the ca_list and none was the result (which happens) // in some cases, we need to map that through. But we need verify_attesation_ca_chain // to still return these option types due to re-attestation in the future. - let ca_crt = ca_crt.ok_or(WebauthnError::AttestationNotVerifiable)?; + let ca_crt = ca_crt.ok_or_else(|| { + warn!("device attested with a certificate not present in attestation ca chain"); + WebauthnError::AttestationNotVerifiable + })?; Some(ca_crt) } else { None @@ -878,21 +938,17 @@ impl WebauthnCore { Ok(data.authenticator_data) } - /// Authenticate a set of credentials allowing the user verification policy to be set. - /// If no credentials are provided, this will start a discoverable credential authentication. + /// Generate a new challenge builder for client authentication. This is the first + /// step in authentication of a credential. This function will return an + /// authentication builder allowing you to customise the parameters that will be + /// sent to the client. /// - /// Policy defines the require UserVerification policy. This MUST be consistent with the - /// credentials being used in the authentication. - /// - /// `allow_backup_eligible_upgrade` allows rejecting credentials whos backup eligibility - /// has changed between registration and authentication. - pub fn generate_challenge_authenticate( + /// If creds is an empty `Vec` this implies a discoverable authentication attempt. + pub fn new_challenge_authenticate_builder( &self, creds: Vec, policy: Option, - extensions: Option, - allow_backup_eligible_upgrade: Option, - ) -> Result<(RequestChallengeResponse, AuthenticationState), WebauthnError> { + ) -> Result { let policy = if let Some(policy) = policy { policy } else { @@ -910,8 +966,36 @@ impl WebauthnCore { policy }; - // Defaults to false. - let allow_backup_eligible_upgrade = allow_backup_eligible_upgrade.unwrap_or_default(); + Ok(ChallengeAuthenticateBuilder { + creds, + policy, + extensions: Default::default(), + allow_backup_eligible_upgrade: Default::default(), + hints: Default::default(), + }) + } + + /// Generate a new challenge for client authentication from the parameters defined by the + /// [ChallengeAuthenticateBuilder]. + /// + /// This function will return: + /// + /// * a [RequestChallengeResponse], which is sent to the client (and can be serialised as JSON). + /// A web application would then pass the structure to the browser's navigator.credentials.create() API to trigger authentication. + /// + /// * an [AuthenticationState], which must be persisted on the server side. Your application + /// must associate the state with a private session ID, to prevent use in other sessions. + pub fn generate_challenge_authenticate( + &self, + challenge_builder: ChallengeAuthenticateBuilder, + ) -> Result<(RequestChallengeResponse, AuthenticationState), WebauthnError> { + let ChallengeAuthenticateBuilder { + creds, + policy, + extensions, + allow_backup_eligible_upgrade, + hints, + } = challenge_builder; let chal = self.generate_challenge(); @@ -941,6 +1025,7 @@ impl WebauthnCore { allow_credentials: ac, user_verification: policy, extensions, + hints, }, mediation: None, }; @@ -1171,7 +1256,6 @@ impl WebauthnCore { mod tests { #![allow(clippy::panic)] - use crate::attestation::AttestationFormat; use crate::constants::CHALLENGE_SIZE_BYTES; use crate::core::{CreationChallengeResponse, RegistrationState, WebauthnError}; use crate::internals::*; @@ -2813,7 +2897,7 @@ mod tests { // Ensure we get a bad result. assert!( - wan.generate_challenge_authenticate(creds.clone(), None, None, None) + wan.new_challenge_authenticate_builder(creds.clone(), None) .unwrap_err() == WebauthnError::InconsistentUserVerificationPolicy ); @@ -2838,7 +2922,11 @@ mod tests { .unwrap(); } - let r = wan.generate_challenge_authenticate(creds.clone(), None, None, None); + let builder = wan + .new_challenge_authenticate_builder(creds.clone(), None) + .expect("Unable to create authenticate builder"); + + let r = wan.generate_challenge_authenticate(builder); debug!("{:?}", r); assert!(r.is_ok()); @@ -2862,7 +2950,12 @@ mod tests { .unwrap(); } - let r = wan.generate_challenge_authenticate(creds.clone(), None, None, None); + let builder = wan + .new_challenge_authenticate_builder(creds.clone(), None) + .expect("Unable to create authenticate builder"); + + let r = wan.generate_challenge_authenticate(builder); + debug!("{:?}", r); assert!(r.is_ok()); } diff --git a/webauthn-rs-core/src/interface.rs b/webauthn-rs-core/src/interface.rs index 016c0071..e0b2aec5 100644 --- a/webauthn-rs-core/src/interface.rs +++ b/webauthn-rs-core/src/interface.rs @@ -1,7 +1,7 @@ //! Extended Structs and representations for Webauthn Operations. These types are designed //! to allow persistence and should not change. -use crate::attestation::{verify_attestation_ca_chain, AttestationFormat}; +use crate::attestation::verify_attestation_ca_chain; use crate::error::*; pub use crate::internals::AttestationObject; use std::fmt; diff --git a/webauthn-rs-core/src/internals.rs b/webauthn-rs-core/src/internals.rs index 1d210e4d..0bed2b39 100644 --- a/webauthn-rs-core/src/internals.rs +++ b/webauthn-rs-core/src/internals.rs @@ -1,7 +1,6 @@ //! Internal structures for parsing webauthn registrations and challenges. This *may* change //! at anytime and should not be relied on in your library. -use crate::attestation::AttestationFormat; use crate::error::WebauthnError; use crate::proto::*; use serde::Deserialize; @@ -188,7 +187,9 @@ impl Credential { let backup_eligible = auth_data.backup_eligible; let backup_state = auth_data.backup_state; - let transports = if attestation_format.transports_valid() { + let transports = if attestation_format == AttestationFormat::Packed + || attestation_format == AttestationFormat::Tpm + { transports.clone() } else { None diff --git a/webauthn-rs-proto/src/attest.rs b/webauthn-rs-proto/src/attest.rs index b112a971..bb015978 100644 --- a/webauthn-rs-proto/src/attest.rs +++ b/webauthn-rs-proto/src/attest.rs @@ -27,10 +27,6 @@ pub struct PublicKeyCredentialCreationOptions { #[serde(skip_serializing_if = "Option::is_none")] pub timeout: Option, - /// The requested attestation level from the device. - #[serde(skip_serializing_if = "Option::is_none")] - pub attestation: Option, - /// Credential ID's that are excluded from being able to be registered. #[serde(skip_serializing_if = "Option::is_none")] pub exclude_credentials: Option>, @@ -39,6 +35,18 @@ pub struct PublicKeyCredentialCreationOptions { #[serde(skip_serializing_if = "Option::is_none")] pub authenticator_selection: Option, + /// Hints defining which credentials may be used in this operation. + #[serde(skip_serializing_if = "Option::is_none")] + pub hints: Option>, + + /// The requested attestation level from the device. + #[serde(skip_serializing_if = "Option::is_none")] + pub attestation: Option, + + /// The list of attestation formats that the RP will accept. + #[serde(skip_serializing_if = "Option::is_none")] + pub attestation_formats: Option>, + /// Non-standard extensions that may be used by the browser/authenticator. #[serde(skip_serializing_if = "Option::is_none")] pub extensions: Option, diff --git a/webauthn-rs-proto/src/auth.rs b/webauthn-rs-proto/src/auth.rs index 309bb7fb..009d071f 100644 --- a/webauthn-rs-proto/src/auth.rs +++ b/webauthn-rs-proto/src/auth.rs @@ -25,6 +25,11 @@ pub struct PublicKeyCredentialRequestOptions { pub allow_credentials: Vec, /// The verification policy the browser will request. pub user_verification: UserVerificationPolicy, + + /// Hints defining which types credentials may be used in this operation. + #[serde(skip_serializing_if = "Option::is_none")] + pub hints: Option>, + /// extensions. #[serde(skip_serializing_if = "Option::is_none")] pub extensions: Option, diff --git a/webauthn-rs-proto/src/options.rs b/webauthn-rs-proto/src/options.rs index 1cdeba34..c2801082 100644 --- a/webauthn-rs-proto/src/options.rs +++ b/webauthn-rs-proto/src/options.rs @@ -179,6 +179,56 @@ impl ToString for AuthenticatorTransport { } } +/// The type of attestation on the credential +/// +/// +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub enum AttestationFormat { + /// Packed attestation + #[serde(rename = "packed", alias = "Packed")] + Packed, + /// TPM attestation (like Microsoft) + #[serde(rename = "tpm", alias = "Tpm", alias = "TPM")] + Tpm, + /// Android hardware attestation + #[serde(rename = "android-key", alias = "AndroidKey")] + AndroidKey, + /// Older Android Safety Net + #[serde( + rename = "android-safetynet", + alias = "AndroidSafetyNet", + alias = "AndroidSafetynet" + )] + AndroidSafetyNet, + /// Old U2F attestation type + #[serde(rename = "fido-u2f", alias = "FIDOU2F")] + FIDOU2F, + /// Apple touchID/faceID + #[serde(rename = "apple", alias = "AppleAnonymous")] + AppleAnonymous, + /// No attestation + #[serde(rename = "none", alias = "None")] + None, +} + +impl TryFrom<&str> for AttestationFormat { + type Error = (); + + fn try_from(a: &str) -> Result { + match a { + "packed" => Ok(AttestationFormat::Packed), + "tpm" => Ok(AttestationFormat::Tpm), + "android-key" => Ok(AttestationFormat::AndroidKey), + "android-safetynet" => Ok(AttestationFormat::AndroidSafetyNet), + "fido-u2f" => Ok(AttestationFormat::FIDOU2F), + "apple" => Ok(AttestationFormat::AppleAnonymous), + "none" => Ok(AttestationFormat::None), + // _ => Err(WebauthnError::AttestationNotSupported), + _ => Err(()), + } + } +} + /// #[derive(Debug, Serialize, Clone, Deserialize, PartialEq, Eq)] pub struct PublicKeyCredentialDescriptor { @@ -209,6 +259,21 @@ pub enum AuthenticatorAttachment { CrossPlatform, } +/// A hint as to the class of device that is expected to fufil this operation. +/// +/// +#[derive(Debug, Serialize, Clone, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +#[allow(unused)] +pub enum PublicKeyCredentialHints { + /// The credential is a removable security key + SecurityKey, + /// The credential is a platform authenticator + ClientDevice, + /// The credential will come from an external device + Hybrid, +} + /// The Relying Party's requirements for client-side discoverable credentials. /// /// diff --git a/webauthn-rs/src/lib.rs b/webauthn-rs/src/lib.rs index 3347818c..3874429e 100644 --- a/webauthn-rs/src/lib.rs +++ b/webauthn-rs/src/lib.rs @@ -211,12 +211,12 @@ pub mod prelude { pub use base64urlsafedata::Base64UrlSafeData; pub use url::Url; pub use uuid::Uuid; - pub use webauthn_rs_core::attestation::AttestationFormat; pub use webauthn_rs_core::error::{WebauthnError, WebauthnResult}; #[cfg(feature = "danger-credential-internals")] pub use webauthn_rs_core::proto::Credential; pub use webauthn_rs_core::proto::{ - AttestationCa, AttestationCaList, AttestationCaListBuilder, AuthenticatorAttachment, + AttestationCa, AttestationCaList, AttestationCaListBuilder, AttestationFormat, + AuthenticatorAttachment, }; pub use webauthn_rs_core::proto::{ AttestationMetadata, AuthenticationResult, AuthenticationState, CreationChallengeResponse, @@ -569,6 +569,7 @@ impl Webauthn { .user_verification_policy(UserVerificationPolicy::Required) .reject_synchronised_authenticators(false) .exclude_credentials(exclude_credentials) + .hints(None) .extensions(extensions); self.core @@ -627,6 +628,7 @@ impl Webauthn { .user_verification_policy(UserVerificationPolicy::Required) .reject_synchronised_authenticators(false) .exclude_credentials(exclude_credentials) + .hints(Some(vec![PublicKeyCredentialHints::ClientDevice])) .extensions(extensions); self.core @@ -677,15 +679,18 @@ impl Webauthn { let extensions = None; let creds = creds.iter().map(|sk| sk.cred.clone()).collect(); let policy = Some(UserVerificationPolicy::Required); - let allow_backup_eligible_upgrade = Some(true); + let allow_backup_eligible_upgrade = true; + let hints = None; self.core - .generate_challenge_authenticate( - creds, - policy, - extensions, - allow_backup_eligible_upgrade, - ) + .new_challenge_authenticate_builder(creds, policy) + .map(|builder| { + builder + .extensions(extensions) + .allow_backup_eligible_upgrade(allow_backup_eligible_upgrade) + .hints(hints) + }) + .and_then(|b| self.core.generate_challenge_authenticate(b)) .map(|(rcr, ast)| (rcr, PasskeyAuthentication { ast })) } @@ -889,6 +894,7 @@ impl Webauthn { .user_verification_policy(policy) .reject_synchronised_authenticators(false) .exclude_credentials(exclude_credentials) + .hints(Some(vec![PublicKeyCredentialHints::SecurityKey])) .extensions(extensions); self.core @@ -952,21 +958,25 @@ impl Webauthn { ) -> WebauthnResult<(RequestChallengeResponse, SecurityKeyAuthentication)> { let extensions = None; let creds = creds.iter().map(|sk| sk.cred.clone()).collect(); - let allow_backup_eligible_upgrade = Some(false); + let allow_backup_eligible_upgrade = false; let policy = if self.user_presence_only_security_keys { - UserVerificationPolicy::Discouraged_DO_NOT_USE + Some(UserVerificationPolicy::Discouraged_DO_NOT_USE) } else { - UserVerificationPolicy::Preferred + Some(UserVerificationPolicy::Preferred) }; + let hints = Some(vec![PublicKeyCredentialHints::SecurityKey]); + self.core - .generate_challenge_authenticate( - creds, - Some(policy), - extensions, - allow_backup_eligible_upgrade, - ) + .new_challenge_authenticate_builder(creds, policy) + .map(|builder| { + builder + .extensions(extensions) + .allow_backup_eligible_upgrade(allow_backup_eligible_upgrade) + .hints(hints) + }) + .and_then(|b| self.core.generate_challenge_authenticate(b)) .map(|(rcr, ast)| (rcr, SecurityKeyAuthentication { ast })) } @@ -1003,12 +1013,23 @@ impl Webauthn { /// and assert their identity. Because of this reliance on the authenticator, attestation of /// the authenticator and its properties is strongly recommended. /// - /// The primary difference to a passkey, is that these credentials *can not* 'roam' between multiple - /// devices, and must be bound to a single authenticator. This precludes the use of certain types - /// of authenticators (such as Apple's Passkeys as these are always synced). + /// The primary difference to a passkey, is that these credentials must provide an attestation + /// certificate which will be cryptographically validated to strictly enforce that only certain + /// devices may be registered. + /// + /// This attestation requires that private key material is bound to a single hardware + /// authenticator, and cannot be copied or moved out of it. At present, all widely deployed + /// Hybrid authenticators (Apple iCloud Keychain and Google Passkeys in Google Password + /// Manager) are synchronised authenticators which can roam between multiple devices, and so can + /// never be attested. /// - /// Additionally, these credentials must provide an attestation certificate of authenticity - /// which will be cryptographically validated to strictly enforce that only certain devices may be used. + /// As of webauthn-rs v0.5.0, this creates a registration challenge with + /// [credential selection hints](PublicKeyCredentialHints) that only use ClientDevice or + /// SecurityKey devices, so a user-agent supporting Webauthn L3 won't offer to use Hybrid + /// credentials. On user-agents not supporting Webauthn L3, and on older versions of + /// webauthn-rs, user-agents would show a QR code and a user could attempt to register a + /// Hybrid authenticator, but it would always fail at the end -- which is a frustrating user + /// experience! /// /// You *should* recommend to the user to register multiple attested_passkey keys to their account on /// separate devices so that they have fall back authentication in the case of device failure or loss. @@ -1161,6 +1182,17 @@ impl Webauthn { .user_verification_policy(UserVerificationPolicy::Required) .reject_synchronised_authenticators(true) .exclude_credentials(exclude_credentials) + .hints(Some( + // hybrid does NOT perform attestation + vec![ + PublicKeyCredentialHints::ClientDevice, + PublicKeyCredentialHints::SecurityKey, + ], + )) + .attestation_formats(Some(vec![ + AttestationFormat::Packed, + AttestationFormat::Tpm, + ])) .extensions(extensions); self.core @@ -1227,15 +1259,22 @@ impl Webauthn { }); let policy = Some(UserVerificationPolicy::Required); - let allow_backup_eligible_upgrade = Some(false); + let allow_backup_eligible_upgrade = false; + + let hints = Some(vec![ + PublicKeyCredentialHints::SecurityKey, + PublicKeyCredentialHints::ClientDevice, + ]); self.core - .generate_challenge_authenticate( - creds, - policy, - extensions, - allow_backup_eligible_upgrade, - ) + .new_challenge_authenticate_builder(creds, policy) + .map(|builder| { + builder + .extensions(extensions) + .allow_backup_eligible_upgrade(allow_backup_eligible_upgrade) + .hints(hints) + }) + .and_then(|b| self.core.generate_challenge_authenticate(b)) .map(|(rcr, ast)| (rcr, AttestedPasskeyAuthentication { ast })) } @@ -1285,15 +1324,18 @@ impl Webauthn { uvm: Some(true), hmac_get_secret: None, }); - let allow_backup_eligible_upgrade = Some(false); + let allow_backup_eligible_upgrade = false; + let hints = None; self.core - .generate_challenge_authenticate( - vec![], - policy, - extensions, - allow_backup_eligible_upgrade, - ) + .new_challenge_authenticate_builder(Vec::with_capacity(0), policy) + .map(|builder| { + builder + .extensions(extensions) + .allow_backup_eligible_upgrade(allow_backup_eligible_upgrade) + .hints(hints) + }) + .and_then(|b| self.core.generate_challenge_authenticate(b)) .map(|(mut rcr, ast)| { // Force conditional ui - this is not a generic discoverable credential // workflow! @@ -1400,6 +1442,17 @@ impl Webauthn { .user_verification_policy(UserVerificationPolicy::Required) .reject_synchronised_authenticators(true) .exclude_credentials(exclude_credentials) + .hints(Some( + // hybrid does NOT perform attestation + vec![ + PublicKeyCredentialHints::ClientDevice, + PublicKeyCredentialHints::SecurityKey, + ], + )) + .attestation_formats(Some(vec![ + AttestationFormat::Packed, + AttestationFormat::Tpm, + ])) .extensions(extensions); self.core @@ -1450,15 +1503,22 @@ impl Webauthn { }); let policy = Some(UserVerificationPolicy::Required); - let allow_backup_eligible_upgrade = Some(false); + let allow_backup_eligible_upgrade = false; + + let hints = Some(vec![ + PublicKeyCredentialHints::SecurityKey, + PublicKeyCredentialHints::ClientDevice, + ]); self.core - .generate_challenge_authenticate( - creds, - policy, - extensions, - allow_backup_eligible_upgrade, - ) + .new_challenge_authenticate_builder(creds, policy) + .map(|builder| { + builder + .extensions(extensions) + .allow_backup_eligible_upgrade(allow_backup_eligible_upgrade) + .hints(hints) + }) + .and_then(|b| self.core.generate_challenge_authenticate(b)) .map(|(rcr, ast)| (rcr, AttestedResidentKeyAuthentication { ast })) }