fix(crypto): ECDSA-P256 verify "false rejection" + nistcurves v0.2.0 bump - #40
Merged
Merged
Conversation
…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>
3 tasks
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ecdsa_verify(P-256) rejects a known-good signature" issue:tools/test_x509.pywas launching VICE without-reu. The siblinglibs/nistcurvesfp_mulfetches 8×8 multiply rows from REU banks 0/1 populated bysrc/boot.s::reu_mul_init. Without-reuthe row fetch silently no-ops andmul_dma_lo/hiat $BA00/$BB00 stays stuck atreu_mul_init's final-iteration residue (a=255), so everyfp_mulreturnsa*255*b mod p. Cascade: wrongw=s⁻¹, wrongu1/u2, wrong computedR, 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.libs/nistcurvesto v0.2.0 (released 2026-05-12). Picks up the upstream-recommended baseline including its defensive REU register-residue guard ($DF04=0/$DF0A=0re-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.tools/test_ecdsa_verify.py→tools/test_ecdsa_kat_oracle.py) — 3 NIST P-256/SHA-256 Result=P vectors driven against the residentecdsa_verifyentry, independent oftest_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
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.tools/test_ecdsa_kat_oracle.py: 3/3 CAVP Result=P vectors PASS at ~60 s each under VICE warp.tools/uci/test_https_local.py: full TLS 1.3 handshake + HTTP 200 + body decoded;http_resp_bufcontainsHELLO 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).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.importstatements (reu_reu_lo,reu_addr_ctrl) preserved via sedcrewrites + a new minimalreu_equates_raw.sstaging file. v0.2.0's defensive REU-init inec_scalar_mul_var/fp_inv/ecdsa256requires these.[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
ViceConfigsites that needextra_args=["-reu", "-reusize", "512"], and a note that the same pattern is already correct intools/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