pqkey builds with Rust stable, 1.89 or later; rust-toolchain.toml picks the
toolchain. The daemon and CLI require Linux, with a compile-time diagnostic
on other systems. pqkey-ctap and pqkey-mldsa are portable and can be
linted and tested on their own, including on macOS, as CI's "test (macOS,
portable crates)" job does:
cargo clippy -p pqkey-ctap -p pqkey-mldsa --all-targets --locked -- -D warnings
cargo test --locked -p pqkey-ctap -p pqkey-mldsa --all-targets
cargo test --locked -p pqkey-ctap -p pqkey-mldsa --doc./install.sh is the Linux installation entry point. It rejects other
systems before creating logs, installing tools or changing system setup.
| Script | What it does |
|---|---|
scripts/check.sh |
Runs every check CI runs: formatting, lints, tests, docs, the minimum Rust version, dependencies and coverage. --quick runs formatting, installer checks, lints and tests. On macOS its lints and tests are those of the portable crates above, and it skips the checks that need Linux. |
scripts/install-tests.sh |
Runs the actual installer in disposable Docker filesystems with controlled prerequisites, PTYs and fault injection; requires Docker. |
scripts/e2e.sh |
Runs the end-to-end tests against four test keys, through libfido2 and python-fido2. |
scripts/fuzz.sh |
Runs each fuzz target for 30 seconds, or one target for as long as you ask. |
scripts/diagnose.sh |
Shows the state of the key on this computer: binary, service, devices and log. |
scripts/browser.sh |
Starts a local relying party for testing in real browsers (details). |
Each script prints one line per step and keeps the full output in
target/scripts/. The top of each script says what it needs.
The heap residue test runs credential generation, reconstruction, signing
and store roundtrips under the binary's wiping allocator. Its test allocator
retains freed blocks until they have been scanned, and live controls prove
that the scan finds each secret pattern. CI and scripts/check.sh run it in
debug and release, as they do the stack residue tests.
The final cleanup scan also checks blocks released by the last controls.
ML-DSA drops its owned expanded keys inside a separate operation frame, then
scrubs the retired stack before returning either success or an error. Seed
expansion creates plain locals, moves SHAKE state and clones an expanded key;
wiping the final owned values does not erase all retired copies. Parameter
dispatch precedes the secret-handling closure, so each retired frame holds
only one parameter set's key and decode result. The bounds are 640 KiB for
ML-DSA-44, 960 KiB for ML-DSA-65 and 1,600 KiB for ML-DSA-87: at least 30%
more than the deepest the operations of each parameter set reach without the
scrub, which debug builds do, at up to 454, 724 and 1,195 KiB. The table of
measured depths is in the documentation of scrub_stack in pqkey-mldsa.
The former 1 MiB bound left the deepest 170 KiB of ML-DSA-87's debug frames
unscrubbed, below the copies of rho-prime and K the probes look for.
The stack probes retain their 2 MiB scans and controls, and check both
32-byte halves of the 64-byte rho-prime and K after individual seed,
expanded-key, context, malformed-key and CTAP wrapper operations. The ML-DSA
probe reports how far down it finds each copy, and measures depth too: it
fills the 2 MiB with a canary first, and nothing below the zeros an
operation's scrub leaves may change but the scrub's own frame. Each operation
then runs again from a copy of the wrapper's ml-dsa calls with no scrub
after it, and the probe fails unless the bound is at least 25% deeper than
that run reaches. In debug builds, which go deepest, the copy's frames match
the wrapper's function for function, to within 48 bytes; an optimised build
can inline the copy differently. Linux children also force an OS randomness
failure after seed expansion to check the returned-error cleanup path. CI
forces Rust 1.89.0 for runtime stack and heap probes in debug and release on
x86_64 and aarch64, alongside the stable tests and the declared-MSRV
compilation check.
These are bounded empirical checks of selected patterns and operations. The stack scanners read uninitialized, retired storage, outside Rust's defined memory model; a clean scan is weaker evidence than a detected copy. Compiler, profile and CPU backend change layouts and incidental overwrites. The probes do not establish erasure of every intermediate, register, live caller-owned key, unwinding frame or swap copy. Heap probes separately retain surrendered allocations and require every byte to be zero; that guarantee comes from the daemon's wiping allocator. Standalone wrapper consumers still depend on each owned secret's explicit drop cleanup.
U2F residue cases cover registration certificates and signatures,
authentication with and without presence, and the provisioned attestation
key. Stack checks independently reconstruct the nonces of U2F signing
bases, and look for the P-256 scalar sealed into a key handle after
call_u2f registers it and authenticates with it on a file store; heap
checks retain released blocks for all three attestation modes.
The packet fuzz target includes U2F APDUs, repeats, approval, denial and
abandonment. Structured and sequence targets mix both protocols; the
sequence model verifies U2F signatures, credential eligibility, application
binding, shared counter monotonicity and always-UV. Its traces retain full
U2F status words. Failed resets retain counter floors; successful resets keep
invalidated IDs for the existing individual retired-ID probes. File-store
reset tests inject failures and process exits at every persistence checkpoint
in both layouts, including key publication, counter deletion, manifest and
PIN writes, and verify explicit retry recovery. PIN/configuration regressions
cover every preparation and post-rename checkpoint, both PIN protocols,
legacy and managed stores, live/restarted policy and old-token invalidation.
Private test observations prove unconfirmed retry debits reach no decryption,
comparison or token issuance. The sequence harness schedules pre-publication
failures and committed-but-unconfirmed outcomes, tracks live and published
retry budgets, and reconstructs engines over the same published store. Seeds
cover these outcomes, retry recovery, protocol crossings and U2F disabled by
always-UV. Fault controls exist only in unit tests and the fuzz harness.
Large-blob tests cover fragmented writes, interrupted staging and restart
persistence; the fuzz sequence model checks committed-array visibility and
the lifetime of pending writes, and that credential keys are stable and
returned only when requested. Heap residue tests include those keys and
their registration, assertion and enumeration response copies.
A successful PIN/configuration command publishes its settings immediately. A warning about unconfirmed durability means a process restart observes the new settings, while power loss may roll back the rename. Read getInfo to observe PIN presence, minimum length, forced change and alwaysUv; RP disclosure is tested through the minPinLength extension. Avoid treating a durability warning as evidence that the old PIN or policy is still active.
A retry-write processing error gives no verdict on the supplied PIN and issues no token. Inspect getPINRetries before further attempts: a failed write can conservatively reduce the running budget, and an unconfirmed published debit is visible after restart. Restoring storage permits another attempt if retries remain. Restart clears the three-mismatch power-cycle block; it does not clear persisted retry exhaustion. An unreadable record remains untouched and fails closed until an explicit reset.
Reset preserves attestation and rotates the credential key before removing counters or policy. Earlier errors may leave partial cleanup, but cannot revive invalidated credentials or lower surviving credentials' protections. Retrying explicit reset recovers without needing readable old records. Once all preceding work completes and default PIN state is published, reset returns success and adopts defaults even if the final publication is unconfirmed.
CTAPHID packet fuzzing models partial transmission explicitly: report selection, successful and failed writes, INIT aborts and session teardown. A separate request/worker oracle checks response bytes, ordering and ownership through the final successful write; its incremental parser rejects stale continuations and truncation without an explicit abort. Seeds cover single-report and maximum-length responses, final-report competition, selected-report INIT, keepalives, late workers, cancellation and U2F pending/cached answers.
Raw HID transport E2E tests pace maximum-length PING requests, send a competing
CID during the reply and resynchronize a transmitting channel. Run them on a
Linux host with /dev/uhid; socket and callback tests cannot replace that check.
Management-client recording links verify zero rejected-command reports at
the advertised and HID limits, including authenticated CBOR and getInfo
refreshes. CLI tests preserve PIN policy and the RP list after oversized
configuration; test_cli.py repeats this through real HID with and without
a PIN, then successfully authorizes eight shorter RP IDs.
./install.sh rebuilds pqkey and restarts the key with your changes. To
watch the key's debug log, run it in the foreground. The log includes site and
user names.
pqkey stop
RUST_LOG=debug cargo run --release -p pqkey -- runCtrl-C stops it, and pqkey start brings the service back.
GitHub Actions runs these workflows:
- CI runs the checks of
scripts/check.shon Linux, with workspace tests on both x86_64 and arm64, and lints and tests the portable crates on macOS. - E2E runs
scripts/e2e.sh's real HID tests on Linux. - Security runs
cargo auditandcargo deny. - Fuzz runs each target for 30 seconds on changes and for 30 minutes every week.
Dependabot proposes updates every week. Cargo updates that are
semver-compatible and pass every workflow are merged automatically. Commit
changes to Cargo.lock and fuzz/Cargo.lock together.
The architecture notes describe the boundary between
command and protocol logic and the Linux backend. Keep kernel, udev,
systemd, D-Bus and process operations under
crates/pqkey/src/platform/linux/. Tests inject devices, clocks and
notification servers while retaining Linux uhid assertions. Run the full
workspace and fuzz checks on Linux and the unchanged E2E suite on a Linux
host with /dev/uhid.
On macOS, test the portable crates natively and use a Linux environment,
such as Docker, for daemon and fuzz checks. Container unit tests can use
socket-backed devices; real HID E2E tests require access to /dev/uhid.
Run scripts/install-tests.sh with Docker running. It sends the installer and
fixtures to disposable Linux containers, with no writable host mounts, host
sudo or host devices. INSTALL_TEST_IMAGE may select a compatible preloaded
image (Bash, Python 3 and coreutils); the default is rust:1.89-bookworm.
Both scripts/check.sh modes and the CI installer job run these checks.
Missing Docker is a failed required check rather than an implicit pass.
Fixtures execute the real install.sh, recording prerequisite commands,
installed executables, recovery copies, output and exit statuses. A PTY checks
confirmation and hidden PIN input. Rust setup tests run the shared setup
orchestration in child processes and verify PIN readiness against the existing
socket-backed CTAP client/daemon. No protocol or state-format test seam is
added to the shipped executable.
The E2E workflow additionally runs tests/e2e/test_setup.py with
PQKEY_INSTALLER_E2E=1 after the other test keys stop. This opt-in test requires
a disposable Linux host with sudo, a systemd user manager and /dev/uhid.
It executes installation, setup reruns and executable updates using real HID
clients and the existing fake desktop notification server. It is skipped in
ordinary developer E2E runs so it cannot install over a personal key.
For macOS Linux validation, send a source snapshot to Docker and extract it
with tar -m (or tar -xm) so container rebuilds use the snapshot's contents.
Exclude build artifacts and macOS extended attributes, retain pipeline failure
statuses with set -o pipefail, and reuse the pqkey-cargo,
pqkey-cargo-registry, pqkey-rustup and pqkey-target volumes. Keep fuzz
build outputs separate within the target volume. Containers without
/dev/uhid can run fixture and socket-backed tests, but cannot verify real HID.
Each algorithm is described once, in the table in
crates/pqkey-ctap/src/crypto/alg.rs, and the compiler finds most of what
else has to change.
- Add a
CoseAlgvariant whose value is its COSE identifier, its row inCoseAlg::properties(its name and signature scheme), and its place inCoseAlg::ALL, the order getInfo lists the algorithms in. An algorithm that differs from another only in its identifier, as ESP256 does from ES256, shares its scheme and needs nothing more in Rust. - A new scheme needs a
Schemevariant and a module for its family, ascrypto/ecdsa.rs,crypto/eddsa.rs,crypto/mldsa.rsandcrypto/rsa.rsare. ECDSA or EdDSA on a new curve needs aCurveorEdwardsCurvevariant, a module for the curve next tocrypto/ecdsa_p384.rsorcrypto/eddsa_ed25519.rs, a case intests/residue.rsand, for keys derived from a seed, known answers in its family's module. A new type of key needs aCredentialSecretKeyvariant. Keys kept in a new form need aKeyKind, aPrivateKeyMaterialvariant and a key type instore/codec.rs;CoseAlg::is_sealablechecks both material width and algorithm encoding. Both must fit the existing sealed credential ID. A key that does not, as RSA's primes do not, is stored even for a non-discoverable credential. - Build and run clippy, and handle every match they point to, among them
the test verifier in
crypto/verify.rs, which pqkey's tests and the fuzz targets reach through pqkey-ctap'stest-supportfeature. Compile-time invariants and explicit eligibility tests keep the signed-byte sealed algorithm field separate from the private material width. - Run the tests. The table-driven ones cover the new algorithm, and the
known-answer tests of the table, of getInfo and of the CLI say what to
add to them. If
UNSUPPORTED_ALGORITHMSinfuzz/src/requests.rslists the algorithm, the fuzz crate's tests fail: replace it there with an identifier the key does not support. - Outside Rust, add it to
ALGORITHMSintests/e2e/ctap.pyand totest_get_info.py, toNAMESintests/browser/q.pyandtests/browser/index.html, to thefido2-token -Iline thattests/e2e/libfido2.shexpects (and its register-and-assert tests, if libfido2 implements the algorithm), and to the algorithm lists in the README and the architecture notes.
Then run scripts/check.sh, regenerate the fuzz seeds with cargo run --release --manifest-path fuzz/Cargo.toml --example seeds (the
credential_key seeds pick algorithms by their place in CoseAlg::ALL, so
give a new one layouts in fuzz/examples/seeds.rs first), run
scripts/fuzz.sh, and run scripts/e2e.sh on Linux.
The E2E suite also exercises python-fido2 2.2.1's Ctap1 registration and signature verification, WebAuthn fido-u2f verification, protocol crossings, PIN independence, always-UV and notification polling. A helper repeats only presence-required APDUs. libfido2's forced-U2F commands register and assert ES256 credentials and verify both directions of protocol compatibility. Raw HID tests observe busy replies without the client's automatic retries.
The 2026-10-15 native browser run
verified source 83837b767bb3c6c35bf6871a8289f88aa8715e6d on Ubuntu 24.04 x86_64,
with Rust 1.98.1 and Python 3.14.7. Both browsers completed all R1–R35/L1–L7
presets and the B1/B2 Brainpool probes, including mandatory PIN, discoverable,
replacement and stored-ID controls, with no infrastructure or unexpected failures.
| Exact browser | Verified presets/probes | Expected errors | Measured algorithm limits | Ceremonies |
|---|---|---|---|---|
| Chromium 158.0.8107.0, snapshot 1719332 | 41 | 3 | 0 | 91 |
| Firefox 158.0 | 34 | 1 | 9 | 83 |
Chromium created and asserted all 21 algorithms. Its expected errors are R14
exclusion and R17/R19's inconsistent credProtect inputs. Firefox verified 15
algorithms; its nine limit observations are ML-DSA in both credential modalities,
ES256K, and the two Brainpool probes. Firefox's unknown algorithm filtering and
missing secp256k1 curve representation are documented with pinned sources and
correlated CTAP responses in the acceptance guide.
Each accepted registration has independently verified companion assertions.
Both runs observed native PIN/account actions, exited cleanly and removed their
HID devices. Presence was explicitly automated with auto-approve; this does
not establish manual physical-presence or browser/SPC payment behavior.
The following are historical manual observations from 2026-09-30 on Ubuntu 26.04.1 (aarch64, GNOME 50.1). They are not results from the new automated matrix.
| Client | ES256 | ML-DSA | Notes |
|---|---|---|---|
| Chromium 153 (snap) | ✓ | ✓ | Passkeys and user verification need a PIN on the key. getPublicKey() returns null for ML-DSA and toJSON() leaves the key out, so relying parties read it from the attestation object. |
| Firefox 154 (snap) | ✓ | Drops algorithms it does not know: an ML-DSA-only request fails with NotAllowedError. Without a PIN, its account chooser shows "Unknown account". |
|
| python-fido2 2.2.1 | ✓ | ✓ | Used by the end-to-end tests. |
| libfido2 1.16 | ✓ | Has no ML-DSA type, so fido2-token -L -k cannot list ML-DSA passkeys; pqkey passkeys can. PIN, credential management and hmac-secret work. |
The manually dispatched browser/UHID workflow
builds the current Linux release and exercises the existing RP's R1–R35 and
L1–L7 presets in isolated upstream Chromium and Mozilla Firefox jobs on native
Linux x86_64. See the acceptance guide for pinned
downloads, local entry points, fixture order, native prompt automation, result
policy and diagnostic artifacts. Presence uses the daemon's explicit
auto-approve mode, so these are compatibility measurements rather than manual
physical-presence tests.
Only a completed exact-version native run establishes a preset pass. The matrix separates verified ceremonies, expected errors, measured client capabilities and infrastructure failures, with classic ES256/PIN/discovery and stored-ID controls. Independent RP verification remains mandatory when python-fido2 cannot verify an algorithm; such secondary-library limits are explicit in the evidence. macOS/Docker without UHID can run harness/RP regressions and collection, but that does not execute the browser/HID path. Retain command/status/version records outside the repository and keep them separate from actual acceptance artifacts.
Start with pqkey status, which names what is missing and how to fix it, or
scripts/diagnose.sh for the whole picture.
Installation exits without “pqkey is ready.” The service may already be
installed while a requirement remains incomplete. Keep the log at
~/.cache/pqkey/install.log, fix the reported requirement and rerun
./install.sh in the same desktop terminal. Setup requires an answering HID
device, the installed service and matching notification session, a notification
server advertising actions and message bodies, and a configured PIN with no
outstanding change requirement. --yes approves installation but does not
supply a PIN. Readiness checks do not approve future requests for you.
The installer just added me to plugdev. Setup grants your UID a /dev/uhid
ACL so the existing session and user manager can start the key immediately.
Its hidraw node needs an active local desktop seat for uaccess. A plain SSH
session, container or Xvfb display alone does not establish that permission
path. Rerun ./install.sh to repair permissions; inspect getfacl /dev/uhid
and the device shown by pqkey status if it still fails.
Dunst shows a prompt but no inline buttons. Open its actions with
dunstctl context or your configured context shortcut and choose Approve or
Deny. Dunst needs dmenu or a configured compatible menu. An unanswered request
expires after 30 seconds. Client cancellation and service restart withdraw the
pending prompt; a previous approval cannot authorize the next request.
The installed desktop acceptance guide documents the manually dispatched native test and its artifacts. It uses automated interaction with real Dunst, separately from browser compatibility tests using explicit auto-approve presence and E2E tests using a fake notification server. On 2026-10-15, native run 38828959894 passed all 34 fresh-install and 15 baseline-update cases on Ubuntu 24.04 x86_64 with Dunst 1.9.2, including default-presence failures/recovery, retained credentials/PIN/policy and both cleanup layers. Its public signatures and consecutive counters were independently replayed; the guide records source and runtime identities. This establishes the tested automated desktop interaction, not physical human presence or untested desktop environments.
The key stops answering and pqkey stop hangs. The Linux uhid queue has
an unlocked read race, seen especially on arm64. When it hits, dmesg shows
the following, with uhid_char_read in the call trace:
usercopy: Kernel memory exposure attempt detected from null address (offset 0, size 4376)
kernel BUG at mm/usercopy.c:102
pqkey waits for each event to settle before reading it to avoid the race, but cannot rule it out. Restart the key; if its process cannot be stopped, reboot.
The browser waits, but no prompt appears. On GNOME a prompt can queue behind another banner, so pqkey nudges the queue every 2 seconds. The prompt is also in the notification list (click the clock).
Every registration or sign-in fails at once. No notification could be
shown, so the key said no. journalctl --user -u pqkey says why. If your
desktop starts its own D-Bus session bus, see the comments in
contrib/systemd/user/pqkey.service.
A snap browser never asks for the key. Snaps open only devices tagged for
them, and the udev rules tag the key for the Firefox and Chromium snaps. If
pqkey status says the tag is missing, run ./install.sh.
pqkey passkeys lists nothing, although sites accepted the key. It lists
passkeys, the credentials the key can find by itself. A site that registers it
as a second factor (residentKey: "discouraged") keeps the credential's ID
and sends it back to sign in, and so does Chromium when the key has no PIN.
Those sign-ins work, but the key does not list them, and their prompt says
"Register a security key" instead of "Create a passkey".
Chromium says "Your device can't be used with this site". Chromium uses a
security key for passkeys only if it has a PIN. Set one with pqkey pin.
Firefox's account chooser shows "Unknown account". Without a PIN, the key leaves user names out of its answer (CTAP 2.3 §6.2.2). With a PIN set, Firefox asks for it and shows the names.
Chromium shows "Something went wrong" after Deny or after 30 seconds. The request was denied or timed out. Chromium keeps its sheet open until you click Cancel.
Another program fails while a browser waits for approval. The key serves one request at a time and answers the others "busy", as CTAP requires. Answer or cancel the waiting request first.
The service and the command use different passkeys. If you set
XDG_DATA_HOME in your shell, set it for the systemd user manager too (see
environment.d(5)).
The CTAPHID state machine tests inject monotonic milliseconds and check exact
lease and identification expiry, resynchronization, selected output, and final
successful writes. The independent fuzz model tracks leases and identification
tokens separately from transactions and the CTAP worker. Regenerate only these
seeds with cargo run --locked --manifest-path fuzz/Cargo.toml --example seeds -- --ctaphid-only; the encoder round-trips each seed and runs the oracle.
Real HID tests allocate competing CIDs before acquiring a lease and release
leases in cleanup, including after failed assertions and partial responses.
The disposable notification server checks Python FIDO WINK and libfido2's
forced-U2F touch API, which sends WINK before its separate presence operation.
The two notifications are checked independently. Prepare the pinned Python
environment before running tests/e2e/libfido2.sh; E2E_PYTHON selects it.
On macOS, distinguish E2E syntax and collection from actual HID execution.
Containers without /dev/uhid can run simulated-device tests and sanitizer
fuzzing, but cannot execute the real HID suite. Record validation architecture,
compiler versions, command exit statuses, and each fuzz target's explicit
30-second budget. Source snapshots must come from git archive, extracted
with tar -xm, with separate cached outputs for each compiler and architecture.
CTAP 2.3 §12.9 and §6.8, February 26, 2026 Proposed Standard
support is tested at the authenticator boundary for every
advertised algorithm and both credential modalities. Rust regressions verify
signed bytes and counters, mixed next assertions, legacy record encodings,
credential enumeration and updates, restart/reset/replacement, capacity,
filesystem fault/crash recovery, protection and ES256 protocol crossings.
The fuzz model independently follows registration metadata, assertion requests
and per-credential next-assertion outputs. Regenerate all seeds after changing
its input layout; fixture encoders round-trip against the locked arbitrary
dependency before the corpus is written.
tests/e2e/test_third_party_payment.py covers real Python FIDO HID requests.
Its malformed-input tests bypass client validation and send CBOR null directly,
because python-fido2 2.2.1 cannot encode null. libfido2_payment.py uses only
libfido2 1.14.0 APIs: that version cannot request the extension or expose its CM
flag. Its management API always sends prototype command 0x41; pqkey now
advertises credentialMgmtPreview and its native check requires successful
enumeration. Standard 0x0A enumeration is checked through Python FIDO. The
native library asserts payment-enabled credentials and verifies signed
authenticator data containing the unknown extension. The shell runner includes
the check before its final summary.
The portable payment_compatibility example emits 12 real CTAP engine
assertions. Pass its stdout to libfido2_payment.py --fixtures PATH in the pinned
Python environment to verify absent/false/true requests, increasing counters
and signature rejection after changing the payment boolean without a HID node.
This verifies signed-byte compatibility; real transport and enumeration checks
still require a Linux host with /dev/uhid.
Browser/WebAuthn and Secure Payment Confirmation verification are separate. The client platform determines payment eligibility and UI; advertising this extension does not establish browser or SPC support. Historical client results above concern authentication, and do not claim payment verification.
Final CTAP 2.3 §§6.13, 6.8 and 6.5
and the prototype wire format
define compatibility command 0x41. Subcommand 7 is a documented backport of
standard user updates, required by libfido2 1.14.0's native implementation.
Rust engine regressions exercise both PIN protocols, explicit legacy issuance,
modern permission isolation, RP scopes, expiry and consumption, reissue,
PIN/policy publication failures, restart/reset, family switches and independent
cursors. They check all seven operations, public keys for every advertised
algorithm, capacity, truncation, rejected mutation usability, protection,
largeBlobKey and thirdPartyPayment preservation. Preview uses the current
protocol MAC verifier; repeated MAC failures never debit persistent PIN retries.
The sequence oracle independently tracks authorization and cursor grants,
including valid MAC use before later permission/data rejection. Fixture encoders
round-trip against the locked arbitrary graph. After input-layout changes,
regenerate every seed with cargo run --locked --release --manifest-path fuzz/Cargo.toml --example seeds, repeat, and compare filenames and contents.
tests/e2e/test_credential_management_preview.py sends actual HID 0x41
requests. python-fido2 2.2.1's convenience API selects 0x0A when credMgmt is
present, so preview tests use its raw command API without modifying getInfo.
Calling ClientPin.get_pin_token(PIN) obtains the legacy grant; explicit CM
permissions obtain a modern grant. Tests include exact authentication statuses,
RP restrictions, family switches, independent cursors and WINK/LOCK preservation.
tests/e2e/libfido2_credman.py keeps libfido2's native HID I/O and exercises
metadata, multi-RP/credential enumeration, user updates and deletion with ES256,
ES384, EdDSA and RS256. Raw checks cover mixed payment flags and large-blob keys;
libfido2 1.14 exposes largeBlobKey but ignores the additional payment field.
Its separate ML-DSA-44 decoder check fits the library's 2 KiB receive buffer and
expects FIDO_ERR_RX_INVALID_CBOR while raw preview enumeration and credential
signing still succeed. An ML-DSA-87 public key exceeds that old receive buffer.
Supported credentials are never filtered for older decoders. Every shell
run_test precedes the summary,
including the existing payment helper's advertised-preview success branch.
Engine checks and E2E syntax/collection can run on macOS and Linux/aarch64
containers. Actual HID execution requires Linux with /dev/uhid; container
fixtures and collection do not establish native transport success. Keep exact
commands, statuses, compiler/architecture details, deterministic regeneration
results and unavailable HID/x86_64 execution in validation evidence outside the
repository. Use fresh git archives extracted with tar -xm, preserve pipeline
failures, and separate cache outputs by compiler, architecture and sanitizer.
Start and export the disposable D-Bus session before every test key, including auto-approve keys: WINK identification uses notifications in every presence mode.
tests/e2e/brainpool_vectors.py regenerates the committed independent vectors
with pinned cryptography/OpenSSL, hashlib and Python integers. The vectors cover
two seeds per curve, private/public derivation, SHAKE output, RFC 6979 nonces,
and signatures with the correct and wrong hashes. Existing derivation vectors
remain regression requirements. The stack probe independently computes both
new scalars/nonces, handles bits2octets reduction and rejected candidates, and
retains its full 2 MiB scan; heap controls include scalars, SHAKE output and
nonces in both byte orders and retain the all-freed-bytes-zero requirement.
Run cargo run --locked -p pqkey-ctap --example brainpool_compatibility and pass
stdout to tests/e2e/brainpool_compatibility.py --fixtures PATH in the pinned
Python environment. It independently verifies eight registrations and sixteen
assertions with OpenSSL, including both stored modalities, payment/blob metadata,
DER and tampering. This offline path does not establish HID or browser support.
Raw HID regressions use the existing Python FIDO cryptography helpers with
Brainpool curves and cover PIN protocols, credProtect and management.
The libfido2 1.14.0 compatibility tests use its existing algorithm getters and
fido_cred_set_type, plus native preview management. Advertising -265/-267
succeeds; unsupported native credential types and COSE decoders fail as expected.
Keep ES256/ES384/EdDSA/RS256 and ML-DSA decoder compatibility gates intact;
do not filter authenticator credentials for an older decoder or imply verified
Brainpool browser support. Non-discoverable Brainpool credentials cost a storage
slot even when payment is disabled.
After any algorithm/fixture-layout change, regenerate every committed fuzz seed
with cargo run --locked --release --manifest-path fuzz/Cargo.toml --example seeds
twice and compare filenames/content hashes. The explicit sequence fixtures carry
an algorithm index and check their own round trips against locked arbitrary.
Keep source snapshots, exact commands/statuses, compiler and architecture
identity, full probe logs, corpus manifests and an explicit 30-second sanitizer
result for every fuzz target outside the repository. Linux snapshots come from
git archive extracted with tar -xm, with separate
compiler/architecture/sanitizer caches.
tests/e2e/persistent_management.py builds raw ClientPIN pcmr requests with the
selected Python FIDO protocol. The raw HID suite covers both encrypted lengths,
MAC lengths, permission negotiation, read-only operations and WINK/LOCK;
test_cli.py uses dedicated daemons for restart and PIN rotation. These require
Linux with /dev/uhid; successful collection does not establish HID execution.
libfido2 1.14.0 compatibility gates remain unchanged. Its existing management API
continues to use writable cm; raw Python supplies the missing pcmr request.
Engine and FileStore tests compare live/reopened authority at retry, grant, PIN/policy and reset publication checkpoints. Native transport fixtures exercise listing fallback, explicit protocol selection, restart and cursor controls. Stack probes retain the full 2 MiB scan and cover encrypted and memory stores; heap probes retain live controls and the requirement that freed bytes are zero. The scrub guard also runs on unwinding from an injected RNG panic. Token draws reject zero/repeated entropy before publication, preventing cross-permission secret collisions.
Fuzz sequence fixtures retain protocol-specific client secrets across restart;
the authorization oracle keeps transient and persistent grants separate. After
changing layouts or generators, regenerate all committed corpora twice using
the locked graph and compare every filename and byte. Keep exact commands,
source identities, statuses, compiler/architecture details and full logs outside
the repository. Use fresh git archives extracted with tar -xm, existing cache
volumes, separate compiler/architecture/sanitizer outputs and five explicit
30-second sanitizer runs. Report unavailable real HID, browser and x86_64
execution separately from passing portable/container checks.
Engine regressions cover all seven shared-parser consumers under both PIN protocols, with otherwise valid points, ciphertexts and MACs. Metadata rejection prevents later PIN decryption/comparison, retry writes and credential publication; FileStore checks compare reopened authority. HMAC extension processing follows existing presence collection and token consumption, and rejected commands retain stateful-command invalidation. The native recording link mutates only public getKeyAgreement metadata and records all requests/reports to prove that malformed responses cause no authenticated follow-up, including partial reports.
The stateful fuzz target uses controlled exchanges on a separate engine so its
metadata oracle does not invent a global error ordering for arbitrary requests.
Its cases include the engine-only integer -7 exception and unknown labels;
standard response parsing still requires -25. Every explicit fixture round-trips
against locked arbitrary. Regenerate all corpora twice using the locked release
seed generator and compare filenames and bytes before committing them.
tests/e2e/test_key_agreement.py sends raw Python FIDO requests through the real
HID fixture. Syntax/collection, recording links, callback fixtures and published
python-fido2 bridge workflows do not establish HID execution. Retain external
source/command/status evidence and report unavailable HID or architecture runs
separately; keep the existing libfido2 1.14.0 compatibility gates intact.