use std::{marker::PhantomData, time::Duration};
use passkey_crypto::{CryptoBackend, PublicKeyT, SecretKeyT, coset::Algorithm, iana::EnumI64};
use passkey_types::{
Bytes,
ctap2::{AuthenticatorData, Ctap2Error},
encoding,
webauthn::{
self, AttestationConveyancePreference, AuthenticationExtensionsClientOutputs,
AuthenticatorAttachment, AuthenticatorTransport, CredentialPropertiesOutput,
ResidentKeyRequirement, UserVerificationRequirement,
},
};
use serde::Serialize;
use crate::{ClientData, Fetcher, Origin, RpIdVerifier, WebauthnError};
use windows_sys::Win32::{
Networking::WindowsWebServices::{
WEBAUTHN_API_VERSION_1, WEBAUTHN_ASSERTION,
WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT,
WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT,
WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE, WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY,
WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM,
WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM, WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS,
WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_4,
WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS,
WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_3, WEBAUTHN_CLIENT_DATA,
WEBAUTHN_CLIENT_DATA_CURRENT_VERSION, WEBAUTHN_COSE_CREDENTIAL_PARAMETER,
WEBAUTHN_COSE_CREDENTIAL_PARAMETER_CURRENT_VERSION, WEBAUTHN_COSE_CREDENTIAL_PARAMETERS,
WEBAUTHN_CREDENTIAL_ATTESTATION, WEBAUTHN_CREDENTIAL_EX,
WEBAUTHN_CREDENTIAL_EX_CURRENT_VERSION, WEBAUTHN_CREDENTIAL_LIST,
WEBAUTHN_CREDENTIAL_TYPE_PUBLIC_KEY, WEBAUTHN_CTAP_TRANSPORT_BLE,
WEBAUTHN_CTAP_TRANSPORT_INTERNAL, WEBAUTHN_CTAP_TRANSPORT_NFC, WEBAUTHN_CTAP_TRANSPORT_USB,
WEBAUTHN_HASH_ALGORITHM_SHA_256, WEBAUTHN_RP_ENTITY_INFORMATION,
WEBAUTHN_RP_ENTITY_INFORMATION_CURRENT_VERSION, WEBAUTHN_USER_ENTITY_INFORMATION,
WEBAUTHN_USER_ENTITY_INFORMATION_CURRENT_VERSION,
WEBAUTHN_USER_VERIFICATION_REQUIREMENT_DISCOURAGED,
WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED,
WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED, WebAuthNAuthenticatorGetAssertion,
WebAuthNAuthenticatorMakeCredential, WebAuthNCancelCurrentOperation, WebAuthNFreeAssertion,
WebAuthNFreeCredentialAttestation, WebAuthNGetApiVersionNumber, WebAuthNGetCancellationId,
WebAuthNGetErrorName,
},
UI::WindowsAndMessaging::GetForegroundWindow,
};
use windows_sys::core::{GUID, HRESULT, PCWSTR};
fn check_webauthn_version() -> Result<(), WebauthnError> {
let supported = unsafe { WebAuthNGetApiVersionNumber() } >= WEBAUTHN_API_VERSION_1;
if !supported {
return Err(WebauthnError::NotSupportedError);
}
Ok(())
}
fn to_utf16_null_terminated(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}
unsafe fn pcwstr_to_string(p: PCWSTR) -> String {
let mut len = 0usize;
while unsafe { *p.add(len) } != 0 {
len += 1;
}
let slice = unsafe { std::slice::from_raw_parts(p, len) };
String::from_utf16_lossy(slice)
}
fn get_cancellation_id() -> Result<GUID, WebauthnError> {
let mut id: GUID = unsafe { std::mem::zeroed() };
let hr = unsafe { WebAuthNGetCancellationId(&mut id) };
if hr < 0 {
return Err(WebauthnError::AuthenticatorError(Ctap2Error::Other.into()));
}
Ok(id)
}
fn win_api_ctap_transport_mask_to_transports(flags: u32) -> Vec<AuthenticatorTransport> {
let mut transports = Vec::new();
if flags & WEBAUTHN_CTAP_TRANSPORT_USB != 0 {
transports.push(AuthenticatorTransport::Usb);
}
if flags & WEBAUTHN_CTAP_TRANSPORT_NFC != 0 {
transports.push(AuthenticatorTransport::Nfc);
}
if flags & WEBAUTHN_CTAP_TRANSPORT_BLE != 0 {
transports.push(AuthenticatorTransport::Ble);
}
if flags & WEBAUTHN_CTAP_TRANSPORT_INTERNAL != 0 {
transports.push(AuthenticatorTransport::Internal);
}
transports
}
fn transports_to_win_api_ctap_mask(transports: &[AuthenticatorTransport]) -> u32 {
let mut mask = 0u32;
for t in transports {
mask |= match t {
AuthenticatorTransport::Usb => WEBAUTHN_CTAP_TRANSPORT_USB,
AuthenticatorTransport::Nfc => WEBAUTHN_CTAP_TRANSPORT_NFC,
AuthenticatorTransport::Ble => WEBAUTHN_CTAP_TRANSPORT_BLE,
AuthenticatorTransport::Internal => WEBAUTHN_CTAP_TRANSPORT_INTERNAL,
AuthenticatorTransport::Hybrid => 0,
};
}
mask
}
fn win_attachment(a: Option<AuthenticatorAttachment>) -> u32 {
match a {
None => WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY,
Some(AuthenticatorAttachment::Platform) => WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM,
Some(AuthenticatorAttachment::CrossPlatform) => {
WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM
}
}
}
fn win_uv(uv: UserVerificationRequirement) -> u32 {
match uv {
UserVerificationRequirement::Required => WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED,
UserVerificationRequirement::Preferred => WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED,
UserVerificationRequirement::Discouraged => {
WEBAUTHN_USER_VERIFICATION_REQUIREMENT_DISCOURAGED
}
}
}
fn win_attestation_conveyance(a: AttestationConveyancePreference) -> u32 {
match a {
AttestationConveyancePreference::None => WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
AttestationConveyancePreference::Indirect => {
WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT
}
AttestationConveyancePreference::Direct | AttestationConveyancePreference::Enterprise => {
WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT
}
}
}
fn win_require_resident_key(sel: Option<&webauthn::AuthenticatorSelectionCriteria>) -> bool {
match sel.and_then(|s| s.resident_key) {
Some(ResidentKeyRequirement::Required) => true,
Some(ResidentKeyRequirement::Preferred) | Some(ResidentKeyRequirement::Discouraged) => {
false
}
None => sel.map(|s| s.require_resident_key).unwrap_or(false),
}
}
struct WinCredentialList {
list: WEBAUTHN_CREDENTIAL_LIST,
_ptrs: Vec<*mut WEBAUTHN_CREDENTIAL_EX>,
_entries: Vec<WEBAUTHN_CREDENTIAL_EX>,
_id_bufs: Vec<Vec<u8>>,
}
impl WinCredentialList {
fn from_descriptors(descriptors: &[webauthn::PublicKeyCredentialDescriptor]) -> Self {
let mut id_bufs: Vec<Vec<u8>> = descriptors.iter().map(|d| d.id.to_vec()).collect();
let mut entries: Vec<WEBAUTHN_CREDENTIAL_EX> = id_bufs
.iter_mut()
.zip(descriptors.iter())
.map(|(id_buf, d)| WEBAUTHN_CREDENTIAL_EX {
dwVersion: WEBAUTHN_CREDENTIAL_EX_CURRENT_VERSION,
cbId: u32::try_from(id_buf.len()).expect("credential IDs are at most 1023 bytes"),
pbId: id_buf.as_mut_ptr(),
pwszCredentialType: WEBAUTHN_CREDENTIAL_TYPE_PUBLIC_KEY,
dwTransports: d
.transports
.as_deref()
.map(transports_to_win_api_ctap_mask)
.unwrap_or(0),
})
.collect();
let mut ptrs: Vec<*mut WEBAUTHN_CREDENTIAL_EX> =
entries.iter_mut().map(std::ptr::from_mut).collect();
let list = WEBAUTHN_CREDENTIAL_LIST {
cCredentials: u32::try_from(ptrs.len())
.expect("allow/exclude credential list length is bounded by the request payload"),
ppCredentials: ptrs.as_mut_ptr(),
};
Self {
list,
_ptrs: ptrs,
_entries: entries,
_id_bufs: id_bufs,
}
}
fn as_ptr(&mut self) -> *mut WEBAUTHN_CREDENTIAL_LIST {
if self.list.cCredentials > 0 {
std::ptr::from_mut(&mut self.list)
} else {
std::ptr::null_mut()
}
}
}
fn win_api_error_to_webauthn_error(hr: HRESULT) -> WebauthnError {
let err_string = unsafe { pcwstr_to_string(WebAuthNGetErrorName(hr)) };
match err_string.as_ref() {
"InvalidStateError" => {
WebauthnError::AuthenticatorError(Ctap2Error::CredentialExcluded.into())
}
"ConstraintError" => {
WebauthnError::AuthenticatorError(Ctap2Error::UnsupportedOption.into())
}
"NotSupportedError" => WebauthnError::NotSupportedError,
"NotAllowedError" => WebauthnError::AuthenticatorError(Ctap2Error::OperationDenied.into()),
"UnknownError" => WebauthnError::AuthenticatorError(Ctap2Error::Other.into()),
"Success" => WebauthnError::AuthenticatorError(Ctap2Error::Ok.into()),
_ => WebauthnError::AuthenticatorError(Ctap2Error::Other.into()),
}
}
pub struct WindowsClient<C, P, F>
where
C: CryptoBackend,
P: public_suffix::EffectiveTLDProvider + Sync + 'static,
F: Fetcher + Sync,
{
rp_id_verifier: RpIdVerifier<P, F>,
crypto: PhantomData<C>,
}
impl<C: CryptoBackend> Default for WindowsClient<C, public_suffix::PublicSuffixList, ()> {
fn default() -> Self {
Self::new()
}
}
impl<C: CryptoBackend> WindowsClient<C, public_suffix::PublicSuffixList, ()> {
pub fn new() -> Self {
Self {
rp_id_verifier: RpIdVerifier::new(public_suffix::DEFAULT_PROVIDER, None),
crypto: PhantomData,
}
}
pub async fn register<D: ClientData<E>, E: Serialize + Clone>(
&mut self,
origin: impl Into<Origin<'_>>,
request: webauthn::CredentialCreationOptions,
client_data: D,
) -> Result<webauthn::CreatedPublicKeyCredential, WebauthnError> {
check_webauthn_version()?;
let origin = origin.into();
let mut request = request.public_key;
let cred_props_requested =
request.extensions.as_ref().and_then(|ext| ext.cred_props) == Some(true);
let rp_id = self
.rp_id_verifier
.assert_domain(&origin, request.rp.id.as_deref())
.await?;
let rp_id_wide = to_utf16_null_terminated(rp_id);
let rp_name_wide = to_utf16_null_terminated(&request.rp.name);
let user_name_wide = to_utf16_null_terminated(&request.user.name);
let user_display_name_wide = to_utf16_null_terminated(&request.user.display_name);
let timeout = request.timeout.unwrap_or(
Duration::from_secs(120)
.as_millis()
.try_into()
.expect("120_000 (120s in ms) fits in u32"),
);
let cancellation_id = get_cancellation_id()?;
let rp_info = WEBAUTHN_RP_ENTITY_INFORMATION {
dwVersion: WEBAUTHN_RP_ENTITY_INFORMATION_CURRENT_VERSION,
pwszId: rp_id_wide.as_ptr(),
pwszName: rp_name_wide.as_ptr(),
pwszIcon: std::ptr::null(),
};
let user_info = WEBAUTHN_USER_ENTITY_INFORMATION {
dwVersion: WEBAUTHN_USER_ENTITY_INFORMATION_CURRENT_VERSION,
cbId: u32::try_from(request.user.id.len())
.expect("WebAuthn spec caps user id at 64 bytes"),
pbId: request.user.id.as_mut_ptr(),
pwszName: user_name_wide.as_ptr(),
pwszIcon: std::ptr::null(),
pwszDisplayName: user_display_name_wide.as_ptr(),
};
let collected_client_data = webauthn::CollectedClientData::<E> {
ty: webauthn::ClientDataType::Get,
challenge: encoding::base64url(&request.challenge),
origin: origin.to_string(),
cross_origin: None, extra_data: client_data.extra_client_data(),
unknown_keys: Default::default(),
};
let mut client_data_json = serde_json::to_string(&collected_client_data)
.map_err(|_| WebauthnError::SerializationError)?;
let client_data = WEBAUTHN_CLIENT_DATA {
dwVersion: WEBAUTHN_CLIENT_DATA_CURRENT_VERSION,
cbClientDataJSON: u32::try_from(client_data_json.len())
.expect("serialized client data JSON will never be u32::MAX bytes in practice"),
pbClientDataJSON: client_data_json.as_mut_ptr(),
pwszHashAlgId: WEBAUTHN_HASH_ALGORITHM_SHA_256,
};
let mut credential_params_vec = request
.pub_key_cred_params
.iter()
.map(|e| {
WEBAUTHN_COSE_CREDENTIAL_PARAMETER {
dwVersion: WEBAUTHN_COSE_CREDENTIAL_PARAMETER_CURRENT_VERSION,
pwszCredentialType: WEBAUTHN_CREDENTIAL_TYPE_PUBLIC_KEY,
lAlg: i32::try_from(e.alg.to_i64())
.expect("IANA-registered COSE algorithm identifiers all fit in i32"),
}
})
.collect::<Vec<_>>();
let credential_params = WEBAUTHN_COSE_CREDENTIAL_PARAMETERS {
cCredentialParameters: u32::try_from(credential_params_vec.len())
.expect("pub_key_cred_params list length is bounded by the request payload"),
pCredentialParameters: credential_params_vec.as_mut_ptr(),
};
let sel = request.authenticator_selection.as_ref();
let require_rk = win_require_resident_key(sel);
let uv = win_uv(sel.map(|s| s.user_verification).unwrap_or_default());
let mut exclude_list = WinCredentialList::from_descriptors(
request.exclude_credentials.as_deref().unwrap_or(&[]),
);
let make_credential_options = WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS {
dwVersion: WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_3,
dwTimeoutMilliseconds: request.timeout.unwrap_or(0),
dwAuthenticatorAttachment: win_attachment(sel.and_then(|s| s.authenticator_attachment)),
bRequireResidentKey: require_rk.into(),
dwUserVerificationRequirement: uv,
dwAttestationConveyancePreference: win_attestation_conveyance(request.attestation),
pExcludeCredentialList: exclude_list.as_ptr(),
..unsafe { std::mem::zeroed() }
};
std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(timeout.into()));
unsafe { WebAuthNCancelCurrentOperation(&cancellation_id) };
});
let mut attestation: *mut WEBAUTHN_CREDENTIAL_ATTESTATION = std::ptr::null_mut();
let hr = unsafe {
WebAuthNAuthenticatorMakeCredential(
GetForegroundWindow(),
&rp_info,
&user_info,
&credential_params,
&client_data,
&make_credential_options,
&mut attestation,
)
};
if hr < 0 {
return Err(win_api_error_to_webauthn_error(hr));
}
let credential_id_bytes: Vec<u8> = unsafe {
std::slice::from_raw_parts(
(*attestation).pbCredentialId.cast_const(),
(*attestation)
.cbCredentialId
.try_into()
.expect("usize is always >= 32 bits on Windows"),
)
.to_vec()
};
let authenticator_data_bytes: Vec<u8> = unsafe {
std::slice::from_raw_parts(
(*attestation).pbAuthenticatorData.cast_const(),
(*attestation)
.cbAuthenticatorData
.try_into()
.expect("usize is always >= 32 bits on Windows"),
)
.to_vec()
};
let attestation_object_bytes: Vec<u8> = unsafe {
std::slice::from_raw_parts(
(*attestation).pbAttestationObject.cast_const(),
(*attestation)
.cbAttestationObject
.try_into()
.expect("usize is always >= 32 bits on Windows"),
)
.to_vec()
};
let transport_mask = unsafe { (*attestation).dwUsedTransport };
unsafe {
WebAuthNFreeCredentialAttestation(attestation.cast_const());
}
let parsed_auth_data =
AuthenticatorData::from_slice(&authenticator_data_bytes).map_err(|_| {
WebauthnError::ValidationError {
context: "failed to parse authenticator data returned by webauthn.dll",
}
})?;
let attested = parsed_auth_data.attested_credential_data.as_ref().ok_or(
WebauthnError::ValidationError {
context: "authenticator data from webauthn.dll is missing attested credential data",
},
)?;
let public_key_algorithm =
match attested
.key
.alg
.as_ref()
.ok_or(WebauthnError::ValidationError {
context: "COSE key from webauthn.dll is missing algorithm identifier",
})? {
Algorithm::PrivateUse(val) => *val,
Algorithm::Assigned(alg) => alg.to_i64(),
Algorithm::Text(_) => {
return Err(WebauthnError::ValidationError {
context: "COSE key from webauthn.dll has non-integer algorithm identifier",
});
}
};
let public_key = <C::SecretKey as SecretKeyT>::PublicKey::der_from_cose_key(&attested.key)
.ok()
.map(Bytes::from);
let transports = win_api_ctap_transport_mask_to_transports(transport_mask);
let attachment = match transports.first() {
Some(AuthenticatorTransport::Internal) => AuthenticatorAttachment::Platform,
_ => AuthenticatorAttachment::CrossPlatform,
};
let cred_props =
cred_props_requested.then_some(CredentialPropertiesOutput { discoverable: None });
Ok(webauthn::CreatedPublicKeyCredential {
id: encoding::base64url(&credential_id_bytes),
raw_id: credential_id_bytes.into(),
ty: webauthn::PublicKeyCredentialType::PublicKey,
response: webauthn::AuthenticatorAttestationResponse {
client_data_json: Vec::from(client_data_json).into(),
authenticator_data: authenticator_data_bytes.into(),
public_key,
public_key_algorithm,
attestation_object: attestation_object_bytes.into(),
transports: Some(win_api_ctap_transport_mask_to_transports(transport_mask)),
},
authenticator_attachment: Some(attachment),
client_extension_results: AuthenticationExtensionsClientOutputs {
cred_props,
prf: None,
},
})
}
pub async fn authenticate<D: ClientData<E>, E: Serialize + Clone>(
&mut self,
origin: impl Into<Origin<'_>>,
request: webauthn::CredentialRequestOptions,
client_data: D,
) -> Result<webauthn::AuthenticatedPublicKeyCredential, WebauthnError> {
check_webauthn_version()?;
let origin = origin.into();
let request = request.public_key;
let rp_id = self
.rp_id_verifier
.assert_domain(&origin, request.rp_id.as_deref())
.await?;
let collected_client_data = webauthn::CollectedClientData::<E> {
ty: webauthn::ClientDataType::Get,
challenge: encoding::base64url(&request.challenge),
origin: origin.to_string(),
cross_origin: None,
extra_data: client_data.extra_client_data(),
unknown_keys: Default::default(),
};
let mut client_data_json = serde_json::to_string(&collected_client_data)
.map_err(|_| WebauthnError::SerializationError)?;
let webauthn_client_data = WEBAUTHN_CLIENT_DATA {
dwVersion: WEBAUTHN_CLIENT_DATA_CURRENT_VERSION,
cbClientDataJSON: u32::try_from(client_data_json.len())
.expect("serialized client data JSON will never be u32::MAX bytes in practice"),
pbClientDataJSON: client_data_json.as_mut_ptr(),
pwszHashAlgId: WEBAUTHN_HASH_ALGORITHM_SHA_256,
};
let rp_id_wide = to_utf16_null_terminated(rp_id);
let timeout = request.timeout.unwrap_or(
Duration::from_secs(120)
.as_millis()
.try_into()
.expect("120_000 (120s in ms) fits in u32"),
);
let cancellation_id = get_cancellation_id()?;
let mut allow_list = WinCredentialList::from_descriptors(
request.allow_credentials.as_deref().unwrap_or(&[]),
);
let get_assertion_options = WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS {
dwVersion: WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_4,
dwTimeoutMilliseconds: timeout,
dwAuthenticatorAttachment: WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY,
dwUserVerificationRequirement: win_uv(request.user_verification),
pAllowCredentialList: allow_list.as_ptr(),
..unsafe { std::mem::zeroed() }
};
std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(timeout.into()));
unsafe { WebAuthNCancelCurrentOperation(&cancellation_id) };
});
let mut assertion: *mut WEBAUTHN_ASSERTION = std::ptr::null_mut();
let hr = unsafe {
WebAuthNAuthenticatorGetAssertion(
GetForegroundWindow(),
rp_id_wide.as_ptr(),
&webauthn_client_data,
&get_assertion_options,
&mut assertion,
)
};
if hr < 0 {
return Err(win_api_error_to_webauthn_error(hr));
}
let credential_id_bytes: Vec<u8> = unsafe {
std::slice::from_raw_parts(
(*assertion).Credential.pbId.cast_const(),
(*assertion)
.Credential
.cbId
.try_into()
.expect("usize is always >= 32 bits on Windows"),
)
.to_vec()
};
let authenticator_data_bytes: Vec<u8> = unsafe {
std::slice::from_raw_parts(
(*assertion).pbAuthenticatorData.cast_const(),
(*assertion)
.cbAuthenticatorData
.try_into()
.expect("usize is always >= 32 bits on Windows"),
)
.to_vec()
};
let signature_bytes: Vec<u8> = unsafe {
std::slice::from_raw_parts(
(*assertion).pbSignature.cast_const(),
(*assertion)
.cbSignature
.try_into()
.expect("usize is always >= 32 bits on Windows"),
)
.to_vec()
};
let user_id_len = unsafe { (*assertion).cbUserId };
let user_handle_bytes: Option<Vec<u8>> = (user_id_len > 0).then(||
unsafe {
std::slice::from_raw_parts(
(*assertion).pbUserId.cast_const(),
user_id_len
.try_into()
.expect("usize is always >= 32 bits on Windows"),
)
.to_vec()
});
unsafe {
WebAuthNFreeAssertion(assertion.cast_const());
}
Ok(webauthn::AuthenticatedPublicKeyCredential {
id: encoding::base64url(&credential_id_bytes),
raw_id: credential_id_bytes.into(),
ty: webauthn::PublicKeyCredentialType::PublicKey,
response: webauthn::AuthenticatorAssertionResponse {
client_data_json: Vec::from(client_data_json).into(),
authenticator_data: authenticator_data_bytes.into(),
signature: signature_bytes.into(),
user_handle: user_handle_bytes.map(Into::into),
attestation_object: None,
},
authenticator_attachment: None,
client_extension_results: AuthenticationExtensionsClientOutputs::default(),
})
}
}