Open-source security key/ FIDO2 · CTAP2 · Linux

YOUR
PHONE.
THE KEY.

uKey makes Linux see your Android phone as a virtual FIDO2 security key. No dongle. No cloud relay. Hardware-backed credentials over local loopback.

working: UHID · CTAPHID · CTAP2 in progress: PAM · WebAuthn · SSH

01 / the idea/ one less thing

A SECURITY KEY
SHOULDN'T BE
ANOTHER THING
TO LOSE.

Your phone already has a secure element, a fingerprint sensor, and a battery. uKey asks a simple question: why buy, carry, and lose a separate USB key just to prove who you are on Linux?

The answer is not “trust us.” It is a virtual HID device, the same CTAP2 protocol a hardware key speaks, and a key that stays inside Android's secure hardware. What works today sits right next to what is in active progress.

Read the public status ledger

The trade-off, in plain language

+

No extra hardware

Your phone is the token. The host sees a normal hidraw device.

+

The key stays put

Android Keystore generates the P-256 key. The daemon only sees public keys and signatures.

~

Active development

Browser, SSH, and PAM are in active development. The current status is documented below.

02 / the handoff/ laptop ↔ phone

ONE CABLE.
TWO DEVICES.
NO DONGLE.

A Python daemon creates a virtual HID device. Standard FIDO2 tools do the rest. The phone holds the key and asks for a fingerprint. Switch the scene to see the three CTAP2 commands.

Protocol walkthrough illustrative UI · not a live device
ukey@fedora: ~/ukey
LINUX HOST · /dev/hidraw6
9:41
uKey FIDO2

Discover

Token present

The host sees a FIDO_2_0 authenticator. No approval is needed for getInfo.

passive query

Illustrative UI · no live hardware attached

ANDROID PHONE · Keystore
commands shown are real libfido2 tools Illustration only. The physical phone → libfido2 path is hardware-dependent and not part of CI.
01 / CREATE

A virtual USB token

The daemon opens /dev/uhid and asks the kernel for a HID device. Linux creates a hidraw node. No custom kernel module.

02 / SPEAK

CTAPHID + CTAP2

64-byte HID reports carry CBOR requests and signatures. The daemon handles channel IDs, fragmentation, keepalive, and cancellation.

03 / APPROVE

The phone signs

The Android app prompts for a strong biometric, then asks Android Keystore for an ES256 signature. The private key never crosses the bridge.

03 / what works/ STATUS.md, not marketing

NO VAPORWARE.
JUST A SHORT,
HONEST LIST.

Every card that says “working” maps to a command, test, or file in the repository. All in-progress items live in the status ledger.

working

A real hidraw node, invented in software.

The daemon asks /dev/uhid for a USB HID device. Linux creates /dev/hidraw6; fido2-token -L lists it like a physical security key.

/dev/hidraw6
working

The framing layer, not a shortcut.

INIT, PING, CBOR, CANCEL, and KEEPALIVE across 64-byte reports, with channel IDs and fragmentation. The kernel-facing edge is real CTAPHID.

INITPINGCBORKEEPALIVECANCEL
partial

The key never leaves the phone.

Android Keystore generates a P-256 key. StrongBox is preferred, TEE is the fallback, and software-backed keys are rejected. Every assertion asks for a strong biometric prompt.

Hardware Enclave · StrongBox/TEE hardware-backed key protection.

working

Three commands. One small surface.

getInfo, makeCredential, getAssertion. FIDO_2_0, ES256, packed self-attestation, and non-resident credentials only.

→ getInfo FIDO_2_0 · ES256
→ makeCredential packed self-attestation
→ getAssertion raw r‖s → DER
working

Pair once. Then just approve.

A single-use six-digit PIN issues a 256-bit device token. Lockout, persistence, and revocation are covered by tests.

Zero-PIN auto-connect active after first pairing · revoke anytime.

working

Metadata, not private keys.

Credential ID, RP ID, and public key persist in ~/.config/ukey/credentials.json. The daemon never sees private key material.

working

Boring defaults, on purpose.

Loopback-only bridge. Auto-lock off. Config mode 0600. Direct loopback via adb reverse is the default.

wireless: false
auto_lock: false
listen_host: 127.0.0.1
working

The unglamorous stuff is included.

The local console shows a pairing PIN, lists paired phones, revokes tokens, and pauses assertion approval. It binds to loopback only. No hidden relay.

ukeyctlstatus · config · doctor · logs
127.0.0.1:8444pair · devices · revoke
./install.shFedora-tested · safe defaults
CIPython 3.10–3.12 · shell · systemd
See the local console

04 / the control room/ laptop-side console

PAIR ONCE.
REVOKE ANYTIME.
LOCAL ONLY.

The daemon serves a small console on 127.0.0.1:8444. It shows a single-use pairing PIN, lists paired phones, revokes a token, and pauses assertion approval. It binds to loopback, not the network.

Local console illustrative UI · not a live device
127.0.0.1:8444
http://127.0.0.1:8444
Device access

Pairing & devices

waiting for phone
One-time pairing PINsingle use
482913

Enter it in the Android app once. The phone stores a device token; no PIN after that.

