diff --git a/firmware/teensy41/signer/signer.ino b/firmware/teensy41/signer/signer.ino index 46a3d88..ce07bd4 100644 --- a/firmware/teensy41/signer/signer.ino +++ b/firmware/teensy41/signer/signer.ino @@ -355,6 +355,43 @@ static int run_boot_flow() { // output here does NOT corrupt the framed transport stream. They print the // CrashReport from any prior hard fault (fault type, PC, LR, SP) plus the // last-action marker so we can diagnose the get_public_key crash. + +// Persistent operation marker (DMAMEM survives soft reboot). Stamped before +// each verb's crypto work so a post-crash reboot reports which op faulted. +DMAMEM volatile uint32_t g_last_op = 0; +DMAMEM volatile uint32_t g_last_op_seq = 0; +DMAMEM volatile uint32_t g_stack_min = 0xFFFFFFFFu; +DMAMEM volatile uint32_t g_heap_free_at_crash = 0; +#define OP_ID_GET_INFO 1 +#define OP_ID_GPK 2 +#define OP_ID_SIGN 3 +#define OP_ID_VERIFY 4 +#define OP_ID_DERIVE_SHARED 5 +#define OP_ID_DERIVE 6 +#define OP_ID_NOSTR_GPK 7 +#define OP_ID_NOSTR_SIGN 8 +#define OP_ID_NOSTR_MINE 9 +#define OP_ID_NIP04 10 +#define OP_ID_NIP44 11 +#define OP_ID_ENCAPS 12 +#define OP_ID_DECAPS 13 +#define OP_ID_OTP 14 + +extern char _estack; +extern char _ebss; + +static uint32_t stack_free_bytes(void) { + uint32_t sp; + asm volatile ("mov %0, sp\n" : "=r"(sp)); + uint32_t bot = (uint32_t)&_ebss; + return (sp > bot) ? (sp - bot) : 0; +} + +static uint32_t stack_high_water_free(void) { + uint32_t top = (uint32_t)&_estack; + return (g_stack_min <= top) ? (top - g_stack_min) : 0; +} + void setup() { Serial.begin(115200); while (!Serial && millis() < 3000) ; @@ -372,6 +409,19 @@ void setup() { CrashReport.clear(); } + // Report the last operation marker from before the crash. + if (g_last_op != 0) { + Serial.print("LAST OP BEFORE CRASH: id="); + Serial.print((int)g_last_op); + Serial.print(" seq="); + Serial.print((int)g_last_op_seq); + Serial.print(" stack_hw_free="); + Serial.print((int)stack_high_water_free()); + Serial.print(" heap_free_at_crash="); + Serial.print((int)g_heap_free_at_crash); + Serial.println(); + } + Serial.println("n_signer booting..."); tft.init(320, 480); tft.setRotation(1); diff --git a/firmware/teensy41/signer/src/dispatch.cpp b/firmware/teensy41/signer/src/dispatch.cpp index 5e09156..3d210fd 100644 --- a/firmware/teensy41/signer/src/dispatch.cpp +++ b/firmware/teensy41/signer/src/dispatch.cpp @@ -70,6 +70,34 @@ char g_npub[128]; char g_pubkey_hex[65]; int g_signer_ready = 0; +/* ---- Persistent crash diagnostics (defined in signer.ino, DMAMEM) ---- */ +extern "C" volatile uint32_t g_last_op; +extern "C" volatile uint32_t g_last_op_seq; +extern "C" volatile uint32_t g_stack_min; +extern "C" volatile uint32_t g_heap_free_at_crash; +extern "C" char _estack; +extern "C" char _ebss; + +/* Stamp the operation marker and record stack/heap state before a verb's + * crypto work. If the device faults, the next boot reads these DMAMEM + * values and reports which op crashed and how close the stack was. */ +static void stamp_op(uint32_t op_id) { + g_last_op_seq++; + g_last_op = op_id; + uint32_t sp; + asm volatile ("mov %0, sp\n" : "=r"(sp)); + if (sp < g_stack_min) g_stack_min = sp; + /* Record a rough heap-free estimate: probe by allocating a 1-byte block + * and capturing the returned address relative to the heap start symbol. + * On Teensy the sbrk heap lives between _heap_start and _heap_end. */ + extern char _heap_start[]; + void *p = malloc(1); + if (p) { + g_heap_free_at_crash = (uint32_t)((char *)p - _heap_start); + free(p); + } +} + /* ==================================================================== * Constants * ==================================================================== */ @@ -1013,6 +1041,30 @@ __attribute__((section(".flashmem"))) static void handle_request(cJSON *req, con cJSON *params = cJSON_GetObjectItemCaseSensitive(req, "params"); char key_id[17]; + /* Stamp the operation marker for post-crash diagnostics. Map the verb + * name to a compact id so the next boot can report which op faulted. */ + { + uint32_t op = 0; + if (strcmp(method, VERB_GET_INFO) == 0) op = 1; + else if (strcmp(method, VERB_GET_PUBLIC_KEY) == 0) op = 2; + else if (strcmp(method, VERB_SIGN) == 0) op = 3; + else if (strcmp(method, VERB_VERIFY) == 0) op = 4; + else if (strcmp(method, VERB_DERIVE_SHARED) == 0) op = 5; + else if (strcmp(method, VERB_DERIVE) == 0) op = 6; + else if (strcmp(method, VERB_NOSTR_GET_PUBLIC_KEY) == 0) op = 7; + else if (strcmp(method, VERB_NOSTR_SIGN_EVENT) == 0) op = 8; + else if (strcmp(method, VERB_NOSTR_MINE_EVENT) == 0) op = 9; + else if (strcmp(method, VERB_NOSTR_NIP04_ENCRYPT) == 0 || + strcmp(method, VERB_NOSTR_NIP04_DECRYPT) == 0) op = 10; + else if (strcmp(method, VERB_NOSTR_NIP44_ENCRYPT) == 0 || + strcmp(method, VERB_NOSTR_NIP44_DECRYPT) == 0) op = 11; + else if (strcmp(method, VERB_ENCAPSULATE) == 0) op = 12; + else if (strcmp(method, VERB_DECAPSULATE) == 0) op = 13; + else if (strcmp(method, VERB_ENCRYPT) == 0 || + strcmp(method, VERB_DECRYPT) == 0) op = 14; + if (op) stamp_op(op); + } + /* ===================== get_info (no key material, no approval) ====== */ if (strcmp(method, VERB_GET_INFO) == 0) { (void)params; diff --git a/firmware/teensy41/signer/src/ed25519.c b/firmware/teensy41/signer/src/ed25519.c index 138a38d..530d968 100644 --- a/firmware/teensy41/signer/src/ed25519.c +++ b/firmware/teensy41/signer/src/ed25519.c @@ -30,6 +30,14 @@ #define FLASHMEM_ATTR __attribute__((section(".flashmem"))) #endif +/* DMAMEM attribute for large working buffers that must live in RAM2 + * (.dmabuffers) instead of the DTCM stack, to avoid stack overflow on + * the Teensy 4.1 (~9.6 KB free stack). Used by ed_frombytes, sc_reduce, + * sc_muladd, and the SHA-512 streaming ctx. */ +#ifndef ED_DMAMEM +#define ED_DMAMEM __attribute__((section(".dmabuffers"))) +#endif + typedef int64_t gf[16]; /* Constant 121665 as a field element (low limb only). Declared early so the @@ -331,12 +339,22 @@ typedef struct { gf p[4]; } ed_p3; typedef struct { gf p[3]; } ed_p2; typedef struct { gf p[2]; } ed_p1; +/* DMAMEM workspace for ed_add (12 gf arrays = 1536 B on stack otherwise, + * called 256x per scalarmult -> overflows the 9.6 KB DTCM stack). */ +ED_DMAMEM static int64_t ed_add_a0[16], ed_add_a1[16], ed_add_b0[16], + ed_add_b1[16], ed_add_c0[16], ed_add_c1[16], + ed_add_d0[16], ed_add_d1[16], ed_add_e[16], + ed_add_f[16], ed_add_g[16], ed_add_h[16]; + FLASHMEM_ATTR static void ed_add(ed_p3 *o, const ed_p3 *a, const ed_p3 *b) { /* Unified extended-coordinates addition for the twisted Edwards curve * -x^2 + y^2 = 1 + d x^2 y^2 (a = -1). This formula is also valid when * a == b, so ed_double() simply calls ed_add(o, a, a). All reads of the * inputs happen into locals before any write to o, so o may alias a/b. */ - gf a0, a1, b0, b1, c0, c1, d0, d1, e, f, g, h; + /* Use DMAMEM gf workspace (12 arrays = 1536 B) to avoid stack overflow. */ + int64_t *a0 = ed_add_a0, *a1 = ed_add_a1, *b0 = ed_add_b0, *b1 = ed_add_b1, + *c0 = ed_add_c0, *c1 = ed_add_c1, *d0 = ed_add_d0, *d1 = ed_add_d1, + *e = ed_add_e, *f = ed_add_f, *g = ed_add_g, *h = ed_add_h; int i; FOR(i, 16) { a0[i] = a->p[1][i] - a->p[0][i]; /* Y1 - X1 */ @@ -413,6 +431,14 @@ FLASHMEM_ATTR static void ed_p3_tobytes(uint8_t *o, const ed_p3 *p) { o[31] ^= (uint8_t)((xb[0] & 1) << 7); } +/* DMAMEM gf workspace for ed_frombytes (9 gf arrays = 1152 B on stack + * otherwise, which overflows during ed25519_verify). The gf type is + * int64_t[16]; we declare the DMAMEM arrays and access them through + * pointers so the functions that take `gf` (int64_t*) work unchanged. */ +ED_DMAMEM static int64_t ed_fb_x[16], ed_fb_y[16], ed_fb_z[16], ed_fb_u[16], + ed_fb_v[16], ed_fb_v3[16], ed_fb_uv7[16], + ed_fb_w[16], ed_fb_c[16]; + FLASHMEM_ATTR static int ed_frombytes(ed_p3 *p, const uint8_t *n) { /* Decode a 32-byte point encoding (RFC 8032 section 5.1.3). Curve: * -x^2 + y^2 = 1 + d x^2 y^2, with u = y^2 - 1, v = d y^2 + 1, so @@ -420,7 +446,11 @@ FLASHMEM_ATTR static int ed_frombytes(ed_p3 *p, const uint8_t *n) { * x = u * v^3 * (u * v^7)^((p-5)/8) * which equals (u/v)^((p+3)/8) up to a 4th root of unity, then fix up * the sign of the root with I = sqrt(-1) and the sign bit of n. */ - gf x, y, z, u, v, v3, uv7, w, c; + /* Use DMAMEM gf workspace to avoid 1152 B on the stack. + * gf is int64_t[16]; use int64_t* pointers to the DMAMEM arrays. */ + int64_t *x = ed_fb_x, *y = ed_fb_y, *z = ed_fb_z, *u = ed_fb_u, + *v = ed_fb_v, *v3 = ed_fb_v3, *uv7 = ed_fb_uv7, + *w = ed_fb_w, *c = ed_fb_c; int i, a; unpack25519(y, n); FOR(i, 16) { @@ -500,6 +530,16 @@ static const uint64_t ed_L[32] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10 }; +/* ---- DMAMEM workspace for large ed25519 temporaries ---- + * The Teensy 4.1 has only ~9.6 KB of free DTCM stack. The ed25519 sign/verify + * call chain needs several 512-byte int64_t x[64] buffers (sc_reduce, + * sc_muladd) plus SHA-512 contexts, which overflow the stack when nested + * inside handle_request -> ed25519_sign. Moving these to a persistent DMAMEM + * (RAM2) workspace eliminates the stack pressure. The signer is + * single-threaded so no locking is needed. */ +ED_DMAMEM static int64_t ed_sc_x[64]; /* sc_reduce / sc_muladd working buffer (512 B) */ +ED_DMAMEM static uint8_t ed_sha512ctx_buf[sizeof(ed_sha512ctx)]; /* SHA-512 streaming ctx (328 B) */ + /* Reduce the 64-limb little-endian integer x[0..63] mod L into r[0..31]. * TweetNaCl modL, verbatim. */ FLASHMEM_ATTR static void sc_modL(uint8_t *r, int64_t x[64]) { @@ -529,19 +569,23 @@ FLASHMEM_ATTR static void sc_modL(uint8_t *r, int64_t x[64]) { /* Reduce a 64-byte little-endian integer mod L into 32 bytes. */ FLASHMEM_ATTR static void sc_reduce(uint8_t *o, const uint8_t s[64]) { - int64_t x[64], i; + int64_t i; + int64_t *x = ed_sc_x; FOR(i, 64) x[i] = (uint64_t) s[i]; sc_modL(o, x); + FOR(i, 64) x[i] = 0; /* zeroize sensitive working buffer */ } /* o = (a * b + c) mod L. Builds the 64-byte little-endian product+sum then * reduces via sc_modL (TweetNaCl crypto_sign style). */ FLASHMEM_ATTR static void sc_muladd(uint8_t *o, const uint8_t a[32], const uint8_t b[32], const uint8_t c[32]) { - int64_t x[64], i, j; + int64_t i, j; + int64_t *x = ed_sc_x; FOR(i, 64) x[i] = 0; FOR(i, 32) x[i] = (uint64_t) c[i]; FOR(i, 32) FOR(j, 32) x[i + j] += (int64_t) a[i] * (uint64_t) b[j]; sc_modL(o, x); + FOR(i, 64) x[i] = 0; /* zeroize sensitive working buffer */ } /* ==================================================================== @@ -572,12 +616,13 @@ FLASHMEM_ATTR int ed25519_sign(const uint8_t privkey[32], az[31] &= 127; az[31] |= 64; - /* r = SHA-512(az[32..64] || msg) mod L */ - ed_sha512ctx ctx; - ed_sha512_init(&ctx); - ed_sha512_update(&ctx, az + 32, 32); - ed_sha512_update(&ctx, msg, msg_len); - ed_sha512_final(&ctx, nonce); + /* r = SHA-512(az[32..64] || msg) mod L. + * Use the DMAMEM SHA-512 ctx to avoid a 328-byte stack frame. */ + ed_sha512ctx *ctx = (ed_sha512ctx *)ed_sha512ctx_buf; + ed_sha512_init(ctx); + ed_sha512_update(ctx, az + 32, 32); + ed_sha512_update(ctx, msg, msg_len); + ed_sha512_final(ctx, nonce); sc_reduce(r, nonce); /* R = r * B */ @@ -591,15 +636,23 @@ FLASHMEM_ATTR int ed25519_sign(const uint8_t privkey[32], ed_p3_tobytes(sig + 32, &A); /* k = SHA-512(R || A || msg) mod L */ - ed_sha512_init(&ctx); - ed_sha512_update(&ctx, sig, 32); - ed_sha512_update(&ctx, sig + 32, 32); - ed_sha512_update(&ctx, msg, msg_len); - ed_sha512_final(&ctx, hram); + ed_sha512_init(ctx); + ed_sha512_update(ctx, sig, 32); + ed_sha512_update(ctx, sig + 32, 32); + ed_sha512_update(ctx, msg, msg_len); + ed_sha512_final(ctx, hram); sc_reduce(k, hram); /* S = (r + k * a) mod L */ sc_muladd(sig + 32, k, az, r); + + /* Zeroize sensitive locals */ + memset(az, 0, sizeof(az)); + memset(nonce, 0, sizeof(nonce)); + memset(hram, 0, sizeof(hram)); + memset(r, 0, sizeof(r)); + memset(k, 0, sizeof(k)); + memset(ed_sha512ctx_buf, 0, sizeof(ed_sha512ctx_buf)); return 0; } @@ -627,13 +680,15 @@ FLASHMEM_ATTR int ed25519_verify(const uint8_t sig[64], if (ed_frombytes(&A, pubkey) != 0) return 0; - ed_sha512ctx ctx; - ed_sha512_init(&ctx); - ed_sha512_update(&ctx, sig, 32); - ed_sha512_update(&ctx, pubkey, 32); - ed_sha512_update(&ctx, msg, msg_len); - ed_sha512_final(&ctx, hram); + /* Use the DMAMEM SHA-512 ctx to avoid a 328-byte stack frame. */ + ed_sha512ctx *ctx = (ed_sha512ctx *)ed_sha512ctx_buf; + ed_sha512_init(ctx); + ed_sha512_update(ctx, sig, 32); + ed_sha512_update(ctx, pubkey, 32); + ed_sha512_update(ctx, msg, msg_len); + ed_sha512_final(ctx, hram); sc_reduce(k, hram); + memset(ed_sha512ctx_buf, 0, sizeof(ed_sha512ctx_buf)); /* RFC 8032: verify S*B == R + k*A, i.e. S*B - k*A == R. We compute * k*A, negate it ((X,Y,Z,T) -> (-X, Y, Z, -T)), add S*B, and compare diff --git a/firmware/teensy41/test_classical.py b/firmware/teensy41/test_classical.py new file mode 100644 index 0000000..a41da04 --- /dev/null +++ b/firmware/teensy41/test_classical.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +"""Classical-only Teensy 4.1 n_signer test (no OTP, no PQ). + +Runs the full classical + Nostr sequence in one uninterrupted boot and +prints exactly which verb succeeded and which one crashed the device. +After a crash, re-run to read the CrashReport from the next boot. + +Usage: + python3 firmware/teensy41/test_classical.py [--port /dev/ttyACM0] +""" +import serial, struct, json, time, sys, argparse, hashlib + +DEFAULT_PORT = "/dev/ttyACM0" +BAUD = 115200 + +def send_request(ser, req): + payload = json.dumps(req).encode("utf-8") + ser.write(struct.pack(">I", len(payload)) + payload) + ser.flush() + h = b"" + deadline = time.time() + 30.0 + while len(h) < 4 and time.time() < deadline: + c = ser.read(4 - len(h)) + if c: h += c + else: time.sleep(0.01) + if len(h) < 4: raise TimeoutError("hdr timeout") + n = struct.unpack(">I", h)[0] + if n == 0 or n > 65536: raise ValueError("bad len %d" % n) + p = b"" + while len(p) < n: + c = ser.read(n - len(p)) + if c: p += c + else: time.sleep(0.01) + return json.loads(p.decode()) + +def call(ser, method, params=None, idx=[0]): + idx[0] += 1 + r = {"jsonrpc": "2.0", "id": idx[0], "method": method} + if params is not None: r["params"] = params + print(f" -> {method} {json.dumps(params) if params else '[]'}", flush=True) + resp = send_request(ser, r) + ok = "result" in resp + print(f" <- {'OK' if ok else 'ERR'} {str(resp)[:160]}", flush=True) + return resp + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--port", default=DEFAULT_PORT) + ap.add_argument("--read-crash", action="store_true", help="only drain and print boot/CrashReport text") + args = ap.parse_args() + + ser = serial.Serial(args.port, BAUD, timeout=2.0) + time.sleep(5) + boot = b"" + while ser.in_waiting: + boot += ser.read(ser.in_waiting) + if boot: + print("=== BOOT OUTPUT ===") + print(boot.decode("utf-8", errors="replace")) + print("=== END BOOT ===") + if args.read_crash: + ser.close(); return 0 + + passed = 0; failed = 0 + def t(name, fn): + nonlocal passed, failed + print(f"\n[{name}]", flush=True) + try: + fn() + passed += 1 + except Exception as e: + print(f" !! CRASH at {name}: {e}", flush=True) + failed += 1 + raise + + try: + t("get_info", lambda: call(ser, "get_info")) + # classical pubkeys + pubkeys = {} + def gpk(alg): + r = call(ser, "get_public_key", [{"algorithm": alg, "index": 0}]) + pubkeys[alg] = r["result"]["public_key"] + t("gpk secp256k1", lambda: gpk("secp256k1")) + t("gpk ed25519", lambda: gpk("ed25519")) + t("gpk x25519", lambda: gpk("x25519")) + # secp256k1 schnorr sign + verify + msg = hashlib.sha256(b"test schnorr").hexdigest() + sig = [None] + def schnorr_sign(): + r = call(ser, "sign", [msg, {"algorithm": "secp256k1", "index": 0, "scheme": "schnorr"}]) + sig[0] = r["result"]["signature"] + t("sign schnorr", schnorr_sign) + def schnorr_verify(): + call(ser, "verify", [msg, sig[0], {"algorithm": "secp256k1", "index": 0, "scheme": "schnorr"}]) + t("verify schnorr", schnorr_verify) + # secp256k1 ecdsa sign + verify + msg2 = hashlib.sha256(b"test ecdsa").hexdigest() + sig2 = [None] + def ecdsa_sign(): + r = call(ser, "sign", [msg2, {"algorithm": "secp256k1", "index": 0, "scheme": "ecdsa"}]) + sig2[0] = r["result"]["signature"] + t("sign ecdsa", ecdsa_sign) + def ecdsa_verify(): + call(ser, "verify", [msg2, sig2[0], {"algorithm": "secp256k1", "index": 0, "scheme": "ecdsa"}]) + t("verify ecdsa", ecdsa_verify) + # ed25519 sign + verify + msg3 = b"test ed25519 message".hex() + sig3 = [None] + def ed_sign(): + r = call(ser, "sign", [msg3, {"algorithm": "ed25519", "index": 0}]) + sig3[0] = r["result"]["signature"] + t("sign ed25519", ed_sign) + def ed_verify(): + call(ser, "verify", [msg3, sig3[0], {"algorithm": "ed25519", "index": 0}]) + t("verify ed25519", ed_verify) + # x25519 shared secret + def x25519(): + call(ser, "derive_shared_secret", [pubkeys["x25519"], {"algorithm": "x25519", "index": 0}]) + t("derive_shared_secret x25519", x25519) + # derive (HMAC) + t("derive", lambda: call(ser, "derive", ["derive-test", {"algorithm": "secp256k1", "index": 1}])) + # nostr + npub = [None] + def ngpk(): + r = call(ser, "nostr_get_public_key", [{"nostr_index": 0}]) + npub[0] = r["result"] + t("nostr_get_public_key", ngpk) + def nse(): + ev = {"kind": 1, "created_at": int(time.time()), "tags": [], "content": "hello event"} + call(ser, "nostr_sign_event", [ev, {"nostr_index": 0}]) + t("nostr_sign_event", nse) + # nip04 + def nip04(): + r = call(ser, "nostr_nip04_encrypt", [npub[0], "hello via nip04", {"nostr_index": 0}]) + call(ser, "nostr_nip04_decrypt", [npub[0], r["result"], {"nostr_index": 0}]) + t("nip04 round-trip", nip04) + # nip44 + def nip44(): + r = call(ser, "nostr_nip44_encrypt", [npub[0], "hello via nip44", {"nostr_index": 0}]) + call(ser, "nostr_nip44_decrypt", [npub[0], r["result"], {"nostr_index": 0}]) + t("nip44 round-trip", nip44) + except Exception as e: + print(f"\n!! STOPPED: {e}", flush=True) + + print(f"\nRESULTS: {passed} passed, {failed} failed") + ser.close() + return 0 if failed == 0 else 1 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/main.c b/src/main.c index c09fa3d..ce5cc66 100644 --- a/src/main.c +++ b/src/main.c @@ -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 2 -#define NSIGNER_VERSION "v0.1.2" +#define NSIGNER_VERSION_PATCH 3 +#define NSIGNER_VERSION "v0.1.3" /* NSIGNER_HEADERLESS_DECLS_END */