Skip to content
ecdsa.com
Python

py_webauthn

Duo Labs' py_webauthn, the reference WebAuthn relying-party library for Python: DER-only assertion signatures and strict counters, but user verification is not enforced unless you ask for it.
License
BSD-3-Clause
Checked version
3.0.1 ·
Source
github.com/duo-labs/py_webauthn
Documentation
duo-labs.github.io/py_webauthn/

Compatibility at a glance

WebAuthn

py_webauthn (the webauthn package on PyPI) generates registration and authentication options and verifies the responses, including every attestation format, on top of pyca/cryptography. The check fed it the same software-authenticator registration and assertions used for SimpleWebAuthn, go-webauthn and WebAuthn4J, which is what makes the differences between them visible.

The policy defaults are the loosest of the four. generate_registration_options() sends no authenticatorSelection member at all, so browsers fall back to user verification preferred and resident keys discouraged — passkeys that are not discoverable — and verify_registration_response() and verify_authentication_response() accepted responses with the UV flag clear unless require_user_verification=True is passed. A login built on the defaults therefore accepts a tap on a security key without a PIN. Set the policy explicitly on both sides, as the sample does.

Assertion signatures must be ASN.1 DER: a raw 64-byte r‖s signature failed with Could not verify authentication signature, a message that does not mention the encoding; the DER ⇄ raw converter shows whether a test client produced the wrong form. A high-S DER signature verified. Ed25519 (-8), ES256 (-7) and RS256 (-257) are offered by default; there is no ML-DSA entry, unlike SimpleWebAuthn 14.

Counters are strict: Response sign count of 3 was not greater than current count of 5, and the same for 0 after 5, while 0 after 0 — what synced passkeys report — verified. Store the returned new_sign_count after every successful login.

Decode raw browser output with the WebAuthn inspector and experiment safely in the passkey sandbox.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/py-webauthn/run.sh ran py_webauthn 3.0.1 in the container image python:3.14.8-slim-trixie on October 20, 2026; the page must match what it printed, and each fact lists those lines.

  • Default COSE algorithms

    -8-7-257

    generate_registration_options() lists COSE algorithms -8 (Ed25519), -7 (ES256) and -257 (RS256).

    Recorded by the check
    fact webauthn-default-algorithms = -8, -7, -257
  • Default attestation

    none

    Attestation conveyance defaults to none.

    Recorded by the check
    fact webauthn-attestation = none
  • Default user verification

    preferred

    No authenticatorSelection is sent by default, so the browser applies the WebAuthn default preferred.

    Recorded by the check
    fact webauthn-user-verification = preferred
    evidence webauthn-user-verification: authenticatorSelection in the generated options: absent
  • Default resident key

    discouraged

    With no authenticatorSelection, the WebAuthn default discouraged applies, so authenticators are not asked to create discoverable credentials.

    Recorded by the check
    fact webauthn-resident-key = discouraged
  • User verification enforced by default

    no

    A registration and an assertion with only UP set verified; with require_user_verification=True the assertion failed with User verification is required but user was not verified during authentication.

    Recorded by the check
    fact webauthn-uv-enforced-by-default = no
    evidence webauthn-uv-enforced-by-default: registration with the UV flag clear: verified
  • Assertion signature encoding

    der

    A correct ES256 assertion with a DER signature verified.

    Recorded by the check
    fact webauthn-assertion-encoding = der
  • Raw r‖s assertion signature

    rejected

    A raw 64-byte r‖s signature failed with InvalidAuthenticationResponse: Could not verify authentication signature.

    Recorded by the check
    fact webauthn-raw-signature = rejected
  • High-S assertion signature

    accepted

    A DER signature with s replaced by n − s verified.

    Recorded by the check
    fact webauthn-high-s = accepted
  • Signature counter going back

    rejected

    Counters 3 and 0 after a stored 5 failed with Response sign count of … was not greater than current count of 5; 0 after a stored 0 verified.

    Recorded by the check
    fact webauthn-counter-regression = rejected
  • Ed25519 credentials

    supported

    An Ed25519 (COSE -8) credential registered and authenticated successfully.

    Recorded by the check
    fact webauthn-ed25519 = supported
    evidence webauthn-ed25519: verified

Usage

Generate registration and authentication options that require user verification and a discoverable credential, instead of the library defaults.
import json

from webauthn import generate_authentication_options, generate_registration_options, options_to_json
from webauthn.helpers.structs import AuthenticatorSelectionCriteria, ResidentKeyRequirement, UserVerificationRequirement

# The defaults send no authenticatorSelection at all, and verification does not
# require UV. State the policy in the options, and pass
# require_user_verification=True to verify_registration_response() and
# verify_authentication_response() so the server enforces the same policy.
registration = generate_registration_options(
    rp_id="example.com",
    rp_name="Example",
    user_name="user@example.com",
    authenticator_selection=AuthenticatorSelectionCriteria(
        resident_key=ResidentKeyRequirement.REQUIRED,
        user_verification=UserVerificationRequirement.REQUIRED,
    ),
)
authentication = generate_authentication_options(rp_id="example.com", user_verification=UserVerificationRequirement.REQUIRED)

selection = json.loads(options_to_json(registration))["authenticatorSelection"]
print("uv:", selection["userVerification"], json.loads(options_to_json(authentication))["userVerification"])
print("resident key:", selection["residentKey"])

The snippet harness runs every sample verbatim in the container image python:3.14.8-slim-trixie, and the run fails unless the output matches.

Setup
pip install --quiet --root-user-action=ignore webauthn==3.0.1
Command
python usage.py
Output must match (regular expression)
uv: required required\nresident key: required

Libraries