1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-30 16:23:53 +00:00

Add win_webauthn lib

This commit is contained in:
Isaiah Inuwa
2025-12-18 12:54:56 -06:00
parent 5927f7f278
commit afdd38e838
14 changed files with 5522 additions and 5 deletions

View File

@@ -603,6 +603,33 @@ dependencies = [
"windows",
]
[[package]]
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
name = "ciborium-ll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
]
[[package]]
name = "cipher"
version = "0.4.4"
@@ -724,6 +751,12 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-bigint"
version = "0.5.5"
@@ -1410,6 +1443,17 @@ dependencies = [
"subtle",
]
[[package]]
name = "half"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
"zerocopy",
]
[[package]]
name = "hashbrown"
version = "0.15.3"
@@ -1671,7 +1715,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.53.3",
]
[[package]]
@@ -3840,6 +3884,18 @@ 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 = [
"base64",
"ciborium",
"hex",
"tracing",
"windows",
"windows-core",
]
[[package]]
name = "winapi"
version = "0.3.9"
@@ -4466,18 +4522,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.25"
version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.25"
version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -9,6 +9,7 @@ members = [
"napi",
"process_isolation",
"proxy",
"win_webauthn",
"windows_plugin_authenticator"
]
@@ -30,6 +31,7 @@ byteorder = "=1.5.0"
bytes = "=1.11.0"
cbc = "=0.1.2"
chacha20poly1305 = "=0.10.1"
ciborium = "=0.2.2"
core-foundation = "=0.10.1"
ctor = "=0.5.0"
dirs = "=6.0.0"

View File

@@ -0,0 +1,25 @@
[package]
name = "win_webauthn"
version.workspace = true
license.workspace = true
edition.workspace = true
publish.workspace = true
[dev-dependencies]
hex = { workspace = true }
[lints]
workspace = true
[target.'cfg(windows)'.dependencies]
base64 = { workspace = true }
ciborium = { workspace = true }
tracing = { workspace = true }
windows = { workspace = true, features = [
"Win32_Foundation",
"Win32_Security",
"Win32_Security_Cryptography",
"Win32_System_Com",
"Win32_System_LibraryLoader",
] }
windows-core = { workspace = true }

View File

