Security & Signing
ThistleOS uses a layered security model: the Recovery OS verifies the main firmware, the kernel verifies apps and drivers, and each app's permissions are gated by a cryptographic signature from a trusted developer key.
✎ Edit on GitHubChain of trust
Signing scheme
ThistleOS v0.1.0 uses HMAC-SHA256 with a per-developer shared key stored in the kernel's NVS partition. The developer generates a key pair, registers the public key with the kernel (via a one-time NVS write), and signs binaries with the private key.
A planned upgrade to Ed25519 asymmetric signatures is tracked in the roadmap. Ed25519 will allow the kernel to verify signatures using only the public key embedded in firmware, with no shared secret on the device.
| v0.1.0 (current) | v0.2.0 (planned) | |
|---|---|---|
| Algorithm | HMAC-SHA256 | Ed25519 |
| Key type | Shared secret (32 bytes) | Public/private key pair |
| Key storage on device | NVS (encrypted partition) | Public key only, in firmware |
| Developer workflow | Pre-shared key exchange | Publish public key; sign with private key |
Signing a binary (v0.1.0)
# Generate a developer key (keep this secret)
thistle-keygen --output dev_key.bin
# Sign an app
thistle-sign --key dev_key.bin my_app.app.elf
# Produces: my_app.app.elf.sig
# Sign a driver
thistle-sign --key dev_key.bin my_driver.drv.elf
# Produces: my_driver.drv.elf.sig
# Sign a firmware update
thistle-sign --key dev_key.bin thistle_os.bin
# Produces: thistle_os.bin.sig
Registering a developer key on the device
# One-time setup: flash developer key into NVS
thistle-register-key --port /dev/ttyACM0 --key dev_key.bin
Permission system
Permissions are a bitmask of permission_t values checked on every privileged syscall. The kernel maintains one bitmask per loaded app ID.
How permissions are granted
- App ELF contains a
thistle_app_tstruct with fields declaring required capabilities - The catalog entry for the app also lists the same permissions in its
permissionsarray - During install, the kernel verifies the signature against the app's content
- If valid,
permissions_grant(app_id, declared_perms)is called - At runtime, every privileged syscall calls
permissions_check(app_id, required_perm)
How unsigned apps are sandboxed
An app without a valid signature is loaded with permissions_grant(app_id, 0) — zero permissions. It can still call display and input syscalls (those are unpermissioned), but any attempt to access radio, GPS, storage, network, audio, or IPC returns ESP_ERR_NOT_ALLOWED.
The app itself is not prevented from running — it just cannot do anything sensitive. This is intentional: it allows unsigned apps to be useful (e.g., a calculator or a clock) while preventing them from silently transmitting location data or sending radio packets.
OTA update verification
The OTA flow has two levels of verification:
- SHA-256 — verified immediately after download, before writing to the SD card. A corrupt download is detected and discarded.
- Signature — verified before flashing to
ota_1. A firmware image without a valid signature from the ThistleOS signing key is rejected outright.
After a new firmware image boots successfully, it must call ota_mark_valid() within 30 seconds. If it does not, the ESP-IDF bootloader automatically rolls back to the previous partition on the next reset.
Password vault encryption
The built-in settings app includes a password vault. Vault entries are encrypted at rest on the SD card using:
- Key derivation: Argon2id v1.3 with a random 16-byte salt and versioned, authenticated parameters
- Encryption: AES-256-CBC with a random IV per entry
- Storage:
/sdcard/config/vault.enc, with automatic authenticated migration from legacy PBKDF2 files
The master password is never stored — only the derived key is used for encryption/decryption and is held in DRAM only while the vault app is open. Closing the vault app zeroes the in-memory key.
Key management recommendations
- Use a separate developer key per project or product. Do not reuse keys across unrelated applications.
- Rotate keys periodically. When you rotate, distribute the new public key via a firmware update so that devices can verify future apps.
- For production devices, consider burning the developer public key into eFuse so that it cannot be changed by a firmware update. See the eFuse section below.
- Never commit private keys to version control. Use CI secrets or a hardware security module for signing in CI pipelines.
eFuse policy
ThistleOS never burns eFuses by default. eFuse burning is a one-way operation — once a bit is set, it cannot be cleared. Burning the wrong value can permanently brick a device.
eFuse features available (but not enabled by default):
| Feature | eFuse | Effect |
|---|---|---|
| Secure Boot v2 | ABS_DONE_0 | Bootloader only boots signed firmware. Requires burning the RSA-3072 or ECDSA public key into eFuse key block. |
| Flash Encryption | FLASH_CRYPT_CNT | Flash contents encrypted at rest. JTAG debugging disabled automatically. |
| JTAG Disable | JTAG_DISABLE | Permanently disables JTAG debugging. Useful for production units. |
| Read-Protect NVS Key | Key block protection bits | NVS encryption key cannot be read back via JTAG or software. |