kanidm / webauthn-rs

An implementation of webauthn components for Rustlang servers
Mozilla Public License 2.0
464 stars 79 forks source link

No getTransports when attesting a security key #403

Open zacknewman opened 6 months ago

zacknewman commented 6 months ago

I was told that getTransports was recently added to the master branch; however I don't believe that is true. I cloned master just yesterday (i.e., after the comment was made), and I am still seeing null for transports in the JSON:

{"cred":{"cred_id":"bDtrPzJpvN4FLrD3Cc5TI8r_B4do93OpnjWwsQpd1zrFttEnyaAt7BPG9etzE3pU_QdM9F3gsPJ6diRLJ_BjXw","cred":{"type_":"ES256","key":{"EC_EC2":{"curve":"SECP256R1","x":"CNXexDRYjAW6xDTMnMweFfn2wvuclOx-qF6fvxEG0rY","y":"_pZlbgMvZUOpDDzsjd5-JujbjCVbexQJPMNd9K_1SGY"}}},"counter":2,"transports":null,"user_verified":true,"backup_eligible":false,"backup_state":false,"registration_policy":"preferred","extensions":{"cred_protect":"Ignored","hmac_create_secret":"NotRequested","appid":"NotRequested","cred_props":"Ignored"},"attestation":{"data":{"Basic":["MIIC2DCCAcCgAwIBAgIJAPkeEJBFzRXcMA0GCSqGSIb3DQEBCwUAMC4xLDAqBgNVBAMTI1l1YmljbyBVMkYgUm9vdCBDQSBTZXJpYWwgNDU3MjAwNjMxMCAXDTE0MDgwMTAwMDAwMFoYDzIwNTAwOTA0MDAwMDAwWjBuMQswCQYDVQQGEwJTRTESMBAGA1UECgwJWXViaWNvIEFCMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMScwJQYDVQQDDB5ZdWJpY28gVTJGIEVFIFNlcmlhbCAyNjk0OTE5NjEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATxBlFac6ZCOTaesMSVQq-qRj_pCX5LiTu2t9QhAnsXkanB0XQM4d_lfJW_YIlWvegwqiDVb9IFIBOHWC5patxso4GBMH8wEwYKKwYBBAGCxAoNAQQFBAMFBAMwIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjcwEwYLKwYBBAGC5RwCAQEEBAMCBSAwIQYLKwYBBAGC5RwBAQQEEgQQ7ogoeXIcSROXdT38zpcHKjAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCocCl0EQ8Ke0ySq5k4TyiCmDNWPz8KRz8lg4Gpliwg4KuWfK3S7klIfaqDpEkYUtlYsZPfckEL-_0u0ldjNzG63NwmEmFmKmAA1fVYRRSTfhiY6eIWeikeEajFkatlckQ7apazTTxCx2oO53B7PoGVzznPb1BcUKyCClGzrJg703nbNuwxcnGx6A6ctOomTC3InptvVPkVsC7ekYiTgAPanPJw0jCDSjPEaLVVh3Y7U79siV9fGFoazCoCwDpJ0AusKEZ-9HgZ9nx-zBUaHyPgiLetfCIh6y-yX68Jhc1QaWXOfFro81TLwc3kzKQe83B16eILycdxFRZWOROW2YpK"]},"metadata":{"Packed":{"aaguid":"ee882879-721c-4913-9775-3dfcce97072a"}}},"attestation_format":"Packed"}}}