@@ -0,0 +1,263 @@
/* this ALWAYS GENERATED file contains the definitions for the interfaces */
/* File created by MIDL compiler version 8.01.0628 */
/* @@MIDL_FILE_HEADING( ) */
/* verify that the <rpcndr.h> version is high enough to compile this file*/
#ifndef __REQUIRED_RPCNDR_H_VERSION__
#define __REQUIRED_RPCNDR_H_VERSION__ 501
#endif
/* verify that the <rpcsal.h> version is high enough to compile this file*/
#ifndef __REQUIRED_RPCSAL_H_VERSION__
#define __REQUIRED_RPCSAL_H_VERSION__ 100
#endif
#include "rpc.h"
#include "rpcndr.h"
#ifndef __RPCNDR_H_VERSION__
#error this stub requires an updated version of <rpcndr.h>
#endif /* __RPCNDR_H_VERSION__ */
#ifndef COM_NO_WINDOWS_H
#include "windows.h"
#include "ole2.h"
#endif /*COM_NO_WINDOWS_H*/
#ifndef __pluginauthenticator_h__
#define __pluginauthenticator_h__
#if defined(_MSC_VER) && (_MSC_VER >= 1020)
#pragma once
#endif
#ifndef DECLSPEC_XFGVIRT
#if defined(_CONTROL_FLOW_GUARD_XFG)
#define DECLSPEC_XFGVIRT(base, func) __declspec(xfg_virtual(base, func))
#else
#define DECLSPEC_XFGVIRT(base, func)
#endif
#endif
/* Forward Declarations */
#ifndef __IPluginAuthenticator_FWD_DEFINED__
#define __IPluginAuthenticator_FWD_DEFINED__
typedef interface IPluginAuthenticator IPluginAuthenticator;
#endif /* __IPluginAuthenticator_FWD_DEFINED__ */
/* header files for imported files */
#include "oaidl.h"
#ifdef __cplusplus
extern "C"{
#endif
/* interface __MIDL_itf_pluginauthenticator_0000_0000 */
/* [local] */
typedef
enum _WEBAUTHN_PLUGIN_REQUEST_TYPE
{
WEBAUTHN_PLUGIN_REQUEST_TYPE_CTAP2_CBOR = 0x1
} WEBAUTHN_PLUGIN_REQUEST_TYPE;
typedef struct _WEBAUTHN_PLUGIN_OPERATION_REQUEST
{
HWND hWnd;
GUID transactionId;
DWORD cbRequestSignature;
/* [size_is] */ byte *pbRequestSignature;
WEBAUTHN_PLUGIN_REQUEST_TYPE requestType;
DWORD cbEncodedRequest;
/* [size_is] */ byte *pbEncodedRequest;
} WEBAUTHN_PLUGIN_OPERATION_REQUEST;
typedef struct _WEBAUTHN_PLUGIN_OPERATION_REQUEST *PWEBAUTHN_PLUGIN_OPERATION_REQUEST;
typedef const WEBAUTHN_PLUGIN_OPERATION_REQUEST *PCWEBAUTHN_PLUGIN_OPERATION_REQUEST;
typedef struct _WEBAUTHN_PLUGIN_OPERATION_RESPONSE
{
DWORD cbEncodedResponse;
/* [size_is] */ byte *pbEncodedResponse;
} WEBAUTHN_PLUGIN_OPERATION_RESPONSE;
typedef struct _WEBAUTHN_PLUGIN_OPERATION_RESPONSE *PWEBAUTHN_PLUGIN_OPERATION_RESPONSE;
typedef const WEBAUTHN_PLUGIN_OPERATION_RESPONSE *PCWEBAUTHN_PLUGIN_OPERATION_RESPONSE;
typedef struct _WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST
{
GUID transactionId;
DWORD cbRequestSignature;
/* [size_is] */ byte *pbRequestSignature;
} WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
typedef struct _WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *PWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
typedef const WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
typedef
enum _PLUGIN_LOCK_STATUS
{
PluginLocked = 0,
PluginUnlocked = ( PluginLocked + 1 )
} PLUGIN_LOCK_STATUS;
extern RPC_IF_HANDLE __MIDL_itf_pluginauthenticator_0000_0000_v0_0_c_ifspec;
extern RPC_IF_HANDLE __MIDL_itf_pluginauthenticator_0000_0000_v0_0_s_ifspec;
#ifndef __IPluginAuthenticator_INTERFACE_DEFINED__
#define __IPluginAuthenticator_INTERFACE_DEFINED__
/* interface IPluginAuthenticator */
/* [ref][version][uuid][object] */
EXTERN_C const IID IID_IPluginAuthenticator;
#if defined(__cplusplus) && !defined(CINTERFACE)
MIDL_INTERFACE("d26bcf6f-b54c-43ff-9f06-d5bf148625f7")
IPluginAuthenticator : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE MakeCredential(
/* [in] */ __RPC__in PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
/* [retval][out] */ __RPC__out PWEBAUTHN_PLUGIN_OPERATION_RESPONSE response) = 0;
virtual HRESULT STDMETHODCALLTYPE GetAssertion(
/* [in] */ __RPC__in PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
/* [retval][out] */ __RPC__out PWEBAUTHN_PLUGIN_OPERATION_RESPONSE response) = 0;
virtual HRESULT STDMETHODCALLTYPE CancelOperation(
/* [in] */ __RPC__in PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request) = 0;
virtual HRESULT STDMETHODCALLTYPE GetLockStatus(
/* [retval][out] */ __RPC__out PLUGIN_LOCK_STATUS *lockStatus) = 0;
};
#else /* C style interface */
typedef struct IPluginAuthenticatorVtbl
{
BEGIN_INTERFACE
DECLSPEC_XFGVIRT(IUnknown, QueryInterface)
HRESULT ( STDMETHODCALLTYPE *QueryInterface )(
__RPC__in IPluginAuthenticator * This,
/* [in] */ __RPC__in REFIID riid,
/* [annotation][iid_is][out] */
_COM_Outptr_ void **ppvObject);
DECLSPEC_XFGVIRT(IUnknown, AddRef)
ULONG ( STDMETHODCALLTYPE *AddRef )(
__RPC__in IPluginAuthenticator * This);
DECLSPEC_XFGVIRT(IUnknown, Release)
ULONG ( STDMETHODCALLTYPE *Release )(
__RPC__in IPluginAuthenticator * This);
DECLSPEC_XFGVIRT(IPluginAuthenticator, MakeCredential)
HRESULT ( STDMETHODCALLTYPE *MakeCredential )(
__RPC__in IPluginAuthenticator * This,
/* [in] */ __RPC__in PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
/* [retval][out] */ __RPC__out PWEBAUTHN_PLUGIN_OPERATION_RESPONSE response);
DECLSPEC_XFGVIRT(IPluginAuthenticator, GetAssertion)
HRESULT ( STDMETHODCALLTYPE *GetAssertion )(
__RPC__in IPluginAuthenticator * This,
/* [in] */ __RPC__in PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
/* [retval][out] */ __RPC__out PWEBAUTHN_PLUGIN_OPERATION_RESPONSE response);
DECLSPEC_XFGVIRT(IPluginAuthenticator, CancelOperation)
HRESULT ( STDMETHODCALLTYPE *CancelOperation )(
__RPC__in IPluginAuthenticator * This,
/* [in] */ __RPC__in PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request);
DECLSPEC_XFGVIRT(IPluginAuthenticator, GetLockStatus)
HRESULT ( STDMETHODCALLTYPE *GetLockStatus )(
__RPC__in IPluginAuthenticator * This,
/* [retval][out] */ __RPC__out PLUGIN_LOCK_STATUS *lockStatus);
END_INTERFACE
} IPluginAuthenticatorVtbl;
interface IPluginAuthenticator
{
CONST_VTBL struct IPluginAuthenticatorVtbl *lpVtbl;
};
#ifdef COBJMACROS
#define IPluginAuthenticator_QueryInterface(This,riid,ppvObject) \
( (This)->lpVtbl -> QueryInterface(This,riid,ppvObject) )
#define IPluginAuthenticator_AddRef(This) \
( (This)->lpVtbl -> AddRef(This) )
#define IPluginAuthenticator_Release(This) \
( (This)->lpVtbl -> Release(This) )
#define IPluginAuthenticator_MakeCredential(This,request,response) \
( (This)->lpVtbl -> MakeCredential(This,request,response) )
#define IPluginAuthenticator_GetAssertion(This,request,response) \
( (This)->lpVtbl -> GetAssertion(This,request,response) )
#define IPluginAuthenticator_CancelOperation(This,request) \
( (This)->lpVtbl -> CancelOperation(This,request) )
#define IPluginAuthenticator_GetLockStatus(This,lockStatus) \
( (This)->lpVtbl -> GetLockStatus(This,lockStatus) )
#endif /* COBJMACROS */
#endif /* C style interface */
#endif /* __IPluginAuthenticator_INTERFACE_DEFINED__ */
/* Additional Prototypes for ALL interfaces */
unsigned long __RPC_USER HWND_UserSize( __RPC__in unsigned long *, unsigned long , __RPC__in HWND * );
unsigned char * __RPC_USER HWND_UserMarshal( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in HWND * );
unsigned char * __RPC_USER HWND_UserUnmarshal(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out HWND * );
void __RPC_USER HWND_UserFree( __RPC__in unsigned long *, __RPC__in HWND * );
unsigned long __RPC_USER HWND_UserSize64( __RPC__in unsigned long *, unsigned long , __RPC__in HWND * );
unsigned char * __RPC_USER HWND_UserMarshal64( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in HWND * );
unsigned char * __RPC_USER HWND_UserUnmarshal64(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out HWND * );
void __RPC_USER HWND_UserFree64( __RPC__in unsigned long *, __RPC__in HWND * );
/* end of Additional Prototypes */
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,83 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import "oaidl.idl";
import "objidl.idl";
import "wtypes.idl";
typedef enum _WEBAUTHN_PLUGIN_REQUEST_TYPE {
WEBAUTHN_PLUGIN_REQUEST_TYPE_CTAP2_CBOR = 0x01 // CBOR encoded CTAP2 message. Refer to the FIDO Specifications: Client to Authenticator Protocol (CTAP)
} WEBAUTHN_PLUGIN_REQUEST_TYPE;
typedef struct _WEBAUTHN_PLUGIN_OPERATION_REQUEST {
// Handle of the top level Window of the caller
HWND hWnd;
// Transaction ID
GUID transactionId;
// Request Hash Signature Bytes Buffer Size
DWORD cbRequestSignature;
// Request Hash Signature Bytes Buffer - Signature verified using the "pbOpSignPubKey" in PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE
[size_is(cbRequestSignature)] byte* pbRequestSignature;
// Request Type - Determines the encoding of the request and response buffers
WEBAUTHN_PLUGIN_REQUEST_TYPE requestType;
// Encoded Request Buffer Size
DWORD cbEncodedRequest;
// Encoded Request Buffer - Encoding type is determined by the requestType
[size_is(cbEncodedRequest)] byte* pbEncodedRequest;
} WEBAUTHN_PLUGIN_OPERATION_REQUEST, *PWEBAUTHN_PLUGIN_OPERATION_REQUEST;
typedef const WEBAUTHN_PLUGIN_OPERATION_REQUEST *PCWEBAUTHN_PLUGIN_OPERATION_REQUEST;
typedef struct _WEBAUTHN_PLUGIN_OPERATION_RESPONSE {
// Encoded Response Buffer Size
DWORD cbEncodedResponse;
// Encoded Response Buffer - Encoding type must match the request
[size_is(cbEncodedResponse)] byte* pbEncodedResponse;
} WEBAUTHN_PLUGIN_OPERATION_RESPONSE, *PWEBAUTHN_PLUGIN_OPERATION_RESPONSE;
typedef const WEBAUTHN_PLUGIN_OPERATION_RESPONSE *PCWEBAUTHN_PLUGIN_OPERATION_RESPONSE;
typedef struct _WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST {
// Transaction ID
GUID transactionId;
// Request Hash Signature Bytes Buffer Size
DWORD cbRequestSignature;
// Request Hash Signature Bytes Buffer - Signature verified using the "pbOpSignPubKey" in PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE
[size_is(cbRequestSignature)] byte* pbRequestSignature;
} WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST, *PWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
typedef const WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
typedef enum _PLUGIN_LOCK_STATUS {
PluginLocked = 0,
PluginUnlocked
} PLUGIN_LOCK_STATUS;
[
object,
uuid(d26bcf6f-b54c-43ff-9f06-d5bf148625f7),
version(1.0),
pointer_default(ref)
]
interface IPluginAuthenticator : IUnknown
{
HRESULT MakeCredential(
[in] PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
[out, retval] PWEBAUTHN_PLUGIN_OPERATION_RESPONSE response);
HRESULT GetAssertion(
[in] PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
[out, retval] PWEBAUTHN_PLUGIN_OPERATION_RESPONSE response);
HRESULT CancelOperation(
[in] PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request);
HRESULT GetLockStatus(
[out, retval] PLUGIN_LOCK_STATUS* lockStatus);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,588 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#pragma once
#include <winapifamily.h>
#pragma region Desktop Family or OneCore Family
#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_APP | WINAPI_PARTITION_SYSTEM)
#ifdef __cplusplus
extern "C" {
#endif
#ifndef WINAPI
#define WINAPI __stdcall
#endif
#ifndef INITGUID
#define INITGUID
#include <guiddef.h>
#undef INITGUID
#else
#include <guiddef.h>
#endif
//+------------------------------------------------------------------------------------------
// APIs.
//-------------------------------------------------------------------------------------------
typedef enum _PLUGIN_AUTHENTICATOR_STATE
{
AuthenticatorState_Disabled = 0,
AuthenticatorState_Enabled
} AUTHENTICATOR_STATE;
HRESULT
WINAPI
WebAuthNPluginGetAuthenticatorState(
_In_ REFCLSID rclsid,
_Out_ AUTHENTICATOR_STATE* pluginAuthenticatorState
);
//
// Plugin Authenticator API: WebAuthNAddPluginAuthenticator: Add Plugin Authenticator
//
typedef struct _WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS {
// Authenticator Name
LPCWSTR pwszAuthenticatorName;
// Plugin COM ClsId
REFCLSID rclsid;
// Plugin RPID (Optional. Required for a nested WebAuthN call originating from a plugin)
LPCWSTR pwszPluginRpId;
// Plugin Authenticator Logo for the Light themes. base64 encoded SVG 1.1 (Optional)
LPCWSTR pwszLightThemeLogoSvg;
// Plugin Authenticator Logo for the Dark themes. base64 encoded SVG 1.1 (Optional)
LPCWSTR pwszDarkThemeLogoSvg;
// CTAP CBOR encoded authenticatorGetInfo
DWORD cbAuthenticatorInfo;
_Field_size_bytes_(cbAuthenticatorInfo)
const BYTE* pbAuthenticatorInfo;
// List of supported RP IDs (Relying Party IDs). Should be 0/nullptr if all RPs are supported.
DWORD cSupportedRpIds;
const LPCWSTR* ppwszSupportedRpIds;
} WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS, *PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS;
typedef const WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS *PCWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS;
typedef struct _WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE {
// Plugin operation signing Public Key - Used to sign the request in PCWEBAUTHN_PLUGIN_OPERATION_REQUEST. Refer pluginauthenticator.h.
DWORD cbOpSignPubKey;
_Field_size_bytes_(cbOpSignPubKey)
PBYTE pbOpSignPubKey;
} WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE, *PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE;
typedef const WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE *PCWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE;
HRESULT
WINAPI
WebAuthNPluginAddAuthenticator(
_In_ PCWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS pPluginAddAuthenticatorOptions,
_Outptr_result_maybenull_ PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE* ppPluginAddAuthenticatorResponse);
void
WINAPI
WebAuthNPluginFreeAddAuthenticatorResponse(
_In_opt_ PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE pPluginAddAuthenticatorResponse);
//
// Plugin Authenticator API: WebAuthNRemovePluginAuthenticator: Remove Plugin Authenticator
//
HRESULT
WINAPI
WebAuthNPluginRemoveAuthenticator(
_In_ REFCLSID rclsid);
//
// Plugin Authenticator API: WebAuthNPluginAuthenticatorUpdateDetails: Update Credential Metadata for Browser AutoFill Scenarios
//
typedef struct _WEBAUTHN_PLUGIN_UPDATE_AUTHENTICATOR_DETAILS {
// Authenticator Name (Optional)
LPCWSTR pwszAuthenticatorName;
// Plugin COM ClsId
REFCLSID rclsid;
// New Plugin COM ClsId (Optional)
REFCLSID rclsidNew;
// Plugin Authenticator Logo for the Light themes. base64 encoded SVG 1.1 (Optional)
LPCWSTR pwszLightThemeLogoSvg;
// Plugin Authenticator Logo for the Dark themes. base64 encoded SVG 1.1 (Optional)
LPCWSTR pwszDarkThemeLogoSvg;
// CTAP CBOR encoded authenticatorGetInfo
DWORD cbAuthenticatorInfo;
_Field_size_bytes_(cbAuthenticatorInfo)
const BYTE* pbAuthenticatorInfo;
// List of supported RP IDs (Relying Party IDs). Should be 0/nullptr if all RPs are supported.
DWORD cSupportedRpIds;
const LPCWSTR* ppwszSupportedRpIds;
} WEBAUTHN_PLUGIN_UPDATE_AUTHENTICATOR_DETAILS, *PWEBAUTHN_PLUGIN_UPDATE_AUTHENTICATOR_DETAILS;
typedef const WEBAUTHN_PLUGIN_UPDATE_AUTHENTICATOR_DETAILS *PCWEBAUTHN_PLUGIN_UPDATE_AUTHENTICATOR_DETAILS;
HRESULT
WINAPI
WebAuthNPluginUpdateAuthenticatorDetails(
_In_ PCWEBAUTHN_PLUGIN_UPDATE_AUTHENTICATOR_DETAILS pPluginUpdateAuthenticatorDetails);
//
// Plugin Authenticator API: WebAuthNPluginAuthenticatorAddCredentials: Add Credential Metadata for Browser AutoFill Scenarios
//
typedef struct _WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS {
// Size of pbCredentialId.
DWORD cbCredentialId;
// Credential Identifier bytes. This field is required.
_Field_size_bytes_(cbCredentialId)
const BYTE* pbCredentialId;
// Identifier for the RP. This field is required.
LPCWSTR pwszRpId;
// Contains the friendly name of the Relying Party, such as "Acme Corporation", "Widgets Inc" or "Awesome Site".
// This field is required.
LPCWSTR pwszRpName;
// Identifier for the User. This field is required.
DWORD cbUserId;
// User Identifier bytes. This field is required.
_Field_size_bytes_(cbUserId)
const BYTE* pbUserId;
// Contains a detailed name for this account, such as "john.p.smith@example.com".
LPCWSTR pwszUserName;
// For User: Contains the friendly name associated with the user account such as "John P. Smith".
LPCWSTR pwszUserDisplayName;
} WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS, *PWEBAUTHN_PLUGIN_CREDENTIAL_DETAILS;
typedef const WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS *PCWEBAUTHN_PLUGIN_CREDENTIAL_DETAILS;
HRESULT
WINAPI
WebAuthNPluginAuthenticatorAddCredentials(
_In_ REFCLSID rclsid,
_In_ DWORD cCredentialDetails,
_In_reads_(cCredentialDetails) PCWEBAUTHN_PLUGIN_CREDENTIAL_DETAILS pCredentialDetails);
//
// Plugin Authenticator API: WebAuthNPluginAuthenticatorRemoveCredentials: Remove Credential Metadata for Browser AutoFill Scenarios
//
HRESULT
WINAPI
WebAuthNPluginAuthenticatorRemoveCredentials(
_In_ REFCLSID rclsid,
_In_ DWORD cCredentialDetails,
_In_reads_(cCredentialDetails) PCWEBAUTHN_PLUGIN_CREDENTIAL_DETAILS pCredentialDetails);
//
// Plugin Authenticator API: WebAuthNPluginAuthenticatorRemoveCredentials: Remove All Credential Metadata for Browser AutoFill Scenarios
//
HRESULT
WINAPI
WebAuthNPluginAuthenticatorRemoveAllCredentials(
_In_ REFCLSID rclsid);
//
// Plugin Authenticator API: WebAuthNPluginAuthenticatorGetAllCredentials: Get All Credential Metadata cached for Browser AutoFill Scenarios
//
HRESULT
WINAPI
WebAuthNPluginAuthenticatorGetAllCredentials(
_In_ REFCLSID rclsid,
_Out_ DWORD* pcCredentialDetails,
_Outptr_result_buffer_maybenull_(*pcCredentialDetails) PWEBAUTHN_PLUGIN_CREDENTIAL_DETAILS* ppCredentialDetailsArray);
//
// Plugin Authenticator API: WebAuthNPluginAuthenticatorFreeCredentialDetailsList: Free Credential Metadata cached for Browser AutoFill Scenarios
//
void
WINAPI
WebAuthNPluginAuthenticatorFreeCredentialDetailsArray(
_In_ DWORD cCredentialDetails,
_In_reads_(cCredentialDetails) PWEBAUTHN_PLUGIN_CREDENTIAL_DETAILS pCredentialDetailsArray);
//
// Hello UV API for Plugin: WebAuthNPluginPerformUv: Perform Hello UV related operations
//
typedef enum _WEBAUTHN_PLUGIN_PERFORM_UV_OPERATION_TYPE
{
PerformUserVerification = 1,
GetUserVerificationCount,
GetPublicKey
} WEBAUTHN_PLUGIN_PERFORM_UV_OPERATION_TYPE;
typedef 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.
HWND hwnd;
// The webauthn transaction id from the WEBAUTHN_PLUGIN_OPERATION_REQUEST
REFGUID rguidTransactionId;
// The username attached to the credential that is in use for this webauthn operation
LPCWSTR pwszUsername;
// A text hint displayed on the windows hello prompt
LPCWSTR pwszDisplayHint;
} WEBAUTHN_PLUGIN_USER_VERIFICATION_REQUEST, *PWEBAUTHN_PLUGIN_USER_VERIFICATION_REQUEST;
typedef const WEBAUTHN_PLUGIN_USER_VERIFICATION_REQUEST *PCWEBAUTHN_PLUGIN_USER_VERIFICATION_REQUEST;
HRESULT
WINAPI
WebAuthNPluginPerformUserVerification(
_In_ PCWEBAUTHN_PLUGIN_USER_VERIFICATION_REQUEST pPluginUserVerification,
_Out_ DWORD* pcbResponse,
_Outptr_result_bytebuffer_maybenull_(*pcbResponse) PBYTE* ppbResponse);
void
WINAPI
WebAuthNPluginFreeUserVerificationResponse(
_In_opt_ PBYTE ppbResponse);
HRESULT
WINAPI
WebAuthNPluginGetUserVerificationCount(
_In_ REFCLSID rclsid,
_Out_ DWORD* pdwVerificationCount);
HRESULT
WINAPI
WebAuthNPluginGetUserVerificationPublicKey(
_In_ REFCLSID rclsid,
_Out_ DWORD* pcbPublicKey,
_Outptr_result_bytebuffer_(*pcbPublicKey) PBYTE* ppbPublicKey); // Free using WebAuthNPluginFreePublicKeyResponse
HRESULT
WINAPI
WebAuthNPluginGetOperationSigningPublicKey(
_In_ REFCLSID rclsid,
_Out_ DWORD* pcbOpSignPubKey,
_Outptr_result_buffer_maybenull_(*pcbOpSignPubKey) PBYTE* ppbOpSignPubKey); // Free using WebAuthNPluginFreePublicKeyResponse
void WINAPI WebAuthNPluginFreePublicKeyResponse(
_In_opt_ PBYTE pbOpSignPubKey);
#define WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS_VERSION_1 1
#define WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS_CURRENT_VERSION WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS_VERSION_1
typedef struct _WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS {
//Version of this structure, to allow for modifications in the future.
DWORD dwVersion;
// Following have following values:
// +1 - TRUE
// 0 - Not defined
// -1 - FALSE
//up: "true" | "false"
LONG lUp;
//uv: "true" | "false"
LONG lUv;
//rk: "true" | "false"
LONG lRequireResidentKey;
} WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS, *PWEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS;
typedef const WEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS *PCWEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS;
#define WEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY_VERSION_1 1
#define WEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY_CURRENT_VERSION WEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY_VERSION_1
typedef struct _WEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY {
//Version of this structure, to allow for modifications in the future.
DWORD dwVersion;
// Key type
LONG lKty;
// Hash Algorithm: ES256, ES384, ES512
LONG lAlg;
// Curve
LONG lCrv;
//Size of "x" (X Coordinate)
DWORD cbX;
//"x" (X Coordinate) data. Big Endian.
PBYTE pbX;
//Size of "y" (Y Coordinate)
DWORD cbY;
//"y" (Y Coordinate) data. Big Endian.
PBYTE pbY;
} WEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY, *PWEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY;
typedef const WEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY *PCWEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY;
#define WEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION_VERSION_1 1
#define WEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION_CURRENT_VERSION WEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION_VERSION_1
typedef struct _WEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION {
//Version of this structure, to allow for modifications in the future.
DWORD dwVersion;
// Platform's key agreement public key
PWEBAUTHN_CTAPCBOR_ECC_PUBLIC_KEY pKeyAgreement;
DWORD cbEncryptedSalt;
PBYTE pbEncryptedSalt;
DWORD cbSaltAuth;
PBYTE pbSaltAuth;
} WEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION, *PWEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION;
typedef const WEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION *PCWEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION;
#define WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST_VERSION_1 1
#define WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST_CURRENT_VERSION WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST_VERSION_1
typedef struct _WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST {
//Version of this structure, to allow for modifications in the future.
DWORD dwVersion;
//Input RP ID. Raw UTF8 bytes before conversion.
//These are the bytes to be hashed in the Authenticator Data.
DWORD cbRpId;
PBYTE pbRpId;
//Client Data Hash
DWORD cbClientDataHash;
PBYTE pbClientDataHash;
//RP Information
PCWEBAUTHN_RP_ENTITY_INFORMATION pRpInformation;
//User Information
PCWEBAUTHN_USER_ENTITY_INFORMATION pUserInformation;
// Crypto Parameters
WEBAUTHN_COSE_CREDENTIAL_PARAMETERS WebAuthNCredentialParameters;
//Credentials used for exclusion
WEBAUTHN_CREDENTIAL_LIST CredentialList;
//Optional extensions to parse when performing the operation.
DWORD cbCborExtensionsMap;
PBYTE pbCborExtensionsMap;
// Authenticator Options (Optional)
PWEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS pAuthenticatorOptions;
// Pin Auth (Optional)
BOOL fEmptyPinAuth; // Zero length PinAuth is included in the request
DWORD cbPinAuth;
PBYTE pbPinAuth;
//"hmac-secret": true extension
LONG lHmacSecretExt;
// "hmac-secret-mc" extension
PWEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION pHmacSecretMcExtension;
//"prf" extension
LONG lPrfExt;
DWORD cbHmacSecretSaltValues;
PBYTE pbHmacSecretSaltValues;
//"credProtect" extension. Nonzero if present
DWORD dwCredProtect;
// Nonzero if present
DWORD dwPinProtocol;
// Nonzero if present
DWORD dwEnterpriseAttestation;
//"credBlob" extension. Nonzero if present
DWORD cbCredBlobExt;
PBYTE pbCredBlobExt;
//"largeBlobKey": true extension
LONG lLargeBlobKeyExt;
//"largeBlob": extension
DWORD dwLargeBlobSupport;
//"minPinLength": true extension
LONG lMinPinLengthExt;
// "json" extension. Nonzero if present
DWORD cbJsonExt;
PBYTE pbJsonExt;
} WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST, *PWEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST;
typedef const WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST *PCWEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST;
_Success_(return == S_OK)
HRESULT
WINAPI
WebAuthNEncodeMakeCredentialResponse(
_In_ PCWEBAUTHN_CREDENTIAL_ATTESTATION pCredentialAttestation,
_Out_ DWORD* pcbResp,
_Outptr_result_buffer_maybenull_(*pcbResp) BYTE** ppbResp
);
_Success_(return == S_OK)
HRESULT
WINAPI
WebAuthNDecodeMakeCredentialRequest(
_In_ DWORD cbEncoded,
_In_reads_bytes_(cbEncoded) const BYTE* pbEncoded,
_Outptr_ PWEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST* ppMakeCredentialRequest
);
void
WINAPI
WebAuthNFreeDecodedMakeCredentialRequest(
_In_opt_ PWEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST pMakeCredentialRequest
);
#define WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST_VERSION_1 1
#define WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST_CURRENT_VERSION WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST_VERSION_1
typedef struct _WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST {
//Version of this structure, to allow for modifications in the future.
DWORD dwVersion;
//RP ID. After UTF8 to Unicode conversion,
PCWSTR pwszRpId;
//Input RP ID. Raw UTF8 bytes before conversion.
//These are the bytes to be hashed in the Authenticator Data.
DWORD cbRpId;
PBYTE pbRpId;
//Client Data Hash
DWORD cbClientDataHash;
PBYTE pbClientDataHash;
//Credentials used for inclusion
WEBAUTHN_CREDENTIAL_LIST CredentialList;
//Optional extensions to parse when performing the operation.
DWORD cbCborExtensionsMap;
PBYTE pbCborExtensionsMap;
// Authenticator Options (Optional)
PWEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS pAuthenticatorOptions;
// Pin Auth (Optional)
BOOL fEmptyPinAuth; // Zero length PinAuth is included in the request
DWORD cbPinAuth;
PBYTE pbPinAuth;
// HMAC Salt Extension (Optional)
PWEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION pHmacSaltExtension;
// PRF Extension
DWORD cbHmacSecretSaltValues;
PBYTE pbHmacSecretSaltValues;
DWORD dwPinProtocol;
//"credBlob": true extension
LONG lCredBlobExt;
//"largeBlobKey": true extension
LONG lLargeBlobKeyExt;
//"largeBlob" extension
DWORD dwCredLargeBlobOperation;
DWORD cbCredLargeBlobCompressed;
PBYTE pbCredLargeBlobCompressed;
DWORD dwCredLargeBlobOriginalSize;
// "json" extension. Nonzero if present
DWORD cbJsonExt;
PBYTE pbJsonExt;
} WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST, *PWEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST;
typedef const WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST *PCWEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST;
_Success_(return == S_OK)
HRESULT
WINAPI
WebAuthNDecodeGetAssertionRequest(
_In_ DWORD cbEncoded,
_In_reads_bytes_(cbEncoded) const BYTE* pbEncoded,
_Outptr_ PWEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST* ppGetAssertionRequest
);
void
WINAPI
WebAuthNFreeDecodedGetAssertionRequest(
_In_opt_ PWEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST pGetAssertionRequest
);
typedef struct _WEBAUTHN_CTAPCBOR_GET_ASSERTION_RESPONSE {
// [1] credential (optional)
// [2] authenticatorData
// [3] signature
WEBAUTHN_ASSERTION WebAuthNAssertion;
// [4] user (optional)
PCWEBAUTHN_USER_ENTITY_INFORMATION pUserInformation;
// [5] numberOfCredentials (optional)
DWORD dwNumberOfCredentials;
// [6] userSelected (optional)
LONG lUserSelected;
// [7] largeBlobKey (optional)
DWORD cbLargeBlobKey;
PBYTE pbLargeBlobKey;
// [8] unsignedExtensionOutputs
DWORD cbUnsignedExtensionOutputs;
PBYTE pbUnsignedExtensionOutputs;
} WEBAUTHN_CTAPCBOR_GET_ASSERTION_RESPONSE, *PWEBAUTHN_CTAPCBOR_GET_ASSERTION_RESPONSE;
typedef const WEBAUTHN_CTAPCBOR_GET_ASSERTION_RESPONSE *PCWEBAUTHN_CTAPCBOR_GET_ASSERTION_RESPONSE;
_Success_(return == S_OK)
HRESULT
WINAPI
WebAuthNEncodeGetAssertionResponse(
_In_ PCWEBAUTHN_CTAPCBOR_GET_ASSERTION_RESPONSE pGetAssertionResponse,
_Out_ DWORD* pcbResp,
_Outptr_result_buffer_maybenull_(*pcbResp) BYTE** ppbResp
);
typedef void (CALLBACK* WEBAUTHN_PLUGIN_STATUS_CHANGE_CALLBACK )(void* context);
HRESULT
WINAPI
WebAuthNPluginRegisterStatusChangeCallback(
_In_ WEBAUTHN_PLUGIN_STATUS_CHANGE_CALLBACK callback,
_In_ void* context,
_In_ REFCLSID rclsid,
_Out_ DWORD* pdwRegister
);
HRESULT
WINAPI
WebAuthNPluginUnregisterStatusChangeCallback(
_In_ DWORD* pdwRegister
);
#ifdef __cplusplus
} // Balance extern "C" above
#endif
#endif // WINAPI_FAMILY_PARTITION
#pragma endregion

