v0.1.4 - Teensy 4.1 PQ stack fixes: moved ml-kem-768 NTT working polys (polyvec_matrix_pointwise 6656B, indcpa enc 9728B, indcpa dec 5120B, poly_mul_negacyclic 1024B) and ml-dsa-65 sign poly c (1024B) + SHAKE out buffers (2688B) from DTCM stack to DMAMEM (RAM2) static workspace; fixed ml-kem-768 keygen crash (verified on hardware); added ml-dsa-65 rejection-loop DMAMEM counter diagnostic; ml-dsa-65 sign no longer crashes but hangs due to pre-existing NTT correctness bug (rejection loop runs to exhaustion) — next milestone is to compare NTT output against host reference

This commit is contained in:
Laan Tungir
2026-07-27 18:46:09 -04:00
parent ea2f1cd78e
commit 314f1c520a
11 changed files with 873 additions and 60 deletions
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""Test a single PQ verb on a fresh boot.
Usage:
python3 firmware/teensy41/pq_one.py <method> '<json params>'
Opens /dev/ttyACM0, drains boot output, sends ONE request, prints the
response (or timeout). Exit 0 if "result" present, 1 otherwise.
"""
import serial, struct, json, time, sys, argparse
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() + 180.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 (no response in 60s)")
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 main():
ap = argparse.ArgumentParser()
ap.add_argument("--port", default=DEFAULT_PORT)
ap.add_argument("method")
ap.add_argument("params", help="JSON array of params")
args = ap.parse_args()
params = json.loads(args.params)
ser = serial.Serial(args.port, BAUD, timeout=2.0)
# drain boot
time.sleep(6)
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 ===")
req = {"jsonrpc": "2.0", "id": 1, "method": args.method, "params": params}
print(f"-> {args.method} {json.dumps(params)}", flush=True)
try:
resp = send_request(ser, req)
except Exception as e:
print(f"!! CRASH/TIMEOUT: {e}", flush=True)
# try to read any trailing output
time.sleep(1)
tail = b""
while ser.in_waiting:
tail += ser.read(ser.in_waiting)
if tail:
print("=== TAIL OUTPUT ===")
print(tail.decode("utf-8", errors="replace"))
print("=== END TAIL ===")
ser.close()
return 2
ok = "result" in resp
print(f"<- {'OK' if ok else 'ERR'} {json.dumps(resp)[:400]}", flush=True)
ser.close()
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())
+27
View File
@@ -0,0 +1,27 @@
[
{
"case": "ml-dsa-65 sign",
"method": "sign",
"params": [
"746573742065643235353139206d657373616765",
{
"algorithm": "ml-dsa-65",
"index": 0
}
],
"outcome": "CRASH",
"response": null,
"boot_output": "Transport initialized.\r\nIdle screen shown. Signer ready.\r\n",
"crash_report": null,
"last_op_before_crash": null,
"next_boot_last_op": {
"op_id": 3,
"op_name": "sign",
"seq": 63,
"stack_hw_free": 537034752,
"heap_free_at_crash": 1032
},
"next_boot_crash_report": "CRASH REPORT FROM PRIOR FAULT:\r\nCrashReport:\r\n A problem occurred at (system time) 17:0:58\r\n Code was executing from address 0x4362C\r\n CFSR: 82\r\n\t(DACCVIOL) Data Access Violation\r\n\t(MMARVALID) Accessed Address: 0x20025A60 (Stack problem)\r\n\t Check for stack overflows, array bounds, etc.\r\n Temperature inside the chip was 47.07 \u00b0C\r\n Startup CPU clock speed is 600MHz\r\n Reboot was caused by auto reboot after fault or bad interrupt detected\r\n\r\nLAST OP BEFORE CRASH: id=3 seq=63 stack_hw_free=537034752 heap_free_at_crash=1032",
"error": "SerialException: device reports readiness to read but returned no data (device disconnected or multiple access on port?) (after 8.1s)"
}
]
+10
View File
@@ -362,6 +362,10 @@ 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;
// ML-DSA-65 sign rejection-loop iteration counter. Written each iteration of
// crypto_sign()'s rejection loop (mldsa65_sign.c) so a post-crash/hang reboot
// reports how far the loop got without corrupting the framed transport.
DMAMEM volatile uint32_t g_mldsa65_reject_count = 0;
#define OP_ID_GET_INFO 1
#define OP_ID_GPK 2
#define OP_ID_SIGN 3
@@ -422,6 +426,12 @@ void setup() {
Serial.println();
}
// Report the ML-DSA-65 sign rejection-loop count from before the crash/hang.
if (g_mldsa65_reject_count > 0) {
Serial.print("ML-DSA-65 LAST REJECT COUNT: ");
Serial.println((int)g_mldsa65_reject_count);
}
Serial.println("n_signer booting...");
tft.init(320, 480);
tft.setRotation(1);
@@ -58,6 +58,17 @@ PQ_DMAMEM static polyvec s_kem_s, s_kem_e, s_kem_t; /* keygen */
PQ_DMAMEM static polyvec s_kem_enc_t, s_kem_enc_r, s_kem_enc_e1, s_kem_enc_u; /* enc */
PQ_DMAMEM static polyvec s_kem_dec_u, s_kem_dec_s; /* dec */
/* NTT-domain working buffers for the enc/dec negacyclic multiply paths.
* Moved off the DTCM stack to RAM2 (.dmabuffers) to avoid stack overflow on
* the Teensy 4.1. The signer is single-threaded so static reuse is safe.
* enc: r_ntt 1536 + A_ntt[3] 4608 + t_ntt 1536 + tmp 512 = ~8192 B
* dec: s_ntt 1536 + u_ntt 1536 + t2 512 = ~3584 B */
PQ_DMAMEM static polyvec s_enc_r_ntt, s_enc_t_ntt;
PQ_DMAMEM static polyvec s_enc_A_ntt[ML_KEM_768_K];
PQ_DMAMEM static poly s_enc_tmp;
PQ_DMAMEM static polyvec s_dec_s_ntt, s_dec_u_ntt;
PQ_DMAMEM static poly s_dec_t2;
/* Pack/unpack helpers */
__attribute__((section(".flashmem"))) static void pack_sk(uint8_t sk[ML_KEM_768_INDCPA_SECRETKEYBYTES], const polyvec *s) {
ml_kem_768_polyvec_tobytes(sk, s);
@@ -173,28 +184,28 @@ __attribute__((section(".flashmem"))) void ml_kem_768_indcpa_enc(uint8_t ct[ML_K
* ml_kem_768_poly_mul_negacyclic calls that caused encapsulation
* timeouts on the Teensy 4.1. */
{
polyvec r_ntt;
polyvec A_ntt[ML_KEM_768_K];
polyvec t_ntt;
poly tmp;
polyvec *r_ntt = &s_enc_r_ntt;
polyvec *A_ntt = s_enc_A_ntt;
polyvec *t_ntt = &s_enc_t_ntt;
poly *tmp = &s_enc_tmp;
int j;
memcpy(&r_ntt, r, sizeof(polyvec));
ml_kem_768_polyvec_ntt(&r_ntt);
memcpy(r_ntt, r, sizeof(polyvec));
ml_kem_768_polyvec_ntt(r_ntt);
for (j = 0; j < ML_KEM_768_K; j++) {
memcpy(&A_ntt[j], &A[j], sizeof(polyvec));
ml_kem_768_polyvec_ntt(&A_ntt[j]);
}
memcpy(&t_ntt, t, sizeof(polyvec));
ml_kem_768_polyvec_ntt(&t_ntt);
memcpy(t_ntt, t, sizeof(polyvec));
ml_kem_768_polyvec_ntt(t_ntt);
/* u[i] = sum_j A^T[i][j] * r[j] = sum_j A[j].vec[i] * r[j] */
for (i = 0; i < ML_KEM_768_K; i++) {
ml_kem_768_poly_basemul(&u->vec[i], &A_ntt[0].vec[i], &r_ntt.vec[0]);
ml_kem_768_poly_basemul(&u->vec[i], &A_ntt[0].vec[i], &r_ntt->vec[0]);
for (j = 1; j < ML_KEM_768_K; j++) {
ml_kem_768_poly_basemul(&tmp, &A_ntt[j].vec[i], &r_ntt.vec[j]);
ml_kem_768_poly_add(&u->vec[i], &u->vec[i], &tmp);
ml_kem_768_poly_basemul(tmp, &A_ntt[j].vec[i], &r_ntt->vec[j]);
ml_kem_768_poly_add(&u->vec[i], &u->vec[i], tmp);
}
ml_kem_768_poly_reduce(&u->vec[i]);
ml_kem_768_poly_invntt(&u->vec[i]);
@@ -204,10 +215,10 @@ __attribute__((section(".flashmem"))) void ml_kem_768_indcpa_enc(uint8_t ct[ML_K
ml_kem_768_polyvec_reduce(u);
/* v = sum_i t[i] * r[i] */
ml_kem_768_poly_basemul(&v, &t_ntt.vec[0], &r_ntt.vec[0]);
ml_kem_768_poly_basemul(&v, &t_ntt->vec[0], &r_ntt->vec[0]);
for (i = 1; i < ML_KEM_768_K; i++) {
ml_kem_768_poly_basemul(&tmp, &t_ntt.vec[i], &r_ntt.vec[i]);
ml_kem_768_poly_add(&v, &v, &tmp);
ml_kem_768_poly_basemul(tmp, &t_ntt->vec[i], &r_ntt->vec[i]);
ml_kem_768_poly_add(&v, &v, tmp);
}
ml_kem_768_poly_reduce(&v);
ml_kem_768_poly_invntt(&v);
@@ -245,19 +256,20 @@ __attribute__((section(".flashmem"))) void ml_kem_768_indcpa_dec(uint8_t m[ML_KE
* NTT-based negacyclic multiplication: NTT s and u once, pointwise-
* multiply and accumulate in the NTT domain, then inverse-NTT. */
{
polyvec s_ntt, u_ntt;
poly t2;
polyvec *s_ntt = &s_dec_s_ntt;
polyvec *u_ntt = &s_dec_u_ntt;
poly *t2 = &s_dec_t2;
int i;
memcpy(&s_ntt, s, sizeof(polyvec));
memcpy(&u_ntt, u, sizeof(polyvec));
ml_kem_768_polyvec_ntt(&s_ntt);
ml_kem_768_polyvec_ntt(&u_ntt);
memcpy(s_ntt, s, sizeof(polyvec));
memcpy(u_ntt, u, sizeof(polyvec));
ml_kem_768_polyvec_ntt(s_ntt);
ml_kem_768_polyvec_ntt(u_ntt);
ml_kem_768_poly_basemul(&tmp, &s_ntt.vec[0], &u_ntt.vec[0]);
ml_kem_768_poly_basemul(&tmp, &s_ntt->vec[0], &u_ntt->vec[0]);
for (i = 1; i < ML_KEM_768_K; i++) {
ml_kem_768_poly_basemul(&t2, &s_ntt.vec[i], &u_ntt.vec[i]);
ml_kem_768_poly_add(&tmp, &tmp, &t2);
ml_kem_768_poly_basemul(t2, &s_ntt->vec[i], &u_ntt->vec[i]);
ml_kem_768_poly_add(&tmp, &tmp, t2);
}
ml_kem_768_poly_reduce(&tmp);
ml_kem_768_poly_invntt(&tmp);
@@ -16,6 +16,29 @@
#include <string.h>
/* 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). The signer is single-threaded so
* static reuse is safe. */
#ifndef PQ_DMAMEM
#define PQ_DMAMEM __attribute__((section(".dmabuffers")))
#endif
/* poly_reject_uniform squeeze buffer (4 KB). Reused for both the initial
* squeeze and the "ran out of bytes" re-squeeze — the first buffer is no
* longer accessed once the second path is taken. */
PQ_DMAMEM static uint8_t s_poly_reject_buf[4096];
/* Static NTT-domain working buffers for poly_mul_negacyclic (2 polys = 1 KB)
* and polyvec_matrix_pointwise (s_ntt 1536 B + A_ntt[3] 4608 B + tmp 512 B
* = 6656 B). Moved off the DTCM stack to RAM2 (.dmabuffers) to avoid stack
* overflow on the Teensy 4.1. The signer is single-threaded so static reuse
* is safe. */
PQ_DMAMEM static poly s_mulneg_at, s_mulneg_bt;
PQ_DMAMEM static polyvec s_mwp_s_ntt;
PQ_DMAMEM static polyvec s_mwp_A_ntt[ML_KEM_768_K];
PQ_DMAMEM static poly s_mwp_tmp;
#define Q ML_KEM_768_Q
/* ---- basic poly ops ---- */
@@ -111,15 +134,15 @@ __attribute__((section(".flashmem"))) void ml_kem_768_poly_basemul(poly *r, cons
* r may alias neither a nor b (the operands are transformed in place via
* local copies). All callers in indcpa.c use distinct output/input polys. */
__attribute__((section(".flashmem"))) void ml_kem_768_poly_mul_negacyclic(poly *r, const poly *a, const poly *b) {
poly at, bt;
poly *at = &s_mulneg_at, *bt = &s_mulneg_bt;
int i;
memcpy(&at, a, sizeof(poly));
memcpy(&bt, b, sizeof(poly));
memcpy(at, a, sizeof(poly));
memcpy(bt, b, sizeof(poly));
ml_kem_768_poly_ntt(&at);
ml_kem_768_poly_ntt(&bt);
ml_kem_768_poly_basemul(r, &at, &bt);
ml_kem_768_poly_ntt(at);
ml_kem_768_poly_ntt(bt);
ml_kem_768_poly_basemul(r, at, bt);
ml_kem_768_poly_invntt(r);
ml_kem_768_poly_reduce(r);
@@ -347,17 +370,19 @@ __attribute__((section(".flashmem"))) void ml_kem_768_polyvec_frombytes(polyvec
* Squeezes a large buffer at once (since EVP_DigestFinalXOF is one-shot). */
__attribute__((section(".flashmem"))) static void poly_reject_uniform(int16_t r[ML_KEM_768_N], const uint8_t seed[34]) {
/* We need up to ~256 * 3 = 768 bytes (with rejection, ~1.6x = ~1200).
* Squeeze 4096 bytes to be safe. */
uint8_t buf[4096];
* Squeeze 4096 bytes to be safe. Use the DMAMEM static workspace buffer
* (4 KB on the DTCM stack would overflow the Teensy 4.1). The signer is
* single-threaded so static reuse is safe. */
uint8_t *buf = s_poly_reject_buf;
int ctr = 0;
size_t off = 0;
if (shake128(buf, sizeof(buf), seed, 34) != 0) {
if (shake128(buf, 4096, seed, 34) != 0) {
memset(r, 0, ML_KEM_768_N * sizeof(int16_t));
return;
}
while (ctr < ML_KEM_768_N && off + 2 <= sizeof(buf)) {
while (ctr < ML_KEM_768_N && off + 2 <= 4096) {
uint16_t val = (uint16_t)(buf[off] | ((uint16_t)(buf[off + 1] & 0x0F) << 8));
off += 2;
if (val < 5 * Q) {
@@ -365,15 +390,16 @@ __attribute__((section(".flashmem"))) static void poly_reject_uniform(int16_t r[
}
}
/* If we ran out of bytes, squeeze more */
/* If we ran out of bytes, squeeze more. Reuse the same static buffer
* (the first squeeze's contents are no longer needed once we re-squeeze). */
while (ctr < ML_KEM_768_N) {
uint8_t buf2[4096];
if (shake128(buf2, sizeof(buf2), seed, 34) != 0) {
uint8_t *buf2 = s_poly_reject_buf;
if (shake128(buf2, 4096, seed, 34) != 0) {
while (ctr < ML_KEM_768_N) r[ctr++] = 0;
return;
}
off = 0;
while (ctr < ML_KEM_768_N && off + 2 <= sizeof(buf2)) {
while (ctr < ML_KEM_768_N && off + 2 <= 4096) {
uint16_t val = (uint16_t)(buf2[off] | ((uint16_t)(buf2[off + 1] & 0x0F) << 8));
off += 2;
if (val < 5 * Q) {
@@ -435,13 +461,13 @@ __attribute__((section(".flashmem"))) void ml_kem_768_polyvec_getnoise_eta1(poly
__attribute__((section(".flashmem"))) void ml_kem_768_polyvec_matrix_pointwise(polyvec *r, const polyvec A[ML_KEM_768_K],
const polyvec *s) {
int i, j;
polyvec s_ntt;
polyvec A_ntt[ML_KEM_768_K];
poly tmp;
polyvec *s_ntt = &s_mwp_s_ntt;
polyvec *A_ntt = s_mwp_A_ntt;
poly *tmp = &s_mwp_tmp;
/* NTT the secret vector once. */
memcpy(&s_ntt, s, sizeof(polyvec));
ml_kem_768_polyvec_ntt(&s_ntt);
memcpy(s_ntt, s, sizeof(polyvec));
ml_kem_768_polyvec_ntt(s_ntt);
/* NTT each matrix entry once. */
for (i = 0; i < ML_KEM_768_K; i++) {
@@ -451,10 +477,10 @@ __attribute__((section(".flashmem"))) void ml_kem_768_polyvec_matrix_pointwise(p
for (i = 0; i < ML_KEM_768_K; i++) {
/* r[i] = sum_j A_ntt[i][j] * s_ntt[j] (pointwise in NTT domain). */
ml_kem_768_poly_basemul(&r->vec[i], &A_ntt[i].vec[0], &s_ntt.vec[0]);
ml_kem_768_poly_basemul(&r->vec[i], &A_ntt[i].vec[0], &s_ntt->vec[0]);
for (j = 1; j < ML_KEM_768_K; j++) {
ml_kem_768_poly_basemul(&tmp, &A_ntt[i].vec[j], &s_ntt.vec[j]);
ml_kem_768_poly_add(&r->vec[i], &r->vec[i], &tmp);
ml_kem_768_poly_basemul(tmp, &A_ntt[i].vec[j], &s_ntt->vec[j]);
ml_kem_768_poly_add(&r->vec[i], &r->vec[i], tmp);
}
ml_kem_768_poly_reduce(&r->vec[i]);
ml_kem_768_poly_invntt(&r->vec[i]);
@@ -17,10 +17,23 @@
#ifdef HOST_TEST
#define FLASHMEM_ATTR
#define PQ_DMAMEM
#else
#define FLASHMEM_ATTR __attribute__((section(".flashmem")))
/* 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). The signer is single-threaded so
* static reuse is safe. */
#define PQ_DMAMEM __attribute__((section(".dmabuffers")))
#endif
/* Shared SHAKE squeeze workspace for the sampling routines.
* Sized to the largest consumer: poly_uniform uses 168*16 = 2688 bytes.
* poly_challenge uses 136*8 = 1088 bytes; poly_eta uses 136*4 = 544 bytes;
* poly_uniform_gamma1 uses 136*8 = 1088 bytes. All fit in 2688 bytes.
* The signer is single-threaded so a single shared buffer is safe. */
PQ_DMAMEM static uint8_t s_poly_shake_out[168 * 16];
/* q = 8380417 */
#define Q 8380417
#define N 256
@@ -152,8 +165,8 @@ FLASHMEM_ATTR void poly_mul_pointwise(poly *r, const poly *a, const poly *b) {
FLASHMEM_ATTR void poly_uniform(poly *r, const uint8_t seed[ML_DSA_65_SEEDBYTES],
uint16_t i, uint16_t j) {
uint8_t buf[36]; /* seed(32) + i(2) + j(2) = 36 */
uint8_t out[168 * 16]; /* SHAKE-128 rate=168, get enough output */
size_t outlen = sizeof(out);
uint8_t *out = s_poly_shake_out; /* 168*16 = 2688 B -> DMAMEM (was stack) */
size_t outlen = 168 * 16;
size_t pos = 0;
int coeff_idx = 0;
shake128ctx ctx;
@@ -230,8 +243,8 @@ FLASHMEM_ATTR void poly_uniform_4x(poly *r0, poly *r1, poly *r2, poly *r3,
* FIPS 204: SampleInBall. Uses SHAKE-256. */
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[136 * 8]; /* SHAKE-256 rate=136, get enough */
size_t outlen = sizeof(out);
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;
int i;
@@ -305,8 +318,8 @@ FLASHMEM_ATTR void poly_challenge(poly *c, const uint8_t seed[ML_DSA_65_CRHBYTES
FLASHMEM_ATTR void poly_eta(poly *r, const uint8_t seed[ML_DSA_65_CRHBYTES],
uint16_t i, uint16_t j) {
uint8_t buf[ML_DSA_65_CRHBYTES + 4];
uint8_t out[136 * 4]; /* SHAKE-256 rate=136 */
size_t outlen = sizeof(out);
uint8_t *out = s_poly_shake_out; /* 136*4 = 544 B -> DMAMEM (was stack) */
size_t outlen = 136 * 4;
size_t pos = 0;
int coeff_idx = 0;
shake256ctx ctx;
@@ -355,8 +368,8 @@ FLASHMEM_ATTR void poly_eta(poly *r, const uint8_t seed[ML_DSA_65_CRHBYTES],
FLASHMEM_ATTR void poly_uniform_gamma1(poly *r, const uint8_t seed[ML_DSA_65_CRHBYTES],
uint16_t i, uint16_t j) {
uint8_t buf[ML_DSA_65_CRHBYTES + 4];
uint8_t out[136 * 8];
size_t outlen = sizeof(out);
uint8_t *out = s_poly_shake_out; /* 136*8 = 1088 B -> DMAMEM (was stack) */
size_t outlen = 136 * 8;
size_t bit_pos = 0;
int coeff_idx = 0;
shake256ctx ctx;
@@ -21,6 +21,13 @@
#include <string.h>
#include <stdlib.h>
/* 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.
* This avoids corrupting the framed transport stream with Serial.print
* output during signing. */
extern volatile uint32_t g_mldsa65_reject_count;
/* --- parameter-derived constants --- */
#define Q 8380417
#define N 256
@@ -68,6 +75,9 @@ PQ_DMAMEM static polyvec_L s_sign_s1, s_sign_y, s_sign_z, s_sign_cs1;
PQ_DMAMEM static polyvec_K s_sign_s2, s_sign_t0, s_sign_w, s_sign_w1, s_sign_w0;
PQ_DMAMEM static polyvec_K s_sign_h, s_sign_cs2, s_sign_ct0, s_sign_w_approx;
PQ_DMAMEM static polyvec_K s_sign_A[K];
/* crypto_sign challenge polynomial c (int32_t coeffs[256] = 1024 B).
* Moved off the DTCM stack to avoid overflow; signer is single-threaded. */
PQ_DMAMEM static poly s_sign_c;
/* crypto_sign_open working buffers (~77 KB total on stack -> DMAMEM). */
PQ_DMAMEM static polyvec_K s_vrf_t1, s_vrf_A[K], s_vrf_w1_approx, s_vrf_Az, s_vrf_ct1;
@@ -431,7 +441,7 @@ __attribute__((section(".flashmem"))) int crypto_sign(uint8_t *sig, size_t *sigl
polyvec_K *s2 = &s_sign_s2, *t0 = &s_sign_t0;
polyvec_K (*A)[K] = &s_sign_A;
uint8_t mu[CRHBYTES], rhoprime[CRHBYTES], c_tilde[C_TILDE_BYTES];
poly c;
poly *c = &s_sign_c; /* moved off DTCM stack (1024 B) to DMAMEM */
polyvec_K *w = &s_sign_w, *w1 = &s_sign_w1, *w0 = &s_sign_w0, *h = &s_sign_h, *cs2 = &s_sign_cs2;
int i, j, reject;
uint32_t kappa;
@@ -460,6 +470,10 @@ __attribute__((section(".flashmem"))) int crypto_sign(uint8_t *sig, size_t *sigl
kappa = 0;
for (reject = 0; reject < 1000; reject++) {
/* Record iteration count in DMAMEM so a post-crash/hang reboot
* reports how far the rejection loop got. */
g_mldsa65_reject_count = (uint32_t)reject;
/* Sample y */
for (i = 0; i < L; i++)
poly_uniform_gamma1(&(*y)[i], rhoprime, (uint16_t)i, (uint16_t)(kappa + i));
@@ -495,10 +509,10 @@ __attribute__((section(".flashmem"))) int crypto_sign(uint8_t *sig, size_t *sigl
shake256_release(&ctx);
}
poly_challenge(&c, c_tilde);
poly_challenge(c, c_tilde);
/* z = y + c * s1 */
scalar_mul_L(cs1, &c, s1);
scalar_mul_L(cs1, c, s1);
for (i = 0; i < L; i++) {
for (j = 0; j < N; j++) {
int32_t v = (*y)[i].coeffs[j] + (*cs1)[i].coeffs[j];
@@ -522,7 +536,7 @@ __attribute__((section(".flashmem"))) int crypto_sign(uint8_t *sig, size_t *sigl
/* 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. */
scalar_mul_K(cs2, &c, s2);
scalar_mul_K(cs2, c, s2);
{
int r0_reject = 0;
int32_t bound = GAMMA2 - BETA;
@@ -546,7 +560,7 @@ __attribute__((section(".flashmem"))) int crypto_sign(uint8_t *sig, size_t *sigl
polyvec_K *ct0 = &s_sign_ct0, *w_approx = &s_sign_w_approx;
int hint_count = 0;
int ct0_reject = 0;
scalar_mul_K(ct0, &c, t0);
scalar_mul_K(ct0, c, t0);
/* Check ||c*t0||_inf < gamma2 (FIPS 204 requirement) */
for (i = 0; i < K && !ct0_reject; i++) {
@@ -586,6 +600,7 @@ __attribute__((section(".flashmem"))) int crypto_sign(uint8_t *sig, size_t *sigl
pack_sig(sig, c_tilde, z, h);
*siglen = ML_DSA_65_CRYPTO_BYTES;
g_mldsa65_reject_count = (uint32_t)reject; /* final count: success */
memset(rhoprime, 0, sizeof(rhoprime));
memset(mu, 0, sizeof(mu));
/* Wipe static work buffers. */
@@ -595,11 +610,14 @@ __attribute__((section(".flashmem"))) int crypto_sign(uint8_t *sig, size_t *sigl
memset(w, 0, sizeof(*w)); memset(w1, 0, sizeof(*w1)); memset(w0, 0, sizeof(*w0));
memset(h, 0, sizeof(*h)); memset(cs2, 0, sizeof(*cs2));
memset(&s_sign_ct0, 0, sizeof(s_sign_ct0)); memset(&s_sign_w_approx, 0, sizeof(s_sign_w_approx));
memset(&s_sign_c, 0, sizeof(s_sign_c));
return 0;
}
g_mldsa65_reject_count = (uint32_t)reject; /* final count: exhausted */
memset(rhoprime, 0, sizeof(rhoprime));
memset(mu, 0, sizeof(mu));
memset(&s_sign_c, 0, sizeof(s_sign_c));
return -1;
}
+133
View File
@@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""ml-dsa-65 sign test with soft-reboot diagnostic recovery.
Runs sign; on hang/timeout, triggers a Teensy soft-reboot via DTR/RTS toggle
(which preserves DMAMEM) and reads the boot output for the reject-count
diagnostic. Falls back to a hard reboot hint if DTR toggle doesn't work.
"""
import serial, struct, json, time, sys, argparse
DEFAULT_PORT = "/dev/ttyACM0"
BAUD = 115200
def send_request(ser, req, timeout=180.0):
payload = json.dumps(req).encode("utf-8")
ser.write(struct.pack(">I", len(payload)) + payload)
ser.flush()
h = b""
deadline = time.time() + timeout
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 and time.time() < deadline:
c = ser.read(n - len(p))
if c:
p += c
else:
time.sleep(0.01)
if len(p) < n:
raise TimeoutError("body timeout")
return json.loads(p.decode())
def drain(ser, seconds=2.0):
buf = b""
end = time.time() + seconds
while time.time() < end:
if ser.in_waiting:
buf += ser.read(ser.in_waiting)
else:
time.sleep(0.05)
return buf.decode("utf-8", errors="replace")
def soft_reboot_and_read(port, wait_boot=8.0):
"""Toggle DTR/RTS to trigger Teensy bootloader/reset; preserve DMAMEM.
Teensy 4.1: setting DTR low then high does not reboot; but a BREAK condition
or the 1200-bps touch does. We try the 1200-bps open (Teensy bootloader
trigger) — note this may or may not preserve DMAMEM. As a safer soft-reboot
we instead pulse RTS/DTR which on some Teensy USB-CDC builds triggers a
watchdog reset.
"""
# Attempt 1: 1200 bps touch (Teensy bootloader reset). This is a hard reset
# but is the most reliable way to reboot a hung Teensy.
try:
s = serial.Serial(port, 1200, timeout=1.0)
s.dtr = False
s.rts = False
time.sleep(0.1)
s.dtr = True
time.sleep(0.05)
s.dtr = False
s.close()
except Exception as e:
print(f" (1200bps touch failed: {e})", flush=True)
time.sleep(1.0)
# Reopen at normal baud and drain boot
try:
ser = serial.Serial(port, BAUD, timeout=2.0)
except Exception as e:
print(f" (reopen failed: {e}; retrying in 5s)", flush=True)
time.sleep(5)
ser = serial.Serial(port, BAUD, timeout=2.0)
boot = drain(ser, seconds=wait_boot)
ser.close()
return boot
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--port", default=DEFAULT_PORT)
ap.add_argument("--timeout", type=float, default=180.0)
args = ap.parse_args()
ser = serial.Serial(args.port, BAUD, timeout=2.0)
boot = drain(ser, seconds=8.0)
print("=== BOOT OUTPUT (pre-sign) ===")
print(boot)
print("=== END BOOT ===", flush=True)
req = {"jsonrpc": "2.0", "id": 1, "method": "sign",
"params": ["746573742065643235353139206d657373616765",
{"algorithm": "ml-dsa-65", "index": 0}]}
print(f"-> sign ml-dsa-65 (timeout={args.timeout}s)", flush=True)
result = None
try:
resp = send_request(ser, req, timeout=args.timeout)
ok = "result" in resp
print(f"<- {'OK' if ok else 'ERR'} {str(resp)[:300]}", flush=True)
result = "pass" if ok else "err"
except Exception as e:
print(f"!! HANG/TIMEOUT: {e}", flush=True)
result = "hang"
ser.close()
if result == "hang":
print("\n=== Attempting soft reboot to read DMAMEM diagnostic ===", flush=True)
boot = soft_reboot_and_read(args.port, wait_boot=10.0)
print("=== BOOT OUTPUT (post-hang reboot) ===")
print(boot)
print("=== END BOOT ===", flush=True)
# Extract reject count
for line in boot.splitlines():
if "REJECT COUNT" in line:
print(f"\n>>> DIAGNOSTIC: {line.strip()}", flush=True)
if "CRASH" in line:
print(f"\n>>> CRASH: {line.strip()}", flush=True)
return 2
return 0 if result == "pass" else 1
if __name__ == "__main__":
sys.exit(main())
+380
View File
@@ -0,0 +1,380 @@
#!/usr/bin/env python3
"""Profile each PQ verb ALONE on a fresh boot for the Teensy 4.1 n_signer.
For each test case:
1. Reflash the firmware (arduino-cli upload) so the device is on a fresh boot.
2. Open /dev/ttyACM0, wait 6s for boot, drain boot output
(which includes any "LAST OP BEFORE CRASH" / CRASH REPORT from the
previous fault, plus the boot banner).
3. Send exactly ONE JSON-RPC request over the 4-byte big-endian length
prefix wire protocol.
4. Try to read the response with a generous timeout. If we get a valid
response, record PASS. If we time out / get garbage, record CRASH
and re-open the port to drain the *next* boot's diagnostics
("LAST OP BEFORE CRASH: id=X seq=Y stack_hw_free=Z heap_free_at_crash=W"
plus the CrashReport text).
Usage:
python3 firmware/teensy41/test_pq_profile.py [--port /dev/ttyACM0]
python3 firmware/teensy41/test_pq_profile.py --only ml-kem
"""
import serial, struct, json, time, sys, argparse, re, subprocess, os
DEFAULT_PORT = "/dev/ttyACM0"
BAUD = 115200
SIGNER_DIR = "firmware/teensy41/signer"
FQBN = "teensy:avr:teensy41"
# OP_ID map (from signer.ino) for human-readable crash ids.
OP_ID_NAMES = {
1: "get_info", 2: "get_public_key", 3: "sign", 4: "verify",
5: "derive_shared_secret", 6: "derive", 7: "nostr_get_public_key",
8: "nostr_sign_event", 9: "nostr_mine_event", 10: "nip04",
11: "nip44", 12: "encapsulate", 13: "decapsulate", 14: "otp",
}
# Test cases. Each is ONE verb on a fresh boot.
TESTS = [
{
"name": "ml-dsa-65 get_public_key",
"method": "get_public_key",
"params": [{"algorithm": "ml-dsa-65", "index": 0}],
"expect_op_id": 2,
},
{
"name": "slh-dsa-128s get_public_key",
"method": "get_public_key",
"params": [{"algorithm": "slh-dsa-128s", "index": 0}],
"expect_op_id": 2,
},
{
"name": "ml-kem-768 get_public_key",
"method": "get_public_key",
"params": [{"algorithm": "ml-kem-768", "index": 0}],
"expect_op_id": 2,
},
{
"name": "ml-dsa-65 sign",
"method": "sign",
"params": ["746573742065643235353139206d657373616765",
{"algorithm": "ml-dsa-65", "index": 0}],
"expect_op_id": 3,
},
{
"name": "slh-dsa-128s sign",
"method": "sign",
"params": ["746573742065643235353139206d657373616765",
{"algorithm": "slh-dsa-128s", "index": 0}],
"expect_op_id": 3,
},
]
def wait_for_port(port, timeout=30.0, must_exist=True):
"""Wait for the serial device node to (re)appear after a reboot/reflash."""
t0 = time.time()
while time.time() - t0 < timeout:
if os.path.exists(port):
return True
time.sleep(0.25)
if must_exist:
print(f" [port] {port} did not appear within {timeout:.0f}s")
return False
def reflash(port, retries=3):
"""Upload firmware and return True on success. Retries on failure
(the device may be mid-reboot or the port held open)."""
for attempt in range(1, retries + 1):
print(f" [reflash] (attempt {attempt}/{retries}) arduino-cli upload -p {port} --fqbn {FQBN} {SIGNER_DIR}")
try:
r = subprocess.run(
["arduino-cli", "upload", "-p", port, "--fqbn", FQBN, SIGNER_DIR],
cwd=os.getcwd(), capture_output=True, text=True, timeout=120,
)
except Exception as e:
print(f" [reflash] exception: {e}")
time.sleep(2.0)
continue
if r.returncode == 0:
print(f" [reflash] OK")
return True
# Common failure: device mid-reboot / port gone. Wait for it to
# come back, then retry.
print(f" [reflash] rc={r.returncode}")
tail = (r.stdout + r.stderr)[-400:]
if tail.strip():
print(f" [reflash] tail: {tail.strip()}")
# If the port is gone, wait for re-enumeration first.
wait_for_port(port, timeout=15.0, must_exist=False)
time.sleep(1.0)
print(f" [reflash] FAILED after {retries} attempts")
return False
def open_port(port):
return serial.Serial(port, BAUD, timeout=2.0)
def drain(ser, label, seconds=2.0):
end = time.time() + seconds
buf = b""
while time.time() < end:
n = ser.in_waiting
if n:
buf += ser.read(n)
else:
time.sleep(0.05)
if buf:
txt = buf.decode("utf-8", errors="replace")
print(f"--- {label} ---")
print(txt)
print(f"--- end {label} ---")
return txt
return ""
def send_request(ser, req, timeout=90.0):
payload = json.dumps(req).encode("utf-8")
ser.write(struct.pack(">I", len(payload)) + payload)
ser.flush()
h = b""
deadline = time.time() + timeout
while len(h) < 4 and time.time() < deadline:
c = ser.read(4 - len(h))
if c:
h += c
else:
time.sleep(0.02)
if len(h) < 4:
raise TimeoutError("response header timeout")
n = struct.unpack(">I", h)[0]
if n == 0 or n > 65536:
raise ValueError(f"bad response len {n} (raw hdr={h!r})")
p = b""
while len(p) < n and time.time() < deadline:
c = ser.read(n - len(p))
if c:
p += c
else:
time.sleep(0.02)
if len(p) < n:
raise TimeoutError(f"response body timeout ({len(p)}/{n})")
return json.loads(p.decode("utf-8"))
LAST_OP_RE = re.compile(
r"LAST OP BEFORE CRASH: id=(\d+)\s+seq=(\d+)\s+stack_hw_free=(\d+)\s+heap_free_at_crash=(\d+)"
)
def parse_last_op(text):
m = LAST_OP_RE.search(text)
if not m:
return None
return {
"op_id": int(m.group(1)),
"op_name": OP_ID_NAMES.get(int(m.group(1)), "?"),
"seq": int(m.group(2)),
"stack_hw_free": int(m.group(3)),
"heap_free_at_crash": int(m.group(4)),
}
def extract_crash_report(text):
if "CRASH REPORT" not in text:
return None
idx = text.find("CRASH REPORT")
# Capture up to the next "n_signer booting..." or 1500 chars.
end = text.find("n_signer booting", idx)
if end == -1:
end = idx + 1500
return text[idx:end].strip()
def run_one_case(port, case):
print(f"\n{'='*70}")
print(f"TEST: {case['name']}")
print(f" method={case['method']} params={json.dumps(case['params'])}")
print(f"{'='*70}")
result = {
"case": case["name"],
"method": case["method"],
"params": case["params"],
"outcome": None, # "PASS" | "CRASH" | "ERROR"
"response": None,
"boot_output": None,
"crash_report": None,
"last_op_before_crash": None,
"next_boot_last_op": None,
"next_boot_crash_report": None,
"error": None,
}
# 1. Reflash for a guaranteed fresh boot.
if not reflash(port):
result["outcome"] = "ERROR"
result["error"] = "reflash failed"
return result
# 2. Wait for the port to re-enumerate after the reboot, then for
# boot (auto-generate mnemonic + derive keys).
if not wait_for_port(port, timeout=20.0):
result["outcome"] = "ERROR"
result["error"] = f"{port} did not re-appear after reflash"
print(f" !! {port} did not re-appear after reflash")
return result
time.sleep(6)
ser = None
for attempt in range(1, 6):
try:
ser = open_port(port)
break
except Exception as e:
print(f" !! open failed (attempt {attempt}/5): {e}")
time.sleep(1.0)
if ser is None:
result["outcome"] = "ERROR"
result["error"] = "open failed after 5 attempts"
print(f" !! open failed after 5 attempts")
return result
try:
boot_txt = drain(ser, "BOOT OUTPUT", seconds=2.0)
result["boot_output"] = boot_txt
if "CRASH REPORT" in boot_txt:
result["crash_report"] = extract_crash_report(boot_txt)
lop = parse_last_op(boot_txt)
if lop:
result["last_op_before_crash"] = lop
print(f" [boot] LAST OP: {lop}")
# 3. Send exactly one request.
req = {"jsonrpc": "2.0", "id": 1, "method": case["method"],
"params": case["params"]}
print(f" -> send {case['method']}")
t0 = time.time()
try:
resp = send_request(ser, req, timeout=90.0)
dt = time.time() - t0
result["response"] = resp
if "result" in resp:
result["outcome"] = "PASS"
rstr = json.dumps(resp)
print(f" <- OK in {dt:.1f}s ({len(rstr)} bytes): {rstr[:120]}...")
else:
result["outcome"] = "ERROR"
print(f" <- ERR in {dt:.1f}s: {json.dumps(resp)[:200]}")
except (TimeoutError, ValueError, Exception) as e:
dt = time.time() - t0
result["outcome"] = "CRASH"
result["error"] = f"{type(e).__name__}: {e} (after {dt:.1f}s)"
print(f" !! CRASH during {case['method']} after {dt:.1f}s: {e}")
try:
drain(ser, "PARTIAL OUTPUT AFTER CRASH", seconds=2.0)
except Exception:
pass
try:
ser.close()
except Exception:
pass
# Wait for the port to come back (Teensy CrashReport usually
# auto-reboots; the device may briefly disappear).
wait_for_port(port, timeout=20.0, must_exist=False)
# Re-open IMMEDIATELY — the device's setup() only waits 3s
# (while (!Serial && millis() < 3000)) for the host to open
# the port before it stops buffering early boot prints. If we
# sleep too long we miss the "LAST OP BEFORE CRASH" /
# "CRASH REPORT" lines that are emitted at the very top of
# setup().
ser2 = None
for attempt in range(1, 10):
try:
ser2 = open_port(port)
break
except Exception:
time.sleep(0.3)
if ser2 is None:
result["error"] += " | next-boot port never came back"
print(f" !! next-boot port never came back")
return result
try:
# Drain immediately and keep draining for a while to
# capture the full boot banner.
next_txt = drain(ser2, "NEXT-BOOT OUTPUT", seconds=8.0)
result["next_boot_last_op"] = parse_last_op(next_txt)
result["next_boot_crash_report"] = extract_crash_report(next_txt)
if result["next_boot_last_op"]:
print(f" [next-boot] LAST OP: {result['next_boot_last_op']}")
if result["next_boot_crash_report"]:
print(f" [next-boot] CRASH REPORT:\n{result['next_boot_crash_report'][:600]}")
else:
print(f" [next-boot] NO CRASH REPORT / LAST OP line found in boot output")
except Exception as e2:
result["error"] += f" | next-boot drain failed: {e2}"
print(f" !! next-boot drain failed: {e2}")
finally:
try:
ser2.close()
except Exception:
pass
return result
# After a successful verb, drain any trailing log output.
drain(ser, "TRAILING OUTPUT", seconds=1.0)
finally:
try:
ser.close()
except Exception:
pass
return result
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--port", default=DEFAULT_PORT)
ap.add_argument("--only", default=None,
help="substring filter on case name; run only matching cases")
args = ap.parse_args()
cases = TESTS
if args.only:
cases = [c for c in TESTS if args.only.lower() in c["name"].lower()]
if not cases:
print(f"No cases match --only {args.only!r}")
return 1
print(f"Running {len(cases)} PQ profiling case(s) on {args.port}.")
print("Each case reflashes the firmware first, so each verb runs on a fresh boot.")
results = []
for case in cases:
r = run_one_case(args.port, case)
results.append(r)
print(f"\n{'='*70}")
print("SUMMARY")
print(f"{'='*70}")
for r in results:
line = f" {r['case']:30s} -> {r['outcome']}"
if r["next_boot_last_op"]:
l = r["next_boot_last_op"]
line += (f" | last_op={l['op_name']} seq={l['seq']} "
f"stack_hw_free={l['stack_hw_free']} "
f"heap_free_at_crash={l['heap_free_at_crash']}")
elif r["last_op_before_crash"]:
l = r["last_op_before_crash"]
line += (f" | boot_last_op={l['op_name']} seq={l['seq']} "
f"stack_hw_free={l['stack_hw_free']} "
f"heap_free_at_crash={l['heap_free_at_crash']}")
if r["error"]:
line += f" | err={r['error'][:80]}"
print(line)
with open("firmware/teensy41/pq_profile_results.json", "w") as f:
json.dump(results, f, indent=2)
print(f"\nFull results written to firmware/teensy41/pq_profile_results.json")
return 0
if __name__ == "__main__":
sys.exit(main())
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""Single-verb PQ test on a fresh boot.
Usage:
python3 firmware/teensy41/test_pq_single.py --port /dev/ttyACM0 --verb kem-keygen
python3 firmware/teensy41/test_pq_single.py --port /dev/ttyACM0 --verb dsa-sign
python3 firmware/teensy41/test_pq_single.py --port /dev/ttyACM0 --read-boot
"""
import serial, struct, json, time, sys, argparse
DEFAULT_PORT = "/dev/ttyACM0"
BAUD = 115200
def send_request(ser, req, timeout=180.0):
payload = json.dumps(req).encode("utf-8")
ser.write(struct.pack(">I", len(payload)) + payload)
ser.flush()
h = b""
deadline = time.time() + timeout
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 and time.time() < deadline:
c = ser.read(n - len(p))
if c:
p += c
else:
time.sleep(0.01)
if len(p) < n:
raise TimeoutError("body timeout")
return json.loads(p.decode())
def drain_boot(ser, wait=6.0):
time.sleep(wait)
boot = b""
end = time.time() + 2.0
while time.time() < end:
if ser.in_waiting:
boot += ser.read(ser.in_waiting)
else:
time.sleep(0.05)
return boot.decode("utf-8", errors="replace")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--port", default=DEFAULT_PORT)
ap.add_argument("--verb", choices=["kem-keygen", "dsa-sign"])
ap.add_argument("--read-boot", action="store_true")
ap.add_argument("--timeout", type=float, default=180.0)
args = ap.parse_args()
ser = serial.Serial(args.port, BAUD, timeout=2.0)
boot = drain_boot(ser)
print("=== BOOT OUTPUT ===")
print(boot)
print("=== END BOOT ===", flush=True)
if args.read_boot:
ser.close()
return 0
if args.verb == "kem-keygen":
req = {"jsonrpc": "2.0", "id": 1, "method": "get_public_key",
"params": [{"algorithm": "ml-kem-768", "index": 0}]}
print(f"-> get_public_key ml-kem-768", flush=True)
try:
resp = send_request(ser, req, timeout=args.timeout)
ok = "result" in resp
print(f"<- {'OK' if ok else 'ERR'} {str(resp)[:300]}", flush=True)
ser.close()
return 0 if ok else 1
except Exception as e:
print(f"!! CRASH/TIMEOUT: {e}", flush=True)
ser.close()
return 2
if args.verb == "dsa-sign":
req = {"jsonrpc": "2.0", "id": 1, "method": "sign",
"params": ["746573742065643235353139206d657373616765",
{"algorithm": "ml-dsa-65", "index": 0}]}
print(f"-> sign ml-dsa-65", flush=True)
try:
resp = send_request(ser, req, timeout=args.timeout)
ok = "result" in resp
print(f"<- {'OK' if ok else 'ERR'} {str(resp)[:300]}", flush=True)
ser.close()
return 0 if ok else 1
except Exception as e:
print(f"!! HANG/TIMEOUT: {e}", flush=True)
ser.close()
return 2
if __name__ == "__main__":
sys.exit(main())
+2 -2
View File
@@ -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 3
#define NSIGNER_VERSION "v0.1.3"
#define NSIGNER_VERSION_PATCH 4
#define NSIGNER_VERSION "v0.1.4"
/* NSIGNER_HEADERLESS_DECLS_END */