Skip to content

fix(crypto): ECDSA-P256 verify "false rejection" + nistcurves v0.2.0 bump - #40

Merged
JC-000 merged 3 commits into
masterfrom
feat/ecdsa-verify-fix-nistcurves-v0.2.0
May 12, 2026
Merged

fix(crypto): ECDSA-P256 verify "false rejection" + nistcurves v0.2.0 bump#40
JC-000 merged 3 commits into
masterfrom
feat/ecdsa-verify-fix-nistcurves-v0.2.0

Conversation

@JC-000

Copy link
Copy Markdown
Owner

Summary

  • Root cause of the long-standing "ecdsa_verify (P-256) rejects a known-good signature" issue: tools/test_x509.py was launching VICE without -reu. The sibling libs/nistcurvesfp_mul fetches 8×8 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. Cascade: wrong w=s⁻¹, wrong u1/u2, wrong computed R, comparison fails, verify returns C=1. Tampered (3d) also rejects but for a different reason — outside view is identical. Masqueraded as a crypto bug for ~6 days.
  • Bump libs/nistcurves to v0.2.0 (released 2026-05-12). Picks up the upstream-recommended baseline including its defensive REU register-residue guard ($DF04=0/$DF0A=0 re-established at 10 public REU-DMA entry points, +6 cy/call). Static audit confirmed no live REU polluter in c64-https today, so the defence is cheap insurance vs. future composed-system regressions rather than a fix for an observed bug.
  • Add a CAVP SigVer KAT oracle (tools/test_ecdsa_verify.pytools/test_ecdsa_kat_oracle.py) — 3 NIST P-256/SHA-256 Result=P vectors driven against the resident ecdsa_verify entry, independent of test_x509.py's single internally-generated vector. This is what definitively localized the bug during investigation (all 4 vectors failed identically, ruling out vector-specific edge cases).

The UCI/U64E path was never affected — hardware REU is always present. Symptom was VICE-only.

