Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 32 additions & 11 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -349,16 +349,23 @@ Five latent bugs and three new ones were cleared to get here:
regression — the target has never built cleanly — but should be
fixed before P-384 is actually wired into the TLS path. TLS-level
P-384 verify remains stubbed regardless (see `project_p384_stubbed`).
- `ecdsa_verify` (P-256) rejects a known-good signature. Surfaced
2026-05-06 by `tools/test_x509.py` group 3 subtest 3c
("ECDSA verify: valid signature (C=0)") on macOS once the BSD-sed
portability fixes unblocked the build. The C64 returns C=1 (invalid)
after ~60 s on a Python-pre-verified valid P-256 signature; tampered
signatures (subtest 3d) are correctly rejected, and all five input
buffers (hash, r, s, Qx, Qy) were confirmed Match=True in C64 memory
before the call, so the bug is in the verify path, not input
staging. Used by TLS handshake CertificateVerify; ECDHE/X25519
handshake is unaffected.
- **VICE harness gotcha**: any test that exercises sibling
`libs/nistcurves` P-256 primitives (`fp_mul`, `fp_inv`,
`ec_scalar_mul_var`, `ecdsa_verify_256`, ...) MUST launch VICE with
`-reu`. The sibling's `fp_mul` fetches 8x8 multiply rows from REU
banks 0/1 (populated by `src/boot.s::reu_mul_init`). Without `-reu`,
the row fetch silently no-ops and `mul_dma_lo/hi` at $BA00/$BB00
stays stuck at `reu_mul_init`'s final-iteration residue (a=255), so
every `fp_mul` returns `a*255*b mod p` instead of `a*b mod p`.
Cascade: wrong `w=s^-1 mod n`, wrong `u1`/`u2`, wrong computed `R`,
`R.x != r`, verify returns C=1. Pattern is fixed in
`tools/test_x509.py:769` and `tools/test_ecdsa_kat_oracle.py:293`;
mirror this in any new VICE-driven test for P-256:
`ViceConfig(..., extra_args=["-reu", "-reusize", "512"])`. The same
pattern is already in `tools/test_x25519.py:722`,
`tools/bench_x25519.py:138`, `tools/test_p384_symbols.py:370`. The
UCI path is unaffected because the U64E hardware has REU enabled by
default; the symptom was VICE-only.

### ECDSA P-256 verify wall-clock

Expand All@@ -374,7 +381,21 @@ state-machine overhead.

81.9 s still does not fit a typical 10-30 s real-world server
handshake window, so this is a blocker for arbitrary internet TLS
targets that require ECDSA-P256 CertificateVerify.
targets that require ECDSA-P256 CertificateVerify. Note: the earlier
"`ecdsa_verify` rejects a known-good signature" entry in this section
turned out to be a VICE harness misconfiguration (missing `-reu`), not
a verify-path bug — see "VICE harness gotcha" in the Known issues
list. With `-reu` enabled, `tools/test_x509.py` 3c PASSes cleanly in
~60 s wall-clock under VICE warp.

Under the v0.2.0 submodule pin the U64E 48 MHz handshake measures
**86.7 s** end-to-end (re-measured 2026-05-12, `tools/uci/test_https_local`,
local listener). The +4.8 s vs the 81.9 s v0.1.0-10-gdfdfb59 baseline
above is attributable to v0.2.0's defensive REU register inits at
`fp_mul`/`fp_sqr`/`ec_scalar_mul_var`/`fp_inv`/`ecdsa_verify_256` proc
entry (release notes "Security/correctness defences" — +6 cy/call;
the wall-clock impact compounds across the tens of thousands of
field-mul/sqr calls in the scalar mult).
It is fine for the local listener used by the e2e harness (600 s
budget, ample headroom). Further speedups live in the sibling
`libs/nistcurves` repo — any drop through the Crypto ABI lands
Expand Down
2 changes: 1 addition & 1 deletion libs/nistcurves
39 changes: 32 additions & 7 deletions tools/integration/build_nistcurves_p256.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,7 +102,7 @@ cp "$LIB_SRC"/ecdsa256.s "$STAGING/ecdsa256_raw.s"
# helpers + anchor base-address table + cm_* / sm256_* state vars
# Keeps ec_point_double (line 60-410), ec_point_add (411-761),
# ec_scalar_mul_var (1459-1609), ec_jacobian_to_affine (1610-end).
sed -i '' '762,1458d' "$STAGING/points256_raw.s"
sed -i '' '762,1467d' "$STAGING/points256_raw.s"

