Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Makefile
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,6 +36,19 @@ IP65_BUILD := ip65-build
IP65_BIN := $(IP65_BUILD)/ip65-c64.bin

CA65FLAGS := -I src -I src/inc -I src/crypto/shared -I src/net/$(BACKEND) -I build --debug-info
# Which backend is selected, as a ca65 define. Sources that must name a
# cfg-level symbol need this, because the two cfgs give the same region
# different names: the crypto code+rodata region is CRYPTO_HOT under UCI and
# CRYPTO_RESIDENT under ip65, so the ld65-published __<AREA>_SIZE__ symbol an
# `.import` has to spell differs by backend (src/contract_footprint_asserts.s
# is the current consumer). The `-I src/net/$(BACKEND)` path above cannot
# serve this: it carries net tuning, and region naming is a cfg property, not
# a networking one.
ifeq ($(BACKEND),uci)
CA65FLAGS += -D BACKEND_UCI=1
else
CA65FLAGS += -D BACKEND_IP65=1
endif
# Optional HTTPS target-port override (src/boot.s defaults to 443).
# `make HTTPS_PORT=4433` lets a test rig's TLS listener bind unprivileged.
ifdef HTTPS_PORT
Expand Down
4 changes: 2 additions & 2 deletions docs/library-ingestion-architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -351,7 +351,7 @@ Suitable to file as GitHub issues against `JC-000/c64-https`. Each is sized for

**Scope:** Split `cfg/c64-https-uci.cfg`'s `CRYPTO_RESIDENT` ($6000-$BFFF, 24 KB) into `CRYPTO_HOT` ($6000-$9FFF, 16 KB) and `CRYPTO_COLD_SHADOW` ($A000-$BFFF, 8 KB, under-BASIC-ROM RAM). Move ChaCha20-Poly1305 + SHA-256 + HKDF + TLS state machine + HTTP + record I/O into CRYPTO_HOT; move ECDSA-verify dispatcher, X25519 ECDH glue, per-curve scratch BSS into CRYPTO_COLD_SHADOW. CRYPTO_OVERLAY ($4200-$5FFF, 7.5 KB) absorbs all cold-path *code* via REU-paged overlays.

**Expected output:** `build/c64-https.prg` builds clean under `BACKEND=uci`. Smoke tests (`test_https_local.py`, `test_https_local_p384.py`) PASS. `build/labels.txt` shows CRYPTO_HOT under-allocated by ≥1 KB.
**Expected output:** `build/c64-https.prg` builds clean under `BACKEND=uci`. Smoke tests (`rig_https_local.py`, `rig_https_local_p384.py`) PASS. `build/labels.txt` shows CRYPTO_HOT under-allocated by ≥1 KB.

**Dependencies:** None; this work is c64-https-internal.

Expand DownExpand Up@@ -442,7 +442,7 @@ After all four cells build clean:
**Tier 2 — U64E (slow, gated, ~10-90 minutes):**
- Runs on self-hosted runner with LAN access to the U64E (probably `192.168.1.81` per the project memory).
- Acquires DeviceLock via `c64-test-harness` (queue-aware). If queue depth exceeds threshold (W7), skip with `:warning: queue busy, skipping U64E tier; will retry in 6h`.
- `tools/uci/test_https_local.py`, `test_https_local_p384.py`, `phase3_tcp_echo.py`, `boot_check.py`.
- `tools/uci/rig_https_local.py`, `rig_https_local_p384.py`, `phase3_tcp_echo.py`, `boot_check.py`.
- Wall-clock budgets per CLAUDE.md: ~82 s P-256 handshake, ~7 min P-384 handshake.

