diff --git a/fido2/client/win_api.py b/fido2/client/win_api.py index 18ba854..3cd65a2 100644 --- a/fido2/client/win_api.py +++ b/fido2/client/win_api.py @@ -39,29 +39,23 @@ from __future__ import annotations -from ..utils import websafe_decode -from ..webauthn import AttestationObject, AuthenticatorData, ResidentKeyRequirement -from ..ctap2.extensions import ( - AuthenticatorExtensionsPRFInputs, - HMACGetSecretInput, - AuthenticatorExtensionsLargeBlobInputs, -) - from enum import IntEnum, unique from ctypes.wintypes import BOOL, DWORD, LONG, LPCWSTR, HWND, WORD -from threading import Thread -from typing import Mapping, Any +from typing import Mapping, Sequence, Any import ctypes from ctypes import WinDLL # type: ignore from ctypes import LibraryLoader +# Not implemented: Platform credentials support + windll = LibraryLoader(WinDLL) PBYTE = ctypes.POINTER(ctypes.c_ubyte) # Different from wintypes.PBYTE, which is signed PCWSTR = ctypes.c_wchar_p +PVOID = ctypes.c_void_p class BytesProperty: @@ -70,7 +64,7 @@ class BytesProperty: Allows for easy reading/writing to struct fields using Python bytes objects. """ - def __init__(self, name): + def __init__(self, name: str): self.cbName = "cb" + name self.pbName = "pb" + name @@ -79,9 +73,11 @@ def __get__(self, instance, owner): bytearray(getattr(instance, self.pbName)[: getattr(instance, self.cbName)]) ) - def __set__(self, instance, value): - setattr(instance, self.cbName, len(value) if value is not None else 0) - setattr(instance, self.pbName, ctypes.cast(value or 0, PBYTE)) + def __set__(self, instance, value: bytes | None): + ln = len(value) if value else 0 + buffer = ctypes.create_string_buffer(value) if value else 0 + setattr(instance, self.cbName, ln) + setattr(instance, self.pbName, ctypes.cast(buffer, PBYTE)) class GUID(ctypes.Structure): @@ -109,12 +105,123 @@ def __str__(self): ) +class _FromString: + @classmethod + def from_string(cls, value: str): + return getattr(cls, value.upper().replace("-", "_")) + + +@unique +class WebAuthNUserVerificationRequirement(_FromString, IntEnum): + """Maps to WEBAUTHN_USER_VERIFICATION_REQUIREMENT_*. + + https://github.com/microsoft/webauthn/blob/master/webauthn.h#L335 + """ + + ANY = 0 + REQUIRED = 1 + PREFERRED = 2 + DISCOURAGED = 3 + + +@unique +class WebAuthNAttestationConveyancePreference(_FromString, IntEnum): + """Maps to WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_*. + + https://github.com/microsoft/webauthn/blob/master/webauthn.h#L340 + """ + + ANY = 0 + NONE = 1 + INDIRECT = 2 + DIRECT = 3 + + +@unique +class WebAuthNAuthenticatorAttachment(_FromString, IntEnum): + """Maps to WEBAUTHN_AUTHENTICATOR_ATTACHMENT_*. + + https://github.com/microsoft/webauthn/blob/master/webauthn.h#L330 + """ + + ANY = 0 + PLATFORM = 1 + CROSS_PLATFORM = 2 + CROSS_PLATFORM_U2F_V2 = 3 + + +@unique +class WebAuthNCTAPTransport(_FromString, IntEnum): + """Maps to WEBAUTHN_CTAP_TRANSPORT_*. + + https://github.com/microsoft/webauthn/blob/master/webauthn.h#L225 + """ + + ANY = 0x00000000 + USB = 0x00000001 + NFC = 0x00000002 + BLE = 0x00000004 + TEST = 0x00000008 + INTERNAL = 0x00000010 + FLAGS_MASK = 0x0000001F + + +@unique +class WebAuthNEnterpriseAttestation(_FromString, IntEnum): + """Maps to WEBAUTHN_ENTERPRISE_ATTESTATION_*. + + https://github.com/microsoft/webauthn/blob/master/webauthn.h#L401 + """ + + NONE = 0 + VENDOR_FACILITATED = 1 + PLATFORM_MANAGED = 2 + + +@unique +class WebAuthNLargeBlobSupport(_FromString, IntEnum): + """Maps to WEBAUTHN_LARGE_BLOB_SUPPORT_*. + + https://github.com/microsoft/webauthn/blob/master/webauthn.h#L405 + """ + + NONE = 0 + REQUIRED = 1 + PREFERRED = 2 + + +@unique +class WebAuthNLargeBlobOperation(_FromString, IntEnum): + """Maps to WEBAUTHN_LARGE_BLOB_OPERATION_*. + + https://github.com/microsoft/webauthn/blob/master/webauthn.h#L478 + """ + + NONE = 0 + GET = 1 + SET = 2 + DELETE = 3 + + +@unique +class WebAuthNUserVerification(_FromString, IntEnum): + """Maps to WEBAUTHN_USER_VERIFICATION_*. + + https://github.com/microsoft/webauthn/blob/master/webauthn.h#L482 + """ + + ANY = 0 + OPTIONAL = 1 + OPTIONAL_WITH_CREDENTIAL_ID_LIST = 2 + REQUIRED = 3 + + class WebAuthNCoseCredentialParameter(ctypes.Structure): """Maps to WEBAUTHN_COSE_CREDENTIAL_PARAMETER Struct. https://github.com/microsoft/webauthn/blob/master/webauthn.h#L185 - :param dict[str, Any] cred_params: Dict of Credential parameters. + :param cred_params: Dict of Credential parameters. """ _fields_ = [ @@ -123,7 +230,7 @@ class WebAuthNCoseCredentialParameter(ctypes.Structure): ("lAlg", LONG), ] - def __init__(self, cred_params): + def __init__(self, cred_params: Mapping[str, Any]): self.dwVersion = get_version(self.__class__.__name__) self.pwszCredentialType = cred_params["type"] self.lAlg = cred_params["alg"] @@ -134,7 +241,7 @@ class WebAuthNCoseCredentialParameters(ctypes.Structure): https://github.com/microsoft/webauthn/blob/master/webauthn.h#L191 - :param list[dict[str, Any]] params: List of Credential parameter dicts. + :param params: List of Credential parameter dicts. """ _fields_ = [ @@ -142,7 +249,7 @@ class WebAuthNCoseCredentialParameters(ctypes.Structure): ("pCredentialParameters", ctypes.POINTER(WebAuthNCoseCredentialParameter)), ] - def __init__(self, params): + def __init__(self, params: Sequence[Mapping[str, Any]]): self.cCredentialParameters = len(params) self.pCredentialParameters = (WebAuthNCoseCredentialParameter * len(params))( *(WebAuthNCoseCredentialParameter(param) for param in params) @@ -154,7 +261,7 @@ class WebAuthNClientData(ctypes.Structure): https://github.com/microsoft/webauthn/blob/master/webauthn.h#L153 - :param bytes client_data: ClientData serialized as JSON bytes. + :param client_data_json: ClientData serialized as JSON bytes. """ _fields_ = [ @@ -164,11 +271,11 @@ class WebAuthNClientData(ctypes.Structure): ("pwszHashAlgId", LPCWSTR), ] - json = BytesProperty("ClientDataJSON") + client_data_json = BytesProperty("ClientDataJSON") - def __init__(self, client_data): + def __init__(self, client_data_json: bytes): self.dwVersion = get_version(self.__class__.__name__) - self.json = client_data + self.client_data_json = client_data_json self.pwszHashAlgId = "SHA-256" @@ -177,7 +284,7 @@ class WebAuthNRpEntityInformation(ctypes.Structure): https://github.com/microsoft/webauthn/blob/master/webauthn.h#L98 - :param dict[str, Any] rp: Dict of RP information. + :param rp: Dict of RP information. """ _fields_ = [ @@ -187,7 +294,7 @@ class WebAuthNRpEntityInformation(ctypes.Structure): ("pwszIcon", PCWSTR), ] - def __init__(self, rp): + def __init__(self, rp: Mapping[str, Any]): self.dwVersion = get_version(self.__class__.__name__) self.pwszId = rp["id"] self.pwszName = rp["name"] @@ -199,7 +306,7 @@ class WebAuthNUserEntityInformation(ctypes.Structure): https://github.com/microsoft/webauthn/blob/master/webauthn.h#L127 - :param dict[str, Any] user: Dict of User information. + :param user: Dict of User information. """ _fields_ = [ @@ -213,7 +320,7 @@ class WebAuthNUserEntityInformation(ctypes.Structure): id = BytesProperty("Id") - def __init__(self, user): + def __init__(self, user: Mapping[str, Any]): self.dwVersion = get_version(self.__class__.__name__) self.id = user["id"] self.pwszName = user["name"] @@ -226,7 +333,7 @@ class WebAuthNCredentialEx(ctypes.Structure): https://github.com/microsoft/webauthn/blob/master/webauthn.h#L250 - :param dict[str, Any] cred: Dict of Credential Descriptor data. + :param cred: Dict of Credential Descriptor data. """ _fields_ = [ @@ -239,7 +346,7 @@ class WebAuthNCredentialEx(ctypes.Structure): id = BytesProperty("Id") - def __init__(self, cred): + def __init__(self, cred: Mapping[str, Any]): self.dwVersion = get_version(self.__class__.__name__) self.id = cred["id"] self.pwszCredentialType = cred["type"] @@ -251,8 +358,7 @@ class WebAuthNCredentialList(ctypes.Structure): https://github.com/microsoft/webauthn/blob/master/webauthn.h#L261 - :param list[dict[str, Any]] credentials: List of dict of - Credential Descriptor data. + :param credentials: List of dict of Credential Descriptor data. """ _fields_ = [ @@ -260,7 +366,7 @@ class WebAuthNCredentialList(ctypes.Structure): ("ppCredentials", ctypes.POINTER(ctypes.POINTER(WebAuthNCredentialEx))), ] - def __init__(self, credentials): + def __init__(self, credentials: Sequence[Mapping[str, Any]]): self.cCredentials = len(credentials) self.ppCredentials = (ctypes.POINTER(WebAuthNCredentialEx) * len(credentials))( *(ctypes.pointer(WebAuthNCredentialEx(cred)) for cred in credentials) @@ -279,7 +385,7 @@ class WebAuthNHmacSecretSalt(ctypes.Structure): first = BytesProperty("First") second = BytesProperty("Second") - def __init__(self, first, second=None): + def __init__(self, first: bytes, second: bytes | None = None): self.first = first self.second = second @@ -294,7 +400,7 @@ class WebAuthNCredWithHmacSecretSalt(ctypes.Structure): cred_id = BytesProperty("CredID") - def __init__(self, cred_id, salt): + def __init__(self, cred_id: bytes, salt: WebAuthNHmacSecretSalt): self.cred_id = cred_id self.pHmacSecretSalt = ctypes.pointer(salt) @@ -307,7 +413,11 @@ class WebAuthNHmacSecretSaltValues(ctypes.Structure): ("pCredWithHmacSecretSaltList", ctypes.POINTER(WebAuthNCredWithHmacSecretSalt)), ] - def __init__(self, global_salt, credential_salts=[]): + def __init__( + self, + global_salt: WebAuthNHmacSecretSalt | None, + credential_salts: Sequence[WebAuthNCredWithHmacSecretSalt] = [], + ): if global_salt: self.pGlobalHmacSalt = ctypes.pointer(global_salt) @@ -328,7 +438,9 @@ class WebAuthNCredProtectExtensionIn(ctypes.Structure): ("bRequireCredProtect", BOOL), ] - def __init__(self, cred_protect, require_cred_protect): + def __init__( + self, cred_protect: WebAuthNUserVerification, require_cred_protect: bool + ): self.dwCredProtect = cred_protect self.bRequireCredProtect = require_cred_protect @@ -341,7 +453,7 @@ class WebAuthNCredBlobExtension(ctypes.Structure): cred_blob = BytesProperty("CredBlob") - def __init__(self, blob): + def __init__(self, blob: bytes): self.cred_blob = blob @@ -354,13 +466,13 @@ class WebAuthNExtension(ctypes.Structure): _fields_ = [ ("pwszExtensionIdentifier", LPCWSTR), ("cbExtension", DWORD), - ("pvExtension", PBYTE), + ("pvExtension", PVOID), ] - def __init__(self, identifier, value): + def __init__(self, identifier: str, value: Any): self.pwszExtensionIdentifier = identifier self.cbExtension = ctypes.sizeof(value) - self.pvExtension = ctypes.cast(ctypes.pointer(value), PBYTE) + self.pvExtension = ctypes.cast(ctypes.pointer(value), PVOID) class WebAuthNExtensions(ctypes.Structure): @@ -374,7 +486,7 @@ class WebAuthNExtensions(ctypes.Structure): ("pExtensions", ctypes.POINTER(WebAuthNExtension)), ] - def __init__(self, extensions): + def __init__(self, extensions: Sequence[WebAuthNExtension]): self.cExtensions = len(extensions) self.pExtensions = (WebAuthNExtension * len(extensions))(*extensions) @@ -384,7 +496,7 @@ class WebAuthNCredential(ctypes.Structure): https://github.com/microsoft/webauthn/blob/master/webauthn.h#L212 - :param dict[str, Any] cred: Dict of Credential Descriptor data. + :param cred: Dict of Credential Descriptor data. """ _fields_ = [ @@ -396,22 +508,17 @@ class WebAuthNCredential(ctypes.Structure): id = BytesProperty("Id") - def __init__(self, cred): + def __init__(self, cred: Mapping[str, Any]): self.id = cred["id"] self.pwszCredentialType = cred["type"] - @property - def descriptor(self): - return {"type": self.pwszCredentialType, "id": self.id} - class WebAuthNCredentials(ctypes.Structure): """Maps to WEBAUTHN_CREDENTIALS Struct. https://github.com/microsoft/webauthn/blob/master/webauthn.h#L219 - :param list[dict[str, Any]] credentials: List of dict of - Credential Descriptor data. + :param credentials: List of dict of Credential Descriptor data. """ _fields_ = [ @@ -419,7 +526,7 @@ class WebAuthNCredentials(ctypes.Structure): ("pCredentials", ctypes.POINTER(WebAuthNCredential)), ] - def __init__(self, credentials): + def __init__(self, credentials: Sequence[Mapping[str, Any]]): self.cCredentials = len(credentials) self.pCredentials = (WebAuthNCredential * len(credentials))( *(WebAuthNCredential(cred) for cred in credentials) @@ -457,13 +564,12 @@ class WebAuthNGetAssertionOptions(ctypes.Structure): https://github.com/microsoft/webauthn/blob/master/webauthn.h#L452 - :param int timeout: Time that the operation is expected to complete within. + :param timeout: Time that the operation is expected to complete within. This is used as guidance, and can be overridden by the platform. - :param WebAuthNAuthenticatorAttachment attachment: Platform vs Cross-Platform + :param attachment: Platform vs Cross-Platform Authenticators. - :param WebAuthNUserVerificationRequirement user_verification_requirement: User - Verification Requirement. - :param list[dict[str,Any]] credentials: Allowed Credentials List. + :param uv_requirement: User Verification Requirement. + :param credentials: Allowed Credentials List. """ _fields_ = [ @@ -494,23 +600,23 @@ class WebAuthNGetAssertionOptions(ctypes.Structure): def __init__( self, - timeout, - attachment, - user_verification_requirement, - credentials, - cancellationId, - cred_large_blob_operation, - cred_large_blob, - hmac_secret_salts=None, - extensions=None, - flags=0, - u2f_appid=None, - u2f_appid_used=None, + timeout: int = 0, + attachment: int = WebAuthNAuthenticatorAttachment.ANY, + uv_requirement: int = WebAuthNUserVerificationRequirement.DISCOURAGED, + credentials: Sequence[Mapping[str, Any]] = [], + cancellationId: GUID | None = None, + cred_large_blob_operation: int = WebAuthNLargeBlobOperation.NONE, + cred_large_blob: bytes | None = None, + hmac_secret_salts: WebAuthNHmacSecretSaltValues | None = None, + extensions: Sequence[WebAuthNExtension] = [], + flags: int = 0, + u2f_appid: str | None = None, + u2f_appid_used: BOOL | None = None, ): self.dwVersion = get_version(self.__class__.__name__) self.dwTimeoutMilliseconds = timeout self.dwAuthenticatorAttachment = attachment - self.dwUserVerificationRequirement = user_verification_requirement + self.dwUserVerificationRequirement = uv_requirement self.dwFlags = flags if extensions: @@ -521,8 +627,8 @@ def __init__( if u2f_appid_used is not None: self.pbU2fAppId = ctypes.pointer(u2f_appid_used) - if self.dwVersion >= 3: - self.pCancellationId = cancellationId + if self.dwVersion >= 3 and cancellationId: + self.pCancellationId = ctypes.pointer(cancellationId) if self.dwVersion >= 4: clist = WebAuthNCredentialList(credentials) @@ -578,16 +684,16 @@ class WebAuthNMakeCredentialOptions(ctypes.Structure): https://github.com/microsoft/webauthn/blob/master/webauthn.h#L394 - :param int timeout: Time that the operation is expected to complete within.This + :param timeout: Time that the operation is expected to complete within.This is used as guidance, and can be overridden by the platform. - :param bool require_resident_key: Require key to be resident or not. - :param WebAuthNAuthenticatorAttachment attachment: Platform vs Cross-Platform + :param require_resident_key: Require key to be resident or not. + :param attachment: Platform vs Cross-Platform Authenticators. - :param WebAuthNUserVerificationRequirement user_verification_requirement: User + :param user_verification_requirement: User Verification Requirement. - :param WebAuthNAttestationConveyancePreference attestation_convoyence: + :param attestation_convoyence: Attestation Conveyance Preference. - :param list[dict[str,Any]] credentials: Credentials used for exclusion. + :param credentials: Credentials used for exclusion. """ _fields_ = [ @@ -616,31 +722,31 @@ class WebAuthNMakeCredentialOptions(ctypes.Structure): def __init__( self, - timeout, - require_resident_key, - attachment, - user_verification_requirement, - attestation_convoyence, - credentials, - cancellationId, - enterprise_attestation, - large_blob_support, - prefer_resident_key, - enable_prf=False, - extensions=None, + timeout: int = 0, + require_resident_key: bool = False, + attachment: int = WebAuthNAuthenticatorAttachment.ANY, + uv_requirement: int = WebAuthNUserVerificationRequirement.DISCOURAGED, + attestation_convoyence: int = WebAuthNAttestationConveyancePreference.ANY, + credentials: Sequence[Mapping[str, Any]] = [], + cancellationId: GUID | None = None, + enterprise_attestation: int = WebAuthNEnterpriseAttestation.NONE, + large_blob_support: int = WebAuthNLargeBlobSupport.NONE, + prefer_resident_key: bool = False, + enable_prf: bool = False, + extensions: Sequence[WebAuthNExtension] = [], ): self.dwVersion = get_version(self.__class__.__name__) self.dwTimeoutMilliseconds = timeout self.bRequireResidentKey = require_resident_key self.dwAuthenticatorAttachment = attachment - self.dwUserVerificationRequirement = user_verification_requirement + self.dwUserVerificationRequirement = uv_requirement self.dwAttestationConveyancePreference = attestation_convoyence if extensions: self.Extensions = WebAuthNExtensions(extensions) - if self.dwVersion >= 2: - self.pCancellationId = cancellationId + if self.dwVersion >= 2 and cancellationId: + self.pCancellationId = ctypes.pointer(cancellationId) if self.dwVersion >= 3: self.pExcludeCredentialList = ctypes.pointer( @@ -697,117 +803,6 @@ def __del__(self): WEBAUTHN.WebAuthNFreeCredentialAttestation(ctypes.byref(self)) -class _FromString(object): - @classmethod - def from_string(cls, value): - return getattr(cls, value.upper().replace("-", "_")) - - -@unique -class WebAuthNUserVerificationRequirement(_FromString, IntEnum): - """Maps to WEBAUTHN_USER_VERIFICATION_REQUIREMENT_*. - - https://github.com/microsoft/webauthn/blob/master/webauthn.h#L335 - """ - - ANY = 0 - REQUIRED = 1 - PREFERRED = 2 - DISCOURAGED = 3 - - -@unique -class WebAuthNAttestationConveyancePreference(_FromString, IntEnum): - """Maps to WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_*. - - https://github.com/microsoft/webauthn/blob/master/webauthn.h#L340 - """ - - ANY = 0 - NONE = 1 - INDIRECT = 2 - DIRECT = 3 - - -@unique -class WebAuthNAuthenticatorAttachment(_FromString, IntEnum): - """Maps to WEBAUTHN_AUTHENTICATOR_ATTACHMENT_*. - - https://github.com/microsoft/webauthn/blob/master/webauthn.h#L330 - """ - - ANY = 0 - PLATFORM = 1 - CROSS_PLATFORM = 2 - CROSS_PLATFORM_U2F_V2 = 3 - - -@unique -class WebAuthNCTAPTransport(_FromString, IntEnum): - """Maps to WEBAUTHN_CTAP_TRANSPORT_*. - - https://github.com/microsoft/webauthn/blob/master/webauthn.h#L225 - """ - - ANY = 0x00000000 - USB = 0x00000001 - NFC = 0x00000002 - BLE = 0x00000004 - TEST = 0x00000008 - INTERNAL = 0x00000010 - FLAGS_MASK = 0x0000001F - - -@unique -class WebAuthNEnterpriseAttestation(_FromString, IntEnum): - """Maps to WEBAUTHN_ENTERPRISE_ATTESTATION_*. - - https://github.com/microsoft/webauthn/blob/master/webauthn.h#L401 - """ - - NONE = 0 - VENDOR_FACILITATED = 1 - PLATFORM_MANAGED = 2 - - -@unique -class WebAuthNLargeBlobSupport(_FromString, IntEnum): - """Maps to WEBAUTHN_LARGE_BLOB_SUPPORT_*. - - https://github.com/microsoft/webauthn/blob/master/webauthn.h#L405 - """ - - NONE = 0 - REQUIRED = 1 - PREFERRED = 2 - - -@unique -class WebAuthNLargeBlobOperation(_FromString, IntEnum): - """Maps to WEBAUTHN_LARGE_BLOB_OPERATION_*. - - https://github.com/microsoft/webauthn/blob/master/webauthn.h#L478 - """ - - NONE = 0 - GET = 1 - SET = 2 - DELETE = 3 - - -@unique -class WebAuthNUserVerification(_FromString, IntEnum): - """Maps to WEBAUTHN_USER_VERIFICATION_*. - - https://github.com/microsoft/webauthn/blob/master/webauthn.h#L482 - """ - - ANY = 0 - OPTIONAL = 1 - OPTIONAL_WITH_CREDENTIAL_ID_LIST = 2 - REQUIRED = 3 - - HRESULT = ctypes.HRESULT # type: ignore WEBAUTHN = windll.webauthn # type: ignore WEBAUTHN_API_VERSION = WEBAUTHN.WebAuthNGetApiVersionNumber() @@ -912,327 +907,3 @@ def get_version(class_name: str) -> int: ): return WEBAUTHN_STRUCT_VERSIONS[api_version][class_name] raise ValueError("Unknown class name") - - -class CancelThread(Thread): - def __init__(self, event): - super().__init__() - self.daemon = True - self._completed = False - self.event = event - self.guid = GUID() - WEBAUTHN.WebAuthNGetCancellationId(ctypes.byref(self.guid)) - - def run(self): - self.event.wait() - if not self._completed: - WEBAUTHN.WebAuthNCancelCurrentOperation(ctypes.byref(self.guid)) - - def complete(self): - self._completed = True - self.event.set() - self.join() - - -# Not implemented: Platform credentials support - - -class WinAPI: - """Implementation of Microsoft's WebAuthN APIs. - - :param ctypes.HWND handle: Window handle to use for API calls. - """ - - version = WEBAUTHN_API_VERSION - - def __init__(self, handle=None, allow_hmac_secret=False): - self.handle = handle or windll.user32.GetForegroundWindow() - self._allow_hmac_secret = allow_hmac_secret - - def get_error_name(self, winerror): - """Returns an error name given an error HRESULT value. - - :param int winerror: Windows error code from an OSError. - :return: An error name. - :rtype: str - - Example: - try: - api.make_credential(*args, **kwargs) - except OSError as e: - print(api.get_error_name(e.winerror)) - """ - return WEBAUTHN.WebAuthNGetErrorName(winerror) - - def make_credential( - self, - rp, - user, - pub_key_cred_params, - client_data, - timeout=0, - resident_key=ResidentKeyRequirement.DISCOURAGED, - platform_attachment=WebAuthNAuthenticatorAttachment.ANY, - user_verification=WebAuthNUserVerificationRequirement.ANY, - attestation=WebAuthNAttestationConveyancePreference.DIRECT, - exclude_credentials=None, - extensions=None, - event=None, - enterprise_attestation=WebAuthNEnterpriseAttestation.NONE, - ) -> tuple[AttestationObject, dict[str, Any]]: - """Make credential using Windows WebAuthN API. - - :param dict[str,Any] rp: Relying Party Entity data. - :param dict[str,Any] user: User Entity data. - :param list[dict[str,Any]] pub_key_cred_params: List of - PubKeyCredentialParams data. - :param bytes client_data: ClientData JSON. - :param int timeout: (optional) Timeout value, in ms. - :param ResidentKeyRequirement resident_key: (optional) - Require resident key, default: discouraged. - :param WebAuthNAuthenticatorAttachment platform_attachment: (optional) - Authenticator Attachment, default: any. - :param WebAuthNUserVerificationRequirement user_verification: (optional) - User Verification Requirement, default: any. - :param WebAuthNAttestationConveyancePreference attestation: (optional) - Attestation Conveyance Preference, default: direct. - :param list[dict[str,Any]] exclude_credentials: (optional) List of - PublicKeyCredentialDescriptor of previously registered credentials. - :param Any extensions: Currently not supported. - :param threading.Event event: (optional) Signal to abort the operation. - """ - - win_extensions = [] - large_blob_support = WebAuthNLargeBlobSupport.NONE - enable_prf = False - if extensions: - if "credentialProtectionPolicy" in extensions: - win_extensions.append( - WebAuthNExtension( - "credProtect", - WebAuthNCredProtectExtensionIn( - WebAuthNUserVerification.from_string( - extensions["credentialProtectionPolicy"] - ), - extensions.get("enforceCredentialProtectionPolicy", False), - ), - ) - ) - if "credBlob" in extensions: - win_extensions.append( - WebAuthNExtension( - "credBlob", - WebAuthNCredBlobExtension(extensions["credBlob"]), - ) - ) - if "largeBlob" in extensions: - large_blob_support = WebAuthNLargeBlobSupport.from_string( - extensions["largeBlob"].get("support", "none") - ) - if extensions.get("minPinLength", True): - win_extensions.append(WebAuthNExtension("minPinLength", BOOL(True))) - if "prf" in extensions: - # Windows requires resident key for hmac-secret - resident_key = ResidentKeyRequirement.REQUIRED - enable_prf = True - win_extensions.append(WebAuthNExtension("hmac-secret", BOOL(True))) - elif "hmacCreateSecret" in extensions and self._allow_hmac_secret: - # Windows requires resident key for hmac-secret - resident_key = ResidentKeyRequirement.REQUIRED - win_extensions.append(WebAuthNExtension("hmac-secret", BOOL(True))) - else: - extensions = {} - - if event: - timer = CancelThread(event) - timer.start() - else: - timer = None - - attestation_pointer = ctypes.POINTER(WebAuthNCredentialAttestation)() - WEBAUTHN.WebAuthNAuthenticatorMakeCredential( - self.handle, - ctypes.byref(WebAuthNRpEntityInformation(rp)), - ctypes.byref(WebAuthNUserEntityInformation(user)), - ctypes.byref(WebAuthNCoseCredentialParameters(pub_key_cred_params)), - ctypes.byref(WebAuthNClientData(client_data)), - ctypes.byref( - WebAuthNMakeCredentialOptions( - timeout, - resident_key == ResidentKeyRequirement.REQUIRED, - platform_attachment, - user_verification, - attestation, - exclude_credentials or [], - ctypes.pointer(timer.guid) if timer else None, - enterprise_attestation, - large_blob_support, - resident_key == ResidentKeyRequirement.PREFERRED, - enable_prf, - win_extensions, - ) - ), - ctypes.byref(attestation_pointer), - ) - if timer: - timer.complete() - - obj = attestation_pointer.contents - att_obj = AttestationObject(obj.attestation_object) - - extensions_out = att_obj.auth_data.extensions or {} - extension_outputs = {} - if extensions.get("credProps"): - extension_outputs["credProps"] = {"rk": bool(obj.bResidentKey)} - if "hmac-secret" in extensions_out: - if enable_prf: - extension_outputs["prf"] = {"enabled": extensions_out["hmac-secret"]} - else: - extension_outputs["hmacCreateSecret"] = extensions_out["hmac-secret"] - if "largeBlob" in extensions: - extension_outputs["largeBlob"] = { - "supported": bool(obj.bLargeBlobSupported) - } - - return att_obj, extension_outputs - - def get_assertion( - self, - rp_id, - client_data, - timeout=0, - platform_attachment=WebAuthNAuthenticatorAttachment.ANY, - user_verification=WebAuthNUserVerificationRequirement.ANY, - allow_credentials=None, - extensions=None, - event=None, - ) -> tuple[dict[str, Any], AuthenticatorData, bytes, bytes, dict[str, Any]]: - """Get assertion using Windows WebAuthN API. - - :param str rp_id: Relying Party ID string. - :param bytes client_data: ClientData JSON. - :param int timeout: (optional) Timeout value, in ms. - :param WebAuthNAuthenticatorAttachment platform_attachment: (optional) - Authenticator Attachment, default: any. - :param WebAuthNUserVerificationRequirement user_verification: (optional) - User Verification Requirement, default: any. - :param list[dict[str,Any]] allow_credentials: (optional) List of - PublicKeyCredentialDescriptor of previously registered credentials. - :param Any extensions: Currently not supported. - :param threading.Event event: (optional) Signal to abort the operation. - """ - - flags = 0 - large_blob = None - large_blob_operation = WebAuthNLargeBlobOperation.NONE - hmac_secret_salts = None - win_extensions = [] - u2f_appid = None - u2f_appid_used = BOOL(False) - if extensions: - if extensions.get("appid"): - u2f_appid = extensions["appid"] - if extensions.get("getCredBlob"): - win_extensions.append(WebAuthNExtension("credBlob", BOOL(True))) - large_blob = AuthenticatorExtensionsLargeBlobInputs.from_dict( - extensions.get("largeBlob") - ) - if large_blob: - if large_blob.read: - large_blob_operation = WebAuthNLargeBlobOperation.GET - else: - large_blob = large_blob.write - large_blob_operation = WebAuthNLargeBlobOperation.SET - prf = AuthenticatorExtensionsPRFInputs.from_dict(extensions.get("prf")) - if prf: - cred_salts = prf.eval_by_credential or {} - hmac_secret_salts = WebAuthNHmacSecretSaltValues( - ( - WebAuthNHmacSecretSalt(prf.eval.first, prf.eval.second) - if prf.eval - else None - ), - [ - WebAuthNCredWithHmacSecretSalt( - websafe_decode(cred_id), - WebAuthNHmacSecretSalt(salts.first, salts.second), - ) - for cred_id, salts in cred_salts.items() - ], - ) - elif "hmacGetSecret" in extensions and self._allow_hmac_secret: - flags |= 0x00100000 - salts = HMACGetSecretInput.from_dict(extensions["hmacGetSecret"]) - hmac_secret_salts = WebAuthNHmacSecretSaltValues( - WebAuthNHmacSecretSalt(salts.salt1, salts.salt2) - ) - - if event: - timer = CancelThread(event) - timer.start() - else: - timer = None - - assertion_pointer = ctypes.POINTER(WebAuthNAssertion)() - WEBAUTHN.WebAuthNAuthenticatorGetAssertion( - self.handle, - rp_id, - ctypes.byref(WebAuthNClientData(client_data)), - ctypes.byref( - WebAuthNGetAssertionOptions( - timeout, - platform_attachment, - user_verification, - allow_credentials or [], - ctypes.pointer(timer.guid) if timer else None, - large_blob_operation, - large_blob, - hmac_secret_salts, - win_extensions, - flags, - u2f_appid, - u2f_appid_used, - ) - ), - ctypes.byref(assertion_pointer), - ) - - if timer: - timer.complete() - - obj = assertion_pointer.contents - auth_data = AuthenticatorData(obj.auth_data) - - extension_outputs: dict[str, Any] = {} - - if u2f_appid and obj.dwVersion >= 2: - extension_outputs["appid"] = bool(u2f_appid_used.value) - - if extensions: - if hmac_secret_salts and obj.dwVersion >= 3: - secret = obj.pHmacSecret.contents - if "prf" in extensions: - result = {"first": secret.first} - if secret.second: - result["second"] = secret.second - extension_outputs["prf"] = {"results": result} - else: - result = {"output1": secret.first} - if secret.second: - result["output2"] = secret.second - extension_outputs["hmacGetSecret"] = result - if obj.dwCredLargeBlobStatus != 0: - if extensions["largeBlob"].get("read", False): - extension_outputs["largeBlob"] = {"blob": obj.cred_large_blob} - else: - extension_outputs["largeBlob"] = { - "written": obj.dwCredLargeBlobStatus == 1 - } - - return ( - obj.Credential.descriptor, - auth_data, - obj.signature, - obj.user_id, - extension_outputs, - ) diff --git a/fido2/client/windows.py b/fido2/client/windows.py index 8101141..9f89fdb 100644 --- a/fido2/client/windows.py +++ b/fido2/client/windows.py @@ -27,14 +27,35 @@ from __future__ import annotations -from . import WebAuthnClient, _BaseClient, AssertionSelection, ClientError, _cbor_list from .win_api import ( - WinAPI, + windll, + BOOL, + GUID, + WEBAUTHN_API_VERSION, + WEBAUTHN, WebAuthNAuthenticatorAttachment, WebAuthNUserVerificationRequirement, WebAuthNAttestationConveyancePreference, + WebAuthNCredentialAttestation, WebAuthNEnterpriseAttestation, + WebAuthNUserVerification, + WebAuthNRpEntityInformation, + WebAuthNUserEntityInformation, + WebAuthNCoseCredentialParameters, + WebAuthNClientData, + WebAuthNLargeBlobSupport, + WebAuthNLargeBlobOperation, + WebAuthNExtension, + WebAuthNCredProtectExtensionIn, + WebAuthNCredBlobExtension, + WebAuthNHmacSecretSaltValues, + WebAuthNHmacSecretSalt, + WebAuthNCredWithHmacSecretSalt, + WebAuthNMakeCredentialOptions, + WebAuthNGetAssertionOptions, + WebAuthNAssertion, ) +from . import WebAuthnClient, _BaseClient, AssertionSelection, ClientError, _cbor_list from ..rpid import verify_rp_id from ..webauthn import ( CollectedClientData, @@ -48,6 +69,8 @@ ResidentKeyRequirement, AuthenticatorAttachment, PublicKeyCredentialType, + AttestationObject, + AuthenticatorData, _as_cbor, ) from ..ctap2 import AssertionResponse @@ -56,11 +79,15 @@ AuthenticatorExtensionsPRFOutputs, AuthenticatorExtensionsLargeBlobOutputs, CredentialPropertiesOutput, + AuthenticatorExtensionsPRFInputs, + HMACGetSecretInput, + AuthenticatorExtensionsLargeBlobInputs, ) -from ..utils import _JsonDataObject +from ..utils import _JsonDataObject, websafe_decode -from typing import Callable, Sequence -import sys +from threading import Thread +from typing import Callable, Sequence, Any +import ctypes import logging logger = logging.getLogger(__name__) @@ -79,6 +106,26 @@ def _wrap_ext(key, value): return value +class CancelThread(Thread): + def __init__(self, event): + super().__init__() + self.daemon = True + self._completed = False + self.event = event + self.guid = GUID() + WEBAUTHN.WebAuthNGetCancellationId(ctypes.byref(self.guid)) + + def run(self): + self.event.wait() + if not self._completed: + WEBAUTHN.WebAuthNCancelCurrentOperation(ctypes.byref(self.guid)) + + def complete(self): + self._completed = True + self.event.set() + self.join() + + class WindowsClient(WebAuthnClient, _BaseClient): """Fido2Client-like class using the Windows WebAuthn API. @@ -86,9 +133,6 @@ class WindowsClient(WebAuthnClient, _BaseClient): started restricting access to FIDO devices, causing the standard client classes to require admin priveleges to run (unlike this one). - The make_credential and get_assertion methods are intended to work as a drop-in - replacement for the Fido2Client methods of the same name. - :param str origin: The origin to use. :param verify: Function to verify an RP ID for a given origin. :param ctypes.wintypes.HWND handle: (optional) Window reference to use. @@ -102,14 +146,16 @@ def __init__( allow_hmac_secret=False, ): super().__init__(origin, verify) - self.api = WinAPI(handle, allow_hmac_secret=allow_hmac_secret) + self.handle = handle or windll.user32.GetForegroundWindow() + + self._allow_hmac_secret = allow_hmac_secret # TODO: Decide how to configure this list. self._enterprise_rpid_list: Sequence[str] | None = None @staticmethod def is_available() -> bool: - return sys.platform == "win32" and WinAPI.version > 0 + return WEBAUTHN_API_VERSION > 0 def make_credential(self, options, event=None): """Create a credential using Windows WebAuthN APIs. @@ -129,6 +175,7 @@ def make_credential(self, options, event=None): ) selection = options.authenticator_selection or AuthenticatorSelectionCriteria() + resident_key = selection.resident_key or ResidentKeyRequirement.DISCOURAGED enterprise_attestation = WebAuthNEnterpriseAttestation.NONE if options.attestation == AttestationConveyancePreference.ENTERPRISE: @@ -149,29 +196,112 @@ def make_credential(self, options, event=None): options.attestation or "none" ) + win_extensions = [] + large_blob_support = WebAuthNLargeBlobSupport.NONE + enable_prf = False + if options.extensions: + if "credentialProtectionPolicy" in options.extensions: + win_extensions.append( + WebAuthNExtension( + "credProtect", + WebAuthNCredProtectExtensionIn( + WebAuthNUserVerification.from_string( + options.extensions["credentialProtectionPolicy"] + ), + options.extensions.get( + "enforceCredentialProtectionPolicy", False + ), + ), + ) + ) + if "credBlob" in options.extensions: + win_extensions.append( + WebAuthNExtension( + "credBlob", + WebAuthNCredBlobExtension(options.extensions["credBlob"]), + ) + ) + if "largeBlob" in options.extensions: + large_blob_support = WebAuthNLargeBlobSupport.from_string( + options.extensions["largeBlob"].get("support", "none") + ) + if options.extensions.get("minPinLength", True): + win_extensions.append(WebAuthNExtension("minPinLength", BOOL(True))) + if "prf" in options.extensions: + enable_prf = True + win_extensions.append(WebAuthNExtension("hmac-secret", BOOL(True))) + elif "hmacCreateSecret" in options.extensions and self._allow_hmac_secret: + win_extensions.append(WebAuthNExtension("hmac-secret", BOOL(True))) + + if event: + timer = CancelThread(event) + timer.start() + else: + timer = None + + attestation_pointer = ctypes.POINTER(WebAuthNCredentialAttestation)() try: - att_obj, extensions = self.api.make_credential( - _as_cbor(options.rp), - _as_cbor(options.user), - _cbor_list(options.pub_key_cred_params), - client_data, - options.timeout or 0, - selection.resident_key or ResidentKeyRequirement.DISCOURAGED, - WebAuthNAuthenticatorAttachment.from_string( - selection.authenticator_attachment or "any" + WEBAUTHN.WebAuthNAuthenticatorMakeCredential( + self.handle, + ctypes.byref(WebAuthNRpEntityInformation(_as_cbor(options.rp))), + ctypes.byref(WebAuthNUserEntityInformation(_as_cbor(options.user))), + ctypes.byref( + WebAuthNCoseCredentialParameters( + _cbor_list(options.pub_key_cred_params) + ) ), - WebAuthNUserVerificationRequirement.from_string( - selection.user_verification or "discouraged" + ctypes.byref(WebAuthNClientData(client_data)), + ctypes.byref( + WebAuthNMakeCredentialOptions( + options.timeout or 0, + resident_key == ResidentKeyRequirement.REQUIRED, + WebAuthNAuthenticatorAttachment.from_string( + selection.authenticator_attachment or "any" + ), + WebAuthNUserVerificationRequirement.from_string( + selection.user_verification or "discouraged" + ), + attestation, + _cbor_list(options.exclude_credentials) or [], + timer.guid if timer else None, + enterprise_attestation, + large_blob_support, + resident_key == ResidentKeyRequirement.PREFERRED, + enable_prf, + win_extensions, + ) ), - attestation, - _cbor_list(options.exclude_credentials), - options.extensions, - event, - enterprise_attestation, + ctypes.byref(attestation_pointer), ) except OSError as e: raise ClientError.ERR.OTHER_ERROR(e) + if timer: + # TODO: Avoid setting event? + timer.complete() + + obj = attestation_pointer.contents + att_obj = AttestationObject(obj.attestation_object) + + extension_outputs = {} + if options.extensions: + extensions_out = att_obj.auth_data.extensions or {} + if options.extensions.get("credProps"): + extension_outputs["credProps"] = {"rk": bool(obj.bResidentKey)} + if "hmac-secret" in extensions_out: + if enable_prf: + extension_outputs["prf"] = { + "enabled": extensions_out["hmac-secret"] + } + else: + extension_outputs["hmacCreateSecret"] = extensions_out[ + "hmac-secret" + ] + if "largeBlob" in options.extensions: + extension_outputs["largeBlob"] = { + "supported": bool(obj.bLargeBlobSupported) + } + logger.info("New credential registered") credential = att_obj.auth_data.credential_data @@ -182,7 +312,7 @@ def make_credential(self, options, event=None): response=AuthenticatorAttestationResponse(client_data, att_obj), authenticator_attachment=AuthenticatorAttachment.CROSS_PLATFORM, client_extension_results=AuthenticationExtensionsClientOutputs( - {k: _wrap_ext(k, v) for k, v in extensions.items()} + {k: _wrap_ext(k, v) for k, v in extension_outputs.items()} ), type=PublicKeyCredentialType.PUBLIC_KEY, ) @@ -204,34 +334,140 @@ def get_assertion(self, options, event=None): CollectedClientData.TYPE.GET, options.challenge ) - try: - (credential, auth_data, signature, user_id, extensions) = ( - self.api.get_assertion( - options.rp_id, - client_data, - options.timeout or 0, - WebAuthNAuthenticatorAttachment.ANY, - WebAuthNUserVerificationRequirement.from_string( - options.user_verification or "discouraged" + selection = options.authenticator_selection or AuthenticatorSelectionCriteria() + + flags = 0 + large_blob = None + large_blob_operation = WebAuthNLargeBlobOperation.NONE + hmac_secret_salts = None + win_extensions = [] + u2f_appid = None + u2f_appid_used = BOOL(False) + if options.extensions: + if options.extensions.get("appid"): + u2f_appid = options.extensions["appid"] + if options.extensions.get("getCredBlob"): + win_extensions.append(WebAuthNExtension("credBlob", BOOL(True))) + lg_blob = AuthenticatorExtensionsLargeBlobInputs.from_dict( + options.extensions.get("largeBlob") + ) + if lg_blob: + if lg_blob.read: + large_blob_operation = WebAuthNLargeBlobOperation.GET + else: + large_blob = lg_blob.write + large_blob_operation = WebAuthNLargeBlobOperation.SET + prf = AuthenticatorExtensionsPRFInputs.from_dict( + options.extensions.get("prf") + ) + if prf: + cred_salts = prf.eval_by_credential or {} + hmac_secret_salts = WebAuthNHmacSecretSaltValues( + ( + WebAuthNHmacSecretSalt(prf.eval.first, prf.eval.second) + if prf.eval + else None ), - _cbor_list(options.allow_credentials), - options.extensions, - event, + [ + WebAuthNCredWithHmacSecretSalt( + websafe_decode(cred_id), + WebAuthNHmacSecretSalt(salts.first, salts.second), + ) + for cred_id, salts in cred_salts.items() + ], + ) + elif "hmacGetSecret" in options.extensions and self._allow_hmac_secret: + flags |= 0x00100000 + salts = HMACGetSecretInput.from_dict( + options.extensions["hmacGetSecret"] ) + hmac_secret_salts = WebAuthNHmacSecretSaltValues( + WebAuthNHmacSecretSalt(salts.salt1, salts.salt2) + ) + + if event: + timer = CancelThread(event) + timer.start() + else: + timer = None + + assertion_pointer = ctypes.POINTER(WebAuthNAssertion)() + try: + WEBAUTHN.WebAuthNAuthenticatorGetAssertion( + self.handle, + options.rp_id, + ctypes.byref(WebAuthNClientData(client_data)), + ctypes.byref( + WebAuthNGetAssertionOptions( + options.timeout or 0, + WebAuthNAuthenticatorAttachment.from_string( + selection.authenticator_attachment or "any" + ), + WebAuthNUserVerificationRequirement.from_string( + options.user_verification or "discouraged" + ), + _cbor_list(options.allow_credentials) or [], + timer.guid if timer else None, + large_blob_operation, + large_blob, + hmac_secret_salts, + win_extensions, + flags, + u2f_appid, + u2f_appid_used, + ) + ), + ctypes.byref(assertion_pointer), ) except OSError as e: raise ClientError.ERR.OTHER_ERROR(e) - user = {"id": user_id} if user_id else None + if timer: + # TODO: Avoid setting event? + timer.complete() + + obj = assertion_pointer.contents + auth_data = AuthenticatorData(obj.auth_data) + + extension_outputs: dict[str, Any] = {} + + if u2f_appid and obj.dwVersion >= 2: + extension_outputs["appid"] = bool(u2f_appid_used.value) + + if options.extensions: + if hmac_secret_salts and obj.dwVersion >= 3: + secret = obj.pHmacSecret.contents + if "prf" in options.extensions: + result = {"first": secret.first} + if secret.second: + result["second"] = secret.second + extension_outputs["prf"] = {"results": result} + else: + result = {"output1": secret.first} + if secret.second: + result["output2"] = secret.second + extension_outputs["hmacGetSecret"] = result + if obj.dwCredLargeBlobStatus != 0: + if options.extensions["largeBlob"].get("read", False): + extension_outputs["largeBlob"] = {"blob": obj.cred_large_blob} + else: + extension_outputs["largeBlob"] = { + "written": obj.dwCredLargeBlobStatus == 1 + } + + credential = { + "type": obj.Credential.pwszCredentialType, + "id": obj.Credential.id, + } return AssertionSelection( client_data, [ AssertionResponse( credential=credential, auth_data=auth_data, - signature=signature, - user=user, + signature=obj.signature, + user={"id": obj.user_id} if obj.user_id else None, ) ], - {k: _wrap_ext(k, v) for k, v in extensions.items()}, + {k: _wrap_ext(k, v) for k, v in extension_outputs.items()}, )