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:
@@ -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**: 5–30 seconds
|
||||
- **Signing**: 5–30 seconds
|
||||
- **Verification**: 0.5–2 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, ...);
|
||||
```
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 */
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
171
firmware/cyd_esp32_2432s028/main/pq_crypto_firmware.c
Normal file
171
firmware/cyd_esp32_2432s028/main/pq_crypto_firmware.c
Normal 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);
|
||||
}
|
||||
110
firmware/cyd_esp32_2432s028/main/pq_crypto_firmware.h
Normal file
110
firmware/cyd_esp32_2432s028/main/pq_crypto_firmware.h
Normal 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 */
|
||||
60
firmware/feather_s3_tft/components/pqclean/CMakeLists.txt
Normal file
60
firmware/feather_s3_tft/components/pqclean/CMakeLists.txt
Normal 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
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
38
firmware/feather_s3_tft/components/pqclean/include/pqclean.h
Normal file
38
firmware/feather_s3_tft/components/pqclean/include/pqclean.h
Normal 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 */
|
||||
@@ -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
|
||||
108
firmware/feather_s3_tft/components/pqclean/pq_drbg_firmware.c
Normal file
108
firmware/feather_s3_tft/components/pqclean/pq_drbg_firmware.c
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
171
firmware/feather_s3_tft/main/pq_crypto_firmware.c
Normal file
171
firmware/feather_s3_tft/main/pq_crypto_firmware.c
Normal 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);
|
||||
}
|
||||
110
firmware/feather_s3_tft/main/pq_crypto_firmware.h
Normal file
110
firmware/feather_s3_tft/main/pq_crypto_firmware.h
Normal 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 */
|
||||
Reference in New Issue
Block a user