feat(auth): pin the WebAuthn credential algorithm set and drop RS256 (BACKLOG #1166) - #919
feat(auth): pin the WebAuthn credential algorithm set and drop RS256 (BACKLOG #1166)#919wshallwshall wants to merge 2 commits into
Conversation
…(BACKLOG #1166)
The relying party inherited py_webauthn's default algorithm set, which carries RS256
(-257). A COSE RSA identifier fixes the padding and the hash and leaves the MODULUS
unbounded, so it cannot carry the 128-bit floor ASVS 11.2.3 asks for.
Re-measured by execution before building, on the pinned webauthn==3.0.0 from this
worktree's own venv: an RS256 credential over a 2048-bit modulus registered and was
ACCEPTED, and so did one over a 1024-bit modulus. Positive controls in the same run:
ES256 accepted, garbage refused, PS256 refused (so the library's algorithm screen was
running and discriminating, and the RS256 acceptance was a real allow).
The item's row understates this twice, and the BACKLOG note corrects it:
1. verify_registration_response takes the SAME supported_pub_key_algs parameter and
was also at the library default. Advertising is a hint an authenticator may
ignore; the verify call is the only refusal. Restricting the advertised set alone
would have read like a fix while accepting exactly the same credentials.
2. The accepted set had no floor at all, not merely a 2048-bit one.
Built: one module constant, SUPPORTED_COSE_ALGS = (-8, -7), fed to BOTH ceremony
halves through a single lazy resolver so the offer and the refusal cannot drift. EdDSA
and ES256 carry no equivalent hole: the curve rides in the credential rather than the
identifier, but a credential whose curve is unknown or mismatched cannot produce a
verifiable assertion (measured), so no sub-floor EC2 or OKP credential is ever usable.
Cost, stated rather than hidden: an authenticator offering only RS256 can no longer
enrol a passkey, and TPM-backed Windows Hello is that population. Those operators keep
TOTP, which ADR 0068's 2026-07-17 amendment already records as the alternative second
factor. Deliberately NOT an operator setting: a knob re-admitting -257 would be the
operator-supplied weak configuration this requirement is failing on.
Proof: three mutations, each red, each compiled first and checked to FAIL rather than
ERROR. Eight new rows, one of which fires if a future release narrows the library
default to match the pin, so the file cannot pass vacuously.
Instrument check: this requirement's recorded absence pattern keys on a 3072-bit RSA
comparison. All 341 added lines swept with it, zero matches, against a positive
control that matched a planted comparison. The guard still fires; no re-cut needed.
Does NOT close #1166. Its verdict moves by a scorecard re-score, which no Builder can
perform, and other named surfaces stay open.
|
This pull request is green and nobody has marked it read. Every check that has reported is passing and the To clear it, IN THIS ORDER:
DO NOT TRIAGE THIS FROM A seat can find every pull request in this state in one call: The |
One limb of BACKLOG #1166, the WebAuthn advertised-algorithm restriction. This does NOT close #1166 and does not flip its banner: the item's verdict moves by a scorecard re-score, which no Builder can perform, and several named surfaces stay open.
What was wrong
The relying party inherited py_webauthn's default algorithm set, which carries RS256 (
-257). A COSE RSA identifier fixes the padding and the hash and leaves the modulus unbounded, so it cannot carry the 128-bit floor ASVS 11.2.3 asks for.Re-measurement, by execution, before building
Interpreter named rather than assumed:
C:\...\worktrees\manager-277cbb-b1166\.venv\Scripts\python.exe, CPython 3.14.6. Library versions introspected and compared to the lock in the same run, since the load-bearing question here is what the pinned library advertises:requirements.lockwebauthncryptographycbor2All three agree, so the introspection is evidence about shipped behaviour. Neither bare
pythonnor the primary checkout's venv was used.The row is true and understates the defect on two axes. Both corrections are appended inside item #1166's own section.
verify_registration_responsetakes the samesupported_pub_key_algsparameter and was also at the library default. An authenticator may ignore what is advertised; the verify call is the only place a credential is refused. A change restricting only the advertisement would have read like a fix and accepted exactly the same credentials, which is the shape this item exists to catch.Three positive controls in the same run: ES256 accepted (the fixture builds valid credentials), a garbage response refused (the verifier is live), and PS256 (
-37) refused (the library's algorithm screen was running and discriminating, so the RS256 acceptance was a real allow rather than an unreached branch).What was built
One module constant,
SUPPORTED_COSE_ALGS = (-8, -7)(EdDSA, ES256), fed to both ceremony halves through a single lazy resolver, so the offered set and the accepted set cannot drift. Plain ints at module scope so the module still imports without the optional[webauthn]extra.EdDSA and ES256 carry no equivalent hole. The curve rides in the credential rather than in the identifier, but a credential whose curve is unknown or does not match its key cannot produce a verifiable assertion (measured: it registers, then every assertion against it is refused), so no sub-floor EC2 or OKP credential is ever usable. That asymmetry is why the floor is expressible here as a set of identifiers, and it is the same reasoning the 2026-08-22 signing-key limb recorded for EC.
Also: an ADR 0068 amendment recording the pin as a third policy pin beside
attestation=NONEanduser_verification=PREFERRED.The cost, stated rather than hidden
An authenticator that offers only RS256 can no longer enrol a passkey. TPM-backed Windows Hello is the population that registers RSA credentials. This cost is reasoned, not measured -- no real authenticator was driven here, only the pinned library. Those operators keep TOTP, which ADR 0068's 2026-07-17 amendment already records as the alternative second factor, and the password leg is unchanged.
Deliberately not an operator setting: a knob re-admitting
-257would be exactly the operator-supplied weak configuration this requirement is failing on.Proof
Three mutations, each red, each compiled before running so a syntax break could not masquerade as a behaviour failure, and each checked to FAIL rather than ERROR:
-257re-admitted to the pinned setEight new rows in
tests/test_webauthn_algs.py. One fires if a futurewebauthnrelease narrows its own default to match the pin, so the file cannot pass vacuously; another drives the library's own default path over the same RSA fixture, so a refusal row cannot pass on a broken fixture.The instrument trap was checked and did not fire
This requirement's recorded absence pattern keys on a 3072-bit RSA modulus comparison. All 341 added lines were swept with it: zero matches, against a positive control in the same run where a planted comparison matched. The claim's own positive control still speaks in the corpus. No re-cut was needed and none was made.
A separate glyph sweep over the added lines found zero symbol-other characters (CLAUDE.md section 11), with a positive control.
Cross-item note, recorded rather than edited elsewhere
#1168 (ASVS 11.3.1) names this same site from the padding angle: "the shipped relying party advertises COSE -257 and every enrolled credential is verified with PKCS#1 v1.5 at each login". No credential can now be enrolled under
-257, so that sentence is stale for new enrolments. Whoever works #1168 should re-measure rather than relay it. I did not edit #1168's section.A measured residual, unfiled and named by subject
The COSE
crvfield is not validated at registration, so a credential whose curve is unknown or mismatched enrols and only fails at first assertion. On the mismatched-curve path the failure arrives as a rawValueErrorfromcryptographyrather than aWebAuthnException, soverify_assertion'sexcept WebAuthnExceptiondoes not catch it and the rejection would not land on the audited invalid-input path ADR 0068 decision 1 requires. Self-harm only (the enrolling user is authenticated and breaks their own credential). This change neither causes nor fixes it. Recorded in the backlog note and the ADR amendment.Checks run
All from this worktree's own venv.
ruff check .andruff format --check .-- cleanmypy messagefoundry(strict) --Success: no issues found in 268 source filespytest tests/test_webauthn_algs.py tests/test_webauthn.py tests/test_webauthn_store.py tests/test_mfa.py-- 46 passedpytest tests/test_crypto_inventory_doc.py tests/test_crypto_inventory_scanner.py tests/test_security_static.py tests/test_key_usage_scope_inventory.py tests/test_security_doc_drift.py-- 142 passedpytest tests/test_api_auth.py tests/test_groups.py-- 83 passedpytest tests/test_seam_discovery.py tests/test_session_rotation_primitive.py tests/test_new_dependency_check.py tests/test_worktree_venv_extras_parity.py tests/test_scan_tokens_source.py-- 331 passedpytest tests/ -k asvs-- 564 passed, 12 skipped, 15350 deselectedscripts/security/crypto_inventory_check.py-- passed.messagefoundry/auth/webauthn.pywas already inventoried forsecrets+webauthnand gains no new crypto import;tests/is outsideWALK_ROOTS, so the new file'scryptographyimport registers nothing.Checks NOT run, and the legs somebody must read after I exit
-k asvswas run as a wider sweep.ubuntu-latest. Everything above ran on Windows only. That platform split has already bitten this item once (a prior limb wrote a Windows OpenSSL cipher census into tests as though it were a fact about TLS and went red on ubuntu). Nothing here reads a platform cipher inventory, but read the leg.pip-audit,cla, and the rolled-upCI gate-- never runnable locally.[webauthn]extra hazard, and it did NOT apply here. These testsimportorskip("webauthn"), so without the extra a real downgrade would run completely green. The extra is installed in this worktree (3.0.0, matching the lock) and all eight rows executed rather than skipping. Confirm the CI leg that installs the extra actually ran them rather than skipping them.