Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
06dcbb8
chore(integration): phase 0 — pre-seed sibling allowlists + helper sc…
JC-000 Apr 19, 2026
5e3ca0b
feat(crypto): Phase C.0 — REU-overlay infra + cfg rework
JC-000 Apr 19, 2026
06810b3
feat(crypto): Phase C.1 — x25519 as REU overlay (UCI backend)
JC-000 Apr 19, 2026
fa13dca
feat(crypto): Phase C.2 — relocate aux crypto code to LOADER_OVERFLOW
JC-000 Apr 19, 2026
07a4a32
feat(crypto): Phase C.3 — nist-curves P-384 primitives (build artifacts)
JC-000 Apr 19, 2026
c8f1297
fix(crypto): Phase C.3b — P-384 as external overlay image (no PRG link)
JC-000 Apr 19, 2026
742f48c
test(crypto): Phase E — crypto_init regression test
JC-000 Apr 19, 2026
822746a
fix(exports): promote cc20_data_ptr, cc20_remain, zp_ptr to labels.txt
JC-000 Apr 20, 2026
c0255d5
fix(tools/uci): defensive skip of non-C labels in _load_labels()
JC-000 Apr 20, 2026
39405bf
refactor(tools/uci): migrate _load_labels() to c64_test_harness.Labels
JC-000 Apr 20, 2026
0a4c9bc
chore(submodules): bump nist-curves + switch URLs to GitHub HTTPS
JC-000 Apr 20, 2026
ffbe38a
docs(phase-f): narrow x25519 TLS hang to Montgomery ladder
JC-000 Apr 20, 2026
34b80bf
revert(crypto): roll back x25519 REU overlay integration (C.1)
JC-000 Apr 21, 2026
06bf475
feat(crypto): Phase C.4 — sibling P-256 ECDSA (c64-nist-curves)
JC-000 Apr 21, 2026
7db6a44
docs(phase-f): trace capture analysis — bus-tap undersampling
JC-000 Apr 21, 2026
b8e890d
chore(phase-g): delete orphan in-tree ECDSA primitives
JC-000 Apr 21, 2026
a5cd44b
docs(phase-g): update CLAUDE.md for Phase C.4 end state
JC-000 Apr 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: 3 additions & 0 deletions .gitmodules
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
[submodule "ip65"]
path = ip65
url = https://github.com/cc65/ip65.git
[submodule "libs/nistcurves"]
path = libs/nistcurves
url = https://github.com/JC-000/c64-nist-curves.git
85 changes: 62 additions & 23 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,24 +51,29 @@ Public symbols (calling conventions are AX=pointer-low/high-byte except
where noted, buffers provided by caller, keys/IVs passed via fixed
buffers in the crypto BSS — see per-module headers for details):

