Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8cffc5b
test(https_e2e): add P-384 cert profile to test listener (Phase 4c)
JC-000 May 15, 2026
a473c33
chore(submodules): bump libs/nistcurves to 90830c9 (post-PR #23 + #24)
JC-000 May 15, 2026
bed099d
build(p384-overlay): wire SHA-384 + ecdsa_verify_with_message_384 wra…
JC-000 May 15, 2026
811158d
build(p384-overlay): split overlay into sha384 + curve halves to fit …
JC-000 May 16, 2026
09fe64d
feat(tls): negotiate ecdsa_secp384r1_sha384 (0x0503) in addition to P…
JC-000 May 16, 2026
f284efa
feat(crypto): activate dual-overlay swap dispatcher (P-384 SHA + curv…
JC-000 May 16, 2026
26a82d4
feat(crypto): TLS-side P-384 verify dispatcher (dual-overlay swap + s…
JC-000 May 16, 2026
baa2fb1
test(p384): KAT smoke test for ecdsa_verify_384 dual-overlay flow
JC-000 May 16, 2026
3fe02a7
fix(p384): shrink CertificateVerify signed blob 146 -> 130 B (Phase 5…
JC-000 May 16, 2026
1c31f8d
fix(p384): separate 48 B P-384 pubkey BSS slots (Phase 5 Fix B)
JC-000 May 16, 2026
251fe53
build(p384): generate overlay-resident equates from labels at build t…
JC-000 May 16, 2026
002952b
build(p384): order-only labels.txt dep on overlay-bin + script harden…
JC-000 May 16, 2026
42a8b1e
test(p384): tools/uci/test_https_local_p384.py + CLAUDE.md update (Ph…
JC-000 May 16, 2026
3c60550
fix(uci): bounded uci_wait_not_busy (TOD-budgeted, mirrors uci_wait_i…
JC-000 May 16, 2026
e52216e
refactor(tools/uci): factor P-384 arbiter monkey-patch into shared he…
JC-000 May 16, 2026
4c6d4a3
test(uci): adopt new c64-test-harness lock/health pattern in test_htt…
JC-000 May 16, 2026
0ae3ee5
test(uci): bump SENTINEL_POLL_TIMEOUT to 5400s for P-384 e2e
JC-000 May 16, 2026
8f28941
fix(uci): bound uci_drain_resp + uci_drain_status (TOD, mirrors uci_w…
JC-000 May 16, 2026
28e042e
build(make): emit cc65 debug info (-g + --dbgfile) for PRG + overlays
JC-000 May 16, 2026
f9f746d
docs(CLAUDE.md): note cc65 .dbg sidecars in Build section
JC-000 May 16, 2026
0dee841
tools/uci: memory policy candidate-list carveout (NET_BSS_TAIL + CRYP…
JC-000 May 21, 2026
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,8 @@ ip65-build/*.bin
ip65-build/*.map
.claude/*
!.claude/settings.json
tools/https_e2e/certs/
tools/https_e2e/certs/*
!tools/https_e2e/certs/README
tools/diag_4de0_*.py
tools/diag_read_live.py
.serena/
123 changes: 107 additions & 16 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,8 +16,11 @@ Dependencies:
- VICE (`x64sc`) only for `make run` / the test harness

Targets:
- `make` — default, produces `build/c64-https.prg`
and `build/labels.txt` (VICE label format)
- `make` — default, produces `build/c64-https.prg`,
`build/labels.txt` (VICE label format), and
`build/c64-https.dbg` (cc65 debug info,
consumable by VICE's monitor + diagnostic
agents; P-384 overlays get `.dbg` sidecars too)
- `make clean` — remove build artifacts
- `make run` — autostart the PRG in VICE
- `make ip65-libs` — rebuild ip65 object libraries from the submodule
Expand DownExpand Up@@ -143,18 +146,32 @@ is **$C9**. See `src/net/uci/uci_regs.inc` for the full equate list
### UCI command primitives

`src/net/uci/uci_cmd.s` provides shared subroutines used by `net.s`:
`uci_wait_idle`, `uci_begin_cmd`, `uci_push_wait`, `uci_end_cmd`,
`uci_read_data`, etc. No zero-page usage — all absolute addressing
and self-modifying code.

`uci_wait_idle` is wall-clock-bounded (5 s budget via CIA1 TOD) per
the design note below. On timeout it returns C=1 with `net_last_error
= UCI_ERR_WAIT_TIMEOUT`. The four callers (`net_dhcp_acquire`,
`net_tcp_connect`, `net_tcp_send`, `net_tcp_close`) all `bcs` out to
surface the failure rather than letting the C64 hang indefinitely on
a wedged FPGA. `uci_push_wait` and `uci_end_cmd` are still unbounded
and should be converted to the same TOD pattern if a wedge there is
ever observed.
`uci_wait_idle`, `uci_wait_not_busy`, `uci_begin_cmd`, `uci_push_wait`,
`uci_read_resp_bytes`, etc. No zero-page usage — all absolute
addressing and self-modifying code.

`uci_wait_idle`, `uci_wait_not_busy`, `uci_drain_resp`, and
`uci_drain_status` are all wall-clock-bounded (5 s budget via CIA1
TOD) per the design note below. On timeout they return C=1 with
`net_last_error = UCI_ERR_WAIT_TIMEOUT`. All `uci_wait_idle` callers
(`net_dhcp_acquire`, `net_tcp_connect`, `net_tcp_send`,
`net_tcp_close`) and all `uci_wait_not_busy` / `uci_push_wait`
callers (`net_poll`, `net_dhcp_acquire`, `net_tcp_connect`,
`net_tcp_send`, `net_tcp_close`) `bcs` out to surface the failure
rather than letting the C64 hang indefinitely on a wedged FPGA. All
13 `uci_drain_resp` / `uci_drain_status` call sites in `net.s` also
`bcs` out — on timeout the routine skips its companion drain + ack,
forces the appropriate `net_tcp_state` (ERROR for poll paths,
CONNECT_FAIL for connect, CLOSED for close, untouched for DHCP/send
which use C=1 as their fail sentinel), and returns. `uci_push_wait`
inherits the bound via its tail-call to `uci_wait_not_busy`. The
`uci_wait_not_busy` conversion was driven by a Phase 5 wedge observed
in CertVerify recv on real U64E hardware that converted a wedge into
a 1843 s test sentinel timeout; the drain conversion (Phase 5j)
closed the secondary risk that `net_tcp_send` / `net_poll` /
`net_tcp_close` could still wedge in `uci_drain_resp` /
`uci_drain_status` post-SOCKET_WRITE if firmware ever left DATA_AV /
STAT_AV asserted.

### UCI error codes

Expand DownExpand Up@@ -272,6 +289,28 @@ backends:
- `http_status = 200`, `http_resp_buf = "HELLO FROM TLS SERVER"`,
`http_resp_len = 21`

**ECDSA P-384 also wired end-to-end (Phase 5).** The TLS dispatcher
now negotiates `ecdsa_secp384r1_sha384` (0x0503) alongside the existing
P-256/SHA-256 path; on a 0x0503 CertificateVerify it routes through
`src/crypto/ecdsa_verify_384.s`, which composes the dual-overlay swap
(SHA-384 overlay → ECDSA-P384 curve overlay) plus the sibling's
`ecdsa_verify_384` to verify the server's signature. The
`tls_handle_certificate` cert handler dispatches on `ecdsa_curve_id`
and writes the 48 B P-384 pubkey into the dedicated
`ecdsa_pubkey_x_384` / `_y_384` slots in CRYPTO_BSS (Phase 5 Fix B).
The CertificateVerify signed-content blob is 130 B (RFC 8446 §4.4.3:
64-space pad + 33 B context + 1 B sep + 32 B SHA-256 transcript;
the transcript-hash function stays SHA-256 because c64-https
negotiates only TLS_AES_128_GCM_SHA256 — Phase 5 Fix A). The
end-to-end test is `tools/uci/test_https_local_p384.py` (mirrors
`test_https_local.py` with P-384 cert profile via swapping CERT_PATH
/ KEY_PATH to `tools/https_e2e/certs/server-p384.{pem,key}`); see the
"ECDSA P-384 verify wall-clock" subsection for the wall-clock
expectation. Negotiation plumbing test
`tools/test_tls_p384_negotiation.py` confirms ClientHello advertises
both 0x0403 + 0x0503 and the dispatcher reaches the P-384 path on
0x0503 CertificateVerify (2/2 PASS as of Phase 5).

### Summary of recent fixes (post-PR23 branch)

Five latent bugs and three new ones were cleared to get here:
Expand DownExpand Up@@ -431,10 +470,42 @@ budget, ample headroom). Further speedups live in the sibling
here as a submodule bump without
touching TLS call sites.

### ECDSA P-384 verify wall-clock

Not yet measured end-to-end. The U64E test host was unreachable from
the dev machine when Phase 5's e2e wiring landed (DeviceLock
unavailable; ping/TCP both unreachable to the default
192.168.1.81). Run `tools/uci/test_https_local_p384.py` from a host
with U64E LAN access to capture the number; the script defaults to a
30 minute wall-clock budget (`SENTINEL_POLL_TIMEOUT=1800` /
`ACCEPT_TIMEOUT=1800`) — expect 4-7 minutes per handshake at 48 MHz
turbo, dominated by:

- one ECDSA-P384 verify (sibling `libs/nistcurves`
`ecdsa_verify_384`); P-256 measures 81.9 s, the P-384 cost is
~5x because the field is 1.5x wider and the scalar mul does
proportionally more `fp_mul` / `fp_sqr` calls — extrapolate
~400 s = ~7 min ceiling
- one SHA-384 hash over the 130 B signed-content blob (negligible
vs the verify)
- the dual-overlay swap dance (sha384 overlay swap-in →
sha384_init/update/final → curve overlay swap-in → verify); each
swap is 2 REU DMAs at ~16 ms wallclock — also negligible
- X25519 + Finished HMACs + state-machine overhead (~6-7 s
across the rest of the handshake, per the P-256 baseline)

Once measured, drop the wall-clock here. Phase 4 cert-profile flag
in the local listener (`HTTPS_LISTENER_CERT_PROFILE=p384` or the
`cert_profile="p384"` kwarg to `start_https_listener`) is the
upstream selector; `tools/uci/test_https_local_p384.py` inlines its
own listener (matching `test_https_local.py`'s pattern) and points it
at `tools/https_e2e/certs/server-p384.{pem,key}`.

### Design note — bounded timeouts must use wall-clock time

Robustness work on the UCI adapter's spin-wait helpers (`uci_wait_idle`,
`uci_push_wait`, etc.) MUST use a wall-clock time source — CIA timer
`uci_wait_not_busy`, `uci_drain_resp`, `uci_drain_status`, etc.) MUST
use a wall-clock time source — CIA timer
on stock C64, TOD clock on U64E — rather than a cycle-counted iteration
budget. The fences around every UCI register access make per-iteration
cost scale with CPU speed: a budget that is ample at 1 MHz collapses
Expand All@@ -444,14 +515,34 @@ FPGA's wire-level operation durations. A prior attempt on branch
budgets and broke DHCP at turbo for exactly this reason; the branch
was abandoned.

`uci_wait_idle` is the first helper to follow this pattern (issue #37).
`uci_wait_idle` was the first helper to follow this pattern (issue #37).
At entry it samples CIA1 TOD ($DC08-$DC0B) — read order is HOUR
(latch) → MIN → SEC → TENTHS (unlatch) — and on each spin pass re-reads
TENTHS, bailing with C=1 + `net_last_error = UCI_ERR_WAIT_TIMEOUT`
after 50 transitions (~5 s wall-clock, independent of CPU turbo). State
lives in two SMC bytes inside the routine to match the file's no-ZP
convention. Use this as the template for any future bounded helper.

`uci_wait_not_busy` was converted to the same pattern after a Phase 5
wedge in CertVerify recv on real U64E hardware — the unbounded spin
turned an FPGA wedge into a 1843 s test sentinel timeout. Same 5 s
budget, same error code, same SMC-byte state convention. All six
caller sites (`net_poll`, `net_dhcp_acquire`, `net_tcp_connect`,
`net_tcp_send`, `net_tcp_close` direct + via `uci_push_wait`) `bcs`
out on C=1 to surface the timeout. `uci_push_wait` inherits the bound
via its tail-`jmp` into `uci_wait_not_busy` and needs no separate
conversion.

`uci_drain_resp` and `uci_drain_status` followed in Phase 5j to close
the symmetric risk on the response-drain side: `net_tcp_send` /
`net_poll` / `net_tcp_close` all call drains after their respective
SOCKET_WRITE / POLL_DATA / SOCKET_CLOSE responses, and if firmware
ever leaves DATA_AV / STAT_AV asserted post-response the old
unbounded `jmp <self>` loops would wedge the C64 with no wall-clock
escape. Same 5 s budget, same error code, same SMC-byte state
convention. All 13 call sites in `net.s` `bcs` out on C=1 to skip
the companion drain + ack and force the appropriate exit state.

## Memory layout

Defined in `cfg/c64-https-ip65.cfg`. Physically contiguous file-backed
Expand Down
122 changes: 107 additions & 15 deletions Makefile
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,8 +34,8 @@ IP65_DIR := ip65
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) --debug-info
LD65FLAGS := -C $(CFG) -Ln build/labels.txt -m build/c64-https.map
CA65FLAGS := -I src -I src/inc -I src/crypto/shared -I src/net/$(BACKEND) -I build --debug-info
LD65FLAGS := -C $(CFG) -Ln build/labels.txt -m build/c64-https.map --dbgfile build/c64-https.dbg

# Source inventory.
TOP_SRCS := $(wildcard src/*.s)
Expand DownExpand Up@@ -90,6 +90,22 @@ CRYPTO_SRCS := $(CRYPTO_SRCS_EFFECTIVE)
else ifeq ($(BACKEND),uci)
NET_SRCS := $(UCI_SRCS)
CRYPTO_SRCS := $(CRYPTO_SRCS_EFFECTIVE)
# Phase 3: embed the two P-384 split overlay blobs in the PRG so boot
# can populate REU banks 6/7 at startup. Gated to UCI (ip65 has no
# room for the SHA blob in main RAM) and to !USE_X25519_SIBLING (the
# sibling rodata occupies CRYPTO_OVERLAY at PRG load time, displacing
# the SHA blob). Adds a build-order dep on the .bin files; a missing
# .bin causes the .incbin to fail, so we extend PRG_DEPS below.
ifneq ($(USE_X25519_SIBLING),1)
# Phase 5 Fix D: respect a command-line USE_OVERLAY_P384_EMBED=0 so the
# bootstrap rule below can do a no-overlay-embed prelim link to break
# the overlay-bin <-> labels.txt cycle on a clean tree. Default is
# still 1 unless the operator explicitly disables it.
USE_OVERLAY_P384_EMBED ?= 1
ifeq ($(USE_OVERLAY_P384_EMBED),1)
CA65FLAGS += -D USE_OVERLAY_P384_EMBED=1
endif
endif
# Phase C.3: add c64-nist-curves P-384 primitives as a REU overlay.
# Variable-base P-384 point ops (double/add/jacobian-to-affine) only —
# see tools/integration/build_nistcurves_p384.sh for the scope rationale.
Expand All@@ -107,7 +123,12 @@ CRYPTO_SRCS := $(CRYPTO_SRCS_EFFECTIVE)
# integration can be re-enabled by uncommenting the two lines below once
# the cfg is extended.
#CA65FLAGS += -D USE_NISTCURVES_P384=1
#SIBLING_LIB_ARCHIVES += build/lib/nistcurves-p384.a
# Phase 1.5 split the monolithic nistcurves-p384.a into two halves
# (nistcurves-p384-sha384.a + nistcurves-p384-curve.a) since the
# combined image overflowed the live 7.5 KB CRYPTO_OVERLAY slot.
# Either-of approach for the production wire-up will be Phase 4a.
#SIBLING_LIB_ARCHIVES += build/lib/nistcurves-p384-sha384.a
#SIBLING_LIB_ARCHIVES += build/lib/nistcurves-p384-curve.a
else
$(error Unknown BACKEND=$(BACKEND); expected ip65 or uci)
endif
Expand All@@ -134,24 +155,51 @@ else
PRG_DEPS := $(ALL_OBJS)
endif

# Phase 3: when USE_OVERLAY_P384_EMBED is on, add the two .bin files
# to PRG_DEPS so make builds them before the .incbin in
# src/crypto/shared/p384_overlay_blobs.s tries to read them.
ifeq ($(USE_OVERLAY_P384_EMBED),1)
PRG_DEPS += build/lib/overlay-p384-sha384.bin build/lib/overlay-p384-curve.bin
PRG_DEPS += build/p384_overlay_equates.inc
build/crypto/shared/p384_overlay_blobs.o: build/lib/overlay-p384-sha384.bin build/lib/overlay-p384-curve.bin
endif

$(PRG): $(PRG_DEPS)
@mkdir -p build
$(LD65) $(LD65FLAGS) -o $@ $(ALL_OBJS) $(SIBLING_LIB_ARCHIVES)
# Rewrite ca65 label format `al XXXXXX .name` -> VICE format `al C:XXXX .name`
# so the c64-test-harness Labels.from_file() reader can parse it.
sed -i '' 's/^al 00\([0-9a-fA-F]\{4\}\) /al C:\1 /' $(LABELS)

# Phase 5 Fix D: $(LABELS) is normally a side-effect of the $(PRG)
# link recipe; we don't add an explicit rule. The overlay-bin rule
# below has an order-only dep on $(LABELS) so its lookup_label()
# resolves the main PRG's runtime mul_dma_lo / mul_dma_hi /
# mul_cached_a / reu_fetch_mul_row to real addresses (was: silent
# $0000 fallback that produced a curve overlay whose fp_mul_384
# read/wrote $0000 and silently corrupted downstream state).
#
# Bootstrap workflow (clean tree under USE_OVERLAY_P384_EMBED=1):
# make BACKEND=uci USE_OVERLAY_P384_EMBED=0 # produce labels.txt
# make BACKEND=uci # real link with overlays
# After this two-step bootstrap, plain `make BACKEND=uci` rebuilds
# incrementally without intervention. The script
# tools/integration/build_nistcurves_p384_bin.sh prints a clear error
# pointing at this two-step procedure if it runs without labels.txt
# (vs the old silent $0000 stub fallback).


link: $(PRG)

build/%.o: src/%.s
@mkdir -p $(dir $@)
$(CA65) $(CA65FLAGS) -o $@ $<

# Phase C.3: c64-nist-curves sibling archive (libs/nistcurves/ submodule).
# Same gating as x25519: only linked under BACKEND=uci; ip65 continues
# without P-384 entirely. Exports only the variable-base primitives
# (see the build script for the excluded symbols and why).
build/lib/nistcurves-p384.a:
# Phase 1.5 split: produces TWO archives, one per overlay half. The
# script writes both with a single invocation; the second target is a
# pseudo-rule that piggybacks on the first.
build/lib/nistcurves-p384-sha384.a build/lib/nistcurves-p384-curve.a:
@mkdir -p build/lib
bash tools/integration/build_nistcurves_p384.sh

Expand All@@ -176,18 +224,62 @@ build/lib/x25519.a:
@mkdir -p build/lib
bash tools/integration/build_x25519.sh

# Phase C.3b: P-384 overlay IMAGE + labels for harness-time use only.
# The production PRG does NOT link nistcurves-p384.a — this is smoke-test
# infrastructure. tools/test_p384_symbols.py loads overlay-p384.bin into
# REU at test time via a trampoline, then calls crypto_swap_to_p384 to
# page it into the live slot. Keeps the main PRG size unchanged.
# Phase C.3b / Phase 1.5 split: P-384 overlay IMAGES + labels for
# harness-time use only. The production PRG does NOT link
# nistcurves-p384-{sha384,curve}.a — these are smoke-test infrastructure.
# A future Phase 3 / Phase 4a harness will load both .bins into REU at
# test time, then DMA them into the live slot via two new swap entry
# points (crypto_swap_to_p384_sha384 / crypto_swap_to_p384_curve);
# the existing crypto_swap_to_p384 entry point is now stale -- see the
# comment block at the top of src/crypto/shared/crypto_swap.s.
#
# All four outputs (two .bins + two labels files) are produced by a
# single script invocation; the rule lists all four targets so make
# only runs the script once even when several are stale.
#
# Both outputs live below build/; depend on the archive being built first.
build/lib/overlay-p384.bin build/labels-p384.txt: build/lib/nistcurves-p384.a cfg/p384-overlay.cfg tools/integration/build_nistcurves_p384_bin.sh
# Phase 5 Fix D: build/labels.txt is an ORDER-ONLY dependency. The
# overlay-bin script's lookup_label() reads build/labels.txt to resolve
# mul_dma_lo / mul_dma_hi / mul_cached_a / reu_fetch_mul_row to the
# main PRG's runtime addresses (so the curve overlay's fp_mul_384
# reads/writes the right $BA00 / $BB00 / etc. cells). On a clean
# build, build/labels.txt doesn't exist yet when this rule runs and the
# script falls back to $0000 stubs - silently producing an overlay
# image whose fp_mul_384 reads from $0000. Order-only ('|') ensures
# labels.txt exists before the script runs but doesn't trigger an
# overlay rebuild on every main-PRG link.
build/lib/overlay-p384-sha384.bin build/lib/overlay-p384-curve.bin build/labels-p384-sha384.txt build/labels-p384-curve.txt: \
build/lib/nistcurves-p384-sha384.a build/lib/nistcurves-p384-curve.a \
cfg/p384-overlay-sha384.cfg cfg/p384-overlay-curve.cfg \
tools/integration/build_nistcurves_p384_bin.sh \
| build/labels.txt
bash tools/integration/build_nistcurves_p384_bin.sh

.PHONY: p384-overlay
p384-overlay: build/lib/overlay-p384.bin build/labels-p384.txt
p384-overlay: build/lib/overlay-p384-sha384.bin build/lib/overlay-p384-curve.bin \
build/labels-p384-sha384.txt build/labels-p384-curve.txt

# Phase 5 Fix C: regenerate the P-384 overlay-resident symbol equates
# (build/p384_overlay_equates.inc) from the overlay labels files so the
# TLS-side dispatcher (src/crypto/ecdsa_verify_384.s) picks up address
# changes via .include, with .assert pins catching drift. Whenever
# either labels file is rebuilt, the .inc regenerates and the
# dispatcher .o is forced to rebuild.
build/p384_overlay_equates.inc: build/labels-p384-sha384.txt build/labels-p384-curve.txt \
tools/integration/gen_p384_overlay_equates.sh
bash tools/integration/gen_p384_overlay_equates.sh \
build/labels-p384-sha384.txt build/labels-p384-curve.txt $@

# The dispatcher .o now depends on the generated equates file (via
# .include) AND on the overlay .bin files (PRG_DEPS already lists those
# under USE_OVERLAY_P384_EMBED). Phase 5 Fix D: gate the .inc dep on
# USE_OVERLAY_P384_EMBED so the bootstrap rule for $(LABELS) (which
# sub-makes with USE_OVERLAY_P384_EMBED=0) can skip rebuilding the .inc
# from labels-p384-* (those depend on overlay-bins which depend on
# $(LABELS) -- cycle). The bootstrap pre-creates a placeholder .inc
# before sub-making.
ifeq ($(USE_OVERLAY_P384_EMBED),1)
build/crypto/ecdsa_verify_384.o: build/p384_overlay_equates.inc
endif

# Build ip65 object libraries from the submodule. Only needed if the ip65
# submodule changes; the prebuilt blob is committed to ip65-build/.
Expand Down
Loading