From 7c67e16128e0c8442d430a03cf80fac0bb509150 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus Date: Tue, 29 Apr 2025 10:15:38 +0200 Subject: [PATCH] Implement webauthn-extensions. --- webext/add-on/background.js | 33 ++ webext/add-on/content.js | 42 ++- webext/add-on/manifest.json | 4 +- .../Cargo.lock | 2 +- .../Cargo.toml | 3 +- .../src/dbus.rs | 182 ++++++---- .../src/platform_authenticator/mod.rs | 78 +++- .../src/platform_authenticator/store.rs | 2 +- .../src/webauthn.rs | 333 +++++++++++++----- 9 files changed, 492 insertions(+), 187 deletions(-) diff --git a/webext/add-on/background.js b/webext/add-on/background.js index b50bed8..9c22889 100644 --- a/webext/add-on/background.js +++ b/webext/add-on/background.js @@ -78,6 +78,39 @@ function serializeRequest(options) { cred.id = serializeBytes(cred.id); } } + if (clone.publicKey.extensions && clone.publicKey.extensions.prf) { + if (clone.publicKey.extensions.prf.eval) { + clone.publicKey.extensions.prf.eval.first = serializeBytes(clone.publicKey.extensions.prf.eval.first); + if (clone.publicKey.extensions.prf.eval.second) { + clone.publicKey.extensions.prf.eval.second = serializeBytes(clone.publicKey.extensions.prf.eval.second); + } + } + if (clone.publicKey.extensions.prf.evalByCredential) { + const evalByCredential = clone.publicKey.extensions.prf.evalByCredential; + + // Iterate over all credentialIDs, serialize the first/second bytebuffer and replace the original evalByCredential map + const result = {}; + for (const credId in evalByCredentialData) { + const prfValue = evalByCredentialData[credId]; + + if (prfValue && prfValue.first) { + const newPrfValue = { + first: serializeBytes(prfValue.first) + }; + + if (prfValue.second) { + newPrfValue.second = serializeBytes(prfValue.second); + } + result[credId] = newPrfValue; + }; + } + clone.publicKey.extensions.prf.evalByCredential = result; + } + + if (clone.publicKey.extensions && clone.publicKey.extensions.credBlob) { + clone.publicKey.extensions.credBlob = serializeBytes(clone.publicKey.extensions.credBlob); + } + } return clone } diff --git a/webext/add-on/content.js b/webext/add-on/content.js index e55eb72..2ff5111 100644 --- a/webext/add-on/content.js +++ b/webext/add-on/content.js @@ -29,6 +29,7 @@ function endRequest(requestId, data, error) { request.resolve(data) } } + async function cloneCredentialResponse(credential) { try { const options = { alphabet: "base64url" } @@ -82,13 +83,44 @@ async function cloneCredentialResponse(credential) { else { throw cloneInto(new Error("Unknown credential response type received"), window) } + + // Unlike CreatePublicKey, for GetPublicKey, we have a lot of Byte arrays, + // so we need a lot of deconstructions. So no: obj.clientExtensionResults = cloneInto(credential.clientExtensionResults, obj); + const extensions = {} + if (credential.clientExtensionResults) { + if (credential.clientExtensionResults.hmac_get_secret) { + extensions.hmac_get_secret = {} + extensions.hmac_get_secret.output1 = Uint8Array.fromBase64(credential.clientExtensionResults.hmac_get_secret.output1, options); + if (credential.clientExtensionResults.hmac_get_secret.output2) { + extensions.hmac_get_secret.output2 = Uint8Array.fromBase64(credential.clientExtensionResults.hmac_get_secret.output2, options); + } + } + + if (credential.clientExtensionResults.prf) { + extensions.prf = {} + if (credential.clientExtensionResults.prf.results) { + extensions.prf.results = {} + extensions.prf.results.first = Uint8Array.fromBase64(credential.clientExtensionResults.prf.results.first, options); + if (credential.clientExtensionResults.prf.results.second) { + extensions.prf.results.second = Uint8Array.fromBase64(credential.clientExtensionResults.prf.results.second, options); + } + } + } + + if (credential.clientExtensionResults.large_blob) { + extensions.large_blob = {} + if (credential.clientExtensionResults.large_blob.blob) { + extensions.large_blob.blob = Uint8Array.fromBase64(credential.clientExtensionResults.large_blob.blob, options); + } + } + } obj.response = cloneInto(response, obj, { cloneFunctions: true }) - obj.clientExtensionResults = new window.Object(); + obj.clientExtensionResults = extensions; obj.getClientExtensionResults = function() { - // TODO - return this.clientExtensionResults + return this.clientExtensionResults; } obj.type = "public-key" + obj.toJSON = function() { json = new window.Object(); json.id = this.id @@ -115,8 +147,8 @@ async function cloneCredentialResponse(credential) { throw cloneInto(new Error("Unknown credential type received"), window) } - json.authenticatorAttachment = this.authenticatorAttachment - json.clientExtensionResults = this.clientExtensionResults + json.authenticatorAttachment = this.authenticatorAttachment; + json.clientExtensionResults = this.clientExtensionResults; json.type = this.type return json } diff --git a/webext/add-on/manifest.json b/webext/add-on/manifest.json index 58ec1e7..f2135a1 100644 --- a/webext/add-on/manifest.json +++ b/webext/add-on/manifest.json @@ -1,5 +1,4 @@ { - "description": "Linux WebAuthn Desktop Portal Shim", "manifest_version": 3, "name": "WebAuthn Portal", @@ -20,7 +19,7 @@ }, "content_scripts": [ { - "matches": ["https://webauthn.io/*"], + "matches": ["https://webauthn.io/*", "https://demo.yubico.com/*"], "js": ["content.js"], "run_at": "document_start" } @@ -31,5 +30,4 @@ }, "permissions": ["nativeMessaging"] - } diff --git a/xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock b/xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock index fb54024..535978d 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock +++ b/xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock @@ -1950,7 +1950,7 @@ dependencies = [ [[package]] name = "libwebauthn" version = "0.1.2" -source = "git+https://github.com/linux-credentials/libwebauthn?rev=24eb47113e2282ff31c53de3029928e914349559#24eb47113e2282ff31c53de3029928e914349559" +source = "git+https://github.com/linux-credentials/libwebauthn?rev=dc23daed528f512f2bcb61fce9eb6b8ee74066e2#dc23daed528f512f2bcb61fce9eb6b8ee74066e2" dependencies = [ "aes", "async-trait", diff --git a/xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml b/xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml index ddb3208..6b643aa 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml +++ b/xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml @@ -16,10 +16,11 @@ openssl = "0.10.72" ring = "0.17.14" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" +# serde_cbor = "0.11.1" tracing = "0.1.41" tracing-subscriber = "0.3" zbus = "5.5.0" -libwebauthn = { git = "https://github.com/linux-credentials/libwebauthn", rev = "24eb47113e2282ff31c53de3029928e914349559" } +libwebauthn = { git = "https://github.com/linux-credentials/libwebauthn", rev = "dc23daed528f512f2bcb61fce9eb6b8ee74066e2" } async-trait = "0.1.88" tokio = { version = "1", features = ["rt-multi-thread"] } diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs index 467128a..2ba2572 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs @@ -1,15 +1,20 @@ +use std::collections::HashMap; use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; use async_std::channel::{Receiver, Sender}; use async_std::sync::Mutex as AsyncMutex; +use base64::Engine; +use base64::{self, engine::general_purpose::URL_SAFE_NO_PAD}; use gettextrs::{gettext, LocaleCategory}; use gtk::{gio, glib}; use libwebauthn::ops::webauthn::{ - Assertion, GetAssertionRequest, MakeCredentialRequest, MakeCredentialResponse, - UserVerificationRequirement, + Assertion, CredentialProtectionExtension, GetAssertionHmacOrPrfInput, + GetAssertionLargeBlobExtension, GetAssertionRequest, GetAssertionRequestExtensions, + MakeCredentialHmacOrPrfInput, MakeCredentialRequest, MakeCredentialResponse, + MakeCredentialsRequestExtensions, UserVerificationRequirement, }; use libwebauthn::proto::ctap2::{ Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity, @@ -23,12 +28,13 @@ use zbus::{ use crate::application::ExampleApplication; use crate::config::{GETTEXT_PACKAGE, LOCALEDIR, RESOURCES_FILE}; -use crate::cose; use crate::credential_service::CredentialService; use crate::view_model::CredentialType; use crate::view_model::Operation; use crate::view_model::{self, ViewEvent, ViewUpdate}; -use crate::webauthn::{self, PublicKeyCredentialParameters}; +use crate::webauthn::{ + self, GetPublicKeyCredentialUnsignedExtensionsResponse, PublicKeyCredentialParameters, +}; use ring::digest; pub(crate) async fn start_service(service_name: &str, path: &str) -> Result { @@ -128,10 +134,10 @@ impl CredentialManager { "xyz.iinuwa.credentials.CredentialManager:local".to_string(), ); let (make_cred_request, client_data_json) = - request.clone().try_into_ctap2_request().map_err(|_| { - fdo::Error::Failed( - "Could not parse passkey creation request.".to_owned(), - ) + request.clone().try_into_ctap2_request().map_err(|e| { + fdo::Error::Failed(format!( + "Could not parse passkey creation request: {e:?}" + )) })?; let request = CredentialRequest::CreatePublicKeyCredentialRequest(make_cred_request); @@ -404,7 +410,41 @@ impl CreateCredentialRequest { } else { (false, UserVerificationRequirement::Preferred) }; - let extensions = None; + let extensions = if let Some(incoming_extensions) = other_options.extensions { + let extensions = MakeCredentialsRequestExtensions { + cred_blob: incoming_extensions + .cred_blob + .and_then(|x| URL_SAFE_NO_PAD.decode(x).ok()), + min_pin_length: incoming_extensions.min_pin_length, + cred_protect: match incoming_extensions.cred_protect_policy { + Some(cred_prot_policy) => Some(CredentialProtectionExtension { + policy: cred_prot_policy, + enforce_policy: incoming_extensions + .enforce_credential_protection_policy + .unwrap_or_default(), + }), + None => None, + }, + large_blob: incoming_extensions + .large_blob + .map(|x| x.support.unwrap_or_default()) + .unwrap_or_default(), + hmac_or_prf: if incoming_extensions.prf.is_some() { + // CTAP currently doesn't support PRF queries at credentials.create() + // So we ignore any potential value set in the request and only mark this + // credential to activate HMAC for future PRF queries using credentials.get() + MakeCredentialHmacOrPrfInput::Prf + } else { + // MakeCredentialHmacOrPrfInput::Hmac is not used directly by webauthn + MakeCredentialHmacOrPrfInput::None + }, + ..Default::default() + }; + Some(extensions) + } else { + None + }; + let credential_parameters = request_value .clone() .get("pubKeyCredParams") @@ -494,29 +534,13 @@ impl CreatePublicKeyCredentialResponse { let attested_credential = auth_data.attested_credential.as_ref().ok_or_else(|| { fdo::Error::Failed("Invalid credential received from authenticator".to_string()) })?; - let public_key = cose::encode_cose_key(&attested_credential.credential_public_key) - .map_err(|_| { - fdo::Error::Failed(format!( - "Unable to serialize public key type: {:?}", - &attested_credential.credential_public_key - )) - })?; - let attested_credential_data = webauthn::create_attested_credential_data( - &attested_credential.credential_id, - &public_key, - &attested_credential.aaguid, - ) - .unwrap(); - // TODO: what's the format for extensions... JSON? - // let extensions = &auth_data.extensions.as_ref().map(|e| serde_json::to_vec(&e).unwrap()).as_deref(); - let authenticator_data_blob = webauthn::create_authenticator_data( - &auth_data.rp_id_hash, - &auth_data.flags, - (&auth_data).signature_count, - Some(&attested_credential_data), - None, - ); + let unsigned_extensions = response + .ctap + .unsigned_extensions_output + .as_ref() + .map(|extensions| serde_json::to_string(&extensions).unwrap()); + let authenticator_data_blob = auth_data.to_response_bytes().unwrap(); let attestation_statement = (&response.ctap.attestation_statement) .try_into() @@ -536,14 +560,14 @@ impl CreatePublicKeyCredentialResponse { authenticator_data_blob, client_data_json, Some(response.transport.clone()), - None, + unsigned_extensions, response.attachment_modality.clone(), ) .to_json(); let response = CreatePublicKeyCredentialResponse { registration_response_json, }; - Ok(response.into()) + Ok(response) } } @@ -668,12 +692,43 @@ impl GetCredentialRequest { let (_, effective_domain) = origin.rsplit_once('/').unwrap(); effective_domain.to_string() }); - // TODO(extensions-support) - let extensions = None; + + let extensions = if let Some(incoming_extensions) = request.extensions { + let extensions = GetAssertionRequestExtensions { + cred_blob: incoming_extensions.get_cred_blob, + hmac_or_prf: incoming_extensions + .prf + .and_then(|x| { + x.eval.map(|eval| { + let eval = Some(eval.decode()); + let mut eval_by_credential = HashMap::new(); + if let Some(incoming_eval) = x.eval_by_credential { + for (key, val) in incoming_eval.iter() { + eval_by_credential.insert(key.clone(), val.decode()); + } + } + GetAssertionHmacOrPrfInput::Prf { + eval, + eval_by_credential, + } + }) + }) + .unwrap_or_default(), + large_blob: incoming_extensions + .large_blob + // TODO: Implement GetAssertionLargeBlobExtension::Write, once libwebauthn supports it + .filter(|x| x.read == Some(true)) + .map(|_| GetAssertionLargeBlobExtension::Read) + .unwrap_or(GetAssertionLargeBlobExtension::None), + }; + Some(extensions) + } else { + None + }; + Ok(( GetAssertionRequest { hash: client_data_hash, - relying_party_id, user_verification, allow, @@ -703,40 +758,23 @@ impl GetPublicKeyCredentialResponse { response: &GetAssertionResponseInternal, client_data_json: String, ) -> std::result::Result { - let auth_data = &response.ctap.authenticator_data; - let attested_credential_data = match &auth_data.attested_credential { - None => None, - Some(att) => { - let public_key = - cose::encode_cose_key(&att.credential_public_key).map_err(|_| { - fdo::Error::Failed(format!( - "Unable to serialize public key type: {:?}", - &att.credential_public_key - )) - })?; - let data = webauthn::create_attested_credential_data( - &att.credential_id, - &public_key, - &att.aaguid, - ) - .map_err(|_| { - zbus::Error::Failure("Failed to parse attested credential data".to_string()) - })?; - Some(data) - } - }; - - // TODO: what's the format for extensions... CBOR? - // let ext = auth_data.extensions.as_ref().map(|e| serde_json::to_vec(&e).unwrap()).as_deref(); - let extensions = None; - - let authenticator_data_blob = webauthn::create_authenticator_data( - &auth_data.rp_id_hash, - &auth_data.flags, - (&auth_data).signature_count, - attested_credential_data.as_deref(), - extensions, - ); + let authenticator_data_blob = response + .ctap + .authenticator_data + .to_response_bytes() + .unwrap(); + + // We can't just do this here, because we need encode all byte arrays for the JS-communication: + // let unsigned_extensions = response + // .ctap + // .unsigned_extensions_output + // .as_ref() + // .map(|extensions| serde_json::to_string(&extensions).unwrap()); + let unsigned_extensions = response + .ctap + .unsigned_extensions_output + .as_ref() + .map(GetPublicKeyCredentialUnsignedExtensionsResponse::from); let authentication_response_json = webauthn::GetPublicKeyCredentialResponse::new( client_data_json, @@ -749,12 +787,14 @@ impl GetPublicKeyCredentialResponse { response.ctap.signature.clone(), response.ctap.user.as_ref().map(|u| u.id.clone().into_vec()), response.attachment_modality.clone(), + unsigned_extensions, ) .to_json(); + let response = GetPublicKeyCredentialResponse { authentication_response_json, }; - Ok(response.into()) + Ok(response) } } diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/platform_authenticator/mod.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/platform_authenticator/mod.rs index 6240cd1..6b401a9 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/platform_authenticator/mod.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/platform_authenticator/mod.rs @@ -13,13 +13,13 @@ use ring::{ ECDSA_P256_SHA256_ASN1_SIGNING, RSA_PKCS1_SHA256, }, }; +use serde::Deserialize; use crate::cose::{encode_pkcs8_key, CoseKeyType}; use crate::webauthn::{ self, AttestationStatement, AttestationStatementFormat, CreatePublicKeyCredentialResponse, CredentialDescriptor, CredentialSource, Error as WebAuthnError, GetPublicKeyCredentialResponse, - MakeCredentialOptions, PublicKeyCredentialParameters, PublicKeyCredentialType, RelyingParty, - User, + MakeCredentialOptions, PublicKeyCredentialParameters, PublicKeyCredentialType, }; static P256: &EcdsaSigningAlgorithm = &ECDSA_P256_SHA256_ASN1_SIGNING; @@ -74,6 +74,65 @@ async fn create_passkey( } */ +#[derive(Deserialize)] +pub(crate) struct RelyingParty { + pub name: String, + pub id: String, +} + +/// https://www.w3.org/TR/webauthn-3/#dictionary-user-credential-params +#[derive(Deserialize)] +pub(crate) struct User { + pub id: String, + pub name: String, + #[serde(rename = "displayName")] + pub display_name: String, +} + +struct Assertion {} + +pub(crate) fn create_attested_credential_data( + credential_id: &[u8], + public_key: &[u8], + aaguid: &[u8], +) -> Result, webauthn::Error> { + let mut attested_credential_data: Vec = Vec::new(); + if aaguid.len() != 16 { + return Err(webauthn::Error::Unknown); + } + attested_credential_data.extend(aaguid); + let cred_length: u16 = TryInto::::try_into(credential_id.len()).unwrap(); + let cred_length_bytes: Vec = cred_length.to_be_bytes().to_vec(); + attested_credential_data.extend(&cred_length_bytes); + attested_credential_data.extend(credential_id); + attested_credential_data.extend(public_key); + Ok(attested_credential_data) +} + +pub(crate) fn create_authenticator_data( + rp_id_hash: &[u8], + flags: &AuthenticatorDataFlags, + signature_counter: u32, + attested_credential_data: Option<&[u8]>, + processed_extensions: Option<&[u8]>, +) -> Vec { + let mut authenticator_data: Vec = Vec::new(); + authenticator_data.extend(rp_id_hash); + + authenticator_data.push(flags.bits()); + + authenticator_data.extend(signature_counter.to_be_bytes()); + + if let Some(attested_credential_data) = attested_credential_data { + authenticator_data.extend(attested_credential_data); + } + + if let Some(extensions) = processed_extensions { + authenticator_data.extend(extensions); + } + authenticator_data +} + pub(crate) fn create_credential( origin: &str, options: &str, @@ -343,12 +402,12 @@ pub(crate) fn make_credential( let aaguid = vec![0_u8; 16]; let public_key = encode_pkcs8_key(key_type, &key_pair).map_err(|_| WebAuthnError::Unknown)?; let attested_credential_data = - webauthn::create_attested_credential_data(&credential_id, &public_key, &aaguid)?; + create_attested_credential_data(&credential_id, &public_key, &aaguid)?; flags = flags | AuthenticatorDataFlags::ATTESTED_CREDENTIALS; // Let authenticatorData be the byte array specified in § 6.1 Authenticator Data, including attestedCredentialData as the attestedCredentialData and processedExtensions, if any, as the extensions. let rp_id_hash = ring::digest::digest(&digest::SHA256, &credential_source.rp_id.as_bytes()); - let authenticator_data = webauthn::create_authenticator_data( + let authenticator_data = create_authenticator_data( rp_id_hash.as_ref(), &flags, signature_counter, @@ -504,14 +563,13 @@ fn get_credential( // TODO: Assign AAGUID? let aaguid = vec![0_u8; 16]; let attested_credential_data = if *attestation_format != AttestationStatementFormat::None { - webauthn::create_attested_credential_data(&selected_credential.id, &public_key, &aaguid) - .ok() + create_attested_credential_data(&selected_credential.id, &public_key, &aaguid).ok() } else { None }; // Let authenticatorData be the byte array specified in § 6.1 Authenticator Data including processedExtensions, if any, as the extensions and excluding attestedCredentialData. This authenticatorData MUST include attested credential data if, and only if, attestationFormat is not none. let rp_id_hash = digest::digest(&digest::SHA256, selected_credential.rp_id.as_bytes()); - let authenticator_data = webauthn::create_authenticator_data( + let authenticator_data = create_authenticator_data( rp_id_hash.as_ref(), &flags, signature_counter, @@ -550,6 +608,7 @@ fn get_credential( // Note: In cases where allowCredentialDescriptorList was supplied the returned userHandle value may be null, see: userHandleResult. user_handle: selected_credential.user_handle.clone(), attachment_modality: String::from("platform"), + extensions: None, }; Ok(response) // If the authenticator cannot find any credential corresponding to the specified Relying Party that matches the specified criteria, it terminates the operation and returns an error. @@ -652,9 +711,8 @@ mod test { use crate::cose::encode_pkcs8_key; use crate::webauthn::{ - create_attestation_object, create_attested_credential_data, create_authenticator_data, - AttestationStatement, CredentialSource, PublicKeyCredentialParameters, - PublicKeyCredentialType, + create_attestation_object, AttestationStatement, CredentialSource, + PublicKeyCredentialParameters, PublicKeyCredentialType, }; use super::sign_attestation; diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/platform_authenticator/store.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/platform_authenticator/store.rs index bcacd72..5598fa6 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/platform_authenticator/store.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/platform_authenticator/store.rs @@ -8,7 +8,7 @@ use std::str::FromStr; use base64::{self, engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; use ring::rand::{self, SecureRandom}; -use crate::webauthn::{CredentialDescriptor, Error, RelyingParty}; +use crate::webauthn::Error; static mut CRED_DIR: String = String::new(); pub(crate) fn initialize() { diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs index 6ff6a26..563080d 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs @@ -1,15 +1,15 @@ -use std::time::Duration; +use std::{collections::HashMap, time::Duration}; -use base64::{self, engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; +use base64::{self, engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use libwebauthn::{ - fido::AuthenticatorDataFlags, + ops::webauthn::{CredentialProtectionPolicy, MakeCredentialLargeBlobExtension}, proto::ctap2::{ Ctap2AttestationStatement, Ctap2CredentialType, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialType, Ctap2Transport, }, }; use ring::digest; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use serde_json::json; use tracing::debug; use zbus::zvariant::{DeserializeDict, Type}; @@ -26,49 +26,6 @@ pub enum Error { Internal(String), } -pub(crate) fn create_attested_credential_data( - credential_id: &[u8], - public_key: &[u8], - aaguid: &[u8], -) -> Result, Error> { - let mut attested_credential_data: Vec = Vec::new(); - if aaguid.len() != 16 { - return Err(Error::Unknown); - } - attested_credential_data.extend(aaguid); - let cred_length: u16 = TryInto::::try_into(credential_id.len()).unwrap(); - let cred_length_bytes: Vec = cred_length.to_be_bytes().to_vec(); - attested_credential_data.extend(&cred_length_bytes); - attested_credential_data.extend(credential_id); - attested_credential_data.extend(public_key); - Ok(attested_credential_data) -} - -pub(crate) fn create_authenticator_data( - rp_id_hash: &[u8], - flags: &AuthenticatorDataFlags, - signature_counter: u32, - attested_credential_data: Option<&[u8]>, - processed_extensions: Option<&[u8]>, -) -> Vec { - let mut authenticator_data: Vec = Vec::new(); - authenticator_data.extend(rp_id_hash); - - authenticator_data.push(flags.bits()); - - authenticator_data.extend(signature_counter.to_be_bytes()); - - if let Some(attested_credential_data) = attested_credential_data { - authenticator_data.extend(attested_credential_data); - } - - if processed_extensions.is_some() { - todo!("Implement processed extensions"); - // TODO: authenticator_data.append(processed_extensions.to_bytes()); - } - authenticator_data -} - pub(crate) fn create_attestation_object( authenticator_data: &[u8], attestation_statement: &AttestationStatement, @@ -113,23 +70,6 @@ pub(crate) fn create_attestation_object( Ok(attestation_object) } -#[derive(Deserialize)] -pub(crate) struct RelyingParty { - pub name: String, - pub id: String, -} - -/// https://www.w3.org/TR/webauthn-3/#dictionary-user-credential-params -#[derive(Deserialize)] -pub(crate) struct User { - pub id: String, - pub name: String, - #[serde(rename = "displayName")] - pub display_name: String, -} - -struct Assertion {} - /* #[derive(DeserializeDict, Type)] #[zvariant(signature = "dict")] @@ -156,7 +96,7 @@ pub(crate) struct AssertionOptions { user_presence: Option, } -#[derive(Deserialize)] +#[derive(Debug, Deserialize)] pub(crate) struct MakeCredentialOptions { /// Timeout in milliseconds #[serde(deserialize_with = "crate::serde::duration::from_opt_ms")] @@ -169,11 +109,77 @@ pub(crate) struct MakeCredentialOptions { /// https://www.w3.org/TR/webauthn-3/#enum-attestation-convey pub attestation: Option, /// extensions input as a JSON object - #[serde(rename = "extensionData")] - pub extension_data: Option, + pub extensions: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct MakeCredentialExtensions { + #[serde(skip_serializing_if = "Option::is_none")] + pub cred_blob: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cred_props: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub min_pin_length: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cred_protect_policy: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub enforce_credential_protection_policy: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub large_blob: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub prf: Option, +} + +#[derive(Debug, Default, Deserialize)] +pub(crate) struct LargeBlobExtension { + #[serde(skip_serializing_if = "Option::is_none")] + pub support: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub read: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub write: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct Prf { + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) eval: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) eval_by_credential: Option>, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PRFValue { + // base64 encoded data + pub first: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub second: Option, } -#[derive(Deserialize)] +impl PRFValue { + pub(crate) fn decode(&self) -> libwebauthn::ops::webauthn::PRFValue { + let mut res = libwebauthn::ops::webauthn::PRFValue::default(); + let first = URL_SAFE_NO_PAD.decode(&self.first).unwrap(); + let len_to_copy = std::cmp::min(first.len(), 32); // Determine how many bytes to copy + res.first[..len_to_copy].copy_from_slice(&first[..len_to_copy]); + if let Some(second) = self + .second + .as_ref() + .map(|second| URL_SAFE_NO_PAD.decode(second).unwrap()) + { + let len_to_copy = std::cmp::min(second.len(), 32); // Determine how many bytes to copy + let mut res_second = [0u8; 32]; + res_second[..len_to_copy].copy_from_slice(&second[..len_to_copy]); + res.second = Some(res_second); + } + res + } +} + +#[derive(Debug, Deserialize)] pub(crate) struct GetCredentialOptions { /// Challenge bytes in base64url-encoding with no padding. pub(crate) challenge: String, @@ -202,12 +208,22 @@ pub(crate) struct GetCredentialOptions { #[serde(default)] pub(crate) hints: Vec, - extensions: Option<()>, + pub(crate) extensions: Option, } -// pub(crate) struct CredentialList(Vec); +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GetCredentialExtensions { + // TODO: appid + #[serde(skip_serializing_if = "Option::is_none")] + pub get_cred_blob: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub large_blob: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub prf: Option, +} -#[derive(Deserialize, Type)] +#[derive(Debug, Deserialize, Type)] #[zvariant(signature = "dict")] /// https://www.w3.org/TR/webauthn-3/#dictionary-credential-descriptor pub(crate) struct CredentialDescriptor { @@ -260,7 +276,7 @@ impl TryFrom for Ctap2PublicKeyCredentialDescriptor { } } -#[derive(DeserializeDict, Type)] +#[derive(Debug, DeserializeDict, Type)] #[zvariant(signature = "dict")] /// https://www.w3.org/TR/webauthn-3/#dictionary-authenticatorSelection pub(crate) struct AuthenticatorSelectionCriteria { @@ -428,6 +444,7 @@ impl TryFrom<&Ctap2AttestationStatement> for AttestationStatement { } } } + pub struct CreatePublicKeyCredentialResponse { cred_type: String, @@ -443,6 +460,61 @@ pub struct CreatePublicKeyCredentialResponse { attachment_modality: String, } +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CredentialPropertiesOutput { + /// This OPTIONAL property, known abstractly as the resident key credential property (i.e., client-side discoverable credential property), is a Boolean value indicating whether the PublicKeyCredential returned as a result of a registration ceremony is a client-side discoverable credential. If rk is true, the credential is a discoverable credential. if rk is false, the credential is a server-side credential. If rk is not present, it is not known whether the credential is a discoverable credential or a server-side credential. + #[serde(skip_serializing_if = "Option::is_none")] + pub rk: Option, +} + +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthenticationExtensionsLargeBlobOutputs { + /// true if, and only if, the created credential supports storing large blobs. Only present in registration outputs. + #[serde(skip_serializing_if = "Option::is_none")] + pub supported: Option, + /// The opaque byte string that was associated with the credential identified by rawId. Only valid if read was true. + #[serde(skip_serializing_if = "Option::is_none")] + pub blob: Option>, + /// A boolean that indicates that the contents of write were successfully stored on the authenticator, associated with the specified credential. + #[serde(skip_serializing_if = "Option::is_none")] + pub written: Option, +} + +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthenticationExtensionsPRFValues { + pub first: Vec, + pub second: Option>, +} + +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthenticationExtensionsPRFOutputs { + /// true if, and only if, the one or two PRFs are available for use with the created credential. This is only reported during registration and is not present in the case of authentication. + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + /// The results of evaluating the PRF for the inputs given in eval or evalByCredential. Outputs may not be available during registration; see comments in eval. + #[serde(skip_serializing_if = "Option::is_none")] + pub results: Option, +} + +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreatePublicKeyExtensionsResponse { + #[serde(skip_serializing_if = "Option::is_none")] + pub cred_props: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub large_blob: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub prf: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cred_protect: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub min_pin_length: Option, +} + /// Returned from a creation of a new public key credential. pub struct AttestationResponse { /// clientDataJSON. @@ -498,24 +570,23 @@ impl CreatePublicKeyCredentialResponse { pub fn to_json(&self) -> String { let response = json!({ - "clientDataJSON": URL_SAFE_NO_PAD.encode(&self.response.client_data_json.as_bytes()), + "clientDataJSON": URL_SAFE_NO_PAD.encode(self.response.client_data_json.as_bytes()), "attestationObject": URL_SAFE_NO_PAD.encode(&self.response.attestation_object), "transports": self.response.transports, }); - let mut output = json!({ + let extensions = if let Some(extensions) = &self.extensions { + serde_json::from_str(extensions).expect("Extensions json to be formatted properly") + } else { + // extensions field seems to be required, even if empty + json!({}) + }; + let output = json!({ "id": self.get_id(), "rawId": self.get_id(), "response": response, "authenticatorAttachment": self.attachment_modality, + "clientExtensionResults": extensions, }); - if let Some(extensions) = &self.extensions { - let extension_value = - serde_json::from_str(extensions).expect("Extensions json to be formatted properly"); - output - .as_object_mut() - .unwrap() - .insert("clientExtensionResults".to_string(), extension_value); - } output.to_string() } } @@ -544,6 +615,85 @@ pub struct GetPublicKeyCredentialResponse { /// Whether the used device is "cross-platform" (aka "roaming", i.e.: can be /// removed from the platform) or is built-in ("platform"). pub(crate) attachment_modality: String, + + /// Unsigned extension output + /// Unlike CreatePublicKey, we can't use a directly serialized JSON string here, + /// because we have to encode/decode the byte arrays for the JavaScript-communication + pub(crate) extensions: Option, +} + +#[derive(Clone, Debug, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetPublicKeyCredentialHMACGetSecretOutput { + // base64-encoded bytestring + pub output1: String, + #[serde(skip_serializing_if = "Option::is_none")] + // base64-encoded bytestring + pub output2: Option, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)] +pub struct GetPublicKeyCredentialLargeBlobOutput { + #[serde(skip_serializing_if = "Option::is_none")] + // base64-encoded bytestring + pub blob: Option, + // Not yet supported + // #[serde(skip_serializing_if = "Option::is_none")] + // pub written: Option, +} + +#[derive(Debug, Default, Clone, Serialize)] +pub struct GetPublicKeyCredentialPrfOutput { + #[serde(skip_serializing_if = "Option::is_none")] + pub results: Option, +} + +#[derive(Debug, Default, Clone, Serialize)] +pub struct GetPublicKeyCredentialPRFValue { + // base64-encoded bytestring + pub first: String, + #[serde(skip_serializing_if = "Option::is_none")] + // base64-encoded bytestring + pub second: Option, +} + +#[derive(Debug, Default, Clone, Serialize)] +pub struct GetPublicKeyCredentialUnsignedExtensionsResponse { + pub hmac_get_secret: Option, + pub large_blob: Option, + pub prf: Option, +} + +// Unlike CreatePublicKey, for GetPublicKey, we have a lot of Byte arrays, +// so we need a lot of de/constructions, instead of serializing it directly +impl From<&libwebauthn::ops::webauthn::GetAssertionResponseUnsignedExtensions> + for GetPublicKeyCredentialUnsignedExtensionsResponse +{ + fn from(value: &libwebauthn::ops::webauthn::GetAssertionResponseUnsignedExtensions) -> Self { + Self { + hmac_get_secret: value.hmac_get_secret.as_ref().map(|x| { + GetPublicKeyCredentialHMACGetSecretOutput { + output1: URL_SAFE_NO_PAD.encode(x.output1), + output2: x.output2.map(|output2| URL_SAFE_NO_PAD.encode(output2)), + } + }), + large_blob: value + .large_blob + .as_ref() + .map(|x| GetPublicKeyCredentialLargeBlobOutput { + blob: x.blob.as_ref().map(|blob| URL_SAFE_NO_PAD.encode(blob)), + }), + prf: value.prf.as_ref().map(|x| GetPublicKeyCredentialPrfOutput { + results: x + .results + .as_ref() + .map(|results| GetPublicKeyCredentialPRFValue { + first: URL_SAFE_NO_PAD.encode(results.first), + second: results.second.map(|second| URL_SAFE_NO_PAD.encode(second)), + }), + }), + } + } } impl GetPublicKeyCredentialResponse { @@ -554,6 +704,7 @@ impl GetPublicKeyCredentialResponse { signature: Vec, user_handle: Option>, attachment_modality: String, + extensions: Option, ) -> Self { Self { cred_type: "public-key".to_string(), @@ -563,11 +714,12 @@ impl GetPublicKeyCredentialResponse { signature, user_handle, attachment_modality, + extensions, } } pub fn to_json(&self) -> String { let response = json!({ - "clientDataJSON": URL_SAFE_NO_PAD.encode(&self.client_data_json.as_bytes()), + "clientDataJSON": URL_SAFE_NO_PAD.encode(self.client_data_json.as_bytes()), "authenticatorData": URL_SAFE_NO_PAD.encode(&self.authenticator_data), "signature": URL_SAFE_NO_PAD.encode(&self.signature), "userHandle": self.user_handle.as_ref().map(|h| URL_SAFE_NO_PAD.encode(h)) @@ -578,23 +730,14 @@ impl GetPublicKeyCredentialResponse { // This means we'll have to remember the ID on the request if the allow-list has exactly one // credential descriptor, then we'll need. This should probably be done in libwebauthn. let id = self.raw_id.as_ref().map(|id| URL_SAFE_NO_PAD.encode(id)); + let output = json!({ "id": id, "rawId": id, "authenticatorAttachment": self.attachment_modality, - "response": response + "response": response, + "clientExtensionResults": self.extensions, }); - // TODO: support client extensions - /* - if let Some(extensions) = &self.extensions { - let extension_value = - serde_json::from_str(extensions).expect("Extensions json to be formatted properly"); - output - .as_object_mut() - .unwrap() - .insert("clientExtensionResults".to_string(), extension_value); - } - */ output.to_string() } }