diff --git a/Cargo.toml b/Cargo.toml index 7bd3599d..880d8a4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -122,7 +122,8 @@ yubico = { package = "yubico_ng", version = "0.13.0", features = ["online-tokio" # WebAuthn libraries # danger-allow-state-serialisation is needed to save the state in the db # danger-credential-internals is needed to support U2F to Webauthn migration -webauthn-rs = { version = "0.5.2", features = ["danger-allow-state-serialisation", "danger-credential-internals"] } +# danger-user-presence-only-security-keys is needed to disable UV +webauthn-rs = { version = "0.5.2", features = ["danger-allow-state-serialisation", "danger-credential-internals", "danger-user-presence-only-security-keys"] } webauthn-rs-proto = "0.5.2" # Handling of URL's for WebAuthn and favicons diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs index 17a0bad5..51e66b97 100644 --- a/src/api/core/two_factor/webauthn.rs +++ b/src/api/core/two_factor/webauthn.rs @@ -20,12 +20,12 @@ use std::sync::{Arc, LazyLock}; use std::time::Duration; use url::Url; use uuid::Uuid; -use webauthn_rs::prelude::{Base64UrlSafeData, Passkey, PasskeyAuthentication, PasskeyRegistration}; +use webauthn_rs::prelude::{Base64UrlSafeData, SecurityKey, SecurityKeyAuthentication, SecurityKeyRegistration}; use webauthn_rs::{Webauthn, WebauthnBuilder}; use webauthn_rs_proto::{ AuthenticationExtensionsClientOutputs, AuthenticatorAssertionResponseRaw, AuthenticatorAttestationResponseRaw, PublicKeyCredential, RegisterPublicKeyCredential, RegistrationExtensionsClientOutputs, - RequestAuthenticationExtensions, UserVerificationPolicy, + RequestAuthenticationExtensions, }; pub static WEBAUTHN_2FA_CONFIG: LazyLock> = LazyLock::new(|| { @@ -37,7 +37,8 @@ pub static WEBAUTHN_2FA_CONFIG: LazyLock> = LazyLock::new(|| { let webauthn = WebauthnBuilder::new(&rp_id, &rp_origin) .expect("Creating WebauthnBuilder failed") .rp_name(&domain) - .timeout(Duration::from_millis(60000)); + .timeout(Duration::from_millis(60000)) + .danger_set_user_presence_only_security_keys(true); Arc::new(webauthn.build().expect("Building Webauthn failed")) }); @@ -76,7 +77,7 @@ pub struct WebauthnRegistration { pub name: String, pub migrated: bool, - pub credential: Passkey, + pub credential: SecurityKey, } impl WebauthnRegistration { @@ -129,23 +130,15 @@ async fn generate_webauthn_challenge( .map(|r| r.credential.cred_id().to_owned()) // We return the credentialIds to the clients to avoid double registering .collect(); - let (mut challenge, state) = webauthn.start_passkey_registration( + let (challenge, state) = webauthn.start_securitykey_registration( Uuid::from_str(&user.uuid).expect("Failed to parse UUID"), // Should never fail &user.email, &user.name, Some(registrations), + None, + None, )?; - // this is done since `start_passkey_registration()` always sets this to `Required` which shouldn't be needed for 2FA - challenge.public_key.extensions = None; - if let Some(asc) = challenge.public_key.authenticator_selection.as_mut() { - asc.user_verification = UserVerificationPolicy::Discouraged_DO_NOT_USE; - } - - let mut state = serde_json::to_value(&state)?; - state["rs"]["policy"] = Value::String("discouraged".to_string()); - state["rs"]["extensions"].as_object_mut().unwrap().clear(); - let type_ = TwoFactorType::WebauthnRegisterChallenge; TwoFactor::new(user.uuid.clone(), type_, serde_json::to_string(&state)?).save(&mut conn).await?; @@ -259,7 +252,7 @@ async fn activate_webauthn( let type_ = TwoFactorType::WebauthnRegisterChallenge as i32; let state = match TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await { Some(tf) => { - let state: PasskeyRegistration = serde_json::from_str(&tf.data)?; + let state: SecurityKeyRegistration = serde_json::from_str(&tf.data)?; tf.delete(&mut conn).await?; state } @@ -267,7 +260,7 @@ async fn activate_webauthn( }; // Verify the credentials with the saved state - let credential = webauthn.finish_passkey_registration(&data.device_response.into(), &state)?; + let credential = webauthn.finish_securitykey_registration(&data.device_response.into(), &state)?; let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &mut conn).await?.1; // TODO: Check for repeated ID's @@ -385,14 +378,12 @@ pub async fn generate_webauthn_login( } // Generate a challenge based on the credentials - let (mut response, state) = webauthn.start_passkey_authentication(&creds)?; + let (mut response, state) = webauthn.start_securitykey_authentication(&creds)?; // Modify to discourage user verification let mut state = serde_json::to_value(&state)?; - state["ast"]["policy"] = Value::String("discouraged".to_string()); - response.public_key.user_verification = UserVerificationPolicy::Discouraged_DO_NOT_USE; - // Add appid + // Add appid, this is only needed for U2F compatibility, so maybe it can be removed as well let app_id = format!("{}/app-id.json", &CONFIG.domain()); state["ast"]["appid"] = Value::String(app_id.clone()); response @@ -423,7 +414,7 @@ pub async fn validate_webauthn_login( let type_ = TwoFactorType::WebauthnLoginChallenge as i32; let state = match TwoFactor::find_by_user_and_type(user_id, type_, conn).await { Some(tf) => { - let state: PasskeyAuthentication = serde_json::from_str(&tf.data)?; + let state: SecurityKeyAuthentication = serde_json::from_str(&tf.data)?; tf.delete(conn).await?; state } @@ -440,7 +431,7 @@ pub async fn validate_webauthn_login( let mut registrations = get_webauthn_registrations(user_id, conn).await?.1; - let authentication_result = webauthn.finish_passkey_authentication(&rsp, &state)?; + let authentication_result = webauthn.finish_securitykey_authentication(&rsp, &state)?; for reg in &mut registrations { if reg.credential.cred_id() == authentication_result.cred_id() && authentication_result.needs_update() {