### 5.4 U64E hardware contention
Expand Down
119 changes: 119 additions & 0 deletions src/contract_footprint_asserts.s
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
; src/contract_footprint_asserts.s — c64-lib-contract SPEC §6.6 consumer
; footprint asserts.
;
; Emits no bytes. Companion to src/lib_contract_asserts.s, kept as a
; separate TU because §6.6 landed at contract v0.10.0 and its gating is
; profile-dependent in a way the §1/§3/§8.0/§13.3 gates are not (see the
; comb exclusion below).
;
; Contract: https://github.com/JC-000/c64-lib-contract — read SPEC.md on
; `main`, NOT the newest git tag (tags lag; main is v0.10.0 as of
; 2026-08-15). Clause referenced here: §6.6 (consumer footprint asserts).
;
; ---------------------------------------------------------------------
; WHAT §6.6 IS FOR
; ---------------------------------------------------------------------
; SPEC §6.6 exists because of a failure measured in THIS repo and filed
; upstream as c64-lib-contract#69: a MINOR library bump grows resident
; code or rodata, the consumer's region overflows at link time, and there
; is no advance signal that the bump was a spatial event. c64-https hit it
; twice — most recently v0.7.0's +208 B validation gate against CRYPTO_HOT's
; ~1 byte of slack, bisected across three tags to find.
;
; The clause's consumer pattern binds the library's declared footprint to
; a region budget:
;
; .import LIB_NISTCURVES_RESIDENT_BYTES
; .import LIB_NISTCURVES_COLD_BYTES
; .import __MAIN_SIZE__ ; cfg: MAIN: ... define = yes;
; .assert LIB_NISTCURVES_RESIDENT_BYTES + LIB_NISTCURVES_COLD_BYTES \
; <= __MAIN_SIZE__, lderror, "..."
;
; No cfg change was needed to adopt it: every MEMORY area in both
; cfg/c64-https-uci.cfg and cfg/c64-https-ip65.cfg already carries
; `define = yes`, so ld65 publishes __CRYPTO_HOT_SIZE__ /
; __CRYPTO_RESIDENT_SIZE__ already (verified in build/labels.txt).
;
; ---------------------------------------------------------------------
; WHAT THIS ASSERT ACTUALLY CATCHES — AND WHAT IT DOES NOT
; ---------------------------------------------------------------------
; Stated plainly, because a gate whose reach is overestimated is worse
; than no gate.
;
; It does NOT tightly bound region pressure. CRYPTO_HOT is $4000 (16,384 B)
; and holds c64-https's own crypto as well as the library, so at the v0.9.1
; pin the assert compares 8,700 + 430 = 9,130 against 16,384 — roughly
; 7 KB of slack, while the region's REAL free space is 81 bytes
; (__CRYPTO_HOT_LAST__ = $9FAF against a $A000 end, measured). A bump that
; grows the archive by 100 B still overflows the region without tripping
; this assert. Making it tight would require hardcoding a per-pin budget
; constant, which then needs maintenance on every bump and fires as a
; false alarm on any growth of c64-https's own code; that trade was
; considered and declined.
;
; What it DOES catch is a §6.4 per-variant-manifest regression, and that
; is not hypothetical — it is measured live in this tree on a sibling
; profile. §6.6's whole implication ("declared <= budget implies actual <=
; budget") rests on the manifest describing the archive we link. When it
; does not, the number is not conservative, it is meaningless. At the
; v0.9.1 pin, measured with od65 on the staged archives:
;
; profile manifest member RESIDENT COLD
; reu lib_manifest_p256verify.o 8700 430
; onchip lib_manifest_p256verify_onchip.o 8700 240
; onchip-comb lib_manifest_onchip.o 27000 1650 <- whole library
;
; 27,000 B is the whole-library figure against a 16,384 B region. That is
; the exact number that made §6.6 unadoptable before contract v0.9.0's
; §6.4, and it is still live for the comb profile — so if a future bump
; regresses `reu` or `onchip` the same way, this assert names it instead of
; leaving an opaque segment overflow to be bisected.
;
; ---------------------------------------------------------------------
; WHY THE COMB PROFILE IS EXCLUDED
; ---------------------------------------------------------------------
; The comb archive would FAIL this assert today (27,000 + 1,650 = 28,650
; against 16,384) and the failure would be a false alarm: the comb PRG
; links and runs, so the declared number is wrong, not the build.
;
; The cause is ours, not upstream's. `tools/integration/build_nistcurves_p256.sh`
; builds the comb profile from upstream's FULL `lib-onchip` archive and
; then `rm -f`s ~7 members (fp384, mod384, curve384, points384_*, ...) to
; narrow it to the P-256 comb set. Upstream's `lib_manifest_onchip.o`
; legitimately describes the archive upstream shipped; it survives our
; member surgery still describing the pre-surgery set. The `reu` and
; `onchip` profiles are unaffected because they build from upstream's
; already-minimal `lib-p256-verify` / `lib-p256-verify-onchip` targets,
; which carry per-variant manifests.
;
; This is precisely the harm SPEC §6.1 names: "an archive whose member set
; a consumer has edited is outside every §5/§8.0 manifest claim it ships."
; The sanctioned remedy is §6.2 `CONTRACT_DEFINES` / `CONTRACT_ZP_DEFINES`
; plus §6.3's `lib-app-owned` target, so the configuration is reachable
; without surgery. Neither exists in the pinned v0.9.1; both ARE implemented
; on libs/nistcurves master (v0.10.1, measured). Retiring the surgery is
; therefore unblocked by the wave bump and is sequenced in c64-https#70 —
; when it lands, delete the .ifdef below and the comb profile is covered
; by the same assert as the other two.
;
; The comb profile is deliberately excluded from `make package`, so no
; shipped artifact is affected by the gap this exclusion leaves open.