View File

@@ -0,0 +1,72 @@
#![cfg(target_os = "windows")]
pub mod plugin;
mod types;
mod util;
use std::{error::Error, fmt::Display};
pub use types::{
AuthenticatorInfo, CredentialId, CtapTransport, CtapVersion, PublicKeyCredentialParameters,
UserId,
};
#[derive(Debug)]
pub struct WinWebAuthnError {
kind: ErrorKind,
description: Option<String>,
cause: Option<Box<dyn std::error::Error>>,
}
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<E: std::error::Error + 'static>(
kind: ErrorKind,
description: &str,
cause: E,
) -> Self {
let cause: Box<dyn std::error::Error> = Box::new(cause);
Self {
kind,
description: Some(description.to_string()),
cause: Some(cause),
}
}
}
#[derive(Debug)]
enum ErrorKind {
DllLoad,
Serialization,
InvalidArguments,
Other,
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 {}

View File

@@ -0,0 +1,480 @@
//! Functions for interacting with Windows COM.
#![allow(non_snake_case)]
#![allow(non_camel_case_types)]
use std::{
alloc,
mem::MaybeUninit,
ptr::{self, NonNull},
sync::{Arc, OnceLock},
};
use windows::{
core::{implement, interface, ComObjectInterface, IUnknown, GUID, HRESULT},
Win32::{
Foundation::{E_FAIL, E_INVALIDARG, S_FALSE, S_OK},
System::Com::*,
},
};
use windows_core::{IInspectable, Interface};
use super::{
types::{
PluginLockStatus, WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST,
WEBAUTHN_PLUGIN_OPERATION_REQUEST, WEBAUTHN_PLUGIN_OPERATION_RESPONSE,
},
PluginAuthenticator,
};
use crate::{
plugin::{crypto, PluginGetAssertionRequest, PluginMakeCredentialRequest},
ErrorKind, WinWebAuthnError,
};
static HANDLER: OnceLock<(GUID, Arc<dyn PluginAuthenticator + Send + Sync>)> = OnceLock::new();
static SHUTDOWN: OnceLock<bool> = OnceLock::new();
#[implement(IClassFactory)]
pub struct Factory;
impl IClassFactory_Impl for Factory_Impl {
fn CreateInstance(
&self,
_outer: windows::core::Ref<IUnknown>,
iid: *const windows::core::GUID,
object: *mut *mut core::ffi::c_void,
) -> windows::core::Result<()> {
let (clsid, handler) = match HANDLER.get() {
Some(state) => state,
None => {
tracing::error!("Cannot create COM class object instance because the handler is not initialized. register_server() must be called before starting the COM server.");
return Err(E_FAIL.into());
}
}.clone();
let unknown: IInspectable = PluginAuthenticatorComObject { clsid, handler }.into();
unsafe { unknown.query(iid, object).ok() }
}
fn LockServer(&self, _lock: windows::core::BOOL) -> windows::core::Result<()> {
// TODO: Implement lock server
Ok(())
}
}
// IPluginAuthenticator interface
#[interface("d26bcf6f-b54c-43ff-9f06-d5bf148625f7")]
pub unsafe trait IPluginAuthenticator: windows::core::IUnknown {
fn MakeCredential(
&self,
request: *const WEBAUTHN_PLUGIN_OPERATION_REQUEST,
response: *mut WEBAUTHN_PLUGIN_OPERATION_RESPONSE,
) -> HRESULT;
fn GetAssertion(
&self,
request: *const WEBAUTHN_PLUGIN_OPERATION_REQUEST,
response: *mut WEBAUTHN_PLUGIN_OPERATION_RESPONSE,
) -> HRESULT;
fn CancelOperation(&self, request: *const WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST) -> HRESULT;
fn GetLockStatus(&self, lock_status: *mut PluginLockStatus) -> HRESULT;
}
#[implement(IPluginAuthenticator)]
struct PluginAuthenticatorComObject {
clsid: GUID,
handler: Arc<dyn PluginAuthenticator + Send + Sync>,
}
impl IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Impl {
unsafe fn MakeCredential(
&self,
request: *const WEBAUTHN_PLUGIN_OPERATION_REQUEST,
response: *mut WEBAUTHN_PLUGIN_OPERATION_RESPONSE,
) -> HRESULT {
tracing::debug!("MakeCredential called");
let response = match NonNull::new(response) {
Some(p) => p,
None => {
tracing::warn!(
"MakeCredential called with null response pointer from Windows. Aborting request."
);
return E_INVALIDARG;
}
};
let op_request_ptr = match NonNull::new(request.cast_mut()) {
Some(p) => p,
None => {
tracing::warn!(
"MakeCredential called with null request pointer from Windows. Aborting request."
);
return E_INVALIDARG;
}
};
if let Err(err) = verify_operation_request(op_request_ptr.as_ref(), &self.clsid) {
tracing::error!("Failed to verify request signature: {err}");
return E_INVALIDARG;
}
// SAFETY: we received the pointer from Windows, so we trust that the values are set
// properly.
let registration_request = match PluginMakeCredentialRequest::try_from_ptr(op_request_ptr) {
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) => {
// SAFETY: response pointer was given to us by Windows, so we assume it's valid.
match write_operation_response(&registration_response, response) {
Ok(()) => {
tracing::debug!("MakeCredential completed successfully");
S_OK
}
Err(err) => {
tracing::error!(
"Failed to write MakeCredential response to Windows: {err}"
);
return E_FAIL;
}
}
}
Err(err) => {
tracing::error!("MakeCredential failed: {err}");
E_FAIL
}
}
}
unsafe fn GetAssertion(
&self,
request: *const WEBAUTHN_PLUGIN_OPERATION_REQUEST,
response: *mut WEBAUTHN_PLUGIN_OPERATION_RESPONSE,
) -> HRESULT {
tracing::debug!("GetAssertion called");
let response = match NonNull::new(response) {
Some(p) => p,
None => {
tracing::warn!(
"GetAssertion called with null response pointer from Windows. Aborting request."
);
return E_INVALIDARG;
}
};
let op_request_ptr = match NonNull::new(request.cast_mut()) {
Some(p) => p,
None => {
tracing::warn!(
"GetAssertion called with null request pointer from Windows. Aborting request."
);
return E_INVALIDARG;
}
};
if let Err(err) = verify_operation_request(op_request_ptr.as_ref(), &self.clsid) {
tracing::error!("Failed to verify request signature: {err}");
return E_INVALIDARG;
}
let assertion_request = match PluginGetAssertionRequest::try_from_ptr(op_request_ptr) {
Ok(assertion_request) => assertion_request,
Err(err) => {
tracing::error!("Could not deserialize GetAssertion request: {err}");
return E_FAIL;
}
};
match self.handler.get_assertion(assertion_request) {
Ok(assertion_response) => {
// SAFETY: response pointer was given to us by Windows, so we assume it's valid.
match write_operation_response(&assertion_response, response) {
Ok(()) => {
tracing::debug!("GetAssertion completed successfully");
S_OK
}
Err(err) => {
tracing::error!("Failed to write GetCredential response to Windows: {err}");
return E_FAIL;
}
}
}
Err(err) => {
tracing::error!("GetAssertion failed: {err}");
E_FAIL
}
}
}
unsafe fn CancelOperation(
&self,
request: *const WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST,
) -> HRESULT {
tracing::debug!("CancelOperation called");
let request = match NonNull::new(request as *mut WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST) {
Some(request) => request,
None => {
tracing::warn!("Received null CancelOperation request");
return E_INVALIDARG;
}
};
match self.handler.cancel_operation(request.into()) {
Ok(()) => {
tracing::error!("CancelOperation completed successfully");
S_OK
}
Err(err) => {
tracing::error!("CancelOperation failed: {err}");
E_FAIL
}
}
}
unsafe fn GetLockStatus(&self, lock_status: *mut PluginLockStatus) -> HRESULT {
tracing::debug!(
"GetLockStatus() called <PID {}, Thread {:?}>",
std::process::id(),
std::thread::current().id()
);
if lock_status.is_null() {
return HRESULT(-2147024809); // E_INVALIDARG
}
match self.handler.lock_status() {
Ok(status) => {
tracing::debug!("GetLockStatus received {status:?}");
*lock_status = status;
S_OK
}
Err(err) => {
tracing::error!("GetLockStatus failed: {err}");
E_FAIL
}
}
}
}
/// Copies data as COM-allocated buffer and writes to response pointer.
///
/// Safety constraints: [response] must point to a valid
/// WEBAUTHN_PLUGIN_OPERATION_RESPONSE struct.
unsafe fn write_operation_response(
data: &[u8],
response: NonNull<WEBAUTHN_PLUGIN_OPERATION_RESPONSE>,
) -> Result<(), WinWebAuthnError> {
let len = match data.len().try_into() {
Ok(len) => len,
Err(err) => {
return Err(WinWebAuthnError::with_cause(
ErrorKind::Serialization,
"Response is too long to return to OS",
err,
));
}
};
let buf = data.to_com_buffer();
response.write(WEBAUTHN_PLUGIN_OPERATION_RESPONSE {
cbEncodedResponse: len,
pbEncodedResponse: buf.leak(),
});
Ok(())
}
/// Registers the plugin authenticator COM library with Windows.
pub(super) fn register_server<T>(clsid: &GUID, handler: T) -> Result<(), WinWebAuthnError>
where
T: PluginAuthenticator + Send + Sync + 'static,
{
if HANDLER.get().is_some() {
return Err(WinWebAuthnError::new(
ErrorKind::Other,
"server can only be registered one time per process",
));
}
unsafe {
// Initialize COM on this thread.
let com_init_result = CoInitializeEx(None, COINIT_APARTMENTTHREADED);
match com_init_result {
S_OK | S_FALSE => {} // COM successfully initialized, and should be uninitialized with CoUninitialize later.
code => {
return Err(WinWebAuthnError::with_cause(
ErrorKind::WindowsInternal,
"Could not initialize the COM library",
windows::core::Error::from_hresult(code),
));
}
}
// If this call fails, some library in this process may have already set
// it. We will ignore any failures.
if let Err(err) = CoInitializeSecurity(
None,
-1,
None,
None,
RPC_C_AUTHN_LEVEL_DEFAULT,
RPC_C_IMP_LEVEL_IMPERSONATE,
None,
EOAC_NONE,
None,
) {
tracing::warn!("Could not initialize COM security: {err}",);
};
}
// Store the handler as a static so it can be initialized
HANDLER.set((*clsid, Arc::new(handler))).map_err(|_| {
WinWebAuthnError::new(ErrorKind::WindowsInternal, "Handler already initialized")
})?;
// Register the COM class object so that Windows RPC knows how to start it.
static FACTORY: windows::core::StaticComObject<Factory> = Factory.into_static();
unsafe {
CoRegisterClassObject(
ptr::from_ref(clsid),
FACTORY.as_interface_ref(),
CLSCTX_LOCAL_SERVER,
REGCLS_MULTIPLEUSE,
)
}
.map_err(|err| {
WinWebAuthnError::with_cause(
ErrorKind::WindowsInternal,
"Couldn't register the COM library with Windows",
err,
)
})?;
Ok(())
}
pub(super) fn shutdown_server() -> std::result::Result<(), WinWebAuthnError> {
if HANDLER.get().is_some() {
if let Ok(()) = SHUTDOWN.set(true) {
unsafe { CoUninitialize() };
} else {
tracing::debug!("server already shut down");
}
} else {
tracing::debug!("server was not registered. Ignoring.");
}
Ok(())
}
#[repr(transparent)]
pub(super) struct ComBuffer(NonNull<MaybeUninit<u8>>);
impl ComBuffer {
/// Returns an COM-allocated buffer of `size`.
fn alloc(size: usize, for_slice: bool) -> Self {
#[expect(clippy::as_conversions)]
{
assert!(size <= isize::MAX as usize, "requested bad object size");
}
// SAFETY: Any size is valid to pass to Windows, even `0`.
let ptr = NonNull::new(unsafe { CoTaskMemAlloc(size) }).unwrap_or_else(|| {
// XXX: This doesn't have to be correct, just close enough for an OK OOM error.
let layout = alloc::Layout::from_size_align(size, align_of::<u8>()).unwrap();
alloc::handle_alloc_error(layout)
});
if for_slice {
// Ininitialize the buffer so it can later be treated as `&mut [u8]`.
// SAFETY: The pointer is valid and we are using a valid value for a byte-wise
// allocation.
unsafe { ptr.write_bytes(0, size) };
}
Self(ptr.cast())
}
pub fn leak<T>(self) -> *mut T {
self.0.cast().as_ptr()
}
}
pub(super) trait ComBufferExt {
fn to_com_buffer(&self) -> ComBuffer;
}
impl ComBufferExt for Vec<u8> {
fn to_com_buffer(&self) -> ComBuffer {
ComBuffer::from(&self)
}
}
impl ComBufferExt for &[u8] {
fn to_com_buffer(&self) -> ComBuffer {
ComBuffer::from(self)
}
}
impl ComBufferExt for Vec<u16> {
fn to_com_buffer(&self) -> ComBuffer {
let buffer: Vec<u8> = self.into_iter().flat_map(|x| x.to_le_bytes()).collect();
ComBuffer::from(&buffer)
}
}
impl ComBufferExt for &[u16] {
fn to_com_buffer(&self) -> ComBuffer {
let buffer: Vec<u8> = self
.as_ref()
.into_iter()
.flat_map(|x| x.to_le_bytes())
.collect();
ComBuffer::from(&buffer)
}
}
impl<T: AsRef<[u8]>> From<T> for ComBuffer {
fn from(value: T) -> Self {
let buffer: Vec<u8> = value
.as_ref()
.into_iter()
.flat_map(|x| x.to_le_bytes())
.collect();
let len = buffer.len();
let com_buffer = Self::alloc(len, true);
// SAFETY: `ptr` points to a valid allocation that `len` matches, and we made sure
// the bytes were initialized. Additionally, bytes have no alignment requirements.
unsafe {
NonNull::slice_from_raw_parts(com_buffer.0.cast::<u8>(), len)
.as_mut()
.copy_from_slice(&buffer);
}
com_buffer
}
}
/// Verify the signature on an operation request.
///
/// # Safety
/// The caller must ensure that:
/// - pbEncodedRequest points to a valid non-null byte string of length cbEncodedRequest.
/// - pbRequestSignature points to a valid non-null byte string of length cbRequestSignature.
unsafe fn verify_operation_request(
request: &WEBAUTHN_PLUGIN_OPERATION_REQUEST,
clsid: &GUID,
) -> Result<(), WinWebAuthnError> {
tracing::debug!("Verifying request");
let request_data =
std::slice::from_raw_parts(request.pbEncodedRequest, request.cbEncodedRequest as usize);
let request_hash = crypto::hash_sha256(request_data).map_err(|err| {
WinWebAuthnError::with_cause(ErrorKind::WindowsInternal, "failed to hash request", err)
})?;
let signature = std::slice::from_raw_parts(
request.pbRequestSignature,
request.cbRequestSignature as usize,
);
tracing::debug!("Retrieving signing key");
let op_pub_key = crypto::get_operation_signing_public_key(clsid).map_err(|err| {
WinWebAuthnError::with_cause(
ErrorKind::WindowsInternal,
"Failed to get signing key for operation",
err,
)
})?;
tracing::debug!("Verifying signature");
op_pub_key.verify_signature(&request_hash, signature)
}

