Repository navigation
Description
What
tools/integration/build_nistcurves_p256.sh builds our P-256 archives by editing upstream's member set:
:274 rm -f "$STAGING/mul_8x8.o" "$STAGING/data_shared.o" "$STAGING/reu_mul_init.o"
:278 # onchip-comb: rm -f fp384_onchip.o mod384.o curve384.o points384_core.o
# points384_comb.o data_p384.o data_p384_limlee.o ecdsa384*.o
# sha384*.o data_sha.o inv256.o data_p256_invref.o
:337 rm -f "$STAGING/mul_8x8_onchip.o"
c64-lib-contract SPEC §6.1 now names this as forbidden:
Consumers fetch
build/lib/<shortname>[-<variant>].aand link directly. No mid-buildsed, no copying intermediates around, and noar65member surgery — an archive whose member set a consumer has edited is outside every §5/§8.0 manifest claim it ships. §6.2 and §6.3 exist so surgery is never the only route to a configuration.
This is the root cause of two gaps we already document as deferred:
src/contract_footprint_asserts.sexcludes the comb profile from the §6.6 assert, because the surviving manifest describes the pre-surgery archive (27,000 B declared against a 16,384 B region).src/lib_contract_asserts.srecords the §8.0 disjointness/coverage asserts as "not writable today" for the same reason — the shipped mask describes upstream's archive, not the one we link.
What is now solvable, measured against the v0.10.2 pin
Upstream gained CONTRACT_DEFINES and APP_OWNED_DEFINES in the wave. The variant × APP_OWNED combination is reachable today — verified by building it:
make lib-p256-verify CONTRACT_DEFINES='-D SHARED_SQTAB_INIT -D SHARED_REU_MUL_INIT \
-D SHARED_REU_MUL_FETCH -D SHARED_CT_MUL_8X8'
exit 0, and od65 --dump-exports on the resulting members shows the switches do most of our surgery for us:
| member | exports under the switches | our current surgery |
|---|---|---|
reu_mul_init.o |
nothing at all | rm -f — unnecessary, the object is inert |
mul_8x8.o |
poly_prod_lo/hi, sqtab_lo/hi only (no sqtab_init, no ct_mul_8x8, no mul_8x8) |
rm -f — nearly unnecessary |
data_shared.o |
mul_dma_lo/hi, mul_cached_a, mul_src2_buf (+ prefixed) |
rm -f — still collides |
So items 1 and 2 are close to free. Item 3 is not.
What is hard, and why I am not doing it under release pressure
Dropping the data_shared.o surgery means c64-https stops defining mul_dma_lo/hi, mul_cached_a, mul_src2_buf in src/data.s and imports the library's instead — which is what upstream's own lib-app-owned assumes, since it keeps data_shared.o in the member list. Those buffers are gated only by LIB_NO_BARE_EXPORTS (a naming switch), so there is no way to have the library ship the archive and not the buffers.
That is a re-pointing of the REU multiply row table, and it is the single worst place in this codebase to be wrong: CLAUDE.md's "VICE harness gotcha" records that when mul_dma_lo/hi holds the wrong contents, every fp_mul returns a*255*b mod p, the handshake fails minutes later at the first encrypted record, and there is no link error and no diagnostic. The same note records the near-miss where reu_mul_init would have bound to the wrong provider's table with no diagnostic either.
It is worth doing. It is not worth doing in the same change as a release.
What is blocked upstream regardless
There is no P-256 comb archive target. Upstream's full list is lib, lib-p256-verify, lib-p384-verify, lib-p384-sha384, lib-p384-curve, lib-app-owned, lib-onchip, lib-p256-verify-onchip, lib-p384-verify-onchip, lib-p384-curve-onchip. The comb objects exist only inside the full lib-onchip archive, which is why we narrow it by hand.
So even a perfect fix to items 1-3 leaves the comb profile non-conformant, and §6.3 ("every documented variant/profile axis MUST be reachable through §6.1 targets plus §6.2 defines, with no library edits") is not satisfiable for it from our side. Filed upstream as c64-nist-curves#TBD — link to follow.
Sequencing
- Upstream: a
lib-p256-verify-onchip-comb(or equivalent) target. Blocks the comb arm entirely. - Here: switch the REU and onchip arms to
CONTRACT_DEFINES+ APP_OWNED switches, flippingpoly_prod_lo/hiand thedata_sharedbuffer ownership to imports. Wants its own change, its own KAT run on both profiles, and an e2e handshake — a link that succeeds proves nothing about this failure mode. - Then: delete the
.ifdef USE_NISTCURVES_COMBexclusion insrc/contract_footprint_asserts.sand write the §8.0 disjointness/coverage asserts that the surgery currently makes meaningless.
Related: #70 (the other open consumer-side alignment item, §13 net ABI).
Activity
JC-000 commentedon Aug 15, 2026
Upstream blocker cleared: c64-nist-curves v0.11.0 is released with the missing comb arm (JC-000/c64-nist-curves#117 → PR JC-000/c64-nist-curves#119).
make lib-p256-comb→nistcurves-p256-comb.a(DMA multiply profile)make lib-p256-comb-onchip→nistcurves-p256-comb-onchip.a(theUSE_NISTCURVES_ONCHIP_COMB=1configuration)Each carries its own §6.4 manifest measured from the built objects —
ZP_USAGE_BYTES17,RESIDENT_BYTES9216,COLD_BYTES1050 (DMA) / 870 (onchip),REU_BANKS_USED$07/$04, masks$0007/$0007and$0005/$0005— so the comb profile can come back into the §6.6 assert with real figures, and the §8.0 disjointness asserts become writable. Thelim_lee_comb_p256precalc row is enumerated; the P-384 row is not (per-curve gates).Member set = the
lib-p256-verify[-onchip]set with the comb-fastecdsa256.oin place ofecdsa256_nocomb.o, pluspoints256_comb.o+data_p256_limlee.o— i.e. exactly whattools/integration/build_nistcurves_p256.sh's surgery was approximating, now conformant. Boot obligation addsec_precompute_256; REU bank 2 carries the 16 KB anchor table in both profiles.Pin v0.11.0 (ABI stays 2; PRG byte-identical to v0.10.2, all pre-existing archives unchanged): https://github.com/JC-000/c64-nist-curves/releases/tag/v0.11.0
JC-000 commentedon Aug 15, 2026
Partial progress — the comb third is done
libs/nistcurves v0.11.0 landed
lib-p256-comb/lib-p256-comb-onchip(c64-nist-curves#117), so the comb-specific surgery is retired:build_nistcurves_p256.shnow stageslib-p256-comb-onchipinstead of the fulllib-onchip, and step 4b — therm -fof ~13 P-384/SHA-384/invref members — is gone.63c7ce868cdd…) built either way, so the archive change moved the manifest and nothing else.src/contract_footprint_asserts.sis deleted. The comb profile now links with the footprint assert active, alongsidereuandonchip. That was the concrete payoff named when this issue was filed.zp_config_p256comb.oand failed loudly (unrecognised zp_config member) rather than silently dropping the override — the discovery machinery working as designed. Its gate isLIB_P256_COMB_ONLY, confirmed againstzp_config.s:162andMakefile:302rather than inferred from the basename.Verified: comb builds on both backends, ECDSA KAT oracle 6/6 on the comb PRG (3 valid accepted, 3 invalid rejected),
sqtab_reservedstill at$BC00.What is still open — and it is the risky part
Three kinds of member editing remain, all pre-existing:
build_nistcurves_p256.sh:296mul_8x8.o,data_shared.o,reu_mul_init.o— every profile:359mul_8x8_onchip.owith a locally-rebuilt glue TUzp_config_*.owith a locally-rebuilt overrideFor the first: two of the three are now easy. Building
lib-p256-verifywithCONTRACT_DEFINES='-D SHARED_SQTAB_INIT -D SHARED_REU_MUL_INIT -D SHARED_REU_MUL_FETCH -D SHARED_CT_MUL_8X8'leavesreu_mul_init.oexporting nothing andmul_8x8.oexporting onlypoly_prod_lo/hi+sqtab_lo/hi— so neither needs dropping.data_shared.ois the blocker and is unchanged by v0.11.0. It exportsmul_dma_lo/hi,mul_cached_a,mul_src2_buf, gated only byLIB_NO_BARE_EXPORTS(a naming switch), and upstream's ownlib-app-ownedkeeps the member — so the intended model is that the consumer imports those buffers rather than defining them insrc/data.s. That is a re-pointing of the REU multiply row table, whose failure mode isa*255*b mod pwith no link error and no diagnostic, surfacing minutes later at the first encrypted record.So this stays open deliberately. When it is picked up it wants its own change, an ECDSA KAT run on both profiles, and an e2e handshake — a successful link demonstrates nothing about this failure mode.
JC-000 commentedon Aug 15, 2026
Tested to a conclusion. Summary: half of this is done and proven, the other half is blocked upstream.
The risk I originally described was wrong
This issue said the blocker was flipping
mul_dma_lo/hiownership so c64-https imports the library's buffers, and called that the silent-wrong-crypto path. That framing was mistaken. SPEC §8.2 explicitly rules those four buffers adopter-private and rules the flip out by name:The sanctioned route is the §6.5 rename track plus the
LIB_NO_BARE_EXPORTSsuppression gate. Measured at v0.11.0, buildinglib-p256-verifywith the four APP_OWNED switches plus-D LIB_NO_BARE_EXPORTS=1:reu_mul_init.omul_8x8.opoly_prod_lo/hionlydata_shared.onistcurves_mul_*prefixed onlyAll four symbols we consume (
ec_base_x,ec_gx256,ec_scalar_mul_var,ecdsa_verify_256) survive the gate. No ownership flip, no touching the REU row table.What was proven end-to-end (REU + ip65)
Built and linked with zero member drops:
CONTRACT_DEFINESinstead ofar65deletionsrc/crypto/poly1305.simportspoly_prod_lo/hiunconditionally (it already did under on-chip; the pair now lives inLIB_NISTCURVES_MUL_CODEin every profile)src/lib_contract_asserts.sswitches to the prefixedLIB_NISTCURVES_ABI_VERSION— the bare name is suppressed by the gate, which is exactly the migration that file's own header anticipatedAnd the payoff: the manifest now attests the deferral instead of us resolving it by deletion —
— which makes the §8.0 disjointness and coverage asserts writable for the first time. Both were written and both link:
make BACKEND=uciandmake BACKEND=ip65both link clean with all of that active.What is blocked — and it is the half we ship
The on-chip profiles cannot follow. Upstream's
mul_8x8.sreferencesct_mul_8x8and twosmc_*labels as same-TU symbols insideog_common;SHARED_CT_MUL_8X8gates out their definitions without adding imports, so the define does not assemble:Control with defaults: exit 0. Filed as JC-000/c64-nist-curves#123 (§6.3 reachability — the APP_OWNED × on-chip intersection is unreachable through §6.1 targets plus §6.2 defines).
That matters disproportionately here because every image in our release lineup is an on-chip build (
ip65-onchip,uci-onchip,uci-comb). So landing the REU-only half would change no shipped artifact, while splitting the tree into profiles that assert full deferral and profiles that still use surgery — and would need the §8.0 asserts made profile-conditional again, which is most of what makes them worth having.Decision
Reverted, not landed. The release (PR #121) stands as verified; the working tree rebuilds to the same PRG hash it was verified at.
Unblocking sequence:
poly_prodrelocation is a real layout change, so a clean link proves nothing hereJC-000 commentedon Aug 15, 2026
Correction to the wording above — "every image is an on-chip build"
That phrasing was sloppy, and misleading in exactly the way this project has been at pains to avoid elsewhere:
uci-combrequires an REU (bank 2,LIB_NISTCURVES_REU_BANKS_USED = $04), and calling it "on-chip" without qualification implies the opposite. Two independent axes were conflated:FP_ONCHIP_MUL) or REU DMAuci-combis on-chip on the first axis and REU-requiring on the second. The precise claim is that all three shipped images use the on-chip field layer, and that is what c64-nist-curves#123 blocks, because the broken define gates a TU (mul_8x8_onchip.s) that only the on-chip field layer compiles.And the distinction turns out to be load-bearing
Separating the axes surfaces something the loose wording hid: a fully alignable comb configuration already exists. v0.11.0 ships
lib-p256-comb— comb on the DMA field layer — and it builds clean under the full APP_OWNED gate:Its member set carries
mul_8x8.o, notmul_8x8_onchip.o, so it never reaches the defect.So the accurate statement of the constraint is not "we ship on-chip images and are therefore stuck". It is: the only comb arm that is conformance-clean today is the one whose performance we specifically rejected. The DMA field layer carries the REU floor — measured at D = 48.2 s for the REU verify profile on a U64E, and comb cannot remove it because comb reduces point operations while the floor is DMA-anchored. That puts a DMA-comb verify at ≥48 s at 48 MHz against the 16.402 s we measured for on-chip comb, i.e. roughly 3x worse at exactly the clocks the product targets.
Trading the flagship's headline number for a conformance property is not a trade worth making, so the conclusion stands. But it stands because the conformant arm is too slow, not because no conformant arm exists — and that is a materially different thing to record for whoever picks this up after #123.
JC-000 commentedon Aug 15, 2026
Second upstream blocker cleared: c64-nist-curves v0.11.1 fixes JC-000/c64-nist-curves#123 —
-D SHARED_CT_MUL_8X8now assembles against the on-chip TU (the deferring arm imports the five-symbol §8.3 provider surface:ct_mul_8x8,smc_sum_a_imm/smc_diff_a_imm, andpoly_prod_lo/hi), so the three onchip images can retire the glue-TU workaround and the member surgery together. Note the fifth-and-sixth symbols: your §8.3 provider must exportpoly_prod_lo/hi(the glue that.includes pristinemul_8x8.salready does). Pin v0.11.1: https://github.com/JC-000/c64-nist-curves/releases/tag/v0.11.1 (PRG byte-identical to v0.11.0; ABI stays 2; also carries the #121 precompute-cost correction).fix(contract): address review — CONSUMES is profile-dependent; retire…
Merge pull request #122 from JC-000/feat/spec-6.1-app-owned-v2