Here is a snippet of the application that registers the key:

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct EnableWebauthnData {
    id: u32,
    name: String,
    device_response: RegisterPublicKeyCredential,
    master_password_hash: String,
}
fn build_webauthn() -> Result<Webauthn, WebauthnError> {
    WebauthnBuilder::new(
        config::get_config()
            .domain
            .domain()
            .expect("a valid domain"),
        &Url::parse(&config::get_config().domain_origin()).expect("a valid URL"),
    )?
    .build()
}
#[post("/two-factor/get-webauthn-challenge", data = "<data>")]
async fn generate_webauthn_challenge(
    data: JsonUpcase<PasswordOrOtpData>,
    headers: Headers,
    conn: DbConn,
) -> JsonResult {
    let data: PasswordOrOtpData = data.into_inner().data;
    let user = headers.user;
    data.validate(&user, false, &conn).await?;
    let mut ca_builder = webauthn_rs::prelude::AttestationCaListBuilder::new();
    // We only allow YubiKeys with firmware 5.2 or 5.4.
    ca_builder
        .insert_device_pem(
            b"-----BEGIN CERTIFICATE-----
MIIDHjCCAgagAwIBAgIEG0BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZ
dWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAw
MDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290
IENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk
5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep
8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbw
nebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT
9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXw
LvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJ
hjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAN
BgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4
MYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kt
hX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2k
LVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1U
sG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqc
U9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw==
-----END CERTIFICATE-----"
                .as_slice(),
            Uuid::try_parse("ee882879-721c-4913-9775-3dfcce97072a").expect("invaild UUID"),
            String::from("YubiKey 5"),
            alloc::collections::BTreeMap::new(),
        )
        .expect("unable to insert YubiKey 5C firwmare 5.2 and 5.4 attestation");
    let (challenge, registration) = build_webauthn()?.start_securitykey_registration(
        Uuid::try_parse(user.uuid.as_str()).expect("unable to create UUID"),
        user.email.as_str(),
        user.name.as_str(),
        Some(WebAuthn::get_all_credentials_by_user(&user.uuid, &conn).await?),
        Some(ca_builder.build()),
        Some(AuthenticatorAttachment::CrossPlatform),
    )?;
    // We replace any existing registration challenges.
    TwoFactor::new(
        user.uuid,
        TwoFactorType::WebauthnRegisterChallenge,
        serde_json::to_string(&registration)?,
    )
    .replace_challenge(&conn)
    .await?;
    let mut challenge_value = serde_json::to_value(challenge.public_key)?;
    challenge_value["status"] = "ok".into();
    challenge_value["errorMessage"] = "".into();
    Ok(Json(challenge_value))
}
#[post("/two-factor/webauthn", data = "<data>")]
async fn activate_webauthn(
    data: Json<EnableWebauthnData>,
    headers: Headers,
    conn: DbConn,
) -> JsonResult {
    let data = data.into_inner();
    let user = headers.user;
    PasswordOrOtpData {
        MasterPasswordHash: Some(data.master_password_hash),
        Otp: None,
    }
    .validate(&user, true, &conn)
    .await?;
    // Retrieve and delete the saved challenge state
    let tf_challenge = get_tf_entry(
        &user.uuid,
        i32::from(TwoFactorType::WebauthnRegisterChallenge),
        &conn,
    )
    .await
    .ok_or_else(|| Error::from(String::from("no webauthn challenge")))?;
    let registration = serde_json::from_str::<SecurityKeyRegistration>(&tf_challenge.data)?;
    tf_challenge.delete_challenge(&conn).await?;
    // Verify the credentials with the saved state
    let security_key =
        build_webauthn()?.finish_securitykey_registration(&data.device_response, &registration)?;
    let cred_id = security_key.cred_id().to_string();
    let regs = match get_tf_entry(&user.uuid, i32::from(TwoFactorType::Webauthn), &conn).await {
        None => {
            let regs = vec![WebauthnRegistration {
                id: data.id,
                name: data.name,
                security_key,
            }];
            let tf = TwoFactor::new(
                user.uuid,
                TwoFactorType::Webauthn,
                serde_json::to_string(&regs)?,
            );
            tf.insert_insert_webauthn(tf.create_webauthn(cred_id), &conn)
                .await?;
            regs
        }
        Some(mut tf) => {
            let mut regs = tf.get_webauthn_registrations()?;
            regs.push(WebauthnRegistration {
                id: data.id,
                name: data.name,
                security_key,
            });
            tf.data = serde_json::to_string(&regs)?;
            tf.update_insert_webauthn(tf.create_webauthn(cred_id), &conn)
                .await?;
            regs
        }
    };
    Ok(Json(json!({
        "Enabled": true,
        "Keys": regs.iter().map(WebauthnRegistration::to_json).collect::<Value>(),
        "Object": "twoFactorU2f"
    })))
}

I am saving the raw JSON payload to the database. Registration succeeds, and the payload is mostly correct. Can someone point to me where this was allegedly added, so I can help debug the issue? Perhaps this is a browser problem?

System information

[zack@laptop ~]$ uname -a
Linux laptop 6.6.8-arch1-1 #1 SMP PREEMPT_DYNAMIC Thu, 21 Dec 2023 19:01:01 +0000 x86_64 GNU/Linux
[zack@laptop ~]$ openssl version
OpenSSL 3.2.0 23 Nov 2023 (Library: OpenSSL 3.2.0 23 Nov 2023)

OS: Arch Linux Browser: Firefox 121.0 Client: Vaultwarden's patch to Bitwarden's web-vault v2023.12.0

zacknewman commented 6 months ago

Perhaps I am looking at the wrong thing, but getTransports appears to not be extracted from web_sys::PublicKeyCredential.

stefan0xC commented 2 months ago

https://github.com/kanidm/webauthn-rs/blob/3b80be72dcbb5f4eaece0b14a6c58b5570d14a7d/webauthn-rs-proto/src/attest.rs#L177-L178

I have not looked deeply into web_sys (and have no idea how it works) but maybe the feature PublicKeyCredentialDescriptor needs to be enabled for this to work? Cf. https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.PublicKeyCredentialDescriptor.html#method.transports

Firstyear commented 2 months ago

I just submitted the updated idl to web-sys for this.