View File

@@ -0,0 +1,249 @@
#![allow(non_snake_case)]
use std::{mem::MaybeUninit, ptr::NonNull};
use windows::{
core::{GUID, HRESULT, PCWSTR},
Win32::{
Foundation::E_INVALIDARG,
Security::Cryptography::{
BCryptCreateHash, BCryptFinishHash, BCryptGetProperty, BCryptHashData, NCryptImportKey,
NCryptOpenStorageProvider, NCryptVerifySignature, BCRYPT_HASH_LENGTH, BCRYPT_KEY_BLOB,
BCRYPT_OBJECT_LENGTH, BCRYPT_PKCS1_PADDING_INFO, BCRYPT_PUBLIC_KEY_BLOB,
BCRYPT_RSAPUBLIC_MAGIC, BCRYPT_SHA256_ALGORITHM, BCRYPT_SHA256_ALG_HANDLE,
NCRYPT_FLAGS, NCRYPT_PAD_PKCS1_FLAG,
},
},
};
use crate::{util::webauthn_call, ErrorKind, WinWebAuthnError};
webauthn_call!("WebAuthNPluginGetUserVerificationPublicKey" as fn webauthn_plugin_get_user_verification_public_key(
rclsid: *const GUID,
pcbPublicKey: *mut u32,
ppbPublicKey: *mut *mut u8) -> HRESULT); // Free using WebAuthNPluginFreePublicKeyResponse
webauthn_call!("WebAuthNPluginGetOperationSigningPublicKey" as fn webauthn_plugin_get_operation_signing_public_key(
rclsid: *const GUID,
pcbOpSignPubKey: *mut u32,
ppbOpSignPubKey: *mut *mut u8
) -> HRESULT); // Free using WebAuthNPluginFreePublicKeyResponse
webauthn_call!("WebAuthNPluginFreePublicKeyResponse" as fn webauthn_plugin_free_public_key_response(
pbOpSignPubKey: *mut u8
) -> ());
pub(super) fn get_operation_signing_public_key(
clsid: &GUID,
) -> Result<SigningKey, WinWebAuthnError> {
let mut len = 0;
let mut data = MaybeUninit::uninit();
unsafe {
// SAFETY: We check the OS error code before using the written pointer.
webauthn_plugin_get_operation_signing_public_key(clsid, &mut len, data.as_mut_ptr())?
.ok()
.map_err(|err| {
WinWebAuthnError::with_cause(
ErrorKind::WindowsInternal,
"Failed to retrieve operation signing public key",
err,
)
})?;
match NonNull::new(data.assume_init()) {
Some(data) => Ok(SigningKey {
cbPublicKey: len,
pbPublicKey: data,
}),
None => Err(WinWebAuthnError::new(
ErrorKind::WindowsInternal,
"Windows returned null pointer when requesting operation signing public key",
)),
}
}
}
pub(super) fn get_user_verification_public_key(
clsid: &GUID,
) -> Result<SigningKey, WinWebAuthnError> {
let mut len = 0;
let mut data = MaybeUninit::uninit();
// SAFETY: We check the OS error code before using the written pointer.
unsafe {
webauthn_plugin_get_user_verification_public_key(clsid, &mut len, data.as_mut_ptr())?
.ok()
.map_err(|err| {
WinWebAuthnError::with_cause(
ErrorKind::WindowsInternal,
"Failed to retrieve user verification public key",
err,
)
})?;
match NonNull::new(data.assume_init()) {
Some(data) => Ok(SigningKey {
cbPublicKey: len,
pbPublicKey: data,
}),
None => Err(WinWebAuthnError::new(
ErrorKind::WindowsInternal,
"Windows returned null pointer when requesting user verification public key",
)),
}
}
}
fn verify_signature(
public_key: &SigningKey,
hash: &[u8],
signature: &[u8],
) -> Result<(), windows::core::Error> {
// Verify the signature over the hash of dataBuffer using the hKey
unsafe {
tracing::debug!("Getting provider");
// Get the provider
let mut provider = MaybeUninit::uninit();
NCryptOpenStorageProvider(provider.as_mut_ptr(), PCWSTR::null(), 0)?;
let provider = provider.assume_init();
tracing::debug!("Getting key handle");
// Create a NCrypt key handle from the public key
let mut key_handle = MaybeUninit::uninit();
NCryptImportKey(
provider,
None,
BCRYPT_PUBLIC_KEY_BLOB,
None,
key_handle.as_mut_ptr(),
public_key.as_ref(),
NCRYPT_FLAGS(0),
)?;
let key_handle = key_handle.assume_init();
tracing::debug!("Trying to read public key slice as key blob");
let public_key = public_key.as_ref();
if public_key.len() < size_of::<BCRYPT_KEY_BLOB>() {
return Err(windows::core::Error::from_hresult(E_INVALIDARG));
}
let key_blob: &BCRYPT_KEY_BLOB = &*public_key.as_ptr().cast();
tracing::debug!(" got key magic: {}", key_blob.Magic);
let (padding_info, cng_flags) = if key_blob.Magic == BCRYPT_RSAPUBLIC_MAGIC.0 {
tracing::debug!("Detected RSA key, adding PKCS1 padding");
let padding_info = BCRYPT_PKCS1_PADDING_INFO {
pszAlgId: BCRYPT_SHA256_ALGORITHM,
};
(Some(padding_info), NCRYPT_PAD_PKCS1_FLAG)
} else {
tracing::debug!("Non-RSA key, no PKCS1 padding added");
(None, NCRYPT_FLAGS(0))
};
tracing::debug!("Verifying signature");
NCryptVerifySignature(
key_handle,
padding_info
.as_ref()
.map(|padding: &BCRYPT_PKCS1_PADDING_INFO| std::ptr::from_ref(padding).cast()),
hash,
signature,
cng_flags,
)?;
tracing::debug!("Verified");
Ok(())
}
}
pub(super) fn hash_sha256(data: &[u8]) -> Result<Vec<u8>, windows::core::Error> {
unsafe {
tracing::debug!("Getting length of hash buffer object");
// Get length of SHA-256 buffer
let mut len_size_buf = [0; size_of::<u32>()];
let mut bytes_read = 0;
BCryptGetProperty(
BCRYPT_SHA256_ALG_HANDLE.into(),
BCRYPT_OBJECT_LENGTH,
Some(&mut len_size_buf),
&mut bytes_read,
0,
)
.ok()?;
// SAFETY: We explicitly set the size of the buffer to u32, and we only
// support platforms where usize is at least 32-bits.
let len_size: usize = u32::from_ne_bytes(len_size_buf) as usize;
tracing::debug!(" Length of hash buffer object: {len_size}");
let mut hash_obj_buf = Vec::with_capacity(len_size);
hash_obj_buf.set_len(len_size);
tracing::debug!("Creating hash algorithm handle with buffer object");
// Get SHA256 handle
let mut hash_handle = MaybeUninit::uninit();
BCryptCreateHash(
BCRYPT_SHA256_ALG_HANDLE,
hash_handle.as_mut_ptr(),
Some(hash_obj_buf.as_mut_slice()),
None,
0,
)
.ok()?;
let hash_handle = hash_handle.assume_init();
tracing::debug!("Hashing data");
// Hash data
BCryptHashData(hash_handle, data, 0).ok()?;
// Get length of SHA256 hash output
tracing::debug!("Getting length of hash output");
let mut hash_output_len_buf = [0; size_of::<u32>()];
let mut bytes_read = 0;
BCryptGetProperty(
BCRYPT_SHA256_ALG_HANDLE.into(),
BCRYPT_HASH_LENGTH,
Some(&mut hash_output_len_buf),
&mut bytes_read,
0,
)
.ok()?;
let hash_output_len = u32::from_ne_bytes(hash_output_len_buf) as usize;
tracing::debug!(" Length of hash output: {hash_output_len}");
tracing::debug!("Completing hash");
let mut hash_buffer = Vec::with_capacity(hash_output_len);
hash_buffer.set_len(hash_output_len);
BCryptFinishHash(hash_handle, hash_buffer.as_mut_slice(), 0).ok()?;
tracing::debug!(" Hash: {hash_buffer:?}");
Ok(hash_buffer)
}
}
/// Signing key for an operation request or user verification response buffer.
pub struct SigningKey {
cbPublicKey: u32,
pbPublicKey: NonNull<u8>,
}
impl SigningKey {
/// Verifies a signature over some data with the associated public key.
pub fn verify_signature(&self, data: &[u8], signature: &[u8]) -> Result<(), WinWebAuthnError> {
verify_signature(self, data, signature).map_err(|err| {
WinWebAuthnError::with_cause(
ErrorKind::WindowsInternal,
"Failed to verify signature",
err,
)
})
}
}
impl Drop for SigningKey {
fn drop(&mut self) {
unsafe {
_ = webauthn_plugin_free_public_key_response(self.pbPublicKey.as_mut());
}
}
}
impl AsRef<[u8]> for SigningKey {
fn as_ref(&self) -> &[u8] {
// SAFETY: We only support platforms where usize >= 32-bts
let len = self.cbPublicKey as usize;
// SAFETY: This pointer was given to us from Windows, so we trust it.
unsafe { std::slice::from_raw_parts(self.pbPublicKey.as_ptr(), len) }
}
}

