Skip to content

Instantly share code, notes, and snippets.

@dzmitry-savitski
Last active August 13, 2025 21:30
Show Gist options
  • Select an option

  • Save dzmitry-savitski/b6dabcc3defd54cdde55a3dd7876c466 to your computer and use it in GitHub Desktop.

Select an option

Save dzmitry-savitski/b6dabcc3defd54cdde55a3dd7876c466 to your computer and use it in GitHub Desktop.

Revisions

  1. dzmitry-savitski revised this gist Aug 13, 2025. 1 changed file with 22 additions and 7 deletions.
    29 changes: 22 additions & 7 deletions webauthn.py
    Original file line number Diff line number Diff line change
    @@ -1,21 +1,36 @@
    # pip install fido2
    import os
    from fido2.webauthn import PublicKeyCredentialRequestOptions, UserVerificationRequirement
    from fido2.client.windows import WindowsClient
    from fido2.webauthn import PublicKeyCredentialRequestOptions

    origin = "https://webauthn.io"
    client = WindowsClient(origin) # wraps webauthn.dll on Windows
    # Try to import the new collector (python-fido2 >= 1.2/2.0)
    collector = None
    try:
    from fido2.client import DefaultClientDataCollector
    collector = DefaultClientDataCollector(origin="https://webauthn.io")
    except Exception:
    collector = None

    # Instantiate WindowsClient for both API shapes
    if collector is not None:
    # New API: pass a ClientDataCollector
    client = WindowsClient(client_data_collector=collector)
    else:
    # Old API: constructor accepted origin directly
    client = WindowsClient("https://webauthn.io")

    # Build proper WebAuthn request options
    # Build proper WebAuthn options for an assertion (sign-in)
    options = PublicKeyCredentialRequestOptions(
    challenge=os.urandom(32),
    rp_id="webauthn.io",
    timeout=60000,
    user_verification="discouraged", # we're just probing the transport
    timeout=15000, # ms
    user_verification=UserVerificationRequirement.DISCOURAGED, # we're just probing transport
    )

    try:
    result = client.get_assertion(options)
    print("OK, transport works. Got", len(result.get_response(0).signature), "bytes of signature")
    # If we got here, transport worked and Windows Security likely popped on the client.
    resp = result.get_response(0) # select the first assertion
    print("Success. Signature length:", len(resp.signature))
    except Exception as e:
    print("GetAssertion failed:", repr(e))
  2. dzmitry-savitski revised this gist Aug 13, 2025. 2 changed files with 18 additions and 122 deletions.
    28 changes: 0 additions & 28 deletions webauthn.js
    Original file line number Diff line number Diff line change
    @@ -1,28 +0,0 @@
    // webauthn_probe.js
    const ffi = require('ffi-napi');
    const ref = require('ref-napi');

    // Map a couple of functions. (We avoid wide-string handling here to keep it simple.)
    const webauthn = ffi.Library('webauthn', {
    // DWORD WebAuthNGetApiVersionNumber(void)
    WebAuthNGetApiVersionNumber: ['uint32', []],
    // HRESULT WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable(BOOL *out)
    WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable: ['int32', ['pointer']],
    // If you want error names, add:
    // WebAuthNGetErrorName: ['pointer', ['int32']], // returns PCWSTR (UTF-16)
    });

    const apiVersion = webauthn.WebAuthNGetApiVersionNumber();
    console.log('WebAuthN API version:', apiVersion);

    const BOOL = ref.types.int; // Windows BOOL is a 32-bit int
    const outPtr = ref.alloc(BOOL);
    const hr = webauthn.WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable(outPtr);
    const uvpaa = outPtr.deref() !== 0;

    console.log('UVPAA available:', uvpaa, 'hr=0x' + (hr >>> 0).toString(16));

    // If you also mapped WebAuthNGetErrorName (PCWSTR), you can read it like this:
    // const namePtr = webauthn.WebAuthNGetErrorName(hr);
    // const name = namePtr.isNull() ? '' : namePtr.reinterpretUntilZeros(2).toString('ucs2');
    // console.log('HRESULT name:', name);
    112 changes: 18 additions & 94 deletions webauthn.py
    Original file line number Diff line number Diff line change
    @@ -1,97 +1,21 @@
    # webauthn_probe.py
    import ctypes
    from ctypes import wintypes
    import json
    # pip install fido2
    import os

    # --- Types & DLL ------------------------------------------------------------
    HRESULT = ctypes.c_long # 32-bit signed
    DWORD = wintypes.DWORD
    BOOL = wintypes.BOOL
    LPCWSTR = wintypes.LPCWSTR
    HWND = wintypes.HWND

    webauthn = ctypes.WinDLL("webauthn.dll") # loads %SystemRoot%\System32\webauthn.dll

    # DWORD WebAuthNGetApiVersionNumber(void);
    webauthn.WebAuthNGetApiVersionNumber.restype = DWORD

    # HRESULT WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable(BOOL *out);
    webauthn.WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable.argtypes = [ctypes.POINTER(BOOL)]
    webauthn.WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable.restype = HRESULT

    # PCWSTR WebAuthNGetErrorName(HRESULT hr);
    webauthn.WebAuthNGetErrorName.argtypes = [HRESULT]
    webauthn.WebAuthNGetErrorName.restype = LPCWSTR

    print("WebAuthN API version:", webauthn.WebAuthNGetApiVersionNumber())

    is_uvpaa = BOOL()
    hr = webauthn.WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable(ctypes.byref(is_uvpaa))
    print("UVPAA available:", bool(is_uvpaa.value), "hr=0x%08X" % (hr & 0xFFFFFFFF),
    webauthn.WebAuthNGetErrorName(hr) or "")

    # --- Minimal GetAssertion probe --------------------------------------------
    # This builds the tiny structs needed for WebAuthNAuthenticatorGetAssertion
    # and calls it with *no options*. If a WebAuthn channel is available in a
    # remote session, you'll see the Windows Security UI on the client. Otherwise,
    # you'll typically get a timeout / NotAllowedError.
    #
    # Signatures: https://learn.microsoft.com/windows/win32/api/webauthn/
    # GetAssertion: https://learn.microsoft.com/windows/win32/api/webauthn/nf-webauthn-webauthnauthenticatorgetassertion
    # WEBAUTHN_CLIENT_DATA shape: https://learn.microsoft.com/windows/win32/api/webauthn/ns-webauthn-webauthn_client_data

    # Constants from webauthn.h
    WEBAUTHN_CLIENT_DATA_CURRENT_VERSION = 1 # current version
    WEBAUTHN_HASH_ALGORITHM_SHA_256 = "SHA-256" # documented hash alg ID

    class WEBAUTHN_CLIENT_DATA(ctypes.Structure):
    _fields_ = [
    ("dwVersion", DWORD),
    ("cbClientDataJSON", DWORD),
    ("pbClientDataJSON", ctypes.POINTER(ctypes.c_ubyte)),
    ("pwszHashAlgId", LPCWSTR),
    ]

    # HRESULT WebAuthNAuthenticatorGetAssertion(
    # HWND hWnd, LPCWSTR rpId, PCWEBAUTHN_CLIENT_DATA pClientData,
    # PCWEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS pOptions, PWEBAUTHN_ASSERTION *ppAssertion);
    webauthn.WebAuthNAuthenticatorGetAssertion.argtypes = [
    HWND, LPCWSTR, ctypes.POINTER(WEBAUTHN_CLIENT_DATA), ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p)
    ]
    webauthn.WebAuthNAuthenticatorGetAssertion.restype = HRESULT

    # VOID WebAuthNFreeAssertion(PWEBAUTHN_ASSERTION pAssertion);
    webauthn.WebAuthNFreeAssertion.argtypes = [ctypes.c_void_p]
    webauthn.WebAuthNFreeAssertion.restype = None

    # Build a minimal CollectedClientData for an assertion ("webauthn.get").
    # The challenge can be any bytes for this probe; real flows use server-provided values.
    challenge_b64url = "dGVzdC1jaGFsbGVuZ2U" # "test-challenge" base64url, no '=' padding
    client_data = {
    "type": "webauthn.get",
    "challenge": challenge_b64url,
    # IMPORTANT: origin must match the RP you expect credentials for; this is a probe.
    "origin": "https://webauthn.io",
    }
    client_json = json.dumps(client_data).encode("utf-8")
    buf = (ctypes.c_ubyte * len(client_json)).from_buffer_copy(client_json)

    cd = WEBAUTHN_CLIENT_DATA()
    cd.dwVersion = WEBAUTHN_CLIENT_DATA_CURRENT_VERSION
    cd.cbClientDataJSON = len(client_json)
    cd.pbClientDataJSON = ctypes.cast(buf, ctypes.POINTER(ctypes.c_ubyte))
    cd.pwszHashAlgId = WEBAUTHN_HASH_ALGORITHM_SHA_256

    # rpId typically matches the site (e.g., "webauthn.io"). Options = NULL for defaults.
    ppAssertion = ctypes.c_void_p()
    hr = webauthn.WebAuthNAuthenticatorGetAssertion(
    HWND(0), LPCWSTR("webauthn.io"), ctypes.byref(cd), None, ctypes.byref(ppAssertion)
    from fido2.client.windows import WindowsClient
    from fido2.webauthn import PublicKeyCredentialRequestOptions

    origin = "https://webauthn.io"
    client = WindowsClient(origin) # wraps webauthn.dll on Windows

    # Build proper WebAuthn request options
    options = PublicKeyCredentialRequestOptions(
    challenge=os.urandom(32),
    rp_id="webauthn.io",
    timeout=60000,
    user_verification="discouraged", # we're just probing the transport
    )

    print("GetAssertion hr=0x%08X" % (hr & 0xFFFFFFFF), webauthn.WebAuthNGetErrorName(hr) or "")

    if hr == 0: # S_OK
    # In a real program you'd parse the WEBAUTHN_ASSERTION struct here.
    # Always free the assertion blob.
    webauthn.WebAuthNFreeAssertion(ppAssertion)
    try:
    result = client.get_assertion(options)
    print("OK, transport works. Got", len(result.get_response(0).signature), "bytes of signature")
    except Exception as e:
    print("GetAssertion failed:", repr(e))
  3. dzmitry-savitski created this gist Aug 13, 2025.
    28 changes: 28 additions & 0 deletions webauthn.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,28 @@
    // webauthn_probe.js
    const ffi = require('ffi-napi');
    const ref = require('ref-napi');

    // Map a couple of functions. (We avoid wide-string handling here to keep it simple.)
    const webauthn = ffi.Library('webauthn', {
    // DWORD WebAuthNGetApiVersionNumber(void)
    WebAuthNGetApiVersionNumber: ['uint32', []],
    // HRESULT WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable(BOOL *out)
    WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable: ['int32', ['pointer']],
    // If you want error names, add:
    // WebAuthNGetErrorName: ['pointer', ['int32']], // returns PCWSTR (UTF-16)
    });

    const apiVersion = webauthn.WebAuthNGetApiVersionNumber();
    console.log('WebAuthN API version:', apiVersion);

    const BOOL = ref.types.int; // Windows BOOL is a 32-bit int
    const outPtr = ref.alloc(BOOL);
    const hr = webauthn.WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable(outPtr);
    const uvpaa = outPtr.deref() !== 0;

    console.log('UVPAA available:', uvpaa, 'hr=0x' + (hr >>> 0).toString(16));

    // If you also mapped WebAuthNGetErrorName (PCWSTR), you can read it like this:
    // const namePtr = webauthn.WebAuthNGetErrorName(hr);
    // const name = namePtr.isNull() ? '' : namePtr.reinterpretUntilZeros(2).toString('ucs2');
    // console.log('HRESULT name:', name);
    97 changes: 97 additions & 0 deletions webauthn.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,97 @@
    # webauthn_probe.py
    import ctypes
    from ctypes import wintypes
    import json
    import os

    # --- Types & DLL ------------------------------------------------------------
    HRESULT = ctypes.c_long # 32-bit signed
    DWORD = wintypes.DWORD
    BOOL = wintypes.BOOL
    LPCWSTR = wintypes.LPCWSTR
    HWND = wintypes.HWND

    webauthn = ctypes.WinDLL("webauthn.dll") # loads %SystemRoot%\System32\webauthn.dll

    # DWORD WebAuthNGetApiVersionNumber(void);
    webauthn.WebAuthNGetApiVersionNumber.restype = DWORD

    # HRESULT WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable(BOOL *out);
    webauthn.WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable.argtypes = [ctypes.POINTER(BOOL)]
    webauthn.WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable.restype = HRESULT

    # PCWSTR WebAuthNGetErrorName(HRESULT hr);
    webauthn.WebAuthNGetErrorName.argtypes = [HRESULT]
    webauthn.WebAuthNGetErrorName.restype = LPCWSTR

    print("WebAuthN API version:", webauthn.WebAuthNGetApiVersionNumber())

    is_uvpaa = BOOL()
    hr = webauthn.WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable(ctypes.byref(is_uvpaa))
    print("UVPAA available:", bool(is_uvpaa.value), "hr=0x%08X" % (hr & 0xFFFFFFFF),
    webauthn.WebAuthNGetErrorName(hr) or "")

    # --- Minimal GetAssertion probe --------------------------------------------
    # This builds the tiny structs needed for WebAuthNAuthenticatorGetAssertion
    # and calls it with *no options*. If a WebAuthn channel is available in a
    # remote session, you'll see the Windows Security UI on the client. Otherwise,
    # you'll typically get a timeout / NotAllowedError.
    #
    # Signatures: https://learn.microsoft.com/windows/win32/api/webauthn/
    # GetAssertion: https://learn.microsoft.com/windows/win32/api/webauthn/nf-webauthn-webauthnauthenticatorgetassertion
    # WEBAUTHN_CLIENT_DATA shape: https://learn.microsoft.com/windows/win32/api/webauthn/ns-webauthn-webauthn_client_data

    # Constants from webauthn.h
    WEBAUTHN_CLIENT_DATA_CURRENT_VERSION = 1 # current version
    WEBAUTHN_HASH_ALGORITHM_SHA_256 = "SHA-256" # documented hash alg ID

    class WEBAUTHN_CLIENT_DATA(ctypes.Structure):
    _fields_ = [
    ("dwVersion", DWORD),
    ("cbClientDataJSON", DWORD),
    ("pbClientDataJSON", ctypes.POINTER(ctypes.c_ubyte)),
    ("pwszHashAlgId", LPCWSTR),
    ]

    # HRESULT WebAuthNAuthenticatorGetAssertion(
    # HWND hWnd, LPCWSTR rpId, PCWEBAUTHN_CLIENT_DATA pClientData,
    # PCWEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS pOptions, PWEBAUTHN_ASSERTION *ppAssertion);
    webauthn.WebAuthNAuthenticatorGetAssertion.argtypes = [
    HWND, LPCWSTR, ctypes.POINTER(WEBAUTHN_CLIENT_DATA), ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p)
    ]
    webauthn.WebAuthNAuthenticatorGetAssertion.restype = HRESULT

    # VOID WebAuthNFreeAssertion(PWEBAUTHN_ASSERTION pAssertion);
    webauthn.WebAuthNFreeAssertion.argtypes = [ctypes.c_void_p]
    webauthn.WebAuthNFreeAssertion.restype = None

    # Build a minimal CollectedClientData for an assertion ("webauthn.get").
    # The challenge can be any bytes for this probe; real flows use server-provided values.
    challenge_b64url = "dGVzdC1jaGFsbGVuZ2U" # "test-challenge" base64url, no '=' padding
    client_data = {
    "type": "webauthn.get",
    "challenge": challenge_b64url,
    # IMPORTANT: origin must match the RP you expect credentials for; this is a probe.
    "origin": "https://webauthn.io",
    }
    client_json = json.dumps(client_data).encode("utf-8")
    buf = (ctypes.c_ubyte * len(client_json)).from_buffer_copy(client_json)

    cd = WEBAUTHN_CLIENT_DATA()
    cd.dwVersion = WEBAUTHN_CLIENT_DATA_CURRENT_VERSION
    cd.cbClientDataJSON = len(client_json)
    cd.pbClientDataJSON = ctypes.cast(buf, ctypes.POINTER(ctypes.c_ubyte))
    cd.pwszHashAlgId = WEBAUTHN_HASH_ALGORITHM_SHA_256

    # rpId typically matches the site (e.g., "webauthn.io"). Options = NULL for defaults.
    ppAssertion = ctypes.c_void_p()
    hr = webauthn.WebAuthNAuthenticatorGetAssertion(
    HWND(0), LPCWSTR("webauthn.io"), ctypes.byref(cd), None, ctypes.byref(ppAssertion)
    )

    print("GetAssertion hr=0x%08X" % (hr & 0xFFFFFFFF), webauthn.WebAuthNGetErrorName(hr) or "")

    if hr == 0: # S_OK
    # In a real program you'd parse the WEBAUTHN_ASSERTION struct here.
    # Always free the assertion blob.
    webauthn.WebAuthNFreeAssertion(ppAssertion)