.ifndef USE_NISTCURVES_COMB

.import LIB_NISTCURVES_RESIDENT_BYTES
.import LIB_NISTCURVES_COLD_BYTES

; The code+rodata region differs by backend: CRYPTO_HOT under UCI,
; CRYPTO_RESIDENT under ip65. Both are $6000-$9FFF ($4000 bytes) and both
; are declared `define = yes`, so the published size symbol is the only
; thing that differs. BACKEND_UCI / BACKEND_IP65 come from the Makefile.
.ifdef BACKEND_UCI
.import __CRYPTO_HOT_SIZE__
.assert LIB_NISTCURVES_RESIDENT_BYTES + LIB_NISTCURVES_COLD_BYTES <= __CRYPTO_HOT_SIZE__, lderror, "libs/nistcurves declared footprint (LIB_NISTCURVES_RESIDENT_BYTES + _COLD_BYTES) exceeds CRYPTO_HOT. Contract SPEC 6.6. Most likely cause is a 6.4 regression: the archive ships a whole-library manifest instead of a per-variant one (27000 B is the whole-library value). Check with od65 --dump-exports on the staged lib_manifest*.o before assuming the library really grew."
.else
.import __CRYPTO_RESIDENT_SIZE__
.assert LIB_NISTCURVES_RESIDENT_BYTES + LIB_NISTCURVES_COLD_BYTES <= __CRYPTO_RESIDENT_SIZE__, lderror, "libs/nistcurves declared footprint (LIB_NISTCURVES_RESIDENT_BYTES + _COLD_BYTES) exceeds CRYPTO_RESIDENT. Contract SPEC 6.6. Most likely cause is a 6.4 regression: the archive ships a whole-library manifest instead of a per-variant one (27000 B is the whole-library value). Check with od65 --dump-exports on the staged lib_manifest*.o before assuming the library really grew."
.endif

.endif ; USE_NISTCURVES_COMB
87 changes: 68 additions & 19 deletions tools/integration/build_nistcurves_p256.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -95,24 +95,49 @@ AR65="${AR65:-ar65}"
# unused inside ZP_CRYPTO.
# Other slots match upstream defaults — see libs/nistcurves/src/zp_config.s.
#
# THE POINTER SLOT IS SPELLED `nistcurves_zp_ptr2` FROM v0.10.0, AND THE
# OLD SPELLING NOW HARD-ERRORS. The §6.5 rename window (upstream #107)
# made the four general-purpose scratch slots canonically
# `nistcurves_zp_{tmp1,tmp2,ptr1,ptr2}` and left the bare `zp_*` names as
# unconditional same-address aliases:
# THE POINTER SLOT IS SPELLED `nistcurves_zp_ptr2` FROM v0.10.0. The
# spelling is PROBED, never hardcoded, because both spellings are wrong at
# some pin this script has to build.
#
# c64-lib-contract SPEC §2 gained a ZP prefix registry at v0.9.0: a bare
# `zp_ptr2` is unregistered (three adopters had independently converged on
# bare zp_tmp1/zp_ptr1 — contract #83), so c64-nist-curves renamed its four
# general-scratch slots to the registered `nistcurves_zp_*` family and left
# the bare names as aliases for the §6.5 rename window, in the "loud-break"
# shape that clause ratifies:
#
# .ifndef nistcurves_zp_ptr2
# nistcurves_zp_ptr2 = $fd
# .endif
# zp_ptr2 = nistcurves_zp_ptr2 <- NOT .ifndef-guarded
#
# so `-D zp_ptr2=$3d` no longer suppresses a guarded definition; it
# collides with the alias assignment and stops the build with
# `zp_config.s(56): Error: Symbol 'zp_ptr2' is already defined`. That is
# the good case — a loud failure, not a silently-dropped override. The
# `fp_*` names are documented override knobs and were not renamed.
# Measured, ca65 V2.18, against each pin's own zp_config.s:
#
# spelling v0.9.1 v0.10.1
# -D 'zp_ptr2=$3d' overrides correctly Error: Symbol
# 'zp_ptr2' is
# already defined
# -D 'nistcurves_zp_ptr2=$3d' defines an unused overrides both
# symbol; real slot names to $3D
# stays at $fd
#
# Neither spelling is safe across both, so probe the library source and
# follow it. Note the v0.9.1 + canonical cell is a *wrong value*, not a
# silent one: the `check_zp_slot` guards below read the emitted object with
# od65, so that combination stops the build (as `$fd` != `$3d`, or as
# `<absent>` if the guard is aimed at the canonical name). The probe's value
# is being loud AND correct at both pins, rather than merely loud at one.
#
# fp_mul_i / fp_mul_j need no probe: `fp_` is a registered §2 prefix for
# c64-nist-curves, so those names are canonical already and keep their
# `.ifndef` guards across the migration (verified on v0.10.1).
if grep -qE '^[[:space:]]*\.ifndef[[:space:]]+nistcurves_zp_ptr2[[:space:]]*$' "$LIB_SRC/zp_config.s"; then
ZP_PTR2_SLOT='nistcurves_zp_ptr2'
else
ZP_PTR2_SLOT='zp_ptr2'
fi
ZP_OVERRIDES=(
'-D' 'nistcurves_zp_ptr2=$3d'
'-D' "$ZP_PTR2_SLOT=\$3d"
'-D' 'fp_mul_i=$39'
'-D' 'fp_mul_j=$3a'
)
Expand DownExpand Up@@ -189,14 +214,38 @@ check_zp_slot() {
exit 1
fi
}
# Check the CANONICAL name. The bare `zp_ptr2` alias is also exported in a
# default build and carries the same value, but it disappears under
# `-D LIB_NO_BARE_EXPORTS=1` (SPEC §6.5) and is removed outright at the
# next MAJOR — checking it would make this guard silently vacuous exactly
# when a consumer tightens the build.
check_zp_slot nistcurves_zp_ptr2 61 # $3d
check_zp_slot fp_mul_i 57 # $39
check_zp_slot fp_mul_j 58 # $3a
# 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
if [ "$got" != "$want" ]; then
echo "ERROR: $ZP_MEMBER exports $name = $got, expected $want (c64-https ZP override did not take)" >&2
exit 1
fi
}

