diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index db47507b793..f4106f3c67a 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -4273,6 +4273,14 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" +[[package]] +name = "win_webauthn" +version = "0.0.0" +dependencies = [ + "windows 0.62.2", + "windows-core 0.62.2", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index f79c0e4abee..64c2ab65b8b 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -10,6 +10,7 @@ members = [ "process_isolation", "proxy", "ssh_agent", + "win_webauthn", "windows_plugin_authenticator", ] diff --git a/apps/desktop/desktop_native/win_webauthn/Cargo.toml b/apps/desktop/desktop_native/win_webauthn/Cargo.toml new file mode 100644 index 00000000000..03d603bd291 --- /dev/null +++ b/apps/desktop/desktop_native/win_webauthn/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "win_webauthn" +version.workspace = true +license.workspace = true +edition.workspace = true +publish.workspace = true + +[target.'cfg(windows)'.dependencies] +windows = { workspace = true, features = [ + "Win32_Foundation", + "Win32_Security", + "Win32_Security_Cryptography", + "Win32_System_Com", + "Win32_System_LibraryLoader", +] } +windows-core = { workspace = true } + +[lints] +workspace = true diff --git a/apps/desktop/desktop_native/win_webauthn/README.md b/apps/desktop/desktop_native/win_webauthn/README.md new file mode 100644 index 00000000000..82dd9a200e8 --- /dev/null +++ b/apps/desktop/desktop_native/win_webauthn/README.md @@ -0,0 +1,28 @@ +Rust wrapper for the Windows WebAuthn Library. + + +Based on [microsoft/webauthn@c3ed95f][webauthn-ref], released in Windows 11 +November 2025 update. + +[webauthn-ref]: https://github.com/microsoft/webauthn/tree/c3ed95fd7603441a0253c55c14e79239cb556a9f + +# Current Limitations + +In this initial version, there are some limitations that need to be addressed: + +- The `ErrorKind` enum is too broad. The use needs to be audited and more variants should be added for specific error cases. +- C structs for use in webauthn.dll functions are defined manually. As of + [microsoft/windows-rs@95dfa93](https://github.com/microsoft/windows-rs/tree/95dfa93ce7a004449d5309b36dda9f2300f57db6), + these are included in the `windows` crate, but there has not been a released + version of that crate since it was added. As soon as it is released, we can + update to it. +- We are not using the `dwVersion` fields of the structs and are instead + hard-coding availability for all the structs as of the Windows 11 passkey plugin + authenticator (November 2025). To make this more generally useful, we should + read these fields and optionally omit known fields that were introduced in a + higher version than `dwVersion` specifies (e.g., by returning an Option for + those fields). +- End-to-end tests for the plugin authenticator handler code would be good to + include. We could consider using a Rust trait to wrap the FFI calls, or to + create a DLL from a Rust cdylib that mocks the required webauthn.dll + functionality and is called from the tests. \ No newline at end of file diff --git a/apps/desktop/desktop_native/win_webauthn/src/api/mod.rs b/apps/desktop/desktop_native/win_webauthn/src/api/mod.rs new file mode 100644 index 00000000000..ba05f2bd07f --- /dev/null +++ b/apps/desktop/desktop_native/win_webauthn/src/api/mod.rs @@ -0,0 +1,2 @@ +//! Safe wrappers around raw types and functions for webauthn.dll. +mod sys; diff --git a/apps/desktop/desktop_native/win_webauthn/src/api/sys/mod.rs b/apps/desktop/desktop_native/win_webauthn/src/api/sys/mod.rs new file mode 100644 index 00000000000..78a551515a5 --- /dev/null +++ b/apps/desktop/desktop_native/win_webauthn/src/api/sys/mod.rs @@ -0,0 +1,222 @@ +//! Raw types for the Windows webauthn.dll library. +//! The top-level crate includes types defined in webauthn.h. +#![allow(non_snake_case)] +#![allow(non_camel_case_types)] + +pub(super) mod plugin; +mod util; + +use std::{num::NonZeroU32, ptr::NonNull}; + +use windows::core::BOOL; + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(super) struct WEBAUTHN_COSE_CREDENTIAL_PARAMETER { + /// Version of this structure, to allow for modifications in the future. + pub(super) dwVersion: u32, + /// Well-known credential type specifying a credential to create. + pub(super) pwszCredentialType: NonNull, + /// Well-known COSE algorithm specifying the algorithm to use for the credential. + pub(super) lAlg: i32, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(super) struct WEBAUTHN_COSE_CREDENTIAL_PARAMETERS { + pub(super) cCredentialParameters: u32, + pub(super) pCredentialParameters: *const WEBAUTHN_COSE_CREDENTIAL_PARAMETER, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(super) struct WEBAUTHN_CREDENTIAL_ATTESTATION { + /// Version of this structure, to allow for modifications in the future. + pub(super) dwVersion: u32, + + /// Attestation format type + pub(super) pwszFormatType: *const u16, // PCWSTR + + /// Size of cbAuthenticatorData. + pub(super) cbAuthenticatorData: u32, + /// Authenticator data that was created for this credential. + //_Field_size_bytes_(cbAuthenticatorData) + pub(super) pbAuthenticatorData: *const u8, + + /// Size of CBOR encoded attestation information + /// 0 => encoded as CBOR null value. + pub(super) cbAttestation: u32, + ///Encoded CBOR attestation information + // _Field_size_bytes_(cbAttestation) + pub(super) pbAttestation: *const u8, + + pub(super) 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(super) pvAttestationDecode: *const u8, + + /// The CBOR encoded Attestation Object to be returned to the RP. + pub(super) cbAttestationObject: u32, + // _Field_size_bytes_(cbAttestationObject) + pub(super) pbAttestationObject: *const u8, + + /// The CredentialId bytes extracted from the Authenticator Data. + /// Used by Edge to return to the RP. + pub(super) cbCredentialId: u32, + // _Field_size_bytes_(cbCredentialId) + pub(super) pbCredentialId: *const u8, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_2 + /// Since VERSION 2 + pub(super) 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. + pub(super) dwUsedTransport: u32, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_4 + pub(super) bEpAtt: BOOL, + pub(super) bLargeBlobSupported: BOOL, + pub(super) bResidentKey: BOOL, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_5 + pub(super) bPrfEnabled: BOOL, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_6 + pub(super) cbUnsignedExtensionOutputs: u32, + // _Field_size_bytes_(cbUnsignedExtensionOutputs) + pub(super) pbUnsignedExtensionOutputs: *const u8, + + // + // Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_7 + pub(super) pHmacSecret: *const WEBAUTHN_HMAC_SECRET_SALT, + + /// ThirdPartyPayment Credential or not. + pub(super) 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. + pub(super) dwTransports: u32, + + /// UTF-8 encoded JSON serialization of the client data. + pub(super) cbClientDataJSON: u32, + // _Field_size_bytes_(cbClientDataJSON) + pub(super) pbClientDataJSON: *const u8, + + /// UTF-8 encoded JSON serialization of the RegistrationResponse. + pub(super) cbRegistrationResponseJSON: u32, + // _Field_size_bytes_(cbRegistrationResponseJSON) + pub(super) pbRegistrationResponseJSON: *const u8, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(super) struct WEBAUTHN_CREDENTIAL_EX { + /// Version of this structure, to allow for modifications in the future. + pub(super) dwVersion: u32, + /// Size of pbID. + pub(super) cbId: u32, + /// Unique ID for this particular credential. + pub(super) pbId: *const u8, + /// Well-known credential type specifying what this particular credential is. + pub(super) pwszCredentialType: *const u16, + /// Transports. 0 implies no transport restrictions. + pub(super) dwTransports: u32, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(super) struct WEBAUTHN_CREDENTIAL_LIST { + pub(super) cCredentials: u32, + pub(super) ppCredentials: *const *const WEBAUTHN_CREDENTIAL_EX, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(super) struct WEBAUTHN_EXTENSION { + pwszExtensionIdentifier: *const u16, + cbExtension: u32, + pvExtension: *mut u8, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(super) struct WEBAUTHN_EXTENSIONS { + pub(super) cExtensions: u32, + // _Field_size_(cExtensions) + pub(super) pExtensions: *const WEBAUTHN_EXTENSION, +} + +#[repr(C)] +pub(super) 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, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(super) struct WEBAUTHN_RP_ENTITY_INFORMATION { + /// Version of this structure, to allow for modifications in the future. + /// This field is required and should be set to CURRENT_VERSION above. + pub(super) dwVersion: u32, + + /// Identifier for the RP. This field is required. + pub(super) pwszId: NonNull, // PCWSTR + + /// Contains the friendly name of the Relying Party, such as "Acme + /// Corporation", "Widgets Inc" or "Awesome Site". + /// + /// This member is deprecated in WebAuthn Level 3 because many clients do not display it, but + /// it remains a required dictionary member for backwards compatibility. Relying + /// Parties MAY, as a safe default, set this equal to the RP ID. + pub(super) pwszName: *const u16, // PCWSTR + + /// Optional URL pointing to RP's logo. + /// + /// This field was removed in WebAuthn Level 2. Keeping this here for proper struct sizing. + #[deprecated] + _pwszIcon: *const u16, // PCWSTR +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(super) struct WEBAUTHN_USER_ENTITY_INFORMATION { + /// Version of this structure, to allow for modifications in the future. + /// This field is required and should be set to CURRENT_VERSION above. + pub(super) dwVersion: u32, + + /// Identifier for the User. This field is required. + pub(super) cbId: NonZeroU32, // DWORD + pub(super) pbId: NonNull, // PBYTE + + /// Contains a detailed name for this account, such as "john.p.smith@example.com". + pub(super) pwszName: NonNull, // PCWSTR + + /// Optional URL that can be used to retrieve an image containing the user's current avatar, + /// or a data URI that contains the image data. + #[deprecated] + pub(super) pwszIcon: Option>, // PCWSTR + + /// Contains the friendly name associated with the user account by the Relying Party, such as + /// "John P. Smith". + pub(super) pwszDisplayName: NonNull, // PCWSTR +} diff --git a/apps/desktop/desktop_native/win_webauthn/src/api/sys/plugin.rs b/apps/desktop/desktop_native/win_webauthn/src/api/sys/plugin.rs new file mode 100644 index 00000000000..0d9544e36fb --- /dev/null +++ b/apps/desktop/desktop_native/win_webauthn/src/api/sys/plugin.rs @@ -0,0 +1,565 @@ +//! Types from webauthn.dll as defined in webauthnplugin.h and pluginauthenticator.h. + +use std::num::NonZeroU32; + +use windows::{ + core::{BOOL, GUID, HRESULT}, + Win32::{Foundation::HWND, Security::Cryptography::BCRYPT_KEY_BLOB}, +}; + +use super::{ + util::webauthn_call, WEBAUTHN_COSE_CREDENTIAL_PARAMETERS, WEBAUTHN_CREDENTIAL_ATTESTATION, + WEBAUTHN_CREDENTIAL_LIST, WEBAUTHN_RP_ENTITY_INFORMATION, WEBAUTHN_USER_ENTITY_INFORMATION, +}; + +/// Plugin lock status enum as defined in the IDL +#[repr(u32)] +#[derive(Debug, Copy, Clone)] +pub enum PLUGIN_LOCK_STATUS { + PluginLocked = 0, + PluginUnlocked = 1, +} + +/// Windows WebAuthn Authenticator Options structure +/// Header File Name: _WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS { + /// Version of this structure, to allow for modifications in the future. + pub(in crate::api) dwVersion: u32, + /// "up" option: +1=TRUE, 0=Not defined, -1=FALSE + pub(in crate::api) lUp: i32, + /// "uv" option: +1=TRUE, 0=Not defined, -1=FALSE + pub(in crate::api) lUv: i32, + /// "rk" option: +1=TRUE, 0=Not defined, -1=FALSE + pub(in crate::api) lRequireResidentKey: i32, +} + +#[repr(C)] +pub(in crate::api) struct WEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY { + /// Version of this structure, to allow for modifications in the future. + pub(in crate::api) _dwVersion: u32, + + /// Key type + pub(in crate::api) _lKty: i32, + + /// Hash Algorithm: ES256, ES384, ES512 + pub(in crate::api) _lAlg: i32, + + /// Curve + pub(in crate::api) _lCrv: i32, + + /// Size of "x" (X Coordinate) + pub(in crate::api) _cbX: u32, + + /// "x" (X Coordinate) data. Big Endian. + pub(in crate::api) _pbX: *const u8, + + /// Size of "y" (Y Coordinate) + pub(in crate::api) _cbY: u32, + + /// "y" (Y Coordinate) data. Big Endian. + pub(in crate::api) _pbY: *const u8, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(in crate::api) struct WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST { + /// Version of this structure, to allow for modifications in the future. + pub(in crate::api) dwVersion: u32, + /// RP ID (after UTF-8 to Unicode conversion) + pub(in crate::api) pwszRpId: *const u16, + /// Input RP ID size (raw UTF-8 bytes before conversion) + pub(in crate::api) cbRpId: u32, + /// Raw UTF-8 bytes before conversion to UTF-16 in pwszRpId. These are the + /// bytes to be hashed in the Authenticator Data. + pub(in crate::api) pbRpId: *const u8, + /// Client Data Hash size + pub(in crate::api) cbClientDataHash: u32, + /// Client Data Hash data + pub(in crate::api) pbClientDataHash: *const u8, + /// Credentials used for inclusion + pub(in crate::api) CredentialList: WEBAUTHN_CREDENTIAL_LIST, + /// CBOR extensions map size + pub(in crate::api) cbCborExtensionsMap: u32, + /// CBOR extensions map data + pub(in crate::api) pbCborExtensionsMap: *const u8, + /// Authenticator Options (Optional) + pub(in crate::api) pAuthenticatorOptions: *const WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS, + + // Pin Auth (Optional) + /// Zero length PinAuth is included in the request + pub(in crate::api) fEmptyPinAuth: BOOL, + /// Pin Auth size + pub(in crate::api) cbPinAuth: u32, + /// Pin Auth data + pub(in crate::api) pbPinAuth: *const u8, + + /// HMAC Salt Extension (Optional) + pub(in crate::api) pHmacSaltExtension: *const WEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION, + + /// PRF Extension / HMAC secret salt values size + pub(in crate::api) cbHmacSecretSaltValues: u32, + /// PRF Extension / HMAC secret salt values data + pub(in crate::api) pbHmacSecretSaltValues: *const u8, + + /// Pin protocol + pub(in crate::api) dwPinProtocol: u32, + + /// "credBlob": true extension + pub(in crate::api) lCredBlobExt: i32, + + /// "largeBlobKey": true extension + pub(in crate::api) lLargeBlobKeyExt: i32, + + /// "largeBlob" extension operation + pub(in crate::api) dwCredLargeBlobOperation: u32, + /// Large blob compressed size + pub(in crate::api) cbCredLargeBlobCompressed: u32, + /// Large blob compressed data + pub(in crate::api) pbCredLargeBlobCompressed: *const u8, + /// Large blob original size + pub(in crate::api) dwCredLargeBlobOriginalSize: u32, + + /// "json" extension size. Nonzero if present + pub(in crate::api) cbJsonExt: u32, + /// "json" extension data + pub(in crate::api) pbJsonExt: *const u8, +} + +#[repr(C)] +pub(in crate::api) struct WEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION { + /// Version of this structure, to allow for modifications in the future. + pub(in crate::api) _dwVersion: u32, + + /// Platform's key agreement public key + pub(in crate::api) _pKeyAgreement: *const WEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY, + + /// Encrypted salt size + pub(in crate::api) _cbEncryptedSalt: u32, + /// Encrypted salt data + pub(in crate::api) _pbEncryptedSalt: *const u8, + + /// Salt authentication size + pub(in crate::api) _cbSaltAuth: u32, + /// Salt authentication data + pub(in crate::api) _pbSaltAuth: *const u8, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(in crate::api) struct WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST<'a> { + /// Version of this structure, to allow for modifications in the future. + pub(in crate::api) dwVersion: u32, + /// Input RP ID size (raw UTF-8 bytes before conversion) + pub(in crate::api) cbRpId: u32, + /// Input RP ID data (bytes hashed in Authenticator Data) + pub(in crate::api) pbRpId: *const u8, + /// Client Data Hash size + pub(in crate::api) cbClientDataHash: u32, + /// Client Data Hash data + pub(in crate::api) pbClientDataHash: *const u8, + /// RP Information + pub(in crate::api) pRpInformation: *const WEBAUTHN_RP_ENTITY_INFORMATION, + /// User Information + pub(in crate::api) pUserInformation: *const WEBAUTHN_USER_ENTITY_INFORMATION, + /// Crypto Parameters + pub(in crate::api) WebAuthNCredentialParameters: WEBAUTHN_COSE_CREDENTIAL_PARAMETERS, + /// Credentials used for exclusion + pub(in crate::api) CredentialList: WEBAUTHN_CREDENTIAL_LIST, + /// CBOR extensions map size + pub(in crate::api) cbCborExtensionsMap: u32, + /// CBOR extensions map data + pub(in crate::api) pbCborExtensionsMap: *const u8, + /// Authenticator Options (Optional) + pub(in crate::api) pAuthenticatorOptions: Option<&'a WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS>, + + // Pin Auth (Optional) + /// Indicates zero length PinAuth is included in the request + pub(in crate::api) fEmptyPinAuth: BOOL, + /// Pin Auth size + pub(in crate::api) cbPinAuth: u32, + /// Pin Auth data + pub(in crate::api) pbPinAuth: *const u8, + + /// "hmac-secret": true extension + pub(in crate::api) lHmacSecretExt: i32, + + /// "hmac-secret-mc" extension + pub(in crate::api) pHmacSecretMcExtension: *const WEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION, + + /// "prf" extension + pub(in crate::api) lPrfExt: i32, + /// HMAC secret salt values size + pub(in crate::api) cbHmacSecretSaltValues: u32, + /// HMAC secret salt values data + pub(in crate::api) pbHmacSecretSaltValues: *const u8, + + /// "credProtect" extension. Nonzero if present + pub(in crate::api) dwCredProtect: Option, + + /// Nonzero if present + pub(in crate::api) dwPinProtocol: Option, + + /// Nonzero if present + pub(in crate::api) dwEnterpriseAttestation: Option, + + /// "credBlob" extension. Nonzero if present + pub(in crate::api) cbCredBlobExt: Option, + /// "credBlob" extension data + pub(in crate::api) pbCredBlobExt: *const u8, + + /// "largeBlobKey": true extension + pub(in crate::api) lLargeBlobKeyExt: i32, + + /// "largeBlob": extension + pub(in crate::api) dwLargeBlobSupport: u32, + + /// "minPinLength": true extension + pub(in crate::api) lMinPinLengthExt: i32, + + /// "json" extension. Nonzero if present + pub(in crate::api) cbJsonExt: u32, + /// "json" extension data + pub(in crate::api) pbJsonExt: *const u8, +} + +/// Used when adding a Windows plugin authenticator (stable API). +/// Header File Name: _WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS +/// Header File Usage: WebAuthNPluginAddAuthenticator() +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(in crate::api) struct WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS { + /// Authenticator Name + pub(in crate::api) pwszAuthenticatorName: *const u16, + + /// Plugin COM ClsId + pub(in crate::api) rclsid: *const GUID, + + /// Plugin RPID + /// + /// Required for a nested WebAuthN call originating from a plugin. + pub(in crate::api) pwszPluginRpId: *const u16, + + /// Plugin Authenticator Logo for the Light themes. base64-encoded SVG 1.1 + /// + /// The data should be encoded as `UTF16(BASE64(UTF8(svg_text)))`. + pub(in crate::api) pwszLightThemeLogoSvg: *const u16, + + /// Plugin Authenticator Logo for the Dark themes. base64-encoded SVG 1.1 + /// + /// The data should be encoded as `UTF16(BASE64(UTF8(svg_text)))`. + pub(in crate::api) pwszDarkThemeLogoSvg: *const u16, + + /// CTAP CBOR-encoded authenticatorGetInfo response (size) + pub(in crate::api) cbAuthenticatorInfo: u32, + /// CTAP CBOR-encoded authenticatorGetInfo output + pub(in crate::api) pbAuthenticatorInfo: *const u8, + + /// Count of supported RP IDs + pub(in crate::api) cSupportedRpIds: u32, + /// List of supported RP IDs (Relying Party IDs). + /// + /// Should be null if all RPs are supported. + pub(in crate::api) pbSupportedRpIds: *const *const u16, +} + +/// Used as a response type when adding a Windows plugin authenticator. +/// Header File Name: _WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE +/// Header File Usage: WebAuthNPluginAddAuthenticator() +/// WebAuthNPluginFreeAddAuthenticatorResponse() +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(in crate::api) struct WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE { + /// Size in bytes of the public key pointed to by `pbOpSignPubKey`. + pub(in crate::api) cbOpSignPubKey: u32, + /// Pointer to a [BCRYPT_KEY_BLOB](windows::Win32::Security::Cryptography::BCRYPT_KEY_BLOB). + pub(in crate::api) pbOpSignPubKey: *mut u8, +} + +#[repr(C)] +pub(in crate::api) struct WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST { + pub(in crate::api) transactionId: GUID, + pub(in crate::api) cbRequestSignature: u32, + pub(in crate::api) pbRequestSignature: *const u8, +} + +/// Represents a credential. +/// Header File Name: _WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS +/// Header File Usage: WebAuthNPluginAuthenticatorAddCredentials, etc. +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(in crate::api) struct WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS { + /// Credential Identifier bytes (size) + pub(in crate::api) credential_id_byte_count: u32, + /// Credential Identifier bytes (data, required) + pub(in crate::api) credential_id_pointer: *const u8, + /// Identifier for the RP (required) + pub(in crate::api) rpid: *const u16, + /// Friendly name of the Relying Party (required) + pub(in crate::api) rp_friendly_name: *const u16, + /// User Identifier bytes (size) + pub(in crate::api) user_id_byte_count: u32, + /// User Identifier bytes (data, required) + pub(in crate::api) user_id_pointer: *const u8, + /// Detailed account name (e.g., "john.p.smith@example.com") + pub(in crate::api) user_name: *const u16, + /// Friendly name for the user account (e.g., "John P. Smith") + pub(in crate::api) user_display_name: *const u16, +} + +/// Used when creating and asserting credentials. +/// Header File Name: _WEBAUTHN_PLUGIN_OPERATION_REQUEST +/// Header File Usage: MakeCredential() +/// GetAssertion() +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(in crate::api) struct WEBAUTHN_PLUGIN_OPERATION_REQUEST { + /// Window handle to client that requesting a WebAuthn credential. + pub(in crate::api) hWnd: HWND, + pub(in crate::api) transactionId: GUID, + pub(in crate::api) cbRequestSignature: u32, + /// Signature over request made with the signing key created during authenticator registration. + pub(in crate::api) pbRequestSignature: *mut u8, + pub(in crate::api) requestType: WEBAUTHN_PLUGIN_REQUEST_TYPE, + pub(in crate::api) cbEncodedRequest: u32, + pub(in crate::api) pbEncodedRequest: *const u8, +} + +/// Plugin request type enum as defined in the IDL +#[repr(u32)] +#[derive(Debug, Copy, Clone)] +pub(in crate::api) enum WEBAUTHN_PLUGIN_REQUEST_TYPE { + // This is being used to check the value that Windows gives us, but it isn't + // ever constructed by our library. + #[allow(unused)] + CTAP2_CBOR = 0x01, +} + +/// Used as a response when creating and asserting credentials. +/// Header File Name: _WEBAUTHN_PLUGIN_OPERATION_RESPONSE +/// Header File Usage: MakeCredential() +/// GetAssertion() +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub(in crate::api) struct WEBAUTHN_PLUGIN_OPERATION_RESPONSE { + pub(in crate::api) cbEncodedResponse: u32, + pub(in crate::api) pbEncodedResponse: *mut u8, +} + +#[repr(C)] +#[derive(Debug)] +pub(in crate::api) struct WEBAUTHN_PLUGIN_USER_VERIFICATION_REQUEST { + /// Windows handle of the top-level window displayed by the plugin and + /// currently is in foreground as part of the ongoing WebAuthn operation. + pub(in crate::api) hwnd: HWND, + + /// The WebAuthn transaction id from the WEBAUTHN_PLUGIN_OPERATION_REQUEST + pub(in crate::api) rguidTransactionId: *const GUID, + + /// The username attached to the credential that is in use for this WebAuthn + /// operation. + pub(in crate::api) pwszUsername: *const u16, + + /// A text hint displayed on the Windows Hello prompt. + pub(in crate::api) pwszDisplayHint: *const u16, +} + +webauthn_call!("WebAuthNDecodeGetAssertionRequest" as +/// Decodes a CTAP GetAssertion request. +/// +/// On success, a [WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST] will be written to +/// `ppGetAssertionRequest`, which must be freed by a call to +/// [webauthn_free_decoded_get_assertion_request]. +/// +/// # Arguments +/// - `pbEncoded`: a COM-allocated buffer pointing to a CTAP CBOR get assertion request. +/// - `ppGetAssertionRequest`: An indirect pointer to a [WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST]. +/// +/// # Safety +/// - `pbEncoded` must have been allocated by Windows COM. +/// - `pbEncoded` must be non-null and have the length specified in cbEncoded. +fn webauthn_decode_get_assertion_request( + cbEncoded: u32, + pbEncoded: *const u8, + ppGetAssertionRequest: *mut *mut WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST +) -> HRESULT); + +webauthn_call!("WebAuthNDecodeMakeCredentialRequest" as +/// Decodes a CTAP CBOR `authenticatorMakeCredential` request. +/// +/// On success, a [WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST] will be written to +/// `ppMakeCredentialRequest`, which must be freed by a call to +/// [webauthn_free_decoded_make_credential_request]. +/// +/// # Arguments +/// - `pbEncoded`: a COM-allocated buffer pointing to a CTAP CBOR make credential request. +/// - `ppMakeCredentialRequest`: An indirect pointer to a [WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST]. +/// +/// # Safety +/// - `pbEncoded` must have been allocated by Windows COM. +/// - `pbEncoded` must be non-null and have the length specified in cbEncoded. +fn webauthn_decode_make_credential_request( + cbEncoded: u32, + pbEncoded: *const u8, + ppMakeCredentialRequest: *mut *mut WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST +) -> HRESULT); + +webauthn_call!("WebAuthNEncodeMakeCredentialResponse" as +/// Encode a credential attestation response to a COM-allocated byte buffer +/// containing a CTAP CBOR `authenticatorMakeCredential` response structure. +/// +/// Returns [S_OK](windows::Win32::Foundation::S_OK) on success. +/// +/// # Arguments +/// - `pCredentialAttestation`: A pointer to [WEBAUTHN_CREDENTIAL_ATTESTATION] to encode. +/// - `pcbResp`: A pointer to a u32, which will be filled with the length of the response buffer. +/// - `ppbResponse`: An indirect pointer to a byte buffer, which will be written to on succces. +fn webauthn_encode_make_credential_response( + pCredentialAttestation: *const WEBAUTHN_CREDENTIAL_ATTESTATION, + pcbResp: *mut u32, + ppbResponse: *mut *mut u8 +) -> HRESULT); + +webauthn_call!("WebAuthNFreeDecodedGetAssertionRequest" as +/// Frees a decoded get assertion request from [webauthn_free_decoded_get_assertion_request]. +/// +/// # Arguments +/// - `pGetAssertionRequest`: An pointer to a [WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST] to be freed. +fn webauthn_free_decoded_get_assertion_request( + pGetAssertionRequest: *mut WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST +) -> ()); + +webauthn_call!("WebAuthNFreeDecodedMakeCredentialRequest" as +/// Frees a decoded make credential request from [webauthn_free_decoded_make_credential_request]. +/// +/// # Arguments +/// - `pMakeCredentialRequest`: An pointer to a [WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST] to be freed. +fn webauthn_free_decoded_make_credential_request( + pMakeCredentialRequest: *mut WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST +) -> ()); + +webauthn_call!("WebAuthNPluginAddAuthenticator" as +/// Register authenticator info for a plugin COM server. +/// +/// Returns [S_OK](windows::Win32::Foundation::S_OK) on success. +/// +/// # Arguments +/// - `pPluginAddAuthenticatorOptions`: Details about the authenticator to set. +/// - `ppPluginAddAuthenticatorResponse`: +/// An indirect pointer to a [WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE], which will be written to on success. +/// If the request succeeds, the data must be freed by a call to [webauthn_plugin_free_add_authenticator_response]. +fn webauthn_plugin_add_authenticator( + pPluginAddAuthenticatorOptions: *const WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS, + ppPluginAddAuthenticatorResponse: *mut *mut WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE +) -> HRESULT); + +webauthn_call!("WebAuthNPluginAuthenticatorAddCredentials" as +/// Add metadata for a list of WebAuthn credentials to the autofill store for +/// this plugin authenticator. +/// +/// This will make the credentials available for discovery in Windows Hello +/// WebAuthn autofill dialogs. +/// +/// Returns [S_OK](windows::Win32::Foundation::S_OK) on success. +/// +/// # Arguments +/// - `rclsid`: The CLSID corresponding to this plugin's COM server. +/// - `cCredentialDetails`: The number of credentials in the array pointed to by `pCredentialDetails`. +/// - `pCredentialDetails`: An array of credential metadata. +fn webauthn_plugin_authenticator_add_credentials( + rclsid: *const GUID, + cCredentialDetails: u32, + pCredentialDetails: *const WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS +) -> HRESULT); + +webauthn_call!("WebAuthNPluginAuthenticatorRemoveAllCredentials" as +/// Removes metadata for all credentials currently stored in the autofill store +/// for this plugin authenticator. +/// +/// Returns [S_OK](windows::Win32::Foundation::S_OK) on success. +/// +/// # Arguments +/// - `rclsid`: The CLSID corresponding to this plugin's COM server. +fn webauthn_plugin_authenticator_remove_all_credentials(rclsid: *const GUID) -> HRESULT); + +webauthn_call!("WebAuthNPluginFreeAddAuthenticatorResponse" as +/// Free memory from a [WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE]. +/// +/// # Arguments +/// - `pPluginAddAuthenticatorResponse`: An pointer to a [WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE] to be freed. +fn webauthn_plugin_free_add_authenticator_response( + pPluginAddAuthenticatorResponse: *mut WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE +) -> ()); + +webauthn_call!("WebAuthNPluginFreeUserVerificationResponse" as +/// Free a user verification response received from a call to [webauthn_plugin_perform_user_verification]. +fn webauthn_plugin_free_user_verification_response( + pbResponse: *mut u8 +) -> ()); + +webauthn_call!("WebAuthNPluginPerformUserVerification" as +/// Request user verification for a WebAuthn operation. +/// +/// The OS will prompt the user for verification, and if the user is +/// successfully verified, will write a signature to `ppbResponse`, which must +/// be freed by a call to [webauthn_plugin_free_user_verification_response]. +/// +/// The signature is over the SHA-256 hash of the original WebAuthn operation request buffer +/// corresponding to `pPluginUserVerification.rguidTransactionId`. It can be +/// verified using the user verification public key, which can be retrieved +/// using +/// [webauthn_plugin_get_user_verification_public_key][crate::plugin::crypto::webauthn_plugin_get_user_verification_public_key]. +/// +/// This request will block while the user interacts with the dialog. +/// +/// # Arguments +/// - `pPluginUserVerification`: The user verification prompt and transaction context for the request. +/// - `pcbResponse`: Length in bytes of the signature. +/// - `ppbResponse`: The signature of the request. +fn webauthn_plugin_perform_user_verification( + pPluginUserVerification: *const WEBAUTHN_PLUGIN_USER_VERIFICATION_REQUEST, + pcbResponse: *mut u32, + ppbResponse: *mut *mut u8 +) -> HRESULT); + +webauthn_call!("WebAuthNPluginGetUserVerificationPublicKey" as +/// Retrieve the public key used to verify user verification responses from the OS. +/// +/// Returns [S_OK](windows::Win32::Foundation::S_OK) on success. +/// +/// # Arguments +/// - `rclsid`: The CLSID corresponding to this plugin's COM server. +/// - `pcbPublicKey`: A pointer to an unsigned integer, which will be filled in with the length of the buffer at `ppbPublicKey`. +/// - `ppbPublicKey`: A pointer to a [BCRYPT_PUBLIC_KEY_BLOB], which will be written to on success. +/// On success, this must be freed by a call to [webauthn_plugin_free_public_key_response]. +fn webauthn_plugin_get_user_verification_public_key( + rclsid: *const GUID, + pcbPublicKey: *mut u32, + ppbPublicKey: *mut *mut BCRYPT_KEY_BLOB, +) -> HRESULT); // Free using WebAuthNPluginFreePublicKeyResponse + +webauthn_call!("WebAuthNPluginGetOperationSigningPublicKey" as +/// Retrieve the public key used to verify plugin operation requests from the OS. +/// +/// Returns [S_OK](windows::Win32::Foundation::S_OK) on success. +/// +/// # Arguments +/// - `rclsid`: The CLSID corresponding to this plugin's COM server. +/// - `pcbOpSignPubKey`: A pointer to an unsigned integer, which will be filled in with the length of the buffer at `ppbOpSignPubKey`. +/// - `ppbOpSignPubKey`: An indirect pointer to a [BCRYPT_PUBLIC_KEY_BLOB], which will be written to on success. +/// On success, this must be freed by a call to [webauthn_plugin_free_public_key_response]. +fn webauthn_plugin_get_operation_signing_public_key( + rclsid: *const GUID, + pcbOpSignPubKey: *mut u32, + ppbOpSignPubKey: *mut *mut BCRYPT_KEY_BLOB +) -> HRESULT); // Free using WebAuthNPluginFreePublicKeyResponse + +webauthn_call!("WebAuthNPluginFreePublicKeyResponse" as +/// Free public key memory retrieved from the OS. +/// +/// # Arguments +/// - `pbOpSignPubKey`: A pointer to a [BCRYPT_KEY_BLOB] retrieved from a method in this library. +fn webauthn_plugin_free_public_key_response( + pbOpSignPubKey: *mut BCRYPT_KEY_BLOB +) -> ()); diff --git a/apps/desktop/desktop_native/win_webauthn/src/api/sys/util.rs b/apps/desktop/desktop_native/win_webauthn/src/api/sys/util.rs new file mode 100644 index 00000000000..edd60e02612 --- /dev/null +++ b/apps/desktop/desktop_native/win_webauthn/src/api/sys/util.rs @@ -0,0 +1,94 @@ +use std::sync::OnceLock; + +use windows::{ + core::s, + Win32::{ + Foundation::{FreeLibrary, HMODULE}, + System::LibraryLoader::{ + GetModuleHandleExA, LoadLibraryExA, GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, + GET_MODULE_HANDLE_EX_FLAG_PIN, LOAD_LIBRARY_SEARCH_SYSTEM32, + }, + }, +}; +use windows_core::{PCSTR, PCWSTR}; + +use crate::{ErrorKind, WinWebAuthnError}; + +struct SafeModule(HMODULE); +impl SafeModule { + unsafe fn new(mut module: HMODULE) -> windows::core::Result { + unsafe { + // Pin the module so that it cannot be unloaded. + GetModuleHandleExA( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN, + PCSTR::from_raw(module.0.cast()), + &mut module, + )?; + } + Ok(Self(module)) + } +} +static WEBAUTHN_LIB: OnceLock> = OnceLock::new(); + +unsafe impl Send for SafeModule {} +unsafe impl Sync for SafeModule {} + +/// Defines a Rust function to call a webauthn.dll function over FFI based on +/// the name of the function. Documentation comments will be captured, and the +/// return type will be wrapped in a WinWebAuthnError that will be returned if +/// the function cannot be loaded from webauthn.dll. +/// +/// # Examples +/// +/// ```ignore +/// use crate::api::sys::util::webauthn_call; +/// +/// webauthn_call!("WebAuthNFreeDecodedMakeCredentialRequest" as +/// /// Frees a decoded make credential request from [webauthn_free_decoded_make_credential_request]. +/// /// +/// /// # Arguments +/// /// - `pMakeCredentialRequest`: An pointer to a [WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST] to be freed. +/// fn webauthn_free_decoded_make_credential_request( +/// pMakeCredentialRequest: *mut WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST +/// ) -> ()); +/// ``` +macro_rules! webauthn_call { + ($symbol:literal as $(#[$attr:meta])* fn $fn_name:ident($($arg:ident: $arg_type:ty),+ $(,)?) -> $result_type:ty) => ( + $(#[$attr])* + pub(in crate::api) unsafe fn $fn_name($($arg: $arg_type),*) -> Result<$result_type, crate::WinWebAuthnError> { + let library = crate::api::sys::util::load_webauthn_lib()?; + let response = unsafe { + let address = windows::Win32::System::LibraryLoader::GetProcAddress(*library, windows::core::s!($symbol)).ok_or( + crate::WinWebAuthnError::new( + crate::ErrorKind::DllLoad, + &format!( + "Failed to load function {}", + $symbol + ), + ), + )?; + + let function: unsafe extern "C" fn( + $($arg: $arg_type),* + ) -> $result_type = std::mem::transmute_copy(&address); + function($($arg),*) + }; + Ok(response) + } + ) +} + +pub(super) use webauthn_call; + +pub(super) fn load_webauthn_lib() -> Result<&'static HMODULE, WinWebAuthnError> { + WEBAUTHN_LIB + .get_or_init(|| unsafe { + LoadLibraryExA(s!("webauthn.dll"), None, LOAD_LIBRARY_SEARCH_SYSTEM32) + .and_then(|library| SafeModule::new(library)) + }) + .as_ref() + .map(|module| &module.0) + .map_err(|err| { + WinWebAuthnError::with_cause(ErrorKind::DllLoad, "Failed to load webauthn.dll", err) + }) +} diff --git a/apps/desktop/desktop_native/win_webauthn/src/lib.rs b/apps/desktop/desktop_native/win_webauthn/src/lib.rs new file mode 100644 index 00000000000..96183fffbf3 --- /dev/null +++ b/apps/desktop/desktop_native/win_webauthn/src/lib.rs @@ -0,0 +1,85 @@ +//! A Rust wrapper around the webauthn.dll API. +//! +//! The root module contains common types for WebAuthn clients and plugins. +//! +//! The [plugin] module has types useful for implementing a Windows passkey +//! plugin authenticator. +#![cfg(target_os = "windows")] +// TODO: Temporarily allow unused code while scaffolding. Remove once PR set is finished. +#![expect(unused)] + +#[allow(unsafe_code)] +pub(crate) mod api; + +use std::{error::Error, fmt::Display}; + +/// Errors that may be returned when interacting with this library. +#[derive(Debug)] +pub struct WinWebAuthnError { + kind: ErrorKind, + description: Option, + cause: Option>, +} + +impl WinWebAuthnError { + pub(crate) fn new(kind: ErrorKind, description: &str) -> Self { + Self { + kind, + description: Some(description.to_string()), + cause: None, + } + } + + pub(crate) fn with_cause( + kind: ErrorKind, + description: &str, + cause: E, + ) -> Self { + let cause: Box = Box::new(cause); + Self { + kind, + description: Some(description.to_string()), + cause: Some(cause), + } + } +} + +#[derive(Debug)] +enum ErrorKind { + /// There was an error loading the webauthn.dll library. + DllLoad, + + /// There was an error parsing or serializing data. + Serialization, + + /// An invalid argument was passed. + InvalidArguments, + + /// An unknown error occurred. + Other, + + /// An internal library error occurred. + WindowsInternal, +} + +impl Display for WinWebAuthnError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let msg = match self.kind { + ErrorKind::Serialization => "Failed to serialize data", + ErrorKind::DllLoad => "Failed to load function from DLL", + ErrorKind::InvalidArguments => "Invalid arguments passed to function", + ErrorKind::Other => "An error occurred", + ErrorKind::WindowsInternal => "A Windows error occurred", + }; + f.write_str(msg)?; + if let Some(d) = &self.description { + write!(f, ": {d}")?; + } + if let Some(e) = &self.cause { + write!(f, ". Caused by: {e}")?; + } + Ok(()) + } +} + +impl Error for WinWebAuthnError {}