View File

@@ -0,0 +1,376 @@
pub(crate) mod com;
pub(crate) mod crypto;
pub(crate) mod types;
use std::{error::Error, ptr::NonNull};
use types::*;
pub use types::{
PluginAddAuthenticatorOptions, PluginAddAuthenticatorResponse, PluginCancelOperationRequest,
PluginCredentialDetails, PluginGetAssertionRequest, PluginLockStatus,
PluginMakeCredentialRequest, PluginMakeCredentialResponse, PluginUserVerificationRequest,
PluginUserVerificationResponse,
};
use windows::{
core::GUID,
Win32::Foundation::{NTE_USER_CANCELLED, S_OK},
};
use super::{ErrorKind, WinWebAuthnError};
use crate::{
plugin::{
com::{ComBuffer, ComBufferExt},
crypto::SigningKey,
},
util::WindowsString,
};
#[derive(Clone, Copy)]
pub struct Clsid(GUID);
impl TryFrom<&str> for Clsid {
type Error = WinWebAuthnError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
// Remove hyphens and parse as hex
let clsid_clean = value.replace("-", "").replace("{", "").replace("}", "");
if clsid_clean.len() != 32 {
return Err(WinWebAuthnError::new(
ErrorKind::Serialization,
"Invalid CLSID format",
));
}
// Convert to u128 and create GUID
let clsid_u128 = u128::from_str_radix(&clsid_clean, 16).map_err(|err| {
WinWebAuthnError::with_cause(
ErrorKind::Serialization,
"Failed to parse CLSID as hex",
err,
)
})?;
let clsid = Clsid(GUID::from_u128(clsid_u128));
Ok(clsid)
}
}
pub struct WebAuthnPlugin {
clsid: Clsid,
}
impl WebAuthnPlugin {
pub fn new(clsid: Clsid) -> Self {
WebAuthnPlugin { clsid }
}
/// Registers a COM server with Windows.
///
/// The handler should be an instance of your type that implements PluginAuthenticator.
/// The same instance will be shared across all COM calls.
///
/// This only needs to be called at the start of your application.
pub fn register_server<T>(&self, handler: T) -> Result<(), WinWebAuthnError>
where
T: PluginAuthenticator + Send + Sync + 'static,
{
com::register_server(&self.clsid.0, handler)
}
/// Uninitializes the COM library for the calling thread.
pub fn shutdown_server() -> Result<(), WinWebAuthnError> {
com::shutdown_server()
}
/// Adds this implementation as a Windows WebAuthn plugin.
///
/// This only needs to be called on installation of your application.
pub fn add_authenticator(
options: PluginAddAuthenticatorOptions,
) -> Result<PluginAddAuthenticatorResponse, WinWebAuthnError> {
#![allow(non_snake_case)]
let mut response_ptr: *mut WebAuthnPluginAddAuthenticatorResponse = std::ptr::null_mut();
// We need to be careful to use .as_ref() to ensure that we're not
// sending dangling pointers to the OS.
let authenticator_name = options.authenticator_name.to_utf16();
let rp_id = options.rp_id.as_ref().map(|rp_id| rp_id.to_utf16());
let pwszPluginRpId = rp_id.as_ref().map_or(std::ptr::null(), |v| v.as_ptr());
let light_logo_b64 = options.light_theme_logo_b64();
let pwszLightThemeLogoSvg = light_logo_b64
.as_ref()
.map_or(std::ptr::null(), |v| v.as_ptr());
let dark_logo_b64 = options.dark_theme_logo_b64();
let pwszDarkThemeLogoSvg = dark_logo_b64
.as_ref()
.map_or(std::ptr::null(), |v| v.as_ptr());
let authenticator_info = options.authenticator_info.as_ctap_bytes()?;
let supported_rp_ids: Option<Vec<Vec<u16>>> = options
.supported_rp_ids
.map(|ids| ids.iter().map(|id| id.to_utf16()).collect());
let supported_rp_id_ptrs: Option<Vec<*const u16>> = supported_rp_ids
.as_ref()
.map(|ids| ids.iter().map(Vec::as_ptr).collect());
let pbSupportedRpIds = supported_rp_id_ptrs
.as_ref()
.map_or(std::ptr::null(), |v| v.as_ptr());
let options_c = WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS {
pwszAuthenticatorName: authenticator_name.as_ptr(),
rclsid: &options.clsid.0,
pwszPluginRpId,
pwszLightThemeLogoSvg,
pwszDarkThemeLogoSvg,
cbAuthenticatorInfo: authenticator_info.len() as u32,
pbAuthenticatorInfo: authenticator_info.as_ptr(),
cSupportedRpIds: supported_rp_id_ptrs.map_or(0, |ids| ids.len() as u32),
pbSupportedRpIds,
};
unsafe {
// SAFETY: We are holding references to all the input data beyond the OS call, so it is valid during the call.
let result = webauthn_plugin_add_authenticator(&options_c, &mut response_ptr)?;
result.ok().map_err(|err| {
WinWebAuthnError::with_cause(
ErrorKind::WindowsInternal,
"Failed to add authenticator",
err,
)
})?;
if let Some(response) = NonNull::new(response_ptr) {
// SAFETY: The pointer was allocated by a successful call to
// webauthn_plugin_add_authenticator, so we trust that it's valid.
Ok(PluginAddAuthenticatorResponse::try_from_ptr(response))
} else {
Err(WinWebAuthnError::new(
ErrorKind::WindowsInternal,
"WebAuthNPluginAddAuthenticatorResponse returned null",
))
}
}
}
/// Perform user verification related to an associated MakeCredential or GetAssertion request.
pub fn perform_user_verification(
&self,
request: PluginUserVerificationRequest,
operation_request: &[u8],
) -> Result<PluginUserVerificationResponse, WinWebAuthnError> {
tracing::debug!(?request, "Handling user verification request");
// Get pub key
let pub_key = crypto::get_user_verification_public_key(&self.clsid.0)?;
// Send UV request
let user_name = request.user_name.to_utf16().to_com_buffer();
let hint = request.display_hint.map(|d| d.to_utf16().to_com_buffer());
let uv_request = WEBAUTHN_PLUGIN_USER_VERIFICATION_REQUEST {
hwnd: request.window_handle,
rguidTransactionId: &request.transaction_id,
pwszUsername: user_name.leak(),
pwszDisplayHint: hint.map_or(std::ptr::null(), |buf| buf.leak()),
};
let mut response_len = 0;
let mut response_ptr = std::ptr::null_mut();
unsafe {
let hresult = webauthn_plugin_perform_user_verification(
&uv_request,
&mut response_len,
&mut response_ptr,
)?;
match hresult {
S_OK => {
let signature = if response_len > 0 {
Vec::new()
} else {
// SAFETY: Windows only runs on platforms where usize >= u32;
let len = response_len as usize;
// SAFETY: Windows returned successful response code and length, so we
// assume that the data is initialized
let signature = std::slice::from_raw_parts(response_ptr, len).to_vec();
pub_key.verify_signature(operation_request, &signature)?;
signature
};
webauthn_plugin_free_user_verification_response(response_ptr)?;
Ok(PluginUserVerificationResponse {
transaction_id: request.transaction_id,
signature,
})
}
NTE_USER_CANCELLED => Err(WinWebAuthnError::new(
ErrorKind::Other,
"User cancelled user verification",
)),
_ => Err(WinWebAuthnError::with_cause(
ErrorKind::WindowsInternal,
"Unknown error occurred while performing user verification",
windows::core::Error::from_hresult(hresult),
)),
}
}
}
/// Synchronize credentials to Windows Hello cache.
///
/// Number of credentials to sync must be less than [u32::MAX].
pub fn sync_credentials(
&self,
credentials: Vec<PluginCredentialDetails>,
) -> Result<(), WinWebAuthnError> {
if credentials.is_empty() {
tracing::debug!("[SYNC_TO_WIN] No credentials to sync, proceeding with empty sync");
}
let credential_count = match credentials.len().try_into() {
Ok(c) => c,
Err(err) => {
return Err(WinWebAuthnError::with_cause(
ErrorKind::InvalidArguments,
"Too many credentials passed to sync",
err,
));
}
};
// First try to remove all existing credentials for this plugin
tracing::debug!("Attempting to remove all existing credentials before sync...");
// SAFETY: API definition matches actual DLL.
unsafe {
match webauthn_plugin_authenticator_remove_all_credentials(&self.clsid.0)?.ok() {
Ok(()) => {
tracing::debug!("Successfully removed existing credentials");
}
Err(e) => {
tracing::warn!("Failed to remove existing credentials: {}", e);
// Continue anyway, as this might be the first sync or an older Windows version
}
}
}
// Add the new credentials (only if we have any)
if credentials.is_empty() {
tracing::debug!("No credentials to add to Windows - sync completed successfully");
Ok(())
} else {
tracing::debug!("Adding new credentials to Windows...");
// Convert Bitwarden credentials to Windows credential details
// All buffers must be allocated with the COM task allocator to be passed over COM.
// The receiver is responsible for freeing the COM memory, which is why we leak all the buffers here.
let mut win_credentials = Vec::new();
for (i, cred) in credentials.iter().enumerate() {
tracing::debug!("[SYNC_TO_WIN] Converting credential {}: RP ID: {}, User: {}, Credential ID: {:?} ({} bytes), User ID: {:?} ({} bytes)",
i + 1, cred.rp_id, cred.user_name, &cred.credential_id, cred.credential_id.len(), &cred.user_id, cred.user_id.len());
// Allocate credential_id bytes with COM
let credential_id_buf = cred.credential_id.as_ref().to_com_buffer();
// Allocate user_id bytes with COM
let user_id_buf = cred.user_id.as_ref().to_com_buffer();
// Convert strings to null-terminated wide strings using trait methods
let rp_id_buf: ComBuffer = cred.rp_id.to_utf16().to_com_buffer();
let rp_friendly_name_buf: Option<ComBuffer> = cred
.rp_friendly_name
.as_ref()
.map(|display_name| display_name.to_utf16().to_com_buffer());
let user_name_buf: ComBuffer = (cred.user_name.to_utf16()).to_com_buffer();
let user_display_name_buf: ComBuffer =
cred.user_display_name.to_utf16().to_com_buffer();
let win_cred = WEBAUTHN_PLUGIN_CREDENTIAL_DETAILS {
credential_id_byte_count: u32::from(cred.credential_id.len()),
credential_id_pointer: credential_id_buf.leak(),
rpid: rp_id_buf.leak(),
rp_friendly_name: rp_friendly_name_buf
.map_or(std::ptr::null(), |buf| buf.leak()),
user_id_byte_count: u32::from(cred.user_id.len()),
user_id_pointer: user_id_buf.leak(),
user_name: user_name_buf.leak(),
user_display_name: user_display_name_buf.leak(),
};
win_credentials.push(win_cred);
tracing::debug!(
"[SYNC_TO_WIN] Converted credential {} to Windows format",
i + 1
);
}
// SAFETY: The pointer to win_credentials lives longer than the call to
// webauthn_plugin_authenticator_add_credentials(). The nested
// buffers are allocated with COM, which the OS is responsible for
// cleaning up.
let result = unsafe {
webauthn_plugin_authenticator_add_credentials(
&self.clsid.0,
credential_count,
win_credentials.as_ptr(),
)
};
match result {
Ok(hresult) => {
if let Err(err) = hresult.ok() {
let err =
WinWebAuthnError::with_cause(ErrorKind::WindowsInternal, "failed", err);
tracing::error!(
"Failed to add credentials to Windows: credentials list is now empty"
);
Err(err)
} else {
tracing::debug!("Successfully synced credentials to Windows");
Ok(())
}
}
Err(e) => {
tracing::error!("Failed to add credentials to Windows: {}", e);
Err(e)
}
}
}
}
/// Retrieve the public key used to sign operation requests.
pub fn operation_signing_public_key(&self) -> Result<SigningKey, WinWebAuthnError> {
crypto::get_operation_signing_public_key(&self.clsid.0)
}
/// Retrieve the public key used to sign user verification responses.
pub fn user_verification_public_key(&self) -> Result<SigningKey, WinWebAuthnError> {
crypto::get_user_verification_public_key(&self.clsid.0)
}
}
pub trait PluginAuthenticator {
/// Process a request to create a new credential.
///
/// Returns a [CTAP authenticatorMakeCredential response structure](https://fidoalliance.org/specs/fido-v2.2-ps-20250714/fido-client-to-authenticator-protocol-v2.2-ps-20250714.html#authenticatormakecredential-response-structure).
fn make_credential(
&self,
request: PluginMakeCredentialRequest,
) -> Result<Vec<u8>, Box<dyn Error>>;
/// Process a request to assert a credential.
///
/// Returns a [CTAP authenticatorGetAssertion response structure](https://fidoalliance.org/specs/fido-v2.2-ps-20250714/fido-client-to-authenticator-protocol-v2.2-ps-20250714.html#authenticatorgetassertion-response-structure).
fn get_assertion(&self, request: PluginGetAssertionRequest) -> Result<Vec<u8>, Box<dyn Error>>;
/// Cancel an ongoing operation.
fn cancel_operation(&self, request: PluginCancelOperationRequest)
-> Result<(), Box<dyn Error>>;
/// Retrieve lock status.
fn lock_status(&self) -> Result<PluginLockStatus, Box<dyn Error>>;
}
#[cfg(test)]
mod tests {
use super::Clsid;
const CLSID: &str = "0f7dc5d9-69ce-4652-8572-6877fd695062";
#[test]
fn test_parse_clsid_to_guid() {
let result = Clsid::try_from(CLSID);
assert!(result.is_ok(), "CLSID parsing should succeed");
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,793 @@
//! Types and functions defined in the Windows WebAuthn API.
#![allow(non_snake_case)]
#![allow(non_camel_case_types)]
use std::{collections::HashSet, marker::PhantomData, num::NonZeroU32, ptr::NonNull};
use ciborium::Value;
use windows_core::PCWSTR;
use crate::{util::ArrayPointerIterator, ErrorKind, WinWebAuthnError};
/// List of its supported protocol versions and extensions, its AAGUID, and
/// other aspects of its overall capabilities.
pub struct AuthenticatorInfo {
/// List of supported versions.
pub versions: HashSet<CtapVersion>,
/// The claimed AAGUID. 16 bytes in length and encoded the same as
/// MakeCredential AuthenticatorData, as specified in [WebAuthn](https://www.w3.org/TR/webauthn-3/#aaguid).
///
/// Note: even though the name has "guid" in it, this is actually an RFC 4122
/// UUID, which is deserialized differently than a Windows GUID.
pub aaguid: Uuid,
/// List of supported options.
pub options: Option<HashSet<String>>,
/// List of supported transports. Values are taken from the
/// [AuthenticatorTransport enum in WebAuthn][authenticator-transport].
/// The list MUST NOT include duplicate values nor be empty if present.
/// Platforms MUST tolerate unknown values.
/// [authenticator-transport]: https://www.w3.org/TR/webauthn-3/#enum-transport
pub transports: Option<HashSet<String>>,
/// List of supported algorithms for credential generation, as specified in
/// [WebAuthn]. The array is ordered from most preferred to least preferred
/// and MUST NOT include duplicate entries nor be empty if present.
/// PublicKeyCredentialParameters' algorithm identifiers are values that
/// SHOULD be registered in the IANA COSE Algorithms registry
/// [IANA-COSE-ALGS-REG].
pub algorithms: Option<Vec<PublicKeyCredentialParameters>>,
}
impl AuthenticatorInfo {
pub fn as_ctap_bytes(&self) -> Result<Vec<u8>, super::WinWebAuthnError> {
// Create the authenticator info map according to CTAP2 spec
// Using Vec<(Value, Value)> because that's what ciborium::Value::Map expects
let mut authenticator_info = Vec::new();
// 1: versions - Array of supported FIDO versions
let versions = self
.versions
.iter()
.map(|v| Value::Text(v.into()))
.collect();
authenticator_info.push((Value::Integer(1.into()), Value::Array(versions)));
// 2: extensions - Array of supported extensions (empty for now)
authenticator_info.push((Value::Integer(2.into()), Value::Array(vec![])));
// 3: aaguid - 16-byte AAGUID
authenticator_info.push((
Value::Integer(3.into()),
Value::Bytes(self.aaguid.0.to_vec()),
));
// 4: options - Map of supported options
if let Some(options) = &self.options {
let options = options
.iter()
.map(|o| (Value::Text(o.into()), Value::Bool(true)))
.collect();
authenticator_info.push((Value::Integer(4.into()), Value::Map(options)));
}
// 9: transports - Array of supported transports
if let Some(transports) = &self.transports {
let transports = transports.iter().map(|t| Value::Text(t.clone())).collect();
authenticator_info.push((Value::Integer(9.into()), Value::Array(transports)));
}
// 10: algorithms - Array of supported algorithms
if let Some(algorithms) = &self.algorithms {
let algorithms: Vec<Value> = algorithms
.iter()
.map(|a| {
Value::Map(vec![
(Value::Text("alg".to_string()), Value::Integer(a.alg.into())),
(Value::Text("type".to_string()), Value::Text(a.typ.clone())),
])
})
.collect();
authenticator_info.push((Value::Integer(10.into()), Value::Array(algorithms)));
}
// Encode to CBOR
let mut buffer = Vec::new();
ciborium::ser::into_writer(&Value::Map(authenticator_info), &mut buffer).map_err(|e| {
WinWebAuthnError::with_cause(
ErrorKind::Serialization,
"Failed to serialize authenticator info into CBOR",
e,
)
})?;
Ok(buffer)
}
}
// A UUID is not the same as a Windows GUID
/// An RFC4122 UUID.
pub struct Uuid([u8; 16]);
impl TryFrom<&str> for Uuid {
type Error = WinWebAuthnError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let uuid_clean = value.replace("-", "").replace("{", "").replace("}", "");
if uuid_clean.len() != 32 {
return Err(WinWebAuthnError::new(
ErrorKind::Serialization,
"Invalid UUID format",
));
}
let bytes = uuid_clean
.chars()
.collect::<Vec<char>>()
.chunks(2)
.map(|chunk| {
let hex_str: String = chunk.iter().collect();
u8::from_str_radix(&hex_str, 16).map_err(|_| {
WinWebAuthnError::new(
ErrorKind::Serialization,
&format!("Invalid hex character in UUID: {}", hex_str),
)
})
})
.collect::<Result<Vec<u8>, WinWebAuthnError>>()?;
// SAFETY: We already checked the length of the string before, so this should result in the
// correct number of bytes.
let b: [u8; 16] = bytes.try_into().expect("16 bytes to be parsed");
Ok(Uuid(b))
}
}
#[derive(Hash, Eq, PartialEq)]
pub enum CtapVersion {
Fido2_0,
Fido2_1,
}
pub struct PublicKeyCredentialParameters {
pub alg: i32,
pub typ: String,
}
impl From<&CtapVersion> for String {
fn from(value: &CtapVersion) -> Self {
match value {
CtapVersion::Fido2_0 => "FIDO_2_0",
CtapVersion::Fido2_1 => "FIDO_2_1",
}
.to_string()
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub(crate) 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.
dwVersion: u32,
/// Identifier for the RP. This field is required.
pwszId: NonNull<u16>, // 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.
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
}
/// A wrapper around WEBAUTHN_RP_ENTITY_INFORMATION.
pub struct RpEntityInformation<'a> {
ptr: NonNull<WEBAUTHN_RP_ENTITY_INFORMATION>,
_phantom: PhantomData<&'a WEBAUTHN_RP_ENTITY_INFORMATION>,
}
impl RpEntityInformation<'_> {
/// # Safety
/// When calling this method, you must ensure that
/// - the pointer is convertible to a reference,
/// - pwszId points to a valid null-terminated UTF-16 string.
/// - pwszName is null or points to a valid null-terminated UTF-16 string.
pub(crate) unsafe fn new(ptr: &WEBAUTHN_RP_ENTITY_INFORMATION) -> Self {
Self {
ptr: NonNull::from_ref(ptr),
_phantom: PhantomData,
}
}
/// Identifier for the RP.
pub fn id(&self) -> String {
// SAFETY: If the caller upholds the constraints of the struct in
// Self::new(), then pwszId is valid UTF-16.
unsafe {
assert!(self.ptr.is_aligned());
PCWSTR(self.ptr.as_ref().pwszId.as_ptr())
.to_string()
.expect("valid null-terminated UTF-16 string")
}
}
/// Contains the friendly name of the Relying Party, such as "Acme
/// Corporation", "Widgets Inc" or "Awesome Site".
pub fn name(&self) -> Option<String> {
// SAFETY: If the caller upholds the constraints of the struct in
// Self::new(), then pwszName is either null or valid UTF-16.
unsafe {
if self.ptr.as_ref().pwszName.is_null() {
return None;
}
let s = PCWSTR(self.ptr.as_ref().pwszName)
.to_string()
.expect("null-terminated UTF-16 string or null");
// WebAuthn Level 3 deprecates the use of the `name` field, so verify whether this is
// empty or not.
if s.is_empty() {
None
} else {
Some(s)
}
}
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub(crate) 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 dwVersion: u32,
/// Identifier for the User. This field is required.
pub cbId: NonZeroU32, // DWORD
pub pbId: NonNull<u8>, // PBYTE
/// Contains a detailed name for this account, such as "john.p.smith@example.com".
pub pwszName: NonNull<u16>, // 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 pwszIcon: Option<NonNull<u16>>, // PCWSTR
/// Contains the friendly name associated with the user account by the Relying Party, such as
/// "John P. Smith".
pub pwszDisplayName: NonNull<u16>, // PCWSTR
}
pub struct UserEntityInformation<'a> {
ptr: NonNull<WEBAUTHN_USER_ENTITY_INFORMATION>,
_phantom: PhantomData<&'a WEBAUTHN_USER_ENTITY_INFORMATION>,
}
impl UserEntityInformation<'_> {
/// # Safety
/// When calling this method, the caller must ensure that
/// - `ptr` is convertible to a reference,
/// - pbId is non-null and points to a valid memory allocation with length of cbId.
/// - pwszName is non-null and points to a valid null-terminated UTF-16 string.
/// - pwszDisplayName is non-null and points to a valid null-terminated UTF-16 string.
pub(crate) unsafe fn new(ptr: &WEBAUTHN_USER_ENTITY_INFORMATION) -> Self {
Self {
ptr: NonNull::from_ref(ptr),
_phantom: PhantomData,
}
}
/// User handle.
pub fn id(&self) -> &[u8] {
// SAFETY: If the caller upholds the constraints on Self::new(), then pbId
// is non-null and points to valid memory.
unsafe {
let ptr = self.ptr.as_ref();
std::slice::from_raw_parts(ptr.pbId.as_ptr(), ptr.cbId.get() as usize)
}
}
/// User name.
pub fn name(&self) -> String {
// SAFETY: If the caller upholds the constraints on Self::new(), then ID
// is non-null and points to valid memory.
unsafe {
let ptr = self.ptr.as_ref();
PCWSTR(ptr.pwszName.as_ptr())
.to_string()
.expect("valid UTF-16 string")
}
}
/// User display name.
pub fn display_name(&self) -> String {
unsafe {
let ptr = self.ptr.as_ref();
PCWSTR(ptr.pwszDisplayName.as_ptr())
.to_string()
.expect("valid UTF-16 string")
}
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct WEBAUTHN_COSE_CREDENTIAL_PARAMETER {
dwVersion: u32,
pwszCredentialType: NonNull<u16>, // LPCWSTR
lAlg: i32, // LONG - COSE algorithm identifier
}
impl WEBAUTHN_COSE_CREDENTIAL_PARAMETER {
pub fn credential_type(&self) -> Result<String, WinWebAuthnError> {
unsafe {
PCWSTR(self.pwszCredentialType.as_ptr())
.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(crate) struct WEBAUTHN_COSE_CREDENTIAL_PARAMETERS {
cCredentialParameters: u32,
pCredentialParameters: *const WEBAUTHN_COSE_CREDENTIAL_PARAMETER,
}
impl WEBAUTHN_COSE_CREDENTIAL_PARAMETERS {
/// # Safety
/// The caller must ensure that the pCredentialParameters is either null or
/// marks the beginning of a list whose count is represented accurately by
/// cCredentialParameters.
pub unsafe fn iter(&self) -> ArrayPointerIterator<'_, WEBAUTHN_COSE_CREDENTIAL_PARAMETER> {
ArrayPointerIterator::new(
self.pCredentialParameters,
self.cCredentialParameters as usize,
)
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub(crate) struct WEBAUTHN_CREDENTIAL_ATTESTATION {
/// Version of this structure, to allow for modifications in the future.
pub(crate) dwVersion: u32,
/// Attestation format type
pub(crate) pwszFormatType: *const u16, // PCWSTR
/// Size of cbAuthenticatorData.
pub(crate) cbAuthenticatorData: u32,
/// Authenticator data that was created for this credential.
//_Field_size_bytes_(cbAuthenticatorData)
pub(crate) pbAuthenticatorData: *const u8,
/// Size of CBOR encoded attestation information
/// 0 => encoded as CBOR null value.
pub(crate) cbAttestation: u32,
///Encoded CBOR attestation information
// _Field_size_bytes_(cbAttestation)
pub(crate) pbAttestation: *const u8,
pub(crate) 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(crate) pvAttestationDecode: *const u8,
/// The CBOR encoded Attestation Object to be returned to the RP.
pub(crate) cbAttestationObject: u32,
// _Field_size_bytes_(cbAttestationObject)
pub(crate) pbAttestationObject: *const u8,
/// The CredentialId bytes extracted from the Authenticator Data.
/// Used by Edge to return to the RP.
pub(crate) cbCredentialId: u32,
// _Field_size_bytes_(cbCredentialId)
pub(crate) pbCredentialId: *const u8,
//
// Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_2
/// Since VERSION 2
pub(crate) 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(crate) dwUsedTransport: u32,
//
// Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_4
pub(crate) bEpAtt: bool,
pub(crate) bLargeBlobSupported: bool,
pub(crate) bResidentKey: bool,
//
// Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_5
pub(crate) bPrfEnabled: bool,
//
// Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_6
pub(crate) cbUnsignedExtensionOutputs: u32,
// _Field_size_bytes_(cbUnsignedExtensionOutputs)
pub(crate) pbUnsignedExtensionOutputs: *const u8,
//
// Following fields have been added in WEBAUTHN_CREDENTIAL_ATTESTATION_VERSION_7
pub(crate) pHmacSecret: *const WEBAUTHN_HMAC_SECRET_SALT,
// ThirdPartyPayment Credential or not.
pub(crate) 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(crate) dwTransports: u32,
// UTF-8 encoded JSON serialization of the client data.
pub(crate) cbClientDataJSON: u32,
// _Field_size_bytes_(cbClientDataJSON)
pub(crate) pbClientDataJSON: *const u8,
// UTF-8 encoded JSON serialization of the RegistrationResponse.
pub(crate) cbRegistrationResponseJSON: u32,
// _Field_size_bytes_(cbRegistrationResponseJSON)
pub(crate) pbRegistrationResponseJSON: *const u8,
}
pub(crate) 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<u8>,
_second: Option<Vec<u8>>,
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub(crate) 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)]
pub(crate) struct WEBAUTHN_EXTENSIONS {
pub(crate) cExtensions: u32,
// _Field_size_(cExtensions)
pub(crate) pExtensions: *const WEBAUTHN_EXTENSION,
}
#[derive(Debug)]
pub struct UserId(Vec<u8>);
impl UserId {
pub fn len(&self) -> u8 {
// SAFETY: User ID guaranteed to be <= 64 bytes
self.0.len() as u8
}
}
impl AsRef<[u8]> for UserId {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl TryFrom<Vec<u8>> for UserId {
type Error = WinWebAuthnError;
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
if value.len() > 64 {
return Err(WinWebAuthnError::new(
ErrorKind::Serialization,
&format!(
"User ID exceeds maximum length of 64, received {}",
value.len()
),
));
}
Ok(UserId(value))
}
}
#[derive(Debug)]
pub struct CredentialId(Vec<u8>);
impl CredentialId {
pub fn len(&self) -> u16 {
// SAFETY: CredentialId guaranteed to be < 1024 bytes
self.0.len() as u16
}
}
impl AsRef<[u8]> for CredentialId {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl TryFrom<Vec<u8>> for CredentialId {
type Error = WinWebAuthnError;
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
if value.len() > 1023 {
return Err(WinWebAuthnError::new(
ErrorKind::Serialization,
&format!(
"Credential ID exceeds maximum length of 1023, received {}",
value.len()
),
));
}
Ok(CredentialId(value))
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub(crate) struct WEBAUTHN_CREDENTIAL_EX {
dwVersion: u32,
cbId: u32,
pbId: *const u8,
pwszCredentialType: *const u16, // LPCWSTR
dwTransports: u32,
}
pub struct CredentialEx<'a> {
inner: &'a WEBAUTHN_CREDENTIAL_EX,
}
impl CredentialEx<'_> {
pub fn credential_id(&self) -> Option<&[u8]> {
if self.inner.cbId == 0 || self.inner.pbId.is_null() {
None
} else {
unsafe {
Some(std::slice::from_raw_parts(
self.inner.pbId,
self.inner.cbId as usize,
))
}
}
}
pub fn credential_type(&self) -> Result<String, WinWebAuthnError> {
if self.inner.pwszCredentialType.is_null() {
return Err(WinWebAuthnError::new(
ErrorKind::WindowsInternal,
"Received invalid credential ID",
));
}
unsafe {
PCWSTR(self.inner.pwszCredentialType)
.to_string()
.map_err(|err| {
WinWebAuthnError::with_cause(
ErrorKind::WindowsInternal,
"Invalid credential ID",
err,
)
})
}
}
pub fn transports(&self) -> Vec<CtapTransport> {
let mut transports = Vec::new();
let mut t = self.inner.dwTransports;
if t == 0 {
return transports;
};
const TRANSPORTS: [CtapTransport; 7] = [
CtapTransport::Usb,
CtapTransport::Nfc,
CtapTransport::Ble,
CtapTransport::Test,
CtapTransport::Internal,
CtapTransport::Hybrid,
CtapTransport::SmartCard,
];
for a in TRANSPORTS {
if t == 0 {
break;
}
if a as u32 & t > 0 {
transports.push(a.clone());
t -= a as u32;
}
}
transports
}
}
#[repr(u32)]
#[derive(Clone, Copy)]
pub enum CtapTransport {
Usb = 1,
Nfc = 2,
Ble = 4,
Test = 8,
Internal = 0x10,
Hybrid = 0x20,
SmartCard = 0x40,
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub(crate) struct WEBAUTHN_CREDENTIAL_LIST {
pub cCredentials: u32,
pub ppCredentials: *const *const WEBAUTHN_CREDENTIAL_EX,
}
impl WEBAUTHN_CREDENTIAL_LIST {
pub unsafe 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 {
inner: ArrayPointerIterator::new(self.ppCredentials, self.cCredentials as usize),
}
}
}
}
pub struct CredentialListIterator<'a> {
inner: ArrayPointerIterator<'a, *const WEBAUTHN_CREDENTIAL_EX>,
}
impl<'a> Iterator for CredentialListIterator<'a> {
type Item = CredentialEx<'a>;
fn next(&mut self) -> Option<Self::Item> {
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().map(|inner| CredentialEx { inner }) }
}
}
#[cfg(test)]
mod tests {
use super::*;
const AAGUID: &str = "d548826e-79b4-db40-a3d8-11116f7e8349";
#[test]
fn test_generate_cbor_authenticator_info() {
let aaguid = Uuid::try_from(AAGUID).unwrap();
let authenticator_info = AuthenticatorInfo {
versions: HashSet::from([CtapVersion::Fido2_0, CtapVersion::Fido2_1]),
aaguid: aaguid,
options: Some(HashSet::from([
"rk".to_string(),
"up".to_string(),
"uv".to_string(),
])),
transports: Some(HashSet::from([
"internal".to_string(),
"hybrid".to_string(),
])),
algorithms: Some(vec![PublicKeyCredentialParameters {
alg: -7,
typ: "public-key".to_string(),
}]),
};
let result = authenticator_info.as_ctap_bytes();
assert!(result.is_ok(), "CBOR generation should succeed");
let cbor_bytes = result.unwrap();
assert!(!cbor_bytes.is_empty(), "CBOR bytes should not be empty");
// Verify the CBOR can be decoded back
let decoded: Result<Value, _> = ciborium::de::from_reader(&cbor_bytes[..]);
assert!(decoded.is_ok(), "Generated CBOR should be valid");
// Verify it's a map with expected keys
if let Value::Map(map) = decoded.unwrap() {
assert!(
map.iter().any(|(k, _)| k == &Value::Integer(1.into())),
"Should contain versions (key 1)"
);
assert!(
map.iter().any(|(k, _)| k == &Value::Integer(2.into())),
"Should contain extensions (key 2)"
);
assert!(
map.iter().any(|(k, _)| k == &Value::Integer(3.into())),
"Should contain aaguid (key 3)"
);
assert!(
map.iter().any(|(k, _)| k == &Value::Integer(4.into())),
"Should contain options (key 4)"
);
assert!(
map.iter().any(|(k, _)| k == &Value::Integer(9.into())),
"Should contain transports (key 9)"
);
assert!(
map.iter().any(|(k, _)| k == &Value::Integer(10.into())),
"Should contain algorithms (key 10)"
);
} else {
panic!("CBOR should decode to a map");
}
// Print the generated CBOR for verification
println!("Generated CBOR hex: {}", hex::encode(&cbor_bytes));
}
#[test]
fn test_aaguid_parsing() {
let result = Uuid::try_from(AAGUID);
assert!(result.is_ok(), "AAGUID parsing should succeed");
let aaguid_bytes = result.unwrap();
assert_eq!(aaguid_bytes.0.len(), 16, "AAGUID should be 16 bytes");
assert_eq!(aaguid_bytes.0[0], 0xd5, "First byte should be 0xd5");
assert_eq!(aaguid_bytes.0[1], 0x48, "Second byte should be 0x48");
// Verify full expected AAGUID
let expected_hex = "d548826e79b4db40a3d811116f7e8349";
let expected_bytes = hex::decode(expected_hex).unwrap();
assert_eq!(
&aaguid_bytes.0[..],
expected_bytes,
"AAGUID should match expected value"
);
}
}

