canokey-virtual is an unofficial virtual test device that runs the upstream
canokey-core firmware logic as a
Linux userspace USB/IP device. After the local host imports it through
vhci_hcd, regular Linux clients see a CanoKey-compatible USB security key.
This project is not affiliated with or endorsed by the CanoKeys project. It is a development, integration-test, and debugging tool, not a production authenticator, and should not be used with real accounts, production CA keys, or long-lived credentials.
- FIDO2/WebAuthn over HID, including PIN, makeCredential, getAssertion, reset, and browser WebAuthn flows.
- Legacy U2F over HID, including libfido2 registration and authentication.
- OpenSSH security-key operations through libfido2.
- OpenPGP, PIV, and OATH over CCID through PC/SC.
- WebUSB admin APDUs for CanoKey Console.
- Persistent LittleFS-backed CanoKey state in a local file.
- Local control socket for status queries and touch simulation.
- Optional touch-request hook for desktop notifications or custom approval UI.
The current implementation is tested on Linux with a clean local clone of
canokey-core.
Known working paths:
- FIDO2 PIN setup/change, credential creation, assertions, and reset.
- Legacy U2F registration and authentication through libfido2.
- Browser WebAuthn registration and authentication.
- OpenSSH
*-skkey generation and signing. - PC/SC discovery, OpenPGP card status, PIV management, and PIV signing smoke tests.
- CanoKey Console WebUSB admin APDUs.
Known limitations:
- OpenPGP full on-card key generation may fail in the upstream core path used
here. Importing generated keys with
keytocardis the recommended test path. - WebUSB access from browsers requires the USB/IP device to be attached in the same desktop session where the browser runs.
- The USB/IP server intentionally allows only one active imported client,
because the virtual USB device and the linked
canokey-corestate are process globals.
canokey-virtual is a small host-side port of canokey-core:
- Rust implements the USB/IP server and command-line interface.
csrc/canokey_virtual_shim.cimplements the device/platform hooks expected bycanokey-core.canokey-coreprovides the actual FIDO2, U2F, OpenPGP, PIV, OATH, WebUSB, and applet state-machine logic.- A LittleFS image selected by
--datastores persistent applet state. - A Unix control socket selected by
--socketprovides local status and touch simulation.
The project links against a local canokey-core/build/libcanokey-core.a.
canokey-core is not committed to this repository; clone it as a sibling
directory named canokey-core.
The LittleFS backing file can contain private keys, PIN metadata, certificates,
resident credentials, and other sensitive authenticator state. The program
creates and opens this file with mode 0600, but you should still keep it in a
private directory and never commit or share it.
The control socket can simulate physical touch. It is created with mode 0600;
access to that socket is equivalent to user-presence access.
The optional --touch-request-script hook runs an executable whenever the
virtual device starts waiting for touch. Treat the hook as trusted local code.
It receives the control socket path and may choose to approve operations.
The USB/IP server is unauthenticated. The default --listen 127.0.0.1:3240
keeps it local. Do not bind it to a non-loopback address unless the whole test
network is trusted; doing so exposes the virtual USB device to that network.
Use the Nix flake instead of installing project-specific toolchains globally:
nix developThe dev shell includes Rust, CMake, GCC, libfido2 tools, GnuPG, PC/SC tools,
OpenSC, OpenSSH, PyUSB, and yubico-piv-tool.
Kernel/user-space requirements for running the virtual USB device:
- Linux with
usbip-coreandvhci-hcd. - Permission to run
usbip attachandusbip detachwithsudo. pcscdfor CCID/OpenPGP/PIV/OATH tests.
Clone and build a clean upstream canokey-core:
git clone --recursive https://github.com/canokeys/canokey-core.git canokey-core
nix develop --command cmake -S canokey-core -B canokey-core/build \
-DCMAKE_BUILD_TYPE=Release \
-DVIRTCARD=ON \
-DENABLE_DEBUG_OUTPUT=OFF \
-DENABLE_BYPASS_USER_PRESENCE=OFF \
-DCMAKE_C_FLAGS="-DLFS_NO_DEBUG -DLFS_NO_WARN -DLFS_NO_ERROR -DLFS_NO_ASSERT"
nix develop --command cmake --build canokey-core/build --target canokey-core -j"$(nproc)"Build this project:
nix develop --command cargo build --releaseStart the virtual device:
mkdir -p state
chmod 700 state
./target/release/canokey-virtual run \
--data state/canokey-virtual.lfs \
--socket state/canokey-virtual.sockAttach it from another terminal:
sudo modprobe usbip-core
sudo modprobe vhci-hcd
sudo usbip attach -r 127.0.0.1 -b 1-1Confirm that Linux sees the device:
lsusb -d 20a0:42d4
nix develop --command fido2-token -L
nix develop --command pcsc_scanDetach when finished:
sudo usbip detach -p 0When the backing LittleFS file does not exist, canokey-virtual creates an
erased 128 KiB image and initializes CanoKey state. First initialization stores
a random 4-byte serial number in the core platform config page; later starts
with the same backing file keep the same serial.
build.rs reads the firmware version from git describe --tags --always in
./canokey-core and exposes it through the CanoKey admin version command. It
also refuses to link a canokey-core build with debug output or user-presence
bypass enabled.
Use the control socket to inspect status and simulate touch:
./target/release/canokey-virtual status --socket state/canokey-virtual.sock
./target/release/canokey-virtual touch --socket state/canokey-virtual.sockTo run a notification script whenever CanoKey requests touch:
./target/release/canokey-virtual run \
--data state/canokey-virtual.lfs \
--socket state/canokey-virtual.sock \
--touch-request-script ./notify-touch.shThe script path is resolved to an absolute path at startup and executed
directly, not through a shell. It receives the socket path as argv[1]; the
same path is also available in CANOKEY_VIRTUAL_SOCKET.
CANOKEY_VIRTUAL_EVENT is set to touch-request.
Replace /dev/hidrawX with the path reported by fido2-token -L.
nix develop --command fido2-token -I /dev/hidrawX
nix develop --command fido2-token -S /dev/hidrawXFor older libfido2 versions that do not interoperate with CTAP PIN protocol 2 on the current upstream core, set this compatibility flag only for the affected CLI test:
CV_FIDO_PIN_PROTOCOL1_ONLY=1 nix develop --command fido2-token -S /dev/hidrawXLegacy U2F smoke test:
{
head -c 32 /dev/urandom | base64
printf 'no.tld\n'
printf 'some user name\n'
head -c 32 /dev/urandom | base64
} > /tmp/cv-u2f-cred.in
nix develop --command fido2-cred -M -u \
-i /tmp/cv-u2f-cred.in \
-o /tmp/cv-u2f-cred.out \
/dev/hidrawX es256
nix develop --command fido2-cred -V \
-i /tmp/cv-u2f-cred.out \
-o /tmp/cv-u2f-cred-key.pem \
es256
head -1 /tmp/cv-u2f-cred-key.pem > /tmp/cv-u2f-cred-id.txt
tail -n +2 /tmp/cv-u2f-cred-key.pem > /tmp/cv-u2f-pubkey.pem
{
head -c 32 /dev/urandom | base64
printf 'no.tld\n'
cat /tmp/cv-u2f-cred-id.txt
} > /tmp/cv-u2f-assert.in
nix develop --command fido2-assert -G -u \
-i /tmp/cv-u2f-assert.in \
-o /tmp/cv-u2f-assert.out \
/dev/hidrawX
nix develop --command fido2-assert -V -p \
-i /tmp/cv-u2f-assert.out \
/tmp/cv-u2f-pubkey.pem \
es256For browser testing, open a WebAuthn test page such as
https://webauthn.io/ in Chrome or Chromium after the USB/IP device is
attached. Run canokey-virtual touch whenever registration or authentication
waits for user presence.
hid=/dev/hidrawX
nix develop --command ssh-keygen -t ecdsa-sk \
-O application=ssh:canokey-virtual \
-O device="$hid" \
-f /tmp/cv_ecdsa_sk \
-N ''
nix develop --command ssh-keygen -Y sign -f /tmp/cv_ecdsa_sk -n file /tmp/message-O device="$hid" keeps OpenSSH on the FIDO HID interface; otherwise libfido2
may probe the PC/SC FIDO applet first.
When pcscd is running, make GnuPG's scdaemon use PC/SC instead of claiming
the CCID interface directly:
cat > "$GNUPGHOME/scdaemon.conf" <<'EOF'
disable-ccid
pcsc-shared
EOF
gpgconf --kill scdaemon
nix develop --command gpg --card-statusRecommended smoke-test flow:
nix develop --command gpg --quick-generate-key \
"CanoKey Virtual Test <test@example.invalid>" ed25519 sign 1d
nix develop --command gpg --quick-add-key <fingerprint> cv25519 encr 1d
nix develop --command gpg --card-editInside gpg --card-edit, use admin, keytocard, and then test signing or
decryption with the imported key material.
Basic management and signing smoke tests:
nix develop --command yubico-piv-tool -a status
nix develop --command yubico-piv-tool -s 9a -A ECCP256 -a generate -o /tmp/cv-piv-pub.pem
nix develop --command yubico-piv-tool -a verify-pin -P 123456 \
-s 9a -a selfsign-certificate \
-S "/CN=CanoKey Virtual Test/" \
-i /tmp/cv-piv-pub.pem \
-o /tmp/cv-piv-cert.pem
nix develop --command yubico-piv-tool -a import-certificate -s 9a -i /tmp/cv-piv-cert.pem
nix develop --command yubico-piv-tool -a statusPKCS#11 tests can be run with OpenSC after the certificate is imported:
nix develop --command pkcs11-tool --module opensc-pkcs11.so -L
nix develop --command pkcs11-tool --module opensc-pkcs11.so --login --pin 123456 --list-objectsThe BOS descriptor advertises https://console.canokeys.org as the landing
page. After attaching the USB/IP device, CanoKey Console should be able to read
version, model, serial, and chip information through WebUSB.
For a CLI smoke test matching CanoKey Console's WebUSB path, send an APDU and immediately read the response with PyUSB:
import usb.core
dev = usb.core.find(idVendor=0x20A0, idProduct=0x42D4)
def apdu(hex_cmd):
dev.ctrl_transfer(0x41, 0x00, 0, 1, bytes.fromhex(hex_cmd))
return bytes(dev.ctrl_transfer(0xC1, 0x01, 0, 1, 1500)).hex()
print(apdu("00 A4 04 00 05 F0 00 00 00 00"))
print(apdu("00 31 00 00 00")) # firmware version
print(apdu("00 31 01 00 00")) # model
print(apdu("00 32 00 00 00")) # 4-byte serialRun checks in the Nix shell:
nix develop --command cargo fmt -- --check
nix develop --command cargo test
nix develop --command cargo clippy -- -D warnings
nix develop --command cargo build --releaseDo not commit generated state or secrets, including *.lfs, private keys,
certificates, signatures, or files under state/.
See CONTRIBUTING.md for the smoke-test checklist and SECURITY.md for reporting/security notes.
This repository is licensed under the Apache License, Version 2.0.
canokey-core is also licensed under Apache-2.0. This project links against a
local canokey-core build and preserves the upstream license/notice
requirements in NOTICE.
The original code in this repository could be relicensed by its copyright
holders under a different permissive license such as MIT, because Apache-2.0
does not require downstream projects to use the same license. However,
distributions that include or link canokey-core must still comply with
canokey-core's Apache-2.0 terms, including retaining license and notice
information. Keeping this repository Apache-2.0 is the clearest option.