DEV Community

Bruno Dias
Bruno Dias

Posted on

Build Your Own FIDO2 Security Key and HSM with PicoKeys

Table of Contents

Part 1 — Introduction to PicoKeys

ℹ️ Always use the YubiKey VID/PID

With the VID/PID set to the YubiKey profile, PicoKeys (FIDO version only) can be recognized by ykman (YubiKey Manager). You can view stored FIDO2 credentials and delete individual credentials by their ID:

ykman fido credentials list
ykman fido credentials delete <start-of-ID>
Enter fullscreen mode Exit fullscreen mode

With ykman, we can list and delete FIDO2 credentials, configure YubiKey OTP directly on PicoKeys FIDO, manage the PIN, and more. In my tests, listing and deleting FIDO2 credentials worked without any issues. I was also able to view and change other settings.

I was also able to configure a Yubico OTP credential using the YubiKey slot feature. These slots can store authentication configurations such as Yubico OTP, HOTP, static passwords, and Challenge-Response. Pressing the button once activates slot 1; pressing it twice activates slot 2.

My Recommendation

⚠️ My recommendation

Before getting started, I want to share my recommendation on whether it's worth buying hardware to run PicoKeys firmware.

The answer is: it depends a lot on YOUR goal.

I'd mainly recommend PicoKeys if you really need a physical security key supporting FIDO2 or a simple HSM for personal use, but don't want to pay the price of a YubiKey 5.

However, it's important to consider the total cost of this project. To use PicoKeys properly, we need to apply the recommended security settings. For that, you'll need to purchase a license for the official app, which currently costs around 30 euros (approximately R$ 167.32 at the exchange rate used when I wrote this).

On top of that, you still need to buy a compatible device (such as an ESP32-S3), which will usually cost between R$ 80 and R$ 100, although the price may vary considerably depending on the board, seller, and location.

If you don't want to pay for the app, you can still do everything manually, but then you need to put in the effort to configure the board and keep it up to date.

If your goal is to study and better understand how technologies such as FIDO/FIDO2, HSM, and Smart Cards work, I consider PicoKeys a very good option, especially because the firmware is open source. It lets you experiment with FIDO, HSM, and Smart Cards at a relatively low cost compared with a YubiKey.

However, if you intend to use the key in a real-world environment to protect important accounts, credentials, or keys, my recommendation changes: you need a YubiKey or another more serious security device.

If you still want to use PicoKeys for personal use, you'll need to purchase the official app (or apply all the protections manually) to configure the security features available in the project.

Introduction to PicoKeys

PicoKey is an open-source project that turns microcontrollers (including some RP2040- and ESP32-S3-based boards) into security devices similar to physical authentication keys, such as the YubiKey.

Depending on the firmware used, the device can provide features such as FIDO2, HSM, and OpenPGP & PIV.

The idea is to provide a key similar to a YubiKey: instead of storing certain credentials or private keys directly on the computer, they can be protected inside the physical device. The main difference is that PicoKey is an open-source project.

Because PicoKey uses generic hardware and open-source firmware, it offers plenty of flexibility, but it also requires care. Unlike a YubiKey, whose hardware, firmware, and manufacturing chain are controlled by the manufacturer, PicoKey's security also depends in part on the microcontroller used, the firmware configuration, and the protections enabled on the chip itself.

ℹ️ Firefox compatibility

During my tests, Pico FIDO worked correctly in Google Chrome, including requesting the PIN, but it did not work in Firefox.

I found earlier reports in the Pico FIDO project describing specific incompatibilities with Firefox on Linux. According to those reports, Firefox uses the authenticator-rs library for CTAP/FIDO communication, and some responses generated by Pico FIDO had already caused parsing issues in that implementation.

However, after manually compiling the firmware and changing the VID/PID to YubiKey, it started working in Firefox without issues.

It's worth making clear that the YubiKey doesn't have this problem.

Community vs Enterprise

PicoKey has two main licensing options: Community and Enterprise. The Community version is free and distributed under the AGPLv3 license. It is mainly intended for personal use, studies, labs, testing, and prototype development.

We can download the firmware, study the source code, modify it, and use it normally. However, there's a catch: because of the AGPLv3 license, certain obligations apply if the firmware is modified and redistributed or used as part of certain network services.

The Enterprise version uses a proprietary commercial license and is aimed at production environments, businesses, manufacturers, and deployments involving multiple users or devices. Apart from the licensing difference, Enterprise customers may receive additional features.

For personal use of PicoKey as FIDO2, HSM, or OpenPGP, the Community version is normally sufficient.

PicoKey App

The PicoKey App is the official application for configuring, managing, and updating devices in the PicoKeys ecosystem, including Pico FIDO, Pico HSM, and Pico OpenPGP. It lets us view device information, update firmware, configure board-specific settings, manage credentials, and enable security features such as Secure Boot and Secure Lock.

The app is also responsible for a process called commissioning, which essentially adapts a generic firmware image to the specific hardware on which it is running. This is necessary because the same firmware can run on several different boards, but some characteristics depend on the hardware used, such as:

  • GPIO used by the button;
  • LED and indicator behavior;
  • board identification;
  • USB VID/PID;
  • device-specific settings;
  • certain security settings.

The documentation explains that this information cannot be set generically during firmware compilation and must be configured later on the device.

Among the PicoKey App features are:

  • update firmware;
  • configure VID/PID;
  • configure LED and button;
  • manage passkeys;
  • manage OATH and OTP slots;
  • manage Pico HSM keys;
  • configure PIN and SO-PIN;
  • configure Secure Boot;
  • configure Secure Lock;
  • perform a factory reset;
  • manage some OpenPGP and PIV features.

⚠️ The PicoKey App is not open source

Although the PicoKeys firmware projects remain open source, the PicoKey App is proprietary and its source code is not publicly available.

In addition, the app requires a paid license per device. There are currently two licenses:

  • 1 device: €29.49;
  • 2 devices: €49.49.

The good news is that the licenses are lifetime licenses.

The firmware itself works independently of the app, since its cryptographic features are provided by the firmware rather than the PicoKey App. Even without the app, we'll be able to use FIDO2, WebAuthn, PIC, OpenPGP, HSM, SSH, and so on.

Unfortunately, the problem arises during initial setup and when managing certain hardware-specific features. In newer versions, the app has become the tool for performing commissioning (as mentioned above) and configuring board-specific parameters.

Therefore, for normal PicoKey use in a lab or for simpler personal projects, you won't need the app as much. There is also an alternative for more "advanced" users: you can compile the firmware manually, including the parameters specific to the board you're using.

The developer himself confirmed that PicoKey can be configured without the app. This means the PicoKey App is not technically required for PicoKey to work, but we lose several of its useful features, such as firmware updates and enabling Secure Boot and/or Secure Lock.

Flashing the Firmware

The PicoKeys firmware can be installed using different methods, depending on the controller. The easiest way is to use a ready-made image provided by the project and flash it directly through the browser, or compile the firmware manually.

Earlier, when talking about the app, I mentioned commissioning, but it's worth emphasizing that installing the firmware and performing commissioning are different things. Installing the firmware means flashing Pico FIDO, Pico HSM, or Pico OpenPGP onto the controller.

Commissioning, on the other hand, configures and customizes device characteristics such as VID/PID, USB identification, LED/button, some security options, and other settings specific to the controller being used.

Flashing Through the Browser — ESP32-S2/S3

Boards based on the ESP32-S2 or ESP32-S3 can be flashed directly through a browser. With this method, we don't need to install esptool or ESP-IDF, or download the image manually. The browser communicates directly with the ESP32 bootloader and flashes the selected firmware.

To put an ESP32-S3 into flashing mode, follow these steps:

  1. Hold the BOOT button;
  2. Press the RESET button;
  3. Release the RESET button;
  4. Release the BOOT button.

Then open ESP32 Flasher, select the firmware you want, and click Connect.

💡 DFU or ROM Download Mode?

The PicoKeys documentation calls this procedure DFU Mode, but the term used by the ESP32 manufacturer (Espressif) is ROM Download Mode.

This works because the ESP32 has a bootloader programmed into ROM by the manufacturer. When you use the BOOT + RESET combination, the microcontroller starts that bootloader and waits for new firmware to be flashed. When you flash the firmware, it is written to flash memory, and the ROM starts it.

After flashing, the ESP32 reboots and runs the PicoKeys firmware.

VID/PID

Installing the firmware as explained above uses the parameters provided by the image distributed by the project. However, some third-party tools may not recognize these identifiers. The ESP32 Flasher itself recommends configuring VID/PID to improve compatibility with tools such as OpenSC, PKCS#11, ykman, gpg, and others.

The VID (Vendor ID) identifies the USB device manufacturer, while the PID (Product ID) identifies a product associated with that manufacturer. Some applications use these identifiers to find compatible devices, while others can identify them directly from the protocols they implement.

This means that changing the VID/PID is not required to use every firmware feature, but it may be necessary for certain tools to recognize the device. For example, Pico FIDO uses HID, a USB interface commonly supported by operating systems and browsers. This allows the device to work as a FIDO2/WebAuthn authenticator without relying on the PicoKey App or on another manufacturer's specific VID/PID.

Pico HSM uses CCID, an interface for communicating with smart card devices. In this case, recognition may depend on the identifiers supported by the driver or application being used.

The official documentation describes several ways to change USB identifiers without relying on the PicoKey App, which is the easiest method for changing them, although I've never used it myself.

PicoKey App

The PicoKey App lets you select predefined identifiers or enter a VID/PID manually. The settings are applied during commissioning, without recompiling the firmware. However, as we've already seen, this requires paying for the app.

Pico Patcher

They also provide Pico Patcher, a free tool that lets you change the identifiers directly in your browser, but it only works with Raspberry Pi boards. The procedure is as follows:

  1. Visit the Pico Patcher page.
  2. Select a firmware file in .uf2 format.
  3. Choose one of the available manufacturers or enter a custom VID/PID.
  4. Click Patch to generate a new firmware image.
  5. Flash the modified image onto the device.

Available presets include YubiKey, Nitrokey, GnuPG, and FSIJ. All processing takes place locally in the browser, so the modified firmware is not uploaded to the project's servers.

Dynamic Changes Through Pico HSM

Pico HSM also offers a command-line alternative through the pico-hsm-tool.py tool. According to the official documentation, you can change the VID/PID dynamically using:

./pico-hsm-tool.py phy vidpid 1234:5678
Enter fullscreen mode Exit fullscreen mode

The values 1234:5678 are examples and should be replaced with the desired identifiers. After successfully running the command, you need to restart the device for the new identifiers to take effect. This method lets you change VID/PID without creating a new firmware image.

Manual Compilation

Another method is to configure the USB identifiers directly in the source code or firmware build parameters. The Pico HSM and Pico FIDO repositories document options such as USB_VID, USB_PID, and VIDPID for builds targeting Raspberry Pi Pico boards.

On ESP32-S3, you need to use -DVIDPID=XXX to specify the profile you want to apply. The table below lists the available profile names.

Group Profile VID:PID
HSM NitroHSM 20A0:4230
HSM YubiHSM 1050:0030
FIDO NitroFIDO2 20A0:42B1
FIDO Nitro3 20A0:42B2
FIDO Yubikey5 1050:0407
FIDO YubikeyNeo 1050:0116
PGP/OpenPGP NitroStart 20A0:4211
PGP/OpenPGP NitroPro 20A0:4108
PGP/OpenPGP Gnuk 234B:0000
PGP/OpenPGP GnuPG 1209:2440

Using an ESP32-S3

I bought a XIAO ESP32-S3 because the PicoKeys project itself recommends it, especially for security reasons. The main advantage of this board is that it has security features implemented directly in hardware, improving protection for both the firmware and the keys stored on it.

One of its main features is Secure Boot v2. It cryptographically verifies the bootloader and firmware before executing them. Once configured, an attacker cannot simply flash modified firmware onto the device to try to extract its keys. Only firmware signed with an authorized key will be executed.

⚠️ Be careful with Secure Boot

Be very careful when using Secure Boot v2. Once PicoKeys enables it, the board will only run PicoKeys firmware with a valid signature recognized by the device.

This means the board will only work with firmware signed by PicoKeys and will not run other firmware. Even resetting the board won't let you install other firmware.

This happens because Secure Boot programs a reference to the accepted public key into an eFuse. As a result, any new bootloader or firmware must be signed with a key corresponding to that reference. If you flash firmware that isn't signed with the private key corresponding to the public key in the eFuse, the ESP32-S3 will refuse to run it.

So only enable Secure Boot when you're sure you won't use the board for any other purpose.

