mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-06-09 04:23:55 +00:00
update webauthn to 0.5
This commit is contained in:
parent
0d3f283c37
commit
cbf9a2a519
7 changed files with 380 additions and 100 deletions
189
Cargo.lock
generated
189
Cargo.lock
generated
|
@ -103,6 +103,45 @@ dependencies = [
|
|||
"password-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048"
|
||||
dependencies = [
|
||||
"asn1-rs-derive",
|
||||
"asn1-rs-impl",
|
||||
"displaydoc",
|
||||
"nom 7.1.3",
|
||||
"num-traits",
|
||||
"rusticata-macros",
|
||||
"thiserror 1.0.69",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs-derive"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs-impl"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "1.9.0"
|
||||
|
@ -702,12 +741,6 @@ dependencies = [
|
|||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
|
@ -736,6 +769,17 @@ version = "1.7.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3"
|
||||
|
||||
[[package]]
|
||||
name = "base64urlsafedata"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f0ad38ce7fbed55985ad5b2197f05cff8324ee6eb6638304e78f0108fae56c"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"paste",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bigdecimal"
|
||||
version = "0.4.8"
|
||||
|
@ -1038,6 +1082,23 @@ version = "0.1.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e769b5c8c8283982a987c6e948e540254f1058d5a74b8794914d4ef5fc2a24"
|
||||
|
||||
[[package]]
|
||||
name = "compact_jwt"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12bbab6445446e8d0b07468a01d0bfdae15879de5c440c5e47ae4ae0e18a1fba"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"base64urlsafedata",
|
||||
"hex",
|
||||
"openssl",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
|
@ -1284,6 +1345,20 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der-parser"
|
||||
version = "9.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553"
|
||||
dependencies = [
|
||||
"asn1-rs",
|
||||
"displaydoc",
|
||||
"nom 7.1.3",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"rusticata-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.4.0"
|
||||
|
@ -3043,6 +3118,15 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oid-registry"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9"
|
||||
dependencies = [
|
||||
"asn1-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
|
@ -4131,6 +4215,15 @@ dependencies = [
|
|||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusticata-macros"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
|
||||
dependencies = [
|
||||
"nom 7.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.44"
|
||||
|
@ -4370,10 +4463,10 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_cbor"
|
||||
version = "0.11.2"
|
||||
name = "serde_cbor_2"
|
||||
version = "0.12.0-dev"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5"
|
||||
checksum = "b46d75f449e01f1eddbe9b00f432d616fbbd899b809c837d0fbc380496a0dd55"
|
||||
dependencies = [
|
||||
"half",
|
||||
"serde",
|
||||
|
@ -5286,6 +5379,7 @@ dependencies = [
|
|||
"url",
|
||||
"uuid",
|
||||
"webauthn-rs",
|
||||
"webauthn-rs-proto",
|
||||
"which 7.0.3",
|
||||
"yubico_ng",
|
||||
]
|
||||
|
@ -5447,22 +5541,70 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "webauthn-rs"
|
||||
version = "0.3.2"
|
||||
name = "webauthn-attestation-ca"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90b266eccb4b32595876f5c73ea443b0516da0b1df72ca07bc08ed9ba7f96ec1"
|
||||
checksum = "29e77e8859ecb93b00e4a8e56ae45f8a8dd69b1539e3d32cf4cce1db9a3a0b99"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"base64urlsafedata",
|
||||
"openssl",
|
||||
"serde",
|
||||
"tracing",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webauthn-rs"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b44347ee0d66f222043663a6aaf5ec78022b9b11c3a9ed488c21f2bd5680856"
|
||||
dependencies = [
|
||||
"base64urlsafedata",
|
||||
"serde",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
"webauthn-rs-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webauthn-rs-core"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ef48f07ed8f3dfe304d6c48e85317feba0439675f31a13063b2936c9b4eaf0d"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"base64urlsafedata",
|
||||
"compact_jwt",
|
||||
"der-parser",
|
||||
"hex",
|
||||
"nom 7.1.3",
|
||||
"openssl",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"serde",
|
||||
"serde_cbor",
|
||||
"serde_derive",
|
||||
"serde_cbor_2",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
"webauthn-attestation-ca",
|
||||
"webauthn-rs-proto",
|
||||
"x509-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webauthn-rs-proto"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14e1367f70e7dc7b83afc971ce8a54d578f4fdf488ea093021180e073744a69f"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"base64urlsafedata",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -5936,6 +6078,23 @@ version = "0.6.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
|
||||
|
||||
[[package]]
|
||||
name = "x509-parser"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69"
|
||||
dependencies = [
|
||||
"asn1-rs",
|
||||
"data-encoding",
|
||||
"der-parser",
|
||||
"lazy_static",
|
||||
"nom 7.1.3",
|
||||
"oid-registry",
|
||||
"rusticata-macros",
|
||||
"thiserror 1.0.69",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xmlparser"
|
||||
version = "0.13.6"
|
||||
|
|
|
@ -120,7 +120,10 @@ totp-lite = "2.0.1"
|
|||
yubico = { package = "yubico_ng", version = "0.13.0", features = ["online-tokio"], default-features = false }
|
||||
|
||||
# WebAuthn libraries
|
||||
webauthn-rs = "0.3.2"
|
||||
# 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", features = ["danger-allow-state-serialisation", "danger-credential-internals"] }
|
||||
webauthn-rs-proto = "0.5"
|
||||
|
||||
# Handling of URL's for WebAuthn and favicons
|
||||
url = "2.5.4"
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use once_cell::sync::Lazy;
|
||||
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 uuid::Uuid;
|
||||
use webauthn_rs::{Webauthn, WebauthnBuilder};
|
||||
use webauthn_rs::prelude::{Base64UrlSafeData, Passkey, PasskeyAuthentication, PasskeyRegistration};
|
||||
use webauthn_rs_proto::{AuthenticationExtensionsClientOutputs, AuthenticatorAssertionResponseRaw, AuthenticatorAttestationResponseRaw, PublicKeyCredential, RegisterPublicKeyCredential, RegistrationExtensionsClientOutputs, UserVerificationPolicy};
|
||||
use crate::{
|
||||
api::{
|
||||
core::{log_user_event, two_factor::_generate_recover_code},
|
||||
|
@ -19,6 +24,26 @@ use crate::{
|
|||
CONFIG,
|
||||
};
|
||||
|
||||
pub static WEBAUTHN_2FA_CONFIG: Lazy<Arc<Webauthn>> = Lazy::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);
|
||||
|
||||
// TODO check what happened to get_require_uv_consistency()
|
||||
|
||||
// TODO check if there is a better way to handle these errors (would they instantly through or only when used?)
|
||||
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 +70,14 @@ 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,
|
||||
// TODO should this be renamed or just stay this way
|
||||
pub credential: Passkey,
|
||||
}
|
||||
|
||||
impl WebauthnRegistration {
|
||||
|
@ -125,7 +112,7 @@ 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,18 +122,24 @@ 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,
|
||||
// TODO handle errors
|
||||
let (mut challenge, state) = webauthn.start_passkey_registration(
|
||||
Uuid::from_str(&*user.uuid).unwrap(),
|
||||
&user.email,
|
||||
&user.name,
|
||||
Some(registrations),
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
|
||||
// TODO is there a nicer way to do this?
|
||||
// this is done since `start_passkey_registration()` always sets this to `Required` which shouldn't be needed for 2FA
|
||||
challenge.public_key.authenticator_selection = challenge.public_key.authenticator_selection.map(|mut a| {
|
||||
a.user_verification = UserVerificationPolicy::Discouraged_DO_NOT_USE;
|
||||
a
|
||||
});
|
||||
|
||||
let type_ = TwoFactorType::WebauthnRegisterChallenge;
|
||||
TwoFactor::new(user.uuid.clone(), type_, serde_json::to_string(&state)?).save(&mut conn).await?;
|
||||
|
||||
|
@ -193,8 +186,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 +200,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 +233,7 @@ 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 +248,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: PasskeyRegistration = serde_json::from_str(&tf.data)?;
|
||||
tf.delete(&mut conn).await?;
|
||||
state
|
||||
}
|
||||
|
@ -261,8 +256,8 @@ 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_passkey_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 +286,8 @@ 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 +330,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,9 +357,9 @@ 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> =
|
||||
let creds: Vec<_> =
|
||||
get_webauthn_registrations(user_id, conn).await?.1.into_iter().map(|r| r.credential).collect();
|
||||
|
||||
if creds.is_empty() {
|
||||
|
@ -372,8 +367,11 @@ pub async fn generate_webauthn_login(user_id: &UserId, conn: &mut DbConn) -> Jso
|
|||
}
|
||||
|
||||
// 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_passkey_authentication(&creds)?;
|
||||
response.public_key.user_verification = UserVerificationPolicy::Discouraged_DO_NOT_USE;
|
||||
|
||||
// TODO does the appid extension matter? As far as I understand, this was only put into the authentication state anyway
|
||||
// let ext = RequestAuthenticationExtensions::builder().appid(format!("{}/app-id.json", &CONFIG.domain())).build();
|
||||
|
||||
// Save the challenge state for later validation
|
||||
TwoFactor::new(user_id.clone(), TwoFactorType::WebauthnLoginChallenge, serde_json::to_string(&state)?)
|
||||
|
@ -384,11 +382,11 @@ 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: PasskeyAuthentication = serde_json::from_str(&tf.data)?;
|
||||
tf.delete(conn).await?;
|
||||
state
|
||||
}
|
||||
|
@ -405,13 +403,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_passkey_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)
|
||||
|
|
|
@ -23,6 +23,7 @@ use crate::{
|
|||
error::MapResult,
|
||||
mail, util, CONFIG,
|
||||
};
|
||||
use crate::api::core::two_factor::webauthn::Webauthn2FaConfig;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![login, prelogin, identity_register, register_verification_email, register_finish]
|
||||
|
@ -33,6 +34,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();
|
||||
|
@ -54,7 +56,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")?;
|
||||
|
@ -139,6 +141,7 @@ async fn _password_login(
|
|||
conn: &mut DbConn,
|
||||
ip: &ClientIp,
|
||||
client_version: &Option<ClientVersion>,
|
||||
webauthn: Webauthn2FaConfig<'_>,
|
||||
) -> JsonResult {
|
||||
// Validate scope
|
||||
let scope = data.scope.as_ref().unwrap();
|
||||
|
@ -257,7 +260,7 @@ async fn _password_login(
|
|||
|
||||
let (mut device, new_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 CONFIG.mail_enabled() && new_device {
|
||||
if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device).await {
|
||||
|
@ -488,6 +491,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;
|
||||
|
@ -507,7 +511,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"
|
||||
)
|
||||
}
|
||||
|
@ -524,7 +528,7 @@ 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() {
|
||||
|
@ -556,7 +560,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"
|
||||
)
|
||||
}
|
||||
|
@ -589,6 +593,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!({
|
||||
|
@ -608,7 +613,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;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use serde_json::Value;
|
||||
|
||||
use webauthn_rs::prelude::{Credential, ParsedAttestation};
|
||||
use webauthn_rs_proto::{AttestationFormat, RegisteredExtensions};
|
||||
use super::UserId;
|
||||
use crate::{api::EmptyResult, db::DbConn, error::MapResult};
|
||||
|
||||
|
@ -40,6 +41,107 @@ pub enum TwoFactorType {
|
|||
ProtectedActions = 2000,
|
||||
}
|
||||
|
||||
mod webauthn_0_3 {
|
||||
use webauthn_rs::prelude::ParsedAttestation;
|
||||
use webauthn_rs_proto::{AttestationFormat, RegisteredExtensions};
|
||||
|
||||
// Copied from https://docs.rs/webauthn-rs/0.3.2/src/webauthn_rs/proto.rs.html#316-339
|
||||
pub struct Credential {
|
||||
pub cred_id: Vec<u8>,
|
||||
pub cred: COSEKey,
|
||||
pub counter: u32,
|
||||
pub verified: bool,
|
||||
pub registration_policy: webauthn_rs_proto::UserVerificationPolicy,
|
||||
}
|
||||
|
||||
impl From<Credential> for webauthn_rs::prelude::Credential {
|
||||
fn from(value: Credential) -> Self {
|
||||
Self {
|
||||
cred_id: value.cred_id.into(),
|
||||
cred: value.cred.into(),
|
||||
counter: value.counter,
|
||||
transports: None,
|
||||
user_verified: value.verified,
|
||||
backup_eligible: false,
|
||||
backup_state: false,
|
||||
registration_policy: value.registration_policy,
|
||||
extensions: RegisteredExtensions::none(),
|
||||
attestation: ParsedAttestation::default(),
|
||||
attestation_format: AttestationFormat::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copied from https://docs.rs/webauthn-rs/0.3.2/src/webauthn_rs/proto.rs.html#300-305
|
||||
#[derive(Deserialize)]
|
||||
pub struct COSEKey {
|
||||
pub type_: webauthn_rs::prelude::COSEAlgorithm,
|
||||
pub key: COSEKeyType,
|
||||
}
|
||||
|
||||
impl From<COSEKey> for webauthn_rs::prelude::COSEKey {
|
||||
fn from(value: COSEKey) -> Self {
|
||||
Self {
|
||||
type_: value.type_,
|
||||
key: value.key.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copied from https://docs.rs/webauthn-rs/0.3.2/src/webauthn_rs/proto.rs.html#260-278
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(Deserialize)]
|
||||
pub enum COSEKeyType {
|
||||
EC_OKP,
|
||||
EC_EC2(COSEEC2Key),
|
||||
RSA(COSERSAKey),
|
||||
}
|
||||
|
||||
impl From<COSEKeyType> for webauthn_rs::prelude::COSEKeyType {
|
||||
fn from(value: COSEKeyType) -> Self {
|
||||
match value {
|
||||
COSEKeyType::EC_OKP => panic!(), // TODO what to do here
|
||||
COSEKeyType::EC_EC2(a) => Self::EC_EC2(a.into()),
|
||||
COSEKeyType::RSA(a) => Self::RSA(a.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copied from https://docs.rs/webauthn-rs/0.3.2/src/webauthn_rs/proto.rs.html#249-254
|
||||
#[derive(Deserialize)]
|
||||
pub struct COSERSAKey {
|
||||
pub n: Vec<u8>,
|
||||
pub e: [u8; 3],
|
||||
}
|
||||
|
||||
impl From<COSERSAKey> for webauthn_rs::prelude::COSERSAKey {
|
||||
fn from(value: COSERSAKey) -> Self {
|
||||
Self {
|
||||
n: value.n.into(),
|
||||
e: value.e,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copied from https://docs.rs/webauthn-rs/0.3.2/src/webauthn_rs/proto.rs.html#235-242
|
||||
#[derive(Deserialize)]
|
||||
pub struct COSEEC2Key {
|
||||
pub curve: webauthn_rs::prelude::ECDSACurve,
|
||||
pub x: [u8; 32],
|
||||
pub y: [u8; 32],
|
||||
}
|
||||
|
||||
impl From<COSEEC2Key> for webauthn_rs::prelude::COSEEC2Key {
|
||||
fn from(value: COSEEC2Key) -> Self {
|
||||
Self {
|
||||
curve: value.curve,
|
||||
x: value.x.into(),
|
||||
y: value.y.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Local methods
|
||||
impl TwoFactor {
|
||||
pub fn new(user_uuid: UserId, atype: TwoFactorType, data: String) -> Self {
|
||||
|
@ -160,7 +262,8 @@ impl TwoFactor {
|
|||
|
||||
use crate::api::core::two_factor::webauthn::U2FRegistration;
|
||||
use crate::api::core::two_factor::webauthn::{get_webauthn_registrations, WebauthnRegistration};
|
||||
use webauthn_rs::proto::*;
|
||||
use webauthn_rs::prelude::{COSEEC2Key, COSEKey, COSEKeyType, ECDSACurve};
|
||||
use webauthn_rs_proto::{COSEAlgorithm, UserVerificationPolicy};
|
||||
|
||||
for mut u2f in u2f_factors {
|
||||
let mut regs: Vec<U2FRegistration> = serde_json::from_str(&u2f.data)?;
|
||||
|
@ -184,8 +287,8 @@ impl TwoFactor {
|
|||
type_: COSEAlgorithm::ES256,
|
||||
key: COSEKeyType::EC_EC2(COSEEC2Key {
|
||||
curve: ECDSACurve::SECP256R1,
|
||||
x,
|
||||
y,
|
||||
x: x.into(),
|
||||
y: y.into(),
|
||||
}),
|
||||
};
|
||||
|
||||
|
@ -195,11 +298,18 @@ impl TwoFactor {
|
|||
name: reg.name.clone(),
|
||||
credential: Credential {
|
||||
counter: reg.counter,
|
||||
verified: false,
|
||||
user_verified: false,
|
||||
cred: key,
|
||||
cred_id: reg.reg.key_handle.clone(),
|
||||
registration_policy: UserVerificationPolicy::Discouraged,
|
||||
},
|
||||
cred_id: reg.reg.key_handle.clone().into(),
|
||||
registration_policy: UserVerificationPolicy::Discouraged_DO_NOT_USE,
|
||||
|
||||
transports: None,
|
||||
backup_eligible: false,
|
||||
backup_state: false,
|
||||
extensions: RegisteredExtensions::none(),
|
||||
attestation: ParsedAttestation::default(),
|
||||
attestation_format: AttestationFormat::None,
|
||||
}.into(),
|
||||
};
|
||||
|
||||
webauthn_regs.push(new_reg);
|
||||
|
@ -217,6 +327,10 @@ impl TwoFactor {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn migrate_credential_to_passkey(conn: &mut DbConn) -> EmptyResult {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, DieselNewType, FromForm, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
|
|
|
@ -54,7 +54,7 @@ use rocket::error::Error as RocketErr;
|
|||
use serde_json::{Error as SerdeErr, Value};
|
||||
use std::io::Error as IoErr;
|
||||
use std::time::SystemTimeError as TimeErr;
|
||||
use webauthn_rs::error::WebauthnError as WebauthnErr;
|
||||
use webauthn_rs::prelude::WebauthnError as WebauthnErr;
|
||||
use yubico::yubicoerror::YubicoError as YubiErr;
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
|
|
@ -66,6 +66,7 @@ pub use error::{Error, MapResult};
|
|||
use rocket::data::{Limits, ToByteUnit};
|
||||
use std::sync::{atomic::Ordering, Arc};
|
||||
pub use util::is_running_in_container;
|
||||
use crate::api::core::two_factor::webauthn::WEBAUTHN_2FA_CONFIG;
|
||||
|
||||
#[rocket::main]
|
||||
async fn main() -> Result<(), Error> {
|
||||
|
@ -86,6 +87,7 @@ async fn main() -> Result<(), Error> {
|
|||
let pool = create_db_pool().await;
|
||||
schedule_jobs(pool.clone());
|
||||
db::models::TwoFactor::migrate_u2f_to_webauthn(&mut pool.get().await.unwrap()).await.unwrap();
|
||||
db::models::TwoFactor::migrate_credential_to_passkey(&mut pool.get().await.unwrap()).await.unwrap();
|
||||
|
||||
let extra_debug = matches!(level, log::LevelFilter::Trace | log::LevelFilter::Debug);
|
||||
launch_rocket(pool, extra_debug).await // Blocks until program termination.
|
||||
|
@ -597,6 +599,7 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error>
|
|||
.manage(pool)
|
||||
.manage(Arc::clone(&WS_USERS))
|
||||
.manage(Arc::clone(&WS_ANONYMOUS_SUBSCRIPTIONS))
|
||||
.manage(Arc::clone(&WEBAUTHN_2FA_CONFIG))
|
||||
.attach(util::AppHeaders())
|
||||
.attach(util::Cors())
|
||||
.attach(util::BetterLogging(extra_debug))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue