From 879cf54ba047c5c7cbe68222e0a855943e6e6800 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus Date: Thu, 17 Apr 2025 15:23:58 +0200 Subject: [PATCH 1/4] Run rustfmt on the files about to be changed --- .../src/credential_service/mod.rs | 318 +++++++++------ .../src/dbus.rs | 382 ++++++++++++------ 2 files changed, 450 insertions(+), 250 deletions(-) diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/mod.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/mod.rs index bcaed94..36ea245 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/mod.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/mod.rs @@ -4,13 +4,27 @@ use std::{ time::{Duration, SystemTime, UNIX_EPOCH}, }; -use libwebauthn::{self, ops::webauthn::{GetAssertionResponse, MakeCredentialResponse}, pin::PinRequestReason, transport::Device as _, webauthn::{Error as WebAuthnError, WebAuthn}, UxUpdate}; +use libwebauthn::{ + self, + ops::webauthn::{GetAssertionResponse, MakeCredentialResponse}, + pin::PinRequestReason, + transport::Device as _, + webauthn::{Error as WebAuthnError, WebAuthn}, + UxUpdate, +}; -use async_std::{channel::TryRecvError, sync::{Arc as AsyncArc, Mutex as AsyncMutex}, task}; +use async_std::{ + channel::TryRecvError, + sync::{Arc as AsyncArc, Mutex as AsyncMutex}, + task, +}; use tokio::runtime::Runtime; use tracing::{debug, warn}; -use crate::{dbus::{CredentialRequest, CredentialResponse}, view_model::{Device, InternalPinState, Transport}}; +use crate::{ + dbus::{CredentialRequest, CredentialResponse}, + view_model::{Device, InternalPinState, Transport}, +}; #[derive(Debug)] pub struct CredentialService { @@ -24,14 +38,16 @@ pub struct CredentialService { internal_pin_attempts_left: u32, internal_pin_unlock_time: Option, - cred_request: CredentialRequest, // Place to store data to be returned to the caller cred_response: Arc>>, } impl CredentialService { - pub fn new(cred_request: CredentialRequest, cred_response: Arc>>) -> Self { + pub fn new( + cred_request: CredentialRequest, + cred_response: Arc>>, + ) -> Self { let devices = vec![ Device { id: String::from("0"), @@ -70,7 +86,6 @@ impl CredentialService { cred_request, cred_response, - } } @@ -99,147 +114,166 @@ impl CredentialService { } if devices.is_empty() { Ok(UsbState::Waiting) - } - else { + } else { Ok(UsbState::Connected) } - }, + } UsbState::Connected => { - // TODO: I'm not sure how we want to handle multiple usb devices - // just take the first one found for now. - // TODO: store this device reference, perhaps in the enum itself - let handler = self.usb_uv_handler.clone(); - let cred_request = self.cred_request.clone(); - let signal_tx = self.usb_uv_handler.signal_tx.clone(); - let pin_rx = self.usb_uv_handler.pin_rx.clone(); - tokio().spawn( async move { - let mut devices = libwebauthn::transport::hid::list_devices().await.unwrap(); - let device = devices.first_mut().unwrap(); - let (mut channel, state_rx) = device.channel().await.unwrap(); - tokio().spawn(async move { - handle_usb_updates(signal_tx, pin_rx, state_rx).await; - debug!("Reached end of USB update task"); - }); - match cred_request { - CredentialRequest::CreatePublicKeyCredentialRequest(make_cred_request) => loop { - match channel.webauthn_make_credential(&make_cred_request).await { - Ok(response) => { - handler.notify_ceremony_completed(AuthenticatorResponse::CredentialCreated(response)).await; - break - }, - Err(WebAuthnError::Ctap(ctap_error)) if ctap_error.is_retryable_user_error() => { - warn!("Retrying WebAuthn make credential operation"); - continue; - }, - Err(err) => { - handler.notify_ceremony_failed(err.to_string()).await; - break; - } - }; - }, - CredentialRequest::GetPublicKeyCredentialRequest(get_cred_request) => loop { - match channel.webauthn_get_assertion(&get_cred_request).await { - Ok(response) => { - handler.notify_ceremony_completed(AuthenticatorResponse::CredentialsAsserted(response)).await; - break - }, - Err(WebAuthnError::Ctap(ctap_error)) if ctap_error.is_retryable_user_error() => { - warn!("Retrying WebAuthn get credential operation"); - continue; - }, - Err(err) => { - handler.notify_ceremony_failed(err.to_string()).await; - break; - } - }; - } - - }; + // TODO: I'm not sure how we want to handle multiple usb devices + // just take the first one found for now. + // TODO: store this device reference, perhaps in the enum itself + let handler = self.usb_uv_handler.clone(); + let cred_request = self.cred_request.clone(); + let signal_tx = self.usb_uv_handler.signal_tx.clone(); + let pin_rx = self.usb_uv_handler.pin_rx.clone(); + tokio().spawn(async move { + let mut devices = libwebauthn::transport::hid::list_devices().await.unwrap(); + let device = devices.first_mut().unwrap(); + let (mut channel, state_rx) = device.channel().await.unwrap(); + tokio().spawn(async move { + handle_usb_updates(signal_tx, pin_rx, state_rx).await; + debug!("Reached end of USB update task"); }); - match self.usb_uv_handler.wait_for_notification().await { - Ok(UsbUvMessage::NeedsPin { attempts_left }) => { - Ok(UsbState::NeedsPin { attempts_left}) - }, - Ok(UsbUvMessage::NeedsUserVerification { attempts_left }) => { - Ok(UsbState::NeedsUserVerification { attempts_left}) - }, - Ok(UsbUvMessage::NeedsUserPresence) => { - Ok(UsbState::NeedsUserPresence) - }, - Ok(UsbUvMessage::ReceivedCredential(response)) => { - match response { - AuthenticatorResponse::CredentialCreated(r) => { - let mut cred_response = self.cred_response.lock().unwrap(); - cred_response.replace(CredentialResponse::CreatePublicKeyCredentialResponse(r)); - Ok(UsbState::Completed) - - }, - AuthenticatorResponse::CredentialsAsserted(r) => { - // at least one credential is returned from the authenticator - assert!(!r.assertions.is_empty()); - if r.assertions.len() == 1 { - let mut cred_response = self.cred_response.lock().unwrap(); - cred_response.replace(CredentialResponse::GetPublicKeyCredentialResponse(r.assertions[0].clone())); - Ok(UsbState::Completed) + match cred_request { + CredentialRequest::CreatePublicKeyCredentialRequest(make_cred_request) => { + loop { + match channel.webauthn_make_credential(&make_cred_request).await { + Ok(response) => { + handler + .notify_ceremony_completed( + AuthenticatorResponse::CredentialCreated(response), + ) + .await; + break; + } + Err(WebAuthnError::Ctap(ctap_error)) + if ctap_error.is_retryable_user_error() => + { + warn!("Retrying WebAuthn make credential operation"); + continue; } - else { - todo!("need to support selection from multiple credentials"); + Err(err) => { + handler.notify_ceremony_failed(err.to_string()).await; + break; } - }, + }; } - }, - Err(err) => Err(err), - + } + CredentialRequest::GetPublicKeyCredentialRequest(get_cred_request) => { + loop { + match channel.webauthn_get_assertion(&get_cred_request).await { + Ok(response) => { + handler + .notify_ceremony_completed( + AuthenticatorResponse::CredentialsAsserted( + response, + ), + ) + .await; + break; + } + Err(WebAuthnError::Ctap(ctap_error)) + if ctap_error.is_retryable_user_error() => + { + warn!("Retrying WebAuthn get credential operation"); + continue; + } + Err(err) => { + handler.notify_ceremony_failed(err.to_string()).await; + break; + } + }; + } + } + }; + }); + match self.usb_uv_handler.wait_for_notification().await { + Ok(UsbUvMessage::NeedsPin { attempts_left }) => { + Ok(UsbState::NeedsPin { attempts_left }) + } + Ok(UsbUvMessage::NeedsUserVerification { attempts_left }) => { + Ok(UsbState::NeedsUserVerification { attempts_left }) + } + Ok(UsbUvMessage::NeedsUserPresence) => Ok(UsbState::NeedsUserPresence), + Ok(UsbUvMessage::ReceivedCredential(response)) => { + match response { + AuthenticatorResponse::CredentialCreated(r) => { + let mut cred_response = self.cred_response.lock().unwrap(); + cred_response.replace( + CredentialResponse::CreatePublicKeyCredentialResponse(r), + ); + Ok(UsbState::Completed) + } + AuthenticatorResponse::CredentialsAsserted(r) => { + // at least one credential is returned from the authenticator + assert!(!r.assertions.is_empty()); + if r.assertions.len() == 1 { + let mut cred_response = self.cred_response.lock().unwrap(); + cred_response.replace( + CredentialResponse::GetPublicKeyCredentialResponse( + r.assertions[0].clone(), + ), + ); + Ok(UsbState::Completed) + } else { + todo!("need to support selection from multiple credentials"); + } + } + } + } + Err(err) => Err(err), } } - UsbState::NeedsPin{ attempts_left: Some(attempts_left) } if attempts_left <= 1 => { - Err("No more USB attempts left".to_string()) - }, - UsbState::NeedsUserVerification{ attempts_left: Some(attempts_left) } if attempts_left <= 1 => { + UsbState::NeedsPin { + attempts_left: Some(attempts_left), + } if attempts_left <= 1 => Err("No more USB attempts left".to_string()), + UsbState::NeedsUserVerification { + attempts_left: Some(attempts_left), + } if attempts_left <= 1 => { Err("No more on-device user device attempts left".to_string()) - }, - UsbState::NeedsPin { .. } | UsbState::NeedsUserVerification { .. } | UsbState::NeedsUserPresence => { + } + UsbState::NeedsPin { .. } + | UsbState::NeedsUserVerification { .. } + | UsbState::NeedsUserPresence => { match self.usb_uv_handler.check_notification().await? { Some(UsbUvMessage::NeedsPin { attempts_left }) => { Ok(UsbState::NeedsPin { attempts_left }) - }, + } Some(UsbUvMessage::NeedsUserVerification { attempts_left }) => { Ok(UsbState::NeedsUserVerification { attempts_left }) - }, - Some(UsbUvMessage::NeedsUserPresence) => { - Ok(UsbState::NeedsUserPresence) - }, + } + Some(UsbUvMessage::NeedsUserPresence) => Ok(UsbState::NeedsUserPresence), Some(UsbUvMessage::ReceivedCredential(response)) => { match response { AuthenticatorResponse::CredentialCreated(r) => { let mut cred_response = self.cred_response.lock().unwrap(); - cred_response.replace(CredentialResponse::CreatePublicKeyCredentialResponse(r)); + cred_response.replace( + CredentialResponse::CreatePublicKeyCredentialResponse(r), + ); Ok(UsbState::Completed) - - }, + } AuthenticatorResponse::CredentialsAsserted(r) => { // at least one credential is returned from the authenticator assert!(!r.assertions.is_empty()); if r.assertions.len() == 1 { let mut cred_response = self.cred_response.lock().unwrap(); - cred_response.replace(CredentialResponse::GetPublicKeyCredentialResponse(r.assertions[0].clone())); + cred_response.replace( + CredentialResponse::GetPublicKeyCredentialResponse( + r.assertions[0].clone(), + ), + ); Ok(UsbState::Completed) - } - else { + } else { todo!("need to support selection from multiple credentials"); } - }, + } } - }, + } None => Ok(prev_usb_state), } } - UsbState::Completed => { - Ok(prev_usb_state) - } - UsbState::UserCancelled => { - Ok(prev_usb_state) - } + UsbState::Completed => Ok(prev_usb_state), + UsbState::UserCancelled => Ok(prev_usb_state), }?; *self.usb_state.lock().await = next_usb_state; @@ -255,11 +289,13 @@ impl CredentialService { pub(crate) async fn validate_usb_device_pin(&mut self, pin: &str) -> Result<(), ()> { let current_state = self.usb_state.lock().await.clone(); match current_state { - UsbState::NeedsPin { attempts_left: Some(attempts_left) } if attempts_left > 1 => { + UsbState::NeedsPin { + attempts_left: Some(attempts_left), + } if attempts_left > 1 => { self.usb_uv_handler.send_pin(pin).await; Ok(()) } - _ => Err(()) + _ => Err(()), } } @@ -375,10 +411,14 @@ pub enum UsbState { Connected, /// The device needs the PIN to be entered. - NeedsPin { attempts_left: Option }, + NeedsPin { + attempts_left: Option, + }, /// The device needs on-device user verification. - NeedsUserVerification { attempts_left: Option }, + NeedsUserVerification { + attempts_left: Option, + }, /// The device needs evidence of user presence (e.g. touch) to release the credential. NeedsUserPresence, @@ -437,7 +477,6 @@ pub(crate) struct CredentialMetadata { pub(crate) username: String, } - #[derive(Clone, Debug)] pub struct UsbUvHandler { signal_tx: async_std::channel::Sender>, @@ -454,12 +493,15 @@ impl UsbUvHandler { signal_tx, signal_rx, pin_tx, - pin_rx + pin_rx, } } async fn notify_ceremony_completed(&self, response: AuthenticatorResponse) { - self.signal_tx.send(Ok(UsbUvMessage::ReceivedCredential(response))).await.unwrap(); + self.signal_tx + .send(Ok(UsbUvMessage::ReceivedCredential(response))) + .await + .unwrap(); } async fn notify_ceremony_failed(&self, err: String) { @@ -486,11 +528,18 @@ impl UsbUvHandler { } } -async fn handle_usb_updates(signal_tx: async_std::channel::Sender>, pin_rx: async_std::channel::Receiver, mut state_rx: tokio::sync::mpsc::Receiver) { +async fn handle_usb_updates( + signal_tx: async_std::channel::Sender>, + pin_rx: async_std::channel::Receiver, + mut state_rx: tokio::sync::mpsc::Receiver, +) { while let Some(msg) = state_rx.recv().await { match msg { UxUpdate::UvRetry { attempts_left } => { - signal_tx.send(Ok(UsbUvMessage::NeedsUserVerification { attempts_left })).await.unwrap(); + signal_tx + .send(Ok(UsbUvMessage::NeedsUserVerification { attempts_left })) + .await + .unwrap(); } UxUpdate::PinRequired(pin_update) => { if pin_update.attempts_left.map_or(false, |num| num <= 1) { @@ -498,19 +547,26 @@ async fn handle_usb_updates(signal_tx: async_std::channel::Sender { - signal_tx.send(Ok(UsbUvMessage::NeedsUserPresence)).await.unwrap(); + signal_tx + .send(Ok(UsbUvMessage::NeedsUserPresence)) + .await + .unwrap(); } } - }; + } debug!("USB update channel closed."); } @@ -541,9 +597,7 @@ enum UsbUvMessage { } fn tokio() -> &'static Runtime { static RUNTIME: OnceLock = OnceLock::new(); - RUNTIME.get_or_init(|| { - Runtime::new().expect("Tokio runtime to start") - }) + RUNTIME.get_or_init(|| Runtime::new().expect("Tokio runtime to start")) } enum AuthenticatorResponse { diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs index 1ea00a1..24a569c 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs @@ -7,10 +7,19 @@ use async_std::sync::Mutex as AsyncMutex; use gettextrs::{gettext, LocaleCategory}; use gtk::{gio, glib}; -use libwebauthn::ops::webauthn::{Assertion, GetAssertionRequest, MakeCredentialRequest, MakeCredentialResponse, UserVerificationRequirement}; -use libwebauthn::proto::ctap2::{Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity, Ctap2PublicKeyCredentialUserEntity}; +use libwebauthn::ops::webauthn::{ + Assertion, GetAssertionRequest, MakeCredentialRequest, MakeCredentialResponse, + UserVerificationRequirement, +}; +use libwebauthn::proto::ctap2::{ + Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity, + Ctap2PublicKeyCredentialUserEntity, +}; use zbus::zvariant::{DeserializeDict, SerializeDict, Type}; -use zbus::{fdo, interface, connection::{self, Connection}, Result}; +use zbus::{ + connection::{self, Connection}, + fdo, interface, Result, +}; use crate::application::ExampleApplication; use crate::config::{GETTEXT_PACKAGE, LOCALEDIR, RESOURCES_FILE}; @@ -24,7 +33,8 @@ use ring::digest; pub(crate) async fn start_service(service_name: &str, path: &str) -> Result { let (gui_tx, gui_rx) = async_std::channel::bounded(1); - let lock: Arc>)>>> = Arc::new(AsyncMutex::new(gui_tx)); + let lock: Arc>)>>> = + Arc::new(AsyncMutex::new(gui_tx)); start_gui_thread(gui_rx); connection::Builder::session()? .name(service_name)? @@ -42,8 +52,12 @@ fn start_gui_thread(rx: Receiver<(CredentialRequest, Sender(); let data = Arc::new(Mutex::new(None)); let operation = match &cred_request { - CredentialRequest::CreatePublicKeyCredentialRequest(_) => Operation::Create { cred_type: CredentialType::Passkey }, - CredentialRequest::GetPublicKeyCredentialRequest(_) => Operation::Get { cred_types: vec![CredentialType::Passkey] }, + CredentialRequest::CreatePublicKeyCredentialRequest(_) => Operation::Create { + cred_type: CredentialType::Passkey, + }, + CredentialRequest::GetPublicKeyCredentialRequest(_) => Operation::Get { + cred_types: vec![CredentialType::Passkey], + }, }; let credential_service = CredentialService::new(cred_request, data.clone()); let event_loop = async_std::task::spawn(async move { @@ -105,21 +119,36 @@ impl CredentialManager { &request.public_key, ) { ("password", Some(password_request), _) => { - let password_response = create_password(&origin, is_same_origin, password_request).await?; + let password_response = + create_password(&origin, is_same_origin, password_request).await?; Ok(password_response.into()) } ("publicKey", _, Some(passkey_request)) => { - _ = request.origin.get_or_insert("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()))?; - let request = CredentialRequest::CreatePublicKeyCredentialRequest(make_cred_request); + _ = request.origin.get_or_insert( + "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(), + ) + })?; + let request = + CredentialRequest::CreatePublicKeyCredentialRequest(make_cred_request); let (data_tx, data_rx) = async_std::channel::bounded(1); tx.send((request, data_tx)).await.unwrap(); let data_rx = Arc::new(data_rx); - if let Some(CredentialResponse::CreatePublicKeyCredentialResponse(cred_response)) = data_rx.recv().await.unwrap() { - let public_key_response = CreatePublicKeyCredentialResponse::try_from_ctap2_response(&cred_response, client_data_json)?; + if let Some(CredentialResponse::CreatePublicKeyCredentialResponse( + cred_response, + )) = data_rx.recv().await.unwrap() + { + let public_key_response = + CreatePublicKeyCredentialResponse::try_from_ctap2_response( + &cred_response, + client_data_json, + )?; Ok(public_key_response.into()) - } - else { + } else { Err(fdo::Error::Failed("Failed to create passkey".to_string())) } } @@ -152,28 +181,45 @@ impl CredentialManager { &request.public_key, ) { ("password", Some(password_request), _) => { - let password_response = get_password(&origin, is_same_origin, password_request).await?; + let password_response = + get_password(&origin, is_same_origin, password_request).await?; Ok(password_response.into()) } ("publicKey", _, Some(passkey_request)) => { - _ = request.origin.get_or_insert("xyz.iinuwa.credentials.CredentialManager:local".to_string()); + _ = request.origin.get_or_insert( + "xyz.iinuwa.credentials.CredentialManager:local".to_string(), + ); // TODO: assert that RP ID is bound to origin: // - if RP ID is not set, set the RP ID to the origin's effective domain // - if RP ID is set, assert that it matches origin's effective domain // - if RP ID is set, but origin's effective domain doesn't match // - query for related origins, if supported // - fail if not supported, or if RP ID doesn't match any related origins. - let (get_cred_request, client_data_json) = request.clone().try_into_ctap2_request().map_err(|_| fdo::Error::Failed("Could not parse passkey assertion request.".to_owned()))?; - let request = CredentialRequest::GetPublicKeyCredentialRequest(get_cred_request); + let (get_cred_request, client_data_json) = + request.clone().try_into_ctap2_request().map_err(|_| { + fdo::Error::Failed( + "Could not parse passkey assertion request.".to_owned(), + ) + })?; + let request = + CredentialRequest::GetPublicKeyCredentialRequest(get_cred_request); let (data_tx, data_rx) = async_std::channel::bounded(1); tx.send((request, data_tx)).await.unwrap(); let data_rx = Arc::new(data_rx); match data_rx.recv().await { - Ok(Some(CredentialResponse::GetPublicKeyCredentialResponse(cred_response))) => { - let public_key_response = GetPublicKeyCredentialResponse::try_from_ctap2_response(&cred_response, client_data_json)?; + Ok(Some(CredentialResponse::GetPublicKeyCredentialResponse( + cred_response, + ))) => { + let public_key_response = + GetPublicKeyCredentialResponse::try_from_ctap2_response( + &cred_response, + client_data_json, + )?; Ok(public_key_response.into()) - }, - Ok(_) => Err(fdo::Error::Failed("Invalid credential response received from authenticator".to_string())), + } + Ok(_) => Err(fdo::Error::Failed( + "Invalid credential response received from authenticator".to_string(), + )), Err(_) => Err(fdo::Error::Failed("User cancelled operation".to_string())), } } @@ -197,7 +243,9 @@ async fn create_password( request: &CreatePasswordCredentialRequest, ) -> fdo::Result { if !is_same_origin { - return Err(fdo::Error::AccessDenied("Passwords may only be requested from same-origin contexts".to_string())); + return Err(fdo::Error::AccessDenied( + "Passwords may only be requested from same-origin contexts".to_string(), + )); } /* store::store_password(&request.origin, &request.id, &request.password).await @@ -210,22 +258,26 @@ async fn create_password( request.password.replace('%', "%25").replace('&', "%26") ); let display_name = format!("Password for {origin}"); // TODO - /* - store::store_secret( - &[origin], - &display_name, - &request.id, - "secret/password", - None, - contents.as_bytes(), - ) - .await - .map_err(|_| fdo::Error::Failed("".to_string()))?; - */ + /* + store::store_secret( + &[origin], + &display_name, + &request.id, + "secret/password", + None, + contents.as_bytes(), + ) + .await + .map_err(|_| fdo::Error::Failed("".to_string()))?; + */ Ok(CreatePasswordCredentialResponse {}) } -async fn get_password(origin: &str, is_same_origin: bool, request: &GetPasswordCredentialRequest) -> Result{ +async fn get_password( + origin: &str, + is_same_origin: bool, + request: &GetPasswordCredentialRequest, +) -> Result { todo!() } @@ -239,7 +291,7 @@ pub(crate) enum CredentialRequest { #[derive(Clone, Debug)] pub(crate) enum CredentialResponse { CreatePublicKeyCredentialResponse(MakeCredentialResponse), - GetPublicKeyCredentialResponse(Assertion) + GetPublicKeyCredentialResponse(Assertion), } // D-Bus <-> Client types @@ -256,7 +308,9 @@ pub struct CreateCredentialRequest { } impl CreateCredentialRequest { - fn try_into_ctap2_request(&self) -> std::result::Result<(MakeCredentialRequest, String), webauthn::Error> { + fn try_into_ctap2_request( + &self, + ) -> std::result::Result<(MakeCredentialRequest, String), webauthn::Error> { if self.public_key.is_none() { return Err(webauthn::Error::NotSupported); } @@ -273,19 +327,25 @@ impl CreateCredentialRequest { .to_owned(); let rp = json .get("rp") - .and_then(|val| serde_json::from_str::(&val.to_string()).ok()) + .and_then(|val| { + serde_json::from_str::(&val.to_string()).ok() + }) .ok_or_else(|| webauthn::Error::Internal("JSON missing `rp` field".to_string()))?; let user = json .get("user") - .ok_or(webauthn::Error::Internal("JSON missing `user` field".to_string())) + .ok_or(webauthn::Error::Internal( + "JSON missing `user` field".to_string(), + )) .and_then(|val| { - serde_json::from_str::(&val.to_string()).map_err(|e| { - let msg = format!("JSON missing `user` field: {e}"); - webauthn::Error::Internal(msg) - }) + serde_json::from_str::(&val.to_string()) + .map_err(|e| { + let msg = format!("JSON missing `user` field: {e}"); + webauthn::Error::Internal(msg) + }) })?; - let other_options = serde_json::from_str::(&request_value.to_string()) - .map_err(|_| webauthn::Error::Internal("Invalid request JSON".to_string()))?; + let other_options = + serde_json::from_str::(&request_value.to_string()) + .map_err(|_| webauthn::Error::Internal("Invalid request JSON".to_string()))?; let (require_resident_key, user_verification) = if let Some(authenticator_selection) = other_options.authenticator_selection { let is_authenticator_storage_capable = true; @@ -294,12 +354,15 @@ impl CreateCredentialRequest { |r| r == "required" || (r == "preferred" && is_authenticator_storage_capable), ); // fallback to authenticator_selection.require_resident_key == true for WebAuthn Level 1? - let user_verification = authenticator_selection.user_verification.map(|uv| match uv.as_ref() { - "required" => UserVerificationRequirement::Required, - "preferred" => UserVerificationRequirement::Preferred, - "discouraged" => UserVerificationRequirement::Discouraged, - _ => todo!("This should be fixed in the future"), - }).unwrap_or(UserVerificationRequirement::Preferred); + let user_verification = authenticator_selection + .user_verification + .map(|uv| match uv.as_ref() { + "required" => UserVerificationRequirement::Required, + "preferred" => UserVerificationRequirement::Preferred, + "discouraged" => UserVerificationRequirement::Discouraged, + _ => todo!("This should be fixed in the future"), + }) + .unwrap_or(UserVerificationRequirement::Preferred); (require_resident_key, user_verification) } else { @@ -310,50 +373,66 @@ impl CreateCredentialRequest { .clone() .get("pubKeyCredParams") .ok_or_else(|| { - webauthn::Error::Internal("Request JSON missing or invalid `pubKeyCredParams` key".to_string()) + webauthn::Error::Internal( + "Request JSON missing or invalid `pubKeyCredParams` key".to_string(), + ) }) .and_then(|val| -> std::result::Result, webauthn::Error> { - serde_json::from_str::>(&val.to_string()).map_err( - |e| { + serde_json::from_str::>(&val.to_string()) + .map_err(|e| { webauthn::Error::Internal(format!( "Request JSON missing or invalid `pubKeyCredParams` key: {e}" )) - }, - ) + }) })?; - let algorithms = credential_parameters.iter().filter_map(|p| p.try_into().ok()).collect(); - let exclude = other_options.excluded_credentials - .map(|v| - v.iter().map(|e| e.try_into()) - .filter_map(|e| e.ok()) - .collect()); + let algorithms = credential_parameters + .iter() + .filter_map(|p| p.try_into().ok()) + .collect(); + let exclude = other_options.excluded_credentials.map(|v| { + v.iter() + .map(|e| e.try_into()) + .filter_map(|e| e.ok()) + .collect() + }); let (origin, is_cross_origin) = match (self.origin.as_ref(), self.is_same_origin.as_ref()) { (Some(origin), Some(is_same_origin)) => (origin.to_string(), !is_same_origin), (Some(origin), None) => (origin.to_string(), true), // origin should always be set on request either by client or D-Bus service, // so this shouldn't be called (None, _) => { - return Err(webauthn::Error::Internal("Error reading origin from request".to_string())); + return Err(webauthn::Error::Internal( + "Error reading origin from request".to_string(), + )); } }; - let client_data_json = format_client_data_json(Operation::Create { cred_type: CredentialType::Passkey }, &challenge, &origin, is_cross_origin); + let client_data_json = format_client_data_json( + Operation::Create { + cred_type: CredentialType::Passkey, + }, + &challenge, + &origin, + is_cross_origin, + ); let client_data_hash = digest::digest(&digest::SHA256, client_data_json.as_bytes()) .as_ref() .to_owned(); - Ok((MakeCredentialRequest { - hash: client_data_hash, - origin, - - relying_party: rp, - user, - require_resident_key, - user_verification, - algorithms, - exclude, - extensions, - timeout: other_options.timeout.unwrap_or(Duration::from_secs(300)), - - }, client_data_json)) + Ok(( + MakeCredentialRequest { + hash: client_data_hash, + origin, + + relying_party: rp, + user, + require_resident_key, + user_verification, + algorithms, + exclude, + extensions, + timeout: other_options.timeout.unwrap_or(Duration::from_secs(300)), + }, + client_data_json, + )) } } @@ -364,7 +443,6 @@ pub struct CreatePasswordCredentialRequest { password: String, } - #[derive(Clone, Debug, DeserializeDict, Type)] #[zvariant(signature = "dict")] pub struct CreatePublicKeyCredentialRequest { @@ -372,12 +450,27 @@ pub struct CreatePublicKeyCredentialRequest { } impl CreatePublicKeyCredentialResponse { - fn try_from_ctap2_response(response: &MakeCredentialResponse, client_data_json: String) -> std::result::Result { + fn try_from_ctap2_response( + response: &MakeCredentialResponse, + client_data_json: String, + ) -> std::result::Result { let auth_data = &response.authenticator_data; - 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(); + 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(); @@ -386,9 +479,11 @@ impl CreatePublicKeyCredentialResponse { &auth_data.flags, (&auth_data).signature_count, Some(&attested_credential_data), - None + None, ); - let attestation_statement = (&response.attestation_statement).try_into().map_err(|_| fdo::Error::Failed("Could not serialize attestation statement".to_string()))?; + let attestation_statement = (&response.attestation_statement).try_into().map_err(|_| { + fdo::Error::Failed("Could not serialize attestation statement".to_string()) + })?; let attestation_object = webauthn::create_attestation_object( &authenticator_data_blob, &attestation_statement, @@ -403,7 +498,8 @@ impl CreatePublicKeyCredentialResponse { client_data_json, Some(vec!["usb".to_string()]), None, - ).to_json(); + ) + .to_json(); let response = CreatePublicKeyCredentialResponse { registration_response_json, }; @@ -464,17 +560,25 @@ pub struct GetCredentialRequest { } impl GetCredentialRequest { - fn try_into_ctap2_request(&self) -> std::result::Result<(GetAssertionRequest, String), webauthn::Error> { + fn try_into_ctap2_request( + &self, + ) -> std::result::Result<(GetAssertionRequest, String), webauthn::Error> { if self.public_key.is_none() { return Err(webauthn::Error::NotSupported); } let options = self.public_key.as_ref().unwrap(); - let request: webauthn::GetCredentialOptions = serde_json::from_str(&options.request_json) - .map_err(|e| webauthn::Error::Internal(format!("Invalid request JSON: {:?}", e)))?; - let mut allow: Vec = request.allow_credentials.iter() + let request: webauthn::GetCredentialOptions = + serde_json::from_str(&options.request_json) + .map_err(|e| webauthn::Error::Internal(format!("Invalid request JSON: {:?}", e)))?; + let mut allow: Vec = request + .allow_credentials + .iter() .filter_map(|cred| { - if cred.cred_type == "public-key" { cred.try_into().ok() } - else { None } + if cred.cred_type == "public-key" { + cred.try_into().ok() + } else { + None + } }) .collect(); // TODO: The allow is returning an empty list instead of either None or a list of transports. @@ -489,19 +593,36 @@ impl GetCredentialRequest { // origin should always be set on request either by client or D-Bus service, // so this shouldn't be called (None, _) => { - return Err(webauthn::Error::Internal("Error reading origin from request".to_string())); + return Err(webauthn::Error::Internal( + "Error reading origin from request".to_string(), + )); } }; - let client_data_json = format_client_data_json(Operation::Get { cred_types: vec![CredentialType::Passkey] }, &request.challenge, &origin, is_cross_origin); + let client_data_json = format_client_data_json( + Operation::Get { + cred_types: vec![CredentialType::Passkey], + }, + &request.challenge, + &origin, + is_cross_origin, + ); let client_data_hash = digest::digest(&digest::SHA256, client_data_json.as_bytes()) .as_ref() .to_owned(); // TODO: actually calculate correct effective domain, and use fallback to related origin requests to fill this in. For now, just default to origin. - let user_verification = match request.user_verification.unwrap_or_else(|| String::from("preferred")).as_ref() { + let user_verification = match request + .user_verification + .unwrap_or_else(|| String::from("preferred")) + .as_ref() + { "required" => UserVerificationRequirement::Required, "preferred" => UserVerificationRequirement::Preferred, "discouraged" => UserVerificationRequirement::Discouraged, - _ => return Err(webauthn::Error::Internal("Invalid user verification requirement specified".to_string())) + _ => { + return Err(webauthn::Error::Internal( + "Invalid user verification requirement specified".to_string(), + )) + } }; let relying_party_id = request.rp_id.unwrap_or_else(|| { let (_, effective_domain) = origin.rsplit_once('/').unwrap(); @@ -509,16 +630,18 @@ impl GetCredentialRequest { }); // TODO(extensions-support) let extensions = None; - Ok((GetAssertionRequest { - hash: client_data_hash, - - relying_party_id, - user_verification, - allow, - extensions, - timeout: request.timeout.unwrap_or(Duration::from_secs(300)), - - }, client_data_json)) + Ok(( + GetAssertionRequest { + hash: client_data_hash, + + relying_party_id, + user_verification, + allow, + extensions, + timeout: request.timeout.unwrap_or(Duration::from_secs(300)), + }, + client_data_json, + )) } } @@ -529,7 +652,6 @@ pub struct GetPasswordCredentialRequest { password: String, } - #[derive(Clone, Debug, DeserializeDict, Type)] #[zvariant(signature = "dict")] pub struct GetPublicKeyCredentialRequest { @@ -537,16 +659,31 @@ pub struct GetPublicKeyCredentialRequest { } impl GetPublicKeyCredentialResponse { - fn try_from_ctap2_response(response: &Assertion, client_data_json: String) -> std::result::Result { + fn try_from_ctap2_response( + response: &Assertion, + client_data_json: String, + ) -> std::result::Result { let auth_data = &response.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()))?; + 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? @@ -563,11 +700,15 @@ impl GetPublicKeyCredentialResponse { let authentication_response_json = webauthn::GetPublicKeyCredentialResponse::new( client_data_json, - response.credential_id.as_ref().map(|c| c.id.clone().into_vec()), + response + .credential_id + .as_ref() + .map(|c| c.id.clone().into_vec()), authenticator_data_blob, response.signature.clone(), response.user.as_ref().map(|u| u.id.clone().into_vec()), - ).to_json(); + ) + .to_json(); let response = GetPublicKeyCredentialResponse { authentication_response_json, }; @@ -615,10 +756,15 @@ impl From for GetCredentialResponse { } } -fn format_client_data_json(op: Operation, challenge: &str, origin: &str, is_cross_origin: bool) -> String { +fn format_client_data_json( + op: Operation, + challenge: &str, + origin: &str, + is_cross_origin: bool, +) -> String { let op_str = match op { Operation::Create { .. } => "webauthn.create", - Operation::Get { .. }=> "webauthn.get", + Operation::Get { .. } => "webauthn.get", }; let cross_origin_str = if is_cross_origin { "true" } else { "false" }; format!("{{\"type\":\"{op_str}\",\"challenge\":\"{challenge}\",\"origin\":\"{origin}\",\"crossOrigin\":{cross_origin_str}}}") From bf797ecedd754932efb4d52ac081b30d5ade4467 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus Date: Thu, 17 Apr 2025 15:37:42 +0200 Subject: [PATCH 2/4] Replace hardcoded transport --- .../src/credential_service/mod.rs | 16 +++++++-- .../src/dbus.rs | 34 ++++++++++++++----- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/mod.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/mod.rs index 36ea245..0e82286 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/mod.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/mod.rs @@ -22,7 +22,7 @@ use tokio::runtime::Runtime; use tracing::{debug, warn}; use crate::{ - dbus::{CredentialRequest, CredentialResponse}, + dbus::{CredentialRequest, CredentialResponse, MakeCredentialResponseInternal}, view_model::{Device, InternalPinState, Transport}, }; @@ -200,7 +200,12 @@ impl CredentialService { AuthenticatorResponse::CredentialCreated(r) => { let mut cred_response = self.cred_response.lock().unwrap(); cred_response.replace( - CredentialResponse::CreatePublicKeyCredentialResponse(r), + CredentialResponse::CreatePublicKeyCredentialResponse( + MakeCredentialResponseInternal::new( + r, + vec![String::from("usb"), String::from("usb")], + ), + ), ); Ok(UsbState::Completed) } @@ -248,7 +253,12 @@ impl CredentialService { AuthenticatorResponse::CredentialCreated(r) => { let mut cred_response = self.cred_response.lock().unwrap(); cred_response.replace( - CredentialResponse::CreatePublicKeyCredentialResponse(r), + CredentialResponse::CreatePublicKeyCredentialResponse( + MakeCredentialResponseInternal::new( + r, + vec![String::from("usb")], + ), + ), ); Ok(UsbState::Completed) } diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs index 24a569c..0dc109d 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs @@ -290,10 +290,25 @@ pub(crate) enum CredentialRequest { #[derive(Clone, Debug)] pub(crate) enum CredentialResponse { - CreatePublicKeyCredentialResponse(MakeCredentialResponse), + CreatePublicKeyCredentialResponse(MakeCredentialResponseInternal), GetPublicKeyCredentialResponse(Assertion), } +#[derive(Clone, Debug)] +pub(crate) struct MakeCredentialResponseInternal { + ctap: MakeCredentialResponse, + transport: Vec, +} + +impl MakeCredentialResponseInternal { + pub(crate) fn new(response: MakeCredentialResponse, transport: Vec) -> Self { + Self { + ctap: response, + transport, + } + } +} + // D-Bus <-> Client types #[derive(Clone, Debug, DeserializeDict, Type)] #[zvariant(signature = "dict")] @@ -451,10 +466,10 @@ pub struct CreatePublicKeyCredentialRequest { impl CreatePublicKeyCredentialResponse { fn try_from_ctap2_response( - response: &MakeCredentialResponse, + response: &MakeCredentialResponseInternal, client_data_json: String, ) -> std::result::Result { - let auth_data = &response.authenticator_data; + let auth_data = &response.ctap.authenticator_data; let attested_credential = auth_data.attested_credential.as_ref().ok_or_else(|| { fdo::Error::Failed("Invalid credential received from authenticator".to_string()) })?; @@ -481,13 +496,16 @@ impl CreatePublicKeyCredentialResponse { Some(&attested_credential_data), None, ); - let attestation_statement = (&response.attestation_statement).try_into().map_err(|_| { - fdo::Error::Failed("Could not serialize attestation statement".to_string()) - })?; + let attestation_statement = + (&response.ctap.attestation_statement) + .try_into() + .map_err(|_| { + fdo::Error::Failed("Could not serialize attestation statement".to_string()) + })?; let attestation_object = webauthn::create_attestation_object( &authenticator_data_blob, &attestation_statement, - response.enterprise_attestation.unwrap_or(false), + response.ctap.enterprise_attestation.unwrap_or(false), ) .map_err(|_| zbus::Error::Failure("Failed to create attestation object".to_string()))?; // do we need to check that the client_data_hash is the same? @@ -496,7 +514,7 @@ impl CreatePublicKeyCredentialResponse { attestation_object, authenticator_data_blob, client_data_json, - Some(vec!["usb".to_string()]), + Some(response.transport.clone()), None, ) .to_json(); From ebda2e8bde40b66c3c5c73b10e38a4b28c68ee49 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus Date: Thu, 17 Apr 2025 15:58:53 +0200 Subject: [PATCH 3/4] Run rustfmt on two more files --- .../src/platform_authenticator/mod.rs | 151 ++++++++++-------- .../src/webauthn.rs | 58 ++++--- 2 files changed, 126 insertions(+), 83 deletions(-) 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 4cc052d..b411b17 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 @@ -14,8 +14,13 @@ use ring::{ }, }; -use crate::webauthn::{self, AttestationStatement, AttestationStatementFormat, CreatePublicKeyCredentialResponse, CredentialDescriptor, CredentialSource, Error as WebAuthnError, GetPublicKeyCredentialResponse, MakeCredentialOptions, PublicKeyCredentialParameters, PublicKeyCredentialType, RelyingParty, User}; -use crate::cose::{CoseKeyType, encode_pkcs8_key}; +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, +}; static P256: &EcdsaSigningAlgorithm = &ECDSA_P256_SHA256_ASN1_SIGNING; // static RNG: &Box = &Box::new(SystemRandom::new()); @@ -90,7 +95,9 @@ pub(crate) fn create_credential( .ok_or_else(|| WebAuthnError::Internal("JSON missing `rp` field".to_string()))?; let user = json .get("user") - .ok_or(WebAuthnError::Internal("JSON missing `user` field".to_string())) + .ok_or(WebAuthnError::Internal( + "JSON missing `user` field".to_string(), + )) .and_then(|val| { serde_json::from_str::(&val.to_string()).map_err(|e| { let msg = format!("JSON missing `user` field: {e}"); @@ -124,7 +131,9 @@ pub(crate) fn create_credential( .clone() .get("pubKeyCredParams") .ok_or_else(|| { - WebAuthnError::Internal("Request JSON missing or invalid `pubKeyCredParams` key".to_string()) + WebAuthnError::Internal( + "Request JSON missing or invalid `pubKeyCredParams` key".to_string(), + ) }) .and_then(|val| { serde_json::from_str::>(&val.to_string()).map_err( @@ -197,12 +206,13 @@ pub(crate) fn make_credential( let cred_pub_key_parameters = match cred_pub_key_algs .iter() .filter(|p| p.cred_type == "public-key") - .find(|p| if let Ok(ref key_type) = (*p).try_into() { - supported_algorithms.contains(key_type) - } else { - false - }) - { + .find(|p| { + if let Ok(ref key_type) = (*p).try_into() { + supported_algorithms.contains(key_type) + } else { + false + } + }) { Some(cred_pub_key_parameters) => cred_pub_key_parameters, None => return Err(WebAuthnError::NotSupported), }; @@ -256,14 +266,15 @@ pub(crate) fn make_credential( } let mut flags = if require_user_verification { AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::USER_VERIFIED - } - else { + } else { AuthenticatorDataFlags::USER_PRESENT }; // Once the authorization gesture has been completed and user consent has been obtained, generate a new credential object: // Let (publicKey, privateKey) be a new pair of cryptographic keys using the combination of PublicKeyCredentialType and cryptographic parameters represented by the first item in credTypesAndPubKeyAlgs that is supported by this authenticator. - let key_type = cred_pub_key_parameters.try_into().map_err(|_| WebAuthnError::Unknown)?; + let key_type = cred_pub_key_parameters + .try_into() + .map_err(|_| WebAuthnError::Unknown)?; let key_pair = create_key_pair(key_type)?; // Let userHandle be userEntity.id. let user_handle = URL_SAFE_NO_PAD @@ -347,13 +358,8 @@ pub(crate) fn make_credential( // Create an attestation object for the new credential using the procedure specified in § 6.5.4 Generating an Attestation Object, using an authenticator-chosen attestation statement format, authenticatorData, and hash, as well as taking into account the value of enterpriseAttestationPossible. For more details on attestation, see § 6.5 Attestation. // TODO: attestation not supported for now - let signature = sign_attestation( - &authenticator_data, - &client_data_hash, - &key_pair, - &key_type, - )?; - let attestation_statment = AttestationStatement::Packed{ + let signature = sign_attestation(&authenticator_data, &client_data_hash, &key_pair, &key_type)?; + let attestation_statment = AttestationStatement::Packed { algorithm: key_type.algorithm(), signature, certificates: vec![], @@ -377,7 +383,6 @@ pub(crate) fn make_credential( } fn get_credential( - rp_entity: RelyingParty, challenge: String, @@ -409,38 +414,38 @@ fn get_credential( // Let credentialOptions be a new empty set of public key credential sources. // If allowCredentialDescriptorList was supplied, then for each descriptor of allowCredentialDescriptorList: - let credential_options: Vec<&CredentialSource> = if let Some(ref allowed_credentials) = allow_credential_descriptor_list { - // Let credSource be the result of looking up descriptor.id in this authenticator. - // If credSource is not null, append it to credentialOptions. - allowed_credentials.iter() - .filter_map(|cred| stored_credentials.get(&cred.id)) - // Remove any items from credentialOptions whose rpId is not equal to rpId. - .filter(|cred_source| cred_source.rp_id == rp_entity.id) - .collect() - } - else { - // Otherwise (allowCredentialDescriptorList was not supplied), for each key → credSource of this authenticator’s credentials map, append credSource to credentialOptions. - stored_credentials - .values() - // Remove any items from credentialOptions whose rpId is not equal to rpId. - .filter(|cred_source| cred_source.rp_id == rp_entity.id) - .collect() - }; - + let credential_options: Vec<&CredentialSource> = + if let Some(ref allowed_credentials) = allow_credential_descriptor_list { + // Let credSource be the result of looking up descriptor.id in this authenticator. + // If credSource is not null, append it to credentialOptions. + allowed_credentials + .iter() + .filter_map(|cred| stored_credentials.get(&cred.id)) + // Remove any items from credentialOptions whose rpId is not equal to rpId. + .filter(|cred_source| cred_source.rp_id == rp_entity.id) + .collect() + } else { + // Otherwise (allowCredentialDescriptorList was not supplied), for each key → credSource of this authenticator’s credentials map, append credSource to credentialOptions. + stored_credentials + .values() + // Remove any items from credentialOptions whose rpId is not equal to rpId. + .filter(|cred_source| cred_source.rp_id == rp_entity.id) + .collect() + }; // If credentialOptions is now empty, return an error code equivalent to "NotAllowedError" and terminate the operation. if credential_options.is_empty() { return Err(WebAuthnError::NotAllowed); } // Prompt the user to select a public key credential source selectedCredential from credentialOptions. Collect an authorization gesture confirming user consent for using selectedCredential. The prompt for the authorization gesture may be shown by the authenticator if it has its own output capability, or by the user agent otherwise. - // TODO, already done? Move up to D-Bus call - // If requireUserVerification is true, the authorization gesture MUST include user verification. - // If requireUserPresence is true, the authorization gesture MUST include a test of user presence. - // If the user does not consent, return an error code equivalent to "NotAllowedError" and terminate the operation. + // TODO, already done? Move up to D-Bus call + // If requireUserVerification is true, the authorization gesture MUST include user verification. + // If requireUserPresence is true, the authorization gesture MUST include a test of user presence. + // If the user does not consent, return an error code equivalent to "NotAllowedError" and terminate the operation. if collect_authorization_gesture(require_user_presence, require_user_verification).is_err() { return Err(WebAuthnError::NotAllowed); } - let flags = if require_user_verification { + let flags = if require_user_verification { AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::USER_VERIFIED } else { AuthenticatorDataFlags::USER_VERIFIED @@ -484,14 +489,25 @@ fn get_credential( // let attestationFormat be the attestation statement format most preferred by this authenticator. If it does not support attestation during assertion then let this be none. let supported_formats = [AttestationStatementFormat::Packed]; let preferred_format = AttestationStatementFormat::None; - let attestation_format = attestation_formats.iter().find(|f| supported_formats.contains(f)).unwrap_or(&preferred_format); + let attestation_format = attestation_formats + .iter() + .find(|f| supported_formats.contains(f)) + .unwrap_or(&preferred_format); - let key_type = (&selected_credential.key_parameters).try_into().map_err(|_| WebAuthnError::Unknown)?; - let public_key = encode_pkcs8_key(key_type, &selected_credential.private_key).map_err(|_| WebAuthnError::Unknown)?; + let key_type = (&selected_credential.key_parameters) + .try_into() + .map_err(|_| WebAuthnError::Unknown)?; + let public_key = encode_pkcs8_key(key_type, &selected_credential.private_key) + .map_err(|_| WebAuthnError::Unknown)?; // 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() } else { None }; + let attested_credential_data = if *attestation_format != AttestationStatementFormat::None { + webauthn::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( @@ -499,7 +515,7 @@ fn get_credential( &flags, signature_counter, attested_credential_data.as_deref(), - None + None, ); // Let signature be the assertion signature of the concatenation authenticatorData || hash using the privateKey of selectedCredential as shown in Figure , below. A simple, undelimited concatenation is safe to use here because the authenticator data describes its own length. The hash of the serialized client data (which potentially has a variable length) is always the last element. let signature = sign_attestation( @@ -517,7 +533,7 @@ fn get_credential( // selectedCredential.id, if either a list of credentials (i.e., allowCredentialDescriptorList) of length 2 or greater was supplied by the client, or no such list was supplied. // Note: If, within allowCredentialDescriptorList, the client supplied exactly one credential and it was successfully employed, then its credential ID is not returned since the client already knows it. This saves transmitting these bytes over what may be a constrained connection in what is likely a common case. - raw_id: if allow_credential_descriptor_list.map_or(true, |l| l.len() > 1) { + raw_id: if allow_credential_descriptor_list.map_or(true, |l| l.len() > 1) { Some(selected_credential.id.clone()) } else { None @@ -540,8 +556,12 @@ fn get_credential( fn create_key_pair(parameters: CoseKeyType) -> Result, WebAuthnError> { let rng = &SystemRandom::new(); let key_pair = match parameters { - CoseKeyType::ES256_P256 => EcdsaKeyPair::generate_pkcs8(P256, rng).map(|d| d.as_ref().to_vec()), - CoseKeyType::EDDSA_ED25519 => Ed25519KeyPair::generate_pkcs8(rng).map(|d| d.as_ref().to_vec()), + CoseKeyType::ES256_P256 => { + EcdsaKeyPair::generate_pkcs8(P256, rng).map(|d| d.as_ref().to_vec()) + } + CoseKeyType::EDDSA_ED25519 => { + Ed25519KeyPair::generate_pkcs8(rng).map(|d| d.as_ref().to_vec()) + } CoseKeyType::RS256 => { let rsa_key = Rsa::generate(2048).unwrap(); let private_key = PKey::from_rsa(rsa_key).unwrap(); @@ -587,7 +607,12 @@ fn sign_attestation( let rng = &SystemRandom::new(); match key_type { CoseKeyType::ES256_P256 => { - let ecdsa = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, key_pair, &SystemRandom::new()).unwrap(); + let ecdsa = EcdsaKeyPair::from_pkcs8( + &ECDSA_P256_SHA256_ASN1_SIGNING, + key_pair, + &SystemRandom::new(), + ) + .unwrap(); Ok(ecdsa.sign(rng, &signed_data).unwrap().as_ref().to_vec()) } CoseKeyType::EDDSA_ED25519 => { @@ -625,7 +650,9 @@ 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, create_attested_credential_data, create_authenticator_data, + AttestationStatement, CredentialSource, PublicKeyCredentialParameters, + PublicKeyCredentialType, }; use super::sign_attestation; @@ -666,7 +693,9 @@ mod test { other_ui: None, }; - let flags = AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::USER_VERIFIED | AuthenticatorDataFlags::ATTESTED_CREDENTIALS; + let flags = AuthenticatorDataFlags::USER_PRESENT + | AuthenticatorDataFlags::USER_VERIFIED + | AuthenticatorDataFlags::ATTESTED_CREDENTIALS; let authenticator_data = create_authenticator_data( &credential_source.rp_id_hash(), &flags, @@ -677,21 +706,15 @@ mod test { let client_data_encoded = "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiWWlReFY0VWhjZk9pUmZBdkF4bWpEakdhaUVXbkYtZ0ZFcWxndmdEaWsyakZiSGhoaVlxUGJqc0F5Q0FrbDlMUGQwRGRQaHNNb2luY0cxckV5cFlXUVEiLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ"; let client_data = URL_SAFE_NO_PAD.decode(client_data_encoded).unwrap(); let client_data_hash = digest(&SHA256, &client_data).as_ref().to_vec(); - let signature = sign_attestation( - &authenticator_data, - &client_data_hash, - &key_file, - &key_type, - ) - .unwrap(); + let signature = + sign_attestation(&authenticator_data, &client_data_hash, &key_file, &key_type).unwrap(); let att_stmt = AttestationStatement::Packed { algorithm: key_type.algorithm(), signature, certificates: vec![], }; let attestation_object = - create_attestation_object(&authenticator_data, &att_stmt, false) - .unwrap(); + create_attestation_object(&authenticator_data, &att_stmt, false).unwrap(); let expected = std::fs::read("output.bin").unwrap(); assert_eq!(expected, attestation_object); } diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs index 5b0dfb0..e566fbb 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs @@ -1,7 +1,13 @@ use std::time::Duration; use base64::{self, engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; -use libwebauthn::{fido::AuthenticatorDataFlags, proto::ctap2::{Ctap2AttestationStatement, Ctap2CredentialType, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialType, Ctap2Transport}}; +use libwebauthn::{ + fido::AuthenticatorDataFlags, + proto::ctap2::{ + Ctap2AttestationStatement, Ctap2CredentialType, Ctap2PublicKeyCredentialDescriptor, + Ctap2PublicKeyCredentialType, Ctap2Transport, + }, +}; use ring::digest; use serde::Deserialize; use serde_json::json; @@ -20,7 +26,6 @@ pub enum Error { Internal(String), } - pub(crate) fn create_attested_credential_data( credential_id: &[u8], public_key: &[u8], @@ -39,7 +44,7 @@ pub(crate) fn create_attested_credential_data( Ok(attested_credential_data) } -pub (crate) fn create_authenticator_data( +pub(crate) fn create_authenticator_data( rp_id_hash: &[u8], flags: &AuthenticatorDataFlags, signature_counter: u32, @@ -66,7 +71,7 @@ pub (crate) fn create_authenticator_data( pub(crate) fn create_attestation_object( authenticator_data: &[u8], - attestation_statement:&AttestationStatement, + attestation_statement: &AttestationStatement, _enterprise_attestation_possible: bool, ) -> Result, Error> { let mut attestation_object = Vec::new(); @@ -74,7 +79,11 @@ pub(crate) fn create_attestation_object( cbor_writer.write_map_start(3).unwrap(); cbor_writer.write_text("fmt").unwrap(); match attestation_statement { - AttestationStatement::Packed { algorithm, signature, certificates } => { + AttestationStatement::Packed { + algorithm, + signature, + certificates, + } => { cbor_writer.write_text("packed").unwrap(); cbor_writer.write_text("attStmt").unwrap(); let len = if certificates.is_empty() { 2 } else { 3 }; @@ -90,7 +99,7 @@ pub(crate) fn create_attestation_object( cbor_writer.write_bytes(cert).unwrap(); } } - }, + } AttestationStatement::None => { cbor_writer.write_text("none").unwrap(); cbor_writer.write_text("attStmt").unwrap(); @@ -104,7 +113,6 @@ pub(crate) fn create_attestation_object( Ok(attestation_object) } - #[derive(Deserialize)] pub(crate) struct RelyingParty { pub name: String, @@ -197,10 +205,8 @@ pub(crate) struct GetCredentialOptions { extensions: Option<()>, } - // pub(crate) struct CredentialList(Vec); - #[derive(Deserialize, Type)] #[zvariant(signature = "dict")] /// https://www.w3.org/TR/webauthn-3/#dictionary-credential-descriptor @@ -222,7 +228,7 @@ impl TryFrom<&CredentialDescriptor> for Ctap2PublicKeyCredentialDescriptor { type Error = Error; fn try_from(value: &CredentialDescriptor) -> Result { let transports = value.transports.as_ref().filter(|t| !t.is_empty()); - let transports = match transports { + let transports = match transports { Some(transports) => { let mut transport_list = transports.iter().map(|t| match t.as_ref() { "ble" => Some(Ctap2Transport::BLE), @@ -232,10 +238,12 @@ impl TryFrom<&CredentialDescriptor> for Ctap2PublicKeyCredentialDescriptor { _ => None, }); if transport_list.any(|t| t.is_none()) { - return Err(Error::Internal("Invalid transport type specified".to_owned())); + return Err(Error::Internal( + "Invalid transport type specified".to_owned(), + )); } transport_list.collect() - }, + } None => None, }; Ok(Self { @@ -283,7 +291,10 @@ pub(crate) struct PublicKeyCredentialParameters { impl PublicKeyCredentialParameters { pub(crate) fn new(alg: i64) -> Self { - Self { cred_type: "public-key".to_string(), alg } + Self { + cred_type: "public-key".to_string(), + alg, + } } } @@ -295,7 +306,11 @@ impl TryFrom<&PublicKeyCredentialParameters> for Ctap2CredentialType { -7 => libwebauthn::proto::ctap2::Ctap2COSEAlgorithmIdentifier::ES256, -8 => libwebauthn::proto::ctap2::Ctap2COSEAlgorithmIdentifier::EDDSA, // TODO: we should still pass on the raw value to the authenticator and let it decide whether it's supported. - _ => return Err(Error::Internal("Invalid algorithm passed for new credential".to_owned())), + _ => { + return Err(Error::Internal( + "Invalid algorithm passed for new credential".to_owned(), + )) + } }; Ok(Self { public_key_type: Ctap2PublicKeyCredentialType::PublicKey, @@ -358,7 +373,7 @@ pub struct CredentialSource { } impl CredentialSource { - pub(crate) fn rp_id_hash<'a> (&'a self) -> Vec { + pub(crate) fn rp_id_hash<'a>(&'a self) -> Vec { let hash = digest::digest(&digest::SHA256, self.rp_id.as_bytes()); hash.as_ref().to_owned() } @@ -375,7 +390,6 @@ pub(crate) enum AttestationStatementFormat { Packed, } - #[derive(Debug, PartialEq)] pub(crate) enum AttestationStatement { None, @@ -393,11 +407,18 @@ impl TryFrom<&Ctap2AttestationStatement> for AttestationStatement { match value { Ctap2AttestationStatement::None(_) => Ok(AttestationStatement::None), Ctap2AttestationStatement::PackedOrAndroid(att_stmt) => { - let alg = att_stmt.algorithm.try_into().map_err(|_| Error::NotSupported)?; + let alg = att_stmt + .algorithm + .try_into() + .map_err(|_| Error::NotSupported)?; Ok(Self::Packed { algorithm: alg, signature: att_stmt.signature.as_ref().to_vec(), - certificates: att_stmt.certificates.iter().map(|c| c.as_ref().to_vec()).collect() + certificates: att_stmt + .certificates + .iter() + .map(|c| c.as_ref().to_vec()) + .collect(), }) } _ => { @@ -419,7 +440,6 @@ pub struct CreatePublicKeyCredentialResponse { extensions: Option, } - /// Returned from a creation of a new public key credential. pub struct AttestationResponse { /// clientDataJSON. From 2cab0c72e59dea20f64df0929ec19acbccbdf251 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus Date: Thu, 17 Apr 2025 16:30:20 +0200 Subject: [PATCH 4/4] Replace hard-coded attachment modality --- webext/add-on/content.js | 3 +- .../src/credential_service/mod.rs | 19 +++++++--- .../src/dbus.rs | 36 +++++++++++++++---- .../src/platform_authenticator/mod.rs | 2 ++ .../src/webauthn.rs | 26 +++++++++++--- 5 files changed, 69 insertions(+), 17 deletions(-) diff --git a/webext/add-on/content.js b/webext/add-on/content.js index fee7276..e55eb72 100644 --- a/webext/add-on/content.js +++ b/webext/add-on/content.js @@ -35,8 +35,7 @@ async function cloneCredentialResponse(credential) { const obj = {} obj.id = credential.id; obj.rawId = cloneInto(Uint8Array.fromBase64(credential.rawId, options), obj) - // TODO: get authenticator attachment - obj.authenticatorAttachment = undefined + obj.authenticatorAttachment = credential.authenticatorAttachment; const response = {} // credential registration response if (credential.response.attestationObject) { diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/mod.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/mod.rs index 0e82286..ad16000 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/mod.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/mod.rs @@ -22,7 +22,10 @@ use tokio::runtime::Runtime; use tracing::{debug, warn}; use crate::{ - dbus::{CredentialRequest, CredentialResponse, MakeCredentialResponseInternal}, + dbus::{ + CredentialRequest, CredentialResponse, GetAssertionResponseInternal, + MakeCredentialResponseInternal, + }, view_model::{Device, InternalPinState, Transport}, }; @@ -203,7 +206,8 @@ impl CredentialService { CredentialResponse::CreatePublicKeyCredentialResponse( MakeCredentialResponseInternal::new( r, - vec![String::from("usb"), String::from("usb")], + vec![String::from("usb")], + String::from("cross-platform"), ), ), ); @@ -216,7 +220,10 @@ impl CredentialService { let mut cred_response = self.cred_response.lock().unwrap(); cred_response.replace( CredentialResponse::GetPublicKeyCredentialResponse( - r.assertions[0].clone(), + GetAssertionResponseInternal::new( + r.assertions[0].clone(), + String::from("cross-platform"), + ), ), ); Ok(UsbState::Completed) @@ -257,6 +264,7 @@ impl CredentialService { MakeCredentialResponseInternal::new( r, vec![String::from("usb")], + String::from("cross-platform"), ), ), ); @@ -269,7 +277,10 @@ impl CredentialService { let mut cred_response = self.cred_response.lock().unwrap(); cred_response.replace( CredentialResponse::GetPublicKeyCredentialResponse( - r.assertions[0].clone(), + GetAssertionResponseInternal::new( + r.assertions[0].clone(), + String::from("cross-platform"), + ), ), ); Ok(UsbState::Completed) diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs index 0dc109d..467128a 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs @@ -291,20 +291,41 @@ pub(crate) enum CredentialRequest { #[derive(Clone, Debug)] pub(crate) enum CredentialResponse { CreatePublicKeyCredentialResponse(MakeCredentialResponseInternal), - GetPublicKeyCredentialResponse(Assertion), + GetPublicKeyCredentialResponse(GetAssertionResponseInternal), } #[derive(Clone, Debug)] pub(crate) struct MakeCredentialResponseInternal { ctap: MakeCredentialResponse, transport: Vec, + attachment_modality: String, } impl MakeCredentialResponseInternal { - pub(crate) fn new(response: MakeCredentialResponse, transport: Vec) -> Self { + pub(crate) fn new( + response: MakeCredentialResponse, + transport: Vec, + attachment_modality: String, + ) -> Self { Self { ctap: response, transport, + attachment_modality, + } + } +} + +#[derive(Clone, Debug)] +pub(crate) struct GetAssertionResponseInternal { + ctap: Assertion, + attachment_modality: String, +} + +impl GetAssertionResponseInternal { + pub(crate) fn new(ctap: Assertion, attachment_modality: String) -> Self { + Self { + ctap, + attachment_modality, } } } @@ -516,6 +537,7 @@ impl CreatePublicKeyCredentialResponse { client_data_json, Some(response.transport.clone()), None, + response.attachment_modality.clone(), ) .to_json(); let response = CreatePublicKeyCredentialResponse { @@ -678,10 +700,10 @@ pub struct GetPublicKeyCredentialRequest { impl GetPublicKeyCredentialResponse { fn try_from_ctap2_response( - response: &Assertion, + response: &GetAssertionResponseInternal, client_data_json: String, ) -> std::result::Result { - let auth_data = &response.authenticator_data; + let auth_data = &response.ctap.authenticator_data; let attested_credential_data = match &auth_data.attested_credential { None => None, Some(att) => { @@ -719,12 +741,14 @@ impl GetPublicKeyCredentialResponse { let authentication_response_json = webauthn::GetPublicKeyCredentialResponse::new( client_data_json, response + .ctap .credential_id .as_ref() .map(|c| c.id.clone().into_vec()), authenticator_data_blob, - response.signature.clone(), - response.user.as_ref().map(|u| u.id.clone().into_vec()), + response.ctap.signature.clone(), + response.ctap.user.as_ref().map(|u| u.id.clone().into_vec()), + response.attachment_modality.clone(), ) .to_json(); let response = GetPublicKeyCredentialResponse { 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 b411b17..6240cd1 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 @@ -378,6 +378,7 @@ pub(crate) fn make_credential( client_data_json, None, None, + String::from("platform"), ); Ok((response, credential_source)) } @@ -548,6 +549,7 @@ fn get_credential( // selectedCredential.userHandle // 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"), }; 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. diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs index e566fbb..6ff6a26 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs @@ -438,6 +438,9 @@ pub struct CreatePublicKeyCredentialResponse { /// JSON string of extension output extensions: Option, + + /// If the device used is builtin ("platform") or removable ("cross-platform", aka "roaming") + attachment_modality: String, } /// Returned from a creation of a new public key credential. @@ -473,6 +476,7 @@ impl CreatePublicKeyCredentialResponse { client_data_json: String, transports: Option>, extension_output_json: Option, + attachment_modality: String, ) -> Self { Self { cred_type: "public-key".to_string(), @@ -484,6 +488,7 @@ impl CreatePublicKeyCredentialResponse { authenticator_data, }, extensions: extension_output_json, + attachment_modality, } } @@ -500,7 +505,8 @@ impl CreatePublicKeyCredentialResponse { let mut output = json!({ "id": self.get_id(), "rawId": self.get_id(), - "response": response + "response": response, + "authenticatorAttachment": self.attachment_modality, }); if let Some(extensions) = &self.extensions { let extension_value = @@ -534,10 +540,21 @@ pub struct GetPublicKeyCredentialResponse { /// created. This item is nullable, however user handle MUST always be /// populated for discoverable credentials. pub(crate) user_handle: Option>, + + /// 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, } impl GetPublicKeyCredentialResponse { - pub(crate) fn new(client_data_json: String, id: Option>, authenticator_data: Vec, signature: Vec, user_handle: Option>) -> Self { + pub(crate) fn new( + client_data_json: String, + id: Option>, + authenticator_data: Vec, + signature: Vec, + user_handle: Option>, + attachment_modality: String, + ) -> Self { Self { cred_type: "public-key".to_string(), client_data_json, @@ -545,6 +562,7 @@ impl GetPublicKeyCredentialResponse { authenticator_data, signature, user_handle, + attachment_modality, } } pub fn to_json(&self) -> String { @@ -560,12 +578,10 @@ 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)); - // TODO: Fix for platorm authenticator - let attachment = "cross-platform"; let output = json!({ "id": id, "rawId": id, - "authenticatorAttachment": attachment, + "authenticatorAttachment": self.attachment_modality, "response": response }); // TODO: support client extensions