X25519 / field arithmetic (c64-x25519 sibling)
X25519 / field arithmetic (in-tree; c64-x25519 overlay deferred, see #33)
x25519_scalarmult — X25519 scalar × point, 32-byte buffers
fe25519_mul, fe25519_sqr, fe25519_inv

ChaCha20-Poly1305 (c64-ChaCha20-Poly1305 sibling)
ChaCha20-Poly1305 (in-tree, permanent)
chacha20_encrypt
poly1305_init, poly1305_update, poly1305_final
aead_encrypt, aead_decrypt

SHA-256 (in-tree; no sibling)
sha256_init, sha256_update, sha256_final

ECDSA P-256 point ops (c64-nist-curves sibling)
ec_point_double, ec_point_add, ec_jacobian_to_affine
ECDSA P-256 (libs/nistcurves sibling, Phase C.4)
ecdsa_verify_256 — TLS dispatcher in src/crypto/ecdsa_verify.s
packs the BE struct + calls the sibling entry
ec_scalar_mul_var — variable-base scalar multiplication
(in-tree ecdsa_{curve,fp,mod,points}.s were deleted in Phase G)

P-384 is *stubbed* (see `project_p384_stubbed` memory note). The
`ecdsa_*_384.asm` files exist but are not assembled in the ca65 build
— they must be restored before real cert chains that require P-384.
P-384 is *stubbed at the TLS layer* (see `project_p384_stubbed` memory
note). The sibling `libs/nistcurves` P-384 primitives are buildable as
an external overlay image (Phase C.3b, `make p384-overlay`) but the
target has a pre-existing unresolved-symbol bug (`ec_base384_x/y` in
points384_raw.s) — fix that before wiring P-384 into the TLS path.

MEMORY requirements for a drop-in sibling library:
- Code + rodata must load into the `CRYPTO` region at **$6000-$9FFF**
Expand All@@ -78,8 +83,18 @@ MEMORY requirements for a drop-in sibling library:
- Zero-page usage is defined in `src/constants.inc` — fe25519 lives at
`$2C-$37`, x25519 state at `$38-$3A`, ECDSA bignum at `$22-$3C`.
These ranges are time-shared (fe25519 and ChaCha20 never overlap).
- REU Profile B is the baseline. `project_x25519_optimization` notes
that VICE needs `-reu -reusize 512` for the optimized X25519 tables.
- REU Profile B is the baseline. The shipped build does not currently
use REU banks 0-1 for optimised X25519 mul tables (the sibling
overlay integration in Phase C.1 was rolled back — see Known issues);
the in-tree x25519 implementation uses a smaller on-chip squaring
table in `TABLES_BSS`. Banks 4-7 are reserved for the P-384
precompute stashed by the `make p384-overlay` external-image
smoke test (Phase C.3b).
- `crypto_init` currently bootstraps `mul_tables_init` only. X25519
state and any per-run setup happens from the boot path in
`src/boot.s`. The overlay swap dispatcher
(`src/crypto/shared/crypto_swap.s`) is present but idle under the
shipped build.

## Networking backend ABI

Expand DownExpand Up@@ -212,8 +227,9 @@ see `tests/test_phase3_https_1mhz.py`). The flow, identical across both
backends:

