
⚠️ Early proof of concept. This project has not been thoroughly tested or audited. Do not use it for anything beyond experimentation.
Portable, hardware-backed WebAuthn/passkey credentials that work across multiple devices using TPM 2.0.
Traditional TPM credentials are locked to the device that created them. This project makes them portable by importing a deterministic parent key (derived from a master seed) into each device's TPM. Credential blobs encrypted by one TPM can then be loaded and used by any other TPM provisioned with the same seed. Private signing keys are generated randomly by the TPM for each credential and never exist in plaintext outside hardware.
The master seed is only needed once per device during provisioning. After that, all cryptographic operations happen entirely inside the TPM.
A browser extension overrides navigator.credentials and routes WebAuthn calls through native messaging to a Python backend that talks directly to the TPM. On registration, the TPM creates a fresh signing key under the portable parent and returns an encrypted blob. On authentication, it loads the blob back, decrypts it internally, and signs the challenge.
Linux (Debian/Ubuntu):
sudo apt install tpm2-tools python3-tpm2-pytss python3-cryptography
Windows:
pip install cryptography
Windows talks to the TPM via TBS (TPM Base Services) through ctypes, so no extra native packages are needed.
The browser extension communicates with native_host.py through native messaging. You need to register a manifest that tells Firefox where to find the host.
Create the file ~/.mozilla/native-messaging-hosts/webauthn_tpm_portable.json:
{
"name": "webauthn_tpm_portable",
"description": "WebAuthn TPM Portable Credentials Backend",
"path": "/absolute/path/to/native_host.py",
"type": "stdio",
"allowed_extensions": [
"[email protected]"
]
}
Replace path with the absolute path to native_host.py. Make sure it's executable (chmod +x native_host.py).
On Windows, the native messaging manifest is registered via the Windows Registry. See the Firefox native messaging docs for details.
cd extension
npm install
npm run build
Then in Firefox:
about:debugging#/runtime/this-firefoxextension/dist folder (e.g. manifest.json)Open the extension popup and either paste an existing seed or click "Generate" to create a new one, then click "Provision". If you generate a new seed, save it somewhere safe. You will need it to provision additional devices, and losing it means losing the ability to set up new devices (existing credentials on already-provisioned devices will keep working).
Provisioning can also be done from the CLI:
./webauthn_cli.py provision --generate
# or with an existing seed:
./webauthn_cli.py provision <seed_hex>
./webauthn_cli.py test
This creates a credential, signs a challenge, and verifies the signature.
webauthn_cli.py [--backend=BACKEND] <command> [args]
Commands:
Backends (selected with --backend or WEBAUTHN_BACKEND env var):
| Backend | Description |
|---|---|
tpm | Cross-platform TPM via raw commands (default) |
linux | Linux-only, uses tpm2-pytss library |
soft | Pure software, no TPM required (for testing) |
Credential signing keys are generated inside the TPM and never leave it in plaintext. The encrypted blob in the credential ID is useless without access to a provisioned TPM. Unlike software-based credential stores, private keys are never present in host memory during authentication, which protects against cold boot attacks and malware-based key extraction.
TPM2_Duplicate (which could theoretically export keys) is blocked because credentials are created with an empty authPolicy, making duplication impossible even with full system access.
Malware running on a provisioned system can ask the TPM to sign challenges, since there is no user verification (no button press or biometric). It cannot extract the keys, but it can use them while active. This is a narrower attack surface than software-based credential stores where malware can steal keys outright.
The master seed is the root of trust. If it's compromised, an attacker can provision their own TPM and use any credential blobs they obtain. If it's lost and all provisioned devices become unavailable, credentials are unrecoverable. Store it like you would a hardware wallet recovery phrase: offline, in a secure location, ideally with redundancy (e.g. split across multiple locations).
The project includes three interchangeable backends:
webauthn_tpm_portable.py is the primary backend. It constructs TPM commands at the byte level and works on both Linux (/dev/tpmrm0) and Windows (TBS API via ctypes).
webauthn_tpm_linux.py uses the tpm2-pytss Python library and only works on Linux.
webauthn_soft.py is a pure-software implementation that emulates the same credential format without any TPM. Useful for testing and development, but provides no hardware protection.
"Permission denied" on /dev/tpmrm0:
Add your user to the tss group and re-login: sudo usermod -aG tss $USER
tpm2-pytss import errors:
Try sudo apt install python3-tpm2-pytss or pip install --upgrade tpm2-pytss --break-system-packages.
Extension not connecting to native host:
Check that the path in the native messaging manifest is an absolute path pointing to native_host.py. Open the Browser Console in Firefox (Ctrl+Shift+J) to check for errors from the extension.
MIT
| Command | Description |
|---|
status | Check if the TPM is provisioned |
provision --generate | Generate a random seed and provision |
provision <seed_hex> | Provision with an existing seed |
create <rp_id> | Create a credential for a relying party |
sign <cred_id> <rp_id> <challenge> | Sign a challenge |
verify <cred_id> <rp_id> <challenge> <sig> | Verify a signature |
clear | Remove the portable parent key from the TPM |
test | Run a full create/sign/verify cycle |