A FIDO2 security key in software for Linux, with post-quantum ML-DSA passkeys.
pqkey shows up in your browser as an ordinary USB security key. A user daemon
creates a virtual HID device through the kernel's uhid driver, so any FIDO2
client can use it, and every registration, reset and sign-in that asks for
your presence waits for you to approve it in a desktop notification.
- Post-quantum: ML-DSA-44, ML-DSA-65 and ML-DSA-87 credentials (FIPS 204), alongside ECDSA (ES256, ESP256, ES384, ESP384, ES512, ESP512 and ES256K), EdDSA (EdDSA, Ed25519 and Ed448), and RSA-2048 (RS256, RS384, RS512, PS256, PS384 and PS512).
- CTAP 2.3: PIN (protocols 1 and 2), passkeys with credential management,
always-UV, minimum PIN length and PIN complexity configuration,
minPinLength,pinComplexityPolicy,credBlob,hmac-secret,hmac-secret-mc,credProtect,largeBlobKey,thirdPartyPayment, fragmented large-blob storage, and packed self-attestation. - U2F: CTAP1 registration and authentication over USB HID, using ES256 credentials that also work over CTAP2. U2F uses approval rather than a PIN, and its prompt cannot identify the site. Always-UV disables it.
- Device identification: CTAPHID_WINK briefly names the software key in a desktop notification with no approval buttons. It expires after two seconds and does not authorize an operation. CTAPHID_LOCK supports channel leases of up to ten seconds, renewed or released by their owner.
- Rust throughout: pure-Rust cryptography, and
unsafecode only where pqkey calls into the operating system.
Warning
pqkey is a research project, not for production use. A software key is not a hardware key: its private keys are files in your home directory, which anything running as you, or as root, can use. See SECURITY.md for the threat model.
FIDO clients can identify a connection with WINK even when a notification server cannot show action buttons. If delivery fails or exceeds 500 ms, WINK returns a transport error; the key remains usable. Identification also works with PIN complexity and always-UV enabled. LOCK is volatile: expiry or daemon shutdown releases it, while channel resynchronization and authenticator reset preserve an unexpired lease. Clients should release leases in cleanup handlers.
You need Linux with systemd and the uhid module (Ubuntu, Debian and Fedora
have it), and a desktop whose notifications show buttons (GNOME, KDE Plasma or
dunst). Copy this one command:
git clone https://github.com/rainzhang05/SoftwareAuthenticator.git && SoftwareAuthenticator/install.shThe installer says how much disk space it will use (about 1.2 GB with the build tools and Rust), asks once to continue and once for your sudo password, and shows its progress as it:
- installs a C linker, Rust and ACL tools, if they are missing;
- builds pqkey and installs it as
/usr/local/bin/pqkey; - gives the key access to
/dev/uhidwith udev rules, and loads theuhidmodule at every boot; - starts the key as a systemd user service and asks you to choose its PIN.
A successful installation means the installed service is running, its HID
client can exchange messages, the configured presence provider is usable,
and its PIN requirements are complete. pqkey works in the same terminal,
with no reboot or new login. Missing requirements produce a nonzero exit
status and instructions; the installer log is ~/.cache/pqkey/install.log
(or $XDG_CACHE_HOME/pqkey/install.log).
./install.sh --yes approves installation; you still choose and confirm the
PIN. Without terminal input, a fresh key remains installed but setup fails
until you run pqkey pin, then pqkey setup, in a terminal. A configured key
can be updated without PIN input unless its policy requires a PIN change.
If your terminal's PATH excludes /usr/local/bin, run
export PATH="/usr/local/bin:$PATH" in that terminal and rerun the installer.
Before upgrading, stop the key and back up its complete state directory,
including the keys. Then run git pull and ./install.sh again. Reruns repair
partial setup and preserve credentials, valid PINs and authenticator policy.
Updates replace the executable atomically. If setup fails after replacement,
the previous executable remains at /usr/local/bin/pqkey.previous; the
installer prints retry and restore commands. The recovery copy is removed
after successful setup.
Registering or replacing a discoverable credential upgrades older stores to the authenticated membership format. Restore only the executable when the previous version understands that format. A version predating this format requires its matching complete state backup as well; credentials created after that backup will be absent.
Note
Anyone who can open /dev/uhid can create any HID device, keyboards
included. The rules give it to the plugdev group, and the installer adds
you to that group if you are not in it yet (on Ubuntu you already are).
Register pqkey on any site that supports passkeys or security keys, and approve the notification that appears.
| Command | What it does |
|---|---|
pqkey |
Shows the key's status, its PIN, its free passkey slots and anything to fix |
pqkey pin |
Sets or changes the PIN |
pqkey config |
Shows always-UV and PIN policy settings |
pqkey config always-uv on|off |
Requires or stops requiring the PIN |
pqkey config min-pin-length N [--rp RP_ID]... |
Raises the PIN minimum |
pqkey config pin-complexity [--yes] |
Enables the ASCII letter and digit rule |
pqkey config force-pin-change |
Requires a different PIN |
pqkey passkeys |
Lists the passkeys stored on the key |
pqkey passkeys delete QUERY |
Deletes the passkey whose site, user or ID contains QUERY |
pqkey reset |
Erases credentials, large blobs and PIN; resets settings |
pqkey stop, pqkey start |
Unplugs and plugs in the key |
Configuration changes ask for the current PIN when one is set. Always-UV
requires the PIN for registrations and sign-ins that ask for user presence.
Without a PIN, turn always-UV off or set one with pqkey pin before using
the key in a browser.
The minimum PIN length counts Unicode code points, from 4 to 63, with a
separate limit of 63 UTF-8 bytes. Raising it asks for confirmation, unless
--yes is given: only pqkey reset, which erases every passkey and large
blob, can lower it again. Repeated --rp options replace the RP IDs allowed
to receive the minPinLength extension; omitting them keeps the list. A key
can remember 8 RP IDs, each up to 253 bytes. The complete configuration request
must also fit the key's advertised 1,768-byte message limit, including CBOR
and PIN authentication. If it is too large, pqkey reports its encoded size
and limit before sending the command; use fewer or shorter RP IDs. The list
is applied in one operation. If a PIN change becomes required,
run pqkey pin and choose a different PIN meeting the current minimum.
Enable PIN complexity with
pqkey config pin-complexity. New PINs must then contain an ASCII letter and
digit in addition to meeting the length requirements. Only reset disables it;
enabling with an existing PIN requires one replacement.
pqkey passkeys lists only passkeys, which are discoverable credentials. A
site that registers the key as a second factor keeps the credential's ID and
sends it back to sign in, so the key does not list that credential. The
prompt makes the difference clear, saying "Create a passkey" or "Register a
security key".
Like a hardware key, pqkey accepts a reset only within 10 seconds of being
plugged in, so pqkey reset restarts it first. The key's state lives in
~/.local/share/pqkey.
FIDO clients can store a serialized large-blob array of up to 16,384 bytes,
transferred in fragments of at most 1,704 bytes. Writes require the PIN's
large-blob write permission when a PIN is set or always-UV is on. Reset
restores the empty array; deleting a credential leaves blob collection to
the client. Discoverable credentials registered with largeBlobKey keep a
secret 32-byte key for platforms to encrypt and decrypt their blobs.
Credentials explicitly registered with thirdPartyPayment: true retain that
metadata for both discoverable and non-discoverable credentials. Assertions
requesting it return the stored boolean inside signed authenticator data;
creation returns no payment extension output. pqkey passkeys marks enabled
passkeys in its PAYMENT column. Enabled non-discoverable credentials consume
storage but still require their RP's allow list and remain outside passkey
management. Ordinary eligible credentials continue to use sealed IDs.
The flag does not grant cross-RP access or implement a payment prompt. The client platform determines payment eligibility and UI, including Secure Payment Confirmation (SPC). Authenticator regressions and libfido2 signed-byte checks are separate from browser/SPC verification; see the verification notes.
| Client | ES256 | ML-DSA | Notes |
|---|---|---|---|
| Chromium 153 | ✓ | ✓ | Needs a PIN on the key for passkeys |
| Firefox 154 | ✓ | Ignores algorithms it does not know | |
| python-fido2 2.2.1 | ✓ | ✓ | Used by the end-to-end tests |
| libfido2 1.16 | ✓ | PIN, credential management and hmac-secret work |
These were tested on Ubuntu 26.04. Each client's limits are listed under Clients in the development notes.
pqkey status names anything that is missing and how to fix it, and
scripts/diagnose.sh shows the whole state of the key on your computer. For
browser behaviour, see
Troubleshooting.
pqkey setup --uninstallThis stops the key and removes its service. It then prints one sudo command
that removes pqkey, its udev rules and the uhid setting. Your passkeys stay
in ~/.local/share/pqkey until you delete that directory.
- SECURITY.md: the threat model, and how to report a vulnerability
- Architecture: how pqkey works, and what it supports of CTAP 2.3
- Development notes: the scripts that run every
check and test (
scripts/check.sh,scripts/e2e.shand more), and troubleshooting - Browser test kit: a local relying party that checks everything the key returns
pqkey is free software under the MIT License: you may use, change and share it, as long as the copyright notice comes with it. It comes with no warranty.