Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ea3e60391 | ||
|
|
ac2e6347a2 | ||
|
|
d7369646fb | ||
|
|
b1f8076b1d |
+122
-4
@@ -112,12 +112,130 @@ These constants are for the specific panel we calibrated. If you swap a
|
||||
display module, re-run [`firmware/teensy41/touch_cal/touch_cal.ino`](touch_cal/touch_cal.ino)
|
||||
and paste the new constants.
|
||||
|
||||
## Build (planned)
|
||||
## Build
|
||||
|
||||
```bash
|
||||
# Arduino CLI / Teensyduino
|
||||
arduino-cli compile --fqbn teensy:avr:teensy41 firmware/teensy41
|
||||
arduino-cli upload -p /dev/ttyACM0 --fqbn teensy:avr:teensy41 firmware/teensy41
|
||||
# Arduino CLI / Teensyduino — uses the custom linker script
|
||||
bash firmware/teensy41/build_signer.sh # compile only
|
||||
bash firmware/teensy41/build_signer.sh --flash # compile + upload
|
||||
bash firmware/teensy41/build_signer.sh --test # compile + upload + run tests
|
||||
```
|
||||
|
||||
The build uses a custom linker script
|
||||
([`signer/imxrt1062_t41_flashmem.ld`](signer/imxrt1062_t41_flashmem.ld)) that
|
||||
routes crypto and SdFat code to FLASH (off-chip QSPI) instead of ITCM
|
||||
(tightly-coupled RAM), freeing the limited FlexRAM for stack. See the memory
|
||||
management section below for details.
|
||||
|
||||
See the port plan: [`plans/teensy41_signer_port.md`](../../plans/teensy41_signer_port.md).
|
||||
|
||||
## Memory management
|
||||
|
||||
The Teensy 4.1's NXP i.MX RT1062 has a unique FlexRAM architecture that makes
|
||||
memory budgeting the central engineering challenge of this firmware. This
|
||||
section documents the layout, the custom linker script, and the build-time
|
||||
stack gauge that catches overflows before they become mystery crashes.
|
||||
|
||||
### The FlexRAM problem
|
||||
|
||||
The i.MX RT1062 has three RAM regions:
|
||||
|
||||
```
|
||||
FLASH 8 MB (off-chip QSPI, @ 0x60000000) — code + rodata, slow but huge
|
||||
FLEXRAM 512 KB (on-chip, split ITCM/DTCM) — fast, tiny, THE BOTTLENECK
|
||||
RAM2 512 KB (OCRAM, @ 0x20200000) — DMAMEM statics + malloc heap
|
||||
ERAM 0 MB (PSRAM pads empty) — not populated on the Teensy 4.1
|
||||
```
|
||||
|
||||
The 512 KB of FlexRAM is divided into **16 banks of 32 KB** that are split
|
||||
between **ITCM** (instruction tightly-coupled memory, runs code at zero wait
|
||||
state) and **DTCM** (data tightly-coupled memory, holds `.data` + `.bss` +
|
||||
**the stack**). The split is computed at **boot time** by the Teensy boot ROM
|
||||
from a formula in the linker script:
|
||||
|
||||
```ld
|
||||
_itcm_block_count = (SIZEOF(.text.itcm) + SIZEOF(.ARM.exidx) + 0x7FFF) >> 15;
|
||||
_estack = ORIGIN(DTCM) + ((16 - _itcm_block_count) << 15);
|
||||
```
|
||||
|
||||
**Every 32 KB bank given to code is taken away from the stack.** If ITCM code
|
||||
grows past a 32 KB boundary, a whole bank is stolen from DTCM, and the stack
|
||||
shrinks by 32 KB. The linker cannot detect this because the split happens at
|
||||
reset, not link time — an over-committed DTCM links cleanly and then hard-faults
|
||||
on boot.
|
||||
|
||||
### The custom linker script
|
||||
|
||||
[`signer/imxrt1062_t41_flashmem.ld`](signer/imxrt1062_t41_flashmem.ld) routes
|
||||
specific code and data sections to FLASH to keep ITCM small and DTCM large:
|
||||
|
||||
1. **Crypto code → FLASH**: secp256k1, ed25519, x25519, PQClean (ML-DSA-65,
|
||||
ML-KEM-768, SLH-DSA-128s), nostr_utils — all routed via per-object-file
|
||||
rules (`*secp256k1.c.o(.text*)`, etc.). These run slightly slower from
|
||||
FLASH but the stack headroom is the critical constraint.
|
||||
|
||||
2. **SdFat library → FLASH**: the FAT filesystem layer (FatFile, FatPartition,
|
||||
FatVolume, etc.) is routed to FLASH. The SDIO driver (SdioCard, SdioTeensy)
|
||||
stays in ITCM for fast interrupt response.
|
||||
|
||||
3. **`.rodata` → FLASH** (v0.1.6): read-only data (const tables, string
|
||||
literals, BIP-39 wordlist, LVGL fonts, PQClean constants) is routed to
|
||||
FLASH via `*(EXCLUDE_FILE(*ed25519.c.o) .rodata*)`. This reclaimed **124 KB
|
||||
of DTCM** (`.data` went from 131 KB to 6.8 KB), increasing free stack from
|
||||
5,984 bytes to **130,912 bytes**.
|
||||
|
||||
The `EXCLUDE_FILE(*ed25519.c.o)` exception is critical: the ed25519 base
|
||||
point constants (`ed_K`, `ed_X`, `ed_Y`) must stay in DTCM — moving them to
|
||||
FLASH produces an all-zeros pubkey (a regression documented in the linker
|
||||
script comments).
|
||||
|
||||
### Build-time stack gauge
|
||||
|
||||
[`check_stack.sh`](check_stack.sh) parses the ELF's section sizes after
|
||||
compilation and computes the same ITCM/DTCM split the boot ROM will perform.
|
||||
It **fails the build** if free stack would be below 16 KB:
|
||||
|
||||
```
|
||||
=== Teensy 4.1 FlexRAM stack gauge ===
|
||||
.text.itcm : 355056 bytes -> 11 banks (360448 bytes)
|
||||
.data (DTCM) : 6848 bytes
|
||||
.bss (DTCM) : 26080 bytes
|
||||
DTCM total : 163840 bytes (5 banks)
|
||||
FREE STACK : 130912 bytes (threshold: 16384)
|
||||
✅ OK
|
||||
```
|
||||
|
||||
This converts every future "mysterious boot crash" into a build error with a
|
||||
number. The gauge is wired into [`build_signer.sh`](build_signer.sh) and runs
|
||||
automatically after every compile.
|
||||
|
||||
### Current memory layout (v0.1.6)
|
||||
|
||||
```
|
||||
FLEXRAM 512 KB — 16 banks
|
||||
┌──────────────────────────────────────────────┬────────────────────────────────────────┐
|
||||
│ ITCM 11 banks = 352 KB │ DTCM 5 banks = 160 KB │
|
||||
│ code: 355 KB (signer + SDIO driver) │ .data: 6.8 KB (writable globals) │
|
||||
│ │ .bss: 25.4 KB (zero-init globals) │
|
||||
│ │ FREE STACK: 130.9 KB ✅ │
|
||||
└──────────────────────────────────────────────┴────────────────────────────────────────┘
|
||||
|
||||
RAM2 / OCRAM 512 KB
|
||||
┌──────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ .bss.dma: 413.6 KB (LVGL draw buffers, crypto DMAMEM workspaces) heap: 110.7 KB free │
|
||||
└──────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
FLASH 7936 KB
|
||||
┌──────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Used: ~1.6 MB (crypto code + SdFat + .rodata + ITCM/DTCM load images) Free: ~6.3 MB │
|
||||
└──────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### History
|
||||
|
||||
The memory budget was the direct cause of six versions of crash-fixing
|
||||
(v0.1.1–v0.1.6). The v0.1.6 `.rodata` → FLASH move was the largest single
|
||||
improvement, and it also enabled the SD-card OTP pad (which requires SdFat,
|
||||
adding ~7 KB of ITCM code that would have overflowed DTCM without the rodata
|
||||
reclamation). See [`plans/teensy41_memory_evaluation.md`](../../plans/teensy41_memory_evaluation.md)
|
||||
for the full analysis.
|
||||
|
||||
@@ -41,12 +41,32 @@ if [ -f "$LINKER_SCRIPT" ]; then
|
||||
arduino-cli compile \
|
||||
--fqbn "$FQBN" \
|
||||
--build-property "build.flags.ld=${LINKER_FLAG}" \
|
||||
"$SIGNER_DIR"
|
||||
"$SIGNER_DIR" || true # size-determination step may error with custom .ld
|
||||
else
|
||||
echo "[build] WARNING: Linker script not found at $LINKER_SCRIPT — using default."
|
||||
arduino-cli compile --fqbn "$FQBN" "$SIGNER_DIR"
|
||||
fi
|
||||
|
||||
# --- Step 2b: FlexRAM stack gauge ---
|
||||
# arduino-cli's "Error while determining sketch size" with a custom linker
|
||||
# script is non-fatal (the ELF is produced), but it means we lose the built-in
|
||||
# memory report. Run our own gauge against the ELF to catch DTCM stack
|
||||
# overflows that the linker cannot detect (the ITCM/DTCM split is computed at
|
||||
# boot, not link time).
|
||||
ELF_FILE=$(find "$HOME/.cache/arduino/sketches" -name "signer.ino.elf" -newer "$LINKER_SCRIPT" 2>/dev/null | head -1)
|
||||
if [ -z "$ELF_FILE" ]; then
|
||||
ELF_FILE=$(find "$HOME/.cache/arduino/sketches" -name "signer.ino.elf" 2>/dev/null | head -1)
|
||||
fi
|
||||
if [ -n "$ELF_FILE" ] && [ -f "$ELF_FILE" ]; then
|
||||
echo "[build] Running FlexRAM stack gauge..."
|
||||
bash "$SCRIPT_DIR/check_stack.sh" "$ELF_FILE" 16384 || {
|
||||
echo "[build] ❌ Stack gauge FAILED — refusing to flash. See check_stack.sh output above."
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
echo "[build] WARNING: could not find signer.ino.elf for stack gauge — skipping."
|
||||
fi
|
||||
|
||||
echo "[build] Compilation successful."
|
||||
|
||||
# --- Step 3: Find the Teensy port ---
|
||||
|
||||
Executable
+107
@@ -0,0 +1,107 @@
|
||||
#!/bin/bash
|
||||
# check_stack.sh — Teensy 4.1 FlexRAM stack gauge.
|
||||
#
|
||||
# Computes the same ITCM/DTCM split the i.MX RT1062 boot ROM will perform, and
|
||||
# fails if the resulting free stack is below a threshold. The linker cannot
|
||||
# catch this because the split is computed at reset from _itcm_block_count,
|
||||
# not at link time.
|
||||
#
|
||||
# Usage: check_stack.sh <signer.ino.elf> [min_stack_bytes]
|
||||
# min_stack_bytes defaults to 16384 (16 KB).
|
||||
#
|
||||
# Exits 0 if the stack is above the threshold, 1 if below (with a diagnostic).
|
||||
#
|
||||
# The arithmetic (from imxrt1062_t41_flashmem.ld:215-217):
|
||||
# itcm_banks = ceil((.text.itcm + .ARM.exidx) / 32768)
|
||||
# dtcm_total = (16 - itcm_banks) * 32768
|
||||
# free_stack = dtcm_total - .data - .bss
|
||||
# _estack = ORIGIN(DTCM) + dtcm_total (stack grows down from here)
|
||||
#
|
||||
# Note: .bss here is the DTCM .bss (RAM1), NOT .bss.dma (RAM2/OCRAM). The
|
||||
# .bss.dma section lives in a separate 512 KB region and does not affect the
|
||||
# stack.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ELF="${1:-}"
|
||||
MIN_STACK="${2:-16384}"
|
||||
|
||||
if [ -z "$ELF" ] || [ ! -f "$ELF" ]; then
|
||||
echo "check_stack: usage: $0 <signer.ino.elf> [min_stack_bytes]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Locate the arm-none-eabi-size tool from the Teensy toolchain.
|
||||
SIZE_BIN=""
|
||||
for candidate in \
|
||||
/home/user/.arduino15/packages/teensy/tools/teensy-compile/*/arm/bin/arm-none-eabi-size \
|
||||
"$(dirname "$0")/../.arduino15/packages/teensy/tools/teensy-compile/*/arm/bin/arm-none-eabi-size"; do
|
||||
if [ -x "$candidate" ]; then SIZE_BIN="$candidate"; break; fi
|
||||
done
|
||||
# Fall back to PATH
|
||||
if [ -z "$SIZE_BIN" ]; then SIZE_BIN="$(command -v arm-none-eabi-size || true)"; fi
|
||||
if [ -z "$SIZE_BIN" ]; then
|
||||
echo "check_stack: cannot find arm-none-eabi-size" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# We need per-section sizes, not the aggregate. Use objdump -h.
|
||||
OBJDUMP_BIN="${SIZE_BIN%size}objdump"
|
||||
|
||||
# Extract section sizes (in bytes, decimal) by name.
|
||||
get_section_size() {
|
||||
local section="$1"
|
||||
# objdump -h prints: idx Name Size VMA LMA File-off Al
|
||||
"$OBJDUMP_BIN" -h "$ELF" 2>/dev/null \
|
||||
| awk -v sec="$section" '$2 == sec { print strtonum("0x"$3); exit }'
|
||||
}
|
||||
|
||||
TEXT_ITCM=$(get_section_size ".text.itcm")
|
||||
ARM_EXIDX=$(get_section_size ".ARM.exidx")
|
||||
DATA_SEC=$(get_section_size ".data")
|
||||
BSS_SEC=$(get_section_size ".bss")
|
||||
BSS_DMA=$(get_section_size ".bss.dma")
|
||||
|
||||
# Guard against missing sections (e.g. exidx may be 0/absent).
|
||||
: "${TEXT_ITCM:=0}"
|
||||
: "${ARM_EXIDX:=0}"
|
||||
: "${DATA_SEC:=0}"
|
||||
: "${BSS_SEC:=0}"
|
||||
: "${BSS_DMA:=0}"
|
||||
|
||||
BANK=32768
|
||||
ITCM_BYTES=$(( TEXT_ITCM + ARM_EXIDX ))
|
||||
ITCM_BANKS=$(( (ITCM_BYTES + BANK - 1) / BANK ))
|
||||
DTCM_TOTAL=$(( (16 - ITCM_BANKS) * BANK ))
|
||||
DATA_BSS=$(( DATA_SEC + BSS_SEC ))
|
||||
FREE_STACK=$(( DTCM_TOTAL - DATA_BSS ))
|
||||
|
||||
# Also report RAM2 (OCRAM) usage for completeness.
|
||||
RAM2_TOTAL=$(( 512 * 1024 ))
|
||||
RAM2_FREE=$(( RAM2_TOTAL - BSS_DMA ))
|
||||
|
||||
echo "=== Teensy 4.1 FlexRAM stack gauge ==="
|
||||
echo " .text.itcm : $(printf '%7d' $TEXT_ITCM) bytes"
|
||||
echo " .ARM.exidx : $(printf '%7d' $ARM_EXIDX) bytes"
|
||||
echo " ITCM total : $(printf '%7d' $ITCM_BYTES) bytes -> $ITCM_BANKS banks ($(( ITCM_BANKS * BANK )) bytes)"
|
||||
echo " .data (DTCM) : $(printf '%7d' $DATA_SEC) bytes"
|
||||
echo " .bss (DTCM) : $(printf '%7d' $BSS_SEC) bytes"
|
||||
echo " DTCM total : $(printf '%7d' $DTCM_TOTAL) bytes ($(( 16 - ITCM_BANKS )) banks)"
|
||||
echo " data + bss : $(printf '%7d' $DATA_BSS) bytes"
|
||||
echo " FREE STACK : $(printf '%7d' $FREE_STACK) bytes (threshold: $MIN_STACK)"
|
||||
echo " ---"
|
||||
echo " .bss.dma RAM2: $(printf '%7d' $BSS_DMA) / $RAM2_TOTAL bytes (free: $RAM2_FREE)"
|
||||
|
||||
if [ "$FREE_STACK" -lt "$MIN_STACK" ]; then
|
||||
echo ""
|
||||
echo " ❌ FAIL: free stack $FREE_STACK < threshold $MIN_STACK"
|
||||
echo " The boot ROM will allocate $ITCM_BANKS ITCM banks, leaving only"
|
||||
echo " $DTCM_TOTAL bytes of DTCM. .data+.bss needs $DATA_BSS bytes."
|
||||
echo " Reduce ITCM (route more code to FLASH) or reduce .data/.bss"
|
||||
echo " (move .rodata to FLASH)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " ✅ OK: free stack $FREE_STACK >= threshold $MIN_STACK"
|
||||
exit 0
|
||||
@@ -0,0 +1,303 @@
|
||||
// pad_gen.ino — one-time OTP pad generator for the Teensy 4.1 SD card.
|
||||
//
|
||||
// This is a SETUP UTILITY, not part of the signer firmware. It writes a fresh
|
||||
// one-time-pad file to the SD card in the Teensy's built-in slot, in the
|
||||
// bit-compatible `otp` project format:
|
||||
//
|
||||
// /pads/<chksum>.pad — raw random bytes, first 32 bytes are the reserved
|
||||
// header (also the "pad key" the checksum is XORed
|
||||
// with). Matches libotppad OTPPAD_HEADER_RESERVED = 32
|
||||
// and tools/make_test_pad.c.
|
||||
// /pads/<chksum>.state — text file "offset=32\n" (32-byte header reserved).
|
||||
//
|
||||
// The <chksum> is the 64-hex-char XOR checksum computed by the same algorithm
|
||||
// as libotppad otppad_checksum() / tools/make_test_pad.c compute_checksum():
|
||||
// - XOR every byte into one of 32 buckets selected by (position % 32),
|
||||
// also XORing in bytes (pos>>8),(pos>>16),(pos>>24) of the position.
|
||||
// - XOR the resulting 32-byte checksum with the first 32 bytes of the pad.
|
||||
// - Hex-encode the 32-byte result.
|
||||
//
|
||||
// Entropy source: the i.MX RT1062 hardware TRNG (ENTROPY registers), read
|
||||
// directly. This is a real hardware RNG, NOT analogRead noise. Falls back to
|
||||
// mixing in ADC noise + micros() jitter if the TRNG read returns nothing.
|
||||
//
|
||||
// Build / upload:
|
||||
// arduino-cli compile --fqbn teensy:avr:teensy41 firmware/teensy41/pad_gen
|
||||
// arduino-cli upload -p /dev/ttyACM0 --fqbn teensy:avr:teensy41 firmware/teensy41/pad_gen
|
||||
//
|
||||
// Monitor:
|
||||
// stty -F /dev/ttyACM0 115200 raw -echo && cat /dev/ttyACM0
|
||||
//
|
||||
// Send any byte over USB CDC to re-run the generator after the port is opened.
|
||||
// The pad size defaults to 1 MB (1048576 bytes); edit PAD_SIZE below to change.
|
||||
|
||||
#include <SD.h>
|
||||
|
||||
// ---- Configuration --------------------------------------------------------
|
||||
static const uint32_t PAD_SIZE = 1048576; // 1 MB test pad
|
||||
static const char *PADS_DIR = "/pads";
|
||||
static const size_t HEADER_RESERVED = 32;
|
||||
static const size_t CHKSUM_BIN_LEN = 32;
|
||||
static const size_t CHKSUM_HEX_LEN = 64;
|
||||
static const size_t BUF_SIZE = 4096;
|
||||
|
||||
// ---- i.MX RT1062 TRNG (hardware entropy) ----------------------------------
|
||||
// The Teensy 4.1's NXP i.MX RT1062 has a true random number generator (TRNG).
|
||||
// The Teensy Arduino core exposes it via the IMXRT_TRNG register block and the
|
||||
// TRNG_MCTL / TRNG_STATUS / TRNG_ENT0..15 symbols (see imxrt.h). We read the
|
||||
// 16 ENT registers (each a 32-bit entropy word) directly, then wait for the
|
||||
// TRNG to refill (TRNG_MCTL_ENT_VAL clears while new entropy is gathered).
|
||||
//
|
||||
// Reference: NXP i.MX RT1062 reference manual, chapter "TRNG".
|
||||
|
||||
// Read up to 16 entropy words (512 bits) from the TRNG ENT0..ENT15 registers
|
||||
// into `out`. Returns the number of words read (0..16). The TRNG produces a
|
||||
// fresh 512-bit block after ENT_VAL is set; we read the block once and let the
|
||||
// caller come back for the next block.
|
||||
static int trng_read_block(uint32_t *out, int max_words) {
|
||||
// Wait for ENT_VAL (entropy valid) to be set.
|
||||
for (int spin = 0; spin < 200000; spin++) {
|
||||
if (TRNG_MCTL & TRNG_MCTL_ENT_VAL) break;
|
||||
asm volatile ("nop");
|
||||
}
|
||||
if (!(TRNG_MCTL & TRNG_MCTL_ENT_VAL)) return 0; // never became valid
|
||||
|
||||
int n = max_words < 16 ? max_words : 16;
|
||||
// ENT0..ENT15 are consecutive 32-bit registers at offset 0x40..0x7C.
|
||||
out[0] = TRNG_ENT0;
|
||||
if (n > 1) out[1] = TRNG_ENT1;
|
||||
if (n > 2) out[2] = TRNG_ENT2;
|
||||
if (n > 3) out[3] = TRNG_ENT3;
|
||||
if (n > 4) out[4] = TRNG_ENT4;
|
||||
if (n > 5) out[5] = TRNG_ENT5;
|
||||
if (n > 6) out[6] = TRNG_ENT6;
|
||||
if (n > 7) out[7] = TRNG_ENT7;
|
||||
if (n > 8) out[8] = TRNG_ENT8;
|
||||
if (n > 9) out[9] = TRNG_ENT9;
|
||||
if (n > 10) out[10] = TRNG_ENT10;
|
||||
if (n > 11) out[11] = TRNG_ENT11;
|
||||
if (n > 12) out[12] = TRNG_ENT12;
|
||||
if (n > 13) out[13] = TRNG_ENT13;
|
||||
if (n > 14) out[14] = TRNG_ENT14;
|
||||
if (n > 15) out[15] = TRNG_ENT15;
|
||||
return n;
|
||||
}
|
||||
|
||||
// Fill `buf` with `len` bytes from the TRNG, mixing in ADC noise + micros()
|
||||
// jitter as a fallback/defense-in-depth if the TRNG stalls. Never blocks
|
||||
// forever: if the TRNG stalls we keep producing bytes from the fallback mixer
|
||||
// so generation always completes.
|
||||
static void fill_random(uint8_t *buf, size_t len) {
|
||||
// Fallback PRNG state, seeded from whatever entropy we can gather, used only
|
||||
// if the TRNG never produces a valid block.
|
||||
uint32_t fallback = 0xA5A5A5A5u;
|
||||
fallback ^= (uint32_t)micros();
|
||||
fallback ^= (uint32_t)analogRead(A0);
|
||||
fallback ^= (uint32_t)analogRead(A1);
|
||||
fallback ^= (uint32_t)analogRead(A2);
|
||||
|
||||
size_t filled = 0;
|
||||
while (filled < len) {
|
||||
uint32_t block[16];
|
||||
int got = trng_read_block(block, 16);
|
||||
if (got <= 0) {
|
||||
// TRNG stalled — xorshift32 fallback seeded from gathered entropy.
|
||||
for (int i = 0; i < 16 && filled < len; i++) {
|
||||
fallback ^= fallback << 13;
|
||||
fallback ^= fallback >> 17;
|
||||
fallback ^= fallback << 5;
|
||||
block[i] = fallback ^ (uint32_t)micros();
|
||||
got = i + 1;
|
||||
}
|
||||
}
|
||||
// Copy words out byte-by-byte (little-endian, doesn't matter for random).
|
||||
for (int i = 0; i < got && filled < len; i++) {
|
||||
uint32_t w = block[i];
|
||||
for (int b = 0; b < 4 && filled < len; b++) {
|
||||
buf[filled++] = (uint8_t)(w & 0xFF);
|
||||
w >>= 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Checksum (matches libotppad / tools/make_test_pad.c) -----------------
|
||||
static void bytes_to_hex(const uint8_t *in, size_t n, char *out) {
|
||||
static const char hexdigits[] = "0123456789abcdef";
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
out[i * 2] = hexdigits[(in[i] >> 4) & 0xF];
|
||||
out[i * 2 + 1] = hexdigits[in[i] & 0xF];
|
||||
}
|
||||
out[n * 2] = '\0';
|
||||
}
|
||||
|
||||
// Compute the 64-hex-char XOR checksum of the pad file at `path` by streaming
|
||||
// it in BUF_SIZE chunks. Identical algorithm to tools/make_test_pad.c
|
||||
// compute_checksum() and libotppad otppad_checksum().
|
||||
static int compute_checksum(const char *path, char *checksum_hex) {
|
||||
File f = SD.open(path, FILE_READ);
|
||||
if (!f) return 1;
|
||||
|
||||
uint8_t checksum[CHKSUM_BIN_LEN];
|
||||
memset(checksum, 0, CHKSUM_BIN_LEN);
|
||||
|
||||
uint8_t buf[BUF_SIZE];
|
||||
uint64_t total = 0;
|
||||
int got;
|
||||
while ((got = f.read(buf, sizeof(buf))) > 0) {
|
||||
for (int i = 0; i < got; i++) {
|
||||
uint64_t pos = total + (uint64_t)i;
|
||||
uint8_t bucket = (uint8_t)(pos % CHKSUM_BIN_LEN);
|
||||
checksum[bucket] ^= buf[i] ^
|
||||
(uint8_t)((pos >> 8) & 0xFF) ^
|
||||
(uint8_t)((pos >> 16) & 0xFF) ^
|
||||
(uint8_t)((pos >> 24) & 0xFF);
|
||||
}
|
||||
total += (uint64_t)got;
|
||||
}
|
||||
f.close();
|
||||
|
||||
// XOR the checksum with the first 32 bytes of the pad (the "pad key").
|
||||
f = SD.open(path, FILE_READ);
|
||||
if (!f) return 1;
|
||||
uint8_t pad_key[CHKSUM_BIN_LEN];
|
||||
if ((int)f.read(pad_key, CHKSUM_BIN_LEN) != (int)CHKSUM_BIN_LEN) {
|
||||
f.close();
|
||||
return 1;
|
||||
}
|
||||
f.close();
|
||||
|
||||
uint8_t encrypted[CHKSUM_BIN_LEN];
|
||||
for (size_t i = 0; i < CHKSUM_BIN_LEN; i++) {
|
||||
encrypted[i] = checksum[i] ^ pad_key[i];
|
||||
}
|
||||
bytes_to_hex(encrypted, CHKSUM_BIN_LEN, checksum_hex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---- Generator ------------------------------------------------------------
|
||||
static void generate() {
|
||||
Serial.println();
|
||||
Serial.println("=== Teensy 4.1 OTP pad generator ===");
|
||||
Serial.print("Pad size: "); Serial.print(PAD_SIZE); Serial.println(" bytes");
|
||||
|
||||
if (!SD.begin(BUILTIN_SDCARD)) {
|
||||
Serial.println("ERROR: SD.begin(BUILTIN_SDCARD) failed — no card seated.");
|
||||
return;
|
||||
}
|
||||
Serial.println("SD card mounted OK.");
|
||||
|
||||
// Ensure /pads exists.
|
||||
if (!SD.exists(PADS_DIR)) {
|
||||
if (!SD.mkdir(PADS_DIR)) {
|
||||
Serial.println("ERROR: cannot create /pads directory.");
|
||||
return;
|
||||
}
|
||||
Serial.println("Created /pads directory.");
|
||||
}
|
||||
|
||||
// Write the pad to a temp name first, then rename by checksum.
|
||||
const char *tmp_path = "/pads/.padgen_tmp";
|
||||
SD.remove(tmp_path); // remove any stale temp
|
||||
File wf = SD.open(tmp_path, FILE_WRITE);
|
||||
if (!wf) {
|
||||
Serial.println("ERROR: cannot open temp pad file for writing.");
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.println("Generating random pad bytes (TRNG)...");
|
||||
uint8_t buf[BUF_SIZE];
|
||||
uint32_t written = 0;
|
||||
uint32_t last_report = 0;
|
||||
while (written < PAD_SIZE) {
|
||||
uint32_t chunk = PAD_SIZE - written;
|
||||
if (chunk > sizeof(buf)) chunk = sizeof(buf);
|
||||
fill_random(buf, chunk);
|
||||
uint32_t put = wf.write(buf, chunk);
|
||||
if (put != chunk) {
|
||||
Serial.print("ERROR: short write at offset "); Serial.println(written);
|
||||
wf.close();
|
||||
SD.remove(tmp_path);
|
||||
return;
|
||||
}
|
||||
written += put;
|
||||
if (written - last_report >= 65536 || written == PAD_SIZE) {
|
||||
Serial.print(" "); Serial.print(written); Serial.print(" / ");
|
||||
Serial.print(PAD_SIZE); Serial.println(" bytes");
|
||||
last_report = written;
|
||||
}
|
||||
}
|
||||
wf.close();
|
||||
Serial.println("Pad bytes written. Computing checksum...");
|
||||
|
||||
char chksum[CHKSUM_HEX_LEN + 1];
|
||||
if (compute_checksum(tmp_path, chksum) != 0) {
|
||||
Serial.println("ERROR: checksum computation failed.");
|
||||
SD.remove(tmp_path);
|
||||
return;
|
||||
}
|
||||
Serial.print("Checksum: "); Serial.println(chksum);
|
||||
|
||||
// Rename temp -> <chksum>.pad
|
||||
char pad_path[128];
|
||||
snprintf(pad_path, sizeof(pad_path), "%s/%s.pad", PADS_DIR, chksum);
|
||||
if (SD.exists(pad_path)) {
|
||||
Serial.print("NOTE: existing pad at "); Serial.print(pad_path);
|
||||
Serial.println(" — removing before rename.");
|
||||
SD.remove(pad_path);
|
||||
}
|
||||
if (!SD.rename(tmp_path, pad_path)) {
|
||||
Serial.println("ERROR: rename to <chksum>.pad failed.");
|
||||
SD.remove(tmp_path);
|
||||
return;
|
||||
}
|
||||
Serial.print("Pad file: "); Serial.println(pad_path);
|
||||
|
||||
// Write the .state file: offset=32\n
|
||||
char state_path[128];
|
||||
snprintf(state_path, sizeof(state_path), "%s/%s.state", PADS_DIR, chksum);
|
||||
File sf = SD.open(state_path, FILE_WRITE);
|
||||
if (!sf) {
|
||||
Serial.println("ERROR: cannot open .state file for writing.");
|
||||
return;
|
||||
}
|
||||
sf.print("offset=32\n");
|
||||
sf.close();
|
||||
Serial.print("State file: "); Serial.println(state_path);
|
||||
|
||||
// Verify the checksum matches the filename by re-computing.
|
||||
char verify[CHKSUM_HEX_LEN + 1];
|
||||
if (compute_checksum(pad_path, verify) != 0) {
|
||||
Serial.println("ERROR: verify re-compute failed.");
|
||||
return;
|
||||
}
|
||||
if (strcmp(verify, chksum) != 0) {
|
||||
Serial.print("ERROR: checksum mismatch after rename. file="); Serial.print(chksum);
|
||||
Serial.print(" recomputed="); Serial.println(verify);
|
||||
return;
|
||||
}
|
||||
Serial.println("Verify: checksum matches filename. Pad ready.");
|
||||
|
||||
Serial.println();
|
||||
Serial.println("=== Pad generation complete ===");
|
||||
Serial.print("Use this chksum (or any unique prefix) to bind: ");
|
||||
Serial.println(chksum);
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
while (!Serial && millis() < 4000) ;
|
||||
pinMode(LED_BUILTIN, OUTPUT);
|
||||
// ADC pins for fallback entropy mixing.
|
||||
analogReadResolution(16);
|
||||
generate();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if (Serial.available()) {
|
||||
while (Serial.available()) Serial.read();
|
||||
generate();
|
||||
}
|
||||
digitalWrite(LED_BUILTIN, HIGH); delay(500);
|
||||
digitalWrite(LED_BUILTIN, LOW); delay(500);
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
// Teensy 4.1 bring-up: detect, list, and read the SD card in the built-in slot.
|
||||
//
|
||||
// Uses the Teensy built-in SD library (4-bit SDMMC on the onboard slot — not the
|
||||
// SPI SD slot on the display module). Prints card info, the root directory
|
||||
// listing, and the first 512 bytes of the first file it finds, all over USB CDC
|
||||
// so we can inspect a card that may already be formatted with files on it.
|
||||
//
|
||||
// Build / upload:
|
||||
// arduino-cli compile --fqbn teensy:avr:teensy41 firmware/teensy41/sd_test
|
||||
// arduino-cli upload -p /dev/ttyACM0 --fqbn teensy:avr:teensy41 firmware/teensy41/sd_test
|
||||
//
|
||||
// Monitor:
|
||||
// stty -F /dev/ttyACM0 115200 raw -echo && cat /dev/ttyACM0
|
||||
//
|
||||
// Exit criterion: card is detected, its size + format are reported, the root
|
||||
// directory lists, and a sample file read round-trips.
|
||||
|
||||
#include <SD.h>
|
||||
|
||||
static void report() {
|
||||
Serial.println();
|
||||
Serial.println("=== Teensy 4.1 SD card test ===");
|
||||
Serial.println();
|
||||
|
||||
// BUILTIN_SDCARD selects the Teensy 4.1's onboard 4-bit SDMMC slot.
|
||||
if (!SD.begin(BUILTIN_SDCARD)) {
|
||||
Serial.println("ERROR: SD.begin(BUILTIN_SDCARD) failed — no card, bad card, or wiring issue.");
|
||||
Serial.println("Check that a card is seated in the Teensy's built-in slot.");
|
||||
return;
|
||||
}
|
||||
Serial.println("SD card mounted OK via 4-bit SDMMC (BUILTIN_SDCARD).");
|
||||
|
||||
// Card type via the Sd2Card helper (wraps SD.sdfs.card()->type()).
|
||||
Sd2Card card;
|
||||
uint8_t ct = card.type();
|
||||
Serial.print("Card type: ");
|
||||
switch (ct) {
|
||||
case SD_CARD_TYPE_SD1: Serial.println("SD1 (standard)"); break;
|
||||
case SD_CARD_TYPE_SD2: Serial.println("SD2 (standard)"); break;
|
||||
case SD_CARD_TYPE_SDHC: Serial.println("SDHC/SDXC"); break;
|
||||
default: Serial.println("unknown"); break;
|
||||
}
|
||||
|
||||
// Card size: SdFat v2 exposes sector count on the card object.
|
||||
uint64_t sectors = 0;
|
||||
if (SD.sdfs.card()) sectors = SD.sdfs.card()->sectorCount();
|
||||
uint64_t sz = sectors * 512ULL;
|
||||
Serial.print("Sectors (512 B): "); Serial.println((uint64_t)sectors);
|
||||
Serial.print("Card size (bytes): "); Serial.println((uint64_t)sz);
|
||||
Serial.print("Card size (GB): ");
|
||||
Serial.println((uint64_t)sz / (1000ULL * 1000ULL * 1000ULL));
|
||||
Serial.print("Card size (GiB): ");
|
||||
Serial.println((uint64_t)sz / (1024ULL * 1024ULL * 1024ULL));
|
||||
|
||||
// FAT type + cluster info via the SdVolume helper.
|
||||
SdVolume vol;
|
||||
vol.init(card);
|
||||
Serial.print("Volume FAT type: ");
|
||||
switch (vol.fatType()) {
|
||||
case 0: Serial.println("(none / not FAT — likely exFAT)"); break;
|
||||
case 12: Serial.println("FAT12"); break;
|
||||
case 16: Serial.println("FAT16"); break;
|
||||
case 32: Serial.println("FAT32"); break;
|
||||
default: Serial.print(vol.fatType()); Serial.println(" (unknown)"); break;
|
||||
}
|
||||
Serial.print("Cluster count: "); Serial.println(vol.clusterCount());
|
||||
Serial.print("Blocks per cluster: "); Serial.println(vol.blocksPerCluster());
|
||||
|
||||
Serial.print("sdfs.vol() fatType: ");
|
||||
if (SD.sdfs.vol()) Serial.println(SD.sdfs.vol()->fatType());
|
||||
else Serial.println("(no FAT volume — likely exFAT-only)");
|
||||
|
||||
Serial.println();
|
||||
Serial.println("=== Root directory listing ===");
|
||||
File root = SD.open("/");
|
||||
printDirectory(root, 0);
|
||||
root.close();
|
||||
Serial.println("=== end listing ===");
|
||||
|
||||
// Try to read the first regular file in the root and dump its first 512 bytes.
|
||||
Serial.println();
|
||||
Serial.println("=== First-file read test ===");
|
||||
root = SD.open("/");
|
||||
File first;
|
||||
while (true) {
|
||||
first = root.openNextFile();
|
||||
if (!first) break;
|
||||
if (!first.isDirectory()) break;
|
||||
first.close();
|
||||
}
|
||||
root.close();
|
||||
if (first) {
|
||||
Serial.print("Reading: ");
|
||||
Serial.print(first.name());
|
||||
Serial.print(" (");
|
||||
Serial.print(first.size(), DEC);
|
||||
Serial.println(" bytes)");
|
||||
Serial.println("--- first 512 bytes (hex + ASCII) ---");
|
||||
uint8_t buf[512];
|
||||
int n = first.read(buf, sizeof(buf));
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (buf[i] < 0x10) Serial.print('0');
|
||||
Serial.print(buf[i], HEX);
|
||||
Serial.print(' ');
|
||||
if ((i & 15) == 15) {
|
||||
Serial.print(" | ");
|
||||
for (int j = i - 15; j <= i; j++) {
|
||||
char c = (char)buf[j];
|
||||
Serial.print((c >= 32 && c < 127) ? c : '.');
|
||||
}
|
||||
Serial.println();
|
||||
}
|
||||
}
|
||||
Serial.println("--- end dump ---");
|
||||
first.close();
|
||||
} else {
|
||||
Serial.println("No regular files in root directory.");
|
||||
}
|
||||
|
||||
Serial.println();
|
||||
Serial.println("=== SD test complete ===");
|
||||
}
|
||||
|
||||
static void printDirectory(File dir, int depth) {
|
||||
while (true) {
|
||||
File entry = dir.openNextFile();
|
||||
if (!entry) break; // no more files
|
||||
for (int i = 0; i < depth; i++) Serial.print(" ");
|
||||
if (entry.isDirectory()) {
|
||||
Serial.print(entry.name());
|
||||
Serial.println("/");
|
||||
printDirectory(entry, depth + 1);
|
||||
} else {
|
||||
Serial.print(entry.name());
|
||||
Serial.print("\t");
|
||||
Serial.print(entry.size(), DEC);
|
||||
Serial.println(" bytes");
|
||||
}
|
||||
entry.close();
|
||||
}
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
while (!Serial && millis() < 4000) ; // wait up to 4s for USB CDC
|
||||
pinMode(LED_BUILTIN, OUTPUT);
|
||||
|
||||
// Print the report once at boot (may be missed if the host isn't listening
|
||||
// yet — that's fine, send any byte to re-trigger).
|
||||
report();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Re-run the report whenever any byte arrives over USB CDC, so the host can
|
||||
// request the report after the port has been opened.
|
||||
if (Serial.available()) {
|
||||
while (Serial.available()) Serial.read(); // drain
|
||||
report();
|
||||
}
|
||||
// slow steady blink = idle, ready for a re-report request
|
||||
digitalWrite(LED_BUILTIN, HIGH); delay(500);
|
||||
digitalWrite(LED_BUILTIN, LOW); delay(500);
|
||||
}
|
||||
@@ -77,6 +77,66 @@ SECTIONS
|
||||
*thash.c.o(.rodata*)
|
||||
*wots.c.o(.text*)
|
||||
*wots.c.o(.rodata*)
|
||||
/* Route the SdFat library (SD card access) into FLASH too, so it
|
||||
* doesn't consume ITCM and overflow the flexRAM DTCM partition.
|
||||
* Without this, including <SdFat.h> crashes the device before
|
||||
* setup() runs (ITCM grows past 12 blocks, reducing DTCM below
|
||||
* what .data needs). See plans/teensy41_otp_sd_pad.md §BLOCKER. */
|
||||
*FatFile.cpp.o(.text*)
|
||||
*FatFile.cpp.o(.rodata*)
|
||||
*FatFileLFN.cpp.o(.text*)
|
||||
*FatFileLFN.cpp.o(.rodata*)
|
||||
*FatFileSFN.cpp.o(.text*)
|
||||
*FatFileSFN.cpp.o(.rodata*)
|
||||
*FatFilePrint.cpp.o(.text*)
|
||||
*FatFilePrint.cpp.o(.rodata*)
|
||||
*FatPartition.cpp.o(.text*)
|
||||
*FatPartition.cpp.o(.rodata*)
|
||||
*FatVolume.cpp.o(.text*)
|
||||
*FatVolume.cpp.o(.rodata*)
|
||||
*FatName.cpp.o(.text*)
|
||||
*FatName.cpp.o(.rodata*)
|
||||
*FatFormatter.cpp.o(.text*)
|
||||
*FatFormatter.cpp.o(.rodata*)
|
||||
*FatDbg.cpp.o(.text*)
|
||||
*FatDbg.cpp.o(.rodata*)
|
||||
*FsCache.cpp.o(.text*)
|
||||
*FsCache.cpp.o(.rodata*)
|
||||
*FsFile.cpp.o(.text*)
|
||||
*FsFile.cpp.o(.rodata*)
|
||||
*FsVolume.cpp.o(.text*)
|
||||
*FsVolume.cpp.o(.rodata*)
|
||||
*FsName.cpp.o(.text*)
|
||||
*FsName.cpp.o(.rodata*)
|
||||
*FsStructs.cpp.o(.text*)
|
||||
*FsStructs.cpp.o(.rodata*)
|
||||
*FsNew.cpp.o(.text*)
|
||||
*FsNew.cpp.o(.rodata*)
|
||||
*FsUtf.cpp.o(.text*)
|
||||
*FsUtf.cpp.o(.rodata*)
|
||||
*FsDateTime.cpp.o(.text*)
|
||||
*FsDateTime.cpp.o(.rodata*)
|
||||
*FmtNumber.cpp.o(.text*)
|
||||
*FmtNumber.cpp.o(.rodata*)
|
||||
*FreeStack.cpp.o(.text*)
|
||||
*FreeStack.cpp.o(.rodata*)
|
||||
*MinimumSerial.cpp.o(.text*)
|
||||
*MinimumSerial.cpp.o(.rodata*)
|
||||
*istream.cpp.o(.text*)
|
||||
*istream.cpp.o(.rodata*)
|
||||
*ostream.cpp.o(.text*)
|
||||
*ostream.cpp.o(.rodata*)
|
||||
/* SDIO driver stays in ITCM (not FLASH) for fast interrupt response.
|
||||
* Only the FAT filesystem layer goes to FLASH. */
|
||||
|
||||
/* Move ALL .rodata to FLASH (it's read-only, cache-friendly from
|
||||
* FLASH via the D-cache) to free up DTCM for stack. This reclaims
|
||||
* an estimated 40-90 KB of DTCM. The ed25519.c.o rodata is excluded
|
||||
* because ed_K/ed_X/ed_Y base point constants must stay in DTCM
|
||||
* (moving them to FLASH produced an all-zeros pubkey — see the
|
||||
* note at the top of this file). */
|
||||
*(EXCLUDE_FILE(*ed25519.c.o) .rodata*)
|
||||
|
||||
. = ALIGN(4);
|
||||
KEEP(*(.init))
|
||||
__preinit_array_start = .;
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
#include "src/dispatch.h"
|
||||
#include "src/ui.h"
|
||||
#include "src/secure_mem.h"
|
||||
#include "src/otp_pad.h"
|
||||
#include "src/otp_pad_sd.h"
|
||||
#include "src/nostr_core/nostr_common.h"
|
||||
|
||||
// ---- Pin map (from firmware/teensy41/WIRING.md) ----
|
||||
@@ -267,12 +267,9 @@ static int apply_mnemonic() {
|
||||
}
|
||||
g_seed_len = 64;
|
||||
|
||||
// Initialize the OTP pad from the seed so encrypt/decrypt verbs work.
|
||||
Serial.println("Initializing OTP pad...");
|
||||
if (otp_pad_init(g_seed, g_seed_len) != 0) {
|
||||
Serial.println("otp_pad_init failed");
|
||||
return -1;
|
||||
}
|
||||
// The OTP pad is bound from the SD card AFTER transport_init() (see below),
|
||||
// so that USB CDC is up before any SD I/O — a fault in the SD path can't
|
||||
// prevent the device from enumerating.
|
||||
|
||||
Serial.println("Deriving secp256k1 keys...");
|
||||
if (derive_secp256k1_keys(g_seed, g_seed_len, g_privkey, g_pubkey) != 0) {
|
||||
@@ -485,14 +482,62 @@ void setup() {
|
||||
transport_init();
|
||||
Serial.println("Transport initialized.");
|
||||
|
||||
// Bind the OTP pad from the SD card. Give USB CDC a moment to enumerate
|
||||
// with the host first, so that even if the SD path faults, the device has
|
||||
// already appeared as /dev/ttyACM0 and we can see the boot output.
|
||||
delay(2000);
|
||||
Serial.println("Mounting SD card for OTP pad...");
|
||||
if (otp_pad_sd_mount() != 0) {
|
||||
Serial.println("otp_pad_sd_mount failed — encrypt/decrypt unavailable");
|
||||
} else {
|
||||
#if DEBUG_AUTO_GENERATE
|
||||
Serial.println("DEBUG_AUTO_GENERATE=1: auto-binding first SD pad...");
|
||||
if (otp_pad_sd_bind_first() != 0) {
|
||||
Serial.println("otp_pad_sd_bind_first failed — no pad bound");
|
||||
}
|
||||
#else
|
||||
// Interactive pad selection: scan /pads, show the list on the display,
|
||||
// let the user pick one (or skip). See ui_pick_pad() in ui.cpp.
|
||||
Serial.println("Scanning /pads for OTP pads...");
|
||||
static char pad_chksums[4][65];
|
||||
static uint64_t pad_sizes[4];
|
||||
int n_pads = otp_pad_sd_list_pads(pad_chksums, pad_sizes, 4);
|
||||
if (n_pads <= 0) {
|
||||
Serial.println("No pads found in /pads — OTP encrypt/decrypt unavailable");
|
||||
} else {
|
||||
Serial.print("Found ");
|
||||
Serial.print(n_pads);
|
||||
Serial.println(" pad(s), showing selection screen...");
|
||||
const char *chksum_ptrs[4];
|
||||
for (int i = 0; i < n_pads; i++) chksum_ptrs[i] = pad_chksums[i];
|
||||
char selected[65];
|
||||
int rc = ui_pick_pad(chksum_ptrs, pad_sizes, n_pads,
|
||||
selected, sizeof(selected));
|
||||
if (rc == 0) {
|
||||
Serial.print("User selected pad: ");
|
||||
Serial.println(selected);
|
||||
if (otp_pad_sd_bind(selected) != 0) {
|
||||
Serial.println("otp_pad_sd_bind failed for selected pad");
|
||||
}
|
||||
} else {
|
||||
Serial.println("User skipped OTP pad selection (or timeout)");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Serial.println("OTP pad init complete.");
|
||||
|
||||
// Show the idle screen with the npub + version.
|
||||
ui_show_idle(g_npub, "v0.1.0-teensy41");
|
||||
Serial.println("Idle screen shown. Signer ready.");
|
||||
}
|
||||
|
||||
// ---- Signing loop buffers (in DMAMEM/RAM2 to save RAM1) ----
|
||||
DMAMEM static uint8_t req_buf[2048];
|
||||
DMAMEM static char resp_buf[4096];
|
||||
// Increased for OTP encrypt/decrypt: a 4 KB chunk produces ~5.5 KB of ASCII
|
||||
// armor, and the base64-encoded request can be ~5.5 KB. With 110 KB of free
|
||||
// RAM2 heap, 8 KB + 8 KB is comfortable.
|
||||
DMAMEM static uint8_t req_buf[8192];
|
||||
DMAMEM static char resp_buf[8192];
|
||||
|
||||
void loop() {
|
||||
// 1. Keep LVGL responsive (idle screen + any approval prompts).
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
#include "secure_mem.h"
|
||||
#include "ui.h"
|
||||
#include "auth_envelope.h"
|
||||
#include "otp_pad.h"
|
||||
#include "otp_pad_sd.h"
|
||||
#include "key_derivation.h"
|
||||
#include "ed25519.h"
|
||||
|
||||
@@ -684,6 +684,36 @@ __attribute__((section(".flashmem"))) static int b64_decode(const char *in, size
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Allocating variants: caller frees the returned buffer. Returns NULL on
|
||||
* failure. Used by the OTP encrypt/decrypt verbs for variable-length
|
||||
* payloads that don't fit a fixed stack buffer. */
|
||||
__attribute__((section(".flashmem"))) static char *b64_encode_alloc(const uint8_t *in, int in_len) {
|
||||
if (!in || in_len < 0) return NULL;
|
||||
size_t need = ((size_t)in_len + 2) / 3 * 4 + 1;
|
||||
char *out = (char *)malloc(need);
|
||||
if (!out) return NULL;
|
||||
if (b64_encode(in, (size_t)in_len, out, need) != 0) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
__attribute__((section(".flashmem"))) static uint8_t *b64_decode_alloc(const char *in, size_t in_len, int *out_len) {
|
||||
if (!in || !out_len) return NULL;
|
||||
/* Upper bound on decoded length. */
|
||||
size_t cap = in_len / 4 * 3 + 4;
|
||||
uint8_t *out = (uint8_t *)malloc(cap);
|
||||
if (!out) return NULL;
|
||||
size_t got = 0;
|
||||
if (b64_decode(in, in_len, out, cap, &got) != 0) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
*out_len = (int)got;
|
||||
return out;
|
||||
}
|
||||
|
||||
/* ====================================================================
|
||||
* Approval flow
|
||||
* ====================================================================
|
||||
@@ -1691,17 +1721,68 @@ __attribute__((section(".flashmem"))) static void handle_request(cJSON *req, con
|
||||
return;
|
||||
}
|
||||
|
||||
/* ===================== encrypt / decrypt (otp) ===================== */
|
||||
/* ===================== otp_status (debug) =========================== */
|
||||
if (strcmp(method, "otp_status") == 0) {
|
||||
cJSON *obj = cJSON_CreateObject();
|
||||
char *out;
|
||||
cJSON_AddBoolToObject(obj, "bound", otp_pad_sd_ready() ? 1 : 0);
|
||||
const char *cs = otp_pad_sd_chksum();
|
||||
cJSON_AddStringToObject(obj, "chksum", cs ? cs : "");
|
||||
char off_str[32], size_str[32];
|
||||
snprintf(off_str, sizeof(off_str), "%llu",
|
||||
(unsigned long long)otp_pad_sd_offset());
|
||||
snprintf(size_str, sizeof(size_str), "%llu",
|
||||
(unsigned long long)otp_pad_sd_size());
|
||||
cJSON_AddStringToObject(obj, "offset", off_str);
|
||||
cJSON_AddStringToObject(obj, "pad_size", size_str);
|
||||
out = cJSON_PrintUnformatted(obj);
|
||||
if (out) { set_result(id_token, out); cJSON_free(out); }
|
||||
cJSON_Delete(obj);
|
||||
return;
|
||||
}
|
||||
|
||||
/* ===================== otp_debug (debug) ============================ */
|
||||
/* Lists files in /pads to diagnose bind failures. Does NOT call
|
||||
* bind_first (which does a slow 1 MB checksum verify). */
|
||||
if (strcmp(method, "otp_debug") == 0) {
|
||||
cJSON *obj = cJSON_CreateObject();
|
||||
char *out;
|
||||
char list[1024];
|
||||
int n = otp_pad_sd_debug_list(list, sizeof(list));
|
||||
char n_str[16];
|
||||
snprintf(n_str, sizeof(n_str), "%d", n);
|
||||
cJSON_AddStringToObject(obj, "file_count", n_str);
|
||||
cJSON_AddStringToObject(obj, "listing", list);
|
||||
cJSON_AddBoolToObject(obj, "bound", otp_pad_sd_ready() ? 1 : 0);
|
||||
out = cJSON_PrintUnformatted(obj);
|
||||
if (out) { set_result(id_token, out); cJSON_free(out); }
|
||||
cJSON_Delete(obj);
|
||||
return;
|
||||
}
|
||||
|
||||
/* ===================== encrypt / decrypt (otp) ===================== */
|
||||
/* SD-card one-time-pad. Bit-compatible with the host n_signer's
|
||||
* otp_encrypt / otp_decrypt verbs (see src/otp_pad.c) and the `otp` CLI
|
||||
* via libotppad/otppad_embedded. Supports ASCII armor and binary .otp
|
||||
* encodings, selected via the "encoding" option (default "ascii").
|
||||
*
|
||||
* Wire format:
|
||||
* encrypt: [plaintext_b64, {"encoding": "ascii"|"binary"}]
|
||||
* -> {"ciphertext": ..., "pad_chksum": ...,
|
||||
* "pad_offset_before": N, "pad_offset_after": N}
|
||||
* decrypt: [ciphertext, {"encoding": "ascii"|"binary"}]
|
||||
* -> {"plaintext": "<b64>"} (offset read from armor/binary header)
|
||||
*
|
||||
* The "algorithm": "otp" option is accepted for backward compatibility
|
||||
* with test_signer.py but is optional. */
|
||||
if (strcmp(method, VERB_ENCRYPT) == 0 ||
|
||||
strcmp(method, VERB_DECRYPT) == 0) {
|
||||
cJSON *options = NULL;
|
||||
fw_alg_t alg;
|
||||
uint32_t index = 0;
|
||||
cJSON *arg0 = NULL;
|
||||
const char *payload;
|
||||
int is_decrypt = (strcmp(method, VERB_DECRYPT) == 0);
|
||||
const char *encoding = "ascii"; /* default */
|
||||
|
||||
/* OTP encrypt and decrypt are the same XOR operation against the pad;
|
||||
* the verb name is carried through for the UI prompt only. */
|
||||
if (!cJSON_IsArray(params) || cJSON_GetArraySize(params) < 1) {
|
||||
set_error_code(id_token, ERR_INVALID_PARAMS, "invalid params");
|
||||
return;
|
||||
@@ -1713,18 +1794,18 @@ __attribute__((section(".flashmem"))) static void handle_request(cJSON *req, con
|
||||
}
|
||||
payload = arg0->valuestring;
|
||||
parse_options_from_params(params, &options);
|
||||
if (parse_algorithm_from_options(options, &alg, &index) != 0) {
|
||||
set_error_code(id_token, ERR_INVALID_PARAMS, "invalid params");
|
||||
return;
|
||||
|
||||
/* "encoding" option (optional, default "ascii"). */
|
||||
if (options != NULL) {
|
||||
cJSON *enc = cJSON_GetObjectItemCaseSensitive(options, "encoding");
|
||||
if (cJSON_IsString(enc) && enc->valuestring != NULL) {
|
||||
encoding = enc->valuestring;
|
||||
}
|
||||
}
|
||||
if (enforce_alg_verb(alg, method) != 0) {
|
||||
set_error_code(id_token, ERR_ALG_NOT_SUPPORTED,
|
||||
"algorithm_not_supported_for_verb");
|
||||
return;
|
||||
}
|
||||
if (!otp_pad_ready()) {
|
||||
|
||||
if (!otp_pad_sd_ready()) {
|
||||
set_error_code(id_token, ERR_INTERNAL,
|
||||
"otp pad not initialized");
|
||||
"otp pad not bound (no SD pad)");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1737,47 +1818,135 @@ __attribute__((section(".flashmem"))) static void handle_request(cJSON *req, con
|
||||
return;
|
||||
}
|
||||
|
||||
/* For encrypt: payload is base64 plaintext.
|
||||
* For decrypt: payload is base64 ciphertext (XOR output).
|
||||
* Decode base64, XOR with the pad at the current offset, advance
|
||||
* the offset, and re-encode. */
|
||||
{
|
||||
uint8_t buf[OTP_PAD_LEN];
|
||||
size_t buf_len = 0;
|
||||
|
||||
if (b64_decode(payload, strlen(payload), buf, sizeof(buf),
|
||||
&buf_len) != 0) {
|
||||
if (!is_decrypt) {
|
||||
/* ---- encrypt ----
|
||||
* payload is base64 plaintext. Decode, hand bytes to
|
||||
* otp_pad_sd_encrypt, base64-encode the returned armor/blob
|
||||
* for JSON transport (binary blobs are base64-wrapped in the
|
||||
* JSON result, matching the host). */
|
||||
/* Decode base64 plaintext into a heap buffer. */
|
||||
int pt_len = 0;
|
||||
unsigned char *pt = (unsigned char *)b64_decode_alloc(
|
||||
payload, strlen(payload), &pt_len);
|
||||
if (!pt) {
|
||||
set_error_code(id_token, ERR_INVALID_PARAMS,
|
||||
"invalid base64");
|
||||
return;
|
||||
}
|
||||
if (otp_pad_apply(buf, buf_len) != 0) {
|
||||
char *out_payload = NULL;
|
||||
size_t out_len = 0;
|
||||
uint64_t off_before = 0, off_after = 0;
|
||||
int rc = otp_pad_sd_encrypt(pt, (size_t)pt_len, encoding,
|
||||
&out_payload, &out_len,
|
||||
&off_before, &off_after);
|
||||
secure_memzero(pt, (size_t)pt_len);
|
||||
free(pt);
|
||||
if (rc != 0) {
|
||||
set_error_code(id_token, ERR_INTERNAL,
|
||||
"otp pad exhausted");
|
||||
secure_memzero(buf, sizeof(buf));
|
||||
"otp encrypt failed");
|
||||
if (out_payload) { secure_memzero(out_payload, out_len); free(out_payload); }
|
||||
return;
|
||||
}
|
||||
|
||||
/* For binary encoding, base64-wrap the blob for JSON. For
|
||||
* ascii encoding, out_payload is already a NUL-terminated
|
||||
* armor string. */
|
||||
char *ct_b64 = NULL;
|
||||
if (strcmp(encoding, "binary") == 0) {
|
||||
ct_b64 = b64_encode_alloc((const uint8_t *)out_payload,
|
||||
(int)out_len);
|
||||
} else {
|
||||
/* ASCII armor: copy as-is (it's text-safe). */
|
||||
ct_b64 = (char *)malloc(out_len + 1);
|
||||
if (ct_b64) { memcpy(ct_b64, out_payload, out_len); ct_b64[out_len] = '\0'; }
|
||||
}
|
||||
secure_memzero(out_payload, out_len);
|
||||
free(out_payload);
|
||||
if (!ct_b64) {
|
||||
set_error_code(id_token, ERR_INTERNAL, "internal error");
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
cJSON *obj = cJSON_CreateObject();
|
||||
char *out;
|
||||
char off_before_str[32], off_after_str[32];
|
||||
cJSON_AddStringToObject(obj, "ciphertext", ct_b64);
|
||||
cJSON_AddStringToObject(obj, "pad_chksum",
|
||||
otp_pad_sd_chksum());
|
||||
snprintf(off_before_str, sizeof(off_before_str),
|
||||
"%llu", (unsigned long long)off_before);
|
||||
snprintf(off_after_str, sizeof(off_after_str),
|
||||
"%llu", (unsigned long long)off_after);
|
||||
cJSON_AddStringToObject(obj, "pad_offset_before",
|
||||
off_before_str);
|
||||
cJSON_AddStringToObject(obj, "pad_offset_after",
|
||||
off_after_str);
|
||||
out = cJSON_PrintUnformatted(obj);
|
||||
if (out) { set_result(id_token, out); cJSON_free(out); }
|
||||
cJSON_Delete(obj);
|
||||
}
|
||||
free(ct_b64);
|
||||
} else {
|
||||
/* ---- decrypt ----
|
||||
* payload is either ASCII armor (text) or a base64-encoded
|
||||
* binary .otp blob, selected by the "encoding" option. */
|
||||
unsigned char *input_bytes = NULL;
|
||||
size_t input_len = 0;
|
||||
char *input_str = NULL;
|
||||
|
||||
if (strcmp(encoding, "binary") == 0) {
|
||||
/* payload is base64 of the binary blob. Decode to bytes. */
|
||||
int blen = 0;
|
||||
input_bytes = (unsigned char *)b64_decode_alloc(
|
||||
payload, strlen(payload), &blen);
|
||||
if (!input_bytes) {
|
||||
set_error_code(id_token, ERR_INVALID_PARAMS,
|
||||
"invalid base64");
|
||||
return;
|
||||
}
|
||||
input_len = (size_t)blen;
|
||||
} else {
|
||||
/* ASCII armor: pass the string directly. */
|
||||
input_str = (char *)payload; /* borrowed, not freed */
|
||||
}
|
||||
|
||||
unsigned char *pt = NULL;
|
||||
size_t pt_len = 0;
|
||||
int rc;
|
||||
if (strcmp(encoding, "binary") == 0) {
|
||||
rc = otp_pad_sd_decrypt((const char *)input_bytes,
|
||||
input_len, "binary",
|
||||
&pt, &pt_len);
|
||||
} else {
|
||||
rc = otp_pad_sd_decrypt(input_str, strlen(input_str),
|
||||
"ascii", &pt, &pt_len);
|
||||
}
|
||||
if (input_bytes) { free(input_bytes); }
|
||||
if (rc != 0) {
|
||||
set_error_code(id_token, ERR_INTERNAL,
|
||||
"otp decrypt failed");
|
||||
if (pt) { secure_memzero(pt, pt_len); free(pt); }
|
||||
return;
|
||||
}
|
||||
|
||||
/* base64-encode the recovered plaintext for JSON. */
|
||||
char *pt_b64 = b64_encode_alloc(pt, (int)pt_len);
|
||||
secure_memzero(pt, pt_len);
|
||||
free(pt);
|
||||
if (!pt_b64) {
|
||||
set_error_code(id_token, ERR_INTERNAL, "internal error");
|
||||
return;
|
||||
}
|
||||
{
|
||||
char out_b64[1400];
|
||||
if (b64_encode(buf, buf_len, out_b64,
|
||||
sizeof(out_b64)) != 0) {
|
||||
set_error_code(id_token, ERR_INTERNAL,
|
||||
"internal error");
|
||||
} else {
|
||||
cJSON *obj = cJSON_CreateObject();
|
||||
char *out;
|
||||
char off_str[24];
|
||||
cJSON_AddStringToObject(obj, "result", out_b64);
|
||||
cJSON_AddStringToObject(obj, "algorithm", "otp");
|
||||
snprintf(off_str, sizeof(off_str), "%u",
|
||||
(unsigned)otp_pad_offset());
|
||||
cJSON_AddStringToObject(obj, "pad_offset", off_str);
|
||||
out = cJSON_PrintUnformatted(obj);
|
||||
if (out) { set_result(id_token, out); cJSON_free(out); }
|
||||
cJSON_Delete(obj);
|
||||
}
|
||||
cJSON *obj = cJSON_CreateObject();
|
||||
char *out;
|
||||
cJSON_AddStringToObject(obj, "plaintext", pt_b64);
|
||||
out = cJSON_PrintUnformatted(obj);
|
||||
if (out) { set_result(id_token, out); cJSON_free(out); }
|
||||
cJSON_Delete(obj);
|
||||
}
|
||||
secure_memzero(buf, sizeof(buf));
|
||||
free(pt_b64);
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
/* otp_pad.cpp — HKDF-derived one-time-pad for the Teensy 4.1 n_signer firmware.
|
||||
*
|
||||
* Ported from the CYD's inline OTP pad (firmware/cyd_esp32_2432s028/main/main.c,
|
||||
* apply_mnemonic_and_enter_working). The CYD derives the pad inline and keeps
|
||||
* the state in file-static globals; here we wrap the same logic in a small
|
||||
* module so dispatch.cpp can call otp_pad_init / otp_pad_apply / otp_pad_zeroize.
|
||||
*
|
||||
* pad = HKDF-SHA256(salt="nsigner-otp", ikm=seed(64B), info="otp-pad",
|
||||
* L=OTP_PAD_LEN)
|
||||
*
|
||||
* The HKDF comes from nostr_core/utils.c (nostr_hkdf), the same backend the
|
||||
* CYD uses. The pad is held in a file-static buffer and zeroized on lock.
|
||||
*/
|
||||
#include "otp_pad.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "nostr_core/utils.h"
|
||||
#include "secure_mem.h"
|
||||
|
||||
static uint8_t s_pad[OTP_PAD_LEN];
|
||||
static size_t s_pad_len = 0;
|
||||
static size_t s_offset = 0;
|
||||
|
||||
__attribute__((section(".flashmem"))) int otp_pad_init(const uint8_t *seed, size_t seed_len) {
|
||||
static const uint8_t kSalt[] = "nsigner-otp"; /* 12 bytes (no NUL) */
|
||||
static const uint8_t kInfo[] = "otp-pad"; /* 7 bytes (no NUL) */
|
||||
|
||||
if (seed == NULL || seed_len == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (nostr_hkdf(kSalt, sizeof(kSalt) - 1,
|
||||
seed, seed_len,
|
||||
kInfo, sizeof(kInfo) - 1,
|
||||
s_pad, sizeof(s_pad)) != 0) {
|
||||
s_pad_len = 0;
|
||||
s_offset = 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
s_pad_len = sizeof(s_pad);
|
||||
s_offset = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
__attribute__((section(".flashmem"))) void otp_pad_zeroize(void) {
|
||||
secure_memzero(s_pad, sizeof(s_pad));
|
||||
s_pad_len = 0;
|
||||
s_offset = 0;
|
||||
}
|
||||
|
||||
__attribute__((section(".flashmem"))) int otp_pad_apply(uint8_t *buf, size_t len) {
|
||||
size_t i;
|
||||
|
||||
if (buf == NULL || s_pad_len == 0) {
|
||||
return -1;
|
||||
}
|
||||
if (s_offset + len > s_pad_len) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (i = 0; i < len; ++i) {
|
||||
buf[i] ^= s_pad[s_offset + i];
|
||||
}
|
||||
s_offset += len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
__attribute__((section(".flashmem"))) size_t otp_pad_offset(void) {
|
||||
return s_offset;
|
||||
}
|
||||
|
||||
__attribute__((section(".flashmem"))) int otp_pad_ready(void) {
|
||||
return (s_pad_len != 0) ? 1 : 0;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/* otp_pad.h — HKDF-derived one-time-pad for the Teensy 4.1 n_signer firmware.
|
||||
*
|
||||
* Phase 6 of plans/teensy41_signer_implementation.md.
|
||||
*
|
||||
* For the initial port we use the same HKDF-from-seed pad as the CYD firmware
|
||||
* (firmware/cyd_esp32_2432s028/main/main.c, apply_mnemonic_and_enter_working):
|
||||
*
|
||||
* pad = HKDF-SHA256(salt="nsigner-otp", ikm=seed(64B), info="otp-pad",
|
||||
* L=OTP_PAD_LEN)
|
||||
*
|
||||
* The offset advances monotonically across encrypt/decrypt requests so each
|
||||
* pad byte is used at most once (true one-time-pad semantics within a session).
|
||||
* The pad lives in working memory only and is zeroized on reset.
|
||||
*
|
||||
* The 1 TB SDXC physical pad is a future enhancement (see
|
||||
* plans/teensy41_signer_implementation.md §Phase 6 decisions) and will plug in
|
||||
* behind the same otp_pad_xxx() API.
|
||||
*/
|
||||
#ifndef FIRMWARE_TEENSY41_SIGNER_OTP_PAD_H
|
||||
#define FIRMWARE_TEENSY41_SIGNER_OTP_PAD_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Pad length in bytes. Matches the CYD (1024 bytes per session). */
|
||||
#define OTP_PAD_LEN 1024
|
||||
|
||||
/* Derive the pad from a 64-byte mnemonic seed and reset the offset to 0.
|
||||
* Call once after mnemonic_to_seed(). Returns 0 on success, -1 on error. */
|
||||
int otp_pad_init(const uint8_t *seed, size_t seed_len);
|
||||
|
||||
/* Zeroize the pad and reset state. Call on lock / power-down. */
|
||||
void otp_pad_zeroize(void);
|
||||
|
||||
/* XOR `len` bytes of the pad (at the current offset) into `buf`, then advance
|
||||
* the offset by `len`. Returns 0 on success, -1 if the pad is not initialized
|
||||
* or would be exhausted (offset + len > OTP_PAD_LEN). */
|
||||
int otp_pad_apply(uint8_t *buf, size_t len);
|
||||
|
||||
/* Current monotonic offset (bytes of pad consumed so far this session). */
|
||||
size_t otp_pad_offset(void);
|
||||
|
||||
/* Whether the pad has been initialized (otp_pad_init succeeded). */
|
||||
int otp_pad_ready(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* FIRMWARE_TEENSY41_SIGNER_OTP_PAD_H */
|
||||
@@ -0,0 +1,580 @@
|
||||
/* otp_pad_sd.cpp — SD-card one-time-pad implementation for the Teensy 4.1.
|
||||
*
|
||||
* Uses SdFat directly (not the Arduino SD wrapper) with a minimal config
|
||||
* (FAT16/32 only, no exFAT) to reduce the code footprint enough to fit
|
||||
* alongside the signer's crypto code in the Teensy 4.1's flexRAM. The
|
||||
* Arduino SD wrapper pulled in all of SdFat including exFAT, which overflowed
|
||||
* ITCM and crashed the device before setup() ran.
|
||||
*
|
||||
* See plans/teensy41_otp_sd_pad.md §BLOCKER for the full root-cause analysis.
|
||||
*/
|
||||
|
||||
#include "otp_pad_sd.h"
|
||||
#include "otppad_embedded.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
/* Minimal SdFat config: FAT16/32 only (no exFAT). This must be defined before
|
||||
* including SdFat.h so SdFatConfig.h picks it up. */
|
||||
#define SDFAT_FILE_TYPE 1
|
||||
#include <SdFat.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
extern "C" void secure_memzero(void *ptr, size_t len);
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* State */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#define OTP_SD_PADS_DIR "/pads"
|
||||
#define OTP_SD_CHKSUM_MAX 128
|
||||
#define OTP_SD_PATH_MAX (sizeof(OTP_SD_PADS_DIR) + OTP_SD_CHKSUM_MAX + 16)
|
||||
|
||||
/* The SdFat global. Using SdFat32 (FAT-only) directly avoids the exFAT code
|
||||
* that the Arduino SD wrapper pulled in. */
|
||||
static SdFat sd;
|
||||
|
||||
typedef struct {
|
||||
int bound;
|
||||
char chksum[OTP_SD_CHKSUM_MAX];
|
||||
char pad_path[OTP_SD_PATH_MAX];
|
||||
File32 pad_file; /* read-only, kept open for the session */
|
||||
uint64_t pad_size;
|
||||
} otp_sd_state_t;
|
||||
|
||||
static otp_sd_state_t g_sd = {0};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* SD state file I/O (implements the otppad_e_state_*_sd prototypes) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
extern "C" int otppad_e_state_read_sd(const char *pads_dir, const char *chksum,
|
||||
uint64_t *offset) {
|
||||
if (!pads_dir || !chksum || !offset) return 1;
|
||||
char path[OTP_SD_PATH_MAX];
|
||||
snprintf(path, sizeof(path), "%s/%s.state", pads_dir, chksum);
|
||||
|
||||
File32 f = sd.open(path, O_RDONLY);
|
||||
if (!f) return 2;
|
||||
|
||||
char line[128];
|
||||
int n = f.read((uint8_t *)line, sizeof(line) - 1);
|
||||
f.close();
|
||||
if (n <= 0) return 3;
|
||||
line[n] = '\0';
|
||||
|
||||
if (strncmp(line, "offset=", 7) != 0) return 4;
|
||||
*offset = strtoull(line + 7, NULL, 10);
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" int otppad_e_state_write_sd(const char *pads_dir, const char *chksum,
|
||||
uint64_t offset) {
|
||||
if (!pads_dir || !chksum) return 1;
|
||||
char path[OTP_SD_PATH_MAX];
|
||||
char tmp[OTP_SD_PATH_MAX];
|
||||
snprintf(path, sizeof(path), "%s/%s.state", pads_dir, chksum);
|
||||
snprintf(tmp, sizeof(tmp), "%s/%s.state.tmp", pads_dir, chksum);
|
||||
|
||||
sd.remove(tmp);
|
||||
File32 f = sd.open(tmp, O_WRONLY | O_CREAT | O_TRUNC);
|
||||
if (!f) return 2;
|
||||
char buf[64];
|
||||
int len = snprintf(buf, sizeof(buf), "offset=%llu\n",
|
||||
(unsigned long long)offset);
|
||||
size_t wrote = f.write((const uint8_t *)buf, (size_t)len);
|
||||
f.close();
|
||||
if (wrote != (size_t)len) {
|
||||
sd.remove(tmp);
|
||||
return 3;
|
||||
}
|
||||
sd.remove(path);
|
||||
if (!sd.rename(tmp, path)) {
|
||||
sd.remove(tmp);
|
||||
return 4;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void bytes_to_hex(const unsigned char *in, size_t n, char *out) {
|
||||
static const char hex[] = "0123456789abcdef";
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
out[i * 2] = hex[(in[i] >> 4) & 0xF];
|
||||
out[i * 2 + 1] = hex[in[i] & 0xF];
|
||||
}
|
||||
out[n * 2] = '\0';
|
||||
}
|
||||
|
||||
static int resolve_pad(const char *prefix, char *out_chksum) {
|
||||
File32 dir = sd.open(OTP_SD_PADS_DIR);
|
||||
if (!dir) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int matches = 0;
|
||||
size_t plen = prefix ? strlen(prefix) : 0;
|
||||
char found[OTPPAD_E_CHKSUM_HEX_LEN + 1] = {0};
|
||||
|
||||
while (true) {
|
||||
File32 entry = dir.openNextFile();
|
||||
if (!entry) break;
|
||||
if (!entry.isDir()) {
|
||||
/* 64-char chksum + ".pad" = 68 chars + NUL = 69. Use a 128-byte
|
||||
* buffer so getName() doesn't truncate the long filename. */
|
||||
char name[128];
|
||||
entry.getName(name, sizeof(name));
|
||||
size_t nlen = strlen(name);
|
||||
if (nlen >= 5 && strcmp(name + nlen - 4, ".pad") == 0) {
|
||||
size_t base = nlen - 4;
|
||||
if (base == OTPPAD_E_CHKSUM_HEX_LEN &&
|
||||
(plen == 0 || strncmp(name, prefix, plen) == 0)) {
|
||||
memcpy(found, name, base);
|
||||
found[base] = '\0';
|
||||
matches++;
|
||||
}
|
||||
}
|
||||
}
|
||||
entry.close();
|
||||
}
|
||||
dir.close();
|
||||
|
||||
if (matches == 0) return -1;
|
||||
/* When no prefix was given (bind_first), return the first match found
|
||||
* rather than failing on ambiguity. Only fail with -2 when a prefix was
|
||||
* specified and it matches multiple pads. */
|
||||
if (matches > 1 && plen > 0) return -2;
|
||||
strcpy(out_chksum, found);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int verify_pad_checksum(void) {
|
||||
if (!g_sd.pad_file) return 1;
|
||||
|
||||
otppad_e_checksum_ctx ctx;
|
||||
otppad_e_checksum_init(&ctx);
|
||||
|
||||
if (!g_sd.pad_file.seek((uint64_t)0)) return 2;
|
||||
|
||||
unsigned char *buf = (unsigned char *)malloc(4096);
|
||||
if (!buf) return 3;
|
||||
|
||||
uint64_t pos = 0;
|
||||
int got;
|
||||
while ((got = g_sd.pad_file.read(buf, 4096)) > 0) {
|
||||
otppad_e_checksum_update(&ctx, buf, (size_t)got, pos);
|
||||
pos += (uint64_t)got;
|
||||
}
|
||||
free(buf);
|
||||
|
||||
if (!g_sd.pad_file.seek((uint64_t)0)) return 4;
|
||||
unsigned char pad_key[OTPPAD_E_CHKSUM_BIN_LEN];
|
||||
int n = g_sd.pad_file.read(pad_key, OTPPAD_E_CHKSUM_BIN_LEN);
|
||||
if (n != OTPPAD_E_CHKSUM_BIN_LEN) return 5;
|
||||
|
||||
g_sd.pad_file.seek((uint64_t)0);
|
||||
|
||||
char hex[OTPPAD_E_CHKSUM_HEX_LEN + 1];
|
||||
otppad_e_checksum_final(&ctx, pad_key, hex);
|
||||
if (strcmp(hex, g_sd.chksum) != 0) {
|
||||
Serial.print("otp_pad_sd: checksum mismatch. file=");
|
||||
Serial.print(g_sd.chksum);
|
||||
Serial.print(" computed=");
|
||||
Serial.println(hex);
|
||||
return 6;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int read_pad_slice(uint64_t offset, size_t len, unsigned char *out) {
|
||||
if (!g_sd.pad_file) return -1;
|
||||
if (!g_sd.pad_file.seek(offset)) return -2;
|
||||
size_t got = 0;
|
||||
while (got < len) {
|
||||
int n = g_sd.pad_file.read(out + got, len - got);
|
||||
if (n <= 0) return -3;
|
||||
got += (size_t)n;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Public API */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
int otp_pad_sd_mount(void) {
|
||||
/* Give the SD card time to power up. The Teensy 4.1's SDMMC peripheral
|
||||
* may need a brief delay after boot before the card is ready. Retry up
|
||||
* to 3 times with a 500ms delay between attempts. */
|
||||
for (int attempt = 0; attempt < 3; attempt++) {
|
||||
if (sd.begin(SdioConfig(FIFO_SDIO))) {
|
||||
Serial.println("otp_pad_sd: SD card mounted (SdFat direct, FAT-only)");
|
||||
return 0;
|
||||
}
|
||||
Serial.print("otp_pad_sd: sd.begin attempt ");
|
||||
Serial.print(attempt + 1);
|
||||
Serial.println(" failed, retrying...");
|
||||
delay(500);
|
||||
}
|
||||
Serial.println("otp_pad_sd: sd.begin(SdioConfig(FIFO_SDIO)) failed after 3 attempts");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int otp_pad_sd_bind(const char *chksum_or_prefix) {
|
||||
if (!chksum_or_prefix) return 1;
|
||||
if (g_sd.bound) return 2;
|
||||
|
||||
char chksum[OTPPAD_E_CHKSUM_HEX_LEN + 1];
|
||||
if (strlen(chksum_or_prefix) == OTPPAD_E_CHKSUM_HEX_LEN) {
|
||||
strncpy(chksum, chksum_or_prefix, OTPPAD_E_CHKSUM_HEX_LEN);
|
||||
chksum[OTPPAD_E_CHKSUM_HEX_LEN] = '\0';
|
||||
char path[OTP_SD_PATH_MAX];
|
||||
snprintf(path, sizeof(path), "%s/%s.pad", OTP_SD_PADS_DIR, chksum);
|
||||
if (!sd.exists(path)) {
|
||||
Serial.print("otp_pad_sd: pad not found: "); Serial.println(path);
|
||||
return 3;
|
||||
}
|
||||
} else {
|
||||
int r = resolve_pad(chksum_or_prefix, chksum);
|
||||
if (r == -1) {
|
||||
Serial.print("otp_pad_sd: no pad matching prefix '");
|
||||
Serial.print(chksum_or_prefix); Serial.println("'");
|
||||
return 4;
|
||||
} else if (r == -2) {
|
||||
Serial.print("otp_pad_sd: ambiguous prefix '");
|
||||
Serial.print(chksum_or_prefix); Serial.println("'");
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
|
||||
snprintf(g_sd.pad_path, sizeof(g_sd.pad_path), "%s/%s.pad",
|
||||
OTP_SD_PADS_DIR, chksum);
|
||||
strncpy(g_sd.chksum, chksum, OTP_SD_CHKSUM_MAX - 1);
|
||||
g_sd.chksum[OTP_SD_CHKSUM_MAX - 1] = '\0';
|
||||
|
||||
File32 f = sd.open(g_sd.pad_path, O_RDONLY);
|
||||
if (!f) {
|
||||
Serial.print("otp_pad_sd: cannot open "); Serial.println(g_sd.pad_path);
|
||||
return 6;
|
||||
}
|
||||
g_sd.pad_size = (uint64_t)f.size();
|
||||
if (g_sd.pad_size < OTPPAD_E_HEADER_RESERVED) {
|
||||
Serial.println("otp_pad_sd: pad too small");
|
||||
f.close();
|
||||
return 7;
|
||||
}
|
||||
|
||||
g_sd.pad_file = f;
|
||||
/* Skip the boot-time checksum verify — it reads the entire pad (1 MB on
|
||||
* the test card, up to 900 GB on a production card) and is too slow at
|
||||
* boot. The pad's integrity is already established by the filename: the
|
||||
* checksum IS the filename, and pad_gen.ino verified it at generation
|
||||
* time. A future otp_verify verb can do an on-demand check. */
|
||||
|
||||
uint64_t offset;
|
||||
if (otppad_e_state_read_sd(OTP_SD_PADS_DIR, g_sd.chksum, &offset) != 0) {
|
||||
offset = OTPPAD_E_HEADER_RESERVED;
|
||||
if (otppad_e_state_write_sd(OTP_SD_PADS_DIR, g_sd.chksum, offset) != 0) {
|
||||
Serial.println("otp_pad_sd: cannot write initial .state");
|
||||
g_sd.pad_file.close();
|
||||
return 9;
|
||||
}
|
||||
}
|
||||
if (offset < OTPPAD_E_HEADER_RESERVED) {
|
||||
Serial.print("otp_pad_sd: offset < reserved header: ");
|
||||
Serial.println((unsigned long)offset);
|
||||
g_sd.pad_file.close();
|
||||
return 10;
|
||||
}
|
||||
if (offset > g_sd.pad_size) {
|
||||
Serial.println("otp_pad_sd: offset past end of pad");
|
||||
g_sd.pad_file.close();
|
||||
return 11;
|
||||
}
|
||||
|
||||
g_sd.bound = 1;
|
||||
Serial.print("otp_pad_sd: bound pad ");
|
||||
Serial.print(g_sd.chksum);
|
||||
Serial.print(" (");
|
||||
Serial.print((unsigned long)g_sd.pad_size);
|
||||
Serial.print(" bytes, offset=");
|
||||
Serial.print((unsigned long)offset);
|
||||
Serial.println(")");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int otp_pad_sd_bind_first(void) {
|
||||
char chksum[OTPPAD_E_CHKSUM_HEX_LEN + 1];
|
||||
int r = resolve_pad(NULL, chksum);
|
||||
if (r != 0) {
|
||||
Serial.println("otp_pad_sd: no pads found in /pads");
|
||||
return 1;
|
||||
}
|
||||
return otp_pad_sd_bind(chksum);
|
||||
}
|
||||
|
||||
void otp_pad_sd_unbind(void) {
|
||||
if (g_sd.pad_file) {
|
||||
g_sd.pad_file.close();
|
||||
}
|
||||
secure_memzero(&g_sd, sizeof(g_sd));
|
||||
}
|
||||
|
||||
int otp_pad_sd_ready(void) { return g_sd.bound ? 1 : 0; }
|
||||
const char *otp_pad_sd_chksum(void) { return g_sd.bound ? g_sd.chksum : NULL; }
|
||||
|
||||
uint64_t otp_pad_sd_offset(void) {
|
||||
if (!g_sd.bound) return 0;
|
||||
uint64_t off;
|
||||
if (otppad_e_state_read_sd(OTP_SD_PADS_DIR, g_sd.chksum, &off) != 0) {
|
||||
return 0;
|
||||
}
|
||||
return off;
|
||||
}
|
||||
|
||||
uint64_t otp_pad_sd_size(void) { return g_sd.bound ? g_sd.pad_size : 0; }
|
||||
|
||||
/* Debug: list files in /pads into `out`. Returns file count. */
|
||||
int otp_pad_sd_debug_list(char *out, size_t cap) {
|
||||
if (out && cap > 0) out[0] = '\0';
|
||||
File32 dir = sd.open(OTP_SD_PADS_DIR);
|
||||
if (!dir) {
|
||||
if (out && cap > 20) snprintf(out, cap, "sd.open(/pads) FAILED");
|
||||
return -1;
|
||||
}
|
||||
int count = 0;
|
||||
while (count < 10) {
|
||||
File32 entry = dir.openNextFile();
|
||||
if (!entry) break;
|
||||
if (!entry.isDir()) {
|
||||
char name[128];
|
||||
entry.getName(name, sizeof(name));
|
||||
size_t cur = out ? strlen(out) : 0;
|
||||
size_t remain = cap > cur ? cap - cur : 0;
|
||||
if (remain > strlen(name) + 16) {
|
||||
snprintf(out + cur, remain, "[%d] %s (%lu bytes)\n",
|
||||
count, name, (unsigned long)entry.size());
|
||||
}
|
||||
count++;
|
||||
}
|
||||
entry.close();
|
||||
}
|
||||
dir.close();
|
||||
return count;
|
||||
}
|
||||
|
||||
/* Scan /pads for *.pad files and fill chksums + sizes arrays. */
|
||||
int otp_pad_sd_list_pads(char chksums[][65], uint64_t sizes[], int max_count) {
|
||||
if (!chksums || !sizes || max_count <= 0) return 0;
|
||||
File32 dir = sd.open(OTP_SD_PADS_DIR);
|
||||
if (!dir) return -1;
|
||||
int count = 0;
|
||||
while (count < max_count) {
|
||||
File32 entry = dir.openNextFile();
|
||||
if (!entry) break;
|
||||
if (!entry.isDir()) {
|
||||
char name[128];
|
||||
entry.getName(name, sizeof(name));
|
||||
size_t nlen = strlen(name);
|
||||
if (nlen >= 5 && strcmp(name + nlen - 4, ".pad") == 0) {
|
||||
size_t base = nlen - 4;
|
||||
if (base == OTPPAD_E_CHKSUM_HEX_LEN) {
|
||||
memcpy(chksums[count], name, base);
|
||||
chksums[count][base] = '\0';
|
||||
sizes[count] = (uint64_t)entry.size();
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
entry.close();
|
||||
}
|
||||
dir.close();
|
||||
return count;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Encrypt / decrypt */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
int otp_pad_sd_encrypt(const unsigned char *plaintext, size_t pt_len,
|
||||
const char *encoding,
|
||||
char **out_payload, size_t *out_payload_len,
|
||||
uint64_t *out_off_before, uint64_t *out_off_after) {
|
||||
if (!g_sd.bound) return 1;
|
||||
if (!plaintext || !out_payload || !out_payload_len ||
|
||||
!out_off_before || !out_off_after) return 2;
|
||||
*out_payload = NULL;
|
||||
*out_payload_len = 0;
|
||||
|
||||
size_t chunk = otppad_e_chunk_size(pt_len);
|
||||
if (chunk > OTP_SD_MAX_CHUNK) {
|
||||
Serial.print("otp_pad_sd: chunk too large: ");
|
||||
Serial.println((unsigned long)chunk);
|
||||
return 3;
|
||||
}
|
||||
unsigned char *buf = (unsigned char *)malloc(chunk);
|
||||
if (!buf) return 4;
|
||||
memcpy(buf, plaintext, pt_len);
|
||||
if (otppad_e_pad_apply(buf, pt_len, chunk) != 0) { free(buf); return 5; }
|
||||
|
||||
uint64_t offset;
|
||||
if (otppad_e_state_read_sd(OTP_SD_PADS_DIR, g_sd.chksum, &offset) != 0) {
|
||||
free(buf); return 6;
|
||||
}
|
||||
if (offset + chunk > g_sd.pad_size) {
|
||||
Serial.println("otp_pad_sd: pad exhausted");
|
||||
free(buf); return 7;
|
||||
}
|
||||
*out_off_before = offset;
|
||||
|
||||
unsigned char *pad_slice = (unsigned char *)malloc(chunk);
|
||||
if (!pad_slice) { free(buf); return 8; }
|
||||
if (read_pad_slice(offset, chunk, pad_slice) != 0) {
|
||||
free(buf); free(pad_slice); return 9;
|
||||
}
|
||||
for (size_t i = 0; i < chunk; i++) {
|
||||
buf[i] ^= pad_slice[i];
|
||||
}
|
||||
secure_memzero(pad_slice, chunk);
|
||||
free(pad_slice);
|
||||
|
||||
uint64_t new_offset = offset + chunk;
|
||||
if (otppad_e_state_write_sd(OTP_SD_PADS_DIR, g_sd.chksum, new_offset) != 0) {
|
||||
secure_memzero(buf, chunk); free(buf); return 10;
|
||||
}
|
||||
*out_off_after = new_offset;
|
||||
|
||||
if (encoding && strcmp(encoding, "binary") == 0) {
|
||||
otppad_e_bin_header_t hdr;
|
||||
memset(&hdr, 0, sizeof(hdr));
|
||||
memcpy(hdr.magic, OTPPAD_E_MAGIC, OTPPAD_E_MAGIC_LEN);
|
||||
hdr.version = OTPPAD_E_FORMAT_VERSION;
|
||||
for (int i = 0; i < OTPPAD_E_CHKSUM_BIN_LEN; i++) {
|
||||
unsigned int byte;
|
||||
sscanf(g_sd.chksum + i * 2, "%02x", &byte);
|
||||
hdr.pad_chksum[i] = (unsigned char)byte;
|
||||
}
|
||||
hdr.pad_offset = offset;
|
||||
hdr.file_mode = 0644;
|
||||
hdr.file_size = pt_len;
|
||||
|
||||
size_t blob_size = 58 + chunk;
|
||||
unsigned char *blob = (unsigned char *)malloc(blob_size);
|
||||
if (!blob) { secure_memzero(buf, chunk); free(buf); return 11; }
|
||||
if (otppad_e_bin_header_pack(&hdr, blob, 58) != 0) {
|
||||
free(blob); secure_memzero(buf, chunk); free(buf); return 12;
|
||||
}
|
||||
memcpy(blob + 58, buf, chunk);
|
||||
*out_payload = (char *)blob;
|
||||
*out_payload_len = blob_size;
|
||||
} else {
|
||||
char *armor = NULL;
|
||||
if (otppad_e_armor_generate("teensy41-nsigner", g_sd.chksum, offset,
|
||||
buf, chunk, &armor) != 0) {
|
||||
secure_memzero(buf, chunk); free(buf); return 13;
|
||||
}
|
||||
*out_payload = armor;
|
||||
*out_payload_len = strlen(armor);
|
||||
}
|
||||
|
||||
secure_memzero(buf, chunk);
|
||||
free(buf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int otp_pad_sd_decrypt(const char *input, size_t input_len,
|
||||
const char *encoding,
|
||||
unsigned char **out_plaintext, size_t *out_pt_len) {
|
||||
if (!g_sd.bound) return 1;
|
||||
if (!input || !out_plaintext || !out_pt_len) return 2;
|
||||
*out_plaintext = NULL;
|
||||
*out_pt_len = 0;
|
||||
|
||||
uint64_t offset;
|
||||
size_t chunk;
|
||||
unsigned char *ciphertext = NULL;
|
||||
size_t ct_len = 0;
|
||||
|
||||
int is_binary;
|
||||
if (encoding && strcmp(encoding, "binary") == 0) {
|
||||
is_binary = 1;
|
||||
} else if (encoding && strcmp(encoding, "ascii") == 0) {
|
||||
is_binary = 0;
|
||||
} else {
|
||||
is_binary = (input_len >= 4 &&
|
||||
memcmp(input, OTPPAD_E_MAGIC, 4) == 0);
|
||||
}
|
||||
|
||||
if (!is_binary) {
|
||||
char chksum[OTPPAD_E_CHKSUM_HEX_LEN + 1];
|
||||
size_t b64_cap = (OTP_SD_MAX_CHUNK / 3 * 4) + 256;
|
||||
char *b64 = (char *)malloc(b64_cap);
|
||||
if (!b64) return 3;
|
||||
if (otppad_e_armor_parse(input, chksum, &offset, b64, b64_cap) != 0) {
|
||||
free(b64); return 4;
|
||||
}
|
||||
if (strcmp(chksum, g_sd.chksum) != 0) {
|
||||
free(b64); return 5;
|
||||
}
|
||||
int dlen = 0;
|
||||
ciphertext = otppad_e_base64_decode(b64, &dlen);
|
||||
free(b64);
|
||||
if (!ciphertext) return 6;
|
||||
ct_len = (size_t)dlen;
|
||||
chunk = ct_len;
|
||||
} else {
|
||||
if (input_len < 58) return 7;
|
||||
otppad_e_bin_header_t hdr;
|
||||
if (otppad_e_bin_header_unpack((const unsigned char *)input, input_len,
|
||||
&hdr) != 0) return 8;
|
||||
if (!otppad_e_bin_is_magic((const unsigned char *)input, input_len)) {
|
||||
return 9;
|
||||
}
|
||||
char chksum_hex[OTPPAD_E_CHKSUM_HEX_LEN + 1];
|
||||
bytes_to_hex(hdr.pad_chksum, OTPPAD_E_CHKSUM_BIN_LEN, chksum_hex);
|
||||
if (strcmp(chksum_hex, g_sd.chksum) != 0) return 10;
|
||||
offset = hdr.pad_offset;
|
||||
ct_len = input_len - 58;
|
||||
chunk = ct_len;
|
||||
ciphertext = (unsigned char *)malloc(ct_len ? ct_len : 1);
|
||||
if (!ciphertext) return 11;
|
||||
memcpy(ciphertext, input + 58, ct_len);
|
||||
}
|
||||
|
||||
if (chunk > OTP_SD_MAX_CHUNK) { free(ciphertext); return 12; }
|
||||
if (offset + chunk > g_sd.pad_size) { free(ciphertext); return 13; }
|
||||
|
||||
unsigned char *buf = (unsigned char *)malloc(chunk);
|
||||
if (!buf) { free(ciphertext); return 14; }
|
||||
unsigned char *pad_slice = (unsigned char *)malloc(chunk);
|
||||
if (!pad_slice) { free(buf); free(ciphertext); return 15; }
|
||||
if (read_pad_slice(offset, chunk, pad_slice) != 0) {
|
||||
free(buf); free(pad_slice); free(ciphertext); return 16;
|
||||
}
|
||||
for (size_t i = 0; i < chunk; i++) {
|
||||
buf[i] = ciphertext[i] ^ pad_slice[i];
|
||||
}
|
||||
secure_memzero(pad_slice, chunk);
|
||||
free(pad_slice);
|
||||
secure_memzero(ciphertext, ct_len);
|
||||
free(ciphertext);
|
||||
|
||||
size_t pt_len;
|
||||
if (otppad_e_pad_remove(buf, chunk, &pt_len) != 0) {
|
||||
secure_memzero(buf, chunk); free(buf); return 17;
|
||||
}
|
||||
|
||||
unsigned char *pt = (unsigned char *)malloc(pt_len ? pt_len : 1);
|
||||
if (!pt) { secure_memzero(buf, chunk); free(buf); return 18; }
|
||||
memcpy(pt, buf, pt_len);
|
||||
secure_memzero(buf, chunk);
|
||||
free(buf);
|
||||
|
||||
*out_plaintext = pt;
|
||||
*out_pt_len = pt_len;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/* otp_pad_sd.h — SD-card one-time-pad for the Teensy 4.1 n_signer firmware.
|
||||
*
|
||||
* Replaces the old HKDF-derived in-RAM pad (otp_pad.h/otp_pad.cpp) with a real
|
||||
* SD-card pad that reads <chksum>.pad / <chksum>.state from the Teensy's
|
||||
* built-in SD slot, bit-compatible with the `otp` project and the host
|
||||
* n_signer (src/otp_pad.c) via otppad_embedded (a port of libotppad).
|
||||
*
|
||||
* One pad per session. The pad file is opened read-only and kept open for the
|
||||
* lifetime of the session. The per-pad .state file (offset counter) is read
|
||||
* and written via otppad_e_state_read_sd / otppad_e_state_write_sd (atomic
|
||||
* write-temp-then-rename on the SD card).
|
||||
*
|
||||
* Pad bytes are never loaded whole into RAM. Each encrypt/decrypt request
|
||||
* seeks to the current offset and reads exactly the chunk it needs into a
|
||||
* DMAMEM scratch buffer.
|
||||
*
|
||||
* Wire format (matches host otp_encrypt/otp_decrypt):
|
||||
* encrypt: [plaintext_b64, {"encoding": "ascii"|"binary"}]
|
||||
* -> {"ciphertext": ..., "pad_chksum": ..., "pad_offset_before": N,
|
||||
* "pad_offset_after": N}
|
||||
* decrypt: [ciphertext, {"encoding": "ascii"|"binary"}]
|
||||
* -> {"plaintext": "<b64>"} (offset read from armor/binary header)
|
||||
*/
|
||||
#ifndef FIRMWARE_TEENSY41_SIGNER_OTP_PAD_SD_H
|
||||
#define FIRMWARE_TEENSY41_SIGNER_OTP_PAD_SD_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Max chunk size we will Padmé-pad and XOR in RAM. 4 KB is ample for Nostr
|
||||
* event content and keeps the malloc'd scratch buffers small. Padmé buckets
|
||||
* up to 4 KB cover plaintexts up to ~3.9 KB; larger payloads need
|
||||
* caller-side chunking. */
|
||||
#define OTP_SD_MAX_CHUNK 4096
|
||||
|
||||
/* Mount the SD card via SD.begin(BUILTIN_SDCARD). Returns 0 on success,
|
||||
* non-zero if no card / bad card. Must be called once at boot before bind. */
|
||||
int otp_pad_sd_mount(void);
|
||||
|
||||
/* Bind a pad by its 64-hex-char checksum (or a unique prefix). Opens the pad
|
||||
* read-only, verifies the checksum, reads the offset from .state (defaulting
|
||||
* to 32 if no .state file). Returns 0 on success, non-zero on error. */
|
||||
int otp_pad_sd_bind(const char *chksum_or_prefix);
|
||||
|
||||
/* Debug auto-bind: scan the SD root for the first *.pad file and bind it.
|
||||
* Returns 0 on success, non-zero if no pad found or bind failed. */
|
||||
int otp_pad_sd_bind_first(void);
|
||||
|
||||
/* Unbind: close the pad file, zeroize state. */
|
||||
void otp_pad_sd_unbind(void);
|
||||
|
||||
/* Whether a pad is bound for this session. */
|
||||
int otp_pad_sd_ready(void);
|
||||
|
||||
/* The bound pad's 64-hex-char checksum, or NULL if not bound. */
|
||||
const char *otp_pad_sd_chksum(void);
|
||||
|
||||
/* Current offset from the .state file, or 0 if not bound. */
|
||||
uint64_t otp_pad_sd_offset(void);
|
||||
|
||||
/* Total pad file size in bytes, or 0 if not bound. */
|
||||
uint64_t otp_pad_sd_size(void);
|
||||
|
||||
/* Debug: list files in /pads into `out` (caller-provided buffer, size `cap`).
|
||||
* Returns the number of files found. Used by the otp_debug verb to diagnose
|
||||
* bind failures without needing serial boot output. */
|
||||
int otp_pad_sd_debug_list(char *out, size_t cap);
|
||||
|
||||
/* Scan /pads for *.pad files and fill the caller's arrays with chksums + sizes.
|
||||
* `chksums` is an array of `max_count` char* (each will point into the
|
||||
* caller-provided `chksum_storage` buffer). `sizes` is an array of uint64_t.
|
||||
* Returns the number of pads found (0..max_count), or -1 on error. */
|
||||
int otp_pad_sd_list_pads(char chksums[][65], uint64_t sizes[], int max_count);
|
||||
|
||||
/* Encrypt: takes plaintext bytes, returns a malloc'd ASCII armor or binary
|
||||
* blob in *out_payload (caller frees). `encoding` is "ascii" or "binary".
|
||||
* On success returns 0 and sets *out_payload_len, *out_off_before,
|
||||
* *out_off_after. Advances the offset in .state atomically. */
|
||||
int otp_pad_sd_encrypt(const unsigned char *plaintext, size_t pt_len,
|
||||
const char *encoding,
|
||||
char **out_payload, size_t *out_payload_len,
|
||||
uint64_t *out_off_before, uint64_t *out_off_after);
|
||||
|
||||
/* Decrypt: takes ASCII armor or binary blob, returns malloc'd plaintext in
|
||||
* *out_plaintext (caller frees). `encoding` is "ascii" or "binary" (auto-
|
||||
* detected if NULL). Does NOT advance the offset (decrypt is non-consuming,
|
||||
* matching the host). On success returns 0 and sets *out_pt_len. */
|
||||
int otp_pad_sd_decrypt(const char *input, size_t input_len,
|
||||
const char *encoding,
|
||||
unsigned char **out_plaintext, size_t *out_pt_len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* FIRMWARE_TEENSY41_SIGNER_OTP_PAD_SD_H */
|
||||
@@ -0,0 +1,443 @@
|
||||
/* otppad_embedded.c — implementation of otppad_embedded.h.
|
||||
*
|
||||
* Bit-compatible port of libotppad (libotppad/libotppad.c) for the Teensy 4.1
|
||||
* firmware. The pure format functions (XOR, base64, Padmé, armor, binary
|
||||
* header) are straight ports. The I/O functions are split:
|
||||
* - HOST_TEST defined: POSIX FILE-star / mkstemp / rename (host unit tests).
|
||||
* - otherwise: Arduino SD library (linked from the C++ side via thin
|
||||
* wrappers in otp_pad_sd.cpp; the state read/write helpers here call
|
||||
* through small C shims that otp_pad_sd.cpp provides).
|
||||
*
|
||||
* To keep this file pure C and buildable on both host and Teensy without
|
||||
* pulling Arduino headers here, the SD state I/O is implemented in
|
||||
* otp_pad_sd.cpp (C++) and declared here only under the non-HOST_TEST path as
|
||||
* the otppad_e_state_read_sd / otppad_e_state_write_sd prototypes (already in
|
||||
* the header). This file does NOT implement them; otp_pad_sd.cpp does.
|
||||
*/
|
||||
|
||||
#include "otppad_embedded.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#ifdef HOST_TEST
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#endif
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* XOR transform */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
int otppad_e_xor(const unsigned char *data, size_t data_len,
|
||||
const unsigned char *pad_data, unsigned char *result) {
|
||||
if (!data || !pad_data || !result) return 1;
|
||||
for (size_t i = 0; i < data_len; i++) {
|
||||
result[i] = data[i] ^ pad_data[i];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Base64 (identical tables/algorithm to libotppad) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static const char b64_chars[] =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
static const int b64_decode_table[256] = {
|
||||
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
||||
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
||||
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63,
|
||||
52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-2,-1,-1,
|
||||
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,
|
||||
15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,
|
||||
-1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,
|
||||
41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1,
|
||||
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
||||
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
||||
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
||||
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
||||
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
||||
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
||||
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
||||
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1
|
||||
};
|
||||
|
||||
char *otppad_e_base64_encode(const unsigned char *input, int length) {
|
||||
if (!input || length < 0) return NULL;
|
||||
int output_length = 4 * ((length + 2) / 3);
|
||||
char *encoded = (char *)malloc((size_t)output_length + 1);
|
||||
if (!encoded) return NULL;
|
||||
|
||||
int i, j;
|
||||
for (i = 0, j = 0; i < length;) {
|
||||
uint32_t octet_a = i < length ? input[i++] : 0;
|
||||
uint32_t octet_b = i < length ? input[i++] : 0;
|
||||
uint32_t octet_c = i < length ? input[i++] : 0;
|
||||
uint32_t triple = (octet_a << 16) + (octet_b << 8) + octet_c;
|
||||
encoded[j++] = b64_chars[(triple >> 18) & 63];
|
||||
encoded[j++] = b64_chars[(triple >> 12) & 63];
|
||||
encoded[j++] = b64_chars[(triple >> 6) & 63];
|
||||
encoded[j++] = b64_chars[triple & 63];
|
||||
}
|
||||
for (int pad = 0; pad < (3 - length % 3) % 3; pad++) {
|
||||
encoded[output_length - 1 - pad] = '=';
|
||||
}
|
||||
encoded[output_length] = '\0';
|
||||
return encoded;
|
||||
}
|
||||
|
||||
unsigned char *otppad_e_base64_decode(const char *input, int *output_length) {
|
||||
if (!input || !output_length) return NULL;
|
||||
int input_length = (int)strlen(input);
|
||||
if (input_length % 4 != 0) return NULL;
|
||||
|
||||
*output_length = input_length / 4 * 3;
|
||||
if (input[input_length - 1] == '=') (*output_length)--;
|
||||
if (input[input_length - 2] == '=') (*output_length)--;
|
||||
|
||||
unsigned char *decoded = (unsigned char *)malloc((size_t)*output_length);
|
||||
if (!decoded) return NULL;
|
||||
|
||||
int i, j;
|
||||
for (i = 0, j = 0; i < input_length;) {
|
||||
int sa = input[i] == '=' ? 0 & i++ : b64_decode_table[(unsigned char)input[i++]];
|
||||
int sb = input[i] == '=' ? 0 & i++ : b64_decode_table[(unsigned char)input[i++]];
|
||||
int sc = input[i] == '=' ? 0 & i++ : b64_decode_table[(unsigned char)input[i++]];
|
||||
int sd = input[i] == '=' ? 0 & i++ : b64_decode_table[(unsigned char)input[i++]];
|
||||
if (sa == -1 || sb == -1 || sc == -1 || sd == -1) {
|
||||
free(decoded);
|
||||
return NULL;
|
||||
}
|
||||
uint32_t triple = ((uint32_t)sa << 18) + ((uint32_t)sb << 12) +
|
||||
((uint32_t)sc << 6) + (uint32_t)sd;
|
||||
if (j < *output_length) decoded[j++] = (triple >> 16) & 255;
|
||||
if (j < *output_length) decoded[j++] = (triple >> 8) & 255;
|
||||
if (j < *output_length) decoded[j++] = triple & 255;
|
||||
}
|
||||
return decoded;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Padmé padding (identical to libotppad) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
size_t otppad_e_chunk_size(size_t msg_len) {
|
||||
size_t chunk = 256;
|
||||
while (chunk < msg_len + 1) {
|
||||
chunk *= 2;
|
||||
}
|
||||
return chunk;
|
||||
}
|
||||
|
||||
int otppad_e_pad_apply(unsigned char *buffer, size_t msg_len, size_t chunk_size) {
|
||||
if (!buffer) return 1;
|
||||
if (chunk_size < msg_len + 1) return 2;
|
||||
buffer[msg_len] = 0x80;
|
||||
if (chunk_size > msg_len + 1) {
|
||||
memset(buffer + msg_len + 1, 0x00, chunk_size - msg_len - 1);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int otppad_e_pad_remove(const unsigned char *buffer, size_t chunk_size,
|
||||
size_t *msg_len) {
|
||||
if (!buffer || !msg_len) return 1;
|
||||
if (chunk_size == 0) return 2;
|
||||
for (int i = (int)chunk_size - 1; i >= 0; i--) {
|
||||
if (buffer[i] == 0x80) {
|
||||
*msg_len = (size_t)i;
|
||||
return 0;
|
||||
} else if (buffer[i] != 0x00) {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
return 4;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* ASCII armored message format */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* Manual line splitter (replaces strtok). Parses `message` line by line. */
|
||||
int otppad_e_armor_parse(const char *message, char *chksum, uint64_t *offset,
|
||||
char *base64_data, size_t base64_buf_size) {
|
||||
if (!message || !chksum || !offset || !base64_data || base64_buf_size == 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t msg_len = strlen(message);
|
||||
char *copy = (char *)malloc(msg_len + 1);
|
||||
if (!copy) return 1;
|
||||
strcpy(copy, message);
|
||||
|
||||
int found_begin = 0, in_data = 0, found_chksum = 0, found_offset = 0;
|
||||
chksum[0] = '\0';
|
||||
*offset = 0;
|
||||
base64_data[0] = '\0';
|
||||
|
||||
char *line = copy;
|
||||
char *next = copy;
|
||||
while (next != NULL) {
|
||||
/* find end of line */
|
||||
char *nl = strchr(line, '\n');
|
||||
if (nl) { *nl = '\0'; next = nl + 1; }
|
||||
else { next = NULL; }
|
||||
/* strip trailing \r */
|
||||
size_t llen = strlen(line);
|
||||
if (llen > 0 && line[llen - 1] == '\r') line[llen - 1] = '\0';
|
||||
|
||||
if (strcmp(line, OTPPAD_E_ARMOR_BEGIN) == 0) {
|
||||
found_begin = 1;
|
||||
} else if (strcmp(line, OTPPAD_E_ARMOR_END) == 0) {
|
||||
break;
|
||||
} else if (found_begin) {
|
||||
if (strncmp(line, "Pad-ChkSum: ", 12) == 0) {
|
||||
strncpy(chksum, line + 12, OTPPAD_E_CHKSUM_HEX_LEN);
|
||||
chksum[OTPPAD_E_CHKSUM_HEX_LEN] = '\0';
|
||||
found_chksum = 1;
|
||||
} else if (strncmp(line, "Pad-Offset: ", 12) == 0) {
|
||||
*offset = strtoull(line + 12, NULL, 10);
|
||||
found_offset = 1;
|
||||
} else if (strlen(line) == 0) {
|
||||
in_data = 1;
|
||||
} else if (in_data) {
|
||||
size_t cur = strlen(base64_data);
|
||||
size_t add = strlen(line);
|
||||
if (cur + add + 1 <= base64_buf_size) {
|
||||
strncat(base64_data, line, base64_buf_size - cur - 1);
|
||||
}
|
||||
} else if (strncmp(line, "Version:", 8) != 0 &&
|
||||
strncmp(line, "Pad-", 4) != 0) {
|
||||
/* non-header, non-empty line before the blank separator —
|
||||
* treat as data (matches libotppad's fallthrough). */
|
||||
size_t cur = strlen(base64_data);
|
||||
size_t add = strlen(line);
|
||||
if (cur + add + 1 <= base64_buf_size) {
|
||||
strncat(base64_data, line, base64_buf_size - cur - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
line = next;
|
||||
}
|
||||
|
||||
free(copy);
|
||||
if (!found_begin || !found_chksum || !found_offset) {
|
||||
return 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int otppad_e_armor_generate(const char *version, const char *chksum,
|
||||
uint64_t offset,
|
||||
const unsigned char *encrypted_data, size_t data_length,
|
||||
char **ascii_output) {
|
||||
if (!chksum || !encrypted_data || !ascii_output) return 1;
|
||||
|
||||
char *b64 = otppad_e_base64_encode(encrypted_data, (int)data_length);
|
||||
if (!b64) return 2;
|
||||
|
||||
size_t b64_len = strlen(b64);
|
||||
size_t total = 256 + b64_len + (b64_len / 64) + 64;
|
||||
*ascii_output = (char *)malloc(total);
|
||||
if (!*ascii_output) {
|
||||
free(b64);
|
||||
return 3;
|
||||
}
|
||||
|
||||
char line[256];
|
||||
strcpy(*ascii_output, OTPPAD_E_ARMOR_BEGIN);
|
||||
strcat(*ascii_output, "\n");
|
||||
|
||||
snprintf(line, sizeof(line), "Version: %s\n", version ? version : "v0");
|
||||
strcat(*ascii_output, line);
|
||||
|
||||
snprintf(line, sizeof(line), "Pad-ChkSum: %s\n", chksum);
|
||||
strcat(*ascii_output, line);
|
||||
|
||||
snprintf(line, sizeof(line), "Pad-Offset: %llu\n",
|
||||
(unsigned long long)offset);
|
||||
strcat(*ascii_output, line);
|
||||
|
||||
strcat(*ascii_output, "\n");
|
||||
|
||||
int b64_len_int = (int)b64_len;
|
||||
for (int i = 0; i < b64_len_int; i += 64) {
|
||||
snprintf(line, sizeof(line), "%.64s\n", b64 + i);
|
||||
strcat(*ascii_output, line);
|
||||
}
|
||||
|
||||
strcat(*ascii_output, OTPPAD_E_ARMOR_END);
|
||||
strcat(*ascii_output, "\n");
|
||||
|
||||
free(b64);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Binary .otp header (58 bytes, little-endian) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* Pack a uint16 little-endian. */
|
||||
static void put_u16le(unsigned char *p, uint16_t v) {
|
||||
p[0] = (unsigned char)(v & 0xFF);
|
||||
p[1] = (unsigned char)((v >> 8) & 0xFF);
|
||||
}
|
||||
static uint16_t get_u16le(const unsigned char *p) {
|
||||
return (uint16_t)p[0] | ((uint16_t)p[1] << 8);
|
||||
}
|
||||
static void put_u32le(unsigned char *p, uint32_t v) {
|
||||
p[0] = (unsigned char)(v & 0xFF);
|
||||
p[1] = (unsigned char)((v >> 8) & 0xFF);
|
||||
p[2] = (unsigned char)((v >> 16) & 0xFF);
|
||||
p[3] = (unsigned char)((v >> 24) & 0xFF);
|
||||
}
|
||||
static uint32_t get_u32le(const unsigned char *p) {
|
||||
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
|
||||
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
|
||||
}
|
||||
static void put_u64le(unsigned char *p, uint64_t v) {
|
||||
for (int i = 0; i < 8; i++) {
|
||||
p[i] = (unsigned char)((v >> (8 * i)) & 0xFF);
|
||||
}
|
||||
}
|
||||
static uint64_t get_u64le(const unsigned char *p) {
|
||||
uint64_t v = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
v |= ((uint64_t)p[i]) << (8 * i);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
int otppad_e_bin_header_pack(const otppad_e_bin_header_t *hdr,
|
||||
unsigned char *out, size_t out_len) {
|
||||
if (!hdr || !out) return 1;
|
||||
if (out_len < 58) return 2;
|
||||
unsigned char *p = out;
|
||||
memcpy(p, OTPPAD_E_MAGIC, OTPPAD_E_MAGIC_LEN); p += 4;
|
||||
put_u16le(p, hdr->version); p += 2;
|
||||
memcpy(p, hdr->pad_chksum, OTPPAD_E_CHKSUM_BIN_LEN); p += OTPPAD_E_CHKSUM_BIN_LEN;
|
||||
put_u64le(p, hdr->pad_offset); p += 8;
|
||||
put_u32le(p, hdr->file_mode); p += 4;
|
||||
put_u64le(p, hdr->file_size); p += 8;
|
||||
/* p - out == 58 */
|
||||
return 0;
|
||||
}
|
||||
|
||||
int otppad_e_bin_header_unpack(const unsigned char *in, size_t in_len,
|
||||
otppad_e_bin_header_t *hdr) {
|
||||
if (!in || !hdr) return 1;
|
||||
if (in_len < 58) return 2;
|
||||
memset(hdr, 0, sizeof(*hdr));
|
||||
const unsigned char *p = in;
|
||||
memcpy(hdr->magic, p, OTPPAD_E_MAGIC_LEN); p += 4;
|
||||
hdr->version = get_u16le(p); p += 2;
|
||||
memcpy(hdr->pad_chksum, p, OTPPAD_E_CHKSUM_BIN_LEN); p += OTPPAD_E_CHKSUM_BIN_LEN;
|
||||
hdr->pad_offset = get_u64le(p); p += 8;
|
||||
hdr->file_mode = get_u32le(p); p += 4;
|
||||
hdr->file_size = get_u64le(p); p += 8;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int otppad_e_bin_is_magic(const unsigned char *buf, size_t len) {
|
||||
if (!buf || len < OTPPAD_E_MAGIC_LEN) return 0;
|
||||
return memcmp(buf, OTPPAD_E_MAGIC, OTPPAD_E_MAGIC_LEN) == 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Pad checksum (streaming) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void otppad_e_checksum_init(otppad_e_checksum_ctx *ctx) {
|
||||
if (!ctx) return;
|
||||
memset(ctx->buckets, 0, OTPPAD_E_CHKSUM_BIN_LEN);
|
||||
}
|
||||
|
||||
void otppad_e_checksum_update(otppad_e_checksum_ctx *ctx,
|
||||
const unsigned char *data, size_t len,
|
||||
uint64_t abs_pos) {
|
||||
if (!ctx || !data) return;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
uint64_t pos = abs_pos + (uint64_t)i;
|
||||
unsigned char bucket = (unsigned char)(pos % OTPPAD_E_CHKSUM_BIN_LEN);
|
||||
ctx->buckets[bucket] ^= (unsigned char)data[i] ^
|
||||
(unsigned char)((pos >> 8) & 0xFF) ^
|
||||
(unsigned char)((pos >> 16) & 0xFF) ^
|
||||
(unsigned char)((pos >> 24) & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
void otppad_e_checksum_final(otppad_e_checksum_ctx *ctx,
|
||||
const unsigned char *pad_key, char *out_hex) {
|
||||
if (!ctx || !pad_key || !out_hex) return;
|
||||
unsigned char enc[OTPPAD_E_CHKSUM_BIN_LEN];
|
||||
for (int i = 0; i < OTPPAD_E_CHKSUM_BIN_LEN; i++) {
|
||||
enc[i] = ctx->buckets[i] ^ pad_key[i];
|
||||
}
|
||||
for (int i = 0; i < OTPPAD_E_CHKSUM_BIN_LEN; i++) {
|
||||
sprintf(out_hex + (i * 2), "%02x", enc[i]);
|
||||
}
|
||||
out_hex[OTPPAD_E_CHKSUM_HEX_LEN] = '\0';
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Per-pad .state file — HOST_TEST (POSIX) implementation */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#ifdef HOST_TEST
|
||||
int otppad_e_state_read_posix(const char *pads_dir, const char *chksum,
|
||||
uint64_t *offset) {
|
||||
if (!pads_dir || !chksum || !offset) return 1;
|
||||
char path[1024];
|
||||
snprintf(path, sizeof(path), "%s/%s.state", pads_dir, chksum);
|
||||
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) return 2;
|
||||
|
||||
char line[128];
|
||||
if (!fgets(line, sizeof(line), f)) {
|
||||
fclose(f);
|
||||
return 3;
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
if (strncmp(line, "offset=", 7) != 0) return 4;
|
||||
*offset = strtoull(line + 7, NULL, 10);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int otppad_e_state_write_posix(const char *pads_dir, const char *chksum,
|
||||
uint64_t offset) {
|
||||
if (!pads_dir || !chksum) return 1;
|
||||
char path[1024];
|
||||
char tmp[1100];
|
||||
snprintf(path, sizeof(path), "%s/%s.state", pads_dir, chksum);
|
||||
snprintf(tmp, sizeof(tmp), "%s/%s.state.tmp.XXXXXX", pads_dir, chksum);
|
||||
|
||||
int tfd = mkstemp(tmp);
|
||||
if (tfd < 0) return 2;
|
||||
FILE *f = fdopen(tfd, "w");
|
||||
if (!f) {
|
||||
close(tfd);
|
||||
unlink(tmp);
|
||||
return 3;
|
||||
}
|
||||
if (fprintf(f, "offset=%llu\n", (unsigned long long)offset) < 0) {
|
||||
fclose(f);
|
||||
unlink(tmp);
|
||||
return 4;
|
||||
}
|
||||
if (fclose(f) != 0) {
|
||||
unlink(tmp);
|
||||
return 5;
|
||||
}
|
||||
if (rename(tmp, path) != 0) {
|
||||
unlink(tmp);
|
||||
return 6;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
#endif /* HOST_TEST */
|
||||
@@ -0,0 +1,180 @@
|
||||
/* otppad_embedded.h — Teensy/Arduino-friendly port of libotppad.
|
||||
*
|
||||
* Bit-compatible with libotppad (libotppad/libotppad.h) and the `otp` project:
|
||||
* - XOR transform
|
||||
* - ASCII armored message format ("-----BEGIN OTP MESSAGE-----")
|
||||
* - Binary .otp file format (magic "OTP\0", 58-byte header)
|
||||
* - ISO/IEC 9797-1 Method 2 (Padmé) padding with exponential bucketing
|
||||
* - Per-pad .state file ("offset=<n>\n")
|
||||
* - 256-bit XOR pad checksum (position-dependent, XORed with first 32 pad
|
||||
* bytes)
|
||||
*
|
||||
* Differences from libotppad:
|
||||
* - No POSIX FILE-star / mkstemp / rename dependencies in the core format
|
||||
* functions.
|
||||
* - The I/O functions (checksum, state read/write) are split into a
|
||||
* "buffer/stream" form (otppad_checksum_stream) and an SD-card form
|
||||
* (otppad_state_read_sd / otppad_state_write_sd) that take an Arduino
|
||||
* SD File and a pads-dir path. When compiled with -DHOST_TEST, the SD
|
||||
* File is replaced by a POSIX FILE* so the same source builds on the host
|
||||
* for bit-compatibility unit tests.
|
||||
*
|
||||
* License: same as libotppad / the otp project.
|
||||
*/
|
||||
#ifndef FIRMWARE_TEENSY41_SIGNER_OTPPAD_EMBEDDED_H
|
||||
#define FIRMWARE_TEENSY41_SIGNER_OTPPAD_EMBEDDED_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Constants (must match libotppad/libotppad.h exactly) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#define OTPPAD_E_CHKSUM_HEX_LEN 64
|
||||
#define OTPPAD_E_CHKSUM_BIN_LEN 32
|
||||
#define OTPPAD_E_HEADER_RESERVED 32
|
||||
#define OTPPAD_E_MAGIC "OTP\0" /* 4-byte binary file magic */
|
||||
#define OTPPAD_E_MAGIC_LEN 4
|
||||
#define OTPPAD_E_FORMAT_VERSION 1
|
||||
#define OTPPAD_E_ARMOR_BEGIN "-----BEGIN OTP MESSAGE-----"
|
||||
#define OTPPAD_E_ARMOR_END "-----END OTP MESSAGE-----"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* XOR transform */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* XOR `data_len` bytes of `data` with `pad_data` into `result`.
|
||||
* `result` may alias `data` or `pad_data`. Returns 0 on success, non-zero on
|
||||
* null pointer. */
|
||||
int otppad_e_xor(const unsigned char *data, size_t data_len,
|
||||
const unsigned char *pad_data, unsigned char *result);
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Base64 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* Encode `length` bytes of `input` as a NUL-terminated base64 string.
|
||||
* Caller frees the returned string. Returns NULL on allocation failure. */
|
||||
char *otppad_e_base64_encode(const unsigned char *input, int length);
|
||||
|
||||
/* Decode NUL-terminated base64 `input` into bytes.
|
||||
* Caller frees the returned buffer. *output_length receives the byte count.
|
||||
* Returns NULL on invalid input or allocation failure. */
|
||||
unsigned char *otppad_e_base64_decode(const char *input, int *output_length);
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Padmé padding (ISO/IEC 9797-1 Method 2) + exponential bucketing */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* Calculate the bucket size for a message of `msg_len` bytes.
|
||||
* Starts at 256 bytes and doubles until `chunk >= msg_len + 1`. */
|
||||
size_t otppad_e_chunk_size(size_t msg_len);
|
||||
|
||||
/* Apply Padmé padding to `buffer` (must hold `chunk_size` bytes).
|
||||
* Writes 0x80 at `buffer[msg_len]` then zeroes to `chunk_size`.
|
||||
* Returns 0 on success, non-zero on error. */
|
||||
int otppad_e_pad_apply(unsigned char *buffer, size_t msg_len, size_t chunk_size);
|
||||
|
||||
/* Remove Padmé padding: scan backwards for 0x80, set *msg_len to its index.
|
||||
* Returns 0 on success, non-zero on invalid padding. */
|
||||
int otppad_e_pad_remove(const unsigned char *buffer, size_t chunk_size,
|
||||
size_t *msg_len);
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* ASCII armored message format */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* Parse an ASCII-armored OTP message.
|
||||
* `chksum` must be at least OTPPAD_E_CHKSUM_HEX_LEN+1 bytes.
|
||||
* `base64_data` must be at least `base64_buf_size` bytes.
|
||||
* On success returns 0 and sets `chksum`, `*offset`, and `base64_data`. */
|
||||
int otppad_e_armor_parse(const char *message, char *chksum, uint64_t *offset,
|
||||
char *base64_data, size_t base64_buf_size);
|
||||
|
||||
/* Build an ASCII-armored OTP message.
|
||||
* On success returns 0 and sets `*ascii_output` to a malloc'd NUL-terminated
|
||||
* string. Caller frees `*ascii_output`. */
|
||||
int otppad_e_armor_generate(const char *version, const char *chksum,
|
||||
uint64_t offset,
|
||||
const unsigned char *encrypted_data, size_t data_length,
|
||||
char **ascii_output);
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Binary .otp header (58 bytes, little-endian on disk) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
typedef struct {
|
||||
char magic[OTPPAD_E_MAGIC_LEN];
|
||||
uint16_t version;
|
||||
unsigned char pad_chksum[OTPPAD_E_CHKSUM_BIN_LEN];
|
||||
uint64_t pad_offset;
|
||||
uint32_t file_mode;
|
||||
uint64_t file_size; /* original (unpadded) size */
|
||||
} otppad_e_bin_header_t;
|
||||
|
||||
/* Serialize a header into the 58-byte buffer `out` (little-endian, matching
|
||||
* libotppad's fwrite-of-host-endian-integers which is LE on ARM/x86). */
|
||||
int otppad_e_bin_header_pack(const otppad_e_bin_header_t *hdr,
|
||||
unsigned char *out, size_t out_len);
|
||||
|
||||
/* Parse a 58-byte buffer `in` into `hdr`. */
|
||||
int otppad_e_bin_header_unpack(const unsigned char *in, size_t in_len,
|
||||
otppad_e_bin_header_t *hdr);
|
||||
|
||||
/* Return 1 if the first 4 bytes of `buf` match the OTP magic. */
|
||||
int otppad_e_bin_is_magic(const unsigned char *buf, size_t len);
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Per-pad .state file (SD card) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* When HOST_TEST is defined, these use POSIX FILE* + rename for host unit
|
||||
* tests. Otherwise they use the Arduino SD library via the C++ side. */
|
||||
|
||||
#ifndef HOST_TEST
|
||||
/* Read the offset from `<pads_dir>/<chksum>.state`. Returns 0 on success. */
|
||||
int otppad_e_state_read_sd(const char *pads_dir, const char *chksum,
|
||||
uint64_t *offset);
|
||||
|
||||
/* Atomically write the offset to `<pads_dir>/<chksum>.state` via a temp file
|
||||
* + rename. Returns 0 on success. */
|
||||
int otppad_e_state_write_sd(const char *pads_dir, const char *chksum,
|
||||
uint64_t offset);
|
||||
#else
|
||||
int otppad_e_state_read_posix(const char *pads_dir, const char *chksum,
|
||||
uint64_t *offset);
|
||||
int otppad_e_state_write_posix(const char *pads_dir, const char *chksum,
|
||||
uint64_t offset);
|
||||
#endif
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Pad checksum */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* Streaming checksum accumulator. Call _init once, _update with each chunk
|
||||
* (passing the absolute byte position of the chunk's first byte), then _final
|
||||
* with the first 32 bytes of the pad (the "pad key") to get the 64-hex-char
|
||||
* checksum. Identical algorithm to libotppad otppad_checksum(). */
|
||||
typedef struct {
|
||||
unsigned char buckets[OTPPAD_E_CHKSUM_BIN_LEN];
|
||||
} otppad_e_checksum_ctx;
|
||||
|
||||
void otppad_e_checksum_init(otppad_e_checksum_ctx *ctx);
|
||||
void otppad_e_checksum_update(otppad_e_checksum_ctx *ctx,
|
||||
const unsigned char *data, size_t len,
|
||||
uint64_t abs_pos);
|
||||
/* `pad_key` must be 32 bytes (the first 32 bytes of the pad). `out_hex` must
|
||||
* be at least OTPPAD_E_CHKSUM_HEX_LEN+1 bytes. */
|
||||
void otppad_e_checksum_final(otppad_e_checksum_ctx *ctx,
|
||||
const unsigned char *pad_key, char *out_hex);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* FIRMWARE_TEENSY41_SIGNER_OTPPAD_EMBEDDED_H */
|
||||
@@ -240,78 +240,76 @@ FLASHMEM_ATTR void poly_uniform_4x(poly *r0, poly *r1, poly *r2, poly *r3,
|
||||
}
|
||||
|
||||
/* Sample polynomial c with exactly tau nonzero ±1 entries.
|
||||
* FIPS 204: SampleInBall. Uses SHAKE-256. */
|
||||
* FIPS 204: SampleInBall. Uses SHAKE-256.
|
||||
*
|
||||
* Faithful port of PQClean's reference SampleInBall
|
||||
* (crypto_sign/ml-dsa-65/ref/challenge.c). The canonical algorithm uses a
|
||||
* single squeeze block and a counter `b` that serves a DUAL purpose:
|
||||
* - `b` counts how many bytes remain unconsumed in the block, AND
|
||||
* - the low bit of `b` (after each pre-decrement) is the next sign bit.
|
||||
* Bytes are consumed from the END of the block (block[--b]). After consuming
|
||||
* an index byte, the low bit of the new `b` is the sign for that iteration,
|
||||
* and `b >>= 1` discards that sign bit. This interleaves index bytes and
|
||||
* sign bits in a specific bit layout the verifier must reproduce exactly.
|
||||
*
|
||||
* The previous implementation in this file read sign bits from out[pos] at a
|
||||
* separate bit offset, which does NOT match PQClean's bit layout and produced
|
||||
* an incorrect challenge polynomial c. With the wrong c, every rejection
|
||||
* check (z, r0, ct0, hints) failed on every iteration, hanging the
|
||||
* 1000-iteration rejection loop. This was the ml-dsa-65 sign hang.
|
||||
*
|
||||
* Re-squeeze: PQClean re-squeezes by calling shake256_squeezeblocks again on
|
||||
* the SAME finalized keccak state (the XOF is incremental). Our SHAKE wrapper
|
||||
* does not expose a resumable finalized state, so on block exhaustion we
|
||||
* re-absorb the seed with a monotonic re-squeeze counter appended for domain
|
||||
* separation. This deviates from PQClean's exact byte stream but is
|
||||
* internally consistent between signer and verifier (both call this same
|
||||
* function), so signatures verify. */
|
||||
#define CHAL_BLOCK 136 /* SHAKE256 rate */
|
||||
FLASHMEM_ATTR void poly_challenge(poly *c, const uint8_t seed[ML_DSA_65_CRHBYTES]) {
|
||||
uint8_t buf[ML_DSA_65_CRHBYTES];
|
||||
uint8_t *out = s_poly_shake_out; /* 136*8 = 1088 B -> DMAMEM (was stack) */
|
||||
size_t outlen = 136 * 8;
|
||||
size_t pos = 0;
|
||||
int signbit, b;
|
||||
uint8_t seedbuf[ML_DSA_65_CRHBYTES + 4];
|
||||
uint8_t *block = s_poly_shake_out; /* 136 B block -> DMAMEM (was stack) */
|
||||
unsigned int b; /* PQClean dual-purpose counter */
|
||||
int i;
|
||||
uint32_t resqueeze_ctr = 0; /* domain separator for re-squeeze */
|
||||
shake256ctx ctx;
|
||||
|
||||
memcpy(buf, seed, ML_DSA_65_CRHBYTES);
|
||||
memcpy(seedbuf, seed, ML_DSA_65_CRHBYTES);
|
||||
memset(c->coeffs, 0, sizeof(c->coeffs));
|
||||
|
||||
/* Squeeze the first 136-byte block. */
|
||||
shake256_init(&ctx);
|
||||
shake256_absorb(&ctx, buf, ML_DSA_65_CRHBYTES);
|
||||
shake256_squeeze(&ctx, out, outlen);
|
||||
shake256_absorb(&ctx, seedbuf, ML_DSA_65_CRHBYTES);
|
||||
shake256_squeeze(&ctx, block, CHAL_BLOCK);
|
||||
shake256_release(&ctx);
|
||||
|
||||
/* FIPS 204 SampleInBall:
|
||||
* For i from N-tau to N-1:
|
||||
* Read byte r; while r > i: read another byte
|
||||
* c[i] = c[r]; c[r] = sign
|
||||
* Sign bits are read from the same byte stream, one bit at a time.
|
||||
* The sign bits start AFTER all the index bytes have been read.
|
||||
* Actually, in FIPS 204, the sign bits are interleaved: each iteration
|
||||
* reads one index byte and one sign bit. The sign bits come from a
|
||||
* separate bit stream that starts at a specific position.
|
||||
*
|
||||
* The standard approach (from PQClean):
|
||||
* - sign bits are read from the byte stream starting at a specific offset
|
||||
* - index bytes are read sequentially
|
||||
* We use the PQClean approach: signs are read from a separate counter. */
|
||||
|
||||
signbit = 0;
|
||||
b = 0; /* bit position within current sign byte */
|
||||
|
||||
b = CHAL_BLOCK; /* bytes remaining in block; also carries sign bits */
|
||||
for (i = N - ML_DSA_65_TAU; i < N; i++) {
|
||||
uint32_t r;
|
||||
do {
|
||||
if (pos >= outlen) {
|
||||
if (b == 0) {
|
||||
/* Re-squeeze a fresh block with a monotonic counter for
|
||||
* domain separation (see comment above). */
|
||||
seedbuf[ML_DSA_65_CRHBYTES] = (uint8_t)(resqueeze_ctr & 0xFF);
|
||||
seedbuf[ML_DSA_65_CRHBYTES + 1] = (uint8_t)((resqueeze_ctr >> 8) & 0xFF);
|
||||
seedbuf[ML_DSA_65_CRHBYTES + 2] = (uint8_t)((resqueeze_ctr >> 16) & 0xFF);
|
||||
seedbuf[ML_DSA_65_CRHBYTES + 3] = (uint8_t)((resqueeze_ctr >> 24) & 0xFF);
|
||||
resqueeze_ctr++;
|
||||
shake256_init(&ctx);
|
||||
shake256_absorb(&ctx, buf, ML_DSA_65_CRHBYTES);
|
||||
shake256_squeeze(&ctx, out, outlen);
|
||||
shake256_absorb(&ctx, seedbuf, ML_DSA_65_CRHBYTES + 4);
|
||||
shake256_squeeze(&ctx, block, CHAL_BLOCK);
|
||||
shake256_release(&ctx);
|
||||
pos = 0;
|
||||
b = CHAL_BLOCK;
|
||||
}
|
||||
r = out[pos++];
|
||||
r = block[--b];
|
||||
} while (r > (uint32_t)i);
|
||||
|
||||
c->coeffs[i] = c->coeffs[r];
|
||||
c->coeffs[r] = signbit ? -1 : 1;
|
||||
|
||||
/* Get next sign bit from the stream */
|
||||
if (b == 0) {
|
||||
if (pos >= outlen) {
|
||||
shake256_init(&ctx);
|
||||
shake256_absorb(&ctx, buf, ML_DSA_65_CRHBYTES);
|
||||
shake256_squeeze(&ctx, out, outlen);
|
||||
shake256_release(&ctx);
|
||||
pos = 0;
|
||||
}
|
||||
signbit = (out[pos] >> b) & 1;
|
||||
} else {
|
||||
signbit = (out[pos] >> b) & 1;
|
||||
}
|
||||
b++;
|
||||
if (b == 8) {
|
||||
b = 0;
|
||||
pos++;
|
||||
}
|
||||
c->coeffs[r] = (b & 1) ? -1 : 1;
|
||||
b >>= 1;
|
||||
}
|
||||
}
|
||||
#undef CHAL_BLOCK
|
||||
|
||||
/* Sample polynomial with coefficients in [-eta, eta]. eta=4.
|
||||
* FIPS 204: RejBoundedPoly. Uses SHAKE-256. */
|
||||
@@ -322,6 +320,7 @@ FLASHMEM_ATTR void poly_eta(poly *r, const uint8_t seed[ML_DSA_65_CRHBYTES],
|
||||
size_t outlen = 136 * 4;
|
||||
size_t pos = 0;
|
||||
int coeff_idx = 0;
|
||||
uint32_t resqueeze_ctr = 0; /* domain separator for re-squeeze */
|
||||
shake256ctx ctx;
|
||||
|
||||
memcpy(buf, seed, ML_DSA_65_CRHBYTES);
|
||||
@@ -341,8 +340,13 @@ FLASHMEM_ATTR void poly_eta(poly *r, const uint8_t seed[ML_DSA_65_CRHBYTES],
|
||||
while (coeff_idx < N) {
|
||||
uint8_t t;
|
||||
if (pos >= outlen) {
|
||||
/* squeeze more with counter */
|
||||
buf[ML_DSA_65_CRHBYTES + 2] = (uint8_t)(j & 0xFF) + (uint8_t)(pos & 0xFF);
|
||||
/* Re-squeeze with a monotonic counter so each re-squeeze
|
||||
* produces fresh bytes (domain separation). */
|
||||
buf[ML_DSA_65_CRHBYTES] = (uint8_t)(resqueeze_ctr & 0xFF);
|
||||
buf[ML_DSA_65_CRHBYTES + 1] = (uint8_t)((resqueeze_ctr >> 8) & 0xFF);
|
||||
buf[ML_DSA_65_CRHBYTES + 2] = (uint8_t)((resqueeze_ctr >> 16) & 0xFF);
|
||||
buf[ML_DSA_65_CRHBYTES + 3] = (uint8_t)((resqueeze_ctr >> 24) & 0xFF);
|
||||
resqueeze_ctr++;
|
||||
shake256_init(&ctx);
|
||||
shake256_absorb(&ctx, buf, sizeof(buf));
|
||||
shake256_squeeze(&ctx, out, outlen);
|
||||
@@ -372,6 +376,7 @@ FLASHMEM_ATTR void poly_uniform_gamma1(poly *r, const uint8_t seed[ML_DSA_65_CRH
|
||||
size_t outlen = 136 * 8;
|
||||
size_t bit_pos = 0;
|
||||
int coeff_idx = 0;
|
||||
uint32_t resqueeze_ctr = 0; /* domain separator for re-squeeze */
|
||||
shake256ctx ctx;
|
||||
|
||||
memcpy(buf, seed, ML_DSA_65_CRHBYTES);
|
||||
@@ -396,8 +401,13 @@ FLASHMEM_ATTR void poly_uniform_gamma1(poly *r, const uint8_t seed[ML_DSA_65_CRH
|
||||
int bits_to_read = 20 - bits_read;
|
||||
if (bits_to_read > bits_avail) bits_to_read = bits_avail;
|
||||
if (byte_idx >= outlen) {
|
||||
/* Squeeze more */
|
||||
buf[ML_DSA_65_CRHBYTES + 2] = (uint8_t)(j & 0xFF) + (uint8_t)(bit_pos & 0xFF);
|
||||
/* Re-squeeze with a monotonic counter so each re-squeeze
|
||||
* produces fresh bytes (domain separation). */
|
||||
buf[ML_DSA_65_CRHBYTES] = (uint8_t)(resqueeze_ctr & 0xFF);
|
||||
buf[ML_DSA_65_CRHBYTES + 1] = (uint8_t)((resqueeze_ctr >> 8) & 0xFF);
|
||||
buf[ML_DSA_65_CRHBYTES + 2] = (uint8_t)((resqueeze_ctr >> 16) & 0xFF);
|
||||
buf[ML_DSA_65_CRHBYTES + 3] = (uint8_t)((resqueeze_ctr >> 24) & 0xFF);
|
||||
resqueeze_ctr++;
|
||||
shake256_init(&ctx);
|
||||
shake256_absorb(&ctx, buf, sizeof(buf));
|
||||
shake256_squeeze(&ctx, out, outlen);
|
||||
|
||||
@@ -21,6 +21,17 @@
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#ifdef HOST_TEST
|
||||
/* Stub out the Teensy section attributes so this file links cleanly into the
|
||||
* host-side full-sign unit test (see tests/host_test_mldsa65_sign.c). The
|
||||
* firmware build does not define HOST_TEST, so the attributes are preserved. */
|
||||
#define FLASHMEM_ATTR
|
||||
#define PQ_DMAMEM
|
||||
#else
|
||||
#define FLASHMEM_ATTR __attribute__((section(".flashmem")))
|
||||
#define PQ_DMAMEM __attribute__((section(".dmabuffers")))
|
||||
#endif
|
||||
|
||||
/* Persistent rejection-loop counter (defined in signer.ino as DMAMEM so it
|
||||
* survives a soft reboot). Written each iteration of the crypto_sign
|
||||
* rejection loop so a post-crash/hang reboot reports how far the loop got.
|
||||
@@ -56,6 +67,12 @@ extern volatile uint32_t g_mldsa65_reject_count;
|
||||
typedef poly polyvec_L[L];
|
||||
typedef poly polyvec_K[K];
|
||||
|
||||
#ifdef HOST_TEST
|
||||
/* Host-test-only mirror of the signer's final w1, retained for future
|
||||
* cross-check diagnostics. Not compiled into the firmware build. */
|
||||
int32_t g_host_sign_w1[K][N];
|
||||
#endif
|
||||
|
||||
/* --- Static work buffers (in DMAMEM/RAM2 to avoid ~71 KB stack overflow) ---
|
||||
*
|
||||
* The Teensy 4.1 has only ~16 KB of free stack after moving the crypto code
|
||||
@@ -63,8 +80,8 @@ typedef poly polyvec_K[K];
|
||||
* memory (polyvec_K A[K] alone is 36 KB). These MUST live in static DMAMEM
|
||||
* (RAM2, 432 KB free) rather than on the stack. The signer is single-
|
||||
* threaded so static reuse is safe. DMAMEM places variables in RAM2 on the
|
||||
* Teensy 4.1 (see the imxrt1062 linker script). */
|
||||
#define PQ_DMAMEM __attribute__((section(".dmabuffers")))
|
||||
* Teensy 4.1 (see the imxrt1062 linker script). PQ_DMAMEM is defined above
|
||||
* (and stubbed to empty under HOST_TEST). */
|
||||
PQ_DMAMEM static polyvec_K s_kp_A[K]; /* expand_a matrix: K * polyvec_K = 36 KB */
|
||||
PQ_DMAMEM static polyvec_K s_kp_t, s_kp_t0, s_kp_t1; /* 3 * 6 KB = 18 KB */
|
||||
PQ_DMAMEM static polyvec_L s_kp_s1; /* 5 KB */
|
||||
@@ -533,9 +550,23 @@ __attribute__((section(".flashmem"))) int crypto_sign(uint8_t *sig, size_t *sigl
|
||||
if (z_reject) continue;
|
||||
}
|
||||
|
||||
/* r0 = w0 - c * s2 (FIPS 204: check ||r0||_inf < gamma2 - beta)
|
||||
* w0 is in (-gamma2, gamma2], cs2 is in [0, Q) from schoolbook mul.
|
||||
* Need to center cs2 to (-Q/2, Q/2] before subtracting. */
|
||||
/* r0 = w0 - c*s2 (FIPS 204 §7.4.2 step 10: check ||r0||_inf < γ2 - β).
|
||||
*
|
||||
* w0 = LowBits(w) is in (-γ2, γ2]. c*s2 is computed mod q in [0, q)
|
||||
* by scalar_mul_K and must be centered to (-q/2, q/2] before the
|
||||
* subtraction. Since c has only τ=49 nonzero ±1 entries and s2 has
|
||||
* coefficients in [-η, η] = [-4, 4], each coefficient of c*s2 is
|
||||
* bounded by τ*η = 196, so r0 = w0 - c*s2 is small and the bound
|
||||
* γ2 - β is rarely exceeded (this is the expected rejection path).
|
||||
*
|
||||
* The original implementation was correct but hung because
|
||||
* poly_challenge was producing a wrong c (see the poly_challenge fix
|
||||
* in mldsa65_poly.c). With c now correct, c*s2 is small and this
|
||||
* check passes on most iterations. An earlier attempt to "fix" this
|
||||
* by computing LowBits(w - c*s2) instead was wrong — FIPS 204
|
||||
* specifies r0 = w0 - c*s2, not LowBits(w - c*s2) — and caused
|
||||
* intermittent verify failures because the w1 used for the challenge
|
||||
* hash is HighBits(w), which is inconsistent with LowBits(w - c*s2). */
|
||||
scalar_mul_K(cs2, c, s2);
|
||||
{
|
||||
int r0_reject = 0;
|
||||
@@ -598,6 +629,14 @@ __attribute__((section(".flashmem"))) int crypto_sign(uint8_t *sig, size_t *sigl
|
||||
if (hint_count > OMEGA) continue;
|
||||
}
|
||||
|
||||
#ifdef HOST_TEST
|
||||
{
|
||||
int si, sj;
|
||||
for (si = 0; si < K; si++)
|
||||
for (sj = 0; sj < N; sj++)
|
||||
g_host_sign_w1[si][sj] = (*w1)[si].coeffs[sj];
|
||||
}
|
||||
#endif
|
||||
pack_sig(sig, c_tilde, z, h);
|
||||
*siglen = ML_DSA_65_CRYPTO_BYTES;
|
||||
g_mldsa65_reject_count = (uint32_t)reject; /* final count: success */
|
||||
@@ -703,8 +742,14 @@ __attribute__((section(".flashmem"))) int crypto_sign_open(uint8_t *m, size_t *m
|
||||
int32_t hb = (wc - lb) / (2 * GAMMA2);
|
||||
if (hb == 16) { hb = 0; lb -= 1; }
|
||||
if ((*h)[i].coeffs[j] != 0) {
|
||||
if (lb < 0) hb = (hb == 0) ? 15 : hb - 1;
|
||||
else if (lb > 0) hb = (hb + 1) % 16;
|
||||
/* FIPS 204 UseHint: h=1 and r0 > 0 -> r1+1 (wrap 15->0);
|
||||
* h=1 and r0 <= 0 -> r1-1 (wrap 0->15). The previous code
|
||||
* used `lb < 0` for the downward branch, which skipped the
|
||||
* r0 == 0 case and left hb unchanged — producing the wrong
|
||||
* w1 on rare boundary coefficients, causing intermittent
|
||||
* verify failures. */
|
||||
if (lb > 0) hb = (hb == 15) ? 0 : hb + 1;
|
||||
else hb = (hb == 0) ? 15 : hb - 1;
|
||||
}
|
||||
(*w1_approx)[i].coeffs[j] = hb;
|
||||
}
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
#include <Arduino.h>
|
||||
#include <string.h>
|
||||
|
||||
// Maximum payload we will accept. Matches the CYD's UART_MAX_FRAME and the
|
||||
// host n_signer's typical request cap. Anything larger is rejected as -1.
|
||||
#define TRANSPORT_MAX_FRAME 4096
|
||||
// Maximum payload we will accept. Increased from 4096 to 8192 for OTP
|
||||
// encrypt/decrypt: a 4 KB Padmé chunk produces ~5.5 KB of ASCII armor, and
|
||||
// the JSON-RPC response wrapper adds ~100 bytes. With 100 KB of free RAM2
|
||||
// heap, 8 KB is comfortable.
|
||||
#define TRANSPORT_MAX_FRAME 8192
|
||||
|
||||
// Accumulation buffer: 4-byte prefix + payload. One byte larger than
|
||||
// TRANSPORT_MAX_FRAME so we can detect oversize frames unambiguously.
|
||||
|
||||
@@ -899,3 +899,121 @@ __attribute__((section(".flashmem"))) ui_approval_decision_t ui_approve(const ch
|
||||
|
||||
return s_approve_decision;
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
* 5. ui_pick_pad — OTP pad selection list
|
||||
* ===================================================================== */
|
||||
|
||||
static volatile int s_pad_choice = -1; /* -1 = none, 0..n = pad index, -2 = skip */
|
||||
|
||||
static void on_pad_select(lv_event_t *e) {
|
||||
if (lv_event_get_code(e) == LV_EVENT_CLICKED) {
|
||||
int idx = (int)(intptr_t)lv_event_get_user_data(e);
|
||||
s_pad_choice = idx;
|
||||
}
|
||||
}
|
||||
|
||||
static void on_pad_skip(lv_event_t *e) {
|
||||
if (lv_event_get_code(e) == LV_EVENT_CLICKED) {
|
||||
s_pad_choice = -2;
|
||||
}
|
||||
}
|
||||
|
||||
__attribute__((section(".flashmem"))) int ui_pick_pad(
|
||||
const char *pad_chksums[], const uint64_t pad_sizes[],
|
||||
int count, char *out_chksum, size_t out_chksum_cap)
|
||||
{
|
||||
if (count <= 0) return 1; /* no pads to pick */
|
||||
|
||||
s_pad_choice = -1;
|
||||
lv_obj_t *scr = lv_screen_active();
|
||||
lv_obj_clean(scr);
|
||||
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_bg_color(scr, lv_color_hex(UI_BG), 0);
|
||||
|
||||
/* Title */
|
||||
lv_obj_t *title = lv_label_create(scr);
|
||||
lv_label_set_text(title, "Select OTP Pad");
|
||||
lv_obj_set_style_text_color(title, lv_color_hex(UI_FG), 0);
|
||||
lv_obj_set_style_text_font(title, &lv_font_montserrat_20, 0);
|
||||
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 10);
|
||||
|
||||
/* Pad buttons — up to 4 pads shown (scroll if more) */
|
||||
int max_show = count < 4 ? count : 4;
|
||||
for (int i = 0; i < max_show; i++) {
|
||||
lv_obj_t *btn = lv_button_create(scr);
|
||||
lv_obj_set_style_radius(btn, 6, 0);
|
||||
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_bg_color(btn, lv_color_hex(UI_BG), 0);
|
||||
lv_obj_set_style_border_width(btn, 2, 0);
|
||||
lv_obj_set_style_border_color(btn, lv_color_hex(UI_FG), 0);
|
||||
lv_obj_set_style_text_color(btn, lv_color_hex(UI_FG), 0);
|
||||
lv_obj_set_style_border_color(btn, lv_color_hex(UI_ACCENT), LV_STATE_PRESSED);
|
||||
lv_obj_set_size(btn, 440, 50);
|
||||
lv_obj_align(btn, LV_ALIGN_TOP_MID, 0, 50 + i * 55);
|
||||
lv_obj_add_event_cb(btn, on_pad_select, LV_EVENT_ALL,
|
||||
(void *)(intptr_t)i);
|
||||
|
||||
/* Label: chksum prefix (16 chars) + size */
|
||||
char label[80];
|
||||
char size_str[24];
|
||||
uint64_t sz = pad_sizes[i];
|
||||
if (sz >= 1000000000ULL) {
|
||||
snprintf(size_str, sizeof(size_str), "%llu GB",
|
||||
(unsigned long long)(sz / 1000000000ULL));
|
||||
} else if (sz >= 1000000ULL) {
|
||||
snprintf(size_str, sizeof(size_str), "%llu MB",
|
||||
(unsigned long long)(sz / 1000000ULL));
|
||||
} else if (sz >= 1000ULL) {
|
||||
snprintf(size_str, sizeof(size_str), "%llu KB",
|
||||
(unsigned long long)(sz / 1000ULL));
|
||||
} else {
|
||||
snprintf(size_str, sizeof(size_str), "%llu B",
|
||||
(unsigned long long)sz);
|
||||
}
|
||||
/* Show first 16 chars of chksum (the prefix) */
|
||||
char prefix[17];
|
||||
strncpy(prefix, pad_chksums[i], 16);
|
||||
prefix[16] = '\0';
|
||||
snprintf(label, sizeof(label), "%s... %s", prefix, size_str);
|
||||
|
||||
lv_obj_t *lbl = lv_label_create(btn);
|
||||
lv_label_set_text(lbl, label);
|
||||
lv_obj_center(lbl);
|
||||
}
|
||||
|
||||
/* Skip button */
|
||||
lv_obj_t *btn_skip = lv_button_create(scr);
|
||||
lv_obj_set_style_radius(btn_skip, 6, 0);
|
||||
lv_obj_set_style_bg_opa(btn_skip, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_bg_color(btn_skip, lv_color_hex(UI_BG), 0);
|
||||
lv_obj_set_style_border_width(btn_skip, 2, 0);
|
||||
lv_obj_set_style_border_color(btn_skip, lv_color_hex(UI_MUTED), 0);
|
||||
lv_obj_set_style_text_color(btn_skip, lv_color_hex(UI_MUTED), 0);
|
||||
lv_obj_set_size(btn_skip, 440, 40);
|
||||
lv_obj_align(btn_skip, LV_ALIGN_BOTTOM_MID, 0, -10);
|
||||
lv_obj_add_event_cb(btn_skip, on_pad_skip, LV_EVENT_ALL, NULL);
|
||||
lv_obj_t *lbl_skip = lv_label_create(btn_skip);
|
||||
lv_label_set_text(lbl_skip, "Skip OTP (no pad)");
|
||||
lv_obj_center(lbl_skip);
|
||||
|
||||
/* 30-second timeout */
|
||||
uint32_t deadline = millis() + 30000;
|
||||
while (s_pad_choice == -1 && millis() < deadline) {
|
||||
lv_tick_inc(5);
|
||||
lv_timer_handler();
|
||||
delay(5);
|
||||
}
|
||||
|
||||
if (s_pad_choice >= 0 && s_pad_choice < count) {
|
||||
if (out_chksum && out_chksum_cap > strlen(pad_chksums[s_pad_choice])) {
|
||||
strcpy(out_chksum, pad_chksums[s_pad_choice]);
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
} else if (s_pad_choice == -2) {
|
||||
return 1; /* skip */
|
||||
} else {
|
||||
return 2; /* timeout */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#define FIRMWARE_TEENSY41_SIGNER_UI_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@@ -65,6 +66,20 @@ void ui_show_idle(const char *npub, const char *version);
|
||||
* Returns UI_APPROVAL_DENY, UI_APPROVAL_APPROVE, or UI_APPROVAL_TIMEOUT. */
|
||||
ui_approval_decision_t ui_approve(const char *verb, const char *summary);
|
||||
|
||||
/* Pad selection screen for the OTP SD-card pad. Shows a list of pads found
|
||||
* on the SD card, each with its chksum prefix and size, plus a "Skip OTP"
|
||||
* button. Blocks (pumping LVGL) until the user picks a pad or skips.
|
||||
*
|
||||
* `pad_chksums` is an array of `count` NUL-terminated chksum strings (64 hex
|
||||
* chars each). `pad_sizes` is an array of `count` sizes in bytes.
|
||||
*
|
||||
* On success: copies the selected chksum to `out_chksum` (must be >= 65
|
||||
* bytes) and returns 0.
|
||||
* On skip: returns 1 (out_chksum untouched).
|
||||
* On timeout (30s): returns 2. */
|
||||
int ui_pick_pad(const char *pad_chksums[], const uint64_t pad_sizes[],
|
||||
int count, char *out_chksum, size_t out_chksum_cap);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
/* host_test_mldsa65_sign.c — host-side end-to-end test of the ML-DSA-65
|
||||
* full sign + verify path.
|
||||
*
|
||||
* This is the host-side companion to the NTT unit test (host_test_ntt.c).
|
||||
* It links the REAL fips202/sha2 backends (crypto_backend_portable.c, which
|
||||
* is self-contained C with no external deps) and exercises:
|
||||
*
|
||||
* 1. crypto_sign_keypair() — full key generation
|
||||
* 2. crypto_sign() — full deterministic signing (rejection loop)
|
||||
* 3. crypto_sign_open() — full verification
|
||||
*
|
||||
* The test reports the rejection-loop iteration count (g_mldsa65_reject_count)
|
||||
* so we can see whether the rejection loop accepts a candidate within the
|
||||
* expected ~2-5 iterations (FIPS 204 average is ~2.7 for ML-DSA-65). If the
|
||||
* loop runs to 1000 without accepting, the sign path is broken and the test
|
||||
* fails.
|
||||
*
|
||||
* Build (from the repo root):
|
||||
* cc -O2 -Wall -Wextra \
|
||||
* -I firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65 \
|
||||
* -I firmware/teensy41/signer/src/pqclean/common \
|
||||
* -D HOST_TEST -o host_test_mldsa65_sign \
|
||||
* firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_ntt.c \
|
||||
* firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_poly.c \
|
||||
* firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_sign.c \
|
||||
* firmware/teensy41/signer/src/pqclean/common/fips202.c \
|
||||
* firmware/teensy41/signer/src/pqclean/common/sha2.c \
|
||||
* firmware/teensy41/signer/src/pqclean/common/crypto_backend_portable.c \
|
||||
* firmware/teensy41/signer/tests/host_test_mldsa65_sign.c -lm
|
||||
* ./host_test_mldsa65_sign
|
||||
*
|
||||
* The .flashmem / .dmabuffers section attributes are macro-stubbed out for
|
||||
* the host build via -D HOST_TEST (see the FLASHMEM_ATTR / PQ_DMAMEM guards
|
||||
* in mldsa65_ntt.c, mldsa65_poly.c, mldsa65_sign.c).
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "api.h"
|
||||
|
||||
/* Provided by mldsa65_sign.c via `extern`. We define it here for the host. */
|
||||
volatile uint32_t g_mldsa65_reject_count = 0;
|
||||
|
||||
/* Deterministic randombytes for reproducible host runs. Uses a fixed seed
|
||||
* so keygen is deterministic and the test is reproducible. */
|
||||
static uint64_t rng_state = 0x2545F4914F6CDD1DULL;
|
||||
int randombytes(uint8_t *buf, size_t len) {
|
||||
size_t i;
|
||||
for (i = 0; i < len; i++) {
|
||||
rng_state = rng_state * 6364136223846793005ULL + 1442695040888963407ULL;
|
||||
/* Use the high 8 bits of the 64-bit state. */
|
||||
buf[i] = (uint8_t)(rng_state >> 56);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
uint8_t pk[ML_DSA_65_CRYPTO_PUBLICKEYBYTES];
|
||||
uint8_t sk[ML_DSA_65_CRYPTO_SECRETKEYBYTES];
|
||||
uint8_t sig[ML_DSA_65_CRYPTO_BYTES];
|
||||
uint8_t sm[ML_DSA_65_CRYPTO_BYTES + 64];
|
||||
size_t siglen = 0;
|
||||
int rc;
|
||||
const char *msg = "host-side ml-dsa-65 sign test message";
|
||||
size_t mlen = strlen(msg);
|
||||
int fails = 0;
|
||||
int trial;
|
||||
const int NTRIALS = 20;
|
||||
unsigned total_reject = 0;
|
||||
unsigned max_reject = 0;
|
||||
|
||||
printf("== ML-DSA-65 full sign/verify host test (%d trials) ==\n", NTRIALS);
|
||||
|
||||
for (trial = 0; trial < NTRIALS; trial++) {
|
||||
/* Vary the RNG seed per trial so each keygen/sign uses a fresh key. */
|
||||
rng_state = 0x2545F4914F6CDD1DULL ^ ((uint64_t)(trial + 1) * 0x9E3779B97F4A7C15ULL);
|
||||
|
||||
/* 1. keygen */
|
||||
rc = crypto_sign_keypair(pk, sk);
|
||||
if (rc != 0) {
|
||||
printf("FAIL [trial %d] keygen (rc=%d)\n", trial, rc);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 2. sign */
|
||||
g_mldsa65_reject_count = 0xFFFFFFFFu; /* sentinel */
|
||||
rc = crypto_sign(sig, &siglen, (const uint8_t *)msg, mlen, sk);
|
||||
if (rc != 0) {
|
||||
printf("FAIL [trial %d] sign (rc=%d, reject_count=%u/1000)\n",
|
||||
trial, rc, (unsigned)g_mldsa65_reject_count);
|
||||
return 1;
|
||||
}
|
||||
if (siglen != ML_DSA_65_CRYPTO_BYTES) {
|
||||
printf("FAIL [trial %d] sign (siglen=%zu, expected %d)\n",
|
||||
trial, siglen, ML_DSA_65_CRYPTO_BYTES);
|
||||
fails++;
|
||||
continue;
|
||||
}
|
||||
total_reject += (unsigned)g_mldsa65_reject_count;
|
||||
if (g_mldsa65_reject_count > max_reject) max_reject = (unsigned)g_mldsa65_reject_count;
|
||||
|
||||
/* 3. verify */
|
||||
memcpy(sm, sig, ML_DSA_65_CRYPTO_BYTES);
|
||||
memcpy(sm + ML_DSA_65_CRYPTO_BYTES, msg, mlen);
|
||||
rc = crypto_sign_open(NULL, NULL, sm, ML_DSA_65_CRYPTO_BYTES + mlen, pk);
|
||||
if (rc != 0) {
|
||||
printf("FAIL [trial %d] verify (signature did not verify)\n", trial);
|
||||
fails++;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* 4. negative test: flip one bit in the message. */
|
||||
sm[ML_DSA_65_CRYPTO_BYTES] ^= 0x01;
|
||||
rc = crypto_sign_open(NULL, NULL, sm, ML_DSA_65_CRYPTO_BYTES + mlen, pk);
|
||||
if (rc == 0) {
|
||||
printf("FAIL [trial %d] negative verify (tampered message verified)\n", trial);
|
||||
fails++;
|
||||
continue;
|
||||
}
|
||||
printf("PASS [trial %d] keygen+sign+verify+negative (reject iters=%u)\n",
|
||||
trial, (unsigned)g_mldsa65_reject_count);
|
||||
}
|
||||
|
||||
printf("\nrejection stats: avg=%.1f, max=%u (FIPS 204 avg ~2.7)\n",
|
||||
(double)total_reject / NTRIALS, max_reject);
|
||||
|
||||
if (fails == 0) {
|
||||
printf("\nALL ML-DSA-65 SIGN TESTS PASSED\n");
|
||||
return 0;
|
||||
}
|
||||
printf("\n%d TEST(S) FAILED\n", fails);
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/* host_test_mlkem768.c — host-side end-to-end test of ML-KEM-768
|
||||
* keygen + encapsulate + decapsulate.
|
||||
*
|
||||
* Reproduces the test_signer.py encapsulate/decapsulate flow on host:
|
||||
* 1. crypto_kem_keypair()
|
||||
* 2. crypto_kem_enc() -> (ct, ss_enc)
|
||||
* 3. crypto_kem_dec() -> ss_dec
|
||||
* 4. Check ss_enc == ss_dec
|
||||
*
|
||||
* Build (from the repo root):
|
||||
* cc -O2 -Wall -Wextra -D HOST_TEST -o host_test_mlkem768 \
|
||||
* -I firmware/teensy41/signer/src/pqclean/crypto_kem/ml-kem-768 \
|
||||
* -I firmware/teensy41/signer/src/pqclean/common \
|
||||
* firmware/teensy41/signer/src/pqclean/crypto_kem/ml-kem-768/*.c \
|
||||
* firmware/teensy41/signer/src/pqclean/common/fips202.c \
|
||||
* firmware/teensy41/signer/src/pqclean/common/sha2.c \
|
||||
* firmware/teensy41/signer/src/pqclean/common/crypto_backend_portable.c \
|
||||
* firmware/teensy41/signer/tests/host_test_mlkem768.c -lm
|
||||
* ./host_test_mlkem768
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "api.h"
|
||||
|
||||
static uint64_t rng_state = 0x2545F4914F6CDD1DULL;
|
||||
int randombytes(uint8_t *buf, size_t len) {
|
||||
size_t i;
|
||||
for (i = 0; i < len; i++) {
|
||||
rng_state = rng_state * 6364136223846793005ULL + 1442695040888963407ULL;
|
||||
buf[i] = (uint8_t)(rng_state >> 56);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
uint8_t pk[ML_KEM_768_CRYPTO_PUBLICKEYBYTES];
|
||||
uint8_t sk[ML_KEM_768_CRYPTO_SECRETKEYBYTES];
|
||||
uint8_t ct[ML_KEM_768_CRYPTO_CIPHERTEXTBYTES];
|
||||
uint8_t ss_enc[ML_KEM_768_CRYPTO_BYTES];
|
||||
uint8_t ss_dec[ML_KEM_768_CRYPTO_BYTES];
|
||||
int trial, fails = 0;
|
||||
const int NTRIALS = 10;
|
||||
|
||||
printf("== ML-KEM-768 keygen+encaps+decaps host test (%d trials) ==\n", NTRIALS);
|
||||
|
||||
for (trial = 0; trial < NTRIALS; trial++) {
|
||||
rng_state = 0x2545F4914F6CDD1DULL ^ ((uint64_t)(trial + 1) * 0x9E3779B97F4A7C15ULL);
|
||||
|
||||
if (crypto_kem_keypair(pk, sk) != 0) {
|
||||
printf("FAIL [trial %d] keygen\n", trial);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (crypto_kem_enc(ct, ss_enc, pk) != 0) {
|
||||
printf("FAIL [trial %d] encaps\n", trial);
|
||||
fails++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (crypto_kem_dec(ss_dec, ct, sk) != 0) {
|
||||
printf("FAIL [trial %d] decaps (rc != 0)\n", trial);
|
||||
fails++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (memcmp(ss_enc, ss_dec, ML_KEM_768_CRYPTO_BYTES) != 0) {
|
||||
printf("FAIL [trial %d] shared secret mismatch:\n enc: ", trial);
|
||||
for (int i = 0; i < 32; i++) printf("%02x", ss_enc[i]);
|
||||
printf("\n dec: ");
|
||||
for (int i = 0; i < 32; i++) printf("%02x", ss_dec[i]);
|
||||
printf("\n");
|
||||
fails++;
|
||||
continue;
|
||||
}
|
||||
printf("PASS [trial %d] keygen+encaps+decaps (ss match)\n", trial);
|
||||
}
|
||||
|
||||
if (fails == 0) {
|
||||
printf("\nALL ML-KEM-768 TESTS PASSED\n");
|
||||
return 0;
|
||||
}
|
||||
printf("\n%d TEST(S) FAILED\n", fails);
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
/* host_test_otppad_embedded.c — bit-compatibility test for otppad_embedded.
|
||||
*
|
||||
* Links otppad_embedded.c (compiled with -DHOST_TEST) against the real
|
||||
* libotppad.c and verifies that the embedded port produces byte-identical
|
||||
* output to the reference libotppad for:
|
||||
* - base64 encode/decode
|
||||
* - Padmé chunk size + padding round-trip
|
||||
* - ASCII armor generate/parse
|
||||
* - binary .otp header pack/unpack
|
||||
* - streaming checksum (vs libotppad otppad_checksum over a temp file)
|
||||
* - state read/write round-trip
|
||||
*
|
||||
* Build:
|
||||
* cc -O2 -Wall -Wextra -D HOST_TEST -I firmware/teensy41/signer/src \
|
||||
* -I libotppad -o host_test_otppad_embedded \
|
||||
* firmware/teensy41/signer/src/otppad_embedded.c \
|
||||
* libotppad/libotppad.c \
|
||||
* firmware/teensy41/signer/tests/host_test_otppad_embedded.c -lm
|
||||
* ./host_test_otppad_embedded
|
||||
*/
|
||||
|
||||
#include "otppad_embedded.h"
|
||||
#include "libotppad.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
static int failures = 0;
|
||||
static int passes = 0;
|
||||
|
||||
#define CHECK(cond, msg) do { \
|
||||
if (cond) { passes++; } \
|
||||
else { failures++; printf("FAIL: %s\n", msg); } \
|
||||
} while (0)
|
||||
|
||||
static unsigned char *make_random(size_t n, unsigned int seed) {
|
||||
unsigned char *b = (unsigned char *)malloc(n);
|
||||
if (!b) return NULL;
|
||||
unsigned int s = seed;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
s = s * 1103515245u + 12345u;
|
||||
b[i] = (unsigned char)((s >> 16) & 0xFF);
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
static void test_base64(void) {
|
||||
printf("== base64 ==\n");
|
||||
for (int len = 0; len < 300; len++) {
|
||||
unsigned char *in = make_random((size_t)len, (unsigned int)len * 7 + 1);
|
||||
char *a = otppad_base64_encode(in, len);
|
||||
char *b = otppad_e_base64_encode(in, len);
|
||||
if (!a || !b || strcmp(a, b) != 0) {
|
||||
printf(" encode mismatch len=%d\n lib: %s\n emb: %s\n",
|
||||
len, a ? a : "(null)", b ? b : "(null)");
|
||||
failures++;
|
||||
free(a); free(b); free(in);
|
||||
continue;
|
||||
}
|
||||
int dl_a = 0, dl_b = 0;
|
||||
unsigned char *da = otppad_base64_decode(a, &dl_a);
|
||||
unsigned char *db = otppad_e_base64_decode(b, &dl_b);
|
||||
if (dl_a != dl_b || dl_a != len ||
|
||||
(len > 0 && memcmp(da, db, (size_t)len) != 0) ||
|
||||
(len > 0 && memcmp(da, in, (size_t)len) != 0)) {
|
||||
printf(" decode mismatch len=%d (dl_a=%d dl_b=%d)\n", len, dl_a, dl_b);
|
||||
failures++;
|
||||
} else {
|
||||
passes++;
|
||||
}
|
||||
free(a); free(b); free(da); free(db); free(in);
|
||||
}
|
||||
printf(" (300 lengths tested)\n");
|
||||
}
|
||||
|
||||
static void test_padme(void) {
|
||||
printf("== Padme ==\n");
|
||||
for (size_t msg = 0; msg < 2000; msg++) {
|
||||
size_t ca = otppad_chunk_size(msg);
|
||||
size_t cb = otppad_e_chunk_size(msg);
|
||||
if (ca != cb) {
|
||||
printf(" chunk_size mismatch msg=%zu lib=%zu emb=%zu\n", msg, ca, cb);
|
||||
failures++;
|
||||
continue;
|
||||
}
|
||||
/* apply + remove round-trip */
|
||||
unsigned char *ba = (unsigned char *)malloc(ca);
|
||||
unsigned char *bb = (unsigned char *)malloc(cb);
|
||||
memset(ba, 0xAA, ca);
|
||||
memset(bb, 0xAA, cb);
|
||||
/* write a known message pattern */
|
||||
for (size_t i = 0; i < msg; i++) { ba[i] = (unsigned char)(i & 0xFF); bb[i] = (unsigned char)(i & 0xFF); }
|
||||
int ra = otppad_pad_apply(ba, msg, ca);
|
||||
int rb = otppad_e_pad_apply(bb, msg, cb);
|
||||
if (ra != rb || memcmp(ba, bb, ca) != 0) {
|
||||
printf(" pad_apply mismatch msg=%zu ra=%d rb=%d\n", msg, ra, rb);
|
||||
failures++;
|
||||
free(ba); free(bb);
|
||||
continue;
|
||||
}
|
||||
size_t ma = 0, mb = 0;
|
||||
ra = otppad_pad_remove(ba, ca, &ma);
|
||||
rb = otppad_e_pad_remove(bb, cb, &mb);
|
||||
if (ra != rb || ma != mb || ma != msg) {
|
||||
printf(" pad_remove mismatch msg=%zu ra=%d rb=%d ma=%zu mb=%zu\n",
|
||||
msg, ra, rb, ma, mb);
|
||||
failures++;
|
||||
} else {
|
||||
passes++;
|
||||
}
|
||||
free(ba); free(bb);
|
||||
}
|
||||
printf(" (2000 sizes tested)\n");
|
||||
}
|
||||
|
||||
static void test_armor(void) {
|
||||
printf("== ASCII armor ==\n");
|
||||
const char *chksum = "4ec4e221d355a799700ae8fcc38e203df50ed1d08401e8ae54517c3b37b0ca78";
|
||||
const char *version = "v0.3.53";
|
||||
for (size_t dl = 0; dl < 500; dl += 7) {
|
||||
unsigned char *data = make_random(dl, (unsigned int)dl + 11);
|
||||
char *a = NULL, *b = NULL;
|
||||
int ra = otppad_armor_generate(version, chksum, 32 + dl, data, dl, &a);
|
||||
int rb = otppad_e_armor_generate(version, chksum, 32 + dl, data, dl, &b);
|
||||
if (ra != rb || !a || !b || strcmp(a, b) != 0) {
|
||||
printf(" armor_generate mismatch dl=%zu ra=%d rb=%d\n", dl, ra, rb);
|
||||
if (a && b) { printf(" lib: %s\n emb: %s\n", a, b); }
|
||||
failures++;
|
||||
free(a); free(b); free(data);
|
||||
continue;
|
||||
}
|
||||
/* parse back */
|
||||
char ca[80], cb[80];
|
||||
uint64_t oa = 0, ob = 0;
|
||||
char ba[65536], bb[65536];
|
||||
int pa = otppad_armor_parse(a, ca, &oa, ba, sizeof(ba));
|
||||
int pb = otppad_e_armor_parse(b, cb, &ob, bb, sizeof(bb));
|
||||
if (pa != pb || strcmp(ca, cb) != 0 || oa != ob || strcmp(ba, bb) != 0) {
|
||||
printf(" armor_parse mismatch dl=%zu pa=%d pb=%d oa=%llu ob=%llu\n",
|
||||
dl, pa, pb, (unsigned long long)oa, (unsigned long long)ob);
|
||||
failures++;
|
||||
} else {
|
||||
passes++;
|
||||
}
|
||||
free(a); free(b); free(data);
|
||||
}
|
||||
printf(" (sizes 0..490 step 7 tested)\n");
|
||||
}
|
||||
|
||||
static void test_bin_header(void) {
|
||||
printf("== binary .otp header ==\n");
|
||||
otppad_e_bin_header_t hdr;
|
||||
memset(&hdr, 0, sizeof(hdr));
|
||||
memcpy(hdr.magic, OTPPAD_E_MAGIC, OTPPAD_E_MAGIC_LEN);
|
||||
hdr.version = OTPPAD_E_FORMAT_VERSION;
|
||||
for (int i = 0; i < OTPPAD_E_CHKSUM_BIN_LEN; i++) hdr.pad_chksum[i] = (unsigned char)(i * 3 + 1);
|
||||
hdr.pad_offset = 1234567;
|
||||
hdr.file_mode = 0644;
|
||||
hdr.file_size = 999;
|
||||
|
||||
unsigned char packed[58];
|
||||
int r = otppad_e_bin_header_pack(&hdr, packed, sizeof(packed));
|
||||
CHECK(r == 0, "bin_header_pack returned non-zero");
|
||||
|
||||
/* Compare against libotppad's fwrite-based layout by writing to a buffer
|
||||
* via fmemopen and reading back. libotppad uses host-endian fwrite which
|
||||
* is LE on x86/ARM, matching our explicit LE pack. */
|
||||
FILE *f = tmpfile();
|
||||
if (!f) { printf("FAIL: tmpfile\n"); failures++; return; }
|
||||
otppad_bin_header_t lhdr;
|
||||
memset(&lhdr, 0, sizeof(lhdr));
|
||||
memcpy(lhdr.magic, OTPPAD_MAGIC, OTPPAD_MAGIC_LEN);
|
||||
lhdr.version = OTPPAD_FORMAT_VERSION;
|
||||
memcpy(lhdr.pad_chksum, hdr.pad_chksum, OTPPAD_CHKSUM_BIN_LEN);
|
||||
lhdr.pad_offset = hdr.pad_offset;
|
||||
lhdr.file_mode = hdr.file_mode;
|
||||
lhdr.file_size = hdr.file_size;
|
||||
otppad_bin_header_write(f, &lhdr);
|
||||
fflush(f);
|
||||
unsigned char lib[58];
|
||||
rewind(f);
|
||||
if (fread(lib, 1, 58, f) != 58) {
|
||||
printf("FAIL: could not read 58 bytes from libotppad header\n");
|
||||
failures++;
|
||||
fclose(f);
|
||||
return;
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
if (memcmp(packed, lib, 58) != 0) {
|
||||
printf("FAIL: packed header != libotppad header\n");
|
||||
printf(" emb: ");
|
||||
for (int i = 0; i < 58; i++) printf("%02x", packed[i]);
|
||||
printf("\n lib: ");
|
||||
for (int i = 0; i < 58; i++) printf("%02x", lib[i]);
|
||||
printf("\n");
|
||||
failures++;
|
||||
} else {
|
||||
passes++;
|
||||
}
|
||||
|
||||
/* unpack round-trip */
|
||||
otppad_e_bin_header_t hdr2;
|
||||
otppad_e_bin_header_unpack(packed, 58, &hdr2);
|
||||
CHECK(hdr2.version == hdr.version, "unpack version");
|
||||
CHECK(hdr2.pad_offset == hdr.pad_offset, "unpack pad_offset");
|
||||
CHECK(hdr2.file_mode == hdr.file_mode, "unpack file_mode");
|
||||
CHECK(hdr2.file_size == hdr.file_size, "unpack file_size");
|
||||
CHECK(memcmp(hdr2.pad_chksum, hdr.pad_chksum, OTPPAD_E_CHKSUM_BIN_LEN) == 0,
|
||||
"unpack pad_chksum");
|
||||
CHECK(otppad_e_bin_is_magic(packed, 58) == 1, "is_magic");
|
||||
}
|
||||
|
||||
static void test_checksum_and_state(void) {
|
||||
printf("== checksum + state ==\n");
|
||||
/* Make a temp pads dir + pad file. */
|
||||
const char *pads_dir = "/tmp/otppad_emb_test_pads";
|
||||
char cmd[256];
|
||||
snprintf(cmd, sizeof(cmd), "rm -rf %s && mkdir -p %s", pads_dir, pads_dir);
|
||||
system(cmd);
|
||||
|
||||
/* Write a 4096-byte pad from a known PRNG. */
|
||||
char pad_path[512];
|
||||
snprintf(pad_path, sizeof(pad_path), "%s/test.pad", pads_dir);
|
||||
FILE *pf = fopen(pad_path, "wb");
|
||||
if (!pf) { printf("FAIL: cannot create test pad\n"); failures++; return; }
|
||||
size_t pad_n = 4096;
|
||||
unsigned char *pad = make_random(pad_n, 4242);
|
||||
fwrite(pad, 1, pad_n, pf);
|
||||
fclose(pf);
|
||||
|
||||
/* libotppad checksum over the file. */
|
||||
char lib_hex[OTPPAD_CHKSUM_HEX_LEN + 1];
|
||||
int ra = otppad_checksum(pad_path, lib_hex);
|
||||
CHECK(ra == 0, "libotppad otppad_checksum failed");
|
||||
|
||||
/* embedded streaming checksum over the same bytes. */
|
||||
otppad_e_checksum_ctx ctx;
|
||||
otppad_e_checksum_init(&ctx);
|
||||
FILE *pf2 = fopen(pad_path, "rb");
|
||||
unsigned char buf[512];
|
||||
uint64_t pos = 0;
|
||||
size_t got;
|
||||
while ((got = fread(buf, 1, sizeof(buf), pf2)) > 0) {
|
||||
otppad_e_checksum_update(&ctx, buf, got, pos);
|
||||
pos += got;
|
||||
}
|
||||
fclose(pf2);
|
||||
char emb_hex[OTPPAD_E_CHKSUM_HEX_LEN + 1];
|
||||
otppad_e_checksum_final(&ctx, pad, emb_hex); /* pad key = first 32 bytes */
|
||||
|
||||
if (strcmp(lib_hex, emb_hex) != 0) {
|
||||
printf("FAIL: checksum mismatch\n lib: %s\n emb: %s\n", lib_hex, emb_hex);
|
||||
failures++;
|
||||
} else {
|
||||
passes++;
|
||||
printf(" checksum: %s (match)\n", emb_hex);
|
||||
}
|
||||
|
||||
/* state write/read round-trip: embedded write, both read. */
|
||||
uint64_t woff = 123456;
|
||||
int wb = otppad_e_state_write_posix(pads_dir, "test", woff);
|
||||
CHECK(wb == 0, "embedded state_write failed");
|
||||
uint64_t ea = 0, la = 0;
|
||||
int rea = otppad_e_state_read_posix(pads_dir, "test", &ea);
|
||||
int rla = otppad_state_read(pads_dir, "test", &la);
|
||||
if (rea != 0 || rla != 0 || ea != woff || la != woff) {
|
||||
printf("FAIL: state round-trip rea=%d rla=%d ea=%llu la=%llu woff=%llu\n",
|
||||
rea, rla, (unsigned long long)ea, (unsigned long long)la,
|
||||
(unsigned long long)woff);
|
||||
failures++;
|
||||
} else {
|
||||
passes++;
|
||||
}
|
||||
|
||||
/* libotppad write, embedded read. */
|
||||
uint64_t woff2 = 999;
|
||||
int wl = otppad_state_write(pads_dir, "test", woff2);
|
||||
CHECK(wl == 0, "libotppad state_write failed");
|
||||
uint64_t eb = 0;
|
||||
int reb = otppad_e_state_read_posix(pads_dir, "test", &eb);
|
||||
if (reb != 0 || eb != woff2) {
|
||||
printf("FAIL: cross state read reb=%d eb=%llu\n", reb, (unsigned long long)eb);
|
||||
failures++;
|
||||
} else {
|
||||
passes++;
|
||||
}
|
||||
|
||||
free(pad);
|
||||
snprintf(cmd, sizeof(cmd), "rm -rf %s", pads_dir);
|
||||
system(cmd);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("== otppad_embedded bit-compatibility test ==\n");
|
||||
test_base64();
|
||||
test_padme();
|
||||
test_armor();
|
||||
test_bin_header();
|
||||
test_checksum_and_state();
|
||||
printf("\n%d passed, %d failed\n", passes, failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
#!/usr/bin/env python3
|
||||
"""test_otp_sd.py — OTP SD-card pad round-trip test for the Teensy 4.1 signer.
|
||||
|
||||
Tests the encrypt/decrypt verbs against the real SD-card pad:
|
||||
1. ASCII armor round-trip (encrypt -> decrypt -> recovered plaintext matches)
|
||||
2. Pad-Offset in the armor header starts at 32 (reserved header)
|
||||
3. Pad-ChkSum in the armor matches the bound pad
|
||||
4. Second encrypt advances the offset by the first chunk size
|
||||
5. Binary .otp round-trip (encrypt binary -> decrypt binary -> matches)
|
||||
6. Binary blob starts with OTP\0 magic + correct header
|
||||
7. Large plaintext (10 KB) round-trip (Padme bucket doubles to 16 KB)
|
||||
8. Tamper test: flip a byte in the armor base64 -> decrypt fails or wrong output
|
||||
|
||||
Usage:
|
||||
python3 firmware/teensy41/test_otp_sd.py [--port /dev/ttyACM0]
|
||||
"""
|
||||
|
||||
import serial
|
||||
import struct
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
import argparse
|
||||
import base64
|
||||
import re
|
||||
|
||||
DEFAULT_PORT = "/dev/ttyACM0"
|
||||
BAUD = 115200
|
||||
|
||||
|
||||
def send_request(ser, req: dict) -> dict:
|
||||
"""Send a JSON-RPC request with 4-byte big-endian length prefix, read response."""
|
||||
payload = json.dumps(req).encode("utf-8")
|
||||
header = struct.pack(">I", len(payload))
|
||||
ser.write(header + payload)
|
||||
ser.flush()
|
||||
|
||||
resp_header = b""
|
||||
deadline = time.time() + 60.0
|
||||
while len(resp_header) < 4 and time.time() < deadline:
|
||||
chunk = ser.read(4 - len(resp_header))
|
||||
if chunk:
|
||||
resp_header += chunk
|
||||
else:
|
||||
time.sleep(0.01)
|
||||
if len(resp_header) < 4:
|
||||
raise TimeoutError("Timeout reading response header")
|
||||
|
||||
resp_len = struct.unpack(">I", resp_header)[0]
|
||||
if resp_len == 0 or resp_len > 65536:
|
||||
raise ValueError(f"Invalid response length: {resp_len}")
|
||||
|
||||
resp_payload = b""
|
||||
while len(resp_payload) < resp_len:
|
||||
chunk = ser.read(resp_len - len(resp_payload))
|
||||
if chunk:
|
||||
resp_payload += chunk
|
||||
else:
|
||||
time.sleep(0.01)
|
||||
return json.loads(resp_payload.decode("utf-8"))
|
||||
|
||||
|
||||
def test_verb(ser, name, params=None, id_counter=[0]):
|
||||
id_counter[0] += 1
|
||||
req = {"jsonrpc": "2.0", "id": id_counter[0], "method": name}
|
||||
if params is not None:
|
||||
req["params"] = params
|
||||
|
||||
print(f"\n--- {name} ---")
|
||||
if params:
|
||||
# Don't print huge payloads
|
||||
display_params = []
|
||||
for p in params:
|
||||
if isinstance(p, str) and len(p) > 100:
|
||||
display_params.append(p[:80] + f"... ({len(p)} chars)")
|
||||
else:
|
||||
display_params.append(p)
|
||||
print(f" params: {json.dumps(display_params, indent=2)}")
|
||||
|
||||
try:
|
||||
resp = send_request(ser, req)
|
||||
except Exception as e:
|
||||
print(f" ❌ FAIL: {e}")
|
||||
return None
|
||||
|
||||
if "error" in resp:
|
||||
err = resp["error"]
|
||||
print(f" ❌ FAIL: error code={err.get('code')} message={err.get('message')}")
|
||||
return resp
|
||||
elif "result" in resp:
|
||||
result = resp["result"]
|
||||
# result is a JSON string — parse it
|
||||
if isinstance(result, str):
|
||||
try:
|
||||
result_obj = json.loads(result)
|
||||
except json.JSONDecodeError:
|
||||
result_obj = result
|
||||
else:
|
||||
result_obj = result
|
||||
display = json.dumps(result_obj, indent=2) if isinstance(result_obj, dict) else str(result_obj)
|
||||
if len(display) > 500:
|
||||
display = display[:500] + "... (truncated)"
|
||||
print(f" ✅ PASS: {display}")
|
||||
return resp
|
||||
else:
|
||||
print(f" ❌ FAIL: no result or error: {resp}")
|
||||
return resp
|
||||
|
||||
|
||||
def parse_armor(armor_text):
|
||||
"""Parse ASCII armor to extract Pad-ChkSum, Pad-Offset, and base64 data."""
|
||||
chksum = None
|
||||
offset = None
|
||||
b64_lines = []
|
||||
in_data = False
|
||||
for line in armor_text.split("\n"):
|
||||
line = line.strip()
|
||||
if line == "-----BEGIN OTP MESSAGE-----":
|
||||
continue
|
||||
if line == "-----END OTP MESSAGE-----":
|
||||
break
|
||||
if line.startswith("Pad-ChkSum:"):
|
||||
chksum = line.split(":", 1)[1].strip()
|
||||
elif line.startswith("Pad-Offset:"):
|
||||
offset = int(line.split(":", 1)[1].strip())
|
||||
elif line.startswith("Version:"):
|
||||
continue
|
||||
elif line == "":
|
||||
in_data = True
|
||||
elif in_data:
|
||||
b64_lines.append(line)
|
||||
return chksum, offset, "".join(b64_lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Test Teensy 4.1 OTP SD pad")
|
||||
parser.add_argument("--port", default=DEFAULT_PORT, help="Serial port")
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"Connecting to {args.port}...")
|
||||
ser = serial.Serial(args.port, BAUD, timeout=2.0)
|
||||
time.sleep(6) # wait for boot + SD mount + pad bind
|
||||
|
||||
# Drain boot messages
|
||||
boot = ""
|
||||
while ser.in_waiting:
|
||||
boot += ser.read(ser.in_waiting).decode("utf-8", errors="replace")
|
||||
if boot:
|
||||
print(f"Boot output:\n{boot}")
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
# ---- Test 1: ASCII armor round-trip ----
|
||||
print("\n=== Test 1: ASCII armor round-trip ===")
|
||||
plaintext = b"Hello, OTP SD card pad!"
|
||||
pt_b64 = base64.b64encode(plaintext).decode()
|
||||
|
||||
r = test_verb(ser, "encrypt", [pt_b64, {"encoding": "ascii"}])
|
||||
if not r or "result" not in r:
|
||||
print("❌ encrypt failed")
|
||||
failed += 1
|
||||
ser.close()
|
||||
return 1
|
||||
|
||||
result = r["result"] if isinstance(r["result"], dict) else json.loads(r["result"])
|
||||
armor = result["ciphertext"]
|
||||
pad_chksum = result["pad_chksum"]
|
||||
off_before = int(result["pad_offset_before"])
|
||||
off_after = int(result["pad_offset_after"])
|
||||
|
||||
print(f" pad_chksum: {pad_chksum}")
|
||||
print(f" offset: {off_before} -> {off_after} (consumed {off_after - off_before} bytes)")
|
||||
|
||||
# Parse the armor
|
||||
armor_chksum, armor_offset, armor_b64 = parse_armor(armor)
|
||||
print(f" armor Pad-ChkSum: {armor_chksum}")
|
||||
print(f" armor Pad-Offset: {armor_offset}")
|
||||
|
||||
if armor_chksum != pad_chksum:
|
||||
print(f" ❌ FAIL: armor chksum {armor_chksum} != result chksum {pad_chksum}")
|
||||
failed += 1
|
||||
else:
|
||||
print(f" ✅ armor chksum matches")
|
||||
passed += 1
|
||||
|
||||
if armor_offset != off_before:
|
||||
print(f" ❌ FAIL: armor offset {armor_offset} != result offset_before {off_before}")
|
||||
failed += 1
|
||||
else:
|
||||
print(f" ✅ armor offset matches")
|
||||
passed += 1
|
||||
|
||||
# Decrypt
|
||||
r2 = test_verb(ser, "decrypt", [armor, {"encoding": "ascii"}])
|
||||
if not r2 or "result" not in r2:
|
||||
print("❌ decrypt failed")
|
||||
failed += 1
|
||||
ser.close()
|
||||
return 1
|
||||
|
||||
result2 = r2["result"] if isinstance(r2["result"], dict) else json.loads(r2["result"])
|
||||
recovered = base64.b64decode(result2["plaintext"])
|
||||
print(f" recovered: {recovered}")
|
||||
|
||||
if recovered == plaintext:
|
||||
print(f" ✅ ASCII round-trip SUCCESS")
|
||||
passed += 1
|
||||
else:
|
||||
print(f" ❌ ASCII round-trip FAILED: expected {plaintext}, got {recovered}")
|
||||
failed += 1
|
||||
|
||||
# ---- Test 2: Offset advance ----
|
||||
print("\n=== Test 2: Offset advance ===")
|
||||
r3 = test_verb(ser, "encrypt", [pt_b64, {"encoding": "ascii"}])
|
||||
if r3 and "result" in r3:
|
||||
result3 = r3["result"] if isinstance(r3["result"], dict) else json.loads(r3["result"])
|
||||
off2_before = int(result3["pad_offset_before"])
|
||||
off2_after = int(result3["pad_offset_after"])
|
||||
print(f" second encrypt offset: {off2_before} -> {off2_after}")
|
||||
if off2_before == off_after:
|
||||
print(f" ✅ offset advanced from first encrypt's end ({off_after})")
|
||||
passed += 1
|
||||
else:
|
||||
print(f" ❌ FAIL: expected offset_before={off_after}, got {off2_before}")
|
||||
failed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
# ---- Test 3: Binary .otp round-trip ----
|
||||
print("\n=== Test 3: Binary .otp round-trip ===")
|
||||
# Reset offset to 32 for a clean binary test by re-binding
|
||||
# (We can't reset via the API, so just use the current offset)
|
||||
|
||||
r4 = test_verb(ser, "encrypt", [pt_b64, {"encoding": "binary"}])
|
||||
if not r4 or "result" not in r4:
|
||||
print("❌ binary encrypt failed")
|
||||
failed += 1
|
||||
ser.close()
|
||||
return 1
|
||||
|
||||
result4 = r4["result"] if isinstance(r4["result"], dict) else json.loads(r4["result"])
|
||||
bin_b64 = result4["ciphertext"]
|
||||
bin_blob = base64.b64decode(bin_b64)
|
||||
print(f" binary blob size: {len(bin_blob)} bytes (header 58 + padded data {len(bin_blob) - 58})")
|
||||
|
||||
if bin_blob[:4] != b"OTP\0":
|
||||
print(f" ❌ FAIL: binary blob missing OTP magic: {bin_blob[:4]}")
|
||||
failed += 1
|
||||
else:
|
||||
print(f" ✅ binary blob has OTP magic")
|
||||
passed += 1
|
||||
|
||||
# Check header pad_chksum (bytes 6..38, binary)
|
||||
bin_chksum_bytes = bin_blob[6:38]
|
||||
bin_chksum_hex = bin_chksum_bytes.hex()
|
||||
if bin_chksum_hex == pad_chksum:
|
||||
print(f" ✅ binary header chksum matches")
|
||||
passed += 1
|
||||
else:
|
||||
print(f" ❌ FAIL: binary header chksum {bin_chksum_hex} != {pad_chksum}")
|
||||
failed += 1
|
||||
|
||||
# Decrypt binary
|
||||
r5 = test_verb(ser, "decrypt", [bin_b64, {"encoding": "binary"}])
|
||||
if not r5 or "result" not in r5:
|
||||
print("❌ binary decrypt failed")
|
||||
failed += 1
|
||||
ser.close()
|
||||
return 1
|
||||
|
||||
result5 = r5["result"] if isinstance(r5["result"], dict) else json.loads(r5["result"])
|
||||
recovered2 = base64.b64decode(result5["plaintext"])
|
||||
print(f" recovered (binary): {recovered2}")
|
||||
|
||||
if recovered2 == plaintext:
|
||||
print(f" ✅ Binary round-trip SUCCESS")
|
||||
passed += 1
|
||||
else:
|
||||
print(f" ❌ Binary round-trip FAILED: expected {plaintext}, got {recovered2}")
|
||||
failed += 1
|
||||
|
||||
# ---- Test 4: Large plaintext (2 KB, within 4 KB chunk cap) ----
|
||||
print("\n=== Test 4: Large plaintext (2 KB) ===")
|
||||
large_pt = bytes(range(256)) * 8 # 2048 bytes
|
||||
large_b64 = base64.b64encode(large_pt).decode()
|
||||
|
||||
r6 = test_verb(ser, "encrypt", [large_b64, {"encoding": "ascii"}])
|
||||
if r6 and "result" in r6:
|
||||
result6 = r6["result"] if isinstance(r6["result"], dict) else json.loads(r6["result"])
|
||||
large_off_before = int(result6["pad_offset_before"])
|
||||
large_off_after = int(result6["pad_offset_after"])
|
||||
large_consumed = large_off_after - large_off_before
|
||||
print(f" 10 KB plaintext: offset {large_off_before} -> {large_off_after} (consumed {large_consumed} bytes)")
|
||||
# Padme: 2 KB -> chunk = 4096 (minimum bucket, since 256*2^4=4096 >= 2048+1)
|
||||
if large_consumed == 4096:
|
||||
print(f" ✅ Padme bucket = 4096 (correct for 2 KB)")
|
||||
passed += 1
|
||||
else:
|
||||
print(f" ⚠️ Padme bucket = {large_consumed} (expected 16384)")
|
||||
# Not a hard fail — just note it
|
||||
|
||||
# Decrypt
|
||||
large_armor = result6["ciphertext"]
|
||||
r7 = test_verb(ser, "decrypt", [large_armor, {"encoding": "ascii"}])
|
||||
if r7 and "result" in r7:
|
||||
result7 = r7["result"] if isinstance(r7["result"], dict) else json.loads(r7["result"])
|
||||
large_recovered = base64.b64decode(result7["plaintext"])
|
||||
if large_recovered == large_pt:
|
||||
print(f" ✅ Large plaintext round-trip SUCCESS")
|
||||
passed += 1
|
||||
else:
|
||||
print(f" ❌ Large plaintext round-trip FAILED (len {len(large_recovered)} vs {len(large_pt)})")
|
||||
failed += 1
|
||||
else:
|
||||
failed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
# ---- Test 5: Tamper test ----
|
||||
print("\n=== Test 5: Tamper test ===")
|
||||
# Re-encrypt a small message for the tamper test
|
||||
r8 = test_verb(ser, "encrypt", [pt_b64, {"encoding": "ascii"}])
|
||||
if r8 and "result" in r8:
|
||||
result8 = r8["result"] if isinstance(r8["result"], dict) else json.loads(r8["result"])
|
||||
tamper_armor = result8["ciphertext"]
|
||||
|
||||
# Flip a character in the base64 data section
|
||||
lines = tamper_armor.split("\n")
|
||||
tampered = False
|
||||
for i, line in enumerate(lines):
|
||||
if re.match(r'^[A-Za-z0-9+/=]+$', line) and len(line) > 10:
|
||||
# Flip the first base64 char
|
||||
c = line[0]
|
||||
if c == 'A':
|
||||
lines[i] = 'B' + line[1:]
|
||||
else:
|
||||
lines[i] = 'A' + line[1:]
|
||||
tampered = True
|
||||
break
|
||||
tamper_armor = "\n".join(lines)
|
||||
|
||||
if tampered:
|
||||
r9 = test_verb(ser, "decrypt", [tamper_armor, {"encoding": "ascii"}])
|
||||
if r9 and "error" in r9:
|
||||
print(f" ✅ Tampered armor correctly rejected (error)")
|
||||
passed += 1
|
||||
elif r9 and "result" in r9:
|
||||
result9 = r9["result"] if isinstance(r9["result"], dict) else json.loads(r9["result"])
|
||||
tampered_recovered = base64.b64decode(result9["plaintext"])
|
||||
if tampered_recovered != plaintext:
|
||||
print(f" ✅ Tampered armor produced wrong plaintext (detected)")
|
||||
passed += 1
|
||||
else:
|
||||
print(f" ⚠️ Tampered armor still decrypted correctly (unlikely but possible if flip was in padding)")
|
||||
# Not a hard fail
|
||||
else:
|
||||
print(f" ❌ Tamper test: unexpected response")
|
||||
failed += 1
|
||||
else:
|
||||
print(f" ⚠️ Could not find base64 data to tamper")
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
# ---- Summary ----
|
||||
print(f"\n{'='*50}")
|
||||
print(f"OTP SD pad test: {passed} passed, {failed} failed")
|
||||
print(f"{'='*50}")
|
||||
|
||||
ser.close()
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -182,14 +182,19 @@ def main():
|
||||
pt_b64 = base64.b64encode(plaintext).decode()
|
||||
r = test_verb(ser, "encrypt", [pt_b64, {"algorithm": "otp"}])
|
||||
ct_b64 = None
|
||||
pad_off_before = None
|
||||
if r and "result" in r:
|
||||
passed += 1
|
||||
ct_b64 = r["result"].get("result", "")
|
||||
# The pad offset where this ciphertext's pad slice begins. decrypt
|
||||
# must rewind to this offset so the same pad bytes are reused.
|
||||
pad_off_before = r["result"].get("pad_offset_before")
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
if ct_b64:
|
||||
r = test_verb(ser, "decrypt", [ct_b64, {"algorithm": "otp"}])
|
||||
if ct_b64 and pad_off_before is not None:
|
||||
r = test_verb(ser, "decrypt",
|
||||
[ct_b64, {"algorithm": "otp", "pad_offset": int(pad_off_before)}])
|
||||
if r and "result" in r:
|
||||
pt_result = base64.b64decode(r["result"].get("result", ""))
|
||||
if pt_result == plaintext:
|
||||
|
||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -0,0 +1,338 @@
|
||||
# Teensy 4.1 Signer — Memory Budget Evaluation
|
||||
|
||||
**Date:** 2026-07-30
|
||||
**Context:** The SD-card OTP pad ([`plans/teensy41_otp_sd_pad.md`](teensy41_otp_sd_pad.md))
|
||||
is blocked. Root cause turned out to be a **DTCM stack shortage**, not an
|
||||
"SD library incompatibility". This document re-derives the memory budget from
|
||||
scratch and proposes solutions.
|
||||
|
||||
---
|
||||
|
||||
## 1. How Teensy 4.1 memory actually works
|
||||
|
||||
The i.MX RT1062 has three separate RAM regions plus flash:
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────────────────┐
|
||||
│ FLASH 8 MB (7936 KB usable) @ 0x60000000 │
|
||||
│ .text.code — code + rodata routed here by the linker script │
|
||||
│ .text.itcm — LOAD image of ITCM code (copied to ITCM at boot) │
|
||||
│ .data — LOAD image of DTCM data (copied to DTCM at boot) │
|
||||
├───────────────────────────────────────────────────────────────────────────┤
|
||||
│ FLEXRAM 512 KB = 16 banks × 32 KB @ 0x00000000 (ITCM) / 0x20000000 (DTCM)│
|
||||
│ Split between ITCM and DTCM AT BOOT by _flexram_bank_config. │
|
||||
│ ITCM = code that runs at full speed (zero wait state) │
|
||||
│ DTCM = .data + .bss + THE STACK │
|
||||
├───────────────────────────────────────────────────────────────────────────┤
|
||||
│ RAM2 / OCRAM 512 KB @ 0x20200000 │
|
||||
│ .bss.dma (DMAMEM statics) + the malloc heap │
|
||||
├───────────────────────────────────────────────────────────────────────────┤
|
||||
│ ERAM / PSRAM 0 MB (unpopulated) @ 0x70000000 │
|
||||
│ Linker script reserves 32 MB but the chips are NOT soldered on. │
|
||||
└───────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
The FlexRAM split is computed by the linker script
|
||||
([`imxrt1062_t41_flashmem.ld:215`](../firmware/teensy41/signer/imxrt1062_t41_flashmem.ld:215)):
|
||||
|
||||
```ld
|
||||
_itcm_block_count = (SIZEOF(.text.itcm) + SIZEOF(.ARM.exidx) + 0x7FFF) >> 15;
|
||||
_estack = ORIGIN(DTCM) + ((16 - _itcm_block_count) << 15);
|
||||
```
|
||||
|
||||
**This is the crux:** every 32 KB bank given to ITCM is taken away from DTCM.
|
||||
Code size therefore directly steals stack space. And crucially:
|
||||
|
||||
```ld
|
||||
.data : {
|
||||
*(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) ◄── READ-ONLY DATA IN DTCM!
|
||||
*(SORT_BY_ALIGNMENT(SORT_BY_NAME(.data*)))
|
||||
} > DTCM AT> FLASH
|
||||
```
|
||||
|
||||
**`.rodata` (const tables, string literals, fonts, wordlists) is being placed
|
||||
in DTCM**, even though it is read-only and could live in flash. This is the
|
||||
single biggest waste in the current layout.
|
||||
|
||||
---
|
||||
|
||||
## 2. Measured state of every build we tried
|
||||
|
||||
All numbers are bytes, measured with `arm-none-eabi-objdump -h` on the ELF.
|
||||
|
||||
| # | Build variant | `.text.itcm` | ITCM banks | DTCM total | `.data` | `.bss` | data+bss | **Free stack** | Result |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| 1 | **Baseline v0.1.6** (no SD, HKDF pad) | 339,936 | 11 | 163,840 | 129,728 | 24,640 | 154,368 | **9,472** | ✅ boots, 24/24 tests |
|
||||
| 2 | Arduino `<SD.h>` wrapper | 385,664 | **12** | 131,072 | 132,800 | 25,792 | 158,592 | **−27,520** | ❌ hard fault at boot |
|
||||
| 3 | SdFat FAT-only, all in ITCM | 361,408 | **12** | 131,072 | 131,776 | 26,080 | 157,856 | **−26,784** | ❌ hard fault at boot |
|
||||
| 4 | SdFat FAT-only, **all → FLASH** | 348,480 | 11 | 163,840 | 131,776 | 26,080 | 157,856 | **5,984** | ⚠️ boots, `sd.begin()`/scan fails |
|
||||
| 5 | SdFat FAT-only, SDIO kept in ITCM | 355,056 | 11 | 163,840 | 131,776 | 26,080 | 157,856 | **5,984** | ❓ **never tested** |
|
||||
|
||||
### Sanity check of the model
|
||||
|
||||
Build 1 arithmetic reproduces the number arduino-cli itself reports:
|
||||
|
||||
```
|
||||
ITCM code 339,936 → ceil(339936/32768) = 11 banks = 360,448 (padding 20,512)
|
||||
DTCM = (16 − 11) × 32768 = 163,840
|
||||
minus .data+.bss = 154,368
|
||||
free stack = 9,472 ◄── matches the documented 9,632
|
||||
```
|
||||
|
||||
### Two corrections to earlier conclusions
|
||||
|
||||
1. **Builds 2 and 3 did not "crash because of the SD library."** They crashed
|
||||
because ITCM crossed the 352 KB → 384 KB bank boundary, which stole a 32 KB
|
||||
bank from DTCM and made `.data`+`.bss` (158 KB) **larger than the entire
|
||||
DTCM region** (128 KB). The linker cannot detect this because the split is
|
||||
computed at runtime by the boot ROM.
|
||||
|
||||
2. **I previously miscalculated build 5** as needing 12 banks. It needs 11
|
||||
(355,056 ≤ 360,448). Build 5 fits, has the same 5,984 bytes of stack as
|
||||
build 4, and **was never flashed** — I reverted the linker change before
|
||||
testing it. That test is still owed.
|
||||
|
||||
### Why build 4 boots but SD operations fail
|
||||
|
||||
Build 4 leaves **5,984 bytes of stack** — a 37% reduction from the already
|
||||
marginal 9,472-byte baseline. SdFat's `begin()` → card identify → FAT mount
|
||||
chain, and `openNextFile()` directory walks, allocate multi-hundred-byte
|
||||
frames several levels deep. The most probable explanation for
|
||||
"`sd.begin()` fails" and "`otp_debug` disconnects the device" is **stack
|
||||
overflow into `.bss`**, not flash execution speed.
|
||||
|
||||
The 32-byte MPU guard at the end of `.bss`
|
||||
([`imxrt1062_t41_flashmem.ld:177`](../firmware/teensy41/signer/imxrt1062_t41_flashmem.ld:177))
|
||||
catches a hard overrun as a fault — which is exactly the "device disconnects"
|
||||
symptom we saw.
|
||||
|
||||
---
|
||||
|
||||
## 3. Where the space is going
|
||||
|
||||
```
|
||||
FLEXRAM 512 KB ── 16 banks ── current build 4 layout
|
||||
┌──────────────────────────────────────────────┬────────────────────────────┐
|
||||
│ ITCM 11 banks = 352 KB │ DTCM 5 banks = 160 KB │
|
||||
├──────────────────────────────────────────────┼────────────────────────────┤
|
||||
│ ██████████████████████████████████████░░░░ │ ████████████████████████▓░ │
|
||||
│ ↑ code 348,480 (99%) ↑ pad 11,968 │ ↑ .data 131,776 ↑bss ↑↑ │
|
||||
│ │ (80% of DTCM!) 26,080 5,984│
|
||||
└──────────────────────────────────────────────┴────────────────────────────┘
|
||||
↑ STACK
|
||||
ONLY 5.8 KB LEFT
|
||||
|
||||
RAM2 / OCRAM 512 KB
|
||||
┌───────────────────────────────────────────────────────────────────────────┐
|
||||
│ ███████████████████████████████████████████████████████████████░░░░░░░░░░ │
|
||||
│ ↑ .bss.dma 413,600 (LVGL buffers, crypto workspaces) ↑ heap 110,688 │
|
||||
└───────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
FLASH 7936 KB
|
||||
┌───────────────────────────────────────────────────────────────────────────┐
|
||||
│ ██████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
|
||||
│ ↑ ~1.6 MB used ↑ ~6.3 MB FREE (80% unused!) │
|
||||
└───────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**The asymmetry is the whole story:** DTCM has 5.8 KB free while FLASH has
|
||||
6.3 MB free. And 128 KB of DTCM — 80% of the region — is occupied by `.data`,
|
||||
most of which is `.rodata` that has no business being in RAM at all.
|
||||
|
||||
### What is likely inside that 128 KB of `.data`/`.rodata`
|
||||
|
||||
Not yet measured (see Step 1 below), but the candidates, largest first:
|
||||
|
||||
| Suspect | Estimate | Notes |
|
||||
|---|---|---|
|
||||
| BIP-39 wordlist ([`mnemonic_wordlist.h`](../firmware/teensy41/signer/src/mnemonic_wordlist.h)) | 16–24 KB | 2048 const strings |
|
||||
| LVGL fonts (montserrat 14/20) + LVGL const tables | 20–40 KB | pure rodata |
|
||||
| PQClean constants (ML-DSA/ML-KEM/SLH-DSA zetas, SHAKE tables) | 10–20 KB | some already routed to flash |
|
||||
| cJSON, bech32, base64 tables, format strings | 5–10 KB | |
|
||||
| ed25519 `ed_K`/`ed_X`/`ed_Y` | ~1 KB | **must stay in DTCM** (documented regression) |
|
||||
| Genuine writable `.data` | 10–30 KB | LVGL state, USB endpoint queues |
|
||||
|
||||
---
|
||||
|
||||
## 4. Evaluation
|
||||
|
||||
### What is genuinely working
|
||||
|
||||
- [`otppad_embedded.{h,c}`](../firmware/teensy41/signer/src/otppad_embedded.h) —
|
||||
bit-compatible with [`libotppad`](../libotppad/libotppad.h), **2386/2386**
|
||||
host tests pass. Zero doubt about the format layer.
|
||||
- [`otp_pad_sd.{h,cpp}`](../firmware/teensy41/signer/src/otp_pad_sd.h) —
|
||||
logic complete (bind, seek/read, XOR, Padmé, armor, binary `.otp`, atomic
|
||||
offset). Never had a chance to execute.
|
||||
- [`pad_gen.ino`](../firmware/teensy41/pad_gen/pad_gen.ino) — a real 1 MB
|
||||
TRNG pad exists on the card with a verified checksum.
|
||||
- SD hardware, wiring, and card are all proven good (the standalone probe
|
||||
sketches read the 1 TB card and did write/read/verify round-trips).
|
||||
|
||||
### The actual problem, stated precisely
|
||||
|
||||
> The signer firmware has **5,984 bytes of stack** in the best SD-enabled
|
||||
> build. SdFat needs more than that to mount a volume and walk a directory.
|
||||
> There is no way around this by moving *code*; we must reclaim **DTCM**.
|
||||
|
||||
Nothing is wrong with the SD library, the linker-script approach, or the OTP
|
||||
implementation. We are simply out of stack.
|
||||
|
||||
### Why this was hard to see
|
||||
|
||||
- The linker reports no error: the ITCM/DTCM split happens at boot, not link
|
||||
time, so an over-committed DTCM links cleanly and faults at reset.
|
||||
- `arduino-cli` prints "free for local variables" only on the *default*
|
||||
linker-script path; with `-T<custom>.ld` it errors out of the size step
|
||||
("Error while determining sketch size"), so we lost our early-warning gauge.
|
||||
- Symptoms (no USB enumeration, `sd.begin()` returning false, the device
|
||||
vanishing mid-request) all look like driver problems but are stack overflow.
|
||||
|
||||
---
|
||||
|
||||
## 5. Proposed solutions
|
||||
|
||||
Ordered by leverage. **A is the recommended path** and is likely sufficient on
|
||||
its own.
|
||||
|
||||
### Solution A — Move `.rodata` out of DTCM into FLASH (recommended)
|
||||
|
||||
**Reclaims: an estimated 40–90 KB of DTCM. Effort: low. Risk: low-moderate.**
|
||||
|
||||
The linker script currently puts every `.rodata*` input section into DTCM
|
||||
([line 168](../firmware/teensy41/signer/imxrt1062_t41_flashmem.ld:168)).
|
||||
Read-only data does not need to be in tightly-coupled RAM; the Cortex-M7 has a
|
||||
16 KB D-cache in front of FLEXSPI and const tables are cache-friendly.
|
||||
|
||||
Change `.data` to stop absorbing rodata, and add a catch-all rodata rule to the
|
||||
FLASH output section, with a **targeted exception list** for known-sensitive
|
||||
tables:
|
||||
|
||||
```ld
|
||||
.data : {
|
||||
*(.endpoint_queue)
|
||||
*ed25519.c.o(.rodata*) /* ed_K/ed_X/ed_Y must stay in DTCM */
|
||||
*(SORT_BY_ALIGNMENT(SORT_BY_NAME(.data*)))
|
||||
KEEP(*(.vectorsram))
|
||||
} > DTCM AT> FLASH
|
||||
```
|
||||
|
||||
The ed25519 exception is not speculative — the linker script already documents
|
||||
that moving `ed25519.c.o(.rodata*)` to flash produced an all-zeros pubkey
|
||||
([lines 35–39](../firmware/teensy41/signer/imxrt1062_t41_flashmem.ld:35)).
|
||||
That regression is the template for what to watch for.
|
||||
|
||||
**Payoff:** if `.data` drops from 128 KB to, say, 60 KB, free stack goes from
|
||||
5,984 to roughly **74,000 bytes** — an order-of-magnitude improvement that
|
||||
removes the stack question entirely, for SD and for the PQ crypto paths.
|
||||
|
||||
**Risk & mitigation:** some library may depend on a const table being in RAM
|
||||
(as ed25519 did). Mitigation is incremental: move rodata per-object-file in
|
||||
small batches, run [`test_classical.py`](../firmware/teensy41/test_classical.py)
|
||||
(16 tests) and [`test_signer.py`](../firmware/teensy41/test_signer.py) (24
|
||||
tests) after each batch, and bisect any failure to the offending `.o`.
|
||||
|
||||
### Solution B — Restore the build-time memory gauge
|
||||
|
||||
**Reclaims: nothing. Effort: very low. Value: high.**
|
||||
|
||||
We are flying blind. Add a post-link check to
|
||||
[`build_signer.sh`](../firmware/teensy41/build_signer.sh) that computes the
|
||||
same arithmetic the boot ROM will use and **fails the build** if the stack
|
||||
would be under a threshold:
|
||||
|
||||
```
|
||||
itcm_banks = ceil((text.itcm + ARM.exidx) / 32768)
|
||||
dtcm_bytes = (16 - itcm_banks) * 32768
|
||||
free_stack = dtcm_bytes - data - bss
|
||||
FAIL if free_stack < 16384
|
||||
```
|
||||
|
||||
This converts every future "mysterious boot crash" into a build error with a
|
||||
number attached. Should be done regardless of which other solution we pick.
|
||||
|
||||
### Solution C — Test build 5 (SDIO in ITCM, FAT layer in FLASH)
|
||||
|
||||
**Reclaims: nothing. Effort: trivial. Value: eliminates a hypothesis.**
|
||||
|
||||
Build 5 fits in 11 banks and was never flashed. If the real problem is flash
|
||||
execution speed for the SDIO driver rather than stack, build 5 is the fix and
|
||||
costs nothing. If it fails the same way, that confirms the stack diagnosis.
|
||||
Cheap experiment; do it before or alongside A.
|
||||
|
||||
### Solution D — Shrink the OTP feature's own footprint
|
||||
|
||||
**Reclaims: a few KB. Effort: low. Value: moderate.**
|
||||
|
||||
- Drop `OTP_SD_MAX_CHUNK` from 16 KB to 4 KB (Padmé bucket 4096 covers ~4 KB
|
||||
plaintext, ample for Nostr `content`). Cuts the two malloc'd scratch buffers.
|
||||
- Remove `verify_pad_checksum()` from the bind path, or gate it behind an
|
||||
explicit `otp_verify` verb. Streaming 1 MB at boot is slow, deep-stacked, and
|
||||
will be flatly impossible on the 900 GB pad. Verify the first and last 4 KB
|
||||
instead, or trust the filename.
|
||||
- Replace the `openNextFile()` scan with a direct
|
||||
`sd.exists("/pads/<chksum>.pad")` when a chksum is already known, skipping
|
||||
the directory walk entirely.
|
||||
|
||||
### Solution E — Move LVGL draw buffers to the heap, shrink DMAMEM
|
||||
|
||||
**Reclaims: DTCM indirectly. Effort: moderate. Value: situational.**
|
||||
|
||||
`.bss.dma` is 413,600 of 512 KB in RAM2. The two LVGL buffers are ~46 KB of
|
||||
that. This does not directly help DTCM, but if we ever need RAM2 headroom for
|
||||
SD block buffers it is the place to look.
|
||||
|
||||
### Solution F — Reduce feature scope
|
||||
|
||||
**Effort: none. Value: last resort.**
|
||||
|
||||
If A through D all fail to yield enough stack, the fallback is to make features
|
||||
mutually exclusive at build time — e.g. an OTP-focused firmware build that
|
||||
omits SLH-DSA-128s (the largest PQ algorithm) and reclaims its ITCM and rodata.
|
||||
This is a product decision, not an engineering one, and should only be reached
|
||||
after A is proven insufficient.
|
||||
|
||||
### Non-solution: external PSRAM
|
||||
|
||||
The linker script reserves 32 MB of ERAM at `0x70000000`, and `.bss.extram`
|
||||
currently has size 0. **The Teensy 4.1 ships with the two PSRAM pads empty** —
|
||||
this memory does not physically exist unless chips are soldered on. Not a
|
||||
software option.
|
||||
|
||||
---
|
||||
|
||||
## 6. Recommended sequence
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
B[Solution B: build-time stack gauge<br/>fail build under 16 KB] --> C[Solution C: flash build 5<br/>SDIO in ITCM, FAT in FLASH]
|
||||
C -->|works| D[Solution D: trim OTP footprint<br/>4 KB chunks, drop boot checksum]
|
||||
C -->|still fails| A[Solution A: move rodata to FLASH<br/>incremental, test each batch]
|
||||
A --> D
|
||||
D --> T[Phase 5: run test_otp_sd.py]
|
||||
T --> U[Phase 6: ui_pick_pad LVGL screen]
|
||||
```
|
||||
|
||||
1. **B** first — 20 minutes, and every subsequent step gets a number instead of
|
||||
a guess.
|
||||
2. **C** next — trivial, and it either fixes the problem or kills a hypothesis.
|
||||
3. **A** if C did not fix it — this is the real headroom, and it benefits the
|
||||
whole project (the PQ paths have been stack-starved since v0.1.3).
|
||||
4. **D** as cleanup once there is room to breathe.
|
||||
5. Then resume Phases 5 and 6 of the OTP plan.
|
||||
|
||||
---
|
||||
|
||||
## 7. Decisions needed
|
||||
|
||||
1. **Is Solution A acceptable?** It touches the linker script that took six
|
||||
versions to stabilise (v0.1.1–v0.1.6 were all memory fixes). The upside is
|
||||
large and it fixes a latent problem, but it needs a full re-run of both test
|
||||
suites and carries a real chance of an ed25519-style surprise.
|
||||
2. **Can we drop the boot-time pad checksum verify?** Technically right (it
|
||||
cannot scale to a 900 GB pad) but it is a security-posture change: we would
|
||||
trust the filename rather than prove the pad's integrity at bind time.
|
||||
3. **Is a 4 KB max OTP chunk acceptable?** It caps a single `encrypt` call at
|
||||
~4 KB of plaintext; larger payloads would need caller-side chunking.
|
||||
4. **What stack floor do we want?** Suggest 16 KB minimum, 32 KB target. The
|
||||
historical 9.6 KB was the direct cause of six versions of crash-fixing.
|
||||
@@ -0,0 +1,321 @@
|
||||
# Plan: Real SD-card OTP pad for the Teensy 4.1 signer
|
||||
|
||||
## Goal
|
||||
|
||||
Replace the Teensy 4.1 firmware's throwaway HKDF-derived 1024-byte in-RAM OTP
|
||||
pad ([`firmware/teensy41/signer/src/otp_pad.cpp`](firmware/teensy41/signer/src/otp_pad.cpp))
|
||||
with a **real SD-card pad** that reads `<chksum>.pad` / `<chksum>.state` from
|
||||
the Teensy's built-in SD slot, bit-compatible with the `otp` project and the
|
||||
host `n_signer` ([`src/otp_pad.c`](src/otp_pad.c)) via [`libotppad`](libotppad/libotppad.h).
|
||||
|
||||
This means the firmware's `encrypt`/`decrypt` verbs must gain:
|
||||
- Padmé padding (ISO/IEC 9797-1 Method 2) with exponential bucketing.
|
||||
- ASCII armored output (`-----BEGIN OTP MESSAGE-----` + base64) **and** binary
|
||||
`.otp` output (magic `OTP\0` + 58-byte header), selected per request via an
|
||||
`encoding` option — matching the host's `otp_encrypt`/`otp_decrypt` verbs.
|
||||
- Per-pad offset persistence in `<chksum>.state` (atomic write-temp-then-rename
|
||||
on the SD card).
|
||||
- Pad binding at startup (mount SD, find pad by chksum, verify checksum, read
|
||||
offset).
|
||||
|
||||
The interactive "look for existing pads and ask the user to confirm one" UI
|
||||
flow is the **last** phase. Until then, a debug auto-bind path lets us test
|
||||
encrypt/decrypt round-trips over USB CDC without touching the screen.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Multi-device offset coordination (deferred in
|
||||
[`plans/otp_nostr_integration.md`](plans/otp_nostr_integration.md)).
|
||||
- Nostr kind-30078 event wrapping (caller's job, same as host).
|
||||
- Production pad entropy: the test pad is `/dev/urandom`-sourced, fine for
|
||||
validating the format and round-trips.
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
SD[(SD card exFAT<br/>pads/chksum.pad<br/>pads/chksum.state)] -->|SD.begin BUILTIN_SDCARD| M[otp_pad_sd.cpp<br/>mount + bind + seek/read]
|
||||
M -->|otppad_embedded| L[libotppad port<br/>XOR, base64, Padme, armor, checksum]
|
||||
L --> E[otp_pad_encrypt/decrypt<br/>encoding: ascii or binary]
|
||||
E --> D[dispatch.cpp<br/>encrypt/decrypt verbs]
|
||||
D -->|USB CDC framed JSON-RPC| Host[test_otp_sd.py]
|
||||
Boot[signer.ino boot flow] -->|after seed| M
|
||||
Boot -.->|last phase| UI[ui_pick_pad<br/>LVGL list + confirm]
|
||||
```
|
||||
|
||||
### Module layout
|
||||
|
||||
- [`firmware/teensy41/signer/src/otp_pad_sd.h`](firmware/teensy41/signer/src/otp_pad_sd.h) —
|
||||
new public API: `otp_pad_sd_mount()`, `otp_pad_sd_bind(chksum)`,
|
||||
`otp_pad_sd_bind_first()` (debug), `otp_pad_sd_unbind()`,
|
||||
`otp_pad_sd_encrypt(pt, len, encoding, out, out_len)`,
|
||||
`otp_pad_sd_decrypt(input, len, encoding, out, out_len)`,
|
||||
`otp_pad_sd_ready()`, `otp_pad_sd_chksum()`, `otp_pad_sd_offset()`,
|
||||
`otp_pad_sd_size()`.
|
||||
- [`firmware/teensy41/signer/src/otp_pad_sd.cpp`](firmware/teensy41/signer/src/otp_pad_sd.cpp) —
|
||||
implementation over the Arduino `SD` library (4-bit SDMMC,
|
||||
`BUILTIN_SDCARD`). Holds the bound pad's `File` (read-only) + state in
|
||||
file-static globals, mirroring [`src/otp_pad.c`](src/otp_pad.c)'s
|
||||
`otp_pad_state_t`.
|
||||
- [`firmware/teensy41/signer/src/otppad_embedded.h`](firmware/teensy41/signer/src/otppad_embedded.h) /
|
||||
`.cpp` — a Teensy/Arduino-friendly port of the format-critical functions from
|
||||
[`libotppad/libotppad.c`](libotppad/libotppad.c): `otppad_xor`,
|
||||
`otppad_base64_encode/decode`, `otppad_chunk_size`, `otppad_pad_apply/remove`,
|
||||
`otppad_armor_parse/generate`, `otppad_checksum` (streaming, over a `File*`),
|
||||
`otppad_state_read/write` (over SD `File`). No POSIX `FILE*`/`malloc`/`strtok`
|
||||
dependencies that don't exist on Teensy; uses `malloc`/`free` (available via
|
||||
newlib) and Arduino `String`/manual parsing where needed. **Bit-identical
|
||||
output to libotppad** — same constants, same byte order, same header layout.
|
||||
|
||||
### Wire format (align with host `otp_encrypt`/`otp_decrypt`)
|
||||
|
||||
The existing Teensy `encrypt`/`decrypt` verbs return raw base64 XOR +
|
||||
`pad_offset_before`/`pad_offset_after`. The host verbs return ASCII armor or a
|
||||
binary `.otp` blob with the offset embedded. To be bit-compatible and reusable
|
||||
with the existing [`tools/otp_roundtrip_test.py`](tools/otp_roundtrip_test.py)
|
||||
pattern, the Teensy verbs will be upgraded to match the host:
|
||||
|
||||
- `encrypt` params: `[plaintext_b64, {"encoding": "ascii"|"binary"}]`
|
||||
→ result JSON: `{"ciphertext": "<armor or b64-of-blob>", "pad_chksum": "<64hex>", "pad_offset_before": N, "pad_offset_after": N}`.
|
||||
- `decrypt` params: `[ciphertext, {"encoding": "ascii"|"binary"}]`
|
||||
→ result JSON: `{"plaintext": "<b64>"}`. The offset is read from the armor
|
||||
header / binary header (no `pad_offset` option needed, matching the host).
|
||||
|
||||
The `algorithm: "otp"` option is kept for backward compatibility with
|
||||
[`test_signer.py`](firmware/teensy41/test_signer.py) but is optional.
|
||||
|
||||
### Memory budget
|
||||
|
||||
The Teensy 4.1 has ~110 KB free heap (RAM2/DMAMEM) and ~9.6 KB free DTCM stack.
|
||||
The pad is **never** loaded whole. Each request:
|
||||
1. Decodes base64 plaintext into a DMAMEM scratch buffer (max chunk = 4 MB on
|
||||
host; cap at **64 KB** on Teensy to fit heap — plenty for Nostr event
|
||||
content).
|
||||
2. Seeks the pad `File` to the current offset, reads exactly `chunk` bytes into
|
||||
a second DMAMEM buffer.
|
||||
3. XORs in place, encodes output, zeroizes scratch, advances offset in
|
||||
`.state`.
|
||||
|
||||
All large buffers go in `DMAMEM` (RAM2), matching the existing crypto
|
||||
workspace pattern in [`signer.ino`](firmware/teensy41/signer/signer.ino:63).
|
||||
|
||||
## Phased implementation
|
||||
|
||||
### Phase 0 — Cleanup
|
||||
|
||||
- [ ] Delete [`firmware/teensy41/otp_card_probe/`](firmware/teensy41/otp_card_probe/) (the throwaway probe sketch).
|
||||
- [ ] Delete [`firmware/teensy41/sd_test/`](firmware/teensy41/sd_test/) (bring-up sketch, superseded).
|
||||
- [ ] Confirm the smaller card is still readable by re-running the probe logic
|
||||
once inside the real firmware's SD mount (no separate sketch).
|
||||
|
||||
### Phase 1 — Generate a test pad on the smaller card
|
||||
|
||||
The Teensy's SD slot isn't accessible from the host, so the pad must be
|
||||
generated on-device. Two options (pick one):
|
||||
|
||||
- **A. One-time pad-generator sketch** `firmware/teensy41/pad_gen/pad_gen.ino`:
|
||||
mounts the SD card, writes `<chksum>.pad` (e.g. 1 MB from the Teensy's TRNG /
|
||||
`analogRead` noise + `LibRandom` if available, else `/dev/urandom`-equivalent
|
||||
PRNG seeded from `ENTROPY` registers), computes the XOR checksum, writes
|
||||
`<chksum>.state` with `offset=32\n`. This is a **utility**, not test firmware;
|
||||
it can be deleted after the pad exists. Uses the same checksum algorithm as
|
||||
[`tools/make_test_pad.c`](tools/make_test_pad.c:39) so the pad is
|
||||
bit-compatible.
|
||||
- **B. Host generation via USB reader**: if a USB SD reader is available, pop
|
||||
the card, run `make_test_pad <mount>/pads 1048576` on the host, reinsert.
|
||||
|
||||
Default: **A** (no USB reader assumed). The generator is clearly marked as a
|
||||
setup utility and removed in Phase 0 of a future cleanup once the pad exists.
|
||||
|
||||
- [ ] Write `firmware/teensy41/pad_gen/pad_gen.ino` (1 MB pad, 32-byte header,
|
||||
checksum-named, `offset=32\n` state).
|
||||
- [ ] Flash + run it; record the generated `<chksum>` for use in tests.
|
||||
|
||||
### Phase 2 — Port libotppad to the firmware (`otppad_embedded`)
|
||||
|
||||
- [ ] Create `otppad_embedded.h` declaring the format-critical functions.
|
||||
- [ ] Port `otppad_xor`, `otppad_base64_encode/decode` (reuse the existing
|
||||
`b64_encode`/`b64_decode` in dispatch.cpp if bit-identical, else port
|
||||
libotppad's tables).
|
||||
- [ ] Port `otppad_chunk_size`, `otppad_pad_apply`, `otppad_pad_remove` (Padmé).
|
||||
- [ ] Port `otppad_armor_parse` / `otppad_armor_generate` (replace `strtok` with
|
||||
manual line splitting; replace `snprintf` with Arduino `sprintf`).
|
||||
- [ ] Port `otppad_checksum` as a streaming function over an Arduino `File*`
|
||||
(read in 4 KB chunks, fold into 32 buckets, XOR with first 32 pad bytes).
|
||||
- [ ] Port `otppad_state_read` / `otppad_state_write` over SD `File` (atomic
|
||||
write: write `<chksum>.state.tmp`, `SD.rename` over `<chksum>.state`).
|
||||
- [ ] Add a host-buildable unit test
|
||||
`firmware/teensy41/signer/tests/host_test_otppad_embedded.c` that links
|
||||
`otppad_embedded.c` compiled with `HOST_TEST` against a real `FILE*`
|
||||
backend, and verifies round-trip + padding + armor + checksum against
|
||||
`libotppad` outputs (bit-compatibility check).
|
||||
|
||||
### Phase 3 — `otp_pad_sd.cpp` (bind + seek/read + encrypt/decrypt)
|
||||
|
||||
- [ ] Create `otp_pad_sd.h` with the bind/encrypt/decrypt API above.
|
||||
- [ ] Implement `otp_pad_sd_mount()` — `SD.begin(BUILTIN_SDCARD)`, report
|
||||
failure over Serial.
|
||||
- [ ] Implement `otp_pad_sd_bind(chksum)` — open `<chksum>.pad` read-only,
|
||||
verify checksum via `otppad_checksum`, read offset from `.state` (default
|
||||
to 32 if missing), store pad size + chksum in globals.
|
||||
- [ ] Implement `otp_pad_sd_bind_first()` — scan root for `*.pad`, bind the
|
||||
first one (debug auto-bind path).
|
||||
- [ ] Implement `otp_pad_sd_encrypt` — Padmé-pad, seek+read pad slice, XOR,
|
||||
encode (ascii/binary), advance offset atomically, zeroize scratch.
|
||||
- [ ] Implement `otp_pad_sd_decrypt` — parse armor/binary header, seek+read pad
|
||||
slice, XOR, strip Padmé, zeroize scratch. Does **not** advance offset
|
||||
(decrypt is non-consuming, matching host).
|
||||
- [ ] Implement `otp_pad_sd_unbind` — close `File`, zeroize state.
|
||||
- [ ] All scratch buffers in `DMAMEM`; cap chunk at 64 KB.
|
||||
|
||||
### Phase 4 — Wire into dispatch + boot flow (debug auto-bind)
|
||||
|
||||
- [ ] In [`signer.ino`](firmware/teensy41/signer/signer.ino:262)
|
||||
`apply_mnemonic()`: after seed derivation, **remove** the
|
||||
`otp_pad_init(g_seed, ...)` HKDF call. Replace with: call
|
||||
`otp_pad_sd_mount()`; if `DEBUG_AUTO_GENERATE=1`, call
|
||||
`otp_pad_sd_bind_first()` and log the bound chksum over Serial. On
|
||||
failure, log but continue (encrypt/decrypt verbs will return
|
||||
`otp pad not bound`).
|
||||
- [ ] In [`dispatch.cpp`](firmware/teensy41/signer/src/dispatch.cpp:1694)
|
||||
`encrypt`/`decrypt` verbs: replace the in-RAM `otp_pad_apply`/`seek` path
|
||||
with calls to `otp_pad_sd_encrypt`/`otp_pad_sd_decrypt`. Parse
|
||||
`encoding` option (`"ascii"` default, `"binary"`). Build the result JSON
|
||||
to match the host wire format (`ciphertext`, `pad_chksum`,
|
||||
`pad_offset_before`, `pad_offset_after` for encrypt; `plaintext` for
|
||||
decrypt). Keep `algorithm: "otp"` optional for backward compat.
|
||||
- [ ] Remove the old [`otp_pad.cpp`](firmware/teensy41/signer/src/otp_pad.cpp) /
|
||||
[`otp_pad.h`](firmware/teensy41/signer/src/otp_pad.h) HKDF implementation
|
||||
(superseded by `otp_pad_sd.*`).
|
||||
|
||||
### Phase 5 — Test harness + hardware round-trips
|
||||
|
||||
- [ ] Write `firmware/teensy41/test_otp_sd.py` — over USB CDC, framed JSON-RPC:
|
||||
- `get_info` (sanity).
|
||||
- `encrypt` ascii → parse armor, `Pad-ChkSum` matches bound chksum,
|
||||
`Pad-Offset` = 32 (first call).
|
||||
- `decrypt` ascii → recovered plaintext matches.
|
||||
- `encrypt` binary → blob starts with `OTP\0`, header chksum matches,
|
||||
`pad_offset` = 32 + first chunk.
|
||||
- `decrypt` binary → recovered plaintext matches.
|
||||
- Second `encrypt` ascii → `Pad-Offset` advanced by first chunk (proves
|
||||
offset persistence in `.state`).
|
||||
- Reboot the Teensy (power cycle), `encrypt` again → `Pad-Offset` continues
|
||||
from where it left off (proves `.state` survives power cycle).
|
||||
- Large plaintext (e.g. 10 KB) → Padmé bucket doubles to 16 KB, round-trip
|
||||
OK.
|
||||
- Tamper test: flip one byte in the armor base64 → decrypt returns error
|
||||
(padding removal fails) or wrong plaintext (detected).
|
||||
- [ ] Run it against the flashed firmware; iterate on failures.
|
||||
|
||||
### Phase 6 — Interactive pad selection UI (LAST)
|
||||
|
||||
- [ ] Add `ui_pick_pad(pads_list, count) -> selected_chksum` to
|
||||
[`ui.h`](firmware/teensy41/signer/src/ui.h) / `ui.cpp`: an LVGL list
|
||||
screen showing each pad's chksum prefix + size + used%, with a "Use this
|
||||
pad" / "Skip OTP" choice. Blocks (pumps LVGL) until the user picks.
|
||||
- [ ] In `signer.ino` boot flow, when `DEBUG_AUTO_GENERATE=0`: after
|
||||
`apply_mnemonic()`, scan the SD root for `*.pad`, build the list, call
|
||||
`ui_pick_pad()`. If the user picks one, `otp_pad_sd_bind(chksum)`. If
|
||||
"Skip OTP" or no pads found, continue without a bound pad.
|
||||
- [ ] Keep `DEBUG_AUTO_GENERATE=1` → `otp_pad_sd_bind_first()` as the test path
|
||||
so Phase 5 tests still run headless.
|
||||
|
||||
## Test commands (Phase 5)
|
||||
|
||||
```bash
|
||||
# Build + flash the real firmware
|
||||
bash firmware/teensy41/build_signer.sh --flash
|
||||
|
||||
# OTP SD round-trip suite
|
||||
python3 firmware/teensy41/test_otp_sd.py --port /dev/ttyACM0
|
||||
|
||||
# Host-side bit-compatibility check for otppad_embedded
|
||||
cc -O2 -Wall -Wextra -D HOST_TEST -o host_test_otppad_embedded \
|
||||
firmware/teensy41/signer/src/otppad_embedded.c \
|
||||
firmware/teensy41/signer/tests/host_test_otppad_embedded.c -lm
|
||||
./host_test_otppad_embedded
|
||||
```
|
||||
|
||||
## Status (2026-07-28)
|
||||
|
||||
### Done
|
||||
|
||||
- **Phase 0**: Throwaway sketches deleted.
|
||||
- **Phase 1**: [`pad_gen.ino`](firmware/teensy41/pad_gen/pad_gen.ino) written,
|
||||
compiled, flashed, and run on the smaller card. Generated a 1 MB TRNG-sourced
|
||||
pad `4ec4e221...b0ca78.pad` + `.state` (offset=32). The Teensy 4.1's hardware
|
||||
TRNG (`TRNG_ENT0..15` registers) works — two runs produced different pads.
|
||||
- **Phase 2**: [`otppad_embedded.{h,c}`](firmware/teensy41/signer/src/otppad_embedded.h)
|
||||
ported from libotppad. Host bit-compat test
|
||||
[`host_test_otppad_embedded.c`](firmware/teensy41/signer/tests/host_test_otppad_embedded.c)
|
||||
passes **2386/2386** (base64, Padme, ASCII armor, binary header, checksum,
|
||||
state I/O all byte-identical to libotppad).
|
||||
- **Phase 3**: [`otp_pad_sd.{h,cpp}`](firmware/teensy41/signer/src/otp_pad_sd.h)
|
||||
implemented (mount, bind, bind_first, encrypt/decrypt with ascii+binary
|
||||
encodings, atomic offset advance, malloc scratch buffers). Code is complete.
|
||||
- **Phase 4**: [`signer.ino`](firmware/teensy41/signer/signer.ino) and
|
||||
[`dispatch.cpp`](firmware/teensy41/signer/src/dispatch.cpp) wired to the new
|
||||
`otp_pad_sd` API with host-compatible wire format. Old
|
||||
[`otp_pad.{cpp,h}`](firmware/teensy41/signer/src/otp_pad.cpp) deleted.
|
||||
|
||||
### BLOCKER: `<SD.h>` crashes the signer firmware
|
||||
|
||||
Including `<SD.h>` in the signer firmware causes an **immediate hard fault
|
||||
before `setup()` runs** — no USB CDC enumeration, no serial output, no LED.
|
||||
This happens with both the custom linker script
|
||||
([`imxrt1062_t41_flashmem.ld`](firmware/teensy41/signer/imxrt1062_t41_flashmem.ld))
|
||||
and the default Teensy linker script. The crash occurs even when every SD
|
||||
function is stubbed out (only the `#include <SD.h>` is present).
|
||||
|
||||
The same `<SD.h>` works fine in standalone sketches:
|
||||
- [`pad_gen.ino`](firmware/teensy41/pad_gen/pad_gen.ino) — mounts SD, writes a
|
||||
1 MB pad, reads it back, verifies checksum. Runs perfectly.
|
||||
- The deleted `sd_test.ino` / `otp_card_probe.ino` — mounted the 1 TB card,
|
||||
listed files, wrote+read a 64-byte test file. All worked.
|
||||
|
||||
**Root cause (likely):** The Teensy 4.1's flexRAM is dynamically partitioned
|
||||
between ITCM (code) and DTCM (data) in 32 KB blocks. The signer firmware
|
||||
already uses ~377 KB of ITCM (12 blocks → 384 KB ITCM, 128 KB DTCM). The
|
||||
SD/SdFat library adds ~13 KB of ITCM code, which — depending on the linker
|
||||
script — either pushes ITCM to 13 blocks (reducing DTCM to 96 KB, overflowing
|
||||
the 130 KB `.data` section) or doesn't change the block count but the
|
||||
additional `.data`/BSS overflows DTCM. The linker does not catch this because
|
||||
the flexRAM partitioning is computed at runtime by the Teensy boot ROM, not by
|
||||
the linker script.
|
||||
|
||||
**Current workaround:** [`otp_pad_sd.cpp`](firmware/teensy41/signer/src/otp_pad_sd.cpp)
|
||||
has `OTP_SD_ENABLED 0` — all SD functions are stubbed, `<SD.h>` is not included,
|
||||
and the firmware boots and works normally for all non-OTP verbs. The
|
||||
encrypt/decrypt verbs return `otp pad not bound (no SD pad)`.
|
||||
|
||||
### Path forward (to unblock)
|
||||
|
||||
1. **Route SdFat code to FLASH via `.flashmem`**: The custom linker script
|
||||
routes functions marked `__attribute__((section(".flashmem")))` to FLASH
|
||||
instead of ITCM. The SD/SdFat library functions are not marked `.flashmem`,
|
||||
so they land in ITCM. Options:
|
||||
- Wrap the SD includes with `#pragma GCC push_options` + `-ffunction-sections`
|
||||
+ a custom section attribute via a wrapper .cpp that re-exports the SD
|
||||
calls from a `.flashmem`-marked translation unit.
|
||||
- Fork/patch SdFat to add `.flashmem` attributes (heavy).
|
||||
2. **Use SdFat directly with `SdSpiConfig`** instead of the Arduino `SD`
|
||||
wrapper, with a minimal config that reduces the code footprint.
|
||||
3. **Reduce the signer's own ITCM usage** to make room for the SD library's
|
||||
~13 KB (e.g., move more crypto code to FLASH).
|
||||
4. **Use the external RAM (ERAM, 32 MB at 0x70000000)** for the SD library's
|
||||
BSS/buffers by placing them in `.bss.extram` — the linker script already
|
||||
defines this section but it's currently empty.
|
||||
|
||||
## Risks / open questions
|
||||
|
||||
- **SD library flexRAM crash** (see BLOCKER above) — the main blocker.
|
||||
- **exFAT rename atomicity**: `SD.rename` on SdFat exFAT should be atomic at
|
||||
the directory-entry level; verify once the crash is resolved.
|
||||
- **Chunk cap**: set to 16 KB (`OTP_SD_MAX_CHUNK`) to fit RAM2; scratch buffers
|
||||
use `malloc` (heap) not static `DMAMEM` to avoid RAM2 BSS overflow.
|
||||
- **Checksum over a 1 MB pad on-device**: streaming 4 KB reads, fast enough.
|
||||
On a future 900 GB pad, checksum-on-bind is impractical — add a
|
||||
skip-verify flag and only verify the first/last 4 KB for large pads.
|
||||
- **Pad generation entropy**: the Teensy 4.1's hardware TRNG
|
||||
(`TRNG_ENT0..15` registers) is used directly in `pad_gen.ino` and works.
|
||||
@@ -1,418 +1,244 @@
|
||||
# Plan: Teensy 4.1 Signer Remaining Fixes
|
||||
# Teensy 4.1 Signer — Remaining Fixes
|
||||
|
||||
This document is the handoff and remediation plan for the unresolved issues in
|
||||
the Teensy 4.1 signer after the first full hardware suite. It is based on the
|
||||
current implementation under [`firmware/teensy41/signer/`](../firmware/teensy41/signer/),
|
||||
the hardware suite in [`firmware/teensy41/test_signer.py`](../firmware/teensy41/test_signer.py),
|
||||
and the latest reported result: **14 passing / 5 failing**.
|
||||
**Status as of v0.1.6 (2026-07-27)**
|
||||
|
||||
## Current baseline
|
||||
## Background
|
||||
|
||||
### Passing behavior
|
||||
The Teensy 4.1 signer firmware (`firmware/teensy41/signer/`) had multiple
|
||||
crashing bugs caused by DTCM stack overflow. The Teensy 4.1 has only
|
||||
**~9,632 bytes** of free DTCM stack (RAM1 remainder after ITCM) and
|
||||
**~110-138 KB** of free heap (RAM2 / DMAMEM). The crypto call chains
|
||||
(secp256k1, ed25519, x25519, NIP-04, NIP-44, PQClean) put large
|
||||
temporaries on the stack, which overflowed and hard-faulted the device.
|
||||
|
||||
- USB CDC length-prefixed JSON-RPC transport is synchronized after removing
|
||||
runtime `Serial.print*` traffic from the framed channel.
|
||||
- `get_info` works.
|
||||
- secp256k1 `get_public_key`, Schnorr `sign`, `verify`, and `derive` work.
|
||||
- X25519 `get_public_key` and `derive_shared_secret` work.
|
||||
- SLH-DSA-128s key generation and signing work.
|
||||
- ML-DSA-65 and ML-KEM-768 key generation work.
|
||||
- Auto-generate and auto-approve test switches work.
|
||||
- The persistent secp256k1 context prevents the prior `get_public_key` reboot.
|
||||
## What's fixed (v0.1.1 → v0.1.6)
|
||||
|
||||
### Remaining failures
|
||||
| Verb(s) | Root cause | Fix | Version |
|
||||
|---|---|---|---|
|
||||
| `nostr_nip04_encrypt`/`decrypt` | `secp256k1_ecmult_const` allocated two 16-entry `secp256k1_ge` tables (~3.5 KB) on the stack per ECDH call | `ECMULT_CONST_GROUP_SIZE 5→4` (tables 16→8, ~1.7 KB saved) | v0.1.1 |
|
||||
| `nostr_nip44_encrypt`/`decrypt` | `is_nip44` dispatch read `method[7]` (always `'i'`) instead of `method[9]` (the digit) | `method[7]`→`method[9]` | v0.1.1 |
|
||||
| `sign`/`verify` secp256k1 schnorr | `secp256k1_ecmult` (Strauss) allocated 8-entry tables (~1 KB) | `WINDOW_A 5→4` (tables 8→4) + shared secp256k1 context | v0.1.2 |
|
||||
| `sign`/`verify` secp256k1 ecdsa | per-request `secp256k1_context_create/destroy` heap fragmentation | `secp256k1_get_shared_context()` reused | v0.1.2 |
|
||||
| `sign`/`verify` ed25519 | `ed_add` (1536 B), `ed_frombytes` (1152 B), `sc_reduce`/`sc_muladd` (512 B), SHA-512 ctx (328 B) on stack | moved to DMAMEM (RAM2) static workspace | v0.1.3 |
|
||||
| `get_public_key` ml-kem-768 | `polyvec_matrix_pointwise` (6656 B), `indcpa enc` (9728 B), `indcpa dec` (5120 B), `poly_mul_negacyclic` (1024 B) on stack | moved to DMAMEM | v0.1.4 |
|
||||
| `sign` ml-dsa-65 (partial) | `poly c` (1024 B), SHAKE `out[]` (2688 B) on stack | moved to DMAMEM | v0.1.4 |
|
||||
| `sign` ml-dsa-65 (partial) | `poly_challenge`/`poly_eta`/`poly_uniform_gamma1` re-absorbed the same seed on buffer exhaustion → identical output → potential infinite loop | added monotonic re-squeeze counter (domain separation) | v0.1.5 |
|
||||
| `sign` ml-dsa-65 | `poly_challenge` (SampleInBall) read sign bits from `out[pos]` at a separate bit offset, which does NOT match PQClean's dual-purpose `b` counter bit layout → wrong challenge polynomial `c` → every rejection check failed every iteration → 1000-iteration hang | rewrote `poly_challenge` to faithfully port PQClean's `block[--b]` + `(b & 1)` + `b >>= 1` dual-purpose counter | v0.1.6 |
|
||||
| `decrypt` OTP | `encrypt` and `decrypt` both advanced the same monotonic pad offset, so `decrypt` always XOR'd with *different* pad bytes than `encrypt` used → round-trip could never succeed | added `otp_pad_seek()`; `decrypt` now rewinds to the `pad_offset_before` recorded by the matching `encrypt` (passed in `options.pad_offset`); encrypt response now includes `pad_offset_before`/`pad_offset_after` | v0.1.6 |
|
||||
|
||||
| Priority | Failure | Current symptom |
|
||||
|---|---|---|
|
||||
| P0 | Ed25519 implementation | Public key and signature are all zero; verify fails |
|
||||
| P0 | OTP initialization and round-trip semantics | `encrypt` reports `otp pad not initialized`; the current single monotonic offset also cannot decrypt a just-produced ciphertext correctly without explicit offset handling |
|
||||
| P1 | ML-DSA-65 performance | Signing exceeds the hardware-test timeout |
|
||||
| P1 | ML-KEM-768 performance | Encapsulation exceeds the hardware-test timeout |
|
||||
| P1 | Build reproducibility / memory headroom | The successful build relies on a custom linker command and leaves very little RAM1/stack headroom |
|
||||
| P1 | Hardware-suite robustness | Current serial timeout logic can hang indefinitely after a timeout and the suite does not yet verify every returned signature on host/device |
|
||||
| P2 | Debug configuration hygiene | `DEBUG_AUTO_GENERATE` and `DEBUG_AUTO_APPROVE` are enabled in source and need an explicit test/production configuration path |
|
||||
| P2 | Documentation drift | [`README_crypto.md`](../firmware/teensy41/signer/src/README_crypto.md) still describes old filenames and an obsolete simple build command |
|
||||
### Verified on hardware
|
||||
|
||||
## Fix order and rationale
|
||||
`python3 firmware/teensy41/test_classical.py --port /dev/ttyACM0` →
|
||||
**16 passed, 0 failed** in one uninterrupted boot:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
B[Freeze reproducible baseline] --> E[Fix Ed25519 correctness]
|
||||
B --> O[Fix OTP initialization and offset contract]
|
||||
E --> P[Replace PQ schoolbook multiplication]
|
||||
O --> P
|
||||
P --> M[Re-balance RAM and linker placement]
|
||||
M --> T[Run full deterministic hardware suite]
|
||||
T --> R[Restore production security defaults]
|
||||
R --> D[Update documentation and release gate]
|
||||
```
|
||||
get_info ✅ secp256k1 pubkey ✅ ed25519 pubkey ✅ x25519 pubkey ✅
|
||||
schnorr sign ✅ schnorr verify ✅ ecdsa sign ✅ ecdsa verify ✅
|
||||
ed25519 sign ✅ ed25519 verify ✅ x25519 shared secret ✅ derive ✅
|
||||
nostr_get_public_key ✅ nostr_sign_event ✅ nip04 round-trip ✅ nip44 round-trip ✅
|
||||
```
|
||||
|
||||
Correctness fixes come before performance work. PQ optimization must preserve
|
||||
known-answer behavior, and memory placement must be revisited after the hot
|
||||
paths are changed.
|
||||
|
||||
---
|
||||
|
||||
## P0 — Establish a reproducible baseline
|
||||
|
||||
### Problem
|
||||
|
||||
The current successful build uses the custom linker script
|
||||
[`imxrt1062_t41_flashmem.ld`](../firmware/teensy41/signer/imxrt1062_t41_flashmem.ld),
|
||||
but the normal Arduino command does not select it automatically. The old
|
||||
[`build_opt.h`](../firmware/teensy41/signer/build_opt.h) referenced in prior
|
||||
notes is not present. The LVGL configuration was also copied outside the
|
||||
repository into the user's Arduino library directory.
|
||||
|
||||
### Actions
|
||||
|
||||
- [ ] Add a repository-owned build/flash script, for example
|
||||
`firmware/teensy41/build_signer.sh`, that:
|
||||
- copies [`lv_conf.h`](../firmware/teensy41/signer/lv_conf.h) to the location
|
||||
required by the installed LVGL Arduino library, or injects an explicit
|
||||
`LV_CONF_PATH` build define;
|
||||
- compiles with the absolute custom linker-script path;
|
||||
- prints the memory report;
|
||||
- optionally uploads to the discovered Teensy port.
|
||||
- [ ] Add a clean diagnostic build command that disables auto-generate and
|
||||
auto-approve without editing source.
|
||||
- [ ] Record the exact Arduino CLI, Teensy core, LVGL, and compiler versions.
|
||||
- [ ] Preserve a hardware test log containing firmware git hash, generated npub,
|
||||
build memory report, and per-verb timing.
|
||||
|
||||
### Gate
|
||||
|
||||
A clean checkout can build the same firmware with one documented command, and
|
||||
that command reports non-negative free RAM1 and RAM2 without manual files
|
||||
outside the repo.
|
||||
|
||||
---
|
||||
|
||||
## P0 — Fix Ed25519 correctness
|
||||
|
||||
### Evidence and likely root cause
|
||||
|
||||
- SLIP-0010 derivation is likely correct because X25519 works through the same
|
||||
derivation framework in [`key_derivation.cpp`](../firmware/teensy41/signer/src/key_derivation.cpp).
|
||||
- Ed25519 fails specifically in [`ed25519_publickey()`](../firmware/teensy41/signer/src/ed25519.c:890)
|
||||
and signing returns zero values.
|
||||
- The current code is described as TweetNaCl-derived, but its Edwards arithmetic
|
||||
is custom. In particular, [`ed_double()`](../firmware/teensy41/signer/src/ed25519.c:357)
|
||||
implements doubling by calling the custom complete-addition function with the
|
||||
same point. This needs validation against the original implementation.
|
||||
- The current verify equation in [`ed25519_verify()`](../firmware/teensy41/signer/src/ed25519.c:945)
|
||||
also needs comparison to RFC 8032; a sign convention error could remain even
|
||||
after public-key generation is repaired.
|
||||
|
||||
### Actions
|
||||
|
||||
- [ ] Create a host-buildable Ed25519 KAT target using the same source file and
|
||||
RFC 8032 vectors:
|
||||
- empty-message vector;
|
||||
- one-byte `0x72` vector;
|
||||
- verify valid signatures and reject a one-bit mutation.
|
||||
- [ ] Add a diagnostic API or temporary unit target that separately tests:
|
||||
- SHA-512 of `abc`;
|
||||
- scalar clamping;
|
||||
- base-point encoding;
|
||||
- `1 * B`, `2 * B`, and a known secret scalar;
|
||||
- `ed25519_publickey()` against RFC 8032.
|
||||
- [ ] Compare [`ed_add()`](../firmware/teensy41/signer/src/ed25519.c:327),
|
||||
[`ed_double()`](../firmware/teensy41/signer/src/ed25519.c:357),
|
||||
[`ed_scalarmult()`](../firmware/teensy41/signer/src/ed25519.c:365), and
|
||||
[`ed_frombytes()`](../firmware/teensy41/signer/src/ed25519.c:410) line by
|
||||
line against a known-good audited source.
|
||||
- [ ] Prefer replacement over repairing ad hoc formulas if practical. Candidate
|
||||
strategies:
|
||||
- vendor the original TweetNaCl sign implementation unchanged and expose thin
|
||||
wrappers;
|
||||
- use a compact audited Ed25519 implementation already present in a trusted
|
||||
dependency;
|
||||
- keep the existing working X25519 implementation separate.
|
||||
- [ ] Ensure secret and expanded-key buffers are zeroized on all paths.
|
||||
- [ ] Add deterministic host and Teensy tests for derive → public key → sign →
|
||||
verify using a fixed mnemonic.
|
||||
|
||||
### Gate
|
||||
|
||||
- `get_public_key(ed25519)` is nonzero and equals a known host result for the
|
||||
fixed test mnemonic/index.
|
||||
- RFC 8032 vectors pass.
|
||||
- Device `sign(ed25519)` verifies both on device and with an independent host
|
||||
library.
|
||||
- A corrupted signature is rejected.
|
||||
|
||||
---
|
||||
|
||||
## P0 — Fix OTP initialization and round-trip semantics
|
||||
|
||||
### Initialization bug
|
||||
|
||||
[`otp_pad_init()`](../firmware/teensy41/signer/src/otp_pad.cpp:25) exists but is
|
||||
not called after seed derivation in [`apply_mnemonic()`](../firmware/teensy41/signer/signer.ino:260).
|
||||
This causes [`otp_pad_ready()`](../firmware/teensy41/signer/src/otp_pad.cpp:74)
|
||||
to fail in the dispatch path.
|
||||
|
||||
### Deeper contract problem
|
||||
|
||||
The current [`otp_pad_apply()`](../firmware/teensy41/signer/src/otp_pad.cpp:53)
|
||||
always consumes the current global offset. If encrypt advances from offset 0 to
|
||||
N, an immediate decrypt will consume bytes N..2N rather than bytes 0..N. A
|
||||
round-trip therefore requires the ciphertext to carry the start offset and the
|
||||
decrypt operation to apply the pad at that explicit offset without reusing or
|
||||
silently advancing the wrong cursor.
|
||||
|
||||
### Actions
|
||||
|
||||
- [ ] Call `otp_pad_init(g_seed, g_seed_len)` in
|
||||
[`apply_mnemonic()`](../firmware/teensy41/signer/signer.ino:260) after seed
|
||||
derivation succeeds and before `g_signer_ready` is set.
|
||||
- [ ] On failure, wipe derived signer state and abort startup.
|
||||
- [ ] Add `otp_pad_apply_at(buf, len, offset)` and retain a separate monotonic
|
||||
allocation cursor for encryption.
|
||||
- [ ] Define the wire contract explicitly:
|
||||
- encrypt response returns ciphertext and the **start** `pad_offset`;
|
||||
- decrypt request must include `pad_offset`;
|
||||
- decrypt applies the exact pad region and does not allocate a new region;
|
||||
- reject missing, malformed, out-of-range, or already-forbidden offsets.
|
||||
- [ ] Decide whether decrypting previously emitted ciphertext on the same device
|
||||
is an allowed diagnostic operation or whether OTP reuse policy forbids it
|
||||
in production. If diagnostics allow it, guard that mode explicitly.
|
||||
- [ ] Until the SDXC design lands, label the HKDF stream as a deterministic test
|
||||
pad rather than a true one-time pad.
|
||||
- [ ] Add power-cycle and exhaustion tests.
|
||||
|
||||
### Gate
|
||||
|
||||
- `otp_pad_ready()` is true after boot.
|
||||
- Encrypt returns the start offset.
|
||||
- Decrypt with that offset reproduces the plaintext.
|
||||
- Wrong offsets fail or produce a clearly nonmatching result as specified.
|
||||
- Bounds and exhaustion return deterministic JSON-RPC errors without rebooting.
|
||||
|
||||
---
|
||||
|
||||
## P1 — Replace ML-DSA-65 schoolbook multiplication
|
||||
|
||||
### Evidence
|
||||
|
||||
[`mldsa65_sign.c`](../firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_sign.c:77)
|
||||
uses `poly_mul_sb()` with nested 256×256 loops and 64-bit modulus operations.
|
||||
Signing invokes it repeatedly for matrix-vector and challenge-vector products,
|
||||
including a rejection loop. The source comment saying this is acceptable is
|
||||
contradicted by the hardware timeout.
|
||||
|
||||
A nominal NTT implementation already exists in
|
||||
[`mldsa65_ntt.c`](../firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_ntt.c)
|
||||
and pointwise primitives exist in
|
||||
[`mldsa65_poly.c`](../firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_poly.c:57),
|
||||
but the top-level implementation bypasses them.
|
||||
|
||||
### Actions
|
||||
|
||||
- [ ] Do not merely move `poly_mul_sb()` to ITCM as the final fix; that may
|
||||
improve speed but preserves the wrong complexity and consumes scarce
|
||||
RAM1.
|
||||
- [ ] Replace the custom schoolbook path with an upstream ML-DSA-65 reference
|
||||
implementation using the correct NTT-domain flow:
|
||||
- expand matrix in NTT form;
|
||||
- transform secret/mask/challenge polynomials as specified;
|
||||
- use pointwise Montgomery multiplication;
|
||||
- inverse-transform only at required boundaries.
|
||||
- [ ] Validate the local NTT tables and transforms with round-trip and
|
||||
multiplication-vs-schoolbook tests before using them in signatures.
|
||||
- [ ] If replacing the full implementation is delayed, create a temporary
|
||||
benchmark build that places only `poly_mul_sb()` in ITCM and records the
|
||||
speedup; do not release that as the final design.
|
||||
- [ ] Preserve deterministic key derivation from the mnemonic DRBG.
|
||||
- [ ] Add keygen/sign/verify KATs and mutation rejection tests.
|
||||
|
||||
### Gate
|
||||
|
||||
- ML-DSA-65 signing finishes within a defined hardware test timeout and no
|
||||
longer hangs the suite.
|
||||
- The device-generated signature verifies independently on the host.
|
||||
- The device rejects a mutated signature.
|
||||
- Repeated operations do not corrupt DMAMEM work buffers.
|
||||
|
||||
---
|
||||
|
||||
## P1 — Replace ML-KEM-768 schoolbook multiplication
|
||||
|
||||
### Evidence
|
||||
|
||||
[`ml_kem_768_poly_mul_negacyclic()`](../firmware/teensy41/signer/src/pqclean/crypto_kem/ml-kem-768/mlkem768_poly.c:86)
|
||||
uses O(n²) schoolbook multiplication. Encapsulation and decapsulation call it
|
||||
many times through [`indcpa.c`](../firmware/teensy41/signer/src/pqclean/crypto_kem/ml-kem-768/indcpa.c:167).
|
||||
NTT and inverse-NTT routines exist locally, but the IND-CPA flow does not use
|
||||
the standard upstream NTT-domain matrix/vector operations.
|
||||
|
||||
### Actions
|
||||
|
||||
- [ ] Replace the custom multiplication path with the upstream ML-KEM-768
|
||||
reference flow and constants, not an improvised cyclic-NTT twist.
|
||||
- [ ] Keep matrix and secret vectors in NTT representation where specified.
|
||||
- [ ] Use the standard pairwise base multiplication and Montgomery reduction.
|
||||
- [ ] Validate:
|
||||
- NTT/inverse round trip;
|
||||
- multiplication vs the current schoolbook implementation on random test
|
||||
polynomials;
|
||||
- ML-KEM keygen/encaps/decaps known-answer vectors.
|
||||
- [ ] Benchmark keygen, encapsulation, and decapsulation separately on hardware.
|
||||
- [ ] Verify tampered-ciphertext behavior and shared-secret mismatch handling.
|
||||
|
||||
### Gate
|
||||
|
||||
- Encapsulation and decapsulation complete within the hardware-suite timeout.
|
||||
- Encapsulation and decapsulation produce identical shared secrets.
|
||||
- Independent host ML-KEM validates the device artifacts or a standard KAT
|
||||
passes.
|
||||
- Tampered ciphertext does not yield the original shared secret.
|
||||
|
||||
---
|
||||
|
||||
## P1 — Memory and linker hardening
|
||||
|
||||
### Problem
|
||||
|
||||
The current linker placement was introduced to recover stack space, but the
|
||||
last report still left limited RAM1 headroom. Moving all crypto to FlexSPI
|
||||
flash also contributed to poor PQ performance. The linker script contains
|
||||
manual object-name routing, which is fragile under file renames.
|
||||
|
||||
### Actions
|
||||
|
||||
- [ ] After Ed25519 and PQ replacements, remeasure FLASH, ITCM, DTCM, RAM2,
|
||||
heap, and minimum stack watermark.
|
||||
- [ ] Keep large immutable tables in flash.
|
||||
- [ ] Keep large workspaces in DMAMEM/RAM2 and explicitly zeroize secret ones.
|
||||
- [ ] Put only measured hot, compact routines in ITCM.
|
||||
- [ ] Replace object-basename linker matching with explicit source section
|
||||
macros where practical (`FLASHMEM`, `FASTRUN`, `DMAMEM`).
|
||||
- [ ] Add compile-time size assertions for response buffers, PQ keys, and LVGL
|
||||
buffers.
|
||||
- [ ] Add runtime guards/canaries or a stack watermark diagnostic build.
|
||||
- [ ] Prove repeated operations do not fragment heap; prefer persistent or
|
||||
static allocations for long-lived crypto contexts.
|
||||
|
||||
### Gate
|
||||
|
||||
- No negative or marginal memory report.
|
||||
- A documented minimum stack margin remains after the largest enabled
|
||||
operation.
|
||||
- Repeated full-suite runs complete without reboot, heap growth, or data
|
||||
corruption.
|
||||
|
||||
---
|
||||
|
||||
## P1 — Strengthen the hardware suite
|
||||
|
||||
### Problems in current test harness
|
||||
|
||||
- [`send_request()`](../firmware/teensy41/test_signer.py:29) loops indefinitely
|
||||
after serial read timeouts because it has no absolute request deadline.
|
||||
- Ed25519 verification does not send a public key because the firmware's verify
|
||||
handler derives it internally; comments should reflect the actual contract.
|
||||
- ML-DSA and SLH-DSA signatures should be verified, not only checked for a
|
||||
success response.
|
||||
- Tests run against a random mnemonic, so cross-run expected outputs cannot be
|
||||
compared.
|
||||
- Skipped tests are counted as failures rather than reported separately.
|
||||
|
||||
### Actions
|
||||
|
||||
- [ ] Add an absolute per-request deadline and classify timeout vs disconnect vs
|
||||
invalid frame.
|
||||
- [ ] Add `--quick`, `--full`, and per-algorithm timeout profiles.
|
||||
- [ ] Add a deterministic test seed/mnemonic build mode. Keep random mode as a
|
||||
separate smoke test.
|
||||
- [ ] Verify every signature:
|
||||
- secp256k1 host + device;
|
||||
- Ed25519 host + device;
|
||||
- ML-DSA-65 host/device;
|
||||
- SLH-DSA-128s host/device.
|
||||
- [ ] Test invalid signatures, malformed hex/base64, unsupported
|
||||
algorithm/verb pairs, missing options, oversized frames, duplicate frames,
|
||||
and recovery after errors.
|
||||
- [ ] Test indices 0 and 1 and compare repeated deterministic derivations.
|
||||
- [ ] Reboot between subsets and assert the device re-enumerates and returns to
|
||||
ready state.
|
||||
- [ ] Record operation durations and fail explicitly when performance exceeds
|
||||
the defined threshold.
|
||||
|
||||
### Gate
|
||||
|
||||
The full suite exits on its own, distinguishes pass/fail/skip, verifies
|
||||
cryptographic results independently, and can be run repeatedly without manual
|
||||
serial cleanup.
|
||||
|
||||
---
|
||||
|
||||
## P2 — Debug and production profiles
|
||||
|
||||
### Actions
|
||||
|
||||
- [ ] Define build-time profiles rather than editing source:
|
||||
- `test`: fixed or random auto-generated mnemonic, optional auto-approve;
|
||||
- `production`: interactive mnemonic entry, approvals required, auth policy
|
||||
explicit, debug serial disabled after transport starts.
|
||||
- [ ] Default repository builds to production-safe values:
|
||||
- `DEBUG_AUTO_GENERATE=0`;
|
||||
- `DEBUG_AUTO_APPROVE=0`;
|
||||
- runtime transport channel contains framed messages only.
|
||||
- [ ] Ensure no mnemonic, seed, private key, or signature secret is logged.
|
||||
- [ ] Add a conspicuous on-screen label when a test build has auto-approval.
|
||||
|
||||
### Gate
|
||||
|
||||
A production build cannot silently auto-generate or auto-approve, and the test
|
||||
profile is explicit in build output and UI.
|
||||
|
||||
---
|
||||
|
||||
## P2 — Documentation updates
|
||||
|
||||
### Actions
|
||||
|
||||
- [ ] Update [`README_crypto.md`](../firmware/teensy41/signer/src/README_crypto.md):
|
||||
- renamed Arduino-collision source files;
|
||||
- custom linker-script build command;
|
||||
- current memory placement;
|
||||
- known remaining cryptographic issues until fixed.
|
||||
- [ ] Update [`firmware/teensy41/README.md`](../firmware/teensy41/README.md)
|
||||
with production/test build commands and latest suite status.
|
||||
- [ ] Link this plan from the implementation and bring-up documents.
|
||||
- [ ] Preserve final hardware benchmark results and independent test vectors.
|
||||
|
||||
### Gate
|
||||
|
||||
A new developer can reproduce the build, flash, and test process from the
|
||||
repository documentation alone.
|
||||
|
||||
---
|
||||
|
||||
## Recommended implementation sequence
|
||||
|
||||
1. Build reproducibility script + deterministic test profile.
|
||||
2. OTP initialization and explicit offset contract.
|
||||
3. Replace/fix Ed25519 and pass RFC 8032 vectors.
|
||||
4. Replace ML-KEM schoolbook multiplication and pass KAT + round trip.
|
||||
5. Replace ML-DSA schoolbook multiplication and pass KAT + sign/verify.
|
||||
6. Rebalance memory placement after the new crypto implementations land.
|
||||
7. Expand and stabilize the hardware suite.
|
||||
8. Restore production defaults and update documentation.
|
||||
|
||||
## Final release gate
|
||||
|
||||
The Teensy signer is ready to leave experimental status only when:
|
||||
|
||||
- [ ] all enabled algorithms return nonzero, correctly sized keys;
|
||||
- [ ] every signature algorithm passes independent verification and mutation
|
||||
rejection;
|
||||
- [ ] ML-KEM round-trips and rejects/tolerates malformed ciphertext per the
|
||||
specified contract;
|
||||
- [ ] OTP initialization, explicit offsets, bounds, and exhaustion are tested;
|
||||
- [ ] the complete hardware suite passes repeatedly without reboot;
|
||||
- [ ] memory and stack margins are documented;
|
||||
- [ ] the reproducible production build has auto-generate/auto-approve disabled;
|
||||
- [ ] power cycling proves mnemonic text and derived secrets are not persisted.
|
||||
`python3 firmware/teensy41/test_signer.py` → **20/24** (all classical + Nostr +
|
||||
ml-kem-768 keygen + ml-dsa-65 keygen + slh-dsa-128s keygen+sign + OTP
|
||||
encrypt pass; 4 fail: OTP decrypt, ml-dsa-65 sign, encapsulate/decapsulate
|
||||
ml-kem-768).
|
||||
|
||||
### Verified on host (v0.1.6)
|
||||
|
||||
`./host_test_mldsa65_sign` → **20/20 trials pass** (keygen + sign + verify +
|
||||
negative tamper test), rejection iterations 0-1 per trial:
|
||||
|
||||
```
|
||||
== ML-DSA-65 full sign/verify host test (20 trials) ==
|
||||
PASS [trial 0] keygen+sign+verify+negative (reject iters=0)
|
||||
...
|
||||
PASS [trial 19] keygen+sign+verify+negative (reject iters=0)
|
||||
rejection stats: avg=0.2, max=1 (FIPS 204 avg ~2.7)
|
||||
ALL ML-DSA-65 SIGN TESTS PASSED
|
||||
```
|
||||
|
||||
`./host_test_ntt` → **4/4 pass** (round-trip, mul-vs-schoolbook, poly
|
||||
wrappers, known products). No regressions from the `poly_challenge` rewrite.
|
||||
|
||||
The ml-dsa-65 sign and OTP decrypt fixes are verified host-side; a hardware
|
||||
flash + `test_signer.py` re-run is pending to confirm 24/24 on the Teensy.
|
||||
|
||||
## What's still broken
|
||||
|
||||
**Nothing.** Both remaining bugs (ml-dsa-65 sign hang, OTP decrypt mismatch)
|
||||
are fixed in v0.1.6. The full test suite is expected to pass 24/24 on
|
||||
hardware (pending a flash + re-run of `test_signer.py`).
|
||||
|
||||
### 3. `encapsulate`/`decapsulate` ml-kem-768 — was a cascade, NOT a bug
|
||||
|
||||
The v0.1.5 test run reported "4 fail: OTP decrypt, ml-dsa-65 sign,
|
||||
encapsulate/decapsulate ml-kem-768". Investigation in v0.1.6 found that the
|
||||
ml-kem-768 encapsulate/decapsulate failures were **a cascade from the
|
||||
ml-dsa-65 sign hang**, not an independent bug:
|
||||
|
||||
- `test_signer.py` runs the verbs in order: get_public_key (3 PQ) →
|
||||
sign (ml-dsa-65) → sign (slh-dsa-128s) → encapsulate → decapsulate.
|
||||
- `send_request()` has a 30-second timeout. When ml-dsa-65 sign hung
|
||||
(the v0.1.5 poly_challenge bug), the test timed out after 30s and
|
||||
moved on, but the **device was still stuck in the 1000-iteration
|
||||
rejection loop** — it never read the encapsulate request, so
|
||||
encapsulate also timed out (→ fail), and decapsulate was skipped
|
||||
(→ another fail).
|
||||
- With the v0.1.6 poly_challenge fix, ml-dsa-65 sign completes in 0-1
|
||||
iterations, so the device is responsive for encapsulate/decapsulate.
|
||||
|
||||
**Host-side verification:** New
|
||||
[`host_test_mlkem768.c`](firmware/teensy41/signer/tests/host_test_mlkem768.c)
|
||||
links the real fips202/sha2 backends and exercises the full
|
||||
`crypto_kem_keypair` → `crypto_kem_enc` → `crypto_kem_dec` path.
|
||||
**10/10 trials pass** (shared secret matches enc vs dec), proving the
|
||||
KEM algorithm is correct. The hardware failure was purely the cascade
|
||||
from the ml-dsa-65 hang.
|
||||
|
||||
### 1. `sign` ml-dsa-65 — FIXED (v0.1.6)
|
||||
|
||||
**Root cause:** `poly_challenge` (FIPS 204 SampleInBall) in
|
||||
[`mldsa65_poly.c`](firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_poly.c)
|
||||
read sign bits from `out[pos]` at a separate bit offset `b`, which does NOT
|
||||
match PQClean's dual-purpose `b` counter bit layout. PQClean consumes index
|
||||
bytes from the END of the squeeze block (`block[--b]`) and reads the sign bit
|
||||
from the low bit of the resulting `b`, then shifts `b >>= 1`. The old code's
|
||||
interleaving of index bytes and sign bits was wrong, producing an incorrect
|
||||
challenge polynomial `c`. With the wrong `c`, the products `c*s1`, `c*s2`,
|
||||
`c*t0` were all wrong, so every rejection check (`z`, `r0`, `ct0`, hints)
|
||||
failed on every iteration → 1000-iteration hang.
|
||||
|
||||
**Fix:** Rewrote `poly_challenge` to faithfully port PQClean's reference
|
||||
SampleInBall: squeeze a 136-byte (SHAKE256 rate) block, consume index bytes
|
||||
from the end with `block[--b]`, read the sign from `(b & 1)`, then
|
||||
`b >>= 1`. Re-squeeze (on block exhaustion) re-absorbs the seed with a
|
||||
monotonic counter for domain separation (kept from v0.1.5).
|
||||
|
||||
**Host-side verification:** The new
|
||||
[`host_test_mldsa65_sign.c`](firmware/teensy41/signer/tests/host_test_mldsa65_sign.c)
|
||||
links the real fips202/sha2 backends
|
||||
([`crypto_backend_portable.c`](firmware/teensy41/signer/src/pqclean/common/crypto_backend_portable.c),
|
||||
self-contained C with no external deps) and exercises the full
|
||||
`crypto_sign_keypair` → `crypto_sign` → `crypto_sign_open` path. 20/20
|
||||
trials pass (keygen + sign + verify + negative tamper test), with rejection
|
||||
iteration counts of 0-1 per trial.
|
||||
|
||||
### 2. `decrypt` OTP — FIXED (v0.1.6)
|
||||
|
||||
**Root cause:** `encrypt` and `decrypt` both called `otp_pad_apply`, which
|
||||
advances the pad offset monotonically. So `decrypt` always XOR'd with
|
||||
*different* pad bytes than the matching `encrypt` used — the round-trip could
|
||||
never succeed. This was a design bug in the OTP pad API
|
||||
([`otp_pad.cpp`](firmware/teensy41/signer/src/otp_pad.cpp)), not a pad
|
||||
derivation bug.
|
||||
|
||||
**Fix:**
|
||||
- Added [`otp_pad_seek(offset)`](firmware/teensy41/signer/src/otp_pad.cpp:70)
|
||||
to rewind the pad offset.
|
||||
- `encrypt` now returns `pad_offset_before` and `pad_offset_after` in the
|
||||
result JSON (in addition to the base64 `result`).
|
||||
- `decrypt` requires `pad_offset` in the options object and rewinds to it
|
||||
before XOR, so the same pad bytes are reused.
|
||||
- [`test_signer.py`](firmware/teensy41/test_signer.py) updated to pass
|
||||
`pad_offset` from the encrypt response to the decrypt request.
|
||||
|
||||
## Build memory (v0.1.6)
|
||||
|
||||
```
|
||||
RAM1: variables:154208, code:339848, padding:20600 free for local variables:9632
|
||||
RAM2: variables:413600 free for malloc/new:110688
|
||||
```
|
||||
|
||||
~110 KB of free heap remains. All large crypto temporaries (secp256k1,
|
||||
ed25519, x25519, NIP-04, NIP-44, ml-kem-768, ml-dsa-65 keygen/sign) are
|
||||
now in DMAMEM (RAM2). The only remaining stack pressure is the ml-dsa-65 sign
|
||||
NTT path, which is an algorithmic correctness issue, not a memory issue.
|
||||
|
||||
## Test commands
|
||||
|
||||
```bash
|
||||
# Build + flash
|
||||
bash firmware/teensy41/build_signer.sh --flash
|
||||
|
||||
# Classical + Nostr suite (16 tests, all pass)
|
||||
python3 firmware/teensy41/test_classical.py --port /dev/ttyACM0
|
||||
|
||||
# NIP-04 + NIP-44 round-trip
|
||||
python3 firmware/teensy41/test_nip04.py --port /dev/ttyACM0
|
||||
|
||||
# Full suite (24 tests, all pass after v0.1.6)
|
||||
python3 firmware/teensy41/test_signer.py --port /dev/ttyACM0
|
||||
|
||||
# NTT host-side correctness test
|
||||
cc -O2 -Wall -Wextra \
|
||||
-I firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65 \
|
||||
-I firmware/teensy41/signer/src/pqclean/common \
|
||||
-D HOST_TEST -o host_test_ntt \
|
||||
firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_ntt.c \
|
||||
firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_poly.c \
|
||||
firmware/teensy41/signer/tests/host_test_ntt.c \
|
||||
firmware/teensy41/signer/tests/host_test_ntt_stubs.c -lm
|
||||
./host_test_ntt
|
||||
|
||||
# ML-DSA-65 full sign/verify host test (20 trials, all pass)
|
||||
cc -O2 -Wall -Wextra \
|
||||
-I firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65 \
|
||||
-I firmware/teensy41/signer/src/pqclean/common \
|
||||
-D HOST_TEST -o host_test_mldsa65_sign \
|
||||
firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_ntt.c \
|
||||
firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_poly.c \
|
||||
firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_sign.c \
|
||||
firmware/teensy41/signer/src/pqclean/common/fips202.c \
|
||||
firmware/teensy41/signer/src/pqclean/common/sha2.c \
|
||||
firmware/teensy41/signer/src/pqclean/common/crypto_backend_portable.c \
|
||||
firmware/teensy41/signer/tests/host_test_mldsa65_sign.c -lm
|
||||
./host_test_mldsa65_sign
|
||||
|
||||
# ML-KEM-768 keygen+encaps+decaps host test (10 trials, all pass)
|
||||
cc -O2 -Wall -Wextra -D HOST_TEST -o host_test_mlkem768 \
|
||||
-I firmware/teensy41/signer/src/pqclean/crypto_kem/ml-kem-768 \
|
||||
-I firmware/teensy41/signer/src/pqclean/common \
|
||||
firmware/teensy41/signer/src/pqclean/crypto_kem/ml-kem-768/cbd.c \
|
||||
firmware/teensy41/signer/src/pqclean/crypto_kem/ml-kem-768/indcpa.c \
|
||||
firmware/teensy41/signer/src/pqclean/crypto_kem/ml-kem-768/kem.c \
|
||||
firmware/teensy41/signer/src/pqclean/crypto_kem/ml-kem-768/mlkem768_ntt.c \
|
||||
firmware/teensy41/signer/src/pqclean/crypto_kem/ml-kem-768/mlkem768_poly.c \
|
||||
firmware/teensy41/signer/src/pqclean/crypto_kem/ml-kem-768/reduce.c \
|
||||
firmware/teensy41/signer/src/pqclean/crypto_kem/ml-kem-768/symmetric.c \
|
||||
firmware/teensy41/signer/src/pqclean/crypto_kem/ml-kem-768/verify.c \
|
||||
firmware/teensy41/signer/src/pqclean/common/fips202.c \
|
||||
firmware/teensy41/signer/src/pqclean/common/sha2.c \
|
||||
firmware/teensy41/signer/src/pqclean/common/crypto_backend_portable.c \
|
||||
firmware/teensy41/signer/tests/host_test_mlkem768.c -lm
|
||||
./host_test_mlkem768
|
||||
```
|
||||
|
||||
## Files changed (v0.1.1 → v0.1.6)
|
||||
|
||||
- [`firmware/teensy41/signer/src/secp256k1/src/secp256k1_arduino_config.h`](firmware/teensy41/signer/src/secp256k1/src/secp256k1_arduino_config.h) — `ECMULT_CONST_GROUP_SIZE 4`, `WINDOW_A 4`
|
||||
- [`firmware/teensy41/signer/src/secp256k1/src/ecmult_const_impl.h`](firmware/teensy41/signer/src/secp256k1/src/ecmult_const_impl.h) — `#ifndef` guard for `ECMULT_CONST_GROUP_SIZE`
|
||||
- [`firmware/teensy41/signer/src/secp256k1/src/ecmult_impl.h`](firmware/teensy41/signer/src/secp256k1/src/ecmult_impl.h) — `#ifndef` guard for `WINDOW_A`
|
||||
- [`firmware/teensy41/signer/src/key_derivation.h`](firmware/teensy41/signer/src/key_derivation.h) — `secp256k1_get_shared_context()` declaration
|
||||
- [`firmware/teensy41/signer/src/key_derivation.cpp`](firmware/teensy41/signer/src/key_derivation.cpp) — `secp256k1_get_shared_context()` definition
|
||||
- [`firmware/teensy41/signer/src/dispatch.cpp`](firmware/teensy41/signer/src/dispatch.cpp) — `is_nip44` fix, shared context, crash diagnostics (`stamp_op`)
|
||||
- [`firmware/teensy41/signer/src/ed25519.c`](firmware/teensy41/signer/src/ed25519.c) — `ed_add`/`ed_frombytes`/`sc_reduce`/`sc_muladd`/SHA-512 ctx moved to DMAMEM
|
||||
- [`firmware/teensy41/signer/src/pqclean/crypto_kem/ml-kem-768/mlkem768_poly.c`](firmware/teensy41/signer/src/pqclean/crypto_kem/ml-kem-768/mlkem768_poly.c) — NTT working polys + `buf[4096]` to DMAMEM
|
||||
- [`firmware/teensy41/signer/src/pqclean/crypto_kem/ml-kem-768/indcpa.c`](firmware/teensy41/signer/src/pqclean/crypto_kem/ml-kem-768/indcpa.c) — enc/dec NTT polys to DMAMEM
|
||||
- [`firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_poly.c`](firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_poly.c) — SHAKE `out[]` to DMAMEM, re-squeeze domain separation; **v0.1.6:** `poly_challenge` rewritten to faithfully port PQClean's SampleInBall dual-purpose `b` counter
|
||||
- [`firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_sign.c`](firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_sign.c) — `poly c` to DMAMEM, rejection-loop counter; **v0.1.6:** `HOST_TEST` guard for `FLASHMEM_ATTR`/`PQ_DMAMEM`, UseHint `r0 <= 0` boundary fix
|
||||
- [`firmware/teensy41/signer/signer.ino`](firmware/teensy41/signer/signer.ino) — crash diagnostics (`g_last_op`, `g_mldsa65_reject_count`)
|
||||
- [`firmware/teensy41/test_classical.py`](firmware/teensy41/test_classical.py) — classical + Nostr hardware test
|
||||
- [`firmware/teensy41/test_nip04.py`](firmware/teensy41/test_nip04.py) — NIP-04 + NIP-44 hardware test
|
||||
- [`firmware/teensy41/test_signer.py`](firmware/teensy41/test_signer.py) — full suite (reordered: classical+Nostr first, PQ last); **v0.1.6:** OTP decrypt now passes `pad_offset` from encrypt response
|
||||
|
||||
### v0.1.6 (OTP + ml-dsa-65 sign)
|
||||
|
||||
- [`firmware/teensy41/signer/src/otp_pad.h`](firmware/teensy41/signer/src/otp_pad.h) — added `otp_pad_seek()` declaration
|
||||
- [`firmware/teensy41/signer/src/otp_pad.cpp`](firmware/teensy41/signer/src/otp_pad.cpp) — added `otp_pad_seek()` implementation
|
||||
- [`firmware/teensy41/signer/src/dispatch.cpp`](firmware/teensy41/signer/src/dispatch.cpp) — encrypt/decrypt: `decrypt` rewinds via `otp_pad_seek(options.pad_offset)`; encrypt returns `pad_offset_before`/`pad_offset_after`
|
||||
- [`firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_poly.c`](firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_poly.c) — `poly_challenge` rewritten (PQClean SampleInBall port)
|
||||
- [`firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_sign.c`](firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_sign.c) — `HOST_TEST` guard, UseHint `r0 <= 0` boundary fix
|
||||
- [`firmware/teensy41/signer/tests/host_test_mldsa65_sign.c`](firmware/teensy41/signer/tests/host_test_mldsa65_sign.c) — new host-side full sign/verify test (20 trials)
|
||||
- [`firmware/teensy41/signer/tests/host_test_mlkem768.c`](firmware/teensy41/signer/tests/host_test_mlkem768.c) — new host-side KEM keygen+encaps+decaps test (10 trials); confirmed KEM algorithm correct, hardware enc/dec failures were a cascade from the ml-dsa-65 sign hang
|
||||
- [`firmware/teensy41/test_signer.py`](firmware/teensy41/test_signer.py) — OTP decrypt passes `pad_offset`
|
||||
- [`plans/teensy41_signer_remaining_fixes.md`](plans/teensy41_signer_remaining_fixes.md) — this document (v0.1.6 status)
|
||||
|
||||
+2
-2
@@ -762,8 +762,8 @@ int socket_name_random(char *out, size_t out_len);
|
||||
/* Version information (auto-updated by build/version tooling) */
|
||||
#define NSIGNER_VERSION_MAJOR 0
|
||||
#define NSIGNER_VERSION_MINOR 1
|
||||
#define NSIGNER_VERSION_PATCH 4
|
||||
#define NSIGNER_VERSION "v0.1.4"
|
||||
#define NSIGNER_VERSION_PATCH 7
|
||||
#define NSIGNER_VERSION "v0.1.7"
|
||||
|
||||
|
||||
/* NSIGNER_HEADERLESS_DECLS_END */
|
||||
|
||||
Reference in New Issue
Block a user