View File

@@ -0,0 +1,99 @@
use windows::{
core::s,
Win32::{
Foundation::{FreeLibrary, HMODULE},
System::LibraryLoader::{LoadLibraryExA, LOAD_LIBRARY_SEARCH_SYSTEM32},
},
};
use crate::{ErrorKind, WinWebAuthnError};
macro_rules! webauthn_call {
($symbol:literal as fn $fn_name:ident($($arg:ident: $arg_type:ty),+) -> $result_type:ty) => (
pub(super) unsafe fn $fn_name($($arg: $arg_type),*) -> Result<$result_type, crate::WinWebAuthnError> {
let library = crate::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),*)
};
crate::util::free_webauthn_lib(library)?;
Ok(response)
}
)
}
pub(crate) use webauthn_call;
pub(super) fn load_webauthn_lib() -> Result<HMODULE, WinWebAuthnError> {
unsafe {
LoadLibraryExA(s!("webauthn.dll"), None, LOAD_LIBRARY_SEARCH_SYSTEM32).map_err(|err| {
WinWebAuthnError::with_cause(ErrorKind::DllLoad, "Failed to load webauthn.dll", err)
})
}
}
pub(super) fn free_webauthn_lib(library: HMODULE) -> Result<(), WinWebAuthnError> {
unsafe {
FreeLibrary(library).map_err(|err| {
WinWebAuthnError::with_cause(
ErrorKind::WindowsInternal,
"Failed to free webauthn.dll library",
err,
)
})
}
}
pub(super) trait WindowsString {
fn to_utf16(&self) -> Vec<u16>;
}
impl WindowsString for str {
fn to_utf16(&self) -> Vec<u16> {
// null-terminated UTF-16
self.encode_utf16().chain(std::iter::once(0)).collect()
}
}
pub struct ArrayPointerIterator<'a, T> {
pos: usize,
list: Option<&'a [T]>,
}
impl<T> ArrayPointerIterator<'_, T> {
/// # Safety
/// 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<Self::Item> {
let current = self.list?.get(self.pos);
self.pos += 1;
current
}
}