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.