- ClientHello → ServerHello (X25519 key share)
- EncryptedExtensions, Certificate, CertificateVerify (ECDSA-P256
verify against server.pem takes ~85 s wall-clock; see the ECDSA
- EncryptedExtensions, Certificate, CertificateVerify (sibling
c64-nist-curves P-256 ECDSA verify, Phase C.4 cc182f1; full
handshake measured at 81.9 s on U64E 48 MHz — see the ECDSA
benchmark subsection)
- Server Finished verified
- Client Finished computed + sent under HS write key
Expand DownExpand Up@@ -259,6 +275,15 @@ Five latent bugs and three new ones were cleared to get here:
delivered and the TLS reassembly buffer ended up gluing a prefix
of record N onto bytes from record N+1. Replaced with a
16-bit-safe copy loop that mirrors the UCI adapter.
11. Phase C.4 (`cc182f1`) — replaced the in-tree P-256 primitives
(`ecdsa_{curve,fp,mod,points}.s`) with the sibling
`libs/nistcurves/` P-256 integration (`build/lib/nistcurves-p256.a`,
always-resident under both backends). `src/crypto/ecdsa_verify.s`
is now a thin dispatcher that packs the big-endian input struct
and calls `ecdsa_verify_256`. Handshake wall-clock on U64E 48 MHz
dropped to **81.9 s** end-to-end. The orphan in-tree primitives
and legacy ACME-era `ecdsa_*_384.asm` stubs were physically
deleted in Phase G.

### Known issues

Expand DownExpand Up@@ -290,23 +315,37 @@ Five latent bugs and three new ones were cleared to get here:
`tools/uci/test_https_print_body.py` with a mixed-case response
body. `http_resp_buf` still holds raw ASCII — only the render
pipeline is translated.
- X25519 REU overlay deferred (c64-x25519 #33). Phase C.1 (`6c9d2a3`)
integrated the sibling optimised X25519 as a REU overlay but hung
inside the Montgomery ladder under BACKEND=uci at 48 MHz; rolled
back in `b133ac7`. A retry against the v0.3.0 tag failed the same
way. X25519 stays in-tree until the upstream hang is resolved.
- `make p384-overlay` has a pre-existing unresolved-symbol bug:
`points384_raw.s` references `ec_base384_x` / `ec_base384_y`
which aren't exported by the current sibling build. Not a Phase C
regression — the target has never built cleanly — but should be
fixed before P-384 is actually wired into the TLS path. TLS-level
P-384 verify remains stubbed regardless (see `project_p384_stubbed`).

### ECDSA P-256 verify wall-clock

`ecdsa_verify` of the RFC 6979 test vector on a U64E at 48 MHz turbo
runs in ~85 s median (see `tools/uci/bench_ecdsa_u64e.py` for the
protocol). The full `tls_connect` handshake — which does one
ECDSA verify over the CertificateVerify signature — takes ~110 s
wall-clock end-to-end. The remainder is network I/O + SHA-256 +
X25519 + Finished HMACs + handshake state-machine overhead.

~85 s does not fit a typical 10-30 s real-world server handshake
window, so the current implementation is a blocker for arbitrary
internet TLS targets that require ECDSA-P256 CertificateVerify.
runs in ~85 s median in the pre-Phase-C.4 benchmark (see
`tools/uci/bench_ecdsa_u64e.py` for the protocol). The full
`tls_connect` handshake — which does one ECDSA verify over the
CertificateVerify signature — now takes **81.9 s** wall-clock
end-to-end under Phase C.4's sibling `libs/nistcurves` P-256
integration, down from ~110 s pre-integration. The remainder is
network I/O + SHA-256 + X25519 + Finished HMACs + handshake
state-machine overhead.

81.9 s still does not fit a typical 10-30 s real-world server
handshake window, so this is a blocker for arbitrary internet TLS
targets that require ECDSA-P256 CertificateVerify.
It is fine for the local listener used by the e2e harness (600 s
budget, ample headroom). The speedup path is a sibling-style
optimized P-256 implementation (parallel to the `c64-x25519`
effort) that can be dropped in through the Crypto ABI without
budget, ample headroom). Further speedups live in the sibling
`libs/nistcurves` repo — any drop through the Crypto ABI lands
here as a submodule bump without
touching TLS call sites.

### Design note — bounded timeouts must use wall-clock time
Expand Down
84 changes: 79 additions & 5 deletions Makefile
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,29 +24,70 @@ IP65_DIR := ip65
IP65_BUILD := ip65-build
IP65_BIN := $(IP65_BUILD)/ip65-c64.bin

CA65FLAGS := -I src -I src/inc -I src/net/$(BACKEND) --debug-info
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

# Source inventory.
TOP_SRCS := $(wildcard src/*.s)
CRYPTO_SRCS := $(wildcard src/crypto/*.s)
# Crypto sources: wildcard-discovered. In-tree src/crypto/x25519.s +
# src/crypto/fe25519.s are used under both backends; the Phase C.1
# libs/x25519/ overlay integration was rolled back after it broke the
# TLS handshake under BACKEND=uci at 48 MHz — see the commit that
# removed libs/x25519 for details.
# Phase C.4: the in-tree P-256 primitives (ecdsa_{curve,fp,mod,points}.s)
# were replaced by the sibling `libs/nistcurves/` P-256 integration
# (build/lib/nistcurves-p256.a). The now-unused files were physically
# deleted in Phase G. ecdsa_verify.s stays — rewritten as a thin
# dispatcher that packs the BE struct + calls ecdsa_verify_256.
CRYPTO_SRCS_ALL := $(wildcard src/crypto/*.s)
# Shared crypto infrastructure introduced in Phase C.0: canonical ZP map,
# overlay swap dispatcher, init orchestrator, shared sqtab stub. Always
# linked; sibling-lib integration (Phase C.3) hangs off these.
CRYPTO_SHARED_SRCS := $(wildcard src/crypto/shared/*.s)
IP65_SRCS := src/net/ip65/ip65_blob.s src/net/ip65/net.s src/net/ip65/net_banner.s src/net/ip65/exports.s
UCI_SRCS := src/net/uci/net.s src/net/uci/uci_cmd.s

# Sibling-lib archive set. Phase C.3's nistcurves-p384 archive remains an
# external overlay image (see below), not linked into the main PRG.
# Phase C.4 adds nistcurves-p256.a which IS linked in, always-resident,
# for BOTH backends (replaces the in-tree ecdsa_{curve,fp,mod,points}.s).
SIBLING_LIB_ARCHIVES := build/lib/nistcurves-p256.a

# Per-backend source + object selection.
ifeq ($(BACKEND),ip65)
NET_SRCS := $(IP65_SRCS)
CRYPTO_SRCS := $(CRYPTO_SRCS_ALL)
else ifeq ($(BACKEND),uci)
NET_SRCS := $(UCI_SRCS)
CRYPTO_SRCS := $(CRYPTO_SRCS_ALL)
# 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.
# `USE_NISTCURVES_P384` toggles the `.ifdef` guard in
# src/crypto/p384_force_link.s so ld65 pulls the archive members into
# the final PRG. P-256 ECDSA stays in-tree under both backends.
# Phase C.3 is BLOCKED at the cfg level: the current CRYPTO_OVERLAY region
# (7.5 KB at $4200) can only hold one of OVERLAY_X25519 (3.4 KB) and
# OVERLAY_P384 (5.7 KB) at a time; ld65 lays them out sequentially and
# overflows by 1.6 KB. Architecturally max(x25519,p384)=5.7 KB ≤ 7.5 KB,
# so the slot is large enough — what's missing is the `run=CRYPTO_OVERLAY,
# load=<separate staging>` cfg plumbing plus a boot-time stash for both
# images. That restructure is out of scope for Phase C.3 and is gated on
# a supervisor OK. The archive + force-link stub are in place so the
# 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
else
$(error Unknown BACKEND=$(BACKEND); expected ip65 or uci)
endif

TOP_OBJS := $(patsubst src/%.s,build/%.o,$(TOP_SRCS))
CRYPTO_OBJS := $(patsubst src/%.s,build/%.o,$(CRYPTO_SRCS))
CRYPTO_SHARED_OBJS := $(patsubst src/%.s,build/%.o,$(CRYPTO_SHARED_SRCS))
NET_OBJS := $(patsubst src/%.s,build/%.o,$(NET_SRCS))

ALL_OBJS := $(TOP_OBJS) $(CRYPTO_OBJS) $(NET_OBJS)
ALL_OBJS := $(TOP_OBJS) $(CRYPTO_OBJS) $(CRYPTO_SHARED_OBJS) $(NET_OBJS)

PRG := build/c64-https.prg
LABELS := build/labels.txt
Expand All@@ -56,14 +97,16 @@ LABELS := build/labels.txt
all: $(PRG)

ifeq ($(BACKEND),ip65)
PRG_DEPS := $(ALL_OBJS) $(IP65_BIN)
PRG_DEPS := $(ALL_OBJS) $(IP65_BIN) $(SIBLING_LIB_ARCHIVES)
else ifeq ($(BACKEND),uci)
PRG_DEPS := $(ALL_OBJS) $(SIBLING_LIB_ARCHIVES)
else
PRG_DEPS := $(ALL_OBJS)
endif

$(PRG): $(PRG_DEPS)
@mkdir -p build
$(LD65) $(LD65FLAGS) -o $@ $(ALL_OBJS)
$(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)
Expand All@@ -74,6 +117,37 @@ 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:
@mkdir -p build/lib
bash tools/integration/build_nistcurves_p384.sh

# Phase C.4: c64-nist-curves P-256 archive — replaces the in-tree ECDSA
# P-256 primitives (ecdsa_{curve,fp,mod,points}.s) with the sibling's
# variable-base scalar mul + packaged ecdsa_verify_256. Always-resident;
# linked into the PRG under BOTH backends. See the build script for the
# full stripped-symbol list and the dispatcher (src/crypto/ecdsa_verify.s)
# for the 160-byte BE struct packing that bridges TLS to the sibling.
build/lib/nistcurves-p256.a:
@mkdir -p build/lib
bash tools/integration/build_nistcurves_p256.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.
#
# 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
bash tools/integration/build_nistcurves_p384_bin.sh

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

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