diff --git a/CLAUDE.md b/CLAUDE.md index 3642489..3535f10 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -162,7 +162,7 @@ Variables: (`cfg/c64-https-$(BACKEND).cfg`; default ip65). Changing it requires `make clean` — see above. - `USE_X25519_SIBLING=1` — swap the in-tree X25519 for the - `libs/x25519@v0.11.1` sibling. **UCI links; + `libs/x25519@v0.11.2` sibling. **UCI links; ip65 overflows CRYPTO_OVERLAY** — see "Known issues". Off by default either way. - `EMBED_P256_OVERLAY=1` — stage the P-256 verify image into the @@ -218,7 +218,7 @@ buffers in the crypto BSS — see per-module headers for details): X25519 / field arithmetic Default: in-tree `src/crypto/{x25519,fe25519}.s`. - Opt-in: sibling `libs/x25519@v0.11.1` via `make USE_X25519_SIBLING=1` + Opt-in: sibling `libs/x25519@v0.11.2` via `make USE_X25519_SIBLING=1` — links under UCI from the v0.10.1/v0.11.0 wave pins, still overflows under ip65, and ships in nothing; see Known issues. The v0.6.0 pin is c64-lib-contract-aligned (SPEC §8.1) and adds the @@ -235,7 +235,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.11.0` sibling, + ECDSA P-256 (`libs/nistcurves@v0.11.2` 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 @@ -901,7 +901,7 @@ Five latent bugs and three new ones were cleared to get here: `tools/test_x25519.py` RFC 7748 vector 2 — whose u ends `0x93` — passes 73/73 on the in-tree build.) - The pin is now **v0.11.1**. The v0.6.0 -> v0.10.0 migrations the + The pin is now **v0.11.2**. The v0.6.0 -> v0.10.0 migrations the wrapper had to absorb (the §4 `LIB_X25519_CODE` / `LIB_X25519_INIT_CODE` segment renames, the `x25_x1` buffer added by v0.7.0's #64 fix) are done and documented in @@ -980,6 +980,42 @@ Five latent bugs and three new ones were cleared to get here: 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. + - **§6.1 IS SATISFIED: no archive member is edited.** From the + `libs/nistcurves` **v0.11.2** pin the wrapper requests the §8.0 + APP_OWNED shape through `CONTRACT_DEFINES` (SPEC §6.2) instead of + deleting members: four `SHARED_*` switches, `LIB_NO_BARE_EXPORTS=1` + and `LIB_SHARED_SQTAB_BASE=0xBC00`. The manifest then *attests* the + deferral — `SHARED_PRIMITIVES=$0000`, `SHARED_CONSUMES=$0007` — so + the §8.0 disjointness and coverage asserts are live in + `src/lib_contract_asserts.s`, profile-independently, where they were + previously recorded as "not writable today". + + Three ownership questions moved to c64-https as a result, all in the + direction §8.0 always implied: + - `poly_prod_lo/hi` — we provide the §8.3 `ct_mul_8x8` body, so we + own its product scratch. `poly1305.s` exports them in every + profile now (it used to import under onchip). + - `sqtab_lo/hi` — we provide `sqtab_init`, so we own the table. + `data.s` defines them in every profile; the `sqtab_reserved` + placeholder is gone. The sibling still reads through its own + equates derived from `LIB_SHARED_SQTAB_BASE`, deriving + `sqtab_hi = base+$0200`, so the labels MUST land at + `$BC00`/`$BE00` — the Makefile's post-link check asserts it. + - the ABI import is the prefixed `LIB_NISTCURVES_ABI_VERSION`; the + bare name is suppressed by the export gate. + + **Two upstream fixes made this possible, and the second is the one + that cost a day.** v0.11.1 made `SHARED_CT_MUL_8X8` assemble against + the on-chip TU (c64-nist-curves#123, filed from here) — before that + the wrapper needed a glue TU that `.include`d pristine library + source. v0.11.2 added the **knob-staleness guard**: a changed + `CONTRACT_DEFINES` used to reuse stale objects and exit 0 with a + *different archive than requested*. An attempt at this change before + that guard produced a comb image that would not boot, and the cause + looked like our ownership changes; it was mixed objects. If a + `libs/nistcurves` build ever behaves inexplicably after a define + change, check the pin is >= v0.11.2 before debugging anything else. + - **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` / diff --git a/Makefile b/Makefile index ac68af8..9e49935 100644 --- a/Makefile +++ b/Makefile @@ -345,10 +345,13 @@ $(PRG): $(PRG_DEPS) ifeq ($(USE_NISTCURVES_ONCHIP),1) # Onchip-profile invariant: the sibling's sqtab_lo/hi equates are # BAKED to $$BC00/$$BE00 (LIB_SHARED_SQTAB_BASE in the wrapper). - # data.s's sqtab_reserved placeholder must still land exactly there — - # any TABLES_BSS layout drift silently corrupts every multiply. - @grep -q '^al C:BC00 \.sqtab_reserved' $(LABELS) || \ - { echo 'ERROR: sqtab_reserved is not at $$BC00 — TABLES_BSS layout drifted; realign LIB_SHARED_SQTAB_BASE in tools/integration/build_nistcurves_p256.sh'; exit 1; } + # src/data.s owns sqtab_lo/hi, but the sibling reads the table through + # its OWN equates derived from that base, so the two must agree on the + # address. Drift is neither a link nor a boot failure — just every + # multiply reading the wrong memory. + @grep -q '^al C:BC00 \.sqtab_lo' $(LABELS) || \ + { echo 'ERROR: sqtab_lo is not at $$BC00 — TABLES_BSS layout drifted; realign LIB_SHARED_SQTAB_BASE in tools/integration/build_nistcurves_p256.sh'; \ + grep ' \.sqtab_lo$$' $(LABELS); exit 1; } endif ifeq ($(USE_X25519_SIBLING),1) # Same invariant, other sibling: build_x25519.sh bakes sqtab_lo/hi diff --git a/README.md b/README.md index 0f4bae3..ff42544 100644 --- a/README.md +++ b/README.md @@ -294,9 +294,9 @@ Progress: ### Known Issues -- **The handshake is slow, and the ECDSA P-256 verify dominates it.** Every figure here is quoted from the measurement record in `CLAUDE.md`. Except where noted they were taken at the **`libs/nistcurves` v0.6.0 pin**, and the pin is now v0.11.0, so treat them as a baseline rather than as current. The one profile re-measured at the current pin is comb: 46.986 / 24.440 / 16.402 s verify at 16 / 32 / 48 MHz (U64E, n=3, VIC blanking active). End-to-end handshake + GET against the local listener, U64E, master 2ceb5b1: **80.8 s** (REU profile, 48 MHz), **45.5 s** (onchip profile, 48 MHz), **1,157.7 s** (REU, stock 1 MHz). One point of that sweep has been carried forward: 48 MHz REU measures **82.1 s** at v0.9.1 and **82.4 s** at v0.10.1 (n=1 each, so the 0.4% step between them is noise; the 1.6% from v0.6.0 is the FIPS 186-5 public-key validation gate v0.7.0 added). No other clock or profile has been re-measured. On the REU-less stock-C64 path (ip65 + onchip, no REU, honest 1 MHz in VICE) the whole run measured **2,159.7 s = 36.0 min**, of which the verify stretch alone was 1,416.7 s. That is fine for the local listener, which holds the connection open; it exceeds a typical 10-30 s real-world server handshake window. +- **The handshake is slow, and the ECDSA P-256 verify dominates it.** Every figure here is quoted from the measurement record in `CLAUDE.md`. Except where noted they were taken at the **`libs/nistcurves` v0.6.0 pin**, and the pin is now v0.11.2, so treat them as a baseline rather than as current. The one profile re-measured at the current pin is comb: 46.986 / 24.440 / 16.402 s verify at 16 / 32 / 48 MHz (U64E, n=3, VIC blanking active). End-to-end handshake + GET against the local listener, U64E, master 2ceb5b1: **80.8 s** (REU profile, 48 MHz), **45.5 s** (onchip profile, 48 MHz), **1,157.7 s** (REU, stock 1 MHz). One point of that sweep has been carried forward: 48 MHz REU measures **82.1 s** at v0.9.1 and **82.4 s** at v0.10.1 (n=1 each, so the 0.4% step between them is noise; the 1.6% from v0.6.0 is the FIPS 186-5 public-key validation gate v0.7.0 added). No other clock or profile has been re-measured. On the REU-less stock-C64 path (ip65 + onchip, no REU, honest 1 MHz in VICE) the whole run measured **2,159.7 s = 36.0 min**, of which the verify stretch alone was 1,416.7 s. That is fine for the local listener, which holds the connection open; it exceeds a typical 10-30 s real-world server handshake window. - **P-384** ECDSA is stubbed at the TLS layer. The dispatcher advertises `ecdsa_secp384r1_sha384` (0x0503) and routes to `src/crypto/ecdsa_verify_384.s`, but no P-384 build target completes. Measured 2026-08-14: `make p384-overlay` from a clean tree stops at `No rule to make target 'build/labels.txt'`, and after a main build has produced that file it stops at `Segment 'LIB_NISTCURVES_SHA384_TABLES' overflows memory area 'OVERLAY_REGION' by 1536 bytes`. Cert chains requiring P-384 will not verify. -- **`USE_X25519_SIBLING=1` now links under UCI, and still does not under ip65.** The duplicate-symbol failure this entry used to record — `ld65: Error: Duplicate external identifier: 'reu_mul_tables_init'`, on **both** backends — was closed by the `libs/nistcurves` v0.10.1 / `libs/x25519` v0.11.0 bump plus one line of archive surgery: `tools/integration/build_nistcurves_p256.sh` now also drops `reu_mul_init.o`, the SPEC §8.2 `reu_mul` provider that `src/boot.s` supplies itself. Re-measured at the current v0.11.0 / v0.11.1 pins, unchanged: `make clean && make BACKEND=uci USE_X25519_SIBLING=1` produces a 62,977 B PRG, and ip65 stops instead at `Segment 'X25519_RODATA' overflows memory area 'CRYPTO_OVERLAY' by 3584 bytes` — a placement problem (ip65's overlay slot is 4,212 B against UCI's 7,680 B), not a symbol collision. The flag remains **off by default** and no shipped artifact contains the sibling; the in-tree X25519 in `src/crypto/{x25519,fe25519}.s` is what every release PRG is built from. Flipping the default is a separate decision that wants a hardware handshake behind it. +- **`USE_X25519_SIBLING=1` now links under UCI, and still does not under ip65.** The duplicate-symbol failure this entry used to record — `ld65: Error: Duplicate external identifier: 'reu_mul_tables_init'`, on **both** backends — was closed by the `libs/nistcurves` v0.10.1 / `libs/x25519` v0.11.0 bump plus one line of archive surgery: `tools/integration/build_nistcurves_p256.sh` now also drops `reu_mul_init.o`, the SPEC §8.2 `reu_mul` provider that `src/boot.s` supplies itself. Re-measured at the current v0.11.2 pins, unchanged: `make clean && make BACKEND=uci USE_X25519_SIBLING=1` produces a 62,977 B PRG, and ip65 stops instead at `Segment 'X25519_RODATA' overflows memory area 'CRYPTO_OVERLAY' by 3584 bytes` — a placement problem (ip65's overlay slot is 4,212 B against UCI's 7,680 B), not a symbol collision. The flag remains **off by default** and no shipped artifact contains the sibling; the in-tree X25519 in `src/crypto/{x25519,fe25519}.s` is what every release PRG is built from. Flipping the default is a separate decision that wants a hardware handshake behind it. - **Live internet HTTP GET (UCI backend)** has not been re-verified since the FPGA-fence rework; only the local multi-segment listener is exercised regularly. - **VICE 3.9** previously appeared to crash on chained HMAC-SHA256 calls (backend-independent — affects the crypto-only test suites), but this was caused by hardcoded port numbers bypassing the test harness port allocator. With proper `ViceInstanceManager` usage (no hardcoded ports), all N=1..10 chained calls succeed reliably. diff --git a/libs/nistcurves b/libs/nistcurves index ba2833e..58f4cf5 160000 --- a/libs/nistcurves +++ b/libs/nistcurves @@ -1 +1 @@ -Subproject commit ba2833e7e30f91913ed07cc9c280c8c467837489 +Subproject commit 58f4cf53b28eb35c4872c93b9b6d799d031ff892 diff --git a/libs/x25519 b/libs/x25519 index 53e22ad..5b344bf 160000 --- a/libs/x25519 +++ b/libs/x25519 @@ -1 +1 @@ -Subproject commit 53e22ad46631a359140d1011f19cee8ab8780c45 +Subproject commit 5b344bf06a661234664cd8f803aff1d5c0704b74 diff --git a/src/contract_footprint_asserts.s b/src/contract_footprint_asserts.s index 4f638dc..b29819a 100644 --- a/src/contract_footprint_asserts.s +++ b/src/contract_footprint_asserts.s @@ -99,11 +99,10 @@ ; - the switch was PRG-NEUTRAL — the comb image is byte-identical ; (`63c7ce868cdd…`) built either way, so the archive change altered the ; manifest and nothing else; -; - the wrapper still drops three members for other reasons -; (`mul_8x8.o`, `data_shared.o`, `reu_mul_init.o` — the §8.0 APP_OWNED -; collision set, c64-https#119). That surgery does not affect the -; numbers this assert reads, because those are per-variant §6.4 values, -; but it is why the §8.0 disjointness asserts are still not writable. +; - the wrapper drops NO members at all from the v0.11.2 pin +; (c64-https#119 closed): the §8.0 deferral is requested through +; CONTRACT_DEFINES, so the manifest attests it and the §8.0 +; disjointness/coverage asserts in src/lib_contract_asserts.s are live. .import LIB_NISTCURVES_RESIDENT_BYTES diff --git a/src/crypto/poly1305.s b/src/crypto/poly1305.s index 7e8f026..4542f4a 100644 --- a/src/crypto/poly1305.s +++ b/src/crypto/poly1305.s @@ -15,45 +15,34 @@ .import poly_h, poly_r, poly_s, poly_product, poly1305_tag .import aead_scratch -; --- poly_prod_lo / poly_prod_hi ownership (c64-lib-contract v0.9.1 -; "adopter-private buffer" rule) --- +; --- poly_prod_lo / poly_prod_hi: c64-https OWNS them, every profile --- ; -; These two bytes are the a*b -> 16-bit output of the §8.3 `ct_mul_8x8` -; body below, and ordinary scratch for anything else that wants a 16-bit -; product slot. Exactly ONE module in the link may define them, and every -; writer and reader must agree on which — they are a rendezvous, not -; private state, so a second copy is not a duplicate-symbol error but a -; silent wrong answer. +; Two bytes, and getting the direction wrong is silent wrong crypto with no +; link error: they are the a*b -> 16-bit output of the §8.3 `ct_mul_8x8` +; body below, written by that body and read back by the library's +; `og_common` row generator. Exactly ONE module may define them. If both +; sides defined a pair the link SUCCEEDS with two disjoint pairs and every +; on-chip-generated multiply row reads two bytes nothing ever wrote. ; -; Under USE_NISTCURVES_ONCHIP the owner is the sibling's -; `mul_8x8_onchip.o`, and it must be: from libs/nistcurves v0.10.0 that -; object defines poly_prod_lo/hi OUTSIDE the SHARED_CT_MUL_8X8 gate, -; because its own `fp_sqr` diagonal-squaring path writes them -; independently of ct_mul_8x8 (upstream's comment at -; libs/nistcurves/src/mul_8x8.s:207-222). Its `og_common` row generator -; then does `jsr ct_mul_8x8` — resolving to the body below — and reads -; the result back out of poly_prod_lo/hi. If this file kept its own pair, -; og_common would read two zero bytes on every product and every -; on-chip-generated multiply row would be wrong, with no link diagnostic -; anywhere. So we import instead: one pair of bytes, in -; LIB_NISTCURVES_MUL_CODE, written and read by both sides. +; SPEC §8.3: whoever provides the ct_mul_8x8 body owns the product scratch. +; c64-https provides that body, so it owns the pair — in every profile, +; from the libs/nistcurves v0.11.2 pin. ; -; Under every other profile the sibling's mul_8x8 object is dropped from -; the archive entirely, nothing else defines them, and this file owns -; them as before. -.ifdef USE_NISTCURVES_ONCHIP -.import poly_prod_lo -.import poly_prod_hi -.endif +; This was profile-split before, and both arms were workarounds. Under the +; REU profile the sibling's mul_8x8 object was DROPPED from the archive, so +; this file owned the pair by default. Under onchip the sibling exported +; them unconditionally (v0.10.0 moved them outside the SHARED_CT_MUL_8X8 +; gate) and this file had to yield and import. v0.11.1 puts them inside the +; gate as IMPORTS on the library side (c64-nist-curves#123), so a deferring +; consumer provides them everywhere and the split disappears. + ; --- Exports --- .export poly1305_init .export poly1305_clamp .export sqtab_init -.ifndef USE_NISTCURVES_ONCHIP .export poly_prod_lo .export poly_prod_hi -.endif .export mul_8x8 .export poly1305_multiply .export poly1305_reduce @@ -66,10 +55,8 @@ ; ============================================================================= .segment "CRYPTO_BSS" -.ifndef USE_NISTCURVES_ONCHIP poly_prod_lo: .res 1 poly_prod_hi: .res 1 -.endif mul_a: .res 1 mul_b: .res 1 diff --git a/src/data.s b/src/data.s index 157e080..a4dffd5 100644 --- a/src/data.s +++ b/src/data.s @@ -130,24 +130,30 @@ mul_dma_hi: .res 256 ; DMA target: hi bytes of a*b for current a ; These remain in-tree under both modes: sqtab_init is still served by ; src/crypto/poly1305.s (the sibling's mul_8x8.s is intentionally ; excluded from build/lib/x25519.a — see tools/integration/build_x25519.sh). -.ifndef USE_NISTCURVES_ONCHIP +; c64-https OWNS the quarter-square tables in every profile, because it +; owns the §8.0 SQTAB primitive: src/crypto/poly1305.s provides +; `sqtab_init`, and the table a primitive fills belongs to whoever fills it. +; +; This was profile-split before. Under USE_NISTCURVES_ONCHIP the in-tree +; labels yielded to the sibling, which exported sqtab_lo/hi as absolute +; equates baked to LIB_SHARED_SQTAB_BASE, and this file kept a same-size +; `sqtab_reserved` placeholder purely to hold the address still. That +; existed because the sibling's export was unconditional; from the v0.11.2 +; pin the wrapper builds it with `-D LIB_NO_BARE_EXPORTS=1` (SPEC §6.5), so +; the bare names are suppressed and the split collapses in the direction +; §8.0 always implied. +; +; THE ADDRESS IS STILL LOAD-BEARING. The sibling reads the table through +; its OWN equates derived from LIB_SHARED_SQTAB_BASE=0xBC00, and derives +; sqtab_hi as base+$0200 (libs/nistcurves/src/mul_8x8.s:47, with its own +; `.assert sqtab_hi = sqtab_lo + $0200`). These labels must therefore land +; at $BC00/$BE00 or every multiply reads the wrong memory — silently, with +; no link error. The Makefile's post-link check asserts sqtab_lo's address. .align 256 .export sqtab_lo .export sqtab_hi sqtab_lo: .res 512 sqtab_hi: .res 512 -.else -; Under USE_NISTCURVES_ONCHIP the sibling's rebuilt mul_8x8_onchip.o -; exports sqtab_lo/hi as ABSOLUTE EQUATES at LIB_SHARED_SQTAB_BASE=$BC00 -; (its .export is unconditional, so the in-tree labels must yield). -; Keep a same-size aligned placeholder so the TABLES_BSS layout — and -; therefore the $BC00 address the equate is baked to — stays put. The -; placeholder is exported solely so the Makefile's post-link check can -; assert it still sits at $BC00; sqtab_init fills it via the equates. -.align 256 -.export sqtab_reserved -sqtab_reserved: .res 1024 -.endif .segment "BSS" diff --git a/src/lib_contract_asserts.s b/src/lib_contract_asserts.s index 9f41135..83b4ff7 100644 --- a/src/lib_contract_asserts.s +++ b/src/lib_contract_asserts.s @@ -200,8 +200,15 @@ APP_OWNED = LIB_SHARED_PRIMITIVES_SQTAB | LIB_SHARED_PRIMITIVES_REU_MUL | LIB_SH ; is spelled `-D nistcurves_zp_ptr2=$3d` from this pin; the wrapper's ; od65 post-check was retargeted to the canonical name so it cannot go ; vacuous. See that script's ZP_OVERRIDES block. -.import LIB_ABI_VERSION -.assert LIB_ABI_VERSION = 2, lderror, "libs/nistcurves: exported-surface generation changed (LIB_ABI_VERSION != 2) — re-check the integration, then bump the expected value in src/lib_contract_asserts.s" +; PREFIXED FORM, from the v0.11.2 pin. The wrapper builds the archive with +; `-D LIB_NO_BARE_EXPORTS=1` (SPEC §6.5) — which is what lets src/data.s +; keep its own adopter-private mul_dma_* buffers without colliding — so the +; bare `LIB_ABI_VERSION` is no longer exported and importing it fails as an +; unresolved external. This is exactly the migration the note above +; anticipated; it arrived via the bare-export gate rather than via a second +; contract library entering the link. +.import LIB_NISTCURVES_ABI_VERSION +.assert LIB_NISTCURVES_ABI_VERSION = 2, lderror, "libs/nistcurves: exported-surface generation changed (LIB_NISTCURVES_ABI_VERSION != 2) — re-check the integration, then bump the expected value in src/lib_contract_asserts.s" ; ===================================================================== @@ -220,55 +227,70 @@ APP_OWNED = LIB_SHARED_PRIMITIVES_SQTAB | LIB_SHARED_PRIMITIVES_REU_MUL | LIB_SH ; ===================================================================== -; §8.0 — shared-primitive ownership tripwire +; §8.0 — shared-primitive ownership, DECLARED rather than surgical ; ===================================================================== -; See the header note: the contract's disjointness assert is not -; writable here yet. What IS checkable is that upstream's ownership -; claim has not moved out from under the archive surgery in -; tools/integration/build_nistcurves_p256.sh. -; -; THE MASK IS PROFILE-DEPENDENT. This is the same exported symbol in -; both builds, but the REU and FP_ONCHIP_MUL archives ship different -; `lib_manifest*` objects, so its VALUE differs by profile and one -; expected constant cannot describe both. Measured with od65 on the -; staged archives: -; -; profile v0.6.0 v0.9.1 -; default (REU) $0007 $0007 -; FP_ONCHIP_MUL $0007 $0005 <- split -; FP_ONCHIP_MUL + comb $0007 $0005 <- split -; -; $0007 -> $0005 is the §8.2 `reu_mul` bit dropping out, upstream at -; **v0.8.0** (c64-nist-curves #78). It is correct behaviour, not drift: -; the onchip profile has no REU multiply path, so owning nothing there -; is the truthful claim, and the CHANGELOG records that a profile switch -; drops the bit from `SHARED_PRIMITIVES` and `SHARED_CONSUMES` alike. -; An earlier revision of this file pinned $0007 for both profiles — -; correct when measured at v0.6.0, and the reason every onchip build -; failed to link at the v0.9.1 pin. -; -; THE ARCHIVE SURGERY IS UNAFFECTED. Checked rather than assumed, since -; the wrong remedy here is expensive: the §8 primitive bodies each -; archive actually exports are the SAME SET at v0.6.0 and v0.9.1 — -; REU ships only `reu_mul_init`/`reu_mul_tables_init` (never pulled; -; src/boot.s:31 exports its own, so ld65 has no undefined symbol to -; resolve from the member), onchip ships `sqtab_lo`/`sqtab_hi` (absolute -; equates baked to LIB_SHARED_SQTAB_BASE=$BC00, not bodies) plus -; `reu_fetch_mul_row`, `og_common`, `og_src_ld`. Neither ships -; `sqtab_init`, `ct_mul_8x8`, `mul_8x8`, `poly_prod_*` or `smc_*`. -; So `build_nistcurves_p256.sh`'s member drops need no re-derivation -; for this change, and none was made. +; This section used to say the contract's disjointness assert was "not +; writable here yet". It is writable now, and both halves are below. +; +; THE OLD SHAPE. c64-https has always been the §8.0 APP_OWNED case — it +; provides all three shared primitives itself (sqtab_init + mul_8x8 + +; ct_mul_8x8 in src/crypto/poly1305.s, mul_tables_init in +; src/crypto/shared/mul_tables.s, reu_mul_init in src/boot.s, mul_dma_lo/hi +; in src/data.s) — but resolved the resulting double-ownership by DELETING +; archive members. The shipped manifest therefore described upstream's +; archive rather than the one we linked: the mask claimed the library owned +; all three while the linked archive owned none. Asserting disjointness +; against that number would have encoded the mismatch, not caught it. +; +; THE NEW SHAPE (libs/nistcurves v0.11.2 + SPEC §6.2). The wrapper requests +; the deferral through CONTRACT_DEFINES — four SHARED_* switches plus +; LIB_NO_BARE_EXPORTS=1 — instead of ar65 surgery. Per §8.0's +; conditional-mask rule the manifest then ATTESTS the deferral: +; +; archive PRIMITIVES CONSUMES +; lib-p256-verify (REU) $0000 $0007 +; lib-p256-verify-onchip $0000 $0005 +; lib-p256-comb-onchip $0000 $0005 +; +; **Only PRIMITIVES is profile-independent.** CONSUMES is NOT: under +; FP_ONCHIP_MUL the manifest zeroes the reu_mul bit in BOTH masks, because +; that build genuinely does not read the primitive — upstream hard-asserts +; it (`src/lib_manifest.s`, "FP_ONCHIP_MUL manifest must not claim SPEC 8.2 +; reu_mul consumption"). That is §8.0's three-state table working: a +; deferral switch drops a bit from ownership only, while a profile gate +; drops it from ownership AND consumption. +; +; Measured with od65 on the staged archives at the v0.11.2 pin. All three +; asserts below pass in every profile regardless, since $0005 & ~$0007 = 0. +; Do not "correct" $0005 to $0007 on an onchip archive — it is right. .import LIB_NISTCURVES_SHARED_PRIMITIVES +.import LIB_NISTCURVES_SHARED_CONSUMES -.ifdef USE_NISTCURVES_ONCHIP - ; FP_ONCHIP_MUL, and the comb variant which sets the same -D. - NISTCURVES_EXPECTED_PRIMITIVES = LIB_SHARED_PRIMITIVES_SQTAB | LIB_SHARED_PRIMITIVES_CT_MUL_8X8 - .assert LIB_NISTCURVES_SHARED_PRIMITIVES = NISTCURVES_EXPECTED_PRIMITIVES, lderror, "libs/nistcurves FP_ONCHIP_MUL archive: SHARED_PRIMITIVES is not $0005 (sqtab|ct_mul_8x8). This mask is PROFILE-DEPENDENT - the onchip archive drops the reu_mul bit that the REU archive keeps, so read the onchip lib_manifest, not the REU one. If upstream moved it again, update the USE_NISTCURVES_ONCHIP arm in src/lib_contract_asserts.s; only re-derive the wrapper's member drops if the archive's exported primitive BODIES changed too." -.else - ; Default REU profile: all three. - NISTCURVES_EXPECTED_PRIMITIVES = LIB_SHARED_PRIMITIVES_SQTAB | LIB_SHARED_PRIMITIVES_REU_MUL | LIB_SHARED_PRIMITIVES_CT_MUL_8X8 - .assert LIB_NISTCURVES_SHARED_PRIMITIVES = NISTCURVES_EXPECTED_PRIMITIVES, lderror, "libs/nistcurves REU archive: SHARED_PRIMITIVES is not $0007 (sqtab|reu_mul|ct_mul_8x8). This mask is PROFILE-DEPENDENT - the onchip archive legitimately reports $0005, so check which profile this build selected. If upstream moved the REU value, update the .else arm in src/lib_contract_asserts.s; only re-derive the wrapper's member drops if the archive's exported primitive BODIES changed too." -.endif +; --- The library must own nothing, in every profile --- +; If this fires, the wrapper's CONTRACT_DEFINES did not reach the manifest +; TU. Do NOT restore the member drops to work around it: that puts the +; mismatch back and silently invalidates the two asserts below. +.assert LIB_NISTCURVES_SHARED_PRIMITIVES = 0, lderror, "libs/nistcurves: SHARED_PRIMITIVES is not $0000. c64-https requests full 8.0 deferral via CONTRACT_DEFINES, so the archive must own no shared primitive. Most likely the defines did not reach lib_manifest*.o. Do not restore the ar65 member drops to work around this." + +; --- §8.0 disjointness: no primitive owned twice --- +.assert (APP_OWNED & LIB_NISTCURVES_SHARED_PRIMITIVES) = 0, lderror, "c64-https and libs/nistcurves both claim ownership of a shared primitive (SPEC 8.0 disjointness). c64-https provides sqtab_init/mul_8x8/ct_mul_8x8 in poly1305.s and reu_mul_init in boot.s; the library must defer all of them." + +; --- §8.0 coverage: everything the library READS, somebody PROVIDES --- +; CONSUMES & ~(APP_OWNED | LIB_OWNED) = 0. +; +; HONEST SCOPE: this assert CANNOT FIRE TODAY, and must not be read as +; guarding the poly_prod rendezvous. APP_OWNED is $0007, which covers every +; §8.0 bit allocated so far, and CONSUMES is drawn from those same three +; bits — so the expression is identically zero. It is kept because it is +; the clause's canonical form and it arms itself the day a fourth primitive +; is allocated ($0008) and nistcurves consumes it. +; +; The failure it sounds like it catches is covered elsewhere: defines +; missing from the manifest TU trip the PRIMITIVES = 0 assert above; +; defines missing from the code TUs trip a duplicate-external link error +; against our own exports. And the poly_prod rendezvous is caught by +; neither — only by the comb/onchip ECDSA KAT. +.assert (LIB_NISTCURVES_SHARED_CONSUMES & ~(APP_OWNED | LIB_NISTCURVES_SHARED_PRIMITIVES)) = 0, lderror, "libs/nistcurves consumes a shared primitive that neither it nor c64-https provides (SPEC 8.0 coverage)." ; ===================================================================== diff --git a/tools/integration/build_nistcurves_p256.sh b/tools/integration/build_nistcurves_p256.sh index ae45bcb..4bea7e0 100755 --- a/tools/integration/build_nistcurves_p256.sh +++ b/tools/integration/build_nistcurves_p256.sh @@ -85,19 +85,16 @@ if [ "$PROFILE" = "onchip-comb" ]; then # loading the table from disk instead. UPSTREAM_TARGET="lib-p256-comb-onchip" UPSTREAM_ARCHIVE="$LIB_BUILD/lib/nistcurves-p256-comb-onchip.a" - STAGING="$PROJECT_ROOT/build/lib/nistcurves_p256_onchip_comb_staging" ARCHIVE="$OUT_DIR/nistcurves-p256-onchip-comb.a" SIZES="$OUT_DIR/nistcurves-p256-onchip-comb.sizes.txt" elif [ "$PROFILE" = "onchip" ]; then UPSTREAM_TARGET="lib-p256-verify-onchip" UPSTREAM_ARCHIVE="$LIB_BUILD/lib/nistcurves-p256-verify-onchip.a" - STAGING="$PROJECT_ROOT/build/lib/nistcurves_p256_onchip_staging" ARCHIVE="$OUT_DIR/nistcurves-p256-onchip.a" SIZES="$OUT_DIR/nistcurves-p256-onchip.sizes.txt" else UPSTREAM_TARGET="lib-p256-verify" UPSTREAM_ARCHIVE="$LIB_BUILD/lib/nistcurves-p256-verify.a" - STAGING="$PROJECT_ROOT/build/lib/nistcurves_p256_staging" ARCHIVE="$OUT_DIR/nistcurves-p256.a" SIZES="$OUT_DIR/nistcurves-p256.sizes.txt" fi @@ -165,221 +162,148 @@ ZP_OVERRIDES=( # CA65FLAGS hook), so we cannot pass -D overrides via `make CA65=...` here. # We build upstream with its defaults, then rebuild the TUs that need # consumer overrides (zp_config.o always; mul_8x8_onchip.o under onchip). -echo "[p256/$PROFILE] building libs/nistcurves $UPSTREAM_TARGET (upstream defaults)..." -make -s -C "$LIB_DIR" "$UPSTREAM_TARGET" >/dev/null +# SPEC §6.2: request the §8.0 APP_OWNED shape and §6.5 bare-name +# suppression through CONTRACT_DEFINES, rather than building upstream's +# defaults and then deleting the members that collide. §6.1 bans that +# surgery, and the reason is concrete: the manifest then describes an +# archive we did not link, which is what forced the §6.6 comb exemption. +# +# SHARED_SQTAB_INIT / SHARED_CT_MUL_8X8 bodies come from +# src/crypto/poly1305.s +# SHARED_REU_MUL_INIT / _FETCH reu_mul_init from src/boot.s; +# §8.2 requires both or neither +# LIB_NO_BARE_EXPORTS=1 suppress the bare mul_dma_lo/hi, +# mul_cached_a, mul_src2_buf that +# src/data.s defines. §8.2 rules +# those ADOPTER-PRIVATE and rules +# OUT deferring them ("would point +# a library's own field arithmetic +# at another library's memory") — +# the §6.5 rename track plus this +# gate is the sanctioned remedy. +# LIB_SHARED_SQTAB_BASE=0xBC00 the sibling reads the table +# through its own baked equates; +# src/data.s owns the storage and +# must land there. 0x-hex, never +# `$BC00`: SPEC §2 records that an +# unquoted `$BC00` through +# make+shell expands to `$B`+"C00" +# and silently yields a WRONG +# address with no diagnostic. +# +# Requires libs/nistcurves >= v0.11.2: +# v0.11.1 made SHARED_CT_MUL_8X8 assemble against the on-chip TU +# (c64-nist-curves#123) — before that this needed a glue TU. +# v0.11.2 added the knob-staleness guard: a changed CONTRACT_DEFINES used +# to reuse stale objects and exit 0 with a DIFFERENT archive than +# requested. That silent no-op is why an earlier attempt at this +# change produced a comb image that would not boot. +CONTRACT_DEFINES="-D SHARED_SQTAB_INIT -D SHARED_REU_MUL_INIT -D SHARED_REU_MUL_FETCH -D SHARED_CT_MUL_8X8 -D LIB_NO_BARE_EXPORTS=1 -D LIB_SHARED_SQTAB_BASE=0xBC00" +# ZP-slot overrides go in the SEPARATE variable, not the one above: SPEC §6.2 +# splits them because a globally-delivered slot define collides with every +# `.importzp` site. c64-https needs three (upstream defaults collide with our +# canonical map): nistcurves_zp_ptr2 $fd -> $3D (upstream's default hits our +# zp_temp/zp_count during cert parsing), fp_mul_i/j $2c/$2d -> $39/$3A +# (upstream's defaults sit inside our fe25519 claim $2c-$37). +# +# 0x-hex, never `$3d`: SPEC §2 records that an unquoted `$3d` through +# make+shell expands to `$3`+"d" and silently assembles the slot at address +# $00, with no diagnostic at any stage. The od65 post-check below is what +# actually proves the values landed. +CONTRACT_ZP_DEFINES="-D nistcurves_zp_ptr2=0x3d -D fp_mul_i=0x39 -D fp_mul_j=0x3a" +echo "[p256/$PROFILE] building libs/nistcurves $UPSTREAM_TARGET (APP_OWNED + gated bare exports)..." +make -s -C "$LIB_DIR" "$UPSTREAM_TARGET" CONTRACT_DEFINES="$CONTRACT_DEFINES" CONTRACT_ZP_DEFINES="$CONTRACT_ZP_DEFINES" >/dev/null if [ ! -f "$UPSTREAM_ARCHIVE" ]; then echo "ERROR: upstream archive missing: $UPSTREAM_ARCHIVE" >&2 exit 1 fi -# --- 2. Stage upstream object files --- -rm -rf "$STAGING" -mkdir -p "$STAGING" "$OUT_DIR" -cp "$UPSTREAM_ARCHIVE" "$STAGING/upstream.a" -(cd "$STAGING" && "$AR65" x upstream.a $( "$AR65" t upstream.a )) - -# --- 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; } +# --- 2. Verify the ZP overrides landed, then use the archive as built --- +# NOTHING IS STAGED, REBUILT OR RE-ARCHIVED ANY MORE. The archive we link is +# byte-for-byte the one upstream's make produced, which is what makes SPEC +# §6.1's "no ar65 member surgery, no copying intermediates around" true by +# construction rather than by inspection. +# +# This replaces a rebuild-one-member-and-re-archive dance. That dance had a +# §6.2 defect that outlived the member drops: it passed only the ZP +# overrides to the rebuilt object and NOT the CONTRACT_DEFINES string, so +# `zp_config.s`'s `.ifndef LIB_NO_BARE_EXPORTS` gate did not apply to the +# one member we rebuilt — the archive shipped a single object re-exporting +# the bare `zp_*` names every other member had been built to suppress. +# One artifact, two configurations. Dormant while only one contract library +# links, live the moment a second one does: that is exactly the #83 ZP +# collision family the suppression gate exists to prevent. +# +# CONTRACT_ZP_DEFINES is the sanctioned route (upstream since nist#104) and +# is scoped correctly by construction: slot defines reach every TU that +# DEFINES a slot and never one that `.importzp`s it, which is the +# model-independent rule SPEC §6.2 states after measured failures in both +# directions. +# +# The post-check below is kept and is the load-bearing part. A wrong ZP slot +# is not a link error — `nistcurves_zp_ptr2` colliding with c64-https's +# zp_temp/zp_count corrupts cert parsing at runtime, and fp_mul_i/j landing +# inside the fe25519 claim ($2c-$37) corrupts multiplies. Verify from the +# emitted object, never from the fact that a -D was passed. +mkdir -p "$OUT_DIR" -# 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') ;; - # v0.11.0's comb archives (c64-nist-curves#117). Gate name confirmed - # against libs/nistcurves/src/zp_config.s:162 and Makefile:302 rather - # than inferred from the member basename. - zp_config_p256comb.o) ZP_VARIANT_DEFINE=('-D' 'LIB_P256_COMB_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 ;; +# Name the member from the ARCHIVE BEING LINKED, never by globbing the build +# directory. All three profiles are built from the same libs/nistcurves tree, +# so several variants' ZP objects coexist there — after a comb build followed +# by a REU build, `ls zp_config_*.o | head -1` yields zp_config_p256comb.o +# while the REU archive ships zp_config_p256verify.o (reproduced). The check +# would then verify an object this archive does not contain. +# +# Today that would still pass, because v0.11.2's staleness stamp guarantees +# every object in the directory carries the same knob string — but a guard +# that reads a different artifact than the one shipped is the wrong shape +# regardless, and a variant-gated slot could someday differ between variants' +# ZP TUs. Listing an archive is not extracting it, so §6.1 stays clean. +ZP_MEMBER="$( "$AR65" t "$UPSTREAM_ARCHIVE" | grep '^zp_config' )" +case "$(printf '%s\n' "$ZP_MEMBER" | grep -c .)" in + 1) ;; + 0) echo "ERROR: no zp_config member in $(basename "$UPSTREAM_ARCHIVE")" >&2; exit 1 ;; + *) echo "ERROR: multiple zp_config members in $(basename "$UPSTREAM_ARCHIVE"): $ZP_MEMBER" >&2; exit 1 ;; esac +[ -f "$LIB_BUILD/$ZP_MEMBER" ] || { echo "ERROR: $ZP_MEMBER named by the archive but absent from $LIB_BUILD" >&2; exit 1; } -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_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 -} -# Same as check_zp_slot, but tolerates the symbol being absent. Only correct -# for a slot whose ABSENCE is legitimate — see the alias check below. -check_zp_slot_if_present() { - 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}') - [ -z "$got" ] && return 0 + got=$("${OD65:-od65}" --dump-exports "$LIB_BUILD/$ZP_MEMBER" \ + | awk -v n="\"$name\"" '$1=="Name:" && $2==n {f=1; next} f && $1=="Value:" {print $2; exit}') if [ "$got" != "$want" ]; then - echo "ERROR: $ZP_MEMBER exports $name = $got, expected $want (c64-https ZP override did not take)" >&2 + echo "ERROR: $ZP_MEMBER exports $name = ${got:-}, expected $want (CONTRACT_ZP_DEFINES did not take)" >&2 exit 1 fi } +# Canonical spellings only: the bare aliases vanish under LIB_NO_BARE_EXPORTS, +# and a guard that can go vacuous under a build-tightening flag is worse than +# no guard. +check_zp_slot nistcurves_zp_ptr2 0x0000003D +check_zp_slot fp_mul_i 0x00000039 +check_zp_slot fp_mul_j 0x0000003A -# The CANONICAL spelling is checked unconditionally — it is the name the -# library's own code reads, so its absence or disagreement is always a defect. -check_zp_slot "$ZP_PTR2_SLOT" 61 # $3d — canonical spelling for this pin - -# The deprecated bare alias is checked only WHEN PRESENT, and only when it is -# a distinct symbol. On a §2-migrated pin this proves the alias tracks the -# override rather than splitting one slot across two addresses (the silent -# outcome contract §6.5 forbids) — but the alias legitimately disappears under -# `-D LIB_NO_BARE_EXPORTS=1` (§1), and a hard check would then fail the build -# over a symbol the contract expects to be gone. Checking the bare name -# *instead* of the canonical one would be the worse error in the other -# direction: it is the spelling that goes away, so the guard would stop -# guarding exactly when a consumer tightens the build. -if [ "$ZP_PTR2_SLOT" != "zp_ptr2" ]; then - check_zp_slot_if_present zp_ptr2 61 # $3d — deprecated alias, if still emitted +# The suppression gate must have reached this TU too — that is the whole +# point of routing the overrides through CONTRACT_ZP_DEFINES. +if "${OD65:-od65}" --dump-exports "$LIB_BUILD/$ZP_MEMBER" | grep -q '"zp_ptr2"'; then + echo "ERROR: $ZP_MEMBER re-exports the bare 'zp_ptr2' — LIB_NO_BARE_EXPORTS did not reach it." >&2 + echo " One archive must carry one configuration (SPEC 6.2)." >&2 + exit 1 fi +echo "[p256/$PROFILE] ZP overrides verified in $ZP_MEMBER; bare zp_* suppressed" -check_zp_slot fp_mul_i 57 # $39 -check_zp_slot fp_mul_j 58 # $3a +cp "$UPSTREAM_ARCHIVE" "$ARCHIVE" -# --- 4. Drop conflicting members / rebuild the onchip mul object --- -# `reu_mul_init.o` is the SPEC §8.2 `reu_mul` provider. c64-https is the -# §8.0 APP_OWNED case for that primitive — src/boot.s::reu_mul_init builds -# the 128 KB REU multiply table itself — so the library's provider is -# surplus in every configuration. Dropping it is the archive-surgery -# spelling of `-D SHARED_REU_MUL_INIT` (the wrapper cannot pass that -# define: upstream's Makefile builds every module with one recipe, which -# is why step 3 rebuilds rather than reconfigures). -# -# It is a NO-OP for the shipped builds and load-bearing for one that is -# not shipped yet. Default build: boot.o defines `reu_mul_init` and -# `reu_fetch_mul_row` itself, so ld65 has no undefined symbol this member -# could satisfy and never pulls it — the four REU-profile PRGs are -# byte-identical with and without the drop (measured). Onchip archives -# never contained it. But under `USE_X25519_SIBLING=1`, boot.o *imports* -# `reu_mul_init` (the sibling owns the table), ld65 pulls this member to -# satisfy it because nistcurves-p256.a precedes x25519.a on the link line, -# and then the sibling's own provider arrives via `reu_clear_wide` — -# `ld65: Error: Duplicate external identifier: 'reu_mul_tables_init'`, -# the failure README.md and CLAUDE.md both recorded as unconditional. -# Note the near miss: had ld65 resolved instead of erroring, `reu_mul_init` -# would have bound to the library's table builder rather than the -# sibling's, which is a different routine writing through a different -# buffer set. -rm -f "$STAGING/mul_8x8.o" "$STAGING/data_shared.o" "$STAGING/reu_mul_init.o" -# 4b. RETIRED at the libs/nistcurves v0.11.0 pin. This used to delete ~13 -# non-P-256 members (fp384/mod384/curve384/points384_*/data_p384*/ -# ecdsa384*/sha384*/data_sha/inv256/data_p256_invref) from the full -# `lib-onchip` archive, because no narrowed comb archive existed. -# `lib-p256-comb-onchip` is now that archive, so there is nothing to -# strip — see the UPSTREAM_TARGET block at the top of this file. -# -# Do not reinstate this without also restoring the §6.6 comb exclusion in -# src/contract_footprint_asserts.s: the two moved together, and deleting -# members silently invalidates the §6.4 manifest the assert reads. -if [ "$PROFILE" = "onchip" ] || [ "$PROFILE" = "onchip-comb" ]; then - # Rebuild (not drop): fp256_onchip.o imports og_common/og_src_ld which - # only this TU provides. The SHARED_* defines strip everything that - # would collide with the in-tree providers (see header comment #3). - # - # Upstream gap (candidate c64-nist-curves issue): the og_common block - # references ct_mul_8x8 / smc_* as same-TU symbols, so - # SHARED_CT_MUL_8X8 alone leaves them undefined — the guard combo was - # never exercised upstream. Bridge it with a generated glue TU that - # declares the .imports and then .includes the PRISTINE library source - # (composition, not a source patch — libs/ stays untouched). - # - # THE IMPORT LIST SHRANK AT v0.10.1, AND SHRINKING IT WAS MANDATORY. - # Upstream moved three of the five symbols this glue used to supply - # into its own source, so re-declaring them is now a hard assemble - # error rather than a harmless duplicate: - # - # poly_prod_lo / poly_prod_hi defined unconditionally at - # mul_8x8.s:223-225, deliberately OUTSIDE the SHARED_CT_MUL_8X8 - # gate (contract v0.9.1 adopter-private-buffer rule: fp_sqr's - # diagonal path writes them with no ct_mul_8x8 involved). - # Importing them here now yields - # `mul_8x8.s(223): Error: Symbol 'poly_prod_lo' is already an - # import`. The consumer side moved to match: under - # USE_NISTCURVES_ONCHIP, src/crypto/poly1305.s IMPORTS this - # object's pair instead of defining its own, so og_common's - # `jsr ct_mul_8x8` and its poly_prod read-back address the same - # two bytes. See the ownership comment in poly1305.s — getting - # this wrong is silent wrong crypto, not a link error. - # mul_cached_a upstream imports the §6.5 canonical - # `nistcurves_mul_cached_a` itself (mul_8x8.s:30); the bare name - # is aliased for it in src/crypto/shared/mul_tables.s. - # - # ct_mul_8x8 and the two SMC bake sites are still ours to supply: - # upstream declares no import for them under SHARED_CT_MUL_8X8. - cat > "$STAGING/mul_8x8_onchip_glue.s" <<'EOF' -; generated by build_nistcurves_p256.sh (onchip profile) — do not edit -.import ct_mul_8x8 -.import smc_sum_a_imm, smc_diff_a_imm -.include "mul_8x8.s" -EOF - "$CA65" \ - --cpu 6502 \ - -g \ - -I "$LIB_SRC" \ - -D FP_ONCHIP_MUL=1 \ - -D SHARED_CT_MUL_8X8=1 \ - -D SHARED_SQTAB_INIT=1 \ - -D 'LIB_SHARED_SQTAB_BASE=$BC00' \ - -o "$STAGING/mul_8x8_onchip.o" \ - "$STAGING/mul_8x8_onchip_glue.s" -else - rm -f "$STAGING/mul_8x8_onchip.o" -fi -# --- 5. Re-archive into c64-https's expected location --- -# Member list is taken from the upstream archive dynamically (v0.5.0 -# renamed/added members vs v0.3.0: ecdsa256_nocomb.o, precalc_manifest.o, -# lib_manifest_onchip.o, ...) minus the dropped members above, so this -# script no longer needs touching when upstream reshuffles objects. -MEMBERS=() -for m in $( "$AR65" t "$STAGING/upstream.a" ); do - [ -f "$STAGING/$m" ] || continue # dropped members - MEMBERS+=("$STAGING/$m") -done -rm -f "$ARCHIVE" -"$AR65" a "$ARCHIVE" "${MEMBERS[@]}" +# (steps 4 and 5 retired: no member drops, no glue TU, no re-archive) -# --- 6. Per-source byte counts (for the supervisor's PR description) --- +# --- 3. Per-source byte counts (for the PR description) --- { echo "# $(basename "$ARCHIVE") per-source byte counts (ca65 .o file sizes)" - for m in $( "$AR65" t "$STAGING/upstream.a" ); do - if [ -f "$STAGING/$m" ]; then - bytes=$(wc -c < "$STAGING/$m") - printf '%-24s %d bytes (.o)\n' "${m%.o}" "$bytes" - fi + for m in $( "$AR65" t "$ARCHIVE" ); do + f="$LIB_BUILD/$m" + [ -f "$f" ] && printf '%-24s %d bytes (.o)\n' "${m%.o}" "$(wc -c < "$f")" done } > "$SIZES"