# 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
fi

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 ---
# `reu_mul_init.o` is the SPEC §8.2 `reu_mul` provider. c64-https is the
Expand Down
24 changes: 22 additions & 2 deletions tools/integration/build_nistcurves_p384.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,8 +54,20 @@ CA65="${CA65:-ca65}"
AR65="${AR65:-ar65}"

# --- ZP-slot overrides (c64-https canonical map + SHA-384 isolated window) ---
# zp_ptr2's SPELLING is pin-dependent and probed, never hardcoded — see the
# long rationale in build_nistcurves_p256.sh (contract SPEC §2 ZP prefix
# registry + §6.5's loud-break alias shape: on a migrated pin the bare
# spelling is an unguarded alias and `-D zp_ptr2=...` dies with
# "Symbol 'zp_ptr2' is already defined"). `fp_`/`sha_` are registered §2
# prefixes for c64-nist-curves and keep their `.ifndef` guards, so only this
# one slot needs the probe.
if grep -qE '^[[:space:]]*\.ifndef[[:space:]]+nistcurves_zp_ptr2[[:space:]]*$' "$LIB_SRC/zp_config.s"; then
ZP_PTR2_SLOT='nistcurves_zp_ptr2'
else
ZP_PTR2_SLOT='zp_ptr2'
fi
ZP_OVERRIDES=(
'-D' 'zp_ptr2=$3d'
'-D' "$ZP_PTR2_SLOT=\$3d"
'-D' 'fp_mul_i=$39'
'-D' 'fp_mul_j=$3a'
# SHA-384 streaming pointer slots (moved out of $04-$0b defaults
Expand DownExpand Up@@ -168,7 +180,15 @@ for tree in sha curve; do
"${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
# Canonical spelling first (see build_nistcurves_p256.sh). Every check here
# is presence-tolerant because the sha and curve trees export disjoint slot
# subsets — the sha tree has no zp_ptr2 at all — so absence is legitimate
# per-tree and cannot be asserted away. The bare alias is additionally
# optional because `-D LIB_NO_BARE_EXPORTS=1` removes it (§1/§6.5).
check_zp_slot_if_present "$STAGING/$tree/$zp_member" "$ZP_PTR2_SLOT" 61 # $3d
if [ "$ZP_PTR2_SLOT" != "zp_ptr2" ]; then
check_zp_slot_if_present "$STAGING/$tree/$zp_member" zp_ptr2 61 # $3d — deprecated alias
fi
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
Expand Down