Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

canokey-virtual

License: Apache-2.0 Platform: Linux

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.

Features

  • 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.

Status

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 *-sk key 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 keytocard is 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-core state are process globals.

Architecture

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.c implements the device/platform hooks expected by canokey-core.
  • canokey-core provides the actual FIDO2, U2F, OpenPGP, PIV, OATH, WebUSB, and applet state-machine logic.
  • A LittleFS image selected by --data stores persistent applet state.
  • A Unix control socket selected by --socket provides 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.

Security Model

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.

Requirements

Use the Nix flake instead of installing project-specific toolchains globally:

nix develop

The 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-core and vhci-hcd.
  • Permission to run usbip attach and usbip detach with sudo.
  • pcscd for CCID/OpenPGP/PIV/OATH tests.

Quick Start

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 --release

Start the virtual device:

mkdir -p state
chmod 700 state
./target/release/canokey-virtual run \
  --data state/canokey-virtual.lfs \
  --socket state/canokey-virtual.sock

Attach it from another terminal:

sudo modprobe usbip-core
sudo modprobe vhci-hcd
sudo usbip attach -r 127.0.0.1 -b 1-1

Confirm that Linux sees the device:

lsusb -d 20a0:42d4
nix develop --command fido2-token -L
nix develop --command pcsc_scan

Detach when finished:

sudo usbip detach -p 0

When 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.

Touch Simulation

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.sock

To 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.sh

The 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.

FIDO2, WebAuthn, and U2F

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/hidrawX

For 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/hidrawX

Legacy 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 \
  es256

For 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.

OpenSSH Security Keys

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.

OpenPGP

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-status

Recommended 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-edit

Inside gpg --card-edit, use admin, keytocard, and then test signing or decryption with the imported key material.

PIV

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 status

PKCS#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-objects

WebUSB

The 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 serial

Development

Run 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 --release

Do 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.

License

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.

About

Unofficial Linux userspace CanoKey-compatible virtual device backed by canokey-core

Topics

Resources

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages