v0.0.47 - Added post-quantum cryptography (ML-DSA-65, SLH-DSA-128s, ML-KEM-768) and standard ECC (ed25519, x25519) support with algorithm-based API

This commit is contained in:
Laan Tungir
2026-07-16 15:14:57 -04:00
parent 6fd7b8ce1f
commit c5f1a70658
66 changed files with 19091 additions and 257 deletions

View File

@@ -99,3 +99,165 @@ Notes:
- Typical working range is ~4.7uF to 22uF; 10uF is recommended
- Keep leads short for best stability
- Auto-reset behavior for flashing may still work, but if flashing ever becomes unreliable, enter bootloader manually
## Post-quantum crypto support (Phase 7)
Both firmware targets (`feather_s3_tft` and `cyd_esp32_2432s028`) now include
the three NIST-standardized post-quantum algorithms alongside the existing
secp256k1 (Nostr) and new ed25519/x25519 classical algorithms:
| Algorithm | Standard | Purpose | Pub key | Priv key | Sig/Ct |
|---|---|---|---|---|---|
| secp256k1 | — | Nostr (existing) | 32 B | 32 B | 64 B |
| ed25519 | RFC 8032 | SSH signatures | 32 B | 32 B | 64 B |
| x25519 | RFC 7748 | Key agreement (age) | 32 B | 32 B | — |
| ML-DSA-65 | FIPS 204 | PQ signatures | 1952 B | 4032 B | 3309 B |
| SLH-DSA-128s | FIPS 205 | PQ hash-based sigs | 32 B | 64 B | 7856 B |
| ML-KEM-768 | FIPS 203 | PQ key encapsulation | 1184 B | 2400 B | 1088 B |
### mbedtls backend (vs OpenSSL on host)
The host build uses OpenSSL EVP for SHA-256, SHA-512, SHA3-256, SHA3-512,
SHAKE-128, and SHAKE-256. On ESP32, OpenSSL is not available. Instead, the
firmware uses a **crypto backend abstraction** ([`resources/pqclean/common/crypto_backend.h`](../resources/pqclean/common/crypto_backend.h))
with two implementations:
- [`resources/pqclean/common/crypto_backend_openssl.c`](../resources/pqclean/common/crypto_backend_openssl.c) — host build (OpenSSL EVP)
- [`resources/pqclean/common/crypto_backend_mbedtls.c`](../resources/pqclean/common/crypto_backend_mbedtls.c) — ESP32 firmware (mbedtls)
The mbedtls backend uses:
- `mbedtls_sha256_ret()` for SHA-256 (ESP32-S3 hardware accelerated)
- `mbedtls_sha512_ret()` for SHA-512 (ESP32-S3 hardware accelerated)
- `mbedtls_md` API for SHA3-256, SHA3-512, SHAKE-128, SHAKE-256
### Enabling SHA3/SHAKE in menuconfig
mbedtls does not enable SHA3/SHAKE by default. You must enable them in
menuconfig before building:
```
Component config → mbedTLS → Hash functions → SHA-3
Component config → mbedTLS → Hash functions → SHAKE
```
Or add to `sdkconfig.defaults`:
```
CONFIG_MBEDTLS_SHA3_C=y
CONFIG_MBEDTLS_SHAKE_C=y
```
If mbedtls does not have SHAKE support, the firmware build will fail at link
time with unresolved `mbedtls_md_info_from_type(MBEDTLS_MD_SHAKE128)`. In that
case, either enable the config options above or patch mbedtls with a
Keccak/SHAKE contribution.
### PQClean component
The PQClean algorithm code is compiled as an ESP-IDF component at
`components/pqclean/`. The component's `CMakeLists.txt` references the shared
source files in [`resources/pqclean/`](../resources/pqclean/) via relative
paths, so there is a single source of truth for both host and firmware builds.
The component includes:
- ML-DSA-65: `sign.c`, `poly.c`, `ntt.c`
- SLH-DSA-128s: `sign.c`, `fors.c`, `wots.c`, `hash.c`, `thash.c`, `address.c`, `utils.c`
- ML-KEM-768: `kem.c`, `indcpa.c`, `poly.c`, `ntt.c`, `cbd.c`, `reduce.c`, `symmetric.c`, `verify.c`
- Common: `fips202.c`, `sha2.c`, `crypto_backend_mbedtls.c`
- Firmware DRBG: `pq_drbg_firmware.c`, `randombytes_mbedtls.c`
### Flash usage estimates
| Algorithm | Code size (approx) |
|---|---|
| ML-DSA-65 | ~150 KB |
| SLH-DSA-128s | ~80 KB |
| ML-KEM-768 | ~120 KB |
| Total PQ code | ~350 KB |
The ESP32-S3 (Feather S3 TFT) has 8 MB flash and the ESP32 (CYD) has 4 MB
flash. The PQ code fits comfortably in both, but partition sizes may need
adjustment if the total app image exceeds the default partition.
### RAM usage notes
PQ key buffers are large compared to classical ECC keys:
| Buffer | Size |
|---|---|
| ML-DSA-65 private key | 4032 bytes |
| ML-DSA-65 public key | 1952 bytes |
| ML-DSA-65 signature | 3309 bytes |
| SLH-DSA-128s signature | 7856 bytes |
| ML-KEM-768 private key | 2400 bytes |
| ML-KEM-768 public key | 1184 bytes |
| ML-KEM-768 ciphertext | 1088 bytes |
The ESP32 has ~320 KB available heap (after WiFi/BT are disabled). These
buffers **must not be stack-allocated** — the default task stack is 8 KB.
Use `malloc()` or static buffers. The firmware derives PQ keys **on demand**
(not all at startup) to keep peak RAM usage low.
### SLH-DSA-128s signing latency warning
SLH-DSA-128s (SPHINCS+-128s) is a hash-based signature scheme with a deep
hypertree structure (7 layers of WOTS+ + Merkle trees). On the ESP32-S3
(240 MHz dual-core), expect:
- **Key generation**: 530 seconds
- **Signing**: 530 seconds
- **Verification**: 0.52 seconds
This is inherent to the algorithm — it trades computation for minimal trust
assumptions (only SHA-256). The firmware logs a warning before SLH-DSA-128s
keygen/signing. Users should choose whether to use SLH-DSA-128s per-role
based on their latency tolerance. ML-DSA-65 is much faster (~100 ms for
signing on ESP32-S3) and is the recommended PQ signature algorithm for
interactive use.
### Derivation paths
All algorithms derive from the mnemonic using BIP-32/HMAC-SHA512 with
SLIP-0010 all-hardened derivation for ed25519/x25519/PQ:
| Algorithm | Path | Notes |
|---|---|---|
| secp256k1 (Nostr) | `m/44'/1237'/<n>'/0/0` | NIP-06, existing |
| ed25519 (SSH) | `m/44'/102001'/<n>'/0'/0'` | SLIP-0010 |
| x25519 (age) | `m/44'/102002'/<n>'/0'/0'` | SLIP-0010 |
| ML-DSA-65 | `m/44'/102003'/<n>'/0'/0'` | seed → PQClean keygen |
| SLH-DSA-128s | `m/44'/102004'/<n>'/0'/0'` | seed → PQClean keygen |
| ML-KEM-768 | `m/44'/102005'/<n>'/0'/0'` | seed → PQClean keygen |
The PQ derivation produces a 32-byte seed that feeds a deterministic
SHAKE-256 DRBG ([`pq_drbg_firmware.c`](feather_s3_tft/components/pqclean/pq_drbg_firmware.c)),
which replaces PQClean's `randombytes()` during keygen. This gives
deterministic, mnemonic-recoverable PQ keys — same mnemonic, same key pair.
### Firmware API
The firmware exposes PQ operations via [`pq_crypto_firmware.h`](feather_s3_tft/main/pq_crypto_firmware.h):
```c
/* Key generation (deterministic from mnemonic-derived seed) */
int fw_pq_ml_dsa_65_keygen(const uint8_t seed[32], uint8_t *pk, uint8_t *sk);
int fw_pq_slh_dsa_128s_keygen(const uint8_t seed[32], uint8_t *pk, uint8_t *sk);
int fw_pq_ml_kem_768_keygen(const uint8_t seed[32], uint8_t *pk, uint8_t *sk);
/* Signing / verification */
int fw_pq_ml_dsa_65_sign(uint8_t *sig, size_t *siglen, ...);
int fw_pq_slh_dsa_128s_sign(uint8_t *sig, size_t *siglen, ...);
/* KEM encaps / decaps */
int fw_pq_ml_kem_768_encaps(uint8_t *ct, uint8_t *ss, const uint8_t *pk);
int fw_pq_ml_kem_768_decaps(uint8_t *ss, const uint8_t *ct, const uint8_t *sk);
```
Key derivation from the mnemonic seed is via [`key_derivation.h`](feather_s3_tft/main/key_derivation.h):
```c
int derive_ed25519_key(const uint8_t seed[64], uint32_t index, ...);
int derive_x25519_key(const uint8_t seed[64], uint32_t index, ...);
int derive_ml_dsa_65_key(const uint8_t seed[64], uint32_t index, ...);
int derive_slh_dsa_128s_key(const uint8_t seed[64], uint32_t index, ...);
int derive_ml_kem_768_key(const uint8_t seed[64], uint32_t index, ...);
```

View File

@@ -0,0 +1,60 @@
# CMakeLists.txt — ESP-IDF component for PQClean post-quantum algorithms.
#
# Compiles the three PQ algorithms (ML-DSA-65, SLH-DSA-128s, ML-KEM-768)
# from the shared resources/pqclean/ source tree, using the mbedtls
# crypto backend (crypto_backend_mbedtls.c) for SHA-2/SHA3/SHAKE.
#
# The source files are referenced via relative paths back to the shared
# resources/pqclean/ directory so there is a single source of truth.
#
# mbedtls requirements:
# CONFIG_MBEDTLS_SHA3_C=y (for SHA3-256, SHA3-512)
# CONFIG_MBEDTLS_SHAKE_C=y (for SHAKE-128, SHAKE-256)
# Enable these in menuconfig under Component config -> mbedTLS ->
# Hash functions -> SHA-3 and SHAKE.
set(PQCLEAN_ROOT "${CMAKE_CURRENT_LIST_DIR}/../../../../resources/pqclean")
idf_component_register(
SRCS
"${PQCLEAN_ROOT}/crypto_sign/ml-dsa-65/sign.c"
"${PQCLEAN_ROOT}/crypto_sign/ml-dsa-65/poly.c"
"${PQCLEAN_ROOT}/crypto_sign/ml-dsa-65/ntt.c"
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/sign.c"
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/fors.c"
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/wots.c"
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/hash.c"
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/thash.c"
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/address.c"
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/utils.c"
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/kem.c"
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/indcpa.c"
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/poly.c"
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/ntt.c"
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/cbd.c"
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/reduce.c"
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/symmetric.c"
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/verify.c"
"${PQCLEAN_ROOT}/common/fips202.c"
"${PQCLEAN_ROOT}/common/sha2.c"
"${PQCLEAN_ROOT}/common/crypto_backend_mbedtls.c"
"randombytes_mbedtls.c"
"pq_drbg_firmware.c"
INCLUDE_DIRS
"include"
"${PQCLEAN_ROOT}/common"
"${PQCLEAN_ROOT}/crypto_sign/ml-dsa-65"
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s"
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768"
REQUIRES
mbedtls
)
# Suppress warnings from the PQClean code (it uses C99 patterns that
# trigger -Wextra warnings under ESP-IDF's default flags).
target_compile_options(${COMPONENT_LIB} PRIVATE
-Wno-unused-parameter
-Wno-sign-compare
-Wno-unused-variable
-Wno-unused-but-set-variable
)

View File

@@ -0,0 +1,5 @@
/* ml_dsa_65_api.h — firmware wrapper that includes the real PQClean header. */
#ifndef FIRMWARE_ML_DSA_65_API_WRAPPER_H
#define FIRMWARE_ML_DSA_65_API_WRAPPER_H
#include "../../../../resources/pqclean/crypto_sign/ml-dsa-65/api.h"
#endif

View File

@@ -0,0 +1,5 @@
/* ml_kem_768_api.h — firmware wrapper that includes the real PQClean header. */
#ifndef FIRMWARE_ML_KEM_768_API_WRAPPER_H
#define FIRMWARE_ML_KEM_768_API_WRAPPER_H
#include "../../../../resources/pqclean/crypto_kem/ml-kem-768/api.h"
#endif

View File

@@ -0,0 +1,38 @@
/* pqclean.h — Umbrella include for the ESP32 firmware PQClean component.
*
* Exposes the three post-quantum algorithms (ML-DSA-65, SLH-DSA-128s,
* ML-KEM-768) and the deterministic DRBG used for mnemonic-recoverable
* key generation.
*
* On ESP32 the underlying hash/SHAKE primitives are provided by the
* mbedtls backend (crypto_backend_mbedtls.c) instead of OpenSSL.
*/
#ifndef FIRMWARE_PQCLEAN_H
#define FIRMWARE_PQCLEAN_H
#include <stddef.h>
#include <stdint.h>
/* --- ML-DSA-65 (FIPS 204, lattice signatures) --- */
#include "ml_dsa_65_api.h"
/* --- SLH-DSA-128s (FIPS 205, hash-based signatures) --- */
#include "slh_dsa_128s_api.h"
/* --- ML-KEM-768 (FIPS 203, lattice KEM) --- */
#include "ml_kem_768_api.h"
/* --- Deterministic DRBG (replaces randombytes() for keygen) --- */
/* Initializes the DRBG with a 32-byte mnemonic-derived seed. Subsequent
* randombytes() calls will produce a deterministic byte stream. */
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
/* Zeroizes the DRBG state (call after keygen to wipe sensitive material). */
void pq_drbg_zeroize(void);
/* randombytes() — called by the PQClean algorithm code.
* On firmware this is provided by randombytes_mbedtls.c (deterministic DRBG
* for keygen, or mbedtls_ctr_drbg for real randomness during encaps). */
int randombytes(unsigned char *buf, size_t len);
#endif /* FIRMWARE_PQCLEAN_H */

View File

@@ -0,0 +1,5 @@
/* slh_dsa_128s_api.h — firmware wrapper that includes the real PQClean header. */
#ifndef FIRMWARE_SLH_DSA_128S_API_WRAPPER_H
#define FIRMWARE_SLH_DSA_128S_API_WRAPPER_H
#include "../../../../resources/pqclean/crypto_sign/slh-dsa-128s/api.h"
#endif

View File

@@ -0,0 +1,108 @@
/* pq_drbg_firmware.c — Deterministic PRNG for PQ key generation on ESP32.
*
* Same algorithm as the host's src/pq_drbg.c but uses the crypto backend
* abstraction (which resolves to mbedtls on ESP32) for SHAKE-256 instead
* of OpenSSL EVP. This allows deterministic PQ key generation from a
* mnemonic-derived seed: same seed -> same randombytes output sequence.
*
* The PRNG: SHAKE-256(seed || counter) produces a stream of pseudo-random
* bytes. The counter is a 64-bit little-endian integer that increments
* each time we need more output.
*/
#include <string.h>
#include <stdlib.h>
#include "crypto_backend.h"
/* --- DRBG state --- */
static unsigned char g_seed[32];
static int g_seed_len = 0;
static uint64_t g_counter = 0;
static unsigned char g_buffer[168]; /* SHAKE-256 rate = 136, 168 for safety */
static size_t g_buffer_pos = sizeof(g_buffer);
static int g_initialized = 0;
/* --- internal: squeeze more bytes from SHAKE-256 --- */
static void drbg_refill(void) {
unsigned char seed_block[32 + 8]; /* seed + counter (8 bytes LE) */
memcpy(seed_block, g_seed, (size_t)g_seed_len);
seed_block[g_seed_len + 0] = (unsigned char)(g_counter & 0xFF);
seed_block[g_seed_len + 1] = (unsigned char)((g_counter >> 8) & 0xFF);
seed_block[g_seed_len + 2] = (unsigned char)((g_counter >> 16) & 0xFF);
seed_block[g_seed_len + 3] = (unsigned char)((g_counter >> 24) & 0xFF);
seed_block[g_seed_len + 4] = (unsigned char)((g_counter >> 32) & 0xFF);
seed_block[g_seed_len + 5] = (unsigned char)((g_counter >> 40) & 0xFF);
seed_block[g_seed_len + 6] = (unsigned char)((g_counter >> 48) & 0xFF);
seed_block[g_seed_len + 7] = (unsigned char)((g_counter >> 56) & 0xFF);
crypto_backend_shake256(seed_block, (size_t)g_seed_len + 8,
g_buffer, sizeof(g_buffer));
g_counter++;
g_buffer_pos = 0;
}
/* --- public API --- */
void pq_drbg_init(const unsigned char *seed, size_t seed_len) {
if (seed == NULL || seed_len == 0) {
return;
}
memset(g_seed, 0, sizeof(g_seed));
if (seed_len > sizeof(g_seed)) {
seed_len = sizeof(g_seed);
}
memcpy(g_seed, seed, seed_len);
g_seed_len = (int)sizeof(g_seed); /* always use 32-byte seed (zero-padded) */
g_counter = 0;
g_buffer_pos = sizeof(g_buffer);
g_initialized = 1;
}
void pq_drbg_zeroize(void) {
crypto_backend_cleanse(g_seed, sizeof(g_seed));
crypto_backend_cleanse(g_buffer, sizeof(g_buffer));
g_seed_len = 0;
g_counter = 0;
g_buffer_pos = sizeof(g_buffer);
g_initialized = 0;
}
/* Returns 1 if the DRBG has been initialized (keygen mode), 0 otherwise.
* Used by randombytes_mbedtls.c to decide between deterministic DRBG and
* hardware RNG. */
int pq_drbg_is_initialized(void) {
return g_initialized;
}
/* pq_drbg_randombytes is called by randombytes() below. */
int pq_drbg_randombytes(unsigned char *buf, size_t len) {
if (buf == NULL || !g_initialized) {
return -1;
}
while (len > 0) {
size_t avail;
size_t to_copy;
if (g_buffer_pos >= sizeof(g_buffer)) {
drbg_refill();
if (g_buffer_pos >= sizeof(g_buffer)) {
return -1; /* refill failed */
}
}
avail = sizeof(g_buffer) - g_buffer_pos;
to_copy = (len < avail) ? len : avail;
memcpy(buf, g_buffer + g_buffer_pos, to_copy);
g_buffer_pos += to_copy;
buf += to_copy;
len -= to_copy;
}
return 0;
}

View File

@@ -0,0 +1,40 @@
/* randombytes_mbedtls.c — randombytes() implementation for ESP32 firmware.
*
* PQClean's algorithm code calls randombytes() for:
* 1. Key generation (keygen) — must be deterministic from the mnemonic
* seed so keys are recoverable. The DRBG is initialized via
* pq_drbg_init() before keygen, so randombytes() draws from the
* deterministic stream.
* 2. Encapsulation (ML-KEM enc) — needs real cryptographic randomness.
* When the DRBG is NOT initialized, randombytes() falls back to
* esp_fill_random() which uses the ESP32 hardware RNG.
*
* This dual-mode behavior matches the host build (src/pq_drbg.c) where
* the DRBG is initialized for keygen and randombytes() returns -1 if
* called without initialization. On firmware we allow the fallback to
* hardware RNG for encaps, which is the correct behavior.
*/
#include <string.h>
#include "esp_random.h"
/* Defined in pq_drbg_firmware.c */
extern int pq_drbg_randombytes(unsigned char *buf, size_t len);
/* Check if the DRBG is initialized (declared in pq_drbg_firmware.c).
* We use a helper to avoid exposing the static directly. */
extern int pq_drbg_is_initialized(void);
int randombytes(unsigned char *buf, size_t len) {
if (buf == NULL) {
return -1;
}
/* If the deterministic DRBG is active (keygen mode), use it. */
if (pq_drbg_is_initialized()) {
return pq_drbg_randombytes(buf, len);
}
/* Otherwise, use the ESP32 hardware RNG for real randomness (encaps). */
esp_fill_random(buf, len);
return 0;
}

View File

@@ -10,6 +10,7 @@ idf_component_register(
"bech32.c"
"mnemonic.c"
"key_derivation.c"
"pq_crypto_firmware.c"
"secure_mem.c"
"../../../resources/nostr_core_lib/nostr_core/nip004.c"
"../../../resources/nostr_core_lib/nostr_core/nip044.c"
@@ -31,6 +32,7 @@ idf_component_register(
esp_driver_uart
mbedtls
secp256k1
pqclean
json)
target_compile_definitions(${COMPONENT_LIB} PUBLIC LV_CONF_INCLUDE_SIMPLE=1)

View File

@@ -1,17 +1,25 @@
#include "key_derivation.h"
#include "pq_crypto_firmware.h"
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "esp_random.h"
#include "esp_log.h"
#include "mbedtls/md.h"
#include "mbedtls/ed25519.h"
#include "mbedtls/ecp.h"
#include "mbedtls/pk.h"
#include "secp256k1.h"
#include "secp256k1_extrakeys.h"
#include "secp256k1_schnorrsig.h"
static const char *KD_TAG = "key_derivation";
#define BIP32_HARDENED_FLAG 0x80000000u
typedef struct {
@@ -253,3 +261,316 @@ int schnorr_sign32(const uint8_t privkey[32], const uint8_t msg32[32], uint8_t s
secp256k1_context_destroy(ctx);
return 0;
}
/* ====================================================================
* Phase 7: ed25519, x25519, and post-quantum key derivation
* ==================================================================== */
/* SLIP-0010 all-hardened derivation for ed25519/x25519.
*
* SLIP-0010 uses HMAC-SHA512 with a "ed25519 seed" or curve-specific key
* for the master key, and all derivation steps are hardened (the parent
* private key is prepended to the index data).
*
* For ed25519/x25519, the derived 512-bit HMAC output is split:
* - first 32 bytes = private key (the scalar)
* - last 32 bytes = chain code
*
* The private key IS the ed25519/x25519 secret — no tweak-add is needed
* (unlike secp256k1 BIP-32 where the child priv = parent_priv + HMAC).
*/
/* SLIP-0010 master key from seed: HMAC-SHA512(key="ed25519 seed", data=seed) */
static int slip10_master_from_seed(const uint8_t seed[64],
uint8_t priv[32], uint8_t chain[32]) {
static const uint8_t kEd25519Seed[] = "ed25519 seed";
uint8_t i64[64] = {0};
if (hmac_sha512(kEd25519Seed, sizeof(kEd25519Seed) - 1,
seed, 64, i64) != 0) {
return -1;
}
memcpy(priv, i64, 32);
memcpy(chain, i64 + 32, 32);
memset(i64, 0, sizeof(i64));
return 0;
}
/* SLIP-0010 hardened child derivation:
* HMAC-SHA512(key=chain, data=0x00 || priv || index_be32) */
static int slip10_ckd_priv(const uint8_t parent_priv[32],
const uint8_t parent_chain[32],
uint32_t index,
uint8_t child_priv[32],
uint8_t child_chain[32]) {
uint8_t data[37];
uint8_t i64[64] = {0};
/* Hardened derivation: 0x00 || priv || index (big-endian) */
data[0] = 0x00;
memcpy(data + 1, parent_priv, 32);
data[33] = (uint8_t)((index >> 24) & 0xFF);
data[34] = (uint8_t)((index >> 16) & 0xFF);
data[35] = (uint8_t)((index >> 8) & 0xFF);
data[36] = (uint8_t)(index & 0xFF);
if (hmac_sha512(parent_chain, 32, data, sizeof(data), i64) != 0) {
memset(data, 0, sizeof(data));
return -1;
}
memcpy(child_priv, i64, 32);
memcpy(child_chain, i64 + 32, 32);
memset(data, 0, sizeof(data));
memset(i64, 0, sizeof(i64));
return 0;
}
/* Derive a 32-byte seed via SLIP-0010 all-hardened path.
* path[] is an array of hardened indices (the caller sets the hardened flag).
* Returns the final 32-byte private material in `out_seed`. */
static int slip10_derive_seed(const uint8_t seed[64],
const uint32_t *path, size_t path_len,
uint8_t out_seed[32]) {
uint8_t priv[32], chain[32], next_priv[32], next_chain[32];
size_t i;
if (slip10_master_from_seed(seed, priv, chain) != 0) {
return -1;
}
for (i = 0; i < path_len; i++) {
if (slip10_ckd_priv(priv, chain, path[i],
next_priv, next_chain) != 0) {
memset(priv, 0, sizeof(priv));
memset(chain, 0, sizeof(chain));
return -1;
}
memcpy(priv, next_priv, 32);
memcpy(chain, next_chain, 32);
}
memcpy(out_seed, priv, 32);
memset(priv, 0, sizeof(priv));
memset(chain, 0, sizeof(chain));
memset(next_priv, 0, sizeof(next_priv));
memset(next_chain, 0, sizeof(next_chain));
return 0;
}
/* --- ed25519 --- */
int derive_ed25519_key(const uint8_t seed[64], uint32_t index,
uint8_t privkey[32], uint8_t pubkey[32]) {
/* m/44'/102001'/<index>'/0'/0' — all hardened (SLIP-0010) */
const uint32_t path[5] = {
44u | BIP32_HARDENED_FLAG,
102001u | BIP32_HARDENED_FLAG,
index | BIP32_HARDENED_FLAG,
0u | BIP32_HARDENED_FLAG,
0u | BIP32_HARDENED_FLAG,
};
uint8_t derived_seed[32];
if (seed == NULL || privkey == NULL || pubkey == NULL) {
return -1;
}
if (slip10_derive_seed(seed, path, 5, derived_seed) != 0) {
return -1;
}
/* The SLIP-0010 derived 32 bytes IS the ed25519 private key.
* Use mbedtls to derive the public key. */
memcpy(privkey, derived_seed, 32);
/* mbedtls_ed25519_make_public: derive pub from priv */
/* Note: mbedtls ed25519 API may vary by version. The ESP-IDF mbedtls
* component provides mbedtls_ed25519_make_public (or via the PK API).
* We use the low-level function if available. */
int ret = mbedtls_ed25519_make_public((unsigned char *)pubkey, 32,
(const unsigned char *)privkey, 32);
if (ret != 0) {
ESP_LOGE(KD_TAG, "ed25519 make_public failed: %d", ret);
memset(derived_seed, 0, sizeof(derived_seed));
memset(privkey, 0, 32);
return -1;
}
memset(derived_seed, 0, sizeof(derived_seed));
return 0;
}
int ed25519_sign32(const uint8_t privkey[32], const uint8_t msg32[32],
uint8_t sig64[64]) {
/* mbedtls_ed25519_sign: sign a message (not pre-hashed) */
int ret = mbedtls_ed25519_sign((unsigned char *)sig64, 64,
(const unsigned char *)msg32, 32,
(const unsigned char *)privkey, 32,
NULL, NULL);
if (ret != 0) {
ESP_LOGE(KD_TAG, "ed25519 sign failed: %d", ret);
return -1;
}
return 0;
}
/* --- x25519 --- */
int derive_x25519_key(const uint8_t seed[64], uint32_t index,
uint8_t privkey[32], uint8_t pubkey[32]) {
/* m/44'/102002'/<index>'/0'/0' — all hardened (SLIP-0010) */
const uint32_t path[5] = {
44u | BIP32_HARDENED_FLAG,
102002u | BIP32_HARDENED_FLAG,
index | BIP32_HARDENED_FLAG,
0u | BIP32_HARDENED_FLAG,
0u | BIP32_HARDENED_FLAG,
};
uint8_t derived_seed[32];
mbedtls_ecp_group grp;
mbedtls_mpi d;
mbedtls_ecp_point Q;
int ret;
if (seed == NULL || privkey == NULL || pubkey == NULL) {
return -1;
}
if (slip10_derive_seed(seed, path, 5, derived_seed) != 0) {
return -1;
}
/* The SLIP-0010 derived 32 bytes IS the x25519 private key.
* Clamp it per RFC 7748 and derive the public key via mbedtls ECDH. */
memcpy(privkey, derived_seed, 32);
memset(derived_seed, 0, sizeof(derived_seed));
/* x25519 clamping: priv[0] &= 248, priv[31] &= 127, priv[31] |= 64 */
privkey[0] &= 248;
privkey[31] &= 127;
privkey[31] |= 64;
mbedtls_ecp_group_init(&grp);
mbedtls_mpi_init(&d);
mbedtls_ecp_point_init(&Q);
ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_CURVE25519);
if (ret != 0) {
ESP_LOGE(KD_TAG, "x25519 group load failed: %d", ret);
goto cleanup;
}
ret = mbedtls_mpi_read_binary_le(d, privkey, 32);
if (ret != 0) {
ESP_LOGE(KD_TAG, "x25519 mpi read failed: %d", ret);
goto cleanup;
}
ret = mbedtls_ecp_mul(&grp, &Q, d, &grp.G, NULL, NULL);
if (ret != 0) {
ESP_LOGE(KD_TAG, "x25519 ecp_mul failed: %d", ret);
goto cleanup;
}
/* Serialize the public key as raw 32 bytes (little-endian) */
{
size_t olen = 0;
ret = mbedtls_ecp_point_write_binary(&grp, &Q,
MBEDTLS_ECP_PF_COMPRESSED,
&olen, pubkey, 32);
if (ret != 0 || olen != 32) {
ESP_LOGE(KD_TAG, "x25519 pub serialize failed: %d", ret);
ret = -1;
}
}
cleanup:
mbedtls_ecp_group_free(&grp);
mbedtls_mpi_free(&d);
mbedtls_ecp_point_free(&Q);
return (ret == 0) ? 0 : -1;
}
/* --- ML-DSA-65 --- */
int derive_ml_dsa_65_key(const uint8_t seed[64], uint32_t index,
uint8_t *pk, uint8_t *sk) {
/* m/44'/102003'/<index>'/0'/0' — all hardened (SLIP-0010) -> 32-byte seed */
const uint32_t path[5] = {
44u | BIP32_HARDENED_FLAG,
102003u | BIP32_HARDENED_FLAG,
index | BIP32_HARDENED_FLAG,
0u | BIP32_HARDENED_FLAG,
0u | BIP32_HARDENED_FLAG,
};
uint8_t pq_seed[32];
if (seed == NULL || pk == NULL || sk == NULL) {
return -1;
}
if (slip10_derive_seed(seed, path, 5, pq_seed) != 0) {
return -1;
}
int ret = fw_pq_ml_dsa_65_keygen(pq_seed, pk, sk);
memset(pq_seed, 0, sizeof(pq_seed));
return ret;
}
/* --- SLH-DSA-128s --- */
int derive_slh_dsa_128s_key(const uint8_t seed[64], uint32_t index,
uint8_t *pk, uint8_t *sk) {
/* m/44'/102004'/<index>'/0'/0' — all hardened (SLIP-0010) -> 32-byte seed */
const uint32_t path[5] = {
44u | BIP32_HARDENED_FLAG,
102004u | BIP32_HARDENED_FLAG,
index | BIP32_HARDENED_FLAG,
0u | BIP32_HARDENED_FLAG,
0u | BIP32_HARDENED_FLAG,
};
uint8_t pq_seed[32];
if (seed == NULL || pk == NULL || sk == NULL) {
return -1;
}
if (slip10_derive_seed(seed, path, 5, pq_seed) != 0) {
return -1;
}
ESP_LOGW(KD_TAG, "SLH-DSA-128s keygen: this takes 5-30 seconds on ESP32");
int ret = fw_pq_slh_dsa_128s_keygen(pq_seed, pk, sk);
memset(pq_seed, 0, sizeof(pq_seed));
return ret;
}
/* --- ML-KEM-768 --- */
int derive_ml_kem_768_key(const uint8_t seed[64], uint32_t index,
uint8_t *pk, uint8_t *sk) {
/* m/44'/102005'/<index>'/0'/0' — all hardened (SLIP-0010) -> 32-byte seed */
const uint32_t path[5] = {
44u | BIP32_HARDENED_FLAG,
102005u | BIP32_HARDENED_FLAG,
index | BIP32_HARDENED_FLAG,
0u | BIP32_HARDENED_FLAG,
0u | BIP32_HARDENED_FLAG,
};
uint8_t pq_seed[32];
if (seed == NULL || pk == NULL || sk == NULL) {
return -1;
}
if (slip10_derive_seed(seed, path, 5, pq_seed) != 0) {
return -1;
}
int ret = fw_pq_ml_kem_768_keygen(pq_seed, pk, sk);
memset(pq_seed, 0, sizeof(pq_seed));
return ret;
}

View File

@@ -1,7 +1,61 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
/* --- secp256k1 (Nostr, existing) --- */
int derive_nostr_key(const uint8_t seed[64], uint8_t privkey[32], uint8_t pubkey[32]);
int derive_nostr_key_index(const uint8_t seed[64], uint32_t nostr_index, uint8_t privkey[32], uint8_t pubkey[32]);
int schnorr_sign32(const uint8_t privkey[32], const uint8_t msg32[32], uint8_t sig64[64]);
/* --- ed25519 (SSH signatures) --- */
/* Derives an ed25519 keypair from the mnemonic seed using SLIP-0010
* all-hardened derivation: m/44'/102001'/<n>'/0'/0'
* privkey: 32-byte ed25519 private scalar
* pubkey: 32-byte ed25519 public key
* Returns 0 on success, -1 on error. */
int derive_ed25519_key(const uint8_t seed[64], uint32_t index,
uint8_t privkey[32], uint8_t pubkey[32]);
/* Signs a 32-byte message digest with ed25519.
* sig: 64-byte ed25519 signature
* Returns 0 on success, -1 on error. */
int ed25519_sign32(const uint8_t privkey[32], const uint8_t msg32[32],
uint8_t sig64[64]);
/* --- x25519 (age encryption / key agreement) --- */
/* Derives an x25519 keypair from the mnemonic seed using SLIP-0010
* all-hardened derivation: m/44'/102002'/<n>'/0'/0'
* privkey: 32-byte x25519 private scalar
* pubkey: 32-byte x25519 public key
* Returns 0 on success, -1 on error. */
int derive_x25519_key(const uint8_t seed[64], uint32_t index,
uint8_t privkey[32], uint8_t pubkey[32]);
/* --- ML-DSA-65 (post-quantum signatures, FIPS 204) --- */
/* Derives an ML-DSA-65 keypair from the mnemonic seed.
* Path: m/44'/102003'/<n>'/0'/0' -> 32-byte seed -> PQClean keygen
* pk: FW_ML_DSA_65_PUBKEY_LEN (1952) bytes — caller must allocate
* sk: FW_ML_DSA_65_PRIVKEY_LEN (4032) bytes — caller must allocate
* Returns 0 on success, -1 on error. */
int derive_ml_dsa_65_key(const uint8_t seed[64], uint32_t index,
uint8_t *pk, uint8_t *sk);
/* --- SLH-DSA-128s (post-quantum hash-based signatures, FIPS 205) --- */
/* Derives an SLH-DSA-128s keypair from the mnemonic seed.
* Path: m/44'/102004'/<n>'/0'/0' -> 32-byte seed -> PQClean keygen
* pk: FW_SLH_DSA_128S_PUBKEY_LEN (32) bytes
* sk: FW_SLH_DSA_128S_PRIVKEY_LEN (64) bytes
* WARNING: Takes 5-30 seconds on ESP32.
* Returns 0 on success, -1 on error. */
int derive_slh_dsa_128s_key(const uint8_t seed[64], uint32_t index,
uint8_t *pk, uint8_t *sk);
/* --- ML-KEM-768 (post-quantum KEM, FIPS 203) --- */
/* Derives an ML-KEM-768 keypair from the mnemonic seed.
* Path: m/44'/102005'/<n>'/0'/0' -> 32-byte seed -> PQClean keygen
* pk: FW_ML_KEM_768_PUBKEY_LEN (1184) bytes — caller must allocate
* sk: FW_ML_KEM_768_PRIVKEY_LEN (2400) bytes — caller must allocate
* Returns 0 on success, -1 on error. */
int derive_ml_kem_768_key(const uint8_t seed[64], uint32_t index,
uint8_t *pk, uint8_t *sk);

View File

@@ -0,0 +1,171 @@
/* pq_crypto_firmware.c — Post-quantum crypto wrappers for ESP32 firmware.
*
* Wraps the PQClean algorithm API (via the pqclean component) with
* firmware-friendly functions that handle the deterministic DRBG setup
* for keygen and provide clean sign/verify/encaps/decaps interfaces.
*
* The PQ key buffers are large (ML-DSA-65 priv = 4032 bytes, SLH-DSA-128s
* sig = 7856 bytes). Callers must allocate these on the heap or as static
* buffers — stack allocation on ESP32 (8KB task stack default) will overflow
* for the larger buffers.
*/
#include "pq_crypto_firmware.h"
#include "pqclean.h"
#include <stdlib.h>
#include <string.h>
/* --- Key generation (deterministic from seed) --- */
int fw_pq_ml_dsa_65_keygen(const uint8_t seed[32],
uint8_t *pk, uint8_t *sk) {
if (seed == NULL || pk == NULL || sk == NULL) {
return -1;
}
/* Initialize the deterministic DRBG with the mnemonic-derived seed */
pq_drbg_init(seed, 32);
/* Run PQClean keygen — randombytes() draws from the DRBG */
int ret = crypto_sign_keypair(pk, sk);
/* Wipe the DRBG state — the seed material is sensitive */
pq_drbg_zeroize();
return ret;
}
int fw_pq_slh_dsa_128s_keygen(const uint8_t seed[32],
uint8_t *pk, uint8_t *sk) {
if (seed == NULL || pk == NULL || sk == NULL) {
return -1;
}
pq_drbg_init(seed, 32);
/* WARNING: This call takes 5-30 seconds on ESP32 due to the
* hypertree construction (7 layers of WOTS+ + Merkle trees). */
int ret = slh_dsa_128s_crypto_sign_keypair(pk, sk);
pq_drbg_zeroize();
return ret;
}
int fw_pq_ml_kem_768_keygen(const uint8_t seed[32],
uint8_t *pk, uint8_t *sk) {
if (seed == NULL || pk == NULL || sk == NULL) {
return -1;
}
pq_drbg_init(seed, 32);
int ret = crypto_kem_keypair(pk, sk);
pq_drbg_zeroize();
return ret;
}
/* --- Signing --- */
int fw_pq_ml_dsa_65_sign(uint8_t *sig, size_t *siglen,
const uint8_t *m, size_t mlen,
const uint8_t *sk) {
if (sig == NULL || siglen == NULL || m == NULL || sk == NULL) {
return -1;
}
return crypto_sign(sig, siglen, m, mlen, sk);
}
int fw_pq_ml_dsa_65_verify(const uint8_t *sig, size_t siglen,
const uint8_t *m, size_t mlen,
const uint8_t *pk) {
if (sig == NULL || m == NULL || pk == NULL) {
return -1;
}
/* crypto_sign_open expects (m_out, mlen_out, sm, smlen, pk) where sm
* is the signed message. For detached signatures we reconstruct: the
* PQClean API uses crypto_sign_open with sm = sig || m. */
/* For firmware use, we provide a simple verify by re-signing is not
* possible (non-deterministic). The PQClean crypto_sign_open expects
* the concatenated sig||msg format. Callers should use the PQClean
* API directly for verification, or we build the sm buffer here. */
uint8_t *sm = (uint8_t *)malloc(siglen + mlen);
if (sm == NULL) {
return -1;
}
memcpy(sm, sig, siglen);
memcpy(sm + siglen, m, mlen);
uint8_t *m_out = (uint8_t *)malloc(mlen);
if (m_out == NULL) {
free(sm);
return -1;
}
size_t mlen_out = 0;
int ret = crypto_sign_open(m_out, &mlen_out, sm, siglen + mlen, pk);
free(sm);
free(m_out);
return ret;
}
int fw_pq_slh_dsa_128s_sign(uint8_t *sig, size_t *siglen,
const uint8_t *m, size_t mlen,
const uint8_t *sk) {
if (sig == NULL || siglen == NULL || m == NULL || sk == NULL) {
return -1;
}
/* WARNING: This call takes 5-30 seconds on ESP32. */
return slh_dsa_128s_crypto_sign(sig, siglen, m, mlen, sk);
}
int fw_pq_slh_dsa_128s_verify(const uint8_t *sig, size_t siglen,
const uint8_t *m, size_t mlen,
const uint8_t *pk) {
if (sig == NULL || m == NULL || pk == NULL) {
return -1;
}
uint8_t *sm = (uint8_t *)malloc(siglen + mlen);
if (sm == NULL) {
return -1;
}
memcpy(sm, sig, siglen);
memcpy(sm + siglen, m, mlen);
uint8_t *m_out = (uint8_t *)malloc(mlen);
if (m_out == NULL) {
free(sm);
return -1;
}
size_t mlen_out = 0;
int ret = slh_dsa_128s_crypto_sign_open(m_out, &mlen_out, sm,
siglen + mlen, pk);
free(sm);
free(m_out);
return ret;
}
/* --- KEM --- */
int fw_pq_ml_kem_768_encaps(uint8_t *ct, uint8_t *ss, const uint8_t *pk) {
if (ct == NULL || ss == NULL || pk == NULL) {
return -1;
}
/* encaps uses real randomness (hardware RNG) — the DRBG is not
* initialized, so randombytes() falls back to esp_fill_random(). */
return crypto_kem_enc(ct, ss, pk);
}
int fw_pq_ml_kem_768_decaps(uint8_t *ss, const uint8_t *ct, const uint8_t *sk) {
if (ss == NULL || ct == NULL || sk == NULL) {
return -1;
}
return crypto_kem_dec(ss, ct, sk);
}

View File

@@ -0,0 +1,110 @@
/* pq_crypto_firmware.h — Post-quantum crypto wrappers for ESP32 firmware.
*
* Provides firmware-friendly wrappers around the PQClean algorithms:
* - ML-DSA-65 (FIPS 204 signatures)
* - SLH-DSA-128s (FIPS 205 hash-based signatures)
* - ML-KEM-768 (FIPS 203 key encapsulation)
*
* Key generation is deterministic from a 32-byte seed (derived from the
* mnemonic via BIP-32/HMAC-SHA512). The seed feeds the deterministic DRBG
* (pq_drbg_firmware.c) which replaces PQClean's randombytes() during keygen.
*
* ed25519 and x25519 are handled separately via mbedtls (see
* key_derivation.c) and are not part of this PQClean component.
*/
#ifndef FIRMWARE_PQ_CRYPTO_H
#define FIRMWARE_PQ_CRYPTO_H
#include <stddef.h>
#include <stdint.h>
/* --- Algorithm identifiers --- */
typedef enum {
FW_PQ_ALG_ML_DSA_65 = 0,
FW_PQ_ALG_SLH_DSA_128S,
FW_PQ_ALG_ML_KEM_768,
FW_PQ_ALG_UNKNOWN
} fw_pq_alg_t;
/* --- Key sizes (compile-time constants, matching PQClean api.h) --- */
#define FW_ML_DSA_65_PUBKEY_LEN 1952
#define FW_ML_DSA_65_PRIVKEY_LEN 4032
#define FW_ML_DSA_65_SIG_LEN 3309
#define FW_SLH_DSA_128S_PUBKEY_LEN 32
#define FW_SLH_DSA_128S_PRIVKEY_LEN 64
#define FW_SLH_DSA_128S_SIG_LEN 7856
#define FW_ML_KEM_768_PUBKEY_LEN 1184
#define FW_ML_KEM_768_PRIVKEY_LEN 2400
#define FW_ML_KEM_768_CIPHERTEXT_LEN 1088
#define FW_ML_KEM_768_SHARED_SECRET_LEN 32
/* --- Key generation (deterministic from seed) --- */
/* Generate an ML-DSA-65 keypair from a 32-byte seed.
* pk must be at least FW_ML_DSA_65_PUBKEY_LEN bytes.
* sk must be at least FW_ML_DSA_65_PRIVKEY_LEN bytes.
* Returns 0 on success, -1 on error. */
int fw_pq_ml_dsa_65_keygen(const uint8_t seed[32],
uint8_t *pk, uint8_t *sk);
/* Generate an SLH-DSA-128s keypair from a 32-byte seed.
* pk must be at least FW_SLH_DSA_128S_PUBKEY_LEN bytes.
* sk must be at least FW_SLH_DSA_128S_PRIVKEY_LEN bytes.
* Returns 0 on success, -1 on error.
* WARNING: SLH-DSA-128s keygen takes 5-30 seconds on ESP32. */
int fw_pq_slh_dsa_128s_keygen(const uint8_t seed[32],
uint8_t *pk, uint8_t *sk);
/* Generate an ML-KEM-768 keypair from a 32-byte seed.
* pk must be at least FW_ML_KEM_768_PUBKEY_LEN bytes.
* sk must be at least FW_ML_KEM_768_PRIVKEY_LEN bytes.
* Returns 0 on success, -1 on error. */
int fw_pq_ml_kem_768_keygen(const uint8_t seed[32],
uint8_t *pk, uint8_t *sk);
/* --- Signing (ML-DSA-65, SLH-DSA-128s) --- */
/* Sign a message with ML-DSA-65.
* sig must be at least FW_ML_DSA_65_SIG_LEN bytes.
* Returns 0 on success, -1 on error. */
int fw_pq_ml_dsa_65_sign(uint8_t *sig, size_t *siglen,
const uint8_t *m, size_t mlen,
const uint8_t *sk);
/* Verify an ML-DSA-65 signature.
* Returns 0 on valid, -1 on invalid. */
int fw_pq_ml_dsa_65_verify(const uint8_t *sig, size_t siglen,
const uint8_t *m, size_t mlen,
const uint8_t *pk);
/* Sign a message with SLH-DSA-128s.
* sig must be at least FW_SLH_DSA_128S_SIG_LEN bytes.
* WARNING: SLH-DSA-128s signing takes 5-30 seconds on ESP32.
* Returns 0 on success, -1 on error. */
int fw_pq_slh_dsa_128s_sign(uint8_t *sig, size_t *siglen,
const uint8_t *m, size_t mlen,
const uint8_t *sk);
/* Verify an SLH-DSA-128s signature.
* Returns 0 on valid, -1 on invalid. */
int fw_pq_slh_dsa_128s_verify(const uint8_t *sig, size_t siglen,
const uint8_t *m, size_t mlen,
const uint8_t *pk);
/* --- KEM (ML-KEM-768) --- */
/* Encapsulate: generate ciphertext + shared secret from a public key.
* Uses real randomness (ESP32 hardware RNG) — not the deterministic DRBG.
* ct must be at least FW_ML_KEM_768_CIPHERTEXT_LEN bytes.
* ss must be at least FW_ML_KEM_768_SHARED_SECRET_LEN bytes.
* Returns 0 on success, -1 on error. */
int fw_pq_ml_kem_768_encaps(uint8_t *ct, uint8_t *ss, const uint8_t *pk);
/* Decapsulate: recover shared secret from secret key + ciphertext.
* ss must be at least FW_ML_KEM_768_SHARED_SECRET_LEN bytes.
* Returns 0 on success, -1 on error. */
int fw_pq_ml_kem_768_decaps(uint8_t *ss, const uint8_t *ct, const uint8_t *sk);
#endif /* FIRMWARE_PQ_CRYPTO_H */

View File

@@ -0,0 +1,60 @@
# CMakeLists.txt — ESP-IDF component for PQClean post-quantum algorithms.
#
# Compiles the three PQ algorithms (ML-DSA-65, SLH-DSA-128s, ML-KEM-768)
# from the shared resources/pqclean/ source tree, using the mbedtls
# crypto backend (crypto_backend_mbedtls.c) for SHA-2/SHA3/SHAKE.
#
# The source files are referenced via relative paths back to the shared
# resources/pqclean/ directory so there is a single source of truth.
#
# mbedtls requirements:
# CONFIG_MBEDTLS_SHA3_C=y (for SHA3-256, SHA3-512)
# CONFIG_MBEDTLS_SHAKE_C=y (for SHAKE-128, SHAKE-256)
# Enable these in menuconfig under Component config -> mbedTLS ->
# Hash functions -> SHA-3 and SHAKE.
set(PQCLEAN_ROOT "${CMAKE_CURRENT_LIST_DIR}/../../../../resources/pqclean")
idf_component_register(
SRCS
"${PQCLEAN_ROOT}/crypto_sign/ml-dsa-65/sign.c"
"${PQCLEAN_ROOT}/crypto_sign/ml-dsa-65/poly.c"
"${PQCLEAN_ROOT}/crypto_sign/ml-dsa-65/ntt.c"
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/sign.c"
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/fors.c"
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/wots.c"
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/hash.c"
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/thash.c"
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/address.c"
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s/utils.c"
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/kem.c"
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/indcpa.c"
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/poly.c"
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/ntt.c"
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/cbd.c"
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/reduce.c"
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/symmetric.c"
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768/verify.c"
"${PQCLEAN_ROOT}/common/fips202.c"
"${PQCLEAN_ROOT}/common/sha2.c"
"${PQCLEAN_ROOT}/common/crypto_backend_mbedtls.c"
"randombytes_mbedtls.c"
"pq_drbg_firmware.c"
INCLUDE_DIRS
"include"
"${PQCLEAN_ROOT}/common"
"${PQCLEAN_ROOT}/crypto_sign/ml-dsa-65"
"${PQCLEAN_ROOT}/crypto_sign/slh-dsa-128s"
"${PQCLEAN_ROOT}/crypto_kem/ml-kem-768"
REQUIRES
mbedtls
)
# Suppress warnings from the PQClean code (it uses C99 patterns that
# trigger -Wextra warnings under ESP-IDF's default flags).
target_compile_options(${COMPONENT_LIB} PRIVATE
-Wno-unused-parameter
-Wno-sign-compare
-Wno-unused-variable
-Wno-unused-but-set-variable
)

View File

@@ -0,0 +1,5 @@
/* ml_dsa_65_api.h — firmware wrapper that includes the real PQClean header. */
#ifndef FIRMWARE_ML_DSA_65_API_WRAPPER_H
#define FIRMWARE_ML_DSA_65_API_WRAPPER_H
#include "../../../../resources/pqclean/crypto_sign/ml-dsa-65/api.h"
#endif

View File

@@ -0,0 +1,5 @@
/* ml_kem_768_api.h — firmware wrapper that includes the real PQClean header. */
#ifndef FIRMWARE_ML_KEM_768_API_WRAPPER_H
#define FIRMWARE_ML_KEM_768_API_WRAPPER_H
#include "../../../../resources/pqclean/crypto_kem/ml-kem-768/api.h"
#endif

View File

@@ -0,0 +1,38 @@
/* pqclean.h — Umbrella include for the ESP32 firmware PQClean component.
*
* Exposes the three post-quantum algorithms (ML-DSA-65, SLH-DSA-128s,
* ML-KEM-768) and the deterministic DRBG used for mnemonic-recoverable
* key generation.
*
* On ESP32 the underlying hash/SHAKE primitives are provided by the
* mbedtls backend (crypto_backend_mbedtls.c) instead of OpenSSL.
*/
#ifndef FIRMWARE_PQCLEAN_H
#define FIRMWARE_PQCLEAN_H
#include <stddef.h>
#include <stdint.h>
/* --- ML-DSA-65 (FIPS 204, lattice signatures) --- */
#include "ml_dsa_65_api.h"
/* --- SLH-DSA-128s (FIPS 205, hash-based signatures) --- */
#include "slh_dsa_128s_api.h"
/* --- ML-KEM-768 (FIPS 203, lattice KEM) --- */
#include "ml_kem_768_api.h"
/* --- Deterministic DRBG (replaces randombytes() for keygen) --- */
/* Initializes the DRBG with a 32-byte mnemonic-derived seed. Subsequent
* randombytes() calls will produce a deterministic byte stream. */
void pq_drbg_init(const unsigned char *seed, size_t seed_len);
/* Zeroizes the DRBG state (call after keygen to wipe sensitive material). */
void pq_drbg_zeroize(void);
/* randombytes() — called by the PQClean algorithm code.
* On firmware this is provided by randombytes_mbedtls.c (deterministic DRBG
* for keygen, or mbedtls_ctr_drbg for real randomness during encaps). */
int randombytes(unsigned char *buf, size_t len);
#endif /* FIRMWARE_PQCLEAN_H */

View File

@@ -0,0 +1,5 @@
/* slh_dsa_128s_api.h — firmware wrapper that includes the real PQClean header. */
#ifndef FIRMWARE_SLH_DSA_128S_API_WRAPPER_H
#define FIRMWARE_SLH_DSA_128S_API_WRAPPER_H
#include "../../../../resources/pqclean/crypto_sign/slh-dsa-128s/api.h"
#endif

View File

@@ -0,0 +1,108 @@
/* pq_drbg_firmware.c — Deterministic PRNG for PQ key generation on ESP32.
*
* Same algorithm as the host's src/pq_drbg.c but uses the crypto backend
* abstraction (which resolves to mbedtls on ESP32) for SHAKE-256 instead
* of OpenSSL EVP. This allows deterministic PQ key generation from a
* mnemonic-derived seed: same seed -> same randombytes output sequence.
*
* The PRNG: SHAKE-256(seed || counter) produces a stream of pseudo-random
* bytes. The counter is a 64-bit little-endian integer that increments
* each time we need more output.
*/
#include <string.h>
#include <stdlib.h>
#include "crypto_backend.h"
/* --- DRBG state --- */
static unsigned char g_seed[32];
static int g_seed_len = 0;
static uint64_t g_counter = 0;
static unsigned char g_buffer[168]; /* SHAKE-256 rate = 136, 168 for safety */
static size_t g_buffer_pos = sizeof(g_buffer);
static int g_initialized = 0;
/* --- internal: squeeze more bytes from SHAKE-256 --- */
static void drbg_refill(void) {
unsigned char seed_block[32 + 8]; /* seed + counter (8 bytes LE) */
memcpy(seed_block, g_seed, (size_t)g_seed_len);
seed_block[g_seed_len + 0] = (unsigned char)(g_counter & 0xFF);
seed_block[g_seed_len + 1] = (unsigned char)((g_counter >> 8) & 0xFF);
seed_block[g_seed_len + 2] = (unsigned char)((g_counter >> 16) & 0xFF);
seed_block[g_seed_len + 3] = (unsigned char)((g_counter >> 24) & 0xFF);
seed_block[g_seed_len + 4] = (unsigned char)((g_counter >> 32) & 0xFF);
seed_block[g_seed_len + 5] = (unsigned char)((g_counter >> 40) & 0xFF);
seed_block[g_seed_len + 6] = (unsigned char)((g_counter >> 48) & 0xFF);
seed_block[g_seed_len + 7] = (unsigned char)((g_counter >> 56) & 0xFF);
crypto_backend_shake256(seed_block, (size_t)g_seed_len + 8,
g_buffer, sizeof(g_buffer));
g_counter++;
g_buffer_pos = 0;
}
/* --- public API --- */
void pq_drbg_init(const unsigned char *seed, size_t seed_len) {
if (seed == NULL || seed_len == 0) {
return;
}
memset(g_seed, 0, sizeof(g_seed));
if (seed_len > sizeof(g_seed)) {
seed_len = sizeof(g_seed);
}
memcpy(g_seed, seed, seed_len);
g_seed_len = (int)sizeof(g_seed); /* always use 32-byte seed (zero-padded) */
g_counter = 0;
g_buffer_pos = sizeof(g_buffer);
g_initialized = 1;
}
void pq_drbg_zeroize(void) {
crypto_backend_cleanse(g_seed, sizeof(g_seed));
crypto_backend_cleanse(g_buffer, sizeof(g_buffer));
g_seed_len = 0;
g_counter = 0;
g_buffer_pos = sizeof(g_buffer);
g_initialized = 0;
}
/* Returns 1 if the DRBG has been initialized (keygen mode), 0 otherwise.
* Used by randombytes_mbedtls.c to decide between deterministic DRBG and
* hardware RNG. */
int pq_drbg_is_initialized(void) {
return g_initialized;
}
/* pq_drbg_randombytes is called by randombytes() below. */
int pq_drbg_randombytes(unsigned char *buf, size_t len) {
if (buf == NULL || !g_initialized) {
return -1;
}
while (len > 0) {
size_t avail;
size_t to_copy;
if (g_buffer_pos >= sizeof(g_buffer)) {
drbg_refill();
if (g_buffer_pos >= sizeof(g_buffer)) {
return -1; /* refill failed */
}
}
avail = sizeof(g_buffer) - g_buffer_pos;
to_copy = (len < avail) ? len : avail;
memcpy(buf, g_buffer + g_buffer_pos, to_copy);
g_buffer_pos += to_copy;
buf += to_copy;
len -= to_copy;
}
return 0;
}

View File

@@ -0,0 +1,40 @@
/* randombytes_mbedtls.c — randombytes() implementation for ESP32 firmware.
*
* PQClean's algorithm code calls randombytes() for:
* 1. Key generation (keygen) — must be deterministic from the mnemonic
* seed so keys are recoverable. The DRBG is initialized via
* pq_drbg_init() before keygen, so randombytes() draws from the
* deterministic stream.
* 2. Encapsulation (ML-KEM enc) — needs real cryptographic randomness.
* When the DRBG is NOT initialized, randombytes() falls back to
* esp_fill_random() which uses the ESP32 hardware RNG.
*
* This dual-mode behavior matches the host build (src/pq_drbg.c) where
* the DRBG is initialized for keygen and randombytes() returns -1 if
* called without initialization. On firmware we allow the fallback to
* hardware RNG for encaps, which is the correct behavior.
*/
#include <string.h>
#include "esp_random.h"
/* Defined in pq_drbg_firmware.c */
extern int pq_drbg_randombytes(unsigned char *buf, size_t len);
/* Check if the DRBG is initialized (declared in pq_drbg_firmware.c).
* We use a helper to avoid exposing the static directly. */
extern int pq_drbg_is_initialized(void);
int randombytes(unsigned char *buf, size_t len) {
if (buf == NULL) {
return -1;
}
/* If the deterministic DRBG is active (keygen mode), use it. */
if (pq_drbg_is_initialized()) {
return pq_drbg_randombytes(buf, len);
}
/* Otherwise, use the ESP32 hardware RNG for real randomness (encaps). */
esp_fill_random(buf, len);
return 0;
}

View File

@@ -4,6 +4,7 @@ idf_component_register(
"display.c"
"mnemonic.c"
"key_derivation.c"
"pq_crypto_firmware.c"
"bech32.c"
"usb_transport.c"
"buttons.c"
@@ -23,6 +24,7 @@ idf_component_register(
REQUIRES
mbedtls
secp256k1
pqclean
json
espressif__esp_tinyusb
espressif__tinyusb

View File

@@ -1,17 +1,25 @@
#include "key_derivation.h"
#include "pq_crypto_firmware.h"
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "esp_random.h"
#include "esp_log.h"
#include "mbedtls/md.h"
#include "mbedtls/ed25519.h"
#include "mbedtls/ecp.h"
#include "mbedtls/pk.h"
#include "secp256k1.h"
#include "secp256k1_extrakeys.h"
#include "secp256k1_schnorrsig.h"
static const char *KD_TAG = "key_derivation";
#define BIP32_HARDENED_FLAG 0x80000000u
typedef struct {
@@ -253,3 +261,316 @@ int schnorr_sign32(const uint8_t privkey[32], const uint8_t msg32[32], uint8_t s
secp256k1_context_destroy(ctx);
return 0;
}
/* ====================================================================
* Phase 7: ed25519, x25519, and post-quantum key derivation
* ==================================================================== */
/* SLIP-0010 all-hardened derivation for ed25519/x25519.
*
* SLIP-0010 uses HMAC-SHA512 with a "ed25519 seed" or curve-specific key
* for the master key, and all derivation steps are hardened (the parent
* private key is prepended to the index data).
*
* For ed25519/x25519, the derived 512-bit HMAC output is split:
* - first 32 bytes = private key (the scalar)
* - last 32 bytes = chain code
*
* The private key IS the ed25519/x25519 secret — no tweak-add is needed
* (unlike secp256k1 BIP-32 where the child priv = parent_priv + HMAC).
*/
/* SLIP-0010 master key from seed: HMAC-SHA512(key="ed25519 seed", data=seed) */
static int slip10_master_from_seed(const uint8_t seed[64],
uint8_t priv[32], uint8_t chain[32]) {
static const uint8_t kEd25519Seed[] = "ed25519 seed";
uint8_t i64[64] = {0};
if (hmac_sha512(kEd25519Seed, sizeof(kEd25519Seed) - 1,
seed, 64, i64) != 0) {
return -1;
}
memcpy(priv, i64, 32);
memcpy(chain, i64 + 32, 32);
memset(i64, 0, sizeof(i64));
return 0;
}
/* SLIP-0010 hardened child derivation:
* HMAC-SHA512(key=chain, data=0x00 || priv || index_be32) */
static int slip10_ckd_priv(const uint8_t parent_priv[32],
const uint8_t parent_chain[32],
uint32_t index,
uint8_t child_priv[32],
uint8_t child_chain[32]) {
uint8_t data[37];
uint8_t i64[64] = {0};
/* Hardened derivation: 0x00 || priv || index (big-endian) */
data[0] = 0x00;
memcpy(data + 1, parent_priv, 32);
data[33] = (uint8_t)((index >> 24) & 0xFF);
data[34] = (uint8_t)((index >> 16) & 0xFF);
data[35] = (uint8_t)((index >> 8) & 0xFF);
data[36] = (uint8_t)(index & 0xFF);
if (hmac_sha512(parent_chain, 32, data, sizeof(data), i64) != 0) {
memset(data, 0, sizeof(data));
return -1;
}
memcpy(child_priv, i64, 32);
memcpy(child_chain, i64 + 32, 32);
memset(data, 0, sizeof(data));
memset(i64, 0, sizeof(i64));
return 0;
}
/* Derive a 32-byte seed via SLIP-0010 all-hardened path.
* path[] is an array of hardened indices (the caller sets the hardened flag).
* Returns the final 32-byte private material in `out_seed`. */
static int slip10_derive_seed(const uint8_t seed[64],
const uint32_t *path, size_t path_len,
uint8_t out_seed[32]) {
uint8_t priv[32], chain[32], next_priv[32], next_chain[32];
size_t i;
if (slip10_master_from_seed(seed, priv, chain) != 0) {
return -1;
}
for (i = 0; i < path_len; i++) {
if (slip10_ckd_priv(priv, chain, path[i],
next_priv, next_chain) != 0) {
memset(priv, 0, sizeof(priv));
memset(chain, 0, sizeof(chain));
return -1;
}
memcpy(priv, next_priv, 32);
memcpy(chain, next_chain, 32);
}
memcpy(out_seed, priv, 32);
memset(priv, 0, sizeof(priv));
memset(chain, 0, sizeof(chain));
memset(next_priv, 0, sizeof(next_priv));
memset(next_chain, 0, sizeof(next_chain));
return 0;
}
/* --- ed25519 --- */
int derive_ed25519_key(const uint8_t seed[64], uint32_t index,
uint8_t privkey[32], uint8_t pubkey[32]) {
/* m/44'/102001'/<index>'/0'/0' — all hardened (SLIP-0010) */
const uint32_t path[5] = {
44u | BIP32_HARDENED_FLAG,
102001u | BIP32_HARDENED_FLAG,
index | BIP32_HARDENED_FLAG,
0u | BIP32_HARDENED_FLAG,
0u | BIP32_HARDENED_FLAG,
};
uint8_t derived_seed[32];
if (seed == NULL || privkey == NULL || pubkey == NULL) {
return -1;
}
if (slip10_derive_seed(seed, path, 5, derived_seed) != 0) {
return -1;
}
/* The SLIP-0010 derived 32 bytes IS the ed25519 private key.
* Use mbedtls to derive the public key. */
memcpy(privkey, derived_seed, 32);
/* mbedtls_ed25519_make_public: derive pub from priv */
/* Note: mbedtls ed25519 API may vary by version. The ESP-IDF mbedtls
* component provides mbedtls_ed25519_make_public (or via the PK API).
* We use the low-level function if available. */
int ret = mbedtls_ed25519_make_public((unsigned char *)pubkey, 32,
(const unsigned char *)privkey, 32);
if (ret != 0) {
ESP_LOGE(KD_TAG, "ed25519 make_public failed: %d", ret);
memset(derived_seed, 0, sizeof(derived_seed));
memset(privkey, 0, 32);
return -1;
}
memset(derived_seed, 0, sizeof(derived_seed));
return 0;
}
int ed25519_sign32(const uint8_t privkey[32], const uint8_t msg32[32],
uint8_t sig64[64]) {
/* mbedtls_ed25519_sign: sign a message (not pre-hashed) */
int ret = mbedtls_ed25519_sign((unsigned char *)sig64, 64,
(const unsigned char *)msg32, 32,
(const unsigned char *)privkey, 32,
NULL, NULL);
if (ret != 0) {
ESP_LOGE(KD_TAG, "ed25519 sign failed: %d", ret);
return -1;
}
return 0;
}
/* --- x25519 --- */
int derive_x25519_key(const uint8_t seed[64], uint32_t index,
uint8_t privkey[32], uint8_t pubkey[32]) {
/* m/44'/102002'/<index>'/0'/0' — all hardened (SLIP-0010) */
const uint32_t path[5] = {
44u | BIP32_HARDENED_FLAG,
102002u | BIP32_HARDENED_FLAG,
index | BIP32_HARDENED_FLAG,
0u | BIP32_HARDENED_FLAG,
0u | BIP32_HARDENED_FLAG,
};
uint8_t derived_seed[32];
mbedtls_ecp_group grp;
mbedtls_mpi d;
mbedtls_ecp_point Q;
int ret;
if (seed == NULL || privkey == NULL || pubkey == NULL) {
return -1;
}
if (slip10_derive_seed(seed, path, 5, derived_seed) != 0) {
return -1;
}
/* The SLIP-0010 derived 32 bytes IS the x25519 private key.
* Clamp it per RFC 7748 and derive the public key via mbedtls ECDH. */
memcpy(privkey, derived_seed, 32);
memset(derived_seed, 0, sizeof(derived_seed));
/* x25519 clamping: priv[0] &= 248, priv[31] &= 127, priv[31] |= 64 */
privkey[0] &= 248;
privkey[31] &= 127;
privkey[31] |= 64;
mbedtls_ecp_group_init(&grp);
mbedtls_mpi_init(&d);
mbedtls_ecp_point_init(&Q);
ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_CURVE25519);
if (ret != 0) {
ESP_LOGE(KD_TAG, "x25519 group load failed: %d", ret);
goto cleanup;
}
ret = mbedtls_mpi_read_binary_le(d, privkey, 32);
if (ret != 0) {
ESP_LOGE(KD_TAG, "x25519 mpi read failed: %d", ret);
goto cleanup;
}
ret = mbedtls_ecp_mul(&grp, &Q, d, &grp.G, NULL, NULL);
if (ret != 0) {
ESP_LOGE(KD_TAG, "x25519 ecp_mul failed: %d", ret);
goto cleanup;
}
/* Serialize the public key as raw 32 bytes (little-endian) */
{
size_t olen = 0;
ret = mbedtls_ecp_point_write_binary(&grp, &Q,
MBEDTLS_ECP_PF_COMPRESSED,
&olen, pubkey, 32);
if (ret != 0 || olen != 32) {
ESP_LOGE(KD_TAG, "x25519 pub serialize failed: %d", ret);
ret = -1;
}
}
cleanup:
mbedtls_ecp_group_free(&grp);
mbedtls_mpi_free(&d);
mbedtls_ecp_point_free(&Q);
return (ret == 0) ? 0 : -1;
}
/* --- ML-DSA-65 --- */
int derive_ml_dsa_65_key(const uint8_t seed[64], uint32_t index,
uint8_t *pk, uint8_t *sk) {
/* m/44'/102003'/<index>'/0'/0' — all hardened (SLIP-0010) -> 32-byte seed */
const uint32_t path[5] = {
44u | BIP32_HARDENED_FLAG,
102003u | BIP32_HARDENED_FLAG,
index | BIP32_HARDENED_FLAG,
0u | BIP32_HARDENED_FLAG,
0u | BIP32_HARDENED_FLAG,
};
uint8_t pq_seed[32];
if (seed == NULL || pk == NULL || sk == NULL) {
return -1;
}
if (slip10_derive_seed(seed, path, 5, pq_seed) != 0) {
return -1;
}
int ret = fw_pq_ml_dsa_65_keygen(pq_seed, pk, sk);
memset(pq_seed, 0, sizeof(pq_seed));
return ret;
}
/* --- SLH-DSA-128s --- */
int derive_slh_dsa_128s_key(const uint8_t seed[64], uint32_t index,
uint8_t *pk, uint8_t *sk) {
/* m/44'/102004'/<index>'/0'/0' — all hardened (SLIP-0010) -> 32-byte seed */
const uint32_t path[5] = {
44u | BIP32_HARDENED_FLAG,
102004u | BIP32_HARDENED_FLAG,
index | BIP32_HARDENED_FLAG,
0u | BIP32_HARDENED_FLAG,
0u | BIP32_HARDENED_FLAG,
};
uint8_t pq_seed[32];
if (seed == NULL || pk == NULL || sk == NULL) {
return -1;
}
if (slip10_derive_seed(seed, path, 5, pq_seed) != 0) {
return -1;
}
ESP_LOGW(KD_TAG, "SLH-DSA-128s keygen: this takes 5-30 seconds on ESP32");
int ret = fw_pq_slh_dsa_128s_keygen(pq_seed, pk, sk);
memset(pq_seed, 0, sizeof(pq_seed));
return ret;
}
/* --- ML-KEM-768 --- */
int derive_ml_kem_768_key(const uint8_t seed[64], uint32_t index,
uint8_t *pk, uint8_t *sk) {
/* m/44'/102005'/<index>'/0'/0' — all hardened (SLIP-0010) -> 32-byte seed */
const uint32_t path[5] = {
44u | BIP32_HARDENED_FLAG,
102005u | BIP32_HARDENED_FLAG,
index | BIP32_HARDENED_FLAG,
0u | BIP32_HARDENED_FLAG,
0u | BIP32_HARDENED_FLAG,
};
uint8_t pq_seed[32];
if (seed == NULL || pk == NULL || sk == NULL) {
return -1;
}
if (slip10_derive_seed(seed, path, 5, pq_seed) != 0) {
return -1;
}
int ret = fw_pq_ml_kem_768_keygen(pq_seed, pk, sk);
memset(pq_seed, 0, sizeof(pq_seed));
return ret;
}

View File

@@ -1,7 +1,61 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
/* --- secp256k1 (Nostr, existing) --- */
int derive_nostr_key(const uint8_t seed[64], uint8_t privkey[32], uint8_t pubkey[32]);
int derive_nostr_key_index(const uint8_t seed[64], uint32_t nostr_index, uint8_t privkey[32], uint8_t pubkey[32]);
int schnorr_sign32(const uint8_t privkey[32], const uint8_t msg32[32], uint8_t sig64[64]);
/* --- ed25519 (SSH signatures) --- */
/* Derives an ed25519 keypair from the mnemonic seed using SLIP-0010
* all-hardened derivation: m/44'/102001'/<n>'/0'/0'
* privkey: 32-byte ed25519 private scalar
* pubkey: 32-byte ed25519 public key
* Returns 0 on success, -1 on error. */
int derive_ed25519_key(const uint8_t seed[64], uint32_t index,
uint8_t privkey[32], uint8_t pubkey[32]);
/* Signs a 32-byte message digest with ed25519.
* sig: 64-byte ed25519 signature
* Returns 0 on success, -1 on error. */
int ed25519_sign32(const uint8_t privkey[32], const uint8_t msg32[32],
uint8_t sig64[64]);
/* --- x25519 (age encryption / key agreement) --- */
/* Derives an x25519 keypair from the mnemonic seed using SLIP-0010
* all-hardened derivation: m/44'/102002'/<n>'/0'/0'
* privkey: 32-byte x25519 private scalar
* pubkey: 32-byte x25519 public key
* Returns 0 on success, -1 on error. */
int derive_x25519_key(const uint8_t seed[64], uint32_t index,
uint8_t privkey[32], uint8_t pubkey[32]);
/* --- ML-DSA-65 (post-quantum signatures, FIPS 204) --- */
/* Derives an ML-DSA-65 keypair from the mnemonic seed.
* Path: m/44'/102003'/<n>'/0'/0' -> 32-byte seed -> PQClean keygen
* pk: FW_ML_DSA_65_PUBKEY_LEN (1952) bytes — caller must allocate
* sk: FW_ML_DSA_65_PRIVKEY_LEN (4032) bytes — caller must allocate
* Returns 0 on success, -1 on error. */
int derive_ml_dsa_65_key(const uint8_t seed[64], uint32_t index,
uint8_t *pk, uint8_t *sk);
/* --- SLH-DSA-128s (post-quantum hash-based signatures, FIPS 205) --- */
/* Derives an SLH-DSA-128s keypair from the mnemonic seed.
* Path: m/44'/102004'/<n>'/0'/0' -> 32-byte seed -> PQClean keygen
* pk: FW_SLH_DSA_128S_PUBKEY_LEN (32) bytes
* sk: FW_SLH_DSA_128S_PRIVKEY_LEN (64) bytes
* WARNING: Takes 5-30 seconds on ESP32.
* Returns 0 on success, -1 on error. */
int derive_slh_dsa_128s_key(const uint8_t seed[64], uint32_t index,
uint8_t *pk, uint8_t *sk);
/* --- ML-KEM-768 (post-quantum KEM, FIPS 203) --- */
/* Derives an ML-KEM-768 keypair from the mnemonic seed.
* Path: m/44'/102005'/<n>'/0'/0' -> 32-byte seed -> PQClean keygen
* pk: FW_ML_KEM_768_PUBKEY_LEN (1184) bytes — caller must allocate
* sk: FW_ML_KEM_768_PRIVKEY_LEN (2400) bytes — caller must allocate
* Returns 0 on success, -1 on error. */
int derive_ml_kem_768_key(const uint8_t seed[64], uint32_t index,
uint8_t *pk, uint8_t *sk);

View File

@@ -0,0 +1,171 @@
/* pq_crypto_firmware.c — Post-quantum crypto wrappers for ESP32 firmware.
*
* Wraps the PQClean algorithm API (via the pqclean component) with
* firmware-friendly functions that handle the deterministic DRBG setup
* for keygen and provide clean sign/verify/encaps/decaps interfaces.
*
* The PQ key buffers are large (ML-DSA-65 priv = 4032 bytes, SLH-DSA-128s
* sig = 7856 bytes). Callers must allocate these on the heap or as static
* buffers — stack allocation on ESP32 (8KB task stack default) will overflow
* for the larger buffers.
*/
#include "pq_crypto_firmware.h"
#include "pqclean.h"
#include <stdlib.h>
#include <string.h>
/* --- Key generation (deterministic from seed) --- */
int fw_pq_ml_dsa_65_keygen(const uint8_t seed[32],
uint8_t *pk, uint8_t *sk) {
if (seed == NULL || pk == NULL || sk == NULL) {
return -1;
}
/* Initialize the deterministic DRBG with the mnemonic-derived seed */
pq_drbg_init(seed, 32);
/* Run PQClean keygen — randombytes() draws from the DRBG */
int ret = crypto_sign_keypair(pk, sk);
/* Wipe the DRBG state — the seed material is sensitive */
pq_drbg_zeroize();
return ret;
}
int fw_pq_slh_dsa_128s_keygen(const uint8_t seed[32],
uint8_t *pk, uint8_t *sk) {
if (seed == NULL || pk == NULL || sk == NULL) {
return -1;
}
pq_drbg_init(seed, 32);
/* WARNING: This call takes 5-30 seconds on ESP32 due to the
* hypertree construction (7 layers of WOTS+ + Merkle trees). */
int ret = slh_dsa_128s_crypto_sign_keypair(pk, sk);
pq_drbg_zeroize();
return ret;
}
int fw_pq_ml_kem_768_keygen(const uint8_t seed[32],
uint8_t *pk, uint8_t *sk) {
if (seed == NULL || pk == NULL || sk == NULL) {
return -1;
}
pq_drbg_init(seed, 32);
int ret = crypto_kem_keypair(pk, sk);
pq_drbg_zeroize();
return ret;
}
/* --- Signing --- */
int fw_pq_ml_dsa_65_sign(uint8_t *sig, size_t *siglen,
const uint8_t *m, size_t mlen,
const uint8_t *sk) {
if (sig == NULL || siglen == NULL || m == NULL || sk == NULL) {
return -1;
}
return crypto_sign(sig, siglen, m, mlen, sk);
}
int fw_pq_ml_dsa_65_verify(const uint8_t *sig, size_t siglen,
const uint8_t *m, size_t mlen,
const uint8_t *pk) {
if (sig == NULL || m == NULL || pk == NULL) {
return -1;
}
/* crypto_sign_open expects (m_out, mlen_out, sm, smlen, pk) where sm
* is the signed message. For detached signatures we reconstruct: the
* PQClean API uses crypto_sign_open with sm = sig || m. */
/* For firmware use, we provide a simple verify by re-signing is not
* possible (non-deterministic). The PQClean crypto_sign_open expects
* the concatenated sig||msg format. Callers should use the PQClean
* API directly for verification, or we build the sm buffer here. */
uint8_t *sm = (uint8_t *)malloc(siglen + mlen);
if (sm == NULL) {
return -1;
}
memcpy(sm, sig, siglen);
memcpy(sm + siglen, m, mlen);
uint8_t *m_out = (uint8_t *)malloc(mlen);
if (m_out == NULL) {
free(sm);
return -1;
}
size_t mlen_out = 0;
int ret = crypto_sign_open(m_out, &mlen_out, sm, siglen + mlen, pk);
free(sm);
free(m_out);
return ret;
}
int fw_pq_slh_dsa_128s_sign(uint8_t *sig, size_t *siglen,
const uint8_t *m, size_t mlen,
const uint8_t *sk) {
if (sig == NULL || siglen == NULL || m == NULL || sk == NULL) {
return -1;
}
/* WARNING: This call takes 5-30 seconds on ESP32. */
return slh_dsa_128s_crypto_sign(sig, siglen, m, mlen, sk);
}
int fw_pq_slh_dsa_128s_verify(const uint8_t *sig, size_t siglen,
const uint8_t *m, size_t mlen,
const uint8_t *pk) {
if (sig == NULL || m == NULL || pk == NULL) {
return -1;
}
uint8_t *sm = (uint8_t *)malloc(siglen + mlen);
if (sm == NULL) {
return -1;
}
memcpy(sm, sig, siglen);
memcpy(sm + siglen, m, mlen);
uint8_t *m_out = (uint8_t *)malloc(mlen);
if (m_out == NULL) {
free(sm);
return -1;
}
size_t mlen_out = 0;
int ret = slh_dsa_128s_crypto_sign_open(m_out, &mlen_out, sm,
siglen + mlen, pk);
free(sm);
free(m_out);
return ret;
}
/* --- KEM --- */
int fw_pq_ml_kem_768_encaps(uint8_t *ct, uint8_t *ss, const uint8_t *pk) {
if (ct == NULL || ss == NULL || pk == NULL) {
return -1;
}
/* encaps uses real randomness (hardware RNG) — the DRBG is not
* initialized, so randombytes() falls back to esp_fill_random(). */
return crypto_kem_enc(ct, ss, pk);
}
int fw_pq_ml_kem_768_decaps(uint8_t *ss, const uint8_t *ct, const uint8_t *sk) {
if (ss == NULL || ct == NULL || sk == NULL) {
return -1;
}
return crypto_kem_dec(ss, ct, sk);
}

View File

@@ -0,0 +1,110 @@
/* pq_crypto_firmware.h — Post-quantum crypto wrappers for ESP32 firmware.
*
* Provides firmware-friendly wrappers around the PQClean algorithms:
* - ML-DSA-65 (FIPS 204 signatures)
* - SLH-DSA-128s (FIPS 205 hash-based signatures)
* - ML-KEM-768 (FIPS 203 key encapsulation)
*
* Key generation is deterministic from a 32-byte seed (derived from the
* mnemonic via BIP-32/HMAC-SHA512). The seed feeds the deterministic DRBG
* (pq_drbg_firmware.c) which replaces PQClean's randombytes() during keygen.
*
* ed25519 and x25519 are handled separately via mbedtls (see
* key_derivation.c) and are not part of this PQClean component.
*/
#ifndef FIRMWARE_PQ_CRYPTO_H
#define FIRMWARE_PQ_CRYPTO_H
#include <stddef.h>
#include <stdint.h>
/* --- Algorithm identifiers --- */
typedef enum {
FW_PQ_ALG_ML_DSA_65 = 0,
FW_PQ_ALG_SLH_DSA_128S,
FW_PQ_ALG_ML_KEM_768,
FW_PQ_ALG_UNKNOWN
} fw_pq_alg_t;
/* --- Key sizes (compile-time constants, matching PQClean api.h) --- */
#define FW_ML_DSA_65_PUBKEY_LEN 1952
#define FW_ML_DSA_65_PRIVKEY_LEN 4032
#define FW_ML_DSA_65_SIG_LEN 3309
#define FW_SLH_DSA_128S_PUBKEY_LEN 32
#define FW_SLH_DSA_128S_PRIVKEY_LEN 64
#define FW_SLH_DSA_128S_SIG_LEN 7856
#define FW_ML_KEM_768_PUBKEY_LEN 1184
#define FW_ML_KEM_768_PRIVKEY_LEN 2400
#define FW_ML_KEM_768_CIPHERTEXT_LEN 1088
#define FW_ML_KEM_768_SHARED_SECRET_LEN 32
/* --- Key generation (deterministic from seed) --- */
/* Generate an ML-DSA-65 keypair from a 32-byte seed.
* pk must be at least FW_ML_DSA_65_PUBKEY_LEN bytes.
* sk must be at least FW_ML_DSA_65_PRIVKEY_LEN bytes.
* Returns 0 on success, -1 on error. */
int fw_pq_ml_dsa_65_keygen(const uint8_t seed[32],
uint8_t *pk, uint8_t *sk);
/* Generate an SLH-DSA-128s keypair from a 32-byte seed.
* pk must be at least FW_SLH_DSA_128S_PUBKEY_LEN bytes.
* sk must be at least FW_SLH_DSA_128S_PRIVKEY_LEN bytes.
* Returns 0 on success, -1 on error.
* WARNING: SLH-DSA-128s keygen takes 5-30 seconds on ESP32. */
int fw_pq_slh_dsa_128s_keygen(const uint8_t seed[32],
uint8_t *pk, uint8_t *sk);
/* Generate an ML-KEM-768 keypair from a 32-byte seed.
* pk must be at least FW_ML_KEM_768_PUBKEY_LEN bytes.
* sk must be at least FW_ML_KEM_768_PRIVKEY_LEN bytes.
* Returns 0 on success, -1 on error. */
int fw_pq_ml_kem_768_keygen(const uint8_t seed[32],
uint8_t *pk, uint8_t *sk);
/* --- Signing (ML-DSA-65, SLH-DSA-128s) --- */
/* Sign a message with ML-DSA-65.
* sig must be at least FW_ML_DSA_65_SIG_LEN bytes.
* Returns 0 on success, -1 on error. */
int fw_pq_ml_dsa_65_sign(uint8_t *sig, size_t *siglen,
const uint8_t *m, size_t mlen,
const uint8_t *sk);
/* Verify an ML-DSA-65 signature.
* Returns 0 on valid, -1 on invalid. */
int fw_pq_ml_dsa_65_verify(const uint8_t *sig, size_t siglen,
const uint8_t *m, size_t mlen,
const uint8_t *pk);
/* Sign a message with SLH-DSA-128s.
* sig must be at least FW_SLH_DSA_128S_SIG_LEN bytes.
* WARNING: SLH-DSA-128s signing takes 5-30 seconds on ESP32.
* Returns 0 on success, -1 on error. */
int fw_pq_slh_dsa_128s_sign(uint8_t *sig, size_t *siglen,
const uint8_t *m, size_t mlen,
const uint8_t *sk);
/* Verify an SLH-DSA-128s signature.
* Returns 0 on valid, -1 on invalid. */
int fw_pq_slh_dsa_128s_verify(const uint8_t *sig, size_t siglen,
const uint8_t *m, size_t mlen,
const uint8_t *pk);
/* --- KEM (ML-KEM-768) --- */
/* Encapsulate: generate ciphertext + shared secret from a public key.
* Uses real randomness (ESP32 hardware RNG) — not the deterministic DRBG.
* ct must be at least FW_ML_KEM_768_CIPHERTEXT_LEN bytes.
* ss must be at least FW_ML_KEM_768_SHARED_SECRET_LEN bytes.
* Returns 0 on success, -1 on error. */
int fw_pq_ml_kem_768_encaps(uint8_t *ct, uint8_t *ss, const uint8_t *pk);
/* Decapsulate: recover shared secret from secret key + ciphertext.
* ss must be at least FW_ML_KEM_768_SHARED_SECRET_LEN bytes.
* Returns 0 on success, -1 on error. */
int fw_pq_ml_kem_768_decaps(uint8_t *ss, const uint8_t *ct, const uint8_t *sk);
#endif /* FIRMWARE_PQ_CRYPTO_H */