Another important feature of this board is Flash Encryption. It has two operating modes. Under the default configuration, the ESP32-S3 generates an XTS-AES key during its first boot and stores it in the eFuses (this mode is known as Development), protected against reading. The problem is that, in Release mode, a different method is required to flash new firmware: it must be signed and then encrypted with the board's key before flashing.

You can also generate the Flash Encryption key on your computer, write it to the eFuses before enabling encryption, and keep it on the computer to prepare firmware before flashing (this mode is known as Release). The advantage is that we can keep flashing firmware using ROM Download Mode, but we must send an already encrypted image. To do that, we need to use --no-stub in Secure Download Mode.

Because Secure Boot also uses eFuses, it's worth clarifying that it uses a different area from Flash Encryption. With Secure Boot, the eFuses store a reference to the key used to verify firmware signatures. With Flash Encryption, they store the key used to encrypt flash memory data.

When PicoKey uses an eFuse area to store cryptographic data, that area is write-protected to prevent changes. Also, eFuses are permanent: once programmed, their contents cannot simply be deleted or restored as they can in flash memory.

Another useful feature is Secure Lock. It programs permanent locks into eFuses to prevent changes to certain security settings. In theory, that is what Secure Lock does, but in my tests, several protections were applied simply by enabling Secure Boot, including many associated with Secure Lock. JTAG, for example, was permanently disabled, and ROM Download entered Secure Download Mode, which restricts available commands. Therefore, even without applying Secure Lock, these protections remain active after enabling Secure Boot. Secure Lock does a few other things, but I see those additional protections as desirable rather than essential; not having them doesn't invalidate using the device.

The ESP32-S3 also has hardware cryptographic accelerators for AES and SHA, along with HMAC, RSA Digital Signature (RSA_DS), and RNG peripherals. The HMAC peripheral can use a key stored in eFuses without exposing it to software. RSA_DS can also use HMAC to protect RSA private key parameters and perform signatures without exposing the key to firmware.

In Pico HSM, the SDK also enables the internal SAR ADC entropy source and uses the hardware RNG to obtain random data. Physical noise from the chip is mixed into the RNG, helping generate keys and other random values. This source is internal to the ESP32-S3, so there's no need to connect an antenna or any other component.

With this entropy source enabled, the ESP32-S3 hardware RNG uses physical noise from the SAR ADC to generate truly random numbers instead of relying only on a pseudorandom sequence. The SAR ADC stands for Successive Approximation Register Analog-to-Digital Converter. It is a converter that measures an analog voltage and turns it into a digital value. On the ESP32-S3, the chip's natural electrical noise slightly changes these measurements. With the entropy source enabled, the hardware RNG uses and mixes these variations to produce random numbers. The ADC does not generate the numbers on its own; it provides a physical entropy source for the RNG.

That's what I understood about the SAR ADC, but I should make it clear that this is outside my area of study, so I'm sharing my understanding 😅.

⚠️ Be careful with Secure Boot

I've already warned about the care needed when enabling Secure Boot, but it's worth repeating: once enabled, Secure Boot cannot be disabled.

Once this protection is enabled by PicoKeys firmware, the board will only accept PicoKeys firmware or firmware you sign yourself (if you compiled it manually). If you're using the app, make sure you don't plan to reuse this board for other projects or firmware later, because that won't be possible.

A Microcontroller Is Not a Secure Element

⚠️ A microcontroller is not a Secure Element

I want to make it very clear that, as interesting and functional as this project is, even with an Enterprise version and the many security features of the ESP32-S3, this does not make it equivalent to a YubiKey or a certified enterprise HSM.

The ESP32-S3 is a microcontroller designed for a wide range of projects, from very simple ones to more advanced applications. However, that doesn't make the ESP32-S3 a Secure Element. It wasn't specifically designed to withstand advanced physical attacks like a YubiKey.

This is also explained in the PicoKeys project documentation. Attacks such as decapping, microprobing, advanced fault injection, and other forms of laboratory physical analysis are outside the project's threat model.

Flashing FIDO2 Through the Browser

I'll do everything online using the browser-based tools provided by the project. The official project recommends using ESP32 Flasher online. The official installer is available at https://www.picokeys.com/esp32-flasher/.

ℹ️ Note

Perform the entire process using Google Chrome or, if you want to use Firefox, make sure it isn't the Snap version.

Before flashing the firmware, we need to put the XIAO ESP32-S3 into the boot mode used for programming. PicoKeys calls this DFU Mode. With the board connected to the computer, do the following:

  1. Press and hold the BOOT (B) button;
  2. Press and release the RESET (R) button;
  3. Release the BOOT (B) button.

The board will enter a mode where it waits to be flashed. With the ESP32-S3 in DFU Mode, open the official installer at https://www.picokeys.com/esp32-flasher/.

  1. Select Pico Fido;
  2. Click Connect;
  3. Select the device corresponding to the ESP32-S3;
  4. Confirm the connection;
  5. Start flashing;
  6. Wait until the process is completely finished before disconnecting the board.

The website itself downloads and flashes the firmware required by the ESP32-S3. After flashing, disconnect and reconnect the board. It will boot with Pico FIDO.

Now let's check whether Linux specifically recognizes the FIDO2 interface.

# Install the tools:
sudo apt update && sudo apt install -y fido2-tools

# List the devices:
$ fido2-token -L
/dev/hidraw17: vendor=0x2e8a, product=0x10fe (Pol Henarejos Pico Key)

# List our FIDO device's capabilities:
$ fido2-token -I /dev/hidraw17
proto: 0x02
major: 0x08
minor: 0x00
build: 0x00
caps: 0x05 (wink, cbor, msg)
version strings: U2F_V2, FIDO_2_0, FIDO_2_1, FIDO_2_2, FIDO_2_3
extension strings: uvm, credBlob, credProtect, hmac-secret, largeBlobKey, minPinLength, hmac-secret-mc, thirdPartyPayment
algorithms: es256 (public-key), es384 (public-key), unknown (public-key)
aaguid: 89fb94b706c936739b7e30526d968145
options: rk, noalwaysUv, credMgmt, authnrCfg, noclientPin, largeBlobs, perCredMgmtRO, pinUvAuthToken, setMinPINLength, nomakeCredUvNotRqd
fwversion: 0x800
maxmsgsiz: 1024
maxcredcntlst: 16
maxcredlen: 1024
maxlargeblob: 2048
maxrpids in minpinlen: 120
minpinlen: 4
pin protocols: 1, 2
pin retries: 8
pin change required: false
uv retries: undefined
Enter fullscreen mode Exit fullscreen mode
Configuring a PIN

We already discussed PINs when I explained the YubiKey, specifically FIDO authentication. It can require only the physical presence of the key and a touch, or the key plus additional user verification such as a PIN or biometrics.

Basically, the PIN confirms that the person using the physical key is authorized to access certain functions, such as passkeys stored on the device and management operations.

A PIN is optional for some operations, so you can use Pico FIDO without one. However, some services and features may require user verification, making a PIN necessary.

To configure a PIN, we can use fido2-token:

fido2-token -S /dev/hidraw17
Enter fullscreen mode Exit fullscreen mode

Another way to configure the PIN is through the PicoKey App. The app lets you set the PIN and adjust related policies, such as minimum length and complexity.

To change the PIN:

fido2-token -C /dev/hidraw17
Enter fullscreen mode Exit fullscreen mode

To remove the PIN, you need to reset the authenticator. Be careful, though: a reset does not delete just the PIN. It also deletes credentials/passkeys stored on the device and restores the FIDO state.

fido2-token -R /dev/hidraw17
Enter fullscreen mode Exit fullscreen mode
Listing and Deleting Configured Passkeys

We can list resident passkeys stored on the key, but only those that are discoverable. We can also delete them individually using fido2-token. A discoverable credential is a FIDO2 credential stored on the physical key that the key can find without the website first specifying which credential to use. This enables passkey/passwordless scenarios, in which you can sign in without first entering a username and password.

A non-discoverable credential works differently. The private key is still protected inside the physical device, but the device doesn't keep a searchable entry for that login. In this case, the server must send a Credential ID to specify which credential to use.

A discoverable passkey needs to keep a persistent record inside the authenticator with enough data to find that account later. The problem is that this consumes the key's internal storage, so it's always a good idea to delete credentials you no longer use.

A non-discoverable credential usually takes up much less, or almost no, space on the key. This is because the server stores the Credential ID and sends it back during login. The key derives or retrieves the necessary material from that identifier without having to store one entry per website.

⚠️ Bug when deleting passkeys

There is a bug when deleting resident passkeys from the key, and it was still present as of October 6, 2026. Apparently, it's a bug related to fido2-token/libfido2, but we can delete them without problems using Python's fido2 library. I'll include the code I used at the end of this section.

# Check which /dev/hidrawX belongs to the key:
$ fido2-token -L
/dev/hidraw3: vendor=0x2e8a, product=0x10fe (Pol Henarejos Pico Key)

# List the registered discoverable passkeys:
$ sudo fido2-token -L -r /dev/hidraw3
Enter PIN for /dev/hidraw3: 
00: asdasdasdasdafU3lMtcdsffdsdHLsdfsffsOOgd459= teste.com

# To delete one, the command should be:
$ sudo fido2-token -D -i 'asdasdasdasdafU3lMtcdsffdsdHLsdfsffsOOgd459=' /dev/hidraw3
Enter fullscreen mode Exit fullscreen mode

💡 Python script for fido2

#!/usr/bin/env python3

import getpass
import sys

from fido2.hid import CtapHidDevice
from fido2.ctap2 import Ctap2, ClientPin, CredentialManagement


devices = list(CtapHidDevice.list_devices())

if not devices:
    print("No FIDO devices found.")
    sys.exit(1)

print("Devices found:", '\n')

for i, dev in enumerate(devices):
    print(f"\t[{i}] {dev}")

idx = int(input("\nEnter the device ID: "))

dev = devices[idx]
ctap = Ctap2(dev)

#print("\nSupported CTAP versions:", ctap.info.versions)
#print("Options:", ctap.info.options)

pin = getpass.getpass("\nEnter the FIDO PIN: ")

client_pin = ClientPin(ctap)

token = client_pin.get_pin_token(
    pin,
    permissions=ClientPin.PERMISSION.CREDENTIAL_MGMT,
)

credman = CredentialManagement(
    ctap,
    client_pin.protocol,
    token,
)

print("\nDiscoverable credentials:\n")

all_credentials = []

for rp in credman.enumerate_rps():
    rp_info = rp[CredentialManagement.RESULT.RP]
    rp_hash = rp[CredentialManagement.RESULT.RP_ID_HASH]

    print(f"RP: {rp_info}")

    creds = credman.enumerate_creds(rp_hash)

    for cred in creds:
        descriptor = cred[CredentialManagement.RESULT.CREDENTIAL_ID]
        user = cred[CredentialManagement.RESULT.USER]

        n = len(all_credentials)

        print(f"  [{n}]")
        print(f"      User: {user}")
        print(f"      Credential ID: {descriptor['id'].hex()}")

        all_credentials.append(
            (rp_info, user, descriptor)
        )

if not all_credentials:
    print("No discoverable credentials found.")
    sys.exit(0)

print()

choice = input(
    "Enter the number of the credential to delete or press ENTER to exit: "
)

if choice == "":
    sys.exit(0)

try:
    choice = int(choice)
except ValueError:
    print("Invalid index.")
    sys.exit(1)

if choice < 0 or choice >= len(all_credentials):
    print("Index out of range.")
    sys.exit(1)

rp, user, descriptor = all_credentials[choice]

print("\nYou selected:")
print("RP:", rp)
print("User:", user)
print("Credential ID:", descriptor["id"].hex())

confirm = input("\nType APAGAR to confirm: ")

if confirm != "APAGAR":
    print("Cancelled.")
    sys.exit(0)

try:
    credman.delete_cred(descriptor)
except Exception as e:
    print("\nError deleting credential:")
    print(repr(e))
    sys.exit(1)

print("\nCredential deleted successfully.")
Enter fullscreen mode Exit fullscreen mode

The table below summarizes what we can do with the fido2-token command:

App Feature How
Detect Pico FIDO fido2-token -L
View capabilities/AAGUID fido2-token -I <device>
Set PIN fido2-token -S <device>
Change PIN fido2-token -C <device>
Reset FIDO fido2-token -R <device>
List passkeys fido2-token -L -r <device> + -L -k
View passkey metadata fido2-token -I -c <device>
Delete passkeys fido2-token -D -i ...
Change credential name/display name fido2-token -S -c ...
Read/write/delete largeBlob fido2-token -G/-S/-D -b ...
Enable Always UV fido2-token -S -u
Disable Always UV fido2-token -D -u
Set minimum PIN length fido2-token -S -l N
Create credentials manually fido2-cred
Test authentication/assertion fido2-assert

