Simulate a FIDO2 token

This guide will show how to simulate a FIDO2 token for development purposes.

We are going to simulate a complete FIDO2 compliant token, both through the PC/SC interface and through the USBHID interface just like a Yubikey.

We are going to use the opensource FIDO2Applet from Bryan Jacobs, as well as the smartcard simulator JCardEngine from Martin Paljak.

Note: the PGP public key used for signing files linked here is 0x9CB4AFC34639169D.

Obtain JDK 17

Note: if you are not going to build FIDO2Applet and JCardEngine but instead download my pre-built versions, you can use a more modern JDK.

Obtain JDK 17.0.2 from the OpenJDK archives.

Extract it to /usr/lib/jvm then define JAVA_HOME and modify PATH:

export JAVA_HOME=/usr/lib/jvm/jdk-17.0.2
export PATH="$JAVA_HOME/bin:$PATH"

Build JCardEngine

Note: if you want to skip this step, just download the pre-built jcard.jar (sig) and skip to Build FIDO2Applet

Get the repository:

git clone https://github.com/martinpaljak/JCardEngine.git
pushd JCardEngine
git switch --detach v26.08.23
git submodule update --init --checkout --recursive

You can stay on the next branch instead of checking out the tag v26.08.23 but results are not guaranteed.

Apply this patch to resolve a dependency issue:

git apply <<'EOF'
diff --git a/simulator/pom.xml b/simulator/pom.xml
index 6b79565..3f50d16 100644
--- a/simulator/pom.xml
+++ b/simulator/pom.xml
@@ -23,21 +23,24 @@
             <groupId>oracle.javacard</groupId>
             <artifactId>api_classic</artifactId>
             <version>${javacard.version}</version>
-            <scope>provided</scope>
+            <scope>system</scope>
+            <systemPath>${project.basedir}/sdks/jc320v26.0_kit/lib/api_classic-${javacard.version}.jar</systemPath>
             <optional>true</optional>
         </dependency>
         <dependency>
             <groupId>org.globalplatform</groupId>
             <artifactId>api</artifactId>
             <version>${globalplatform.version}</version>
-            <scope>provided</scope>
+            <scope>system</scope>
+            <systemPath>${project.basedir}/gpapi-globalplatform_${globalplatform.version}.jar</systemPath>
             <optional>true</optional>
         </dependency>
         <dependency>
             <groupId>org.globalplatform</groupId>
             <artifactId>contactless</artifactId>
             <version>${globalplatform.contactless.version}</version>
-            <scope>provided</scope>
+            <scope>system</scope>
+            <systemPath>${project.basedir}/gpapi-contactless_${globalplatform.contactless.version}.jar</systemPath>
             <optional>true</optional>
         </dependency>
         <!-- Mandatory deps -->
EOF

Apply the following patch to resolve a timeout issue:

git apply <<'EOF'
diff --git a/tool/src/main/java/pro/javacard/engine/tool/JCardTool.java b/tool/src/main/java/pro/javacard/engine/tool/JCardTool.java
index 057317a..2d977fd 100644
--- a/tool/src/main/java/pro/javacard/engine/tool/JCardTool.java
+++ b/tool/src/main/java/pro/javacard/engine/tool/JCardTool.java
@@ -182,7 +182,7 @@ public class JCardTool {

                 if (options.has(OPT_VSMARTCARD) || options.has(OPT_VSMARTCARD_PORT) || options.has(OPT_VSMARTCARD_HOST) || options.has(OPT_VSMARTCARD_PROTOCOL) || options.has(OPT_VSMARTCARD_ATR)) {
                     var protocol = options.has(OPT_VSMARTCARD_PROTOCOL) ? options.valueOf(OPT_VSMARTCARD_PROTOCOL) : options.valueOf(OPT_PROTOCOL);
-                    AbstractTCPAdapter adapter = new VSmartCardClient(p -> sim.connectFor(Duration.ofSeconds(1), p, true)); // TODO: parameter for timeout
+                    AbstractTCPAdapter adapter = new VSmartCardClient(p -> sim.connectFor(Duration.ZERO, p, true)); // TODO: parameter for timeout
                     adapter = adapter.withProtocol(protocol);
                     adapter = configureVSmartCard(adapter, options);
                     adapters.add(adapter);
EOF

Then build it:

./mvnw verify

You should end up with the file tool/target/jcard.jar.

Now go back to the parent directory:

popd

Build FIDO2Applet

Note: if you want to skip this step, just download the pre-built FIDO2.cap (sig) and skip to Install vsmartcard.

Still with the JAVA_HOME and PATH set as in the Obtain JDK 17 section, get the code of FIDO2Applet:

git clone https://github.com/BryanJacobs/FIDO2Applet.git
cd FIDO2Applet
git submodule update --init --checkout --recursive

Note: this guide was written for revision 3bddc3d4ce05c2668d79ff4542faa2c477adc908. If you run into problems then run git reset --hard HEAD && git switch --detach 3bddc3d4ce05c2668d79ff4542faa2c477adc908 && git clean -fxd to use the same revision and try again.

Apply this patch to select the right SDK:

git apply <<'EOF'
diff --git a/build.gradle b/build.gradle
index 5e62652..604dd32 100644
--- a/build.gradle
+++ b/build.gradle
@@ -62,6 +62,7 @@ if (jcHomeSet) {
     javacard {
         config {
             cap {
+                targetsdk 'sdks/jc304_kit'
                 packageName 'us.q3q.fido2'
                 version '0.4'
                 aid PackageID
EOF

Then define the JC_HOME environment variable to point to the JavaCard 3.1.0 converter:

export JC_HOME="$PWD/sdks/jc310r20210706_kit"

Build the applet:

./gradlew buildJavaCard

You should now have the file build/javacard/FIDO2.cap.

Install vsmartcard

Note: if you are currently using PKCS#11 or FIDO2 unlock for LUKS at boot, installing vsmartcard will break it as the PC/SC daemon of the initramfs won't find the right libraries. You can prevent this by creating the file /etc/dracut.conf.d/vpcd.conf with the following contents:

install_items+=" /usr/lib/pcsc/drivers/serial/libifdvpcd.so "

Run the following (assuming Debian/Ubuntu or similar):

sudo apt install vsmartcard-vpcd

Create the attestation root CA and get the installation parameters

This step is necessary in order to use the full FIDO2 feature set instead of U2F.

Note: on Debian/Ubuntu you need to install the package python3-venv to run the following commands.

Make a virtual environment and install the required packages:

python3 -m venv .venv
source .venv/bin/activate  # run the command deactivate to exit the venv
pip install -r requirements.txt

Then run:

./attestation_key_pair.py

You will get an output similar to the following:

PRIVATE key: b'ZkU9W6d2Al2Aj6bEZc3aim1ZPWyflj3FMvU/QOxFnMI='
PUBLIC key: b'BIqSX2hY8D+jmFDY3oMH1MwVFC6/wRjn4cdccEgnB0mINNRec2ZFPN/9rKKAHvIiuZ3PQIl8fRb4Ixur69UVGiM='

Then define variables with the values for later use:

PRIVKEY='ZkU9W6d2Al2Aj6bEZc3aim1ZPWyflj3FMvU/QOxFnMI='
PUBKEY='BIqSX2hY8D+jmFDY3oMH1MwVFC6/wRjn4cdccEgnB0mINNRec2ZFPN/9rKKAHvIiuZ3PQIl8fRb4Ixur69UVGiM='

Then get the installation parameters we will need to give when starting the simulator:

./get_install_parameters.py --enable-attestation --attestation-private-key "$PRIVKEY"

You can run ./get_install_parameters.py --help to get more options, for instance the --certification-level option if you want to experiment with that.

You will get an output similar to the following:

a900f50505061820071904000818200918fe0a1904000b1904000f582066453d5ba776025d808fa6c465cdda8a6d593d6c9f963dc532f53f40ec459cc2

Which you can define as a variable for later use:

PARAMS=a900f50505061820071904000818200918fe0a1904000b1904000f582066453d5ba776025d808fa6c465cdda8a6d593d6c9f963dc532f53f40ec459cc2

Start the simulator

Open a new terminal tab.

Make sure to have the PC/SC daemon installed:

sudo apt install pcscd

If you are in a VM or in WSL you might get polkit errors, then disable it for the PC/SC daemon:

sudo sh -c 'cat > /etc/default/pcscd' <<'EOF'
PCSCD_ARGS="--disable-polkit"
EOF

The default PC/SC daemon configuration is to automatically exit after a period of inactivity. This will prevent starting/continuously running the simulator, so we need to override the command-line:

sudo mkdir -p /etc/systemd/system/pcscd.service.d
sudo sh -c 'cat > /etc/systemd/system/pcscd.service.d/override.conf' <<'EOF'
[Service]
ExecStart=
ExecStart=/usr/sbin/pcscd --foreground $PCSCD_ARGS
EOF
sudo systemctl daemon-reload
sudo systemctl restart pcscd.service

Then start the applet:

java -jar ../JCardEngine/tool/target/jcard.jar --vsmartcard --params "$PARAMS" build/javacard/FIDO2.cap

Sign and load the attestation certificate

Go back to the previous terminal tab in FIDO2Applet/ where PUBKEY, PRIVKEY, PARAMS are defined.

We make a CA certificate that will be used to sign the attestation certificate:

openssl req -new -x509 \
  -subj '/CN=FIDO2 CA' \
  -newkey EC -pkeyopt ec_paramgen_curve:P-256 -noenc \
  -sha256 \
  -out fido2-ca.pem -keyout fido2-ca-key.pem
openssl x509 -outform DER < fido2-ca.pem > fido2-ca.crt
openssl pkcs8 -outform DER -topk8 -nocrypt < fido2-ca-key.pem > fido2-ca.key

We need to keep the certificate details short as it takes up memory in the FIDO2 applet.

Then sign and load the attestation certificate:

./install_attestation_cert.py \
  --name 'FIDO2 simulator' \
  --ca-cert-bytes $(base64 -w0 fido2-ca.crt) \
  --ca-private-key $(base64 -w0 fido2-ca.key) \
  --already-loaded-public-key "$PUBKEY"

You will get an output similar to the following:

Using AAGUID: 68e89b6a7e33df7d1f7060133c8e5422
Using existing public key b'048a925f6858f03fa39850d8de8307d4cc15142ebfc118e7e1c75c70482707498834d45e7366453cdffdaca2801ef222b99dcf40897c7d16f8231babebd5151a23'
b'68e89b6a7e33df7d1f7060133c8e54220186815901823082017e30820125a00302010202146727cc4ecf29a5f8a41420530447e5afab01c032300a06082a8648ce3d04030230133111300f06035504030c084649444f32204341301e170d3236313031363139333633305a170d3336313031343139333633305a305a310b3009060355040613025553310d300b060355040a0c0441434d4531223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e3118301606035504030c0f4649444f322073696d756c61746f723059301306072a8648ce3d020106082a8648ce3d030107034200048a925f6858f03fa39850d8de8307d4cc15142ebfc118e7e1c75c70482707498834d45e7366453cdffdaca2801ef222b99dcf40897c7d16f8231babebd5151a23a310300e300c0603551d130101ff04023000300a06082a8648ce3d0403020347003044022054a21f2e164f54867d1fd9dd8cd1a6db811130985fcdcaf1bb91414fb3d7187402205861e59d4f5fae80f7c9fdf81c0ab9ee8cf00b2eb52dc71fd66a5259d3d46cd8'
Got response: {} (empty is good)

You can now exit the virtual environment:

deactivate

Now the key is ready for use through the PC/SC interface; but web browsers won't find it, and even the fido2-tools won't find it unless you build libfido2 from source with the -DUSE_PCSC=ON (as of version 1.17.0; it will be enabled by default in the next release).

So we need to setup a PC/SC to USB HID bridge.

Set up the USBHID bridge

Build the bridge

Note: if you want to skip this step, just download the pre-built fido2-hid-bridge for aarch64 or for x86_64 (sig) and skip to Run the bridge.

Get the FIDO2 HID bridge from Bryan Jacobs:

git clone https://github.com/BryanJacobs/fido2-hid-bridge.git
cd fido2-hid-bridge

Note: this guide was written for revision 1f3cad668a37105ea7db484f2f15af88617fa5a7. If you run into problems then run git reset --hard HEAD && git switch --detach 1f3cad668a37105ea7db484f2f15af88617fa5a7 && git clean -fxd to use the same revision and try again.

Make a new virtual environment and install poetry:

python3 -m venv .venv
source .venv/bin/activate
pip install poetry

Add a modern version of cffi to the pyproject.toml otherwise the build will fail:

git apply <<'EOF'
diff --git a/pyproject.toml b/pyproject.toml
index d445c1d..f4940b4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,6 +12,7 @@ uhid = "^0.0.1"
 fido2 = {extras = ["pcsc"], version = "^2.0.0"}
 pyscard = "2.3.1"
 pyinstaller = "^6.21.0"
+cffi = "2.1.1"

 [tool.poetry.scripts]
 fido2-hid-bridge = 'fido2_hid_bridge.bridge:main'
EOF
poetry lock

Install the required packages for compilation:

sudo apt install libffi-dev libpython3-dev

Install the required Python packages:

poetry install

Then build it:

poetry run package

You can now exit the virtual environment:

deactivate

Run the bridge

Create the udev rule so the regular users have access to the device:

sudo sh -c 'cat > /etc/udev/rules.d/virtual-hid.rules' <<'EOF'
ACTION=="add", DEVPATH=="/devices/virtual/misc/uhid/*", MODE="0666"
EOF
sudo udevadm control -R

Now you can run the following to create the bridge (adapt the path if you downloaded the pre-built executable):

sudo ./dist/fido2-hid-bridge

Now make sure to install fido2-tools and you can list the current tokens:

fido2-token -L

And you should see something similar to the following:

/dev/hidraw0: vendor=0x9999, product=0x9999 ( FIDO2 Virtual USB Device)

Set the PIN code and enable enterprise attestation

Now you can run the following to set the PIN code:

fido2-token -S /dev/hidraw0

Note: the path to the device can be different if you already have hidraw devices connected such as a Yubikey, make sure to use the one that says FIDO2 Virtual USB Device.

And enable enterprise attestation:

fido2-token -S -a /dev/hidraw0

Now you can list the properties of the device:

fido2-token -I /dev/hidraw0

Which should give you something like this:

proto: 0x02
major: 0x01
minor: 0x00
build: 0x00
caps: 0x0c (nowink, cbor, nomsg)
version strings: FIDO_2_0, FIDO_2_1, FIDO_2_1_PRE, U2F_V2
extension strings: uvm, credBlob, credProtect, hmac-secret, largeBlobKey, minPinLength
algorithms: es256 (public-key)
aaguid: 68e89b6a7e33df7d1f7060133c8e5422
options: rk, noup, uvAcfg, noalwaysUv, credMgmt, authnrCfg, clientPin, largeBlobs, pinUvAuthToken, setMinPINLength, makeCredUvNotRqd
fwversion: 0x9
maxmsgsiz: 0
maxcredcntlst: 23
maxcredlen: 112
maxcredblob: 32
maxlargeblob: 1024
maxrpids in minpinlen: 2
remaining rk(s): 81
minpinlen: 4
pin protocols: 2, 1
pin retries: 8
pin change required: false
uv retries: undefined
uv modality: 0x200 (none)
long touch for reset: false

You can see the other options of fido2-token in the documentation.

Now your browser should be ready to recognize the token, you can test it out on passkeys.io.

Additionally you can pass through the device into a VM using QEMU for development purpose for Windows software, using the u2f-passthru device. If you are using virt-manager or libvirt in general you will need to edit the configuration to allow access to the /dev/hidrawN device.

links