From f87d76dfcebd065c8427b794f46bbe21b0cb2ffb Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Fri, 14 Aug 2026 09:29:27 -0500 Subject: [PATCH 1/2] chore(libs): bump nistcurves v0.6.0 -> v0.9.1; both backends link, cfgs untouched MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Takes the P-256 ECDSA verify library to upstream latest. The headline reason is security: v0.7.0 added a FIPS 186-5 §3.3 public-key validation gate (Qx,Qy in [0,p-1] plus on-curve Qy^2 = Qx^3 - 3Qx + b mod p, C=1 before any scalar multiplication). c64-https hands ecdsa_verify_256 a Q parsed straight out of an attacker-supplied certificate (src/tls_cert.s -> ecdsa_pubkey_x/y) and performs no range or on-curve check of its own, so this closes a real gap on our side. No cfg change. The v0.7.0/v0.8.0 CRYPTO_HOT overflow is gone: segment v0.6.0 v0.9.1 delta LIB_..._P256_CODE $1FB4 (8116) $2084 (8324) +208 FIPS gate LIB_..._P256_RODATA $01E0 (480) $00C0 (192) -288 dead vectors LIB_..._P256_BSS $0520 (1312) $0520 (1312) 0 UCI CRYPTO_HOT last byte used: $9FFE (1 B free) -> $9FAE (81 B free) The +208 reconciles the recorded failure exactly (1 - 208 = -207, the "overflows CRYPTO_HOT by 207 bytes" in CLAUDE.md); v0.9.0's deletion of 288 B of dead RFC 6979 self-test vectors from curve256.o covers it. The shelved remedy of routing LIB_NISTCURVES_P256_RODATA into CRYPTO_OVERLAY is therefore NOT adopted: the slot stays free for the three flags that contend for it, and tools/uci/_memory_policy.py sees an unchanged $4200-$5FFF. Fixes a latent silent-corruption trap in our own wrapper. v0.9.0 gave each archive a per-variant zp_config member, so nistcurves-p256-verify.a now ships zp_config_p256verify.o. build_nistcurves_p256.sh rebuilt a hardcoded zp_config.o with c64-https's ZP overrides and re-archived strictly by `ar65 t upstream.a`, so the override object would have been dropped without a word and the upstream default archived instead -- restoring zp_ptr2 = $fd (collides with zp_temp/zp_count during cert parsing) and fp_mul_i/j = $2c/$2d (inside the fe25519 claim $2c-$37). No link error; runtime corruption. Both wrappers now discover the member name, apply the matching upstream -D variant gate, hard-error on an absent/ambiguous/unknown name, and post-check the emitted object with od65. Adds a link-time version floor (.assert LIB_NISTCURVES_VERSION_MINOR >= 9) in src/crypto/ecdsa_verify.s so a silent downgrade below the validation gate is a loud ld65 error rather than a quietly reopened gap -- our KAT vectors all carry well-formed public keys, so the tests would not notice. Negative-tested: flipping it to >= 10 fails the link with no PRG. Costs zero PRG bytes and, from v0.9.0, zero ld65 warnings (the version equates export :abs). ABI-break audit, measured rather than inferred. v0.9.0 bumps LIB_ABI_VERSION 0 -> 1 and removes exports. Diffing every export of the staged archive at both pins against every import of c64-https's own objects: 31 symbols left the archive, and the intersection with what we import is EMPTY. Our entire consumed surface is four symbols -- ec_base_x, ec_gx256, ec_scalar_mul_var, ecdsa_verify_256. The removed zero-page slots are doubly moot: c64-https contains no .importzp directives at all and defines every slot locally. Also keeps `make p384-overlay` at its pre-existing blocker. The bump initially moved that failure earlier (the same per-variant manifest rename); with the member-discovery fix the chain again stops exactly at LIB_NISTCURVES_SHA384_- TABLES overflowing OVERLAY_REGION by 1536 bytes, unchanged. Evidence: make clean && make ip65 47,105 B d522e68469b8fc09709f34e64fd5573a0a72465349374a26257194bd5ed96cca make clean && make BACKEND=uci UCI 62,977 B 66e37037deb9b295d3594a50b2bacfe95d09bb7e62f3dfc401c51b03c2461fed tools/test_ecdsa_kat_oracle.py 6/6 tools/test_x509.py 11/11 tools/test_x25519.py 73/73 (control, unmoved) Plus the three non-default profiles link: uci+onchip, ip65+onchip, uci+onchip-comb. Not measured: wall-clock. Every timing figure in CLAUDE.md was taken at the v0.6.0 pin and is now labelled as such. Expected drift is a small regression (the gate adds 2 fp_cmp + 3 mod-p muls + 4 add/subs per verify, noise against a multi-second scalar mul); nothing else in v0.7.0-v0.9.1 touches a hot path. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 138 ++++++++++++++++----- libs/nistcurves | 2 +- src/crypto/ecdsa_verify.s | 23 ++++ tools/integration/build_nistcurves_p256.sh | 65 ++++++++-- tools/integration/build_nistcurves_p384.sh | 96 +++++++++++--- 5 files changed, 271 insertions(+), 53 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1f400f4..aa493c4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -194,7 +194,7 @@ buffers in the crypto BSS — see per-module headers for details): SHA-256 (in-tree; no sibling) sha256_init, sha256_update, sha256_final - ECDSA P-256 (`libs/nistcurves@v0.6.0` sibling, + ECDSA P-256 (`libs/nistcurves@v0.9.1` sibling, c64-lib-contract SPEC §1-§8.1 aligned) ecdsa_verify_256 — TLS dispatcher in src/crypto/ecdsa_verify.s packs the BE struct + calls the sibling entry @@ -203,11 +203,26 @@ buffers in the crypto BSS — see per-module headers for details): archive built via `make -C libs/nistcurves lib-p256-verify` — see `tools/integration/build_nistcurves_p256.sh` for the wrapper.) + **From v0.7.0 the sibling validates the public key at entry** + (FIPS 186-5 §3.3: `Qx,Qy ∈ [0,p-1]` plus on-curve + `Qy² ≡ Qx³ − 3Qx + b (mod p)`, C=1 returned before any scalar + multiplication). This matters to c64-https specifically: the `Q` + handed to `ecdsa_verify_256` comes straight from an + attacker-supplied certificate via `src/tls_cert.s` → + `ecdsa_pubkey_x/y`, and c64-https performs no range or on-curve + check of its own. `src/crypto/ecdsa_verify.s` therefore carries a + link-time `.assert LIB_NISTCURVES_VERSION_MINOR >= 9` so a silent + downgrade below that pin is a loud `ld65: Error` rather than a + quietly reopened gap — every KAT vector we own has a well-formed + public key, so the tests would not notice. The assert costs zero + PRG bytes (hashes identical with and without it) and, from v0.9.0, + zero ld65 warnings, since the version equates are exported `:abs`. + P-384 is *stubbed at the TLS layer* (see `project_p384_stubbed` memory note). The sibling `libs/nistcurves` P-384 primitives were meant to be buildable as an external overlay image (Phase C.3b, `make -p384-overlay`) but every P-384 build target is broken at the v0.6.0 -pin — see "Known issues" for the current failure chain. Fix the build +p384-overlay`) but no P-384 build target has ever completed — see +"Known issues" for the current failure chain. Fix the build before wiring P-384 into the TLS path. MEMORY requirements for a drop-in sibling library (see "Memory layout" @@ -782,25 +797,76 @@ Five latent bugs and three new ones were cleared to get here: #32 and #45 were closed as stale; file fresh issues against this chain when P-384 enablement resumes. TLS-level P-384 verify remains stubbed regardless (see `project_p384_stubbed`). - - **A `libs/nistcurves` bump to v0.7.0/v0.8.0 does not link under - UCI** (measured 2026-08-13). `LIB_NISTCURVES_P256_RODATA` - overflows `CRYPTO_HOT` by **207 B**, because CRYPTO_HOT is - already *one byte* from full at the v0.6.0 pin (`build/ - c64-https.map`: rodata `009E1F..009FFE`, region ends `$9FFF`) and - v0.7.0's FIPS 186-5 §3.3 public-key validation gate adds +512 B. - ip65 links fine at v0.8.0, and both `tools/test_ecdsa_kat_oracle.py` - (3/3) and `tools/test_x509.py` (11/11) pass there — so the blocker - is placement, not function. A link-verified remedy exists (route - `cfg/c64-https-uci.cfg` `LIB_NISTCURVES_P256_RODATA` to the - otherwise-unused `CRYPTO_OVERLAY`; v0.8.0 UCI then links at the - same 62,977 B with 7,200 B of the slot still free) but it has had - no hardware run and it changes what `tools/uci/_memory_policy.py` - sees at $4200-$5FFF, so it needs a UCI e2e before it ships. The - bump is worth taking eventually: the gate validates the - attacker-supplied certificate public key that `src/tls_cert.s` - feeds to `ecdsa_verify_256`, which c64-https does not check - itself. No export was renamed or removed in v0.7.0/v0.8.0 — - `LIB_ABI_VERSION` is still 0. + The v0.9.1 bump briefly moved this failure *earlier* — upstream + #90's per-variant manifests renamed `lib_manifest.o` / + `zp_config.o` to `lib_manifest_sha384.o` / `zp_config_p384verify.o` + etc., which the wrapper's hardcoded archive list could not find. + Both P-384 wrappers now **discover** those member names instead, so + the chain again stops exactly at the SHA-384 `OVERLAY_REGION` + overflow above — verified, same 1536 B, at the v0.9.1 pin. + - **Sibling-archive member names are DISCOVERED, never hardcoded — + and this is load-bearing.** Upstream v0.9.0 gave each of its nine + archives its own per-variant `zp_config_*.o` / + `lib_manifest_*.o` / `precalc_manifest_*.o`. + `tools/integration/build_nistcurves_p256.sh` rebuilds the ZP object + with c64-https's slot overrides (`zp_ptr2 = $3d`, + `fp_mul_i = $39`, `fp_mul_j = $3a`) and then re-archives strictly by + `ar65 t upstream.a`. Writing that rebuild to a hardcoded + `zp_config.o`, as it did before the v0.9.1 bump, means the override + object is **silently dropped** and the upstream-default member + archived in its place — restoring `zp_ptr2 = $fd` (collides with + c64-https `zp_temp`/`zp_count` during cert parsing) and + `fp_mul_i/j = $2c/$2d` (inside the fe25519 claim `$2c-$37`). There + is no link error for this; it is runtime memory corruption several + layers from its cause. The wrappers now fail loudly on an absent, + ambiguous or unrecognised member name, apply the matching upstream + `-D` variant gate (it selects which slots get `.exportzp`), and + **post-check the emitted object with `od65`** so the override is + proven rather than assumed. Verify from the link if you ever doubt + it: `build/labels.txt` must show `fp_mul_i=$39`, `fp_mul_j=$3A`, + `zp_ptr2=$3D`. + - **The v0.7.0/v0.8.0 UCI `CRYPTO_HOT` overflow is RESOLVED at + v0.9.1 — and no cfg change was needed.** Recorded here because the + obvious remedy was nearly taken and would have been the wrong + trade. The failure was real: `LIB_NISTCURVES_P256_RODATA` overflowed + `CRYPTO_HOT` by **207 B** at v0.7.0/v0.8.0, because CRYPTO_HOT sat + *one byte* from full at the v0.6.0 pin (rodata `009E1F..009FFE`, + region ends `$9FFF`) and v0.7.0's validation gate adds +208 B of + **code**. 1 − 208 = −207. Exact. + + v0.9.0 then deleted 288 B of dead RFC 6979 self-test vectors from + `curve256.o` (upstream #91) — an object our archive ships — and the + rodata half of the pressure went away: + + segment v0.6.0 v0.9.1 delta + LIB_..._P256_CODE $1FB4 (8116) $2084 (8324) +208 + LIB_..._P256_RODATA $01E0 (480) $00C0 (192) -288 + LIB_..._P256_BSS $0520 (1312) $0520 (1312) 0 + + UCI CRYPTO_HOT last byte used: $9FFE -> $9FAE (**81 B free**) + + So the shelved remedy (routing `LIB_NISTCURVES_P256_RODATA` to + `CRYPTO_OVERLAY`) was **not adopted**: `CRYPTO_OVERLAY` stays + entirely free, the three mutually exclusive flags that contend for + it keep exactly the contention they had, and + `tools/uci/_memory_policy.py` sees an unchanged `$4200-$5FFF`. + Both cfgs are byte-identical across this bump. Note the corollary + for the next bump: **81 B is the whole margin**, and 288 B of it + was a one-off recovery of dead weight that cannot be recovered + twice. + + v0.9.0 is an ABI break upstream (`LIB_ABI_VERSION` 0 → 1, 17 + exports removed). Measured against our tree rather than assumed: + dumping every export of the staged archive at both pins and every + import of c64-https's own objects, **31** symbols left the archive + (upstream's 17 plus 14 more that the new per-variant + `zp_config_p256verify.o` / `precalc_manifest_p256verify.o` narrow + away) and the intersection with what c64-https imports is + **empty**. The whole library surface we consume is four symbols: + `ec_base_x`, `ec_gx256`, `ec_scalar_mul_var`, `ecdsa_verify_256`. + The removed ZP slots could not have bitten us either way — + c64-https contains zero `.importzp` directives and defines every + slot locally in `src/constants.inc` / `src/crypto/shared/zp_canon.inc`. - **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 @@ -848,13 +914,23 @@ list. With `-reu` enabled, `tools/test_x509.py` 3c PASSes cleanly in ~60 s wall-clock under VICE warp. Under the then-current `libs/nistcurves@v0.3.0` pin (post-PR #55, -c64-lib-contract-aligned; the pin is v0.6.0 today) the U64E 48 MHz +c64-lib-contract-aligned) the U64E 48 MHz handshake measured **82.1 s** end-to-end (verified 2026-05-20 against the local listener; the prior v0.2.0 measurement was 86.7 s, and the pre-Phase-C.4 in-tree path was -~110 s). The pin has since moved to **v0.6.0** — the tables below are -the current numbers; the v0.3.0 row is kept only as the REU-profile -baseline. +~110 s). + +**Every wall-clock figure below was measured at the v0.6.0 pin; the +pin is now v0.9.1 and none of them has been re-measured.** Treat them +as the v0.6.0 baseline, not as HEAD. The expected drift is small but +its sign is known: v0.7.0's public-key validation gate adds 2 `fp_cmp` ++ 3 mod-p muls + 4 mod-p add/subs to every verify, which is noise +against a multi-second scalar multiplication, and nothing else in +v0.7.0-v0.9.1 touches a hot path (the rest is manifest equates, dead +data removal and export hygiene). So expect a small *regression*, not +a speedup, and do not quote these rows as v0.9.1 numbers until someone +re-runs `bench_ecdsa_u64e.py` on hardware. The v0.3.0 row is kept only +as the REU-profile baseline. On the **C64 Ultimate** (10.53.21.158, see "C64 Ultimate notes"), measured 2026-07-19 with the INNER=217 fence and boot-at-speed flow: @@ -1315,8 +1391,14 @@ Tight regions (post-W1): commit `7cb59f7` before upstream v0.4.0, so it has been inside our v0.6.0 pin all along and is not available as future headroom. `LIB_NISTCURVES_P256_BSS` measures $0520 (1,312 B) at v0.6.0 and - is unchanged at v0.8.0, so the union's cap is not threatened by a - bump. + is **unchanged through v0.9.1** (re-measured at the bump), so the + union's cap is not threatened. The segment is genuinely + uninitialised — `libs/nistcurves/src/data_p256.s` is 32 `.res` + directives with no `.byte`/`.word` — which is what makes our + `type = bss` declaration safe under c64-lib-contract §4, where a + `rw`→`bss` flip would drop initialised bytes with no ld65 + diagnostic at all. Re-check that if `data_p256.s` ever grows a + literal. - **CRYPTO_OVERLAY** under UCI doubles as P-384 SHA-384/curve overlay paging slot, the W3 P-256 overlay embed slot, AND the USE_X25519_SIBLING=1 X25519 sibling rodata + BSS slot. Mutually diff --git a/libs/nistcurves b/libs/nistcurves index 00d2626..f9701e1 160000 --- a/libs/nistcurves +++ b/libs/nistcurves @@ -1 +1 @@ -Subproject commit 00d2626ceb6b9179c7885b82a6ebcd27f62fd571 +Subproject commit f9701e1d35c664d494139db4cc7d7a3f62bd3c50 diff --git a/src/crypto/ecdsa_verify.s b/src/crypto/ecdsa_verify.s index 641558b..7402598 100644 --- a/src/crypto/ecdsa_verify.s +++ b/src/crypto/ecdsa_verify.s @@ -27,6 +27,29 @@ .import ec_gx256, ec_gy256 .import ec_base_x, ec_base_y +; --- c64-lib-contract §1/§5: sibling version floor, checked at link time --- +; Two things depend on the pin being >= v0.9.0 and NEITHER fails loudly by +; itself, which is why this assert exists rather than a comment: +; +; - v0.7.0's FIPS 186-5 §3.3 public-key validation gate. The Q packed +; below reaches `ecdsa_verify_256` straight out of an attacker-supplied +; certificate (src/tls_cert.s -> ecdsa_pubkey_x/y) and c64-https performs +; no range or on-curve check of its own. Silently dropping back to a +; pre-v0.7.0 archive would reopen that gap with every test still green, +; because our KAT vectors all carry well-formed public keys. +; +; - v0.9.0's per-variant `zp_config_.o`, which +; tools/integration/build_nistcurves_p256.sh locates by name in order to +; re-apply c64-https's ZP overrides. Against an older archive that lookup +; changes shape. +; +; From v0.9.0 the version equates are exported `:abs` (upstream #95/#96), so +; importing them here costs no `ld65: Warning: Address size mismatch`. +.import LIB_NISTCURVES_VERSION_MAJOR +.import LIB_NISTCURVES_VERSION_MINOR +.assert LIB_NISTCURVES_VERSION_MAJOR = 0, lderror, "libs/nistcurves: expected MAJOR 0" +.assert LIB_NISTCURVES_VERSION_MINOR >= 9, lderror, "libs/nistcurves: pin is older than v0.9.0" + ; --- Phase 4a: P-384 TLS dispatcher (src/crypto/ecdsa_verify_384.s) --- .import ecdsa_verify_384_tls diff --git a/tools/integration/build_nistcurves_p256.sh b/tools/integration/build_nistcurves_p256.sh index 0b5b9b3..488179d 100755 --- a/tools/integration/build_nistcurves_p256.sh +++ b/tools/integration/build_nistcurves_p256.sh @@ -11,11 +11,14 @@ # `make -C libs/nistcurves`, then performs adjustments before placing the # result at the location the top-level Makefile expects: # -# 1. Rebuild `zp_config.o` with c64-https's ZP-slot overrides (the upstream -# defaults collide with c64-https's canonical map on three slots: -# zp_ptr2, fp_mul_i, fp_mul_j). The library's zp_config.s `.ifndef`- -# guards every slot, so an override-built version replaces the -# upstream default cleanly. +# 1. Rebuild the archive's `zp_config*.o` member with c64-https's ZP-slot +# overrides (the upstream defaults collide with c64-https's canonical map +# on three slots: zp_ptr2, fp_mul_i, fp_mul_j). The library's zp_config.s +# `.ifndef`-guards every slot, so an override-built version replaces the +# upstream default cleanly. The member name is DISCOVERED from the +# archive, not hardcoded — upstream v0.9.0 gave each archive its own +# per-variant object (`zp_config_p256verify.o`), and a hardcoded name +# would drop the overrides silently. Post-checked with od65. # # 2. Drop `mul_8x8[_onchip].o` (REU profile) and `data_shared.o` (both # profiles) from the archive. c64-https's in-tree @@ -116,15 +119,63 @@ mkdir -p "$STAGING" "$OUT_DIR" cp "$UPSTREAM_ARCHIVE" "$STAGING/upstream.a" (cd "$STAGING" && "$AR65" x upstream.a $( "$AR65" t upstream.a )) -# --- 3. Rebuild zp_config.o with c64-https overrides --- +# --- 3. Rebuild the archive's zp_config member with c64-https overrides --- +# The member NAME is discovered, never hardcoded. Upstream v0.9.0 (issue #90) +# gave each of the nine archives its own per-variant ZP object, so +# `nistcurves-p256-verify.a` ships `zp_config_p256verify.o` and only the FULL +# archive still ships a plain `zp_config.o`. Step 5 re-archives strictly by +# `ar65 t upstream.a`, so writing to a hardcoded `zp_config.o` here would have +# been SILENTLY DROPPED from the output and the upstream-default object +# archived in its place — reinstating exactly the ZP collisions these +# overrides exist to prevent (zp_ptr2 $fd vs c64-https zp_temp/zp_count during +# cert parsing; fp_mul_i/j $2c/$2d inside the fe25519 claim $2c-$37). That is +# memory corruption at runtime, not a link error, so it must fail loudly here. +ZP_MEMBER="" +for m in $( "$AR65" t "$STAGING/upstream.a" ); do + case "$m" in zp_config*.o) + [ -z "$ZP_MEMBER" ] || { echo "ERROR: upstream archive has more than one zp_config member ($ZP_MEMBER, $m) — teach this script which one to override" >&2; exit 1; } + ZP_MEMBER="$m" ;; + esac +done +[ -n "$ZP_MEMBER" ] || { echo "ERROR: no zp_config*.o member in $UPSTREAM_ARCHIVE — upstream layout changed; the c64-https ZP overrides would be silently lost" >&2; exit 1; } + +# The variant gate decides which slots the object .exportzp's (zp_config.s +# lines ~132-141), so it must match what upstream built the member with. +case "$ZP_MEMBER" in + zp_config.o) ZP_VARIANT_DEFINE=() ;; + zp_config_p256verify.o) ZP_VARIANT_DEFINE=('-D' 'LIB_P256_VERIFY_ONLY') ;; + zp_config_p384verify.o) ZP_VARIANT_DEFINE=('-D' 'LIB_P384_VERIFY_ONLY') ;; + zp_config_p384curve.o) ZP_VARIANT_DEFINE=('-D' 'LIB_P384_CURVE_ONLY') ;; + zp_config_sha384.o) ZP_VARIANT_DEFINE=('-D' 'LIB_SHA384_ONLY') ;; + *) echo "ERROR: unrecognised zp_config member '$ZP_MEMBER' — add its upstream -D gate to this case block" >&2; exit 1 ;; +esac + +echo "[p256/$PROFILE] rebuilding $ZP_MEMBER with c64-https ZP overrides..." "$CA65" \ --cpu 6502 \ -g \ -I "$LIB_SRC" \ + "${ZP_VARIANT_DEFINE[@]+"${ZP_VARIANT_DEFINE[@]}"}" \ "${ZP_OVERRIDES[@]}" \ - -o "$STAGING/zp_config.o" \ + -o "$STAGING/$ZP_MEMBER" \ "$LIB_SRC/zp_config.s" +# 3b. Prove the overrides actually landed in the object that gets archived. +# A ZP collision here is silent at link time and only shows up as corrupted +# cert parsing at runtime, so assert the values rather than trusting the -D. +check_zp_slot() { + local name="$1" want="$2" got + got=$("${OD65:-od65}" --dump-exports "$STAGING/$ZP_MEMBER" \ + | awk -v n="\"$name\"" '$1=="Name:" && $2==n {f=1; next} f && $1=="Value:" {gsub(/[()]/,"",$3); print $3; exit}') + if [ "$got" != "$want" ]; then + echo "ERROR: $ZP_MEMBER exports $name = ${got:-}, expected $want (c64-https ZP override did not take)" >&2 + exit 1 + fi +} +check_zp_slot zp_ptr2 61 # $3d +check_zp_slot fp_mul_i 57 # $39 +check_zp_slot fp_mul_j 58 # $3a + # --- 4. Drop conflicting members / rebuild the onchip mul object --- rm -f "$STAGING/mul_8x8.o" "$STAGING/data_shared.o" # 4b. onchip-comb: the full onchip archive carries both curves + SHA-384 + diff --git a/tools/integration/build_nistcurves_p384.sh b/tools/integration/build_nistcurves_p384.sh index b9a043d..6a9eed4 100755 --- a/tools/integration/build_nistcurves_p384.sh +++ b/tools/integration/build_nistcurves_p384.sh @@ -107,18 +107,78 @@ cp "$UPSTREAM_CURVE_ARCHIVE" "$STAGING/curve/upstream.a" (cd "$STAGING/sha" && "$AR65" x upstream.a $( "$AR65" t upstream.a )) (cd "$STAGING/curve" && "$AR65" x upstream.a $( "$AR65" t upstream.a )) -# --- 3. Rebuild zp_config.o with c64-https overrides (shared by both archives) --- -"$CA65" \ - --cpu 6502 \ - -g \ - -I "$LIB_SRC" \ - "${ZP_OVERRIDES[@]}" \ - -o "$STAGING/zp_config.o" \ - "$LIB_SRC/zp_config.s" +# --- 3. Rebuild each archive's zp_config member with c64-https overrides --- +# Member names are DISCOVERED, never hardcoded. Upstream v0.9.0 (issue #90) +# gave every archive its own per-variant manifest + ZP objects, so +# `zp_config.o` / `lib_manifest.o` no longer exist in these archives — +# they are `zp_config_sha384.o` / `lib_manifest_p384verify.o` and friends. +# Hardcoding the old names made this script die at the ar65 staging step. +# +# The variant gate matters twice over: it selects which slots the object +# `.exportzp`s, and the SHA-384 variant deliberately exports only the four +# sha_* slots — so the c64-https overrides below are a legitimate no-op there +# and the post-check must tolerate their absence rather than demand them. +find_member() { + # find_member -> echoes the single matching member + local dir="$1" glob="$2" hit="" m + for m in $( "$AR65" t "$dir/upstream.a" ); do + case "$m" in $glob) + [ -z "$hit" ] || { echo "ERROR: $dir/upstream.a has >1 member matching '$glob' ($hit, $m)" >&2; exit 1; } + hit="$m" ;; + esac + done + [ -n "$hit" ] || { echo "ERROR: no member matching '$glob' in $dir/upstream.a — upstream layout changed" >&2; exit 1; } + echo "$hit" +} + +zp_variant_define() { + case "$1" in + zp_config.o) ;; + zp_config_p256verify.o) echo '-D LIB_P256_VERIFY_ONLY' ;; + zp_config_p384verify.o) echo '-D LIB_P384_VERIFY_ONLY' ;; + zp_config_p384curve.o) echo '-D LIB_P384_CURVE_ONLY' ;; + zp_config_sha384.o) echo '-D LIB_SHA384_ONLY' ;; + *) echo "ERROR: unrecognised zp_config member '$1' — add its upstream -D gate" >&2; exit 1 ;; + esac +} + +# Present ⇒ must carry the c64-https value. Absent ⇒ this variant does not +# export the slot, which is fine. A wrong value is silent memory corruption +# at runtime, never a link error, so it has to be caught here. +check_zp_slot_if_present() { + local obj="$1" name="$2" want="$3" got + got=$("${OD65:-od65}" --dump-exports "$obj" \ + | awk -v n="\"$name\"" '$1=="Name:" && $2==n {f=1; next} f && $1=="Value:" {gsub(/[()]/,"",$3); print $3; exit}') + [ -z "$got" ] && return 0 + if [ "$got" != "$want" ]; then + echo "ERROR: $(basename "$obj") exports $name = $got, expected $want (c64-https ZP override did not take)" >&2 + exit 1 + fi +} + +for tree in sha curve; do + zp_member="$(find_member "$STAGING/$tree" 'zp_config*.o')" + echo "[p384] rebuilding $tree/$zp_member with c64-https ZP overrides..." + # shellcheck disable=SC2046 # deliberate word-split of the -D pair + "$CA65" \ + --cpu 6502 \ + -g \ + -I "$LIB_SRC" \ + $(zp_variant_define "$zp_member") \ + "${ZP_OVERRIDES[@]}" \ + -o "$STAGING/$tree/$zp_member" \ + "$LIB_SRC/zp_config.s" + check_zp_slot_if_present "$STAGING/$tree/$zp_member" zp_ptr2 61 # $3d + check_zp_slot_if_present "$STAGING/$tree/$zp_member" fp_mul_i 57 # $39 + check_zp_slot_if_present "$STAGING/$tree/$zp_member" fp_mul_j 58 # $3a +done -# Distribute the overridden zp_config.o into both staging trees. -cp "$STAGING/zp_config.o" "$STAGING/sha/zp_config.o" -cp "$STAGING/zp_config.o" "$STAGING/curve/zp_config.o" +SHA_ZP="$(find_member "$STAGING/sha" 'zp_config*.o')" +CURVE_ZP="$(find_member "$STAGING/curve" 'zp_config*.o')" +SHA_MANIFEST="$(find_member "$STAGING/sha" 'lib_manifest*.o')" +CURVE_MANIFEST="$(find_member "$STAGING/curve" 'lib_manifest*.o')" +SHA_PRECALC="$(find_member "$STAGING/sha" 'precalc_manifest*.o')" +CURVE_PRECALC="$(find_member "$STAGING/curve" 'precalc_manifest*.o')" # --- 4. Drop conflicting members from the curve archive --- # Same reasoning as P-256: mul_8x8.o + data_shared.o collide with @@ -184,8 +244,9 @@ SHIM_EOF rm -f "$ARCHIVE_SHA" "$AR65" a "$ARCHIVE_SHA" \ "$STAGING/sha/lib_version.o" \ - "$STAGING/sha/lib_manifest.o" \ - "$STAGING/sha/zp_config.o" \ + "$STAGING/sha/$SHA_MANIFEST" \ + "$STAGING/sha/$SHA_PRECALC" \ + "$STAGING/sha/$SHA_ZP" \ "$STAGING/sha/sha384.o" \ "$STAGING/sha/data_sha.o" @@ -195,8 +256,9 @@ rm -f "$ARCHIVE_SHA" rm -f "$ARCHIVE_CURVE" "$AR65" a "$ARCHIVE_CURVE" \ "$STAGING/curve/lib_version.o" \ - "$STAGING/curve/lib_manifest.o" \ - "$STAGING/curve/zp_config.o" \ + "$STAGING/curve/$CURVE_MANIFEST" \ + "$STAGING/curve/$CURVE_PRECALC" \ + "$STAGING/curve/$CURVE_ZP" \ "$STAGING/curve/constants.o" \ "$STAGING/curve/reu_config.o" \ "$STAGING/curve/fp384.o" \ @@ -210,7 +272,7 @@ rm -f "$ARCHIVE_CURVE" # --- 7. Per-source byte counts --- { echo "# nistcurves-p384-sha384.a per-source byte counts (ca65 .o file sizes)" - for src in lib_version lib_manifest zp_config sha384 data_sha; do + for src in lib_version "${SHA_MANIFEST%.o}" "${SHA_PRECALC%.o}" "${SHA_ZP%.o}" sha384 data_sha; do if [ -f "$STAGING/sha/$src.o" ]; then bytes=$(wc -c < "$STAGING/sha/$src.o") printf '%-32s %d bytes (.o)\n' "$src" "$bytes" @@ -220,7 +282,7 @@ rm -f "$ARCHIVE_CURVE" { echo "# nistcurves-p384-curve.a per-source byte counts (ca65 .o file sizes)" - for src in lib_version lib_manifest zp_config constants reu_config \ + for src in lib_version "${CURVE_MANIFEST%.o}" "${CURVE_PRECALC%.o}" "${CURVE_ZP%.o}" constants reu_config \ fp384 mod384 curve384 points384_core ecdsa384_nocomb \ ec_scalar_mul_384_shim data_p384; do if [ -f "$STAGING/curve/$src.o" ]; then From 2adf6108bb264c3ad8b7fce21dbd7e9af33baf26 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Fri, 14 Aug 2026 09:39:09 -0500 Subject: [PATCH 2/2] Merge master into nistcurves bump; flip the ABI gate 0 -> 1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #92's §1 gate asserts `LIB_ABI_VERSION = 0`. nistcurves v0.9.0 bumped it to 1 (the same release that removed 31 exports), so merging the two PRs fires the gate by design: ld65: Error: src/lib_contract_asserts.s(135): libs/nistcurves: exported-surface generation changed (LIB_ABI_VERSION != 0) ... That is the assert working — a named error with file, line and the remedy, rather than a silent surface change. Flipped the expected value to 1 as the message instructs; the assert stays. Safety of the new generation is established by the link itself: an import of any of the 31 removed symbols would be an unresolved external. Both backends link, so we reference none of them. Verified after the flip: ip65 47,105 B sha d522e684…, uci 62,977 B, ECDSA KAT 6/6 including the three negative CAVP vectors. Co-Authored-By: Claude Opus 5 (1M context) --- src/lib_contract_asserts.s | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib_contract_asserts.s b/src/lib_contract_asserts.s index 7f65aee..bd49e5f 100644 --- a/src/lib_contract_asserts.s +++ b/src/lib_contract_asserts.s @@ -132,7 +132,7 @@ APP_OWNED = LIB_SHARED_PRIMITIVES_SQTAB | LIB_SHARED_PRIMITIVES_REU_MUL | LIB_SH ; working: re-check the integration against the new export surface, then ; update the expected value on the next line — do not delete the assert. .import LIB_ABI_VERSION -.assert LIB_ABI_VERSION = 0, lderror, "libs/nistcurves: exported-surface generation changed (LIB_ABI_VERSION != 0) — re-check the integration, then bump the expected value in src/lib_contract_asserts.s" +.assert LIB_ABI_VERSION = 1, lderror, "libs/nistcurves: exported-surface generation changed (LIB_ABI_VERSION != 1) — re-check the integration, then bump the expected value in src/lib_contract_asserts.s" ; =====================================================================