↑ Back to the Table of Contents

Part 2 — Compiling PicoKeys Firmware

Compiling the Firmware Without Secure Boot

First, I'll show how to compile without enabling Secure Boot, since this feature is irreversible. This way, you can test and use the board, and reflash it more easily if needed. Use a board as a lab device: test and learn before taking irreversible steps. Once everything is working and you feel comfortable and confident, we'll enable the security features.

I'll continue using the XIAO ESP32-S3 for the procedures described here. I want to clarify that I won't specify a tag when cloning the repositories, since the installation procedure has changed and new improvements have been added to each project's main branch. Below are the branches I used for each project:

### FIDO
Hash: ade770281dd4fd8c627add34ed078f56ec40a07b
tag: nightly-main and nightly-development

### HSM
Hash: 5d6a32c81b2367fb83e5320400f61217d2bd0d2f 
tag: nightly-master and nightly-development
Enter fullscreen mode Exit fullscreen mode

I submitted two fixes for security issues in the FIDO project, so they may soon be merged into the main project. In that case, you will still be able to use the hashes above, but you'll need to apply the fixes manually until they are incorporated.

I just want you to understand that using the main branch changes the way the firmware is compiled, since new features have been added and the build process is even easier. That's why I didn't specify a tag when cloning the repositories.

Installing Dependencies and ESP-IDF

I'll install the compilation tools used by ESP-IDF on a virtual machine running Ubuntu 26.04.1:

sudo apt update
sudo apt install -y git wget flex bison gperf python3 python3-pip \
  python3-venv cmake ninja-build ccache libffi-dev libssl-dev \
  dfu-util libusb-1.0-0 openssl
Enter fullscreen mode Exit fullscreen mode

Let's create a directory to store and install the ESP-IDF source code:

mkdir -p ~/src/picokeys
cd ~/src/picokeys
Enter fullscreen mode Exit fullscreen mode

Because ESP-IDF uses Git submodules for its components and dependencies, we need to clone with --recursive to download them. Otherwise, we'd have the main repository but not the complete source code required to compile the firmware. I'll also download tag v5.5.1 specifically, corresponding to version 5.5.1 of the code. Even though this is the latest available version as I write this tutorial, I want to make it very clear which version I'm using so it doesn't get confused with whatever version you might be running.

Another reason for using v5.5.1 specifically is that the Pico FIDO project's build script uses that exact version. It runs git checkout tags/v5.5.1 before installing the tools and compiling.

To find out which version to use, check these files:

Search for tags/v, and you'll see the following:

elif [[ $1 == "esp32" ]]; then
sudo apt install -y git wget flex bison gperf python3 python3-pip python3-venv cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0
git clone --recursive https://github.com/espressif/esp-idf.git
cd esp-idf
git checkout tags/v5.5.1
./install.sh esp32s3
. ./export.sh
Enter fullscreen mode Exit fullscreen mode

The key line for identifying the version is git checkout tags/v5.5.1. So let's clone ESP-IDF at exactly version 5.5.1:

git clone --branch v5.5.1 --recursive https://github.com/espressif/esp-idf.git esp-idf && cd esp-idf
Enter fullscreen mode Exit fullscreen mode

Now let's install the tools needed to develop and compile firmware for the ESP32-S3:

unset IDF_PYTHON_ENV_PATH ; ./install.sh esp32s3
Enter fullscreen mode Exit fullscreen mode

The IDF_PYTHON_ENV_PATH variable specifies where the esp-idf environment is installed. I'm including it in case you have another version already loaded in your environment.

Whenever you open a new terminal, you need to reload the ESP-IDF environment:

cd ~/src/picokeys/esp-idf

# Load the ESP-IDF environment:
. ./export.sh

# Check the version:
$ idf.py --version
ESP-IDF v5.5.1
Enter fullscreen mode Exit fullscreen mode

Downloading and Compiling Pico FIDO

With the ESP-IDF environment loaded, let's clone and compile Pico FIDO:

cd ~/src/picokeys 
git clone --recursive https://github.com/polhenarejos/pico-fido.git && cd pico-fido
Enter fullscreen mode Exit fullscreen mode

⚠️ Removing the automatic notification when connecting the board

Every time I connect the board to my computer, Chrome shows a pop-up saying that a Pico Key was detected. Since I don't like this behavior, I'll disable it.

In Pico FIDO v8.0, the address is stored in the SDK's USB descriptor:

cd ~/src/picokeys/pico-fido

vim pico-keys-sdk/src/usb/usb_descriptors.c
Enter fullscreen mode Exit fullscreen mode

Find TUD_BOS_WEBUSB_DESCRIPTOR, then change 1 to 0, as shown below:

// Before
TUD_BOS_WEBUSB_DESCRIPTOR(VENDOR_REQUEST_WEBUSB, 1),

// After
TUD_BOS_WEBUSB_DESCRIPTOR(VENDOR_REQUEST_WEBUSB, 0),
Enter fullscreen mode Exit fullscreen mode

This parameter is the landing page index. With 0, the firmware no longer advertises a page for Chrome to open, so the automatic notification should disappear.

Failure to Enforce the alwaysUv Policy

The alwaysUv setting specifies that the authenticator (physical security key) must always require user verification (via PIN or biometrics) before performing authentication operations. However, I found a possible inconsistency in src/fido/cbor_get_assertion.c: the firmware checks authorization when it is provided, but doesn't explicitly require that authorization on every execution path when alwaysUv is enabled.

This could allow certain authentication operations to be performed without user verification, bypassing the configured security policy. To fix this, edit the following file:

vim src/fido/cbor_get_assertion.c
Enter fullscreen mode Exit fullscreen mode