Test plan

  • VICE — tools/test_x509.py: 11/11 PASS. Group 3 ECDSA verify 4/4, including [3c] valid signature C=0 [60s]. Previously 10/11 with [3c] C=1 [60s] FAIL.
  • VICE — tools/test_ecdsa_kat_oracle.py: 3/3 CAVP Result=P vectors PASS at ~60 s each under VICE warp.
  • U64E @ 48 MHz turbo — tools/uci/test_https_local.py: full TLS 1.3 handshake + HTTP 200 + body decoded; http_resp_buf contains HELLO FROM TLS SERVER. Wall-clock 86.7 s vs the pre-bump 81.9 s baseline (+4.8 s attributable to v0.2.0's defensive REU register inits compounded across the field-mul/sqr workload — release notes "Security/correctness defences", +6 cy/call).
  • Submodule v0.2.0 build: required two adjustments to tools/integration/build_nistcurves_p256.sh — strip range 762,1458d → 762,1467d (v0.2.0 inserts +9 lines in the stripped Lim-Lee block), and two .import statements (reu_reu_lo, reu_addr_ctrl) preserved via sed c rewrites + a new minimal reu_equates_raw.s staging file. v0.2.0's defensive REU-init in ec_scalar_mul_var/fp_inv/ecdsa256 requires these.
  • No regression: DER parser P-256/P-384 (Groups 1+2) still 7/7; ECDSA boundary tests [3a]/[3b] still PASS instantly; tampered [3d] still PASS-by-rejection.

Documentation

CLAUDE.md "Known issues" entry replaced with a "VICE harness gotcha" documenting the cause, the cascade, the four ViceConfig sites that need extra_args=["-reu", "-reusize", "512"], and a note that the same pattern is already correct in tools/test_x25519.py:722, tools/bench_x25519.py:138, tools/test_p384_symbols.py:370. The ECDSA wall-clock section gets a paragraph noting the new 86.7 s measurement vs the recorded 81.9 s pre-bump baseline.

🤖 Generated with Claude Code

JC-000and others added 3 commits May 12, 2026 16:12
…ejection"
The "ecdsa_verify (P-256) rejects a known-good signature" known issue
in CLAUDE.md was never a crypto bug. tools/test_x509.py launched VICE
with `ViceConfig(prg_path=PRG_PATH, warp=True, ntsc=True, sound=False)`
— no REU. The sibling c64-nist-curves 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. Tampered (3d) also rejects but for a different downstream
reason — outside view is identical.
Pattern verified by primitive-level KAT bisection: under default cfg,
fp_mod_mul + fp_mod_mul_n produce the unmistakable a*255*b signature
on 5/5 random inputs. With extra_args=["-reu","-reusize","512"] both
primitives pass 5/5. test_x509 group 3 then passes 4/4 — including
[3c] valid signature C=0 in ~60 s wall-clock.
The same pattern is already correct in tools/test_x25519.py:722,
tools/bench_x25519.py:138, tools/test_p384_symbols.py:370 — converge
test_x509 on it. The UCI/U64E path is unaffected (hardware REU is
always enabled); the symptom was VICE-only. Verified on U64E @ 48 MHz:
test_https_local PASS 86.7 s (full handshake + HTTP 200 + body OK).
CLAUDE.md "Known issues" entry replaced with a "VICE harness gotcha"
documenting the cause + the four ViceConfig sites that need the flag.
The ECDSA wall-clock paragraph gets a tail note redirecting readers
from the now-stale earlier entry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e build
Pull in the upstream-recommended baseline (released 2026-05-12). v0.2.0
ships a defensive REU register-residue guard (issue #33-class fix
ported from c64-x25519 commit 817f525) that re-establishes $DF04=0 and
$DF0A=0 at 10 public REU-DMA entry points before touching DMA. Of
those, the ones that land in c64-https's retained carve-out are
fp_mul, fp_sqr, ec_scalar_mul_var, ecdsa_verify_256, and the modular
inverse paths in fp256/ecdsa256. The fifth (ec_scalar_mul) is inside
our stripped Lim-Lee block. Static audit (researcher pass) confirmed
no live REU polluter in c64-https today, so the defence is a cheap
insurance belt against future composed-system regressions, not a fix
for an observed bug.
Build-script changes to support the bump:
- Strip line range 762,1458d -> 762,1467d. v0.2.0 inserts +9 lines
of new defensive REU-init code inside the ec_scalar_mul Lim-Lee
body we strip (which still falls entirely inside the new range).
- Two `.import` lines for `reu_reu_lo`/`reu_addr_ctrl` now need to
survive the strip because the v0.2.0 defensive init block at the
top of ec_scalar_mul_var references them. Converted the relevant
sed `d` deletions to `c` rewrites that preserve only those two
symbols out of the original 8-symbol import lists.
- New `reu_equates_raw.s` staging file supplies the two symbol
equates (reu_reu_lo = $df04, reu_addr_ctrl = $df0a). Pulled out
of sibling's constants.s minimally to avoid colliding with our
in-tree VIC/CIA/KERNAL equates. Wired into the ca65 build loop
and ar65 archive list.
Verified VICE: tools/test_x509.py 11/11 (Group 3 ECDSA verify 4/4
incl. [3c] valid sig C=0 in ~60 s warp). Verified hardware: U64E @
48 MHz tools/uci/test_https_local PASS, full handshake 86.7 s vs the
81.9 s v0.1.0-10-gdfdfb59 baseline — the +4.8 s is the +6 cy/call
defence overhead compounded across the field-mul/sqr workload (release
notes "Security/correctness defences"). Within margin; trade is a
clear net win.
CLAUDE.md ECDSA wall-clock section gets a paragraph noting the new
86.7 s measurement vs the recorded 81.9 s baseline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Standalone VICE harness that drives the resident `ecdsa_verify` entry
with NIST CAVP P-256/SHA-256 SigVer Result=P vectors (sourced from
libs/nistcurves/tools/vectors/nist_p256_sigver.rsp). Three valid
signatures are run end-to-end and asserted to return C=0 in addition
to the single test_x509.py group 3 [3c] vector — gives us independent
regression coverage against silent breakage of the integration path
(staging buffers, the ec_scalar_mul shim, the BE-struct pack, the
boot-time REU mul-table state) that would still let test_x509's
single vector pass.
This script is what definitively localized the recent "false-rejection"
investigation: the 3 CAVP vectors all failed identically alongside
[3c] under the missing-`-reu` VICE config, ruling out vector-specific
edge cases (high-bit on r/s, leading-zero patterns, etc.) and pointing
at a systemic integration issue. With the -reu flag now correct in
both this and test_x509, all 4 vectors pass.
Run: C64_SKIP_BUILD=1 PYTHON tools/test_ecdsa_kat_oracle.py
Output: per-vector PASS/FAIL with wall-clock + r/s/h/Qx/Qy hex
prefixes for any failure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@JC-000
JC-000 merged commit be57811 into masterMay 12, 2026
JC-000 added a commit that referenced this pull request May 21, 2026
…19 v0.6.0 (#55)
Aligns c64-https with c64-lib-contract v0.2.0 (published 2026-05-20) and
the new library releases:
* libs/nistcurves: 90830c9 -> v0.3.0 (post-#40 minimal-archive build
targets, SPEC s1-s6 + s8.1 adopted, LIB_ABI_VERSION exported)
* libs/x25519: 47c0ad21 (v0.4.0) -> v0.6.0 (SPEC s1-s5 + s8.1 adopted,
REU bank-2 drop, RAM-reclaim, lib-x25519-1764 minimal variant)
c64-https consumer-side changes:
* UCI cfg restructure (W1): split monolithic 24 KB CRYPTO_RESIDENT into
16 KB CRYPTO_HOT (code+rodata, file-backed) + 8 KB CRYPTO_COLD_SHADOW
($A000-$BFFF, BSS under banked-off BASIC ROM). Routes new
LIB_NISTCURVES_* segments per their cold/hot profile. Closes the
segment-straddles-$A000 boot-zero-wipe regression that caused
Test 3a to time out at 30 s.
* Integration scripts collapse: build_nistcurves_p{256,384}.sh shed
~636 lines of sed-strip shell in favor of make -C libs/nistcurves
lib-p{256,384}-verify. build_x25519.sh adapted to consume v0.6.0's
.exportzp ZP config + REU bank --asm-define (plus SPEC s8.1
LIB_SHARED_SQTAB_BASE=$BC00 + SHARED_SQTAB_INIT=1).
build_nistcurves_p256_bin.sh new -- builds the P-256 verify overlay
.bin for EMBED_P256_OVERLAY=1 path.
* Overlay-swap infrastructure: src/crypto/shared/crypto_swap.s extended
with crypto_swap_to_x25519, crypto_swap_to_p256_verify,
crypto_overlay_call. New overlay_ids.inc for canonical OV_*
equates. src/boot.s reu_p384_overlay_init extended to stash
X25519 + P-256 verify images. New entry points are unused in
default build (W1's hot path stays unchanged) -- they're plumbing
for future cold-path routing.
* BSS_TAIL segment: small new segment for src/data.s::tls_rec_buf
(548 B) + src/der_decode.s::cert_buf (1536 B) routed where each
backend has room.
Verified:
* make BACKEND=uci (two-pass bootstrap): 62,977 B PRG (byte-identical
to pre-bump baseline)
* tools/test_x509.py: 11/11 PASS
* tools/uci/test_https_local.py (U64E hw): PASS at 82.1 s
(vs 81.8 s CLAUDE.md baseline -- +0.3 s noise)
Known limitations (tracked):
* BACKEND=ip65 build: 1662 B over in CRYPTO_COLD_SHADOW. Blocked on
c64-nist-curves#54 (lib-p256-verify BSS slim -- would close ~261 B)
+ likely src-side cert-parse streaming or further library asks.
* make BACKEND=uci USE_X25519_SIBLING=1: 364 B over in CRYPTO_HOT
(was 415 B pre-bump; x25519 v0.6.0 reclaimed 51 B). Will likely
close with c64-nist-curves#54.
* Default UCI build is the production target; both above paths are
non-default flags.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@JC-000