mirror of
				https://github.com/dani-garcia/vaultwarden.git
				synced 2025-10-31 10:18:19 +02:00 
			
		
		
		
	Update webauthn-rs to 0.5.x (#5934)
* update webauthn to 0.5 * add basic migration impl * fix clippy warnings * clear up `COSEKeyType::EC_OKP` case * fix TODOs * use same timeout as in webauthn 0.3 impl * fix: clippy warnings and formatting * Update Cargo.toml Co-authored-by: Daniel <daniel.barabasa@gmail.com> * Update src/api/core/two_factor/webauthn.rs Co-authored-by: Daniel <daniel.barabasa@gmail.com> * Update src/api/core/two_factor/webauthn.rs Co-authored-by: Daniel <daniel.barabasa@gmail.com> * Update src/api/core/two_factor/webauthn.rs Co-authored-by: Daniel <daniel.barabasa@gmail.com> * regenerate Cargo.lock * Use securitykey methods * use CredentialsV3 from webauthn-rs instead of own webauthn_0_3 module * fix cargo fmt issue --------- Co-authored-by: Helmut K. C. Tessarek <tessarek@evermeet.cx> Co-authored-by: Daniel <daniel.barabasa@gmail.com> Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
This commit is contained in:
		| @@ -1,9 +1,3 @@ | ||||
| use rocket::serde::json::Json; | ||||
| use rocket::Route; | ||||
| use serde_json::Value; | ||||
| use url::Url; | ||||
| use webauthn_rs::{base64_data::Base64UrlSafeData, proto::*, AuthenticationState, RegistrationState, Webauthn}; | ||||
|  | ||||
| use crate::{ | ||||
|     api::{ | ||||
|         core::{log_user_event, two_factor::_generate_recover_code}, | ||||
| @@ -18,6 +12,38 @@ use crate::{ | ||||
|     util::NumberOrString, | ||||
|     CONFIG, | ||||
| }; | ||||
| use rocket::serde::json::Json; | ||||
| use rocket::Route; | ||||
| use serde_json::Value; | ||||
| use std::str::FromStr; | ||||
| use std::sync::{Arc, LazyLock}; | ||||
| use std::time::Duration; | ||||
| use url::Url; | ||||
| use uuid::Uuid; | ||||
| use webauthn_rs::prelude::{Base64UrlSafeData, SecurityKey, SecurityKeyAuthentication, SecurityKeyRegistration}; | ||||
| use webauthn_rs::{Webauthn, WebauthnBuilder}; | ||||
| use webauthn_rs_proto::{ | ||||
|     AuthenticationExtensionsClientOutputs, AuthenticatorAssertionResponseRaw, AuthenticatorAttestationResponseRaw, | ||||
|     PublicKeyCredential, RegisterPublicKeyCredential, RegistrationExtensionsClientOutputs, | ||||
|     RequestAuthenticationExtensions, | ||||
| }; | ||||
|  | ||||
| pub static WEBAUTHN_2FA_CONFIG: LazyLock<Arc<Webauthn>> = LazyLock::new(|| { | ||||
|     let domain = CONFIG.domain(); | ||||
|     let domain_origin = CONFIG.domain_origin(); | ||||
|     let rp_id = Url::parse(&domain).map(|u| u.domain().map(str::to_owned)).ok().flatten().unwrap_or_default(); | ||||
|     let rp_origin = Url::parse(&domain_origin).unwrap(); | ||||
|  | ||||
|     let webauthn = WebauthnBuilder::new(&rp_id, &rp_origin) | ||||
|         .expect("Creating WebauthnBuilder failed") | ||||
|         .rp_name(&domain) | ||||
|         .timeout(Duration::from_millis(60000)) | ||||
|         .danger_set_user_presence_only_security_keys(true); | ||||
|  | ||||
|     Arc::new(webauthn.build().expect("Building Webauthn failed")) | ||||
| }); | ||||
|  | ||||
| pub type Webauthn2FaConfig<'a> = &'a rocket::State<Arc<Webauthn>>; | ||||
|  | ||||
| pub fn routes() -> Vec<Route> { | ||||
|     routes![get_webauthn, generate_webauthn_challenge, activate_webauthn, activate_webauthn_put, delete_webauthn,] | ||||
| @@ -45,52 +71,13 @@ pub struct U2FRegistration { | ||||
|     pub migrated: Option<bool>, | ||||
| } | ||||
|  | ||||
| struct WebauthnConfig { | ||||
|     url: String, | ||||
|     origin: Url, | ||||
|     rpid: String, | ||||
| } | ||||
|  | ||||
| impl WebauthnConfig { | ||||
|     fn load() -> Webauthn<Self> { | ||||
|         let domain = CONFIG.domain(); | ||||
|         let domain_origin = CONFIG.domain_origin(); | ||||
|         Webauthn::new(Self { | ||||
|             rpid: Url::parse(&domain).map(|u| u.domain().map(str::to_owned)).ok().flatten().unwrap_or_default(), | ||||
|             url: domain, | ||||
|             origin: Url::parse(&domain_origin).unwrap(), | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl webauthn_rs::WebauthnConfig for WebauthnConfig { | ||||
|     fn get_relying_party_name(&self) -> &str { | ||||
|         &self.url | ||||
|     } | ||||
|  | ||||
|     fn get_origin(&self) -> &Url { | ||||
|         &self.origin | ||||
|     } | ||||
|  | ||||
|     fn get_relying_party_id(&self) -> &str { | ||||
|         &self.rpid | ||||
|     } | ||||
|  | ||||
|     /// We have WebAuthn configured to discourage user verification | ||||
|     /// if we leave this enabled, it will cause verification issues when a keys send UV=1. | ||||
|     /// Upstream (the library they use) ignores this when set to discouraged, so we should too. | ||||
|     fn get_require_uv_consistency(&self) -> bool { | ||||
|         false | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| pub struct WebauthnRegistration { | ||||
|     pub id: i32, | ||||
|     pub name: String, | ||||
|     pub migrated: bool, | ||||
|  | ||||
|     pub credential: Credential, | ||||
|     pub credential: SecurityKey, | ||||
| } | ||||
|  | ||||
| impl WebauthnRegistration { | ||||
| @@ -125,7 +112,12 @@ async fn get_webauthn(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: | ||||
| } | ||||
|  | ||||
| #[post("/two-factor/get-webauthn-challenge", data = "<data>")] | ||||
| async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
| async fn generate_webauthn_challenge( | ||||
|     data: Json<PasswordOrOtpData>, | ||||
|     headers: Headers, | ||||
|     webauthn: Webauthn2FaConfig<'_>, | ||||
|     mut conn: DbConn, | ||||
| ) -> JsonResult { | ||||
|     let data: PasswordOrOtpData = data.into_inner(); | ||||
|     let user = headers.user; | ||||
|  | ||||
| @@ -135,13 +127,13 @@ async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Hea | ||||
|         .await? | ||||
|         .1 | ||||
|         .into_iter() | ||||
|         .map(|r| r.credential.cred_id) // We return the credentialIds to the clients to avoid double registering | ||||
|         .map(|r| r.credential.cred_id().to_owned()) // We return the credentialIds to the clients to avoid double registering | ||||
|         .collect(); | ||||
|  | ||||
|     let (challenge, state) = WebauthnConfig::load().generate_challenge_register_options( | ||||
|         user.uuid.as_bytes().to_vec(), | ||||
|         user.email, | ||||
|         user.name, | ||||
|     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, | ||||
| @@ -193,8 +185,10 @@ impl From<RegisterPublicKeyCredentialCopy> for RegisterPublicKeyCredential { | ||||
|             response: AuthenticatorAttestationResponseRaw { | ||||
|                 attestation_object: r.response.attestation_object, | ||||
|                 client_data_json: r.response.client_data_json, | ||||
|                 transports: None, | ||||
|             }, | ||||
|             type_: r.r#type, | ||||
|             extensions: RegistrationExtensionsClientOutputs::default(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -205,7 +199,7 @@ pub struct PublicKeyCredentialCopy { | ||||
|     pub id: String, | ||||
|     pub raw_id: Base64UrlSafeData, | ||||
|     pub response: AuthenticatorAssertionResponseRawCopy, | ||||
|     pub extensions: Option<AuthenticationExtensionsClientOutputs>, | ||||
|     pub extensions: AuthenticationExtensionsClientOutputs, | ||||
|     pub r#type: String, | ||||
| } | ||||
|  | ||||
| @@ -238,7 +232,12 @@ impl From<PublicKeyCredentialCopy> for PublicKeyCredential { | ||||
| } | ||||
|  | ||||
| #[post("/two-factor/webauthn", data = "<data>")] | ||||
| async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut conn: DbConn) -> JsonResult { | ||||
| async fn activate_webauthn( | ||||
|     data: Json<EnableWebauthnData>, | ||||
|     headers: Headers, | ||||
|     webauthn: Webauthn2FaConfig<'_>, | ||||
|     mut conn: DbConn, | ||||
| ) -> JsonResult { | ||||
|     let data: EnableWebauthnData = data.into_inner(); | ||||
|     let mut user = headers.user; | ||||
|  | ||||
| @@ -253,7 +252,7 @@ async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut | ||||
|     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: RegistrationState = serde_json::from_str(&tf.data)?; | ||||
|             let state: SecurityKeyRegistration = serde_json::from_str(&tf.data)?; | ||||
|             tf.delete(&mut conn).await?; | ||||
|             state | ||||
|         } | ||||
| @@ -261,8 +260,7 @@ async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut | ||||
|     }; | ||||
|  | ||||
|     // Verify the credentials with the saved state | ||||
|     let (credential, _data) = | ||||
|         WebauthnConfig::load().register_credential(&data.device_response.into(), &state, |_| Ok(false))?; | ||||
|     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 | ||||
| @@ -291,8 +289,13 @@ async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut | ||||
| } | ||||
|  | ||||
| #[put("/two-factor/webauthn", data = "<data>")] | ||||
| async fn activate_webauthn_put(data: Json<EnableWebauthnData>, headers: Headers, conn: DbConn) -> JsonResult { | ||||
|     activate_webauthn(data, headers, conn).await | ||||
| async fn activate_webauthn_put( | ||||
|     data: Json<EnableWebauthnData>, | ||||
|     headers: Headers, | ||||
|     webauthn: Webauthn2FaConfig<'_>, | ||||
|     conn: DbConn, | ||||
| ) -> JsonResult { | ||||
|     activate_webauthn(data, headers, webauthn, conn).await | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize)] | ||||
| @@ -335,7 +338,7 @@ async fn delete_webauthn(data: Json<DeleteU2FData>, headers: Headers, mut conn: | ||||
|             Err(_) => err!("Error parsing U2F data"), | ||||
|         }; | ||||
|  | ||||
|         data.retain(|r| r.reg.key_handle != removed_item.credential.cred_id); | ||||
|         data.retain(|r| r.reg.key_handle != removed_item.credential.cred_id().as_slice()); | ||||
|         let new_data_str = serde_json::to_string(&data)?; | ||||
|  | ||||
|         u2f.data = new_data_str; | ||||
| @@ -362,18 +365,36 @@ pub async fn get_webauthn_registrations( | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub async fn generate_webauthn_login(user_id: &UserId, conn: &mut DbConn) -> JsonResult { | ||||
| pub async fn generate_webauthn_login( | ||||
|     user_id: &UserId, | ||||
|     webauthn: Webauthn2FaConfig<'_>, | ||||
|     conn: &mut DbConn, | ||||
| ) -> JsonResult { | ||||
|     // Load saved credentials | ||||
|     let creds: Vec<Credential> = | ||||
|         get_webauthn_registrations(user_id, conn).await?.1.into_iter().map(|r| r.credential).collect(); | ||||
|     let creds: Vec<_> = get_webauthn_registrations(user_id, conn).await?.1.into_iter().map(|r| r.credential).collect(); | ||||
|  | ||||
|     if creds.is_empty() { | ||||
|         err!("No Webauthn devices registered") | ||||
|     } | ||||
|  | ||||
|     // Generate a challenge based on the credentials | ||||
|     let ext = RequestAuthenticationExtensions::builder().appid(format!("{}/app-id.json", &CONFIG.domain())).build(); | ||||
|     let (response, state) = WebauthnConfig::load().generate_challenge_authenticate_options(creds, Some(ext))?; | ||||
|     let (mut response, state) = webauthn.start_securitykey_authentication(&creds)?; | ||||
|  | ||||
|     // Modify to discourage user verification | ||||
|     let mut state = serde_json::to_value(&state)?; | ||||
|  | ||||
|     // 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 | ||||
|         .public_key | ||||
|         .extensions | ||||
|         .get_or_insert(RequestAuthenticationExtensions { | ||||
|             appid: None, | ||||
|             uvm: None, | ||||
|             hmac_get_secret: None, | ||||
|         }) | ||||
|         .appid = Some(app_id); | ||||
|  | ||||
|     // Save the challenge state for later validation | ||||
|     TwoFactor::new(user_id.clone(), TwoFactorType::WebauthnLoginChallenge, serde_json::to_string(&state)?) | ||||
| @@ -384,11 +405,16 @@ pub async fn generate_webauthn_login(user_id: &UserId, conn: &mut DbConn) -> Jso | ||||
|     Ok(Json(serde_json::to_value(response.public_key)?)) | ||||
| } | ||||
|  | ||||
| pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &mut DbConn) -> EmptyResult { | ||||
| pub async fn validate_webauthn_login( | ||||
|     user_id: &UserId, | ||||
|     response: &str, | ||||
|     webauthn: Webauthn2FaConfig<'_>, | ||||
|     conn: &mut DbConn, | ||||
| ) -> EmptyResult { | ||||
|     let type_ = TwoFactorType::WebauthnLoginChallenge as i32; | ||||
|     let state = match TwoFactor::find_by_user_and_type(user_id, type_, conn).await { | ||||
|         Some(tf) => { | ||||
|             let state: AuthenticationState = serde_json::from_str(&tf.data)?; | ||||
|             let state: SecurityKeyAuthentication = serde_json::from_str(&tf.data)?; | ||||
|             tf.delete(conn).await?; | ||||
|             state | ||||
|         } | ||||
| @@ -405,13 +431,11 @@ pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &mu | ||||
|  | ||||
|     let mut registrations = get_webauthn_registrations(user_id, conn).await?.1; | ||||
|  | ||||
|     // If the credential we received is migrated from U2F, enable the U2F compatibility | ||||
|     //let use_u2f = registrations.iter().any(|r| r.migrated && r.credential.cred_id == rsp.raw_id.0); | ||||
|     let (cred_id, auth_data) = WebauthnConfig::load().authenticate_credential(&rsp, &state)?; | ||||
|     let authentication_result = webauthn.finish_securitykey_authentication(&rsp, &state)?; | ||||
|  | ||||
|     for reg in &mut registrations { | ||||
|         if ®.credential.cred_id == cred_id { | ||||
|             reg.credential.counter = auth_data.counter; | ||||
|         if reg.credential.cred_id() == authentication_result.cred_id() && authentication_result.needs_update() { | ||||
|             reg.credential.update_credential(&authentication_result); | ||||
|  | ||||
|             TwoFactor::new(user_id.clone(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?) | ||||
|                 .save(conn) | ||||
|   | ||||
| @@ -9,6 +9,7 @@ use rocket::{ | ||||
| }; | ||||
| use serde_json::Value; | ||||
|  | ||||
| use crate::api::core::two_factor::webauthn::Webauthn2FaConfig; | ||||
| use crate::{ | ||||
|     api::{ | ||||
|         core::{ | ||||
| @@ -48,6 +49,7 @@ async fn login( | ||||
|     data: Form<ConnectData>, | ||||
|     client_header: ClientHeaders, | ||||
|     client_version: Option<ClientVersion>, | ||||
|     webauthn: Webauthn2FaConfig<'_>, | ||||
|     mut conn: DbConn, | ||||
| ) -> JsonResult { | ||||
|     let data: ConnectData = data.into_inner(); | ||||
| @@ -70,7 +72,7 @@ async fn login( | ||||
|             _check_is_some(&data.device_name, "device_name cannot be blank")?; | ||||
|             _check_is_some(&data.device_type, "device_type cannot be blank")?; | ||||
|  | ||||
|             _password_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version).await | ||||
|             _password_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version, webauthn).await | ||||
|         } | ||||
|         "client_credentials" => { | ||||
|             _check_is_some(&data.client_id, "client_id cannot be blank")?; | ||||
| @@ -91,7 +93,7 @@ async fn login( | ||||
|             _check_is_some(&data.device_name, "device_name cannot be blank")?; | ||||
|             _check_is_some(&data.device_type, "device_type cannot be blank")?; | ||||
|  | ||||
|             _sso_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version).await | ||||
|             _sso_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version, webauthn).await | ||||
|         } | ||||
|         "authorization_code" => err!("SSO sign-in is not available"), | ||||
|         t => err!("Invalid type", t), | ||||
| @@ -169,6 +171,7 @@ async fn _sso_login( | ||||
|     conn: &mut DbConn, | ||||
|     ip: &ClientIp, | ||||
|     client_version: &Option<ClientVersion>, | ||||
|     webauthn: Webauthn2FaConfig<'_>, | ||||
| ) -> JsonResult { | ||||
|     AuthMethod::Sso.check_scope(data.scope.as_ref())?; | ||||
|  | ||||
| @@ -267,7 +270,7 @@ async fn _sso_login( | ||||
|         } | ||||
|         Some((mut user, sso_user)) => { | ||||
|             let mut device = get_device(&data, conn, &user).await?; | ||||
|             let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, client_version, conn).await?; | ||||
|             let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, client_version, webauthn, conn).await?; | ||||
|  | ||||
|             if user.private_key.is_none() { | ||||
|                 // User was invited a stub was created | ||||
| @@ -322,6 +325,7 @@ async fn _password_login( | ||||
|     conn: &mut DbConn, | ||||
|     ip: &ClientIp, | ||||
|     client_version: &Option<ClientVersion>, | ||||
|     webauthn: Webauthn2FaConfig<'_>, | ||||
| ) -> JsonResult { | ||||
|     // Validate scope | ||||
|     AuthMethod::Password.check_scope(data.scope.as_ref())?; | ||||
| @@ -431,7 +435,7 @@ async fn _password_login( | ||||
|  | ||||
|     let mut device = get_device(&data, conn, &user).await?; | ||||
|  | ||||
|     let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, client_version, conn).await?; | ||||
|     let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, client_version, webauthn, conn).await?; | ||||
|  | ||||
|     let auth_tokens = auth::AuthTokens::new(&device, &user, AuthMethod::Password, data.client_id); | ||||
|  | ||||
| @@ -664,6 +668,7 @@ async fn twofactor_auth( | ||||
|     device: &mut Device, | ||||
|     ip: &ClientIp, | ||||
|     client_version: &Option<ClientVersion>, | ||||
|     webauthn: Webauthn2FaConfig<'_>, | ||||
|     conn: &mut DbConn, | ||||
| ) -> ApiResult<Option<String>> { | ||||
|     let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await; | ||||
| @@ -683,7 +688,7 @@ async fn twofactor_auth( | ||||
|         Some(ref code) => code, | ||||
|         None => { | ||||
|             err_json!( | ||||
|                 _json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?, | ||||
|                 _json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, webauthn, conn).await?, | ||||
|                 "2FA token not provided" | ||||
|             ) | ||||
|         } | ||||
| @@ -700,7 +705,9 @@ async fn twofactor_auth( | ||||
|         Some(TwoFactorType::Authenticator) => { | ||||
|             authenticator::validate_totp_code_str(&user.uuid, twofactor_code, &selected_data?, ip, conn).await? | ||||
|         } | ||||
|         Some(TwoFactorType::Webauthn) => webauthn::validate_webauthn_login(&user.uuid, twofactor_code, conn).await?, | ||||
|         Some(TwoFactorType::Webauthn) => { | ||||
|             webauthn::validate_webauthn_login(&user.uuid, twofactor_code, webauthn, conn).await? | ||||
|         } | ||||
|         Some(TwoFactorType::YubiKey) => yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?, | ||||
|         Some(TwoFactorType::Duo) => { | ||||
|             match CONFIG.duo_use_iframe() { | ||||
| @@ -732,7 +739,7 @@ async fn twofactor_auth( | ||||
|                 } | ||||
|                 _ => { | ||||
|                     err_json!( | ||||
|                         _json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?, | ||||
|                         _json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, webauthn, conn).await?, | ||||
|                         "2FA Remember token not provided" | ||||
|                     ) | ||||
|                 } | ||||
| @@ -766,6 +773,7 @@ async fn _json_err_twofactor( | ||||
|     user_id: &UserId, | ||||
|     data: &ConnectData, | ||||
|     client_version: &Option<ClientVersion>, | ||||
|     webauthn: Webauthn2FaConfig<'_>, | ||||
|     conn: &mut DbConn, | ||||
| ) -> ApiResult<Value> { | ||||
|     let mut result = json!({ | ||||
| @@ -785,7 +793,7 @@ async fn _json_err_twofactor( | ||||
|             Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ } | ||||
|  | ||||
|             Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => { | ||||
|                 let request = webauthn::generate_webauthn_login(user_id, conn).await?; | ||||
|                 let request = webauthn::generate_webauthn_login(user_id, webauthn, conn).await?; | ||||
|                 result["TwoFactorProviders2"][provider.to_string()] = request.0; | ||||
|             } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user