Replace this:

    if (next == false) {
        if (pinUvAuthParam.present == true) {
            if (pinUvAuthParam.len == 0 || pinUvAuthParam.data == NULL) {
                if (check_user_presence() == false) {
                    CBOR_ERROR(CTAP2_ERR_OPERATION_DENIED);
                }
Enter fullscreen mode Exit fullscreen mode

With this:

    if (next == false) {
        bool alwaysUv = (get_opts() & FIDO2_OPT_AUV) ||
                        (file_has_data(ef_pin) && !keydev_unlocked);

        if (alwaysUv && pinUvAuthParam.present == false) {
            CBOR_ERROR(CTAP2_ERR_PUAT_REQUIRED);
        }

        if (pinUvAuthParam.present == true) {
            if (pinUvAuthParam.len == 0 || pinUvAuthParam.data == NULL) {
                if (check_user_presence() == false) {
                    CBOR_ERROR(CTAP2_ERR_OPERATION_DENIED);
                }
Enter fullscreen mode Exit fullscreen mode

ℹ️ Tests to validate the alwaysUv issue

I tested the behavior using the original firmware and was able to confirm the issue:

$ python3 fido_alwaysUv.py
Factory reset deletes the PIN, credentials, and FIDO settings.
Keep only the device being tested connected.

A Pico with a YubiKey VID/PID may look identical to a real YubiKey; the program cannot physically tell them apart.
With the disposable Pico connected, press Enter. Then disconnect it when the program tells you to:
Waiting for disconnection. Disconnect the Pico now and keep it unplugged from USB...

Disconnection detected. Reconnect the Pico now...

Sending factory reset immediately after reconnection...
Factory reset completed.
Creating a test FIDO credential...
Setting a test PIN and enabling alwaysUv...
alwaysUv is enabled.
Testing getAssertion without PIN/UV...

FAILURE: BUG PRESENT — getAssertion succeeded without PIN/UV while alwaysUv was enabled.
Running a positive control with PIN/UV...
OK: the authenticated positive control worked and set the UV flag.
Enter fullscreen mode Exit fullscreen mode

After applying the fix above, I ran another test that showed the fix was effective.

$ python3 fido_alwaysUv.py
Factory reset deletes the PIN, credentials, and FIDO settings.
Keep only the device being tested connected.

A Pico with a YubiKey VID/PID may look identical to a real YubiKey; the program cannot physically tell them apart.
With the disposable Pico connected, press Enter. Then disconnect it when the program tells you to:
Waiting for disconnection. Disconnect the Pico now and keep it unplugged from USB...

Disconnection detected. Reconnect the Pico now...

Sending factory reset immediately after reconnection...
Factory reset completed.
Creating a test FIDO credential...
Setting a test PIN and enabling alwaysUv...
alwaysUv is enabled.
Testing getAssertion without PIN/UV...

OK: getAssertion without a UV token was rejected with PIN_REQUIRED.
Running a positive control with PIN/UV...
OK: the authenticated positive control worked and set the UV flag.
RESULT: PASSED — getAssertion required PIN/UV and the authenticated control worked.
Enter fullscreen mode Exit fullscreen mode
Memory Validation During U2F Processing

I found a possible problem with how Pico FIDO validates the length of data received during U2F operations. Although this is an older protocol, many systems may still use U2F, so it's worth fixing. The firmware uses a length supplied by the client (such as a browser or any other system requesting the operation) without fully checking whether it matches the actual amount of data received.

This could cause out-of-bounds memory reads, crashes, or unexpected behavior when the device receives malformed messages. Someone with access to the computer connected to Pico FIDO could send crafted USB commands to try to exploit this flaw. To address the problem, edit the following file:

vim src/fido/cmd_authenticate.c
Enter fullscreen mode Exit fullscreen mode

Replace these lines:

    if (apdu.nc < CTAP_CHAL_SIZE + CTAP_APPID_SIZE + 1 + 1) {
        return SW_WRONG_DATA();
    }
    if (req->keyHandleLen < KEY_HANDLE_LEN) {
        return SW_INCORRECT_PARAMS();
    }
Enter fullscreen mode Exit fullscreen mode

With these:

    const size_t key_handle_offset = offsetof(CTAP_AUTHENTICATE_REQ, keyHandle);

    if (apdu.nc < key_handle_offset) {
        return SW_WRONG_DATA();
    }
    if (req->keyHandleLen < KEY_HANDLE_LEN ||
        req->keyHandleLen > CTAP_MAX_KH_SIZE) {
        return SW_INCORRECT_PARAMS();
    }
    if ((size_t)apdu.nc < key_handle_offset + req->keyHandleLen) {
        return SW_WRONG_DATA();
    }
Enter fullscreen mode Exit fullscreen mode

In the same file, replace the two lines below:

    uint8_t *tmp_kh = (uint8_t *) calloc(1, req->keyHandleLen);
    memcpy(tmp_kh, req->keyHandle, req->keyHandleLen);
Enter fullscreen mode Exit fullscreen mode

With these:

    uint8_t *tmp_kh = (uint8_t *) calloc(1, req->keyHandleLen);
    if (tmp_kh == NULL) {
        mbedtls_ecp_keypair_free(&key);
        return SW_MEMORY_FAILURE();
    }
    memcpy(tmp_kh, req->keyHandle, req->keyHandleLen);
Enter fullscreen mode Exit fullscreen mode

ℹ️ Tests to validate the U2F processing issue

I tested the behavior using the original firmware and confirmed the issue:

$ python3 fido_u2f_memory_test.py
Device detected: Pol Henarejos Pico Key, VID=1050, PID=0407

Physically confirm that this is the disposable Pico FIDO.
Type TESTAR to continue: TESTAR

Truncated key handle (expected SW_WRONG_DATA 6700): received 69 85
FAILED: expected 67 00
Oversized key handle (expected SW_INCORRECT_PARAMS 6A80): received 69 85
FAILED: expected 6A 80
RESULT: FAILED — check the received status codes and installed firmware.
Enter fullscreen mode Exit fullscreen mode

After applying the fix above, I ran another test that showed the fix was effective.

$ python3 fido_u2f_memory_test.py
$ python3 fido_u2f_memory_test.py
Device detected: Pol Henarejos Pico Key, VID=1050, PID=0407
Physically confirm that this is the disposable Pico FIDO.
Type TESTAR to continue: TESTAR

Truncated key handle (expected SW_WRONG_DATA 6700): received 67 00
OK
Oversized key handle (expected SW_INCORRECT_PARAMS 6A80): received 6A 80
OK
RESULT: PASSED — both invalid APDUs were rejected.
Enter fullscreen mode Exit fullscreen mode
Compiling FIDO

Before compiling, I'd like to mention two security issues I found in the FIDO firmware. Although I've already implemented the fixes and validated the results in my own tests, I also submitted a suggestion to the official project so the developers can review both the fixes and their impact, and potentially include these improvements in the official code in the future.

The test scripts were generated using AI and reviewed by me. You can access the scripts on my GitLab.

Now let's compile.

# If you compiled HSM before FIDO, run:
idf.py fullclean

# Set ESP32-S3 as the target microcontroller for firmware compilation:
idf.py set-target esp32s3

# Compile the source code using the YubiKey5 VID/PID:
idf.py -DVIDPID=Yubikey5 build

# To compile using the PicoKeys VID/PID, run the command below:
idf.py build
Enter fullscreen mode Exit fullscreen mode

Compiling Pico FIDO generates several .bin files, including the bootloader, partition table, and main firmware. The build/flash_args file tells us which files to flash and at which flash addresses. The command below combines these files into a single image, which I'm calling pico_fido_esp32-s3.bin. This step is optional if you plan to flash the device using idf.py flash.

However, this command is necessary if you want to create a single .bin file to distribute or flash using another method, such as esptool.py.

mkdir -p release && cd build

# Merge the '.bin' files generated during compilation:
esptool.py --chip ESP32-S3 merge_bin -o ../release/pico_fido_esp32-s3.bin @flash_args && cd ..
Enter fullscreen mode Exit fullscreen mode

Downloading and Compiling Pico HSM

⚠️ Use only the v6.6 tag

I ran some partial tests with Pico HSM and had a lot of trouble getting it recognized at first. From what I've seen, it still isn't fully mature. To get the device recognized by pkcs11-tool and opensc-tool, I had to use exactly version 6.6, with commit 251b35dd9c4fd929923fc3b192f6793826efeaef, tagged v6.6.

In my tests, firmware compiled from the main branch wasn't recognized by these tools. I also had to use libccid version 1.8.2 or newer. The earlier versions I tested didn't recognize the HSM.

Another important detail: don't use the YubiHSM VID/PID. Compile without specifying a particular profile. The yubihsm-shell tool doesn't recognize Pico HSM when it is compiled with YubiHSM because that option only changes the USB VID/PID. It doesn't make the firmware implement the YubiHSM 2 protocol, which yubihsm-shell uses. Pico HSM communicates over CCID and should be accessed using compatible tools such as opensc-tool and pkcs11-tool.

The tutorial described here only works with the Pico HSM main branch. I'll keep these build instructions here because a lot has changed since version 6.6, including the way the firmware is compiled, so I believe this pattern will continue with minor adjustments. But keep in mind that using the main branch will cause the HSM not to be recognized (at least at the time I wrote this tutorial).

💡 How to compile version 6.6?

To learn how to compile exactly version 6.6, read this guide. This covers the basic process. Other customizations, such as Secure Boot and Flash Encryption, can still be applied; you just need to fix the issues so the firmware compiles according to the linked guide.

Pico HSM also uses ESP-IDF targeting ESP32-S3, and we'll use version 5.5.1 here as well, for the same reasons as with FIDO.

cd ~/src/picokeys
git clone --recursive https://github.com/polhenarejos/pico-hsm.git && cd pico-hsm
Enter fullscreen mode Exit fullscreen mode

⚠️ Removing the automatic notification when connecting the board

Every time I connect the board to my computer, Chrome shows a pop-up saying that a Pico Key was detected. Since I don't like this behavior, I'll disable it.

In Pico HSM, the address is in the SDK's USB descriptor:

cd ~/src/picokeys/pico-hsm

vim pico-keys-sdk/src/usb/usb_descriptors.c
Enter fullscreen mode Exit fullscreen mode

Find TUD_BOS_WEBUSB_DESCRIPTOR, then change 1 to 0, as shown below:

// Before
TUD_BOS_WEBUSB_DESCRIPTOR(VENDOR_REQUEST_WEBUSB, 1),

// After
TUD_BOS_WEBUSB_DESCRIPTOR(VENDOR_REQUEST_WEBUSB, 0),
Enter fullscreen mode Exit fullscreen mode

This parameter is the landing page index. With 0, the firmware no longer advertises a page for Chrome to open, so the automatic notification should disappear.

In the code I tested, two adjustments were required before compilation. These changes make ESP-IDF register the HSM component and find the libcvc dependency. Edit the CMakeLists.txt file in the Pico HSM root directory:

vim CMakeLists.txt
Enter fullscreen mode Exit fullscreen mode

Inside if(ESP_PLATFORM) (the first block, usually around line 23), define PICOKEYS_COMPONENTS as hsm before the first ESP-IDF include(...). The original section looks like this:

if(ESP_PLATFORM)
    set(EXTRA_COMPONENT_DIRS pico-keys-sdk/config/esp32/components src/hsm)
    include($ENV{IDF_PATH}/tools/cmake/project.cmake)
else()
Enter fullscreen mode Exit fullscreen mode

It should look like this:

if(ESP_PLATFORM)
    set(PICOKEYS_COMPONENTS hsm)
    set(EXTRA_COMPONENT_DIRS pico-keys-sdk/config/esp32/components src/hsm)
    include($ENV{IDF_PATH}/tools/cmake/project.cmake)
else()
Enter fullscreen mode Exit fullscreen mode

Now let's make ESP-IDF find the libcvc dependency. Start by editing the Pico Keys SDK importer:

vim pico-keys-sdk/picokeys_sdk_import.cmake
Enter fullscreen mode Exit fullscreen mode

Inside if(ESP_PLATFORM), there is a set(EXTRA_COMPONENT_DIRS ...) definition. Find it (usually around line 73 in the file on the main branch). The full original section looks like this:

if(ESP_PLATFORM)
    if(DEFINED CONFIG_TINYUSB_DESC_CUSTOM_VID)
        set(USB_VID CONFIG_TINYUSB_DESC_CUSTOM_VID)
    endif()
    if(DEFINED CONFIG_TINYUSB_DESC_CUSTOM_PID)
        set(USB_PID CONFIG_TINYUSB_DESC_CUSTOM_PID)
    endif()
    if(NOT DEFINED ENABLE_POWER_ON_RESET)
        set(ENABLE_POWER_ON_RESET 0)
    endif()
    if(NOT DEFINED ENABLE_PQC)
        set(ENABLE_PQC 0)
    endif()
    if(NOT DEFINED SDKCONFIG_DEFAULTS AND NOT DEFINED ENV{SDKCONFIG_DEFAULTS})
        set(SDKCONFIG_DEFAULTS "pico-keys-sdk/config/esp32/sdkconfig.defaults")
    endif()
    set(EXTRA_COMPONENT_DIRS
        src/${PICOKEYS_COMPONENTS}
        pico-keys-sdk/config/esp32/components/mbedtls
        pico-keys-sdk/config/esp32/components/pico-keys-sdk
    )
    set(COMPONENTS ${PICOKEYS_COMPONENTS} pico-keys-sdk)
    if (USB_ITF_HID)
        list(APPEND EXTRA_COMPONENT_DIRS
            pico-keys-sdk/config/esp32/components/tinycbor
        )
        list(APPEND COMPONENTS tinycbor)
    endif()
    if(ENABLE_PQC)
        list(APPEND EXTRA_COMPONENT_DIRS
            pico-keys-sdk/config/esp32/components/mlkem512
            pico-keys-sdk/config/esp32/components/mlkem768
            pico-keys-sdk/config/esp32/components/mlkem1024
        )
    endif()
endif()
Enter fullscreen mode Exit fullscreen mode

After set(EXTRA_COMPONENT_DIRS ...), add:

    if(ENABLE_LIBCVC)
        list(APPEND EXTRA_COMPONENT_DIRS
            pico-keys-sdk/config/esp32/components/libcvc
        )
    endif()
Enter fullscreen mode Exit fullscreen mode

The new full section looks like this:

if(ESP_PLATFORM)
    if(DEFINED CONFIG_TINYUSB_DESC_CUSTOM_VID)
        set(USB_VID CONFIG_TINYUSB_DESC_CUSTOM_VID)
    endif()
    if(DEFINED CONFIG_TINYUSB_DESC_CUSTOM_PID)
        set(USB_PID CONFIG_TINYUSB_DESC_CUSTOM_PID)
    endif()
    if(NOT DEFINED ENABLE_POWER_ON_RESET)
        set(ENABLE_POWER_ON_RESET 0)
    endif()
    if(NOT DEFINED ENABLE_PQC)
        set(ENABLE_PQC 0)
    endif()
    if(NOT DEFINED SDKCONFIG_DEFAULTS AND NOT DEFINED ENV{SDKCONFIG_DEFAULTS})
        set(SDKCONFIG_DEFAULTS "pico-keys-sdk/config/esp32/sdkconfig.defaults")
    endif()
    set(EXTRA_COMPONENT_DIRS
        src/${PICOKEYS_COMPONENTS}
        pico-keys-sdk/config/esp32/components/mbedtls
        pico-keys-sdk/config/esp32/components/pico-keys-sdk
    )
    if(ENABLE_LIBCVC)
        list(APPEND EXTRA_COMPONENT_DIRS
            pico-keys-sdk/config/esp32/components/libcvc
        )
    endif()
    set(COMPONENTS ${PICOKEYS_COMPONENTS} pico-keys-sdk)
    if (USB_ITF_HID)
        list(APPEND EXTRA_COMPONENT_DIRS
            pico-keys-sdk/config/esp32/components/tinycbor
        )
        list(APPEND COMPONENTS tinycbor)
    endif()
    if(ENABLE_PQC)
        list(APPEND EXTRA_COMPONENT_DIRS
            pico-keys-sdk/config/esp32/components/mlkem512
            pico-keys-sdk/config/esp32/components/mlkem768
            pico-keys-sdk/config/esp32/components/mlkem1024
        )
    endif()
endif()
Enter fullscreen mode Exit fullscreen mode

This adjustment is necessary because the HSM component declares libcvc in REQUIRES, but the SDK importer does not include (at the time of writing) that component's directory in the list searched by ESP-IDF.

💡 An issue was opened for the problem above

I opened an issue asking whether the change above could be incorporated into the main project. If accepted, this manual step may soon become unnecessary.

Now let's disable CONFIG_ESP_COREDUMP_ENABLE_TO_UART. With this option enabled, someone could trigger a failure on the ESP32-S3, causing a memory dump to be sent over UART, a serial communication interface used to send and receive data. The problem is that this dump could contain sensitive information such as cryptographic keys, PINs, and other data stored in RAM.

This feature also applies to FIDO, but it isn't present in the main branch, which is why I didn't document how to disable it there.

This feature could be exploited in an Information Disclosure attack, allowing someone with UART access to obtain sensitive data from the device.

vim sdkconfig.defaults
Enter fullscreen mode Exit fullscreen mode

Change the line by commenting it out. This is how it looks before disabling it:

# Before disabling:
CONFIG_ESP_COREDUMP_ENABLE_TO_UART=y
Enter fullscreen mode Exit fullscreen mode

This is how it should look after disabling it:

# CONFIG_ESP_COREDUMP_ENABLE_TO_UART is not set
Enter fullscreen mode Exit fullscreen mode

Now let's compile Pico HSM:

cd ~/src/picokeys/esp-idf

# Load the ESP-IDF environment:
. ./export.sh

# Enter the Pico HSM project directory:
cd ~/src/picokeys/pico-hsm

# If you compiled FIDO before HSM, run:
idf.py fullclean

# Set ESP32-S3 as the target microcontroller for firmware compilation:
idf.py set-target esp32s3

# Compile the source code using the YubiHSM VID/PID:
idf.py -DVIDPID=YubiHSM build

# To compile using the PicoKeys VID/PID, run the command below:
idf.py build
Enter fullscreen mode Exit fullscreen mode

As with FIDO, compiling HSM generates several .bin files. flash_args specifies which files to flash and at which addresses. merge_bin combines these files into one, but this is optional; we can flash the board with idf.py flash. However, this command is necessary if you want a single .bin file for distribution or for flashing through another method, such as esptool.py.

mkdir -p release && cd build

# Merge the '.bin' files generated during compilation:
esptool.py --chip ESP32-S3 merge_bin -o ../release/pico_hsm_esp32-s3.bin @flash_args && cd ..
Enter fullscreen mode Exit fullscreen mode

Flashing the Firmware Without Secure Boot

Let's flash the firmware without Secure Boot. First, put the ESP32-S3 into Download Mode:

  • Hold BOOT, press and release RESET, then release BOOT.

Next, run this command to see which port the board appeared on:

$ ls -l /dev/ttyACM* /dev/ttyUSB*
/dev/ttyACM0
Enter fullscreen mode Exit fullscreen mode

In my case it was /dev/ttyACM0. I'll use this name in the command below to flash the firmware. If yours is different, use the port shown on your system, but make sure it belongs to the correct ESP32-S3 board you want to flash.

# For FIDO:
cd ~/src/picokeys/pico-fido && esptool.py --chip ESP32-S3 \
    -p /dev/ttyACM0 \
    write_flash 0x0 \
    release/pico_fido_esp32-s3.bin

# For HSM:
cd ~/src/picokeys/pico-hsm && esptool.py --chip ESP32-S3 \
    -p /dev/ttyACM0 \
    write_flash 0x0 \
    release/pico_hsm_esp32-s3.bin
Enter fullscreen mode Exit fullscreen mode

Compiling the Firmware with Secure Boot

The ESP32-S3 includes security features that help protect the device against unauthorized firmware changes. Secure Boot verifies the firmware's digital signature before executing it, allowing only images signed with an authorized key to boot. This prevents someone from replacing the firmware stored in flash memory, since the microcontroller will refuse to execute a new image if its signature is invalid.

Simply enabling Secure Boot v2 on the ESP32-S3 automatically applies some security protections, even without using Secure Lock. In my case, the two additional Secure Boot digest slots were revoked, the JTAG interface was permanently disabled, and the device switched to Secure Download Mode.

Revoking the additional digest slots prevents someone from registering new signing keys and then executing firmware that the board should not originally accept. This closes off one possible way to bypass Secure Boot protection.

With the JTAG interface permanently disabled, someone can no longer use that interface to debug the processor, inspect memory, or interfere with firmware execution. This makes attacks requiring physical access more difficult.

Secure Download Mode restricts the commands available in download mode, preventing operations such as directly reading eFuses and reducing opportunities to manipulate the device through that interface. This makes it harder to use download mode to circumvent configured protections.

All these protections were enabled automatically when I enabled Secure Boot. However, this behavior may change in the future, which is why I included the commits used at the beginning of this page.

⚠️ Attention

Enabling Secure Boot involves irreversible changes to the microcontroller. An incorrect configuration can permanently prevent the board from booting.

That's why we'll prepare the keys, configure firmware signing, and verify that everything works before dealing with eFuse programming.

Do I Need to Enable Flash Encryption?

In the code I analyzed, both Pico FIDO and Pico HSM use containers with authenticated encryption to protect sensitive objects such as private keys and credentials. This protects the data at the firmware level, but it does not mean the entire flash memory is encrypted.

The ESP32-S3's own Flash Encryption feature adds another layer: it encrypts the partitions configured for it, making it harder to read the firmware and other data by directly accessing the flash. This protection complements the container encryption, but does not prevent data access while firmware is running on the device.

In theory, the sensitive data stored by Pico FIDO and Pico HSM in these containers is encrypted before being written to flash. Therefore, directly reading the flash should not reveal that data without the key. Since the data is encrypted and the keys used to decrypt it are stored in a protected form in flash, a flash dump alone should not reveal the keys in plaintext.

Flash Encryption provides additional protection because it also encrypts the configured partitions. This makes analyzing flash contents more difficult for someone with direct access to the memory.

⚠️ Read before continuing

The Flash Encryption process must take place before enabling Secure Boot, because the key used to encrypt the flash must be programmed into the eFuses and made unreadable. When Secure Boot setup is completed, ESP-IDF protects the control that would otherwise allow this read protection to be applied later. Because eFuses are permanent, I need to program and protect the Flash Encryption key first and only then enable Secure Boot.

If you already have a key with Secure Boot configured, you cannot enable Flash Encryption on that board; you'll need to do it on a different board.

So if you want this additional protection, don't follow the procedure described here. Follow Part 3 of this article instead, which explains how to enable Flash Encryption and then activate Secure Boot.

Generating the Signing Keys

To use Secure Boot, we need to generate a cryptographic key pair:

  • Private key: used to sign the firmware during compilation or afterward. It must be stored somewhere secure, outside the ESP32-S3.
  • Public key: used to verify that the firmware signature is legitimate. The ESP32-S3 stores a cryptographic summary (digest) of this key in its eFuses.

The process is relatively simple: when the ESP32-S3 powers on, Secure Boot checks whether the firmware has a valid signature produced by a private key corresponding to one of the public keys authorized on the device. If verification fails, the firmware won't run.

Secure Boot V2, used by the ESP32-S3, works with RSA-3072 signatures. First, let's create a directory to store the keys:

# Create the directory
mkdir -p ~/.config/picokeys/signing

# Allow access only to the owner
chmod 700 ~/.config/picokeys/signing

# Restrict permissions on new files
umask 077
Enter fullscreen mode Exit fullscreen mode

Now let's use espsecure.py, a tool provided by ESP-IDF, to generate an RSA-3072 private key:

espsecure.py generate_signing_key --version 2 --scheme rsa3072 ~/.config/picokeys/signing/secure-boot.pem
Enter fullscreen mode Exit fullscreen mode

The command above creates the secure-boot.pem file containing the private key used to sign the firmware. The parameters are:

  • generate_signing_key: generates a new cryptographic key.
  • --version 2: specifies Secure Boot V2.
  • --scheme rsa3072: selects the RSA algorithm with a 3072-bit key.

If you plan to use Pico HSM on another board, we can generate a second key:

espsecure.py generate_signing_key --version 2 --scheme rsa3072 ~/.config/picokeys/signing/hsm-secure-boot.pem
Enter fullscreen mode Exit fullscreen mode

Finally, let's make sure only the owner can read or modify the files:

chmod 600 ~/.config/picokeys/signing/*-secure-boot.pem
Enter fullscreen mode Exit fullscreen mode

ℹ️ While studying this project, I wondered: "Can I use the same key for Pico FIDO and Pico HSM?"

The simple answer is yes. There is no need to generate a separate key for each firmware, since the key is tied to the microcontroller (ESP32-S3), not to the installed firmware.

This is because the public key digest is programmed into the ESP32-S3 eFuses. Therefore, as long as the firmware is signed with the corresponding private key, Secure Boot can execute it. This makes it possible, for example, to switch between Pico FIDO and Pico HSM using the same signing key.

However, there is an important detail: programming eFuses is permanent. Although the ESP32-S3 supports up to three signing keys, you cannot simply replace a key whose digest has already been programmed and protected. For that reason, it's important to decide which keys to use before enabling these protections.

I also recommend configuring at least two keys in the eFuses: one for each board and a backup key in case you lose the first one. However, this must be done before enabling Secure Boot. When Secure Boot V2 is activated during the first boot, any unconfigured signature slots are revoked. This behavior can be changed, but I don't recommend doing so.

If you plan to use Secure Lock, you should know that it changes the behavior described above. Even if you provision three keys in the eFuses beforehand, Secure Lock will use only one and revoke the other two.

If you lose the private key and have no other authorized key, you won't be able to sign new firmware versions. The device will continue working with the firmware already installed, but it may no longer be possible to update it. Depending on the configuration, this could make the board unusable for new projects.

If the private key is compromised, someone else could sign firmware that Secure Boot would consider legitimate. If you have no other authorized key to replace it, the recommendation is to use the board only for personal, local testing.

The recommendation is to use a different key for each board, avoiding reuse of the same private key across several devices. That way, losing or compromising one key affects only the corresponding board. It's also important to keep secure backups of your private keys, preferably offline. After all, losing a key can mean permanently losing the ability to update the device.

Associating the Key Digest with PicoKeys Firmware

Now that we have our private key, we need to make a small change to the PicoKeys source code to use our own signing key. This is necessary because the PicoKeys SDK has a hard-coded public key digest, meaning a digest defined directly in the source code. This simplifies enabling Secure Boot and Secure Lock through the PicoKey App, since the firmware has a known digest for provisioning and users don't have to generate and configure their own keys.

However, because we're compiling the firmware with our own private key, we need to replace that digest with the one corresponding to our public key. That way, we ensure Secure Boot uses the same signing key configured during firmware compilation.

The digest is a 32-byte cryptographic summary calculated from the public key. Think of it as a fingerprint of the public key: it lets Secure Boot identify which keys are authorized to sign firmware running on the microcontroller. The ESP32-S3 stores this digest in the eFuses.

Generating the Public Key

Let's use espsecure.py again, but this time to calculate the public key digest corresponding to the private key we generated earlier:

espsecure.py digest_sbv2_public_key \
  --keyfile ~/.config/picokeys/signing/secure-boot.pem \
  --output public-digest.bin
Enter fullscreen mode Exit fullscreen mode

The parameters are:

  • digest_sbv2_public_key: calculates the public key digest for Secure Boot V2.
  • --keyfile: specifies the file containing our private key. The tool extracts the public key information to calculate the digest.
  • --output: specifies the name of the file where the digest will be stored.

The command generates public-digest.bin, containing the 32 bytes we need to add to the PicoKeys source code.

Converting the Digest to the Format Used by PicoKeys

The generated file is binary, but the PicoKeys SDK stores the digest as a byte array in C source code. To make things easier, we can use a small Python script to convert those bytes into hexadecimal format:

python3 - public-digest.bin <<'PY'
from pathlib import Path
import sys

digest = Path(sys.argv[1]).read_bytes()

if len(digest) != 32:
    raise SystemExit(f"Esperados 32 bytes; recebidos {len(digest)}")

for start in range(0, 32, 8):
    print("    " + ", ".join(f"0x{b:02x}" for b in digest[start:start+8]) + ",")
PY
Enter fullscreen mode Exit fullscreen mode

The script reads the file, checks that it contains exactly 32 bytes, and prints the hexadecimal output in the format needed for the source code. The output looks like this:

    0x0c, 0x1e, 0xce, 0xf3, 0xb4, 0x8f, 0x4a, 0x81,
    0x45, 0x6c, 0x85, 0x39, 0x15, 0xcc, 0x05, 0x36,
    0xbe, 0x23, 0x24, 0xee, 0xac, 0x8e, 0x3b, 0xb5,
    0x77, 0x6f, 0x2d, 0xb9, 0x62, 0x38, 0x75, 0x6a
Enter fullscreen mode Exit fullscreen mode
Adding the Digest to PicoKeys FIDO and HSM Source Code

For both FIDO and HSM, edit the file below:

vim pico-keys-sdk/src/otp/otp_esp32.c
Enter fullscreen mode Exit fullscreen mode

In the correct file, find the following variable:

static const uint8_t esp_secure_boot_digest[32]
Enter fullscreen mode Exit fullscreen mode

It contains the 32 bytes of the digest originally used by PicoKeys. Now replace the existing values with the bytes we just generated, keeping the array structure:

static const uint8_t esp_secure_boot_digest[32] = {
    0x0c, 0x1e, 0xce, 0xf3, 0xb4, 0x8f, 0x4a, 0x81,
    0x45, 0x6c, 0x85, 0x39, 0x15, 0xcc, 0x05, 0x36,
    0xbe, 0x23, 0x24, 0xee, 0xac, 0x8e, 0x3b, 0xb5,
    0x77, 0x6f, 0x2d, 0xb9, 0x62, 0x38, 0x75, 0x6a
};
Enter fullscreen mode Exit fullscreen mode

If you have separate Pico FIDO and Pico HSM directories, you'll need to repeat this procedure in each one. Use the same digest if it's the same board, or a different one if you're using another board.

⚠️ Attention

This change only tells the PicoKeys SDK which digest to use during Secure Boot provisioning. It does not sign the firmware or automatically program the key into the eFuses.

We still need to configure ESP-IDF to sign the bootloader and application using the corresponding private key. Do not program the eFuses before confirming that all these components use the same signing key.

Once the eFuses are programmed and protected, you cannot simply replace the digest with another one. An incorrect configuration could permanently prevent new firmware from booting.

Configuring and Signing Secure Boot for FIDO and HSM

I recommend performing this procedure in a separate directory to avoid confusing firmware configured with your signing key with another version that doesn't have that configuration. Since we're dealing with Secure Boot and ESP32-S3 eFuses, a mistake could result in permanently programming an incorrect digest, preventing new firmware from booting.

⚠️ Attention

Don't forget to reapply any changes that aren't persistent, such as disabling CONFIG_ESP_COREDUMP_ENABLE_TO_UART, configuring PicoKeys not to show a pop-up when plugged in, and so on.

In the root directory of each project, run the command below:

# Clean the environment and start again:
idf.py fullclean
idf.py set-target esp32s3

# Now edit the important settings:
idf.py menuconfig
Enter fullscreen mode Exit fullscreen mode

Go to Security features and check Enable hardware Secure Boot in bootloader. A new menu will appear. There, select Select secure boot version (Enable Secure Boot version 2), and make sure you see (X) Enable Secure Boot version 2 (in my case everything was already correct, but I wanted to make this clear).

Still under Security features, select Secure boot private signing key (NEW). In my case, it was set to secure_boot_signing_key.pem. Replace that value with the path and filename of the private key we generated earlier. In my case, I configured it exactly as follows: /home/bruno/.config/picokeys/signing/secure-boot.pem.

Go back to the main screen by pressing backspace, select Partition Table, then Offset of partition table. Change the value from 0x8000 to 0x10000.

Press s to save and q to quit. To make sure everything is correct, we can check:

$ grep -iE 'CONFIG_SECURE_BOOT=y|CONFIG_PARTITION_TABLE_OFFSET|CONFIG_SECURE_BOOT_V2_ENABLED=y|CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES=y|CONFIG_SECURE_BOOT_SIGNING_KEY=' /home/bruno/src/picokeys/pico-fido/sdkconfig
CONFIG_SECURE_BOOT=y
CONFIG_SECURE_BOOT_V2_ENABLED=y
CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES=y
CONFIG_SECURE_BOOT_SIGNING_KEY="/home/bruno/.config/picokeys/signing/secure-boot.pem"
CONFIG_PARTITION_TABLE_OFFSET=0x10000
Enter fullscreen mode Exit fullscreen mode

⚠️ Secure Boot activation point

Enabling Secure Boot in the configuration and flashing/booting this image on the ESP32-S3 may cause the bootloader to program eFuses on the first boot. This programming is permanent. If you don't want to activate Secure Boot now, do not use or flash an image built with CONFIG_SECURE_BOOT=y onto your board. Only compile and test the lab configuration without Secure Boot.

Generating Signed FIDO and HSM Firmware

Now that we've laid the groundwork, let's build new firmware, but this time signed.

For FIDO:

cd ~/src/picokeys/pico-fido

# Compile the source code using the YubiKey5 VID/PID:
idf.py -DVIDPID=Yubikey5 build
Enter fullscreen mode Exit fullscreen mode

For HSM:

cd ~/src/picokeys/pico-hsm

# Compile the source code using the YubiKey5 VID/PID:
idf.py -DVIDPID=YubiHSM build
Enter fullscreen mode Exit fullscreen mode

Now, in the directory where we cloned the firmware (either FIDO or HSM), let's validate the bootloader and firmware:


#### FIDO

# Validate the bootloader signature:
$ idf.py secure-verify-signature --keyfile /home/bruno/.config/picokeys/signing/secure-boot.pem bootloader/bootloader.bin
Executing action: secure-verify-signature
Running espsecure in directory /home/bruno/src/picokeys/pico-fido/build
Executing "/home/bruno/.espressif/python_env/idf5.5_py3.14_env/bin/python -m espsecure verify_signature --version 2 --keyfile /home/bruno/.config/picokeys/signing/secure-boot.pem bootloader/bootloader.bin"...
espsecure.py v4.12.0
Signature block 0 is valid (RSA).
Signature block 0 verification successful using the supplied key (RSA).
Signature block 1 invalid. Skipping.
Signature block 2 invalid. Skipping.

# Validate the firmware signature:
$ idf.py secure-verify-signature --keyfile /home/bruno/.config/picokeys/signing/secure-boot.pem pico_fido.bin
Executing action: secure-verify-signature
Running espsecure in directory /home/bruno/src/picokeys/pico-fido/build
Executing "/home/bruno/.espressif/python_env/idf5.5_py3.14_env/bin/python -m espsecure verify_signature --version 2 --keyfile /home/bruno/.config/picokeys/signing/secure-boot.pem pico_fido.bin"...
espsecure.py v4.12.0
Signature block 0 is valid (RSA).
Signature block 0 verification successful using the supplied key (RSA).
Signature block 1 invalid. Skipping.
Signature block 2 invalid. Skipping.


#### HSM

# Validate the bootloader signature:
$ idf.py secure-verify-signature --keyfile /home/bruno/.config/picokeys/signing/secure-boot.pem bootloader/bootloader.bin
Executing action: secure-verify-signature
Running espsecure in directory /home/bruno/src/picokeys/pico-hsm/build
Executing "/home/bruno/.espressif/python_env/idf5.5_py3.12_env/bin/python -m espsecure verify_signature --version 2 --keyfile /home/bruno/.config/picokeys/signing/secure-boot.pem bootloader/bootloader.bin"...
espsecure.py v4.12.0
Signature block 0 is valid (RSA).
Signature block 0 verification successful using the supplied key (RSA).
Signature block 1 invalid. Skipping.
Signature block 2 invalid. Skipping.

# Validate the firmware signature:
$ idf.py secure-verify-signature --keyfile /home/bruno/.config/picokeys/signing/secure-boot.pem pico_hsm.bin
Executing action: secure-verify-signature
Running espsecure in directory /home/bruno/src/picokeys/pico-hsm/build
Executing "/home/bruno/.espressif/python_env/idf5.5_py3.12_env/bin/python -m espsecure verify_signature --version 2 --keyfile /home/bruno/.config/picokeys/signing/secure-boot.pem pico_hsm.bin"...
espsecure.py v4.12.0
Signature block 0 is valid (RSA).
Signature block 0 verification successful using the supplied key (RSA).
Signature block 1 invalid. Skipping.
Signature block 2 invalid. Skipping.
Enter fullscreen mode Exit fullscreen mode

Flashing the Secure Boot Image for FIDO and HSM

⚠️ This process is irreversible

The process of flashing signed firmware is irreversible.

Let's generate the merged file to flash afterward:

mkdir -p release && cd build

# Merge the '.bin' files generated during FIDO compilation:
esptool.py --chip ESP32-S3 merge_bin -o ../release/pico_fido_esp32-s3.bin \
  --flash_mode dio --flash_freq 80m --flash_size keep \
  0x0 bootloader/bootloader.bin \
  0x10000 partition_table/partition-table.bin \
  0x20000 pico_fido.bin


# Merge the '.bin' files generated during HSM compilation:
esptool.py --chip ESP32-S3 merge_bin -o ../release/pico_hsm_esp32-s3.bin \
  --flash_mode dio --flash_freq 80m --flash_size keep \
  0x0 bootloader/bootloader.bin \
  0x10000 partition_table/partition-table.bin \
  0x20000 pico_hsm.bin
Enter fullscreen mode Exit fullscreen mode

Compilation generates separate files for the bootloader, partition table, and firmware. The merge_bin command above combines them into a single image. However, we need to specify the flash address where each file will be written, since .bin files don't contain that position themselves. The partition table offset was increased from 0x8000 to 0x10000 because the build showed that the bootloader was 0x9060 bytes, larger than the original 0x8000 space reserved for it.

Once the image is ready, we can flash it onto the board:

# For FIDO:
esptool.py --chip ESP32-S3 \
    -p /dev/ttyACM0 \
    write_flash 0x0 \
    release/pico_fido_esp32-s3.bin

# For HSM:
esptool.py --chip ESP32-S3 \
    -p /dev/ttyACM0 \
    write_flash 0x0 \
    release/pico_hsm_esp32-s3.bin
Enter fullscreen mode Exit fullscreen mode

Disconnect and reconnect the board, enter Download Mode, then run the command below:

$ esptool.py --chip ESP32-S3 --port /dev/ttyACM0 --no-stub get_security_info
esptool.py v4.12.0
Serial port /dev/ttyACM0
Connecting...
Chip is ESP32-S3 in Secure Download Mode
Enabling default SPI flash mode...

Security Information:
=====================
Flags: 0x000000f5 (0b11110101)
Key Purposes: (9, 0, 0, 0, 0, 0, 12)
  BLOCK_KEY0 - SECURE_BOOT_DIGEST0
  BLOCK_KEY1 - USER/EMPTY
  BLOCK_KEY2 - USER/EMPTY
  BLOCK_KEY3 - USER/EMPTY
  BLOCK_KEY4 - USER/EMPTY
  BLOCK_KEY5 - USER/EMPTY
Chip ID: 9
API Version: 0
Secure Boot: Enabled
Secure Boot Key Revocation Status:

    Secure Boot Key1 is Revoked

    Secure Boot Key2 is Revoked

Flash Encryption: Disabled
SPI Boot Crypt Count (SPI_BOOT_CRYPT_CNT): 0x0
JTAG: Permanently Disabled
Hard resetting via RTS pin...
Enter fullscreen mode Exit fullscreen mode

The BLOCK_KEY0 - SECURE_BOOT_DIGEST0 line shows that the digest of the public key used by Secure Boot was programmed into eFuse block BLOCK_KEY0. We can also see Secure Boot Key1 is Revoked and Secure Boot Key2 is Revoked, indicating that slots 1 and 2 have been revoked and can no longer be used to validate signatures. Therefore, only the key associated with SECURE_BOOT_DIGEST0 remains authorized.

↑ Back to the Table of Contents

Part 3 — Secure Boot and Flash Encryption

Compiling the Firmware with Secure Boot and Flash Encryption

I'm not going to spend time explaining Flash Encryption again because I've already covered it. If you want to understand it, read the earlier section "Do I Need to Enable Flash Encryption?".

In this procedure, the idea is to generate the Flash Encryption key on the computer, keep a secure backup of it, and encrypt the binaries before flashing them using ROM Download Mode. For this to work, the same key must also be stored and protected in an ESP32-S3 eFuse block. The hardware uses this key to decrypt the contents during execution.

As in Part 2, I'll use ESP-IDF 5.5.1, which is used by the PicoKeys project. The projects were compiled using the following commits:

### FIDO
Hash: ade770281dd4fd8c627add34ed078f56ec40a07b
tag: nightly-main and nightly-development

### HSM
Hash: 5d6a32c81b2367fb83e5320400f61217d2bd0d2f 
tag: nightly-master and nightly-development
Enter fullscreen mode Exit fullscreen mode

⚠️ Attention

Programming eFuses is permanent and irreversible. Incorrect configuration may prevent the board from booting, make future updates impossible, or render it unusable.

I am not responsible for any damage, failures, or losses resulting from following the procedures described in this guide, including problems caused by compiling, configuring, or flashing the firmware.

Before performing any procedure, I recommend reading the official documentation from the manufacturers and projects involved. Each user is responsible for understanding the risks and checking that these instructions are compatible with their device.

Cloning the Projects and Installing ESP-IDF 5.5.1

On Ubuntu, install the dependencies specified by ESP-IDF. Then clone the repositories:

# Create a new directory for this project:
mkdir -p ~/src/picokeys-fe && cd ~/src/picokeys-fe

# Clone ESP-IDF version 5.5.1:
git clone --branch v5.5.1 --recursive https://github.com/espressif/esp-idf.git esp-idf

# Clone Pico FIDO:
git clone --recursive https://github.com/polhenarejos/pico-fido.git

# Clone Pico HSM:
git clone --recursive https://github.com/polhenarejos/pico-hsm.git
Enter fullscreen mode Exit fullscreen mode

Now install ESP-IDF and load the environment:

# Enter the ESP-IDF project directory:
cd esp-idf

# Install ESP-IDF for ESP32-S3:
unset IDF_PYTHON_ENV_PATH ; ./install.sh esp32s3

# Load the ESP-IDF environment:
. ./export.sh

# Check the version:
$ idf.py --version
ESP-IDF v5.5.1

# Return to the base directory:
cd ~/src/picokeys-fe
Enter fullscreen mode Exit fullscreen mode

Required Adjustments for HSM and FIDO

As described in the other procedures, we need to make the necessary adjustments to both FIDO and HSM to fix the issues related to key security, along with any other required changes.

I won't show the steps again here to avoid repeating the procedure already covered in Part 2.

Generating the Keys

To use Secure Boot V2, we need an RSA-3072 private key. It will be used to sign the bootloader and application. The ESP32-S3 stores the digest of the corresponding public key in its eFuses.

The Flash Encryption key is a separate key. It uses AES-XTS and will be used to encrypt the binaries on the computer. This key must also be provisioned on the ESP32-S3 in an eFuse block with the purpose XTS_AES_128_KEY.

Let's start by creating the directories and restricting permissions:

mkdir -p ~/.config/picokeys/signing
mkdir -p ~/.config/picokeys/flash-encryption
chmod 700 ~/.config/picokeys/signing ~/.config/picokeys/flash-encryption
umask 077
Enter fullscreen mode Exit fullscreen mode

Now generate the signing key (Secure Boot):

espsecure.py generate_signing_key --version 2 --scheme rsa3072 \
  ~/.config/picokeys/signing/secure-boot.pem

chmod 600 ~/.config/picokeys/signing/secure-boot.pem
Enter fullscreen mode Exit fullscreen mode

Let's get the board's MAC address. Here, the MAC will only serve as a name—an identifier that could be anything. I'll use it to make it easier to identify which board the key belongs to. Enable Download Mode (hold BOOT, press and release RESET, then release BOOT).

Next, run the command below to see which port the board appeared on:

$ ls -l /dev/ttyACM* /dev/ttyUSB*
/dev/ttyACM0
Enter fullscreen mode Exit fullscreen mode

In my case it was /dev/ttyACM0. I'll use this name in the command below to flash the firmware. If yours is different, use the port shown on your system, but make sure it belongs to the correct ESP32-S3 board you want to flash. Now let's check the board's MAC address:

$ esptool.py --chip esp32s3 --port /dev/ttyACM0 read_mac
esptool.py v4.12.0
Serial port /dev/ttyACM0
Connecting...
Chip is ESP32-S3 (QFN56) (revision v0.2)
Features: WiFi, BLE, Embedded PSRAM 8MB (AP_3v3)
Crystal is 40MHz
USB mode: USB-Serial/JTAG
MAC: 68:ee:8f:51:e0:38
Uploading stub...
Running stub...
Stub running...
MAC: 68:ee:8f:51:e0:38
Hard resetting via RTS pin...
Enter fullscreen mode Exit fullscreen mode

Now that we have the MAC, we'll use it as the ID (68ee8f51e038). The command below generates a 256-bit key (used for Flash Encryption):

espsecure.py generate_flash_encryption_key \
  ~/.config/picokeys/flash-encryption/pico-68ee8f51e038.bin

chmod 600 ~/.config/picokeys/flash-encryption/pico-68ee8f51e038.bin
Enter fullscreen mode Exit fullscreen mode

Replace 68ee8f51e038 with your own identifier. Use a different Flash Encryption key for each device and keep a protected backup. If you lose this key, you will no longer be able to use this board.

To store this file somewhere secure, such as a password manager, we can do the following:

xxd -p -c 256 ~/.config/picokeys/flash-encryption/pico-68ee8f51e038.bin
Enter fullscreen mode Exit fullscreen mode

You will see a 256-bit hash:

11c860d531eec46bb9b29319a798da103e7c121c7dcbe9aa2b251a5a855daae6
Enter fullscreen mode Exit fullscreen mode

Save this value somewhere secure. To restore it to a binary file, do the following:

xxd -r -p chave.hex pico-68ee8f51e038-restaurado.bin
Enter fullscreen mode Exit fullscreen mode

The chave.hex file must contain the 256-bit hash.

Associating the Key Digest with PicoKeys Firmware

Now let's calculate the public key digest to validate the firmware signature:

espsecure.py digest_sbv2_public_key \
  --keyfile ~/.config/picokeys/signing/secure-boot.pem \
  --output ~/.config/picokeys/signing/public-digest.bin
Enter fullscreen mode Exit fullscreen mode

The command above generates a 32-byte binary file. Next, we need to convert its contents to hexadecimal format:

python3 - ~/.config/picokeys/signing/public-digest.bin <<'PY'
from pathlib import Path
import sys

digest = Path(sys.argv[1]).read_bytes()
if len(digest) != 32:
    raise SystemExit(f"Esperados 32 bytes; recebidos {len(digest)}")

for start in range(0, 32, 8):
    print("    " + ", ".join(f"0x{b:02x}" for b in digest[start:start+8]) + ",")
PY
Enter fullscreen mode Exit fullscreen mode

For both FIDO and HSM, edit the file:

vim pico-keys-sdk/src/otp/otp_esp32.c
Enter fullscreen mode Exit fullscreen mode

Find esp_secure_boot_digest[32] and replace the 32 bytes with the output of the previous command, as demonstrated in the earlier tutorial. This tells the SDK which digest to use when validating the firmware signature. It does not sign the firmware or write anything to the eFuses.

Configuring Secure Boot and Flash Encryption

In the root directory of each project, clean the build and configure the ESP32-S3 target:

idf.py fullclean
idf.py set-target esp32s3
idf.py menuconfig
Enter fullscreen mode Exit fullscreen mode

Under Security features, configure:

  • Enable hardware Secure Boot in bootloader;
  • Select secure boot version (Enable Secure Boot version 2);
  • Sign binaries during build;
  • Secure boot private signing key: path to the RSA private key corresponding to the digest accepted by the board;
  • Enable flash encryption on boot;
  • Enable usage mode: Release;
  • UART ROM download mode: (Permanently switch to Secure mode (recommended)).

Under Partition Table, change the partition table offset from 0x8000 to 0x10000. Save with s and quit with q. Then verify that the options are correct:

$ grep -E \
  -e '^CONFIG_SECURE_BOOT=' \
  -e '^CONFIG_SECURE_BOOT_V2_ENABLED=' \
  -e '^CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES=' \
  -e '^CONFIG_SECURE_BOOT_SIGNING_KEY=' \
  -e '^CONFIG_SECURE_FLASH_ENC_ENABLED=' \
  -e '^CONFIG_SECURE_FLASH_ENCRYPTION_MODE_RELEASE=' \
  -e '^CONFIG_SECURE_FLASH_ENCRYPTION_AES128=' \
  -e '^CONFIG_SECURE_ENABLE_SECURE_ROM_DL_MODE=' \
  -e '^CONFIG_PARTITION_TABLE_OFFSET=' \
  sdkconfig
CONFIG_SECURE_BOOT=y
CONFIG_SECURE_BOOT_V2_ENABLED=y
CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES=y
CONFIG_SECURE_BOOT_SIGNING_KEY="/home/bruno/.config/picokeys/signing/secure-boot.pem"
CONFIG_SECURE_FLASH_ENC_ENABLED=y
CONFIG_SECURE_FLASH_ENCRYPTION_AES128=y
CONFIG_SECURE_FLASH_ENCRYPTION_MODE_RELEASE=y
CONFIG_SECURE_ENABLE_SECURE_ROM_DL_MODE=y
CONFIG_PARTITION_TABLE_OFFSET=0x10000
Enter fullscreen mode Exit fullscreen mode

Edit the following file, whether you're using FIDO or HSM:

vim pico-keys-sdk/config/esp32/partitions.csv
Enter fullscreen mode Exit fullscreen mode

The default table already uses factory at 0x20000 and part0 at 0x200000, with a size of 1M. We don't need to change those values. The important part is to add the encrypted flag at the end of the part0 line, so it looks like this:

part0,    0x40,  0x1,     0x200000, 1M, encrypted
Enter fullscreen mode Exit fullscreen mode

ESP-IDF encrypts firmware partitions. The part0 partition, however, is a data partition, so we need to add the encrypted flag to have it encrypted as well.

⚠️ Check the keys before flashing

This procedure assumes a fresh ESP32-S3 board with Secure Boot and Flash Encryption disabled. Before flashing the firmware, confirm that it was signed using the private key you generated and that the Flash Encryption key used to encrypt the files is the same one that will be provisioned on the board.

On the first boot, the firmware may program and protect eFuses to enable these features. This change is permanent. Keep a protected backup of the keys and validate the build and images on the computer before booting the board with this firmware.

Generating and Validating the Signed FIDO and HSM Firmware

Let's load the ESP-IDF 5.5.1 environment again (just to be safe) and compile Pico FIDO:

. ~/src/picokeys-fe/esp-idf/export.sh && export IDF_TARGET=esp32s3

cd ~/src/picokeys-fe/pico-fido

idf.py -DIDF_TARGET=esp32s3 -DVIDPID=Yubikey5 build
Enter fullscreen mode Exit fullscreen mode

For Pico HSM:

. ~/src/picokeys-fe/esp-idf/export.sh && export IDF_TARGET=esp32s3

cd ~/src/picokeys-fe/pico-hsm

idf.py -DIDF_TARGET=esp32s3 -DVIDPID=YubiHSM build
Enter fullscreen mode Exit fullscreen mode

After building, we need to validate the bootloader and application signatures, as in the previous procedure. For FIDO:

cd ~/src/picokeys-fe/pico-fido

$ idf.py secure-verify-signature \
  --version 2 \
  --keyfile ~/.config/picokeys/signing/secure-boot.pem \
  bootloader/bootloader.bin
Executing action: secure-verify-signature
Running espsecure in directory /home/bruno/src/picokeys-fe/pico-fido/build
Executing "/home/bruno/.espressif/python_env/idf5.5_py3.12_env/bin/python -m espsecure verify_signature --version 2 --keyfile /home/bruno/.config/picokeys/signing/secure-boot.pem bootloader/bootloader.bin"...
espsecure.py v4.12.0
Signature block 0 is valid (RSA).
Signature block 0 verification successful using the supplied key (RSA).
Signature block 1 invalid. Skipping.
Signature block 2 invalid. Skipping.


$ idf.py secure-verify-signature \
  --version 2 \
  --keyfile ~/.config/picokeys/signing/secure-boot.pem \
  pico_fido.bin
Executing action: secure-verify-signature
Running espsecure in directory /home/bruno/src/picokeys-fe/pico-fido/build
Executing "/home/bruno/.espressif/python_env/idf5.5_py3.12_env/bin/python -m espsecure verify_signature --version 2 --keyfile /home/bruno/.config/picokeys/signing/secure-boot.pem pico_fido.bin"...
espsecure.py v4.12.0
Signature block 0 is valid (RSA).
Signature block 0 verification successful using the supplied key (RSA).
Signature block 1 invalid. Skipping.
Signature block 2 invalid. Skipping.
Enter fullscreen mode Exit fullscreen mode

For HSM:

cd ~/src/picokeys-fe/pico-hsm

idf.py secure-verify-signature \
  --version 2 \
  --keyfile ~/.config/picokeys/signing/secure-boot.pem \
  bootloader/bootloader.bin
Executing action: secure-verify-signature
Running espsecure in directory /home/bruno/src/picokeys-fe/pico-hsm/build
Executing "/home/bruno/.espressif/python_env/idf5.5_py3.12_env/bin/python -m espsecure verify_signature --version 2 --keyfile /home/bruno/.config/picokeys/signing/secure-boot.pem bootloader/bootloader.bin"...
espsecure.py v4.12.0
Signature block 0 is valid (RSA).
Signature block 0 verification successful using the supplied key (RSA).
Signature block 1 invalid. Skipping.
Signature block 2 invalid. Skipping.


idf.py secure-verify-signature \
  --version 2 \
  --keyfile ~/.config/picokeys/signing/secure-boot.pem \
  pico_hsm.bin
Executing action: secure-verify-signature
Running espsecure in directory /home/bruno/src/picokeys-fe/pico-hsm/build
Executing "/home/bruno/.espressif/python_env/idf5.5_py3.12_env/bin/python -m espsecure verify_signature --version 2 --keyfile /home/bruno/.config/picokeys/signing/secure-boot.pem pico_hsm.bin"...
espsecure.py v4.12.0
Signature block 0 is valid (RSA).
Signature block 0 verification successful using the supplied key (RSA).
Signature block 1 invalid. Skipping.
Signature block 2 invalid. Skipping.
Enter fullscreen mode Exit fullscreen mode

For each command, signature block 0 must be valid. Blocks 1 and 2 may show as invalid if they were not used. Do not flash firmware whose signature has not been validated.

Preparing the Board and FE Key — First Firmware Only

Connect only the board you intend to use (this board must not already have Secure Boot enabled). Then put the board into Download Mode (hold BOOT, press and release RESET, then release BOOT).

Next, run the command below to see which port it appeared on:

$ ls -l /dev/ttyACM* /dev/ttyUSB*
/dev/ttyACM0
Enter fullscreen mode Exit fullscreen mode

In my case it was /dev/ttyACM0. I'll use this name in the command below to flash the firmware. If yours is different, use the port shown on your system, but make sure it belongs to the correct ESP32-S3 board you want to flash.

Now we need to confirm a few things. Start by running the commands below:

$ espefuse.py --chip esp32s3 --port "/dev/ttyACM0" summary | grep -E 'ENABLE_SECURITY_DOWNLOAD|DIS_DOWNLOAD_MODE'
DIS_DOWNLOAD_MODE (BLOCK0)                         Set this bit to disable download mode (boot_mode[3 = False R/W (0b0)
ENABLE_SECURITY_DOWNLOAD (BLOCK0)                  Set this bit to enable secure UART download mode   = False R/W (0b0)

$ esptool.py --chip esp32s3 --port "/dev/ttyACM0" get_security_info | grep -iE 'Secure Boot|Flash Encryption|SPI_BOOT_CRYPT_CNT|BLOCK_KEY1'
  BLOCK_KEY1 - USER/EMPTY
Secure Boot: Disabled
Flash Encryption: Disabled
SPI Boot Crypt Count (SPI_BOOT_CRYPT_CNT): 0x0
Enter fullscreen mode Exit fullscreen mode

These commands confirm several details, including:

  • Secure Boot is disabled.
  • Flash Encryption is disabled and SPI_BOOT_CRYPT_CNT is 0.
  • Secure Download Mode is disabled.
  • The block intended for Flash Encryption is empty and available.

In the procedure below, BLOCK_KEY1 is reserved for Flash Encryption and BLOCK_KEY0 for the Secure Boot digest. If the commands above do not confirm that BLOCK_KEY1 is free, stop. Do not substitute another block without reviewing the key allocation.

As required by the official documentation, we now need to erase the flash (I don't see why this would be necessary, but let's follow the instructions exactly) and confirm that the AES-XTS key is 32 bytes long:

$ esptool.py --chip esp32s3 --port "/dev/ttyACM0" erase_flash
esptool.py v4.12.0
Serial port /dev/ttyACM0
Connecting...
Chip is ESP32-S3 (QFN56) (revision v0.2)
Features: WiFi, BLE, Embedded PSRAM 8MB (AP_3v3)
Crystal is 40MHz
USB mode: USB-Serial/JTAG
MAC: 68:ee:8f:51:e0:38
Uploading stub...
Running stub...
Stub running...
Erasing flash (this may take a while)...
Chip erase completed successfully in 4.7 seconds.
Hard resetting via RTS pin...
Enter fullscreen mode Exit fullscreen mode

Now we'll write the Flash Encryption key into eFuse block BLOCK_KEY1:

$ espefuse.py --chip esp32s3 \
  --port "/dev/ttyACM0" \
  burn_key BLOCK_KEY1 "/home/bruno/.config/picokeys/flash-encryption/pico-68ee8f51e038.bin" XTS_AES_128_KEY
espefuse.py v4.12.0
Connecting...

=== Run "burn_key" command ===
Sensitive data will be hidden (see --show-sensitive-info)
Burn keys to blocks:
 - BLOCK_KEY1 -> [?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??]
    Reversing byte order for AES-XTS hardware peripheral
    'KEY_PURPOSE_1': 'USER' -> 'XTS_AES_128_KEY'.
    Disabling write to 'KEY_PURPOSE_1'.
    Disabling read to key block
    Disabling write to key block


Check all blocks for burn...
idx, BLOCK_NAME,          Conclusion
[00] BLOCK0               is not empty
    (written ): 0x0000000000000100000000000000d1f50000000000000000
    (to write): 0x000000000000000000000000400000000000000201000200
    (coding scheme = NONE)
[05] BLOCK_KEY1           is empty, will burn the new value
. 
This is an irreversible operation!
Type 'BURN' (all capitals) to continue.
BURN
BURN BLOCK5  - OK (write block == read block)
BURN BLOCK0  - OK (all write block bits are set)
Reading updated efuses...
Successful
Enter fullscreen mode Exit fullscreen mode

By default, burn_key protects the key against both reading and writing.

The procedure will ask you to type BURN to continue. Type it and press Enter.

Now let's check get_security_info again. BLOCK_KEY1 should have the XTS_AES_128_KEY purpose and be protected against reading and writing.

$ esptool.py --chip esp32s3 --port "/dev/ttyACM0" get_security_info | grep -iE 'Secure Boot|Flash Encryption|SPI_BOOT_CRYPT_CNT|BLOCK_KEY1'
  BLOCK_KEY1 - XTS_AES_128_KEY
Secure Boot: Disabled
Flash Encryption: Disabled
SPI Boot Crypt Count (SPI_BOOT_CRYPT_CNT): 0x0
Enter fullscreen mode Exit fullscreen mode

Flashing the First Firmware onto the Board

Let's merge the files into a single binary:

### FIDO:

cd ~/src/picokeys-fe/pico-fido && mkdir -p release && cd build

esptool.py --chip ESP32-S3 merge_bin \
  -o ../release/pico_fido_esp32-s3.bin \
  --flash_mode dio --flash_freq 80m --flash_size keep \
  0x0 bootloader/bootloader.bin \
  0x10000 partition_table/partition-table.bin \
  0x20000 pico_fido.bin && cd ..

### HSM:

cd ~/src/picokeys-fe/pico-hsm && mkdir -p release && cd build

esptool.py --chip ESP32-S3 merge_bin \
  -o ../release/pico_hsm_esp32-s3.bin \
  --flash_mode dio --flash_freq 80m --flash_size keep \
  0x0 bootloader/bootloader.bin \
  0x10000 partition_table/partition-table.bin \
  0x20000 pico_hsm.bin && cd ..
Enter fullscreen mode Exit fullscreen mode

In the layout above, the addresses used are:

File Address
Bootloader 0x0
Partition table 0x10000
Application/Firmware (factory) 0x20000
Data (part0), if it contains anything 0x200000

Now let's flash the board:

# FIDO:
esptool.py --chip ESP32-S3 -p "/dev/ttyACM0" write_flash \
  --flash_mode dio --flash_freq 80m --flash_size keep \
  0x0 release/pico_fido_esp32-s3.bin


# HSM:
esptool.py --chip ESP32-S3 -p "/dev/ttyACM0" write_flash \
  --flash_mode dio --flash_freq 80m --flash_size keep \
  0x0 release/pico_hsm_esp32-s3.bin
Enter fullscreen mode Exit fullscreen mode

💡 write_flash vs encrypt_flash_data

write_flash is used with the signed binaries when Flash Encryption is not yet active on the board. Since we've configured Flash Encryption on boot and already provisioned the key in the eFuse, the bootloader will encrypt the firmware during its first boot on the board.

For subsequent flashes, after Flash Encryption is active, we'll use encrypt_flash_data to encrypt the new firmware on the computer using the board's Flash Encryption key. Then we'll flash the encrypted file using write_flash, at the same address used for encryption.

Disconnect and reconnect the board, enter Download Mode, then run the command below:

$ esptool.py --chip ESP32-S3 --port /dev/ttyACM0 --no-stub get_security_info
esptool.py v4.12.0
Serial port /dev/ttyACM0
Connecting...
Chip is ESP32-S3 in Secure Download Mode
Enabling default SPI flash mode...

Security Information:
=====================
Flags: 0x000006f5 (0b11011110101)
Key Purposes: (9, 4, 0, 0, 0, 0, 12)
  BLOCK_KEY0 - SECURE_BOOT_DIGEST0
  BLOCK_KEY1 - XTS_AES_128_KEY
  BLOCK_KEY2 - USER/EMPTY
  BLOCK_KEY3 - USER/EMPTY
  BLOCK_KEY4 - USER/EMPTY
  BLOCK_KEY5 - USER/EMPTY
Chip ID: 9
API Version: 0
Secure Boot: Enabled
Secure Boot Key Revocation Status:

    Secure Boot Key1 is Revoked

    Secure Boot Key2 is Revoked

Flash Encryption: Enabled
SPI Boot Crypt Count (SPI_BOOT_CRYPT_CNT): 0x7
Dcache in UART download mode: Disabled
Icache in UART download mode: Disabled
JTAG: Permanently Disabled
Hard resetting via RTS pin...
Enter fullscreen mode Exit fullscreen mode

The BLOCK_KEY0 - SECURE_BOOT_DIGEST0 line shows that the Secure Boot public key digest was programmed into BLOCK_KEY0. The messages Secure Boot Key1 is Revoked and Secure Boot Key2 is Revoked indicate that slots 1 and 2 have been revoked. Therefore, slot 0 remains authorized to validate signatures.

We can also see that Flash Encryption is active. The SPI_BOOT_CRYPT_CNT: 0x7 value confirms that Flash Encryption was enabled, and BLOCK_KEY1 - XTS_AES_128_KEY shows that the FE key is stored in the expected block. The board is also in Secure Download Mode. In this mode, downloading is limited to basic flashing commands. Data and instruction caches are disabled during download, and JTAG has been permanently disabled.

Flashing New Encrypted Firmware — Second or Later Flash

This procedure is for the second or subsequent flash, after the first firmware has already enabled Flash Encryption. After compiling the firmware, we'll merge the binaries into a single file:

### FIDO:

cd ~/src/picokeys-fe/pico-fido && mkdir -p release && cd build

esptool.py --chip ESP32-S3 merge_bin \
  -o ../release/pico_fido_esp32-s3.bin \
  --flash_mode dio --flash_freq 80m --flash_size keep \
  0x0 bootloader/bootloader.bin \
  0x10000 partition_table/partition-table.bin \
  0x20000 pico_fido.bin && cd ..


### HSM:

cd ~/src/picokeys-fe/pico-hsm && mkdir -p release && cd build

esptool.py --chip ESP32-S3 merge_bin \
  -o ../release/pico_hsm_esp32-s3.bin \
  --flash_mode dio --flash_freq 80m --flash_size keep \
  0x0 bootloader/bootloader.bin \
  0x10000 partition_table/partition-table.bin \
  0x20000 pico_hsm.bin && cd ..
Enter fullscreen mode Exit fullscreen mode

Now let's encrypt the firmware:

# FIDO:
espsecure.py encrypt_flash_data --aes_xts \
  --keyfile "/home/bruno/.config/picokeys/flash-encryption/pico-68ee8f51e038.bin" \
  --address 0x0 \
  --output release/pico_fido_esp32-s3.enc.bin \
  release/pico_fido_esp32-s3.bin

# HSM:
espsecure.py encrypt_flash_data --aes_xts \
  --keyfile "/home/bruno/.config/picokeys/flash-encryption/pico-68ee8f51e038.bin" \
  --address 0x0 \
  --output release/pico_hsm_esp32-s3.enc.bin \
  release/pico_hsm_esp32-s3.bin
Enter fullscreen mode Exit fullscreen mode

Before flashing, let's run a test to validate that encryption worked:

espsecure.py decrypt_flash_data --aes_xts \
  --keyfile "/home/bruno/.config/picokeys/flash-encryption/pico-68ee8f51e038.bin" \
  --address 0x0 \
  --output /tmp/pico_hsm.dec.bin \
  release/pico_hsm_esp32-s3.enc.bin

# Compare the original firmware (encrypted) with the newly decrypted firmware (if there is no output, everything matches):
cmp release/pico_hsm_esp32-s3.bin /tmp/pico_hsm.dec.bin
Enter fullscreen mode Exit fullscreen mode

To flash, we need to use the --force option. Without it, the following happens:

$ esptool.py --chip ESP32-S3 -p "/dev/ttyACM0" --no-stub write_flash \
  --flash_mode dio --flash_freq 80m --flash_size keep \
  0x0 release/pico_hsm_esp32-s3.enc.bin
esptool.py v4.12.0
Serial port /dev/ttyACM0
Connecting...
Chip is ESP32-S3 in Secure Download Mode
Enabling default SPI flash mode...
Configuring flash size...
WARNING: In case of failure, please set a specific --flash_size.

A fatal error occurred: WARNING: Detected flash encryption and secure download mode enabled.
Flashing plaintext binary may brick your device! Use --force to override the warning.
Enter fullscreen mode Exit fullscreen mode

This warning is expected from esptool because it detected a board with Flash Encryption and Secure Download Mode enabled. When these features are active, it cannot verify whether the file has been encrypted correctly, which is why we need --force.

Since we've validated the encryption, we can proceed:

# FIDO:
esptool.py --chip ESP32-S3 -p "/dev/ttyACM0" --no-stub write_flash --force \
  --flash_mode dio --flash_freq 80m --flash_size keep \
  0x0 release/pico_fido_esp32-s3.enc.bin


# HSM:
esptool.py --chip ESP32-S3 -p "/dev/ttyACM0" --no-stub write_flash --force \
  --flash_mode dio --flash_freq 80m --flash_size keep \
  0x0 release/pico_hsm_esp32-s3.enc.bin
Enter fullscreen mode Exit fullscreen mode

Sources

https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-reference/peripherals/hmac.html

https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/security/secure-boot-v2.html

https://docs.espressif.com/projects/esp-idf/en/v5.2/esp32/api-reference/kconfig.html

https://docs.espressif.com/projects/esp-idf/en/v5.2/esp32s3/security/flash-encryption.html

https://docs.espressif.com/projects/esp-idf/en/v5.5/esp32s3/security/flash-encryption.html

https://docs.espressif.com/projects/esp-idf/en/v5.5/esp32s3/security/secure-boot-v2.html

https://docs.espressif.com/projects/esp-idf/en/v5.5/esp32s3/security/security-features-enablement-workflows.html

https://docs.picokeys.com/picohsm/first-steps/

https://docs.picokeys.com/picohsm/key-generation/

https://docs.picokeys.com/picohsm/sign-verify/

https://docs.picokeys.com/picokeyapp/security/

https://docs.picokeys.com/production/otp-fuses

https://docs.picokeys.com/production/otp-fuses/

https://docs.picokeys.com/security/threat-model/

https://github.com/polhenarejos/pico-fido

https://github.com/polhenarejos/pico-hsm

https://github.com/polhenarejos/pico-hsm/pull/143

https://github.com/polhenarejos/pico-vault-enroller

https://www.picokeys.com/

https://www.picokeys.com/getting-started/

https://www.picokeys.com/pico-fido/

https://www.picokeys.com/pico-hsm/

https://www.picokeys.com/picokeyapp/

https://www.picokeys.com/pico-openpgp/

↑ Back to the Table of Contents

Top comments (0)