# Strip exports + imports that only the removed bodies used.
sed -i '' '/^\.export ec_precompute_256, ec_scalar_mul, ec_scalar_mul_var$/c\
Expand All@@ -114,10 +114,16 @@ sed -i '' '/^\.import ec_anchor[1-8]_y, ec_anchor[1-8]_y, ec_anchor[1-8]_y, ec_a
sed -i '' '/^\.import ec_anchor.*$/d' "$STAGING/points256_raw.s"
sed -i '' '/^\.import cm_k, mul_dma_lo$/d' "$STAGING/points256_raw.s"
sed -i '' '/^\.import ec_sc_byte, ec_sc_mask$/d' "$STAGING/points256_raw.s"
# REU DMA register imports (only used by stripped REU anchor helpers)
sed -i '' '/^\.import reu_c64_lo, reu_c64_hi, reu_reu_lo, reu_reu_hi$/d' "$STAGING/points256_raw.s"
# REU DMA register imports. v0.2.0 added a "defensive REU register init"
# block at the top of ec_scalar_mul_var (lines 753-757) that touches
# reu_reu_lo + reu_addr_ctrl, so those two must stay imported even though
# ec_scalar_mul_var is the only retained body. The rest are only used by
# the stripped REU anchor helpers and Lim-Lee comb.
sed -i '' '/^\.import reu_c64_lo, reu_c64_hi, reu_reu_lo, reu_reu_hi$/c\
.import reu_reu_lo' "$STAGING/points256_raw.s"
sed -i '' '/^\.import reu_reu_bank, reu_len_lo, reu_len_hi$/d' "$STAGING/points256_raw.s"
sed -i '' '/^\.import reu_addr_ctrl, reu_command$/d' "$STAGING/points256_raw.s"
sed -i '' '/^\.import reu_addr_ctrl, reu_command$/c\
.import reu_addr_ctrl' "$STAGING/points256_raw.s"
# ec_mulp / ec_sqrp are used by all three retained bodies - keep.
# fp_tmp1 is used by ec_scalar_mul_var - keep.

Expand DownExpand Up@@ -288,6 +294,24 @@ ecdsa_u1g_y: .res 32, 0 ; LE affine Y of u1*G
fp_rev_buf: .res 32, 0
DATA_EOF

# --- Emit minimal REU register equates ---
# v0.2.0 added a "defensive REU register init" block at the top of
# ec_scalar_mul_var (and also in fp256/ecdsa256 modular-inverse paths)
# that touches reu_reu_lo + reu_addr_ctrl. The sibling's constants.s
# provides these but also exports VIC/CIA/KERNAL equates that would
# collide with c64-https's in-tree definitions, so we emit a minimal
# equate file with only what the retained bodies actually reference.
cat > "$STAGING/reu_equates_raw.s" <<'REU_EOF'
.setcpu "6502"

; Minimal REU hardware register equates used by retained P-256 bodies
; in v0.2.0 (defensive REU register init in ec_scalar_mul_var, fp_inv,
; ecdsa inverse). Mirror of values in libs/nistcurves/src/constants.s.
.export reu_reu_lo, reu_addr_ctrl
reu_reu_lo = $df04
reu_addr_ctrl = $df0a
REU_EOF

# --- Route CODE segments in the raw .s files to CRYPTO_CODE. ---
# The sibling uses `.segment "CODE"`, which under c64-https's cfg is the
# LOADER region ($0801-$1FFF). We want this code in CRYPTO_RESIDENT.
Expand DownExpand Up@@ -324,7 +348,7 @@ mkdir -p "$OBJ_DIR" "$OUT_DIR"
"${ZP_DEFINES[@]}" \
-o "$OBJ_DIR/zp_config.o" "$STAGING/zp_config.s"

for src in fp256_raw mod256_raw points256_raw ecdsa256_raw curve256_raw data_p256_raw; do
for src in fp256_raw mod256_raw points256_raw ecdsa256_raw curve256_raw data_p256_raw reu_equates_raw; do
"$CA65" \
-I "$STAGING" \
-I "$PROJECT_ROOT/src/crypto/shared" \
Expand All@@ -340,12 +364,13 @@ rm -f "$ARCHIVE"
"$OBJ_DIR/points256_raw.o" \
"$OBJ_DIR/ecdsa256_raw.o" \
"$OBJ_DIR/curve256_raw.o" \
"$OBJ_DIR/data_p256_raw.o"
"$OBJ_DIR/data_p256_raw.o" \
"$OBJ_DIR/reu_equates_raw.o"

# --- Per-source byte counts ---
{
echo "# nistcurves-p256.a per-source byte counts (ca65 .o file sizes)"
for src in zp_config fp256_raw mod256_raw points256_raw ecdsa256_raw curve256_raw data_p256_raw; do
for src in zp_config fp256_raw mod256_raw points256_raw ecdsa256_raw curve256_raw data_p256_raw reu_equates_raw; do
bytes=$(wc -c < "$OBJ_DIR/$src.o")
printf '%-24s %d bytes (.o)\n' "$src" "$bytes"
done
Expand Down
Loading