Generate new PINrotates on request
Paired devices1 paired
Pixel 8last seen 2m ago · token
Revoke
Add another phoneneeds the PIN above

Revoking drops that phone on its next connection.

Assertion approvalAsk the phone
Loopback onlynot exposed to the LAN
LOCAL CONSOLE · 127.0.0.1:8444
the real console binds to loopback only Illustration only. Settings, pairing, and revocation are local; no cloud account.

05 / under the hood/ five layers

FIVE LAYERS.
END-TO-END LOCAL.

Follow a cryptographic challenge from standard Linux FIDO2 tools down into the phone's hardware secure element. 100% local loopback ensures private keys never leave physical silicon.

01 / client

Standard FIDO2 tools

fido2-token, fido2-cred, fido2-assert on the Linux host.

libfido2hidrawno browser extension
02 / kernel

Linux sees a USB HID token

The daemon creates a UHID device; the kernel exposes a /dev/hidraw node.

/dev/uhid/dev/hidraw6vendor 0x1209
03 / daemon

CTAPHID + CTAP2 engine

Python daemon for framing, CBOR, credential metadata, pairing, and the local settings console.

getInfomakeCredentialgetAssertioncredentials.json
trust boundarydirect adb · loopback socket
04 / bridge

Signer bridge on :8443

Direct localhost loopback via adb reverse. Fast, low-latency inter-process communication with zero cloud relays.

adb reverse127.0.0.1:8443local IPC
05 / phone

Android app + Keystore

Kotlin client, per-use BiometricPrompt, then a P-256 signature from AndroidKeyStore.

StrongBox preferredTEE fallbackhardware keys only

crosses the bridge

Requests and signatures

CBOR command payloads, public keys, and raw 64-byte ECDSA signatures converted to DER at the CTAP2 edge.

never crosses it

Private key material

The FIDO2 private key is generated in AndroidKeyStore and is not exported. Biometric data stays on the phone.

stored on disk

Credential metadata

Credential ID, RP ID, and public key. Unencrypted; readable by the local user and root. Not a secret, but integrity-sensitive.

06 / the public ledger/ last verified 2026-11-04

WHERE IT
ACTUALLY
STANDS.

This is a summary of docs/STATUS.md. The first column is what works today; all other items are in active development. Verified on Fedora 43 with libfido2 1.15.0.

Works today

  • UHID virtual device/dev/hidraw node appears
  • CTAPHID framingINIT · PING · CBOR · CANCEL
  • CTAP2 getInfoFIDO_2_0 · ES256
  • CTAP2 makeCredentialpacked self-attestation
  • CTAP2 getAssertionraw r‖s → DER
  • Mock signerdeterministic P-256 test double
  • Credential metadata~/.config/ukey/credentials.json
  • Single-use PIN + tokentests: lockout, token auth, revoke
  • Config + ukeyctlsafe defaults · 0600
  • Local dashboardpairing PIN · devices · revoke
  • CIPython 3.10–3.12 · shell · systemd

Evidence lives in tests/, daemon/, and the commands in STATUS.md.

In progress · Host & Workflows

  • Android signerStrongBox/TEE · hardware integration
  • Fedora installerFedora tested · multi-distro packaging
  • Wireless transportdirect local network bridge
  • Auto-lockproximity timeout & session control
  • Browser WebAuthnChrome & Firefox end-to-end flow
  • SSH ecdsa-skOpenSSH hardware key signing
  • Host fingerprint detectionworkstation identification

In progress means actively implemented with test coverage underway.

In progress · Security & Protocols

  • PAM / U2F / CTAP1CTAP1 backward compatibility & PAM
  • Resident credentialsrk=true discoverable credentials
  • Transport authenticationmutual auth bridge handshake
  • Replay protectionnonce-based anti-replay verification
  • Attestation chainauthenticator attestation chain
  • Context approval / sudo displayinteractive terminal & sudo prompts

Active engineering across protocol depth, hardware isolation, and host workflows.

07 / run it/ fedora 43 · libfido2 1.15.0

BUILD AND
RUN UKEY.

The verified path is a Fedora machine with /dev/uhid, Python 3.10–3.12, and libfido2 installed. The installer sets up udev permissions, a Python virtual environment, and a systemd user service with safe defaults.

  • Fedora 43 is the tested environment.
  • Other distributions use the manual setup path.
  • Android app is built from source in android/.
  • Wireless is opt-in and adds no transport security. Keep the bridge on loopback.
install · Fedora-tested path
# 1. clone
$ git clone https://github.com/siddharthsai218/ukey.git
$ cd ukey

# 2. install the user service + udev rule
$ ./install.sh

# 3. verify the kernel sees a token
$ fido2-token -L
$ fido2-token -I /dev/hidraw6

Expected from step 3: a hidraw node, then FIDO_2_0 / ES256 details. The node name is assigned by the kernel and may differ. No success output is fabricated here.

08 / the invitation/ bring a phone

YOUR PHONE IS
ALREADY A
SECURITY TOKEN.

Explore the project. Read the ledger, test the working CTAP2 flow, or follow development on GitHub.

Open Source · GPL-3.0