From ab1e92e8da24b8a3c53139974f6e5d1691d57d84 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Thu, 20 Nov 2025 17:49:56 -0600 Subject: [PATCH] Implement PluginAuthenticator::make_credential --- .../src/assert.rs | 7 +- .../windows_plugin_authenticator/src/lib.rs | 4 +- .../src/make_credential.rs | 192 ++++- .../src/win_webauthn/com.rs | 48 +- .../src/win_webauthn/mod.rs | 6 +- .../src/win_webauthn/types.rs | 755 +++++++++++++++++- .../src/win_webauthn/util.rs | 31 + 7 files changed, 967 insertions(+), 76 deletions(-) diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/assert.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/assert.rs index 42ac43d0df5..527f579e5c2 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/assert.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/assert.rs @@ -21,7 +21,10 @@ pub fn get_assertion( let client_data_hash = request.client_data_hash().to_vec(); // Extract user verification requirement from authenticator options - let user_verification = match request.authenticator_options().user_verification() { + let user_verification = match request + .authenticator_options() + .and_then(|opts| opts.user_verification()) + { Some(true) => UserVerification::Required, Some(false) => UserVerification::Discouraged, None => UserVerification::Preferred, @@ -29,7 +32,7 @@ pub fn get_assertion( // Extract allowed credentials from credential list let allowed_credential_ids: Vec> = request - .credential_list() + .allow_credentials() .iter() .filter_map(|cred| cred.credential_id()) .map(|id| id.to_vec()) diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs index 6502849da36..90484806fb1 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs @@ -22,6 +22,7 @@ use win_webauthn::{PluginAddAuthenticatorOptions, WebAuthnPlugin}; use crate::{ ipc2::{ConnectionStatus, TimedCallback, WindowsProviderClient}, + make_credential::make_credential, win_webauthn::{ AuthenticatorInfo, CtapVersion, PluginAuthenticator, PluginCancelOperationRequest, PluginGetAssertionRequest, PluginLockStatus, PluginMakeCredentialRequest, @@ -105,7 +106,8 @@ impl PluginAuthenticator for BitwardenPluginAuthenticator { request: PluginMakeCredentialRequest, ) -> Result, Box> { tracing::debug!("Received MakeCredential: {request:?}"); - Err(format!("MakeCredential not implemented").into()) + let client = self.get_client(); + make_credential::make_credential(&client, request) } fn get_assertion( diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/make_credential.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/make_credential.rs index 9043ed33d74..cbc60c89d07 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/make_credential.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/make_credential.rs @@ -1,5 +1,6 @@ use serde_json; use std::collections::HashMap; +use std::io::Write; use std::mem::ManuallyDrop; use std::ptr; use std::sync::Arc; @@ -15,6 +16,120 @@ use crate::ipc2::{ }; use crate::util::{delay_load, wstr_to_string, WindowsString}; use crate::webauthn::WEBAUTHN_CREDENTIAL_LIST; +use crate::win_webauthn::{ + CtapTransport, ErrorKind, HwndExt, PluginMakeCredentialRequest, PluginMakeCredentialResponse, + WinWebAuthnError, +}; + +pub fn make_credential( + ipc_client: &WindowsProviderClient, + request: PluginMakeCredentialRequest, +) -> Result, Box> { + tracing::debug!("=== PluginMakeCredential() called ==="); + + // Extract RP information + let rp_info = request + .rp_information() + .ok_or_else(|| "RP information is null".to_string())?; + + let rpid = rp_info.id()?; + + // let rp_name = rp_info.name().unwrap_or_else(|| String::new()); + + // Extract user information + let user = request + .user_information() + .ok_or_else(|| "User information is null".to_string())?; + + let user_handle = user + .id() + .map_err(|err| format!("User ID is required for registration: {err}"))? + .to_vec(); + + let user_name = user + .name() + .map_err(|err| format!("User name is required for registration: {err}"))?; + + // let user_display_name = user.display_name(); + + // Extract client data hash + let client_data_hash = request + .client_data_hash() + .map_err(|err| format!("Client data hash is required for registration: {err}"))? + .to_vec(); + + // Extract supported algorithms + let supported_algorithms: Vec = request + .pub_key_cred_params() + .iter() + .map(|params| params.alg()) + .collect(); + + // Extract user verification requirement from authenticator options + let user_verification = match request + .authenticator_options() + .and_then(|opts| opts.user_verification()) + { + Some(true) => UserVerification::Required, + Some(false) => UserVerification::Discouraged, + None => UserVerification::Preferred, + }; + + // Extract excluded credentials from credential list + let excluded_credentials: Vec> = request + .exclude_credentials() + .iter() + .filter_map(|cred| cred.credential_id()) + .map(|id| id.to_vec()) + .collect(); + if !excluded_credentials.is_empty() { + tracing::debug!( + "Found {} excluded credentials for make credential", + excluded_credentials.len() + ); + } + + let transaction_id = request.transaction_id.to_u128().to_le_bytes().to_vec(); + let client_pos = request + .window_handle + .center_position() + .unwrap_or((640, 480)); + + // Create Windows registration request + let registration_request = PasskeyRegistrationRequest { + rp_id: rpid.clone(), + user_handle: user_handle, + user_name: user_name, + // user_display_name: user_info.2, + client_data_hash, + excluded_credentials, + user_verification: user_verification, + supported_algorithms, + window_xy: Position { + x: client_pos.0, + y: client_pos.1, + }, + context: transaction_id, + }; + + tracing::debug!( + "Make credential request - RP: {}, User: {}", + rpid, + registration_request.user_name + ); + + // Send registration request + let passkey_response = send_registration_request(ipc_client, registration_request) + .map_err(|err| format!("Registration request failed: {err}"))?; + tracing::debug!("Registration response received: {:?}", passkey_response); + + // Create proper WebAuthn response from passkey_response + tracing::debug!("Creating WebAuthn make credential response"); + let webauthn_response = create_make_credential_response(passkey_response.attestation_object) + .map_err(|err| format!("Failed to create WebAuthn response: {err}"))?; + tracing::debug!("Successfully created WebAuthn response: {webauthn_response:?}"); + Ok(webauthn_response) +} // Windows API types for WebAuthn (from webauthn.h.sample) #[repr(C)] @@ -331,39 +446,64 @@ fn send_registration_request( } /// Creates a CTAP make credential response from Bitwarden's WebAuthn registration response -unsafe fn create_make_credential_response( +fn create_make_credential_response( attestation_object: Vec, -) -> std::result::Result, HRESULT> { +) -> std::result::Result, WinWebAuthnError> { use ciborium::Value; // Use the attestation object directly as the encoded response let att_obj_items = ciborium::from_reader::(&attestation_object[..]) - .map_err(|_| HRESULT(-1))? + .map_err(|err| { + WinWebAuthnError::with_cause( + ErrorKind::Serialization, + "Failed to deserialize WebAuthn attestation object", + err, + ) + })? .into_map() - .map_err(|_| HRESULT(-1))?; + .map_err(|_| WinWebAuthnError::new(ErrorKind::Serialization, "object is not a CBOR map"))?; let webauthn_att_obj: HashMap<&str, &Value> = att_obj_items .iter() .map(|(k, v)| (k.as_text().unwrap(), v)) .collect(); - let webauthn_encode_make_credential_response = - delay_load::( - s!("webauthn.dll"), - s!("WebAuthNEncodeMakeCredentialResponse"), - ) - .unwrap(); let att_fmt = webauthn_att_obj .get("fmt") - .ok_or(HRESULT(-1))? - .as_text() - .ok_or(HRESULT(-1))? - .to_utf16(); + .and_then(|s| s.as_text()) + .ok_or(WinWebAuthnError::new( + ErrorKind::Serialization, + "could not read `fmt` key as a string", + ))? + .to_string(); let authenticator_data = webauthn_att_obj .get("authData") - .ok_or(HRESULT(-1))? - .as_bytes() - .ok_or(HRESULT(-1))?; - let attestation = WEBAUTHN_CREDENTIAL_ATTESTATION { + .and_then(|d| d.as_bytes()) + .ok_or(WinWebAuthnError::new( + ErrorKind::Serialization, + "could not read `authData` key as bytes", + ))? + .clone(); + let attestation = PluginMakeCredentialResponse { + format_type: att_fmt, + authenticator_data: authenticator_data, + attestation_statement: None, + attestation_object: None, + credential_id: None, + extensions: None, + used_transport: CtapTransport::Internal, + ep_att: false, + large_blob_supported: false, + resident_key: true, + prf_enabled: false, + unsigned_extension_outputs: None, + hmac_secret: None, + third_party_payment: false, + transports: Some(vec![CtapTransport::Internal, CtapTransport::Hybrid]), + client_data_json: None, + registration_response_json: None, + }; + /* + { dwVersion: 8, pwszFormatType: att_fmt.as_ptr(), cbAuthenticatorData: authenticator_data.len() as u32, @@ -395,19 +535,8 @@ unsafe fn create_make_credential_response( cbRegistrationResponseJSON: 0, pbRegistrationResponseJSON: ptr::null_mut(), }; - let mut response_len = 0; - let mut response_ptr = ptr::null_mut(); - let result = webauthn_encode_make_credential_response( - &attestation, - &mut response_len, - &mut response_ptr, - ); - if result.is_err() { - return Err(result); - } - let response = Vec::from_raw_parts(response_ptr, response_len as usize, response_len as usize); - - Ok(response) + */ + attestation.to_ctap_response() } /// Implementation of PluginMakeCredential moved from com_provider.rs @@ -429,7 +558,6 @@ pub unsafe fn plugin_make_credential( } let req = &*request; - let transaction_id = format!("{:?}", req.transaction_id); let coords = req.window_coordinates().unwrap_or((400, 400)); diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/com.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/com.rs index 1c809679fa4..5261423dacf 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/com.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/com.rs @@ -105,27 +105,44 @@ impl IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Impl { response: *mut WEBAUTHN_PLUGIN_OPERATION_RESPONSE, ) -> HRESULT { tracing::debug!("MakeCredential called"); - // Convert to legacy format for internal processing - if request.is_null() || response.is_null() { - tracing::debug!("MakeCredential: Invalid request or response pointers passed"); - return HRESULT(-1); + if response.is_null() { + tracing::warn!( + "GetAssertion called with null response pointer from Windows. Aborting request." + ); + return E_INVALIDARG; } - // TODO: verify request signature - return HRESULT(-1); + let op_request_ptr = match NonNull::new(request as *mut WEBAUTHN_PLUGIN_OPERATION_REQUEST) { + Some(p) => p, + None => { + tracing::warn!( + "GetAssertion called with null request pointer from Windows. Aborting request." + ); + return E_INVALIDARG; + } + }; - /* - match self.handler.make_credential(request) { - Ok(response) => { - // todo DECODE + // TODO: verify request signature + + let registration_request = match op_request_ptr.try_into() { + Ok(r) => r, + Err(err) => { + tracing::error!("Could not deserialize MakeCredential request: {err}"); + return E_FAIL; + } + }; + match self.handler.make_credential(registration_request) { + Ok(registration_response) => { + let (ptr, len) = ComBuffer::from_buffer(registration_response); + (*response).cbEncodedResponse = len; + (*response).pbEncodedResponse = ptr; tracing::debug!("MakeCredential completed successfully"); S_OK } Err(err) => { tracing::error!("MakeCredential failed: {err}"); - HRESULT(-1) + E_FAIL } } - */ } unsafe fn GetAssertion( @@ -162,13 +179,6 @@ impl IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Impl { let (ptr, len) = ComBuffer::from_buffer(assertion_response); (*response).cbEncodedResponse = len; (*response).pbEncodedResponse = ptr; - /* - std::ptr::copy_nonoverlapping( - assertion_response.as_ptr(), - (*response).pbEncodedResponse, - assertion_response.len(), - ); - */ tracing::debug!("GetAssertion completed successfully"); S_OK } diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/mod.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/mod.rs index 387868600d8..87c76dc3b8e 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/mod.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/mod.rs @@ -7,9 +7,9 @@ use std::{error::Error, fmt::Display, ptr::NonNull}; use windows::core::GUID; pub use types::{ - AuthenticatorInfo, CtapVersion, PluginAddAuthenticatorOptions, PluginCancelOperationRequest, - PluginGetAssertionRequest, PluginLockStatus, PluginMakeCredentialRequest, - PublicKeyCredentialParameters, + AuthenticatorInfo, CtapTransport, CtapVersion, PluginAddAuthenticatorOptions, + PluginCancelOperationRequest, PluginGetAssertionRequest, PluginLockStatus, + PluginMakeCredentialRequest, PluginMakeCredentialResponse, PublicKeyCredentialParameters, }; pub use com::PluginAuthenticator; diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/types.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/types.rs index ee2de74dd13..c2219f3bdf1 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/types.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/types.rs @@ -1,17 +1,22 @@ //! Types and functions defined in the Windows WebAuthn API. -use std::{collections::HashSet, mem::MaybeUninit, ptr::NonNull}; +use std::{collections::HashSet, fmt::Display, mem::MaybeUninit, ptr::NonNull}; use base64::{engine::general_purpose::STANDARD, Engine as _}; use ciborium::Value; use windows::{ core::{GUID, HRESULT}, - Win32::{Foundation::HWND, System::LibraryLoader::GetProcAddress}, + Win32::{ + Foundation::HWND, System::LibraryLoader::GetProcAddress, + UI::WindowsAndMessaging::WindowFromPoint, + }, }; use windows_core::{s, PCWSTR}; use crate::win_webauthn::{ - com::ComBuffer, util::WindowsString, Clsid, ErrorKind, WinWebAuthnError, + com::ComBuffer, + util::{ArrayPointerIterator, WindowsString}, + Clsid, ErrorKind, WinWebAuthnError, }; macro_rules! webauthn_call { @@ -379,14 +384,719 @@ pub enum WebAuthnPluginRequestType { CTAP2_CBOR = 0x01, } -#[derive(Debug)] -pub struct PluginMakeCredentialRequest {} -// pub struct PluginMakeCredentialResponse {} - -// Windows API types for WebAuthn (from webauthn.h.sample) #[repr(C)] #[derive(Debug, Copy, Clone)] -pub struct WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST { +pub struct WEBAUTHN_RP_ENTITY_INFORMATION { + dwVersion: u32, + pwszId: *const u16, // PCWSTR + pwszName: *const u16, // PCWSTR + pwszIcon: *const u16, // PCWSTR +} + +impl WEBAUTHN_RP_ENTITY_INFORMATION { + /// Relying party ID. + pub fn id(&self) -> Result { + if self.pwszId.is_null() { + return Err(WinWebAuthnError::new( + ErrorKind::WindowsInternal, + "Received invalid RP ID", + )); + } + unsafe { + PCWSTR(self.pwszId).to_string().map_err(|err| { + WinWebAuthnError::with_cause(ErrorKind::WindowsInternal, "Invalid RP ID", err) + }) + } + } + + /// Relying party name. + pub fn name(&self) -> Result { + if self.pwszName.is_null() { + return Err(WinWebAuthnError::new( + ErrorKind::WindowsInternal, + "Received invalid RP name", + )); + } + unsafe { + PCWSTR(self.pwszName).to_string().map_err(|err| { + WinWebAuthnError::with_cause(ErrorKind::WindowsInternal, "Invalid RP name", err) + }) + } + } +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct WEBAUTHN_USER_ENTITY_INFORMATION { + pub dwVersion: u32, + pub cbId: u32, // DWORD + pub pbId: *const u8, // PBYTE + pub pwszName: *const u16, // PCWSTR + pub pwszIcon: *const u16, // PCWSTR + pub pwszDisplayName: *const u16, // PCWSTR +} + +impl WEBAUTHN_USER_ENTITY_INFORMATION { + /// User handle. + pub fn id(&self) -> Result<&[u8], WinWebAuthnError> { + if self.cbId == 0 || self.pbId.is_null() { + return Err(WinWebAuthnError::new( + ErrorKind::WindowsInternal, + "Received invalid user ID", + )); + } + unsafe { Ok(std::slice::from_raw_parts(self.pbId, self.cbId as usize)) } + } + + /// User name. + pub fn name(&self) -> Result { + if self.pwszName.is_null() { + return Err(WinWebAuthnError::new( + ErrorKind::WindowsInternal, + "Received invalid user name", + )); + } + unsafe { + PCWSTR(self.pwszName).to_string().map_err(|err| { + WinWebAuthnError::with_cause(ErrorKind::WindowsInternal, "Invalid user name", err) + }) + } + } + + /// User display name. + pub fn display_name(&self) -> Result { + if self.pwszDisplayName.is_null() { + return Err(WinWebAuthnError::new( + ErrorKind::WindowsInternal, + "Received invalid user name", + )); + } + unsafe { + PCWSTR(self.pwszDisplayName).to_string().map_err(|err| { + WinWebAuthnError::with_cause( + ErrorKind::WindowsInternal, + "Invalid user display name", + err, + ) + }) + } + } +} +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct WEBAUTHN_COSE_CREDENTIAL_PARAMETER { + pub dwVersion: u32, + pub pwszCredentialType: *const u16, // LPCWSTR + pub lAlg: i32, // LONG - COSE algorithm identifier +} + +impl WEBAUTHN_COSE_CREDENTIAL_PARAMETER { + pub fn credential_type(&self) -> Result { + if self.pwszCredentialType.is_null() { + return Err(WinWebAuthnError::new( + ErrorKind::WindowsInternal, + "Invalid credential type", + )); + } + unsafe { + PCWSTR(self.pwszCredentialType).to_string().map_err(|err| { + WinWebAuthnError::with_cause( + ErrorKind::WindowsInternal, + "Invalid credential type", + err, + ) + }) + } + } + pub fn alg(&self) -> i32 { + self.lAlg + } +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct WEBAUTHN_COSE_CREDENTIAL_PARAMETERS { + cCredentialParameters: u32, + pCredentialParameters: *const WEBAUTHN_COSE_CREDENTIAL_PARAMETER, +} + +impl WEBAUTHN_COSE_CREDENTIAL_PARAMETERS { + pub fn iter(&self) -> ArrayPointerIterator<'_, WEBAUTHN_COSE_CREDENTIAL_PARAMETER> { + unsafe { + ArrayPointerIterator::new( + self.pCredentialParameters, + self.cCredentialParameters as usize, + ) + } + } +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +struct WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST { + pub dwVersion: u32, + pub cbRpId: u32, + pub pbRpId: *const u8, + pub cbClientDataHash: u32, + pub pbClientDataHash: *const u8, + pub pRpInformation: *const WEBAUTHN_RP_ENTITY_INFORMATION, + pub pUserInformation: *const WEBAUTHN_USER_ENTITY_INFORMATION, + pub WebAuthNCredentialParameters: WEBAUTHN_COSE_CREDENTIAL_PARAMETERS, // Matches C++ sample + pub CredentialList: WEBAUTHN_CREDENTIAL_LIST, + pub cbCborExtensionsMap: u32, + pub pbCborExtensionsMap: *const u8, + pub pAuthenticatorOptions: *const WebAuthnCtapCborAuthenticatorOptions, + // Add other fields as needed... +} + +#[derive(Debug)] +pub struct PluginMakeCredentialRequest { + inner: *const WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST, + pub window_handle: HWND, + pub transaction_id: GUID, + pub request_signature: Vec, +} + +impl PluginMakeCredentialRequest { + pub fn client_data_hash(&self) -> Result<&[u8], WinWebAuthnError> { + if self.as_ref().cbClientDataHash == 0 || self.as_ref().pbClientDataHash.is_null() { + return Err(WinWebAuthnError::new( + ErrorKind::WindowsInternal, + "Received invalid client data hash", + )); + } + unsafe { + Ok(std::slice::from_raw_parts( + self.as_ref().pbClientDataHash, + self.as_ref().cbClientDataHash as usize, + )) + } + } + + pub fn rp_information(&self) -> Option<&WEBAUTHN_RP_ENTITY_INFORMATION> { + let ptr = self.as_ref().pRpInformation; + if ptr.is_null() { + return None; + } + unsafe { Some(&*ptr) } + } + + pub fn user_information(&self) -> Option<&WEBAUTHN_USER_ENTITY_INFORMATION> { + let ptr = self.as_ref().pUserInformation; + if ptr.is_null() { + return None; + } + unsafe { Some(&*ptr) } + } + + pub fn pub_key_cred_params(&self) -> WEBAUTHN_COSE_CREDENTIAL_PARAMETERS { + self.as_ref().WebAuthNCredentialParameters + } + + pub fn exclude_credentials(&self) -> CredentialList { + self.as_ref().CredentialList + } + + /// CTAP CBOR extensions map + pub fn extensions(&self) -> Option<&[u8]> { + let (len, ptr) = ( + self.as_ref().cbCborExtensionsMap, + self.as_ref().pbCborExtensionsMap, + ); + if len == 0 || ptr.is_null() { + return None; + } + unsafe { Some(std::slice::from_raw_parts(ptr, len as usize)) } + } + + pub fn authenticator_options(&self) -> Option { + let ptr = self.as_ref().pAuthenticatorOptions; + if ptr.is_null() { + return None; + } + unsafe { Some(*ptr) } + } +} + +impl AsRef for PluginMakeCredentialRequest { + fn as_ref(&self) -> &WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST { + unsafe { &*self.inner } + } +} + +impl Drop for PluginMakeCredentialRequest { + fn drop(&mut self) { + if !self.inner.is_null() { + // leak memory if we cannot find the free function + _ = webauthn_free_decoded_make_credential_request( + self.inner as *mut WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST, + ); + } + } +} + +impl TryFrom> for PluginMakeCredentialRequest { + type Error = WinWebAuthnError; + + fn try_from(value: NonNull) -> Result { + unsafe { + let request = value.as_ref(); + if !matches!(request.requestType, WebAuthnPluginRequestType::CTAP2_CBOR) { + return Err(WinWebAuthnError::new( + ErrorKind::Serialization, + "Unknown plugin operation request type", + )); + } + let mut registration_request = MaybeUninit::uninit(); + webauthn_decode_make_credential_request( + request.cbEncodedRequest, + request.pbEncodedRequest, + registration_request.as_mut_ptr(), + )? + .ok() + .map_err(|err| { + WinWebAuthnError::with_cause( + ErrorKind::WindowsInternal, + "Failed to decode get assertion request", + err, + ) + })?; + + let registration_request = registration_request.assume_init(); + Ok(Self { + inner: registration_request as *const WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST, + window_handle: request.hWnd, + transaction_id: request.transactionId, + request_signature: Vec::from_raw_parts( + request.pbRequestSignature, + request.cbEncodedRequest as usize, + request.cbEncodedRequest as usize, + ), + }) + } + } +} + +// Windows API function signatures for decoding make credential requests +webauthn_call!("WebAuthNDecodeMakeCredentialRequest" as fn webauthn_decode_make_credential_request( + cbEncoded: u32, + pbEncoded: *const u8, + ppMakeCredentialRequest: *mut *mut WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST +) -> HRESULT); + +webauthn_call!("WebAuthNFreeDecodedMakeCredentialRequest" as fn webauthn_free_decoded_make_credential_request( + pMakeCredentialRequest: *mut WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST +) -> ()); + +// pub struct PluginMakeCredentialResponse {} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +struct WEBAUTHN_CREDENTIAL_ATTESTATION { + /// Version of this structure, to allow for modifications in the future. + dwVersion: u32, + + /// Attestation format type + pwszFormatType: *const u16, // PCWSTR + + /// Size of cbAuthenticatorData. + cbAuthenticatorData: u32, + /// Authenticator data that was created for this credential. + //_Field_size_bytes_(cbAuthenticatorData) + pbAuthenticatorData: *const u8, + + /// Size of CBOR encoded attestation information + /// 0 => encoded as CBOR null value. + cbAttestation: u32, + ///Encoded CBOR attestation information + // _Field_size_bytes_(cbAttestation) + pbAttestation: *const u8, + + dwAttestationDecodeType: u32, + /// Following depends on the dwAttestationDecodeType + /// WEBAUTHN_ATTESTATION_DECODE_NONE + /// NULL - not able to decode the CBOR attestation information + /// WEBAUTHN_ATTESTATION_DECODE_COMMON + /// PWEBAUTHN_COMMON_ATTESTATION; + pvAttestationDecode: *const u8, + + /// The CBOR encoded Attestation Object to be returned to the RP. + cbAttestationObject: u32, + // _Field_size_bytes_(cbAttestationObject) + pbAttestationObject: *const u8, + + /// The CredentialId bytes extracted from the Authenticator Data. + /// Used by Edge to return to the RP. + cbCredentialId: u32, + // _Field_size_bytes_(cbCredentialId) + pbCredentialId: *const u8, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_2 + // + /// Since VERSION 2 + Extensions: WEBAUTHN_EXTENSIONS, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_3 + // + /// One of the WEBAUTHN_CTAP_TRANSPORT_* bits will be set corresponding to + /// the transport that was used. + dwUsedTransport: u32, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_4 + // + bEpAtt: bool, + bLargeBlobSupported: bool, + bResidentKey: bool, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_5 + // + bPrfEnabled: bool, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_6 + // + cbUnsignedExtensionOutputs: u32, + // _Field_size_bytes_(cbUnsignedExtensionOutputs) + pbUnsignedExtensionOutputs: *const u8, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_7 + // + pHmacSecret: *const WEBAUTHN_HMAC_SECRET_SALT, + + // ThirdPartyPayment Credential or not. + bThirdPartyPayment: bool, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_8 + // + + // Multiple WEBAUTHN_CTAP_TRANSPORT_* bits will be set corresponding to + // the transports that are supported. + dwTransports: u32, + + // UTF-8 encoded JSON serialization of the client data. + cbClientDataJSON: u32, + // _Field_size_bytes_(cbClientDataJSON) + pbClientDataJSON: *const u8, + + // UTF-8 encoded JSON serialization of the RegistrationResponse. + cbRegistrationResponseJSON: u32, + // _Field_size_bytes_(cbRegistrationResponseJSON) + pbRegistrationResponseJSON: *const u8, +} + +pub enum AttestationFormat { + Packed, + Tpm, + AndroidKey, + FidoU2f, + None, + Compound, + Apple, +} + +impl Display for AttestationFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Packed => "packed", + Self::Tpm => "tpm", + Self::AndroidKey => "android-key", + Self::FidoU2f => "fido-u2f", + Self::None => "none", + Self::Compound => "compound", + Self::Apple => "apple", + }) + } +} + +pub enum AttestationDecodeType { + None, + Common(), +} + +struct WEBAUTHN_HMAC_SECRET_SALT { + /// Size of pbFirst. + cbFirst: u32, + // _Field_size_bytes_(cbFirst) + /// Required + pbFirst: *mut u8, + + /// Size of pbSecond. + cbSecond: u32, + // _Field_size_bytes_(cbSecond) + pbSecond: *mut u8, +} + +pub struct HmacSecretSalt { + first: Vec, + second: Option>, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +struct WEBAUTHN_EXTENSION { + pwszExtensionIdentifier: *const u16, + cbExtension: u32, + pvExtension: *mut u8, +} + +pub enum CredProtectOutput { + UserVerificationAny, + UserVerificationOptional, + UserVerificationOptionalWithCredentialIdList, + UserVerificationRequired, +} +pub enum WebAuthnExtensionMakeCredentialOutput { + HmacSecret(bool), + CredProtect(CredProtectOutput), + CredBlob(bool), + MinPinLength(u32), + // LargeBlob, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +struct WEBAUTHN_EXTENSIONS { + cExtensions: u32, + // _Field_size_(cExtensions) + pExtensions: *const WEBAUTHN_EXTENSION, +} + +pub struct PluginMakeCredentialResponse { + /// Attestation format type + pub format_type: String, // PCWSTR + + /// Authenticator data that was created for this credential. + pub authenticator_data: Vec, + + ///Encoded CBOR attestation information + pub attestation_statement: Option>, + + // dwAttestationDecodeType: u32, + /// Following depends on the dwAttestationDecodeType + /// WEBAUTHN_ATTESTATION_DECODE_NONE + /// NULL - not able to decode the CBOR attestation information + /// WEBAUTHN_ATTESTATION_DECODE_COMMON + /// PWEBAUTHN_COMMON_ATTESTATION; + // pub pvAttestationDecode: *mut u8, + + /// The CBOR-encoded Attestation Object to be returned to the RP. + pub attestation_object: Option>, + + /// The CredentialId bytes extracted from the Authenticator Data. + /// Used by Edge to return to the RP. + pub credential_id: Option>, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_2 + // + /// Since VERSION 2 + pub extensions: Option>, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_3 + // + /// One of the WEBAUTHN_CTAP_TRANSPORT_* bits will be set corresponding to + /// the transport that was used. + pub used_transport: CtapTransport, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_4 + // + pub ep_att: bool, + pub large_blob_supported: bool, + pub resident_key: bool, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_5 + // + pub prf_enabled: bool, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_6 + // + pub unsigned_extension_outputs: Option>, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_7 + // + pub hmac_secret: Option, + + /// ThirdPartyPayment Credential or not. + pub third_party_payment: bool, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_8 + // + /// Multiple WEBAUTHN_CTAP_TRANSPORT_* bits will be set corresponding to + /// the transports that are supported. + pub transports: Option>, + + /// UTF-8 encoded JSON serialization of the client data. + pub client_data_json: Option>, + + /// UTF-8 encoded JSON serialization of the RegistrationResponse. + pub registration_response_json: Option>, +} + +impl PluginMakeCredentialResponse { + pub fn to_ctap_response(mut self) -> Result, WinWebAuthnError> { + let attestation = self.try_into()?; + let mut response_len = 0; + let mut response_ptr = std::ptr::null_mut(); + let result = webauthn_encode_make_credential_response( + &attestation, + &mut response_len, + &mut response_ptr, + )? + .ok() + .map_err(|err| { + WinWebAuthnError::with_cause( + ErrorKind::WindowsInternal, + "WebAuthNEncodeMakeCredentialResponse() failed", + err, + ) + })?; + + if response_ptr.is_null() { + return Err(WinWebAuthnError::new( + ErrorKind::WindowsInternal, + "Received null pointer from WebAuthNEncodeMakeCredentialResponse", + )); + } + let response = unsafe { + Vec::from_raw_parts(response_ptr, response_len as usize, response_len as usize) + }; + + Ok(response) + } +} + +impl TryFrom for WEBAUTHN_CREDENTIAL_ATTESTATION { + type Error = WinWebAuthnError; + + fn try_from(value: PluginMakeCredentialResponse) -> Result { + // Convert format type to UTF-16 + let format_type_utf16 = value.format_type.to_utf16(); + let pwszFormatType = format_type_utf16.as_ptr(); + std::mem::forget(format_type_utf16); + + // Get authenticator data pointer and length + let pbAuthenticatorData = value.authenticator_data.as_ptr(); + let cbAuthenticatorData = value.authenticator_data.len() as u32; + std::mem::forget(value.authenticator_data); + + // Get optional attestation statement pointer and length + let (pbAttestation, cbAttestation) = match value.attestation_statement.as_ref() { + Some(data) => (data.as_ptr(), data.len() as u32), + None => (std::ptr::null(), 0), + }; + std::mem::forget(value.attestation_statement); + + // Get optional attestation object pointer and length + let (pbAttestationObject, cbAttestationObject) = match value.attestation_object.as_ref() { + Some(data) => (data.as_ptr(), data.len() as u32), + None => (std::ptr::null(), 0), + }; + std::mem::forget(value.attestation_object); + + // Get optional credential ID pointer and length + let (pbCredentialId, cbCredentialId) = match value.credential_id.as_ref() { + Some(data) => (data.as_ptr(), data.len() as u32), + None => (std::ptr::null(), 0), + }; + std::mem::forget(value.credential_id); + + // Convert extensions (TODO: implement proper extension conversion) + let extensions = WEBAUTHN_EXTENSIONS { + cExtensions: 0, + pExtensions: std::ptr::null(), + }; + + // Convert used transport enum to bitmask + let dwUsedTransport = value.used_transport as u32; + + // Get optional unsigned extension outputs pointer and length + let (pbUnsignedExtensionOutputs, cbUnsignedExtensionOutputs) = + match value.unsigned_extension_outputs.as_ref() { + Some(data) => (data.as_ptr(), data.len() as u32), + None => (std::ptr::null(), 0), + }; + std::mem::forget(value.unsigned_extension_outputs); + + // Convert optional HMAC secret (TODO: implement proper conversion) + let pHmacSecret = std::ptr::null(); + + // Convert optional transports to bitmask + let dwTransports = value + .transports + .as_ref() + .map_or(0, |t| t.iter().map(|transport| *transport as u32).sum()); + + // Get optional client data JSON pointer and length + let (pbClientDataJSON, cbClientDataJSON) = match value.client_data_json.as_ref() { + Some(data) => (data.as_ptr(), data.len() as u32), + None => (std::ptr::null(), 0), + }; + std::mem::forget(value.client_data_json); + + // Get optional registration response JSON pointer and length + let (pbRegistrationResponseJSON, cbRegistrationResponseJSON) = + match value.registration_response_json.as_ref() { + Some(data) => (data.as_ptr(), data.len() as u32), + None => (std::ptr::null(), 0), + }; + std::mem::forget(value.registration_response_json); + + let attestation = WEBAUTHN_CREDENTIAL_ATTESTATION { + // Use version 8 to include all fields + dwVersion: 8, + pwszFormatType, + cbAuthenticatorData, + pbAuthenticatorData, + cbAttestation, + pbAttestation, + // TODO: Support decode type. Just using WEBAUTHN_ATTESTATION_DECODE_NONE (0) for now. + dwAttestationDecodeType: 0, + pvAttestationDecode: std::ptr::null(), + cbAttestationObject, + pbAttestationObject, + cbCredentialId, + pbCredentialId, + Extensions: extensions, + dwUsedTransport, + bEpAtt: value.ep_att, + bLargeBlobSupported: value.large_blob_supported, + bResidentKey: value.resident_key, + bPrfEnabled: value.prf_enabled, + cbUnsignedExtensionOutputs, + pbUnsignedExtensionOutputs, + pHmacSecret, + bThirdPartyPayment: value.third_party_payment, + dwTransports, + cbClientDataJSON, + pbClientDataJSON, + cbRegistrationResponseJSON, + pbRegistrationResponseJSON, + }; + Ok(attestation) + } +} + +webauthn_call!("WebAuthNEncodeMakeCredentialResponse" as fn webauthn_encode_make_credential_response( + cbEncoded: *const WEBAUTHN_CREDENTIAL_ATTESTATION, + pbEncoded: *mut u32, + response_bytes: *mut *mut u8 +) -> HRESULT); + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +struct WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST { pub dwVersion: u32, pub pwszRpId: *const u16, // PCWSTR pub cbRpId: u32, @@ -425,15 +1135,19 @@ impl PluginGetAssertionRequest { } } - pub fn credential_list(&self) -> CredentialList { + pub fn allow_credentials(&self) -> CredentialList { self.as_ref().CredentialList } // TODO: Support extensions // pub fn extensions(&self) -> Options {} - pub fn authenticator_options(&self) -> WebAuthnCtapCborAuthenticatorOptions { - unsafe { *self.as_ref().pAuthenticatorOptions } + pub fn authenticator_options(&self) -> Option { + let ptr = self.as_ref().pAuthenticatorOptions; + if ptr.is_null() { + return None; + } + unsafe { Some(*ptr) } } } @@ -749,26 +1463,29 @@ pub struct CredentialList { type WEBAUTHN_CREDENTIAL_LIST = CredentialList; pub struct CredentialListIterator<'a> { - pos: usize, - list: &'a [*const WEBAUTHN_CREDENTIAL_EX], + inner: ArrayPointerIterator<'a, *const WEBAUTHN_CREDENTIAL_EX>, } impl<'a> Iterator for CredentialListIterator<'a> { type Item = &'a WEBAUTHN_CREDENTIAL_EX; fn next(&mut self) -> Option { - let current = self.list.get(self.pos); - self.pos += 1; - current.and_then(|c| unsafe { c.as_ref() }) + let item = self.inner.next()?; + // SAFETY: This type can only be constructed from this library using + // responses from Windows APIs, and we trust that the pointer and length + // of each inner item of the array is valid. + unsafe { item.as_ref() } } } impl CredentialList { pub fn iter(&self) -> CredentialListIterator<'_> { + // SAFETY: This type can only be constructed from this library using + // responses from Windows APIs. The pointer is checked for null safety + // on construction. unsafe { CredentialListIterator { - pos: 0, - list: std::slice::from_raw_parts(self.ppCredentials, self.cCredentials as usize), + inner: ArrayPointerIterator::new(self.ppCredentials, self.cCredentials as usize), } } } diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/util.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/util.rs index 82ad7bdad99..c8d6454c96b 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/util.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/win_webauthn/util.rs @@ -68,3 +68,34 @@ impl WindowsString for str { (ptr as *mut u16, byte_count) } } + +pub struct ArrayPointerIterator<'a, T> { + pos: usize, + list: Option<&'a [T]>, +} + +impl ArrayPointerIterator<'_, T> { + /// Safety constraints: The caller must ensure that the pointer and length is + /// valid. A null pointer returns an empty iterator. + pub unsafe fn new(data: *const T, len: usize) -> Self { + let slice = if !data.is_null() { + Some(std::slice::from_raw_parts(data, len)) + } else { + None + }; + Self { + pos: 0, + list: slice, + } + } +} + +impl<'a, T> Iterator for ArrayPointerIterator<'a, T> { + type Item = &'a T; + + fn next(&mut self) -> Option { + let current = self.list?.get(self.pos); + self.pos += 1; + current + } +}