mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-13 12:05:58 +03: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