diff --git a/.gitmodules b/.gitmodules index 68df2f4..f8f2e68 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index daa4808..75806e1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,11 +51,11 @@ 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 @@ -63,12 +63,17 @@ 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 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** @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/Makefile b/Makefile index fb4f961..68e3cd4 100644 --- a/Makefile +++ b/Makefile @@ -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=` 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 @@ -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) @@ -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: diff --git a/cfg/c64-https-ip65.cfg b/cfg/c64-https-ip65.cfg index 2289df4..5caeb26 100644 --- a/cfg/c64-https-ip65.cfg +++ b/cfg/c64-https-ip65.cfg @@ -1,58 +1,108 @@ # c64-https ld65 config — ip65/RR-Net backend # -# MEMORY map is load-bearing and derived from the ACME build: -# $0801-$1FFF : LOADER (BASIC stub + boot + tls + http + net wrapper) -# $2000-$3FFF : NET_CODE (ip65 code, delivered as .incbin blob for now) -# $4000-$5FFF : NET_BSS (ip65 BSS, not written to file) -# $6000-$9FFF : CRYPTO (all crypto code + tables, must stay below $A000) -# $A000-$BFFF : SHADOW_BSS (mutable state behind BASIC ROM shadow, port=$36) -# $C000-$CFFF : TCP_BUF (tcp_recv_buf, 4KB ring) +# Memory map: +# $0801-$1FFF : LOADER (BASIC stub + boot + tls + http + net wrapper) +# $2000-$3FFF : NET_CODE (ip65 blob + LOADER_OVERFLOW tail) +# $4000-$5FFF : NET_BSS (ip65 BSS, fully used) +# $6000-$BFFF : CRYPTO_RESIDENT (24 KB — covers old CRYPTO + SHADOW_BSS span) +# $C000-$CFFF : TCP_BUF (tcp_recv_buf, 4 KB ring) +# +# The in-tree src/crypto/x25519.s + src/crypto/fe25519.s provide +# the x25519 primitives under ip65. Phase C.1 attempted a sibling-lib +# overlay integration under BACKEND=uci only; that integration was +# rolled back (see cfg/c64-https-uci.cfg for details). ip65 never +# adopted the overlay, so this cfg is unchanged from the Phase C.0 +# single contiguous CRYPTO_RESIDENT layout. +# +# CPU port $01 is set to $36 at boot (BASIC ROM off), so $8000-$BFFF is +# plain RAM under what would otherwise be BASIC ROM shadow + the top of +# the character ROM gap. FEATURES { STARTADDRESS: default = $0801; } MEMORY { - ZP_IP65: start = $0002, size = $001A, type = rw, define = yes; - ZP_CRYPTO: start = $0022, size = $001E, type = rw, define = yes; - ZP_WIDE: start = $0040, size = $0040, type = rw, define = yes; - - LOADADDR: start = $07FF, size = $0002, file = %O; - LOADER: start = $0801, size = $17FF, file = %O, define = yes, fill = yes, fillval = $00; - NET_CODE: start = $2000, size = $2000, file = %O, define = yes, fill = yes, fillval = $00; - NET_BSS: start = $4000, size = $2000, file = %O, define = yes, fill = yes, fillval = $00; - CRYPTO: start = $6000, size = $4000, file = %O, define = yes, fill = yes, fillval = $00; - - SHADOW_BSS: start = $A000, size = $2000, type = rw, define = yes; - TCP_BUF: start = $C000, size = $1000, type = rw, define = yes; + ZP_IP65: start = $0002, size = $001A, type = rw, define = yes; + ZP_CRYPTO: start = $0022, size = $001E, type = rw, define = yes; + ZP_WIDE: start = $0040, size = $0040, type = rw, define = yes; + + LOADADDR: start = $07FF, size = $0002, file = %O; + LOADER: start = $0801, size = $17FF, file = %O, define = yes, fill = yes, fillval = $00; + NET_CODE: start = $2000, size = $2000, file = %O, define = yes, fill = yes, fillval = $00; + # Phase C.4: NET_BSS is split into the ip65-blob-owned portion + # ($4000-$4F8B per ip65-build/ip65-c64.map, rounded up to $4F8C = + # $F8C B) and a tail slack region reused for TLS_CODE + CRYPTO_AUX_CODE + # relocation. Sibling P-256 integration pushed CRYPTO_RESIDENT over + # its 24 KB budget under ip65; UCI had NET_BSS at only $4000-$41FF so + # it already had headroom. The blob's reserved BSS stops exactly at + # $4F8B; NET_BSS_TAIL starts at the next byte so no blob state is + # clobbered by the relocated code. + NET_BSS: start = $4000, size = $0F8C, file = %O, define = yes, fill = yes, fillval = $00; + NET_BSS_TAIL: start = $4F8C, size = $1074, file = %O, define = yes, fill = yes, fillval = $00; + CRYPTO_RESIDENT: start = $6000, size = $6000, file = %O, define = yes, fill = yes, fillval = $00; + + # CRYPTO_OVERLAY is not used under ip65 (no REU-overlay swapping). + # A zero-size rw alias is declared here only to satisfy + # `crypto_swap.s`'s `.import __CRYPTO_OVERLAY_START__` — ip65 never + # actually issues the DMA, so the address value is unused. + CRYPTO_OVERLAY: start = $6000, size = $0000, type = rw, define = yes; + + TCP_BUF: start = $C000, size = $1000, type = rw, define = yes; } SEGMENTS { - ZP_SHARED: load = ZP_IP65, type = zp, optional = yes; - ZEROPAGE: load = ZP_CRYPTO, type = zp, optional = yes; - ZP_WIDE: load = ZP_WIDE, type = zp, optional = yes; - - LOADADDR: load = LOADADDR, type = ro; - EXEHDR: load = LOADER, type = ro; - STARTUP: load = LOADER, type = ro, optional = yes; - CODE: load = LOADER, type = ro; - RODATA: load = CRYPTO, type = ro; - INIT: load = LOADER, type = ro, optional = yes; - - NET_CODE: load = NET_CODE, type = ro; - # LOADER is historically packed; a small overflow segment rides along - # with the ip65 blob in NET_CODE (the blob is pre-linked to $2000 but - # only fills ~7 KB of the 8 KB region, leaving ~1 KB of tail slack). - LOADER_OVERFLOW: load = NET_CODE, type = ro, optional = yes; - NET_BSS: load = NET_BSS, type = bss, optional = yes; - - CRYPTO_CODE: load = CRYPTO, type = ro; - CRYPTO_RODATA: load = CRYPTO, type = ro; - TLS_CODE: load = CRYPTO, type = ro; - - BSS: load = SHADOW_BSS, type = bss; - CRYPTO_BSS: load = SHADOW_BSS, type = bss; - TABLES_BSS: load = CRYPTO, type = bss, align = $100; - - TCP_RECV_BUF: load = TCP_BUF, type = bss, optional = yes; + ZP_SHARED: load = ZP_IP65, type = zp, optional = yes; + ZEROPAGE: load = ZP_CRYPTO, type = zp, optional = yes; + ZP_WIDE: load = ZP_WIDE, type = zp, optional = yes; + + LOADADDR: load = LOADADDR, type = ro; + EXEHDR: load = LOADER, type = ro; + STARTUP: load = LOADER, type = ro, optional = yes; + CODE: load = LOADER, type = ro; + RODATA: load = CRYPTO_RESIDENT, type = ro; + INIT: load = LOADER, type = ro, optional = yes; + + NET_CODE: load = NET_CODE, type = ro; + # LOADER-overflow rides along with the ip65 blob in the NET_CODE tail. + # Phase C.4: CRYPTO_AUX_CODE2 (hmac_drbg alone) also rides the NET_CODE + # tail under ip65 because TLS_CODE + sha256's CRYPTO_AUX_CODE together + # would overflow NET_BSS_TAIL by 23 bytes if hmac_drbg were co-located + # there. NET_CODE has ~1 KB of tail slack after the ip65 blob + + # LOADER_OVERFLOW, which covers hmac_drbg's ~830 B. sha256 rides + # NET_BSS_TAIL alongside TLS_CODE. + LOADER_OVERFLOW: load = NET_CODE, type = ro, optional = yes; + CRYPTO_AUX_CODE2: load = NET_CODE, type = ro, optional = yes; + NET_BSS: load = NET_BSS, type = bss, optional = yes; + # Phase C.4: TLS_CODE + CRYPTO_AUX_CODE (sha256) relocate out of + # CRYPTO_RESIDENT into NET_BSS_TAIL. hmac_drbg (CRYPTO_AUX_CODE2) + # goes to NET_CODE instead — see the NET_CODE SEGMENTS block. + TLS_CODE: load = NET_BSS_TAIL, type = ro, optional = yes; + CRYPTO_AUX_CODE: load = NET_BSS_TAIL, type = ro, optional = yes; + + # --- Overlay slot placeholders (unused under ip65). --- + # Declared `optional = yes` + `load = CRYPTO_RESIDENT` as harmless + # anchors so shared code that references the segment names still + # links; they receive no bytes under ip65 because no overlay + # archives are linked. + OVERLAY_P256: load = CRYPTO_RESIDENT, type = ro, optional = yes; + OVERLAY_P384: load = CRYPTO_RESIDENT, type = ro, optional = yes; + + # --- Resident crypto + TLS code / rodata. --- + # Phase C.2 backend-divergence: under UCI, TLS_CODE and CRYPTO_AUX_CODE + # (SHA-256 + HMAC-DRBG + ecdsa_verify dispatcher) relocate to NET_CODE + # to free headroom for Phase C.3 overlays. Phase C.4 gives ip65 the + # same treatment but routes them into NET_BSS_TAIL instead (NET_CODE + # is ~88% full with the ip65 blob under ip65). + CRYPTO_CODE: load = CRYPTO_RESIDENT, type = ro; + CRYPTO_RODATA: load = CRYPTO_RESIDENT, type = ro; + RESIDENT_RODATA: load = CRYPTO_RESIDENT, type = ro, optional = yes; + + # --- Resident BSS. Everything that used to live in SHADOW_BSS now + # shares CRYPTO_RESIDENT; TABLES_BSS keeps page alignment. + BSS: load = CRYPTO_RESIDENT, type = bss; + CRYPTO_BSS: load = CRYPTO_RESIDENT, type = bss; + TABLES_BSS: load = CRYPTO_RESIDENT, type = bss, align = $100; + + TCP_RECV_BUF: load = TCP_BUF, type = bss, optional = yes; } diff --git a/cfg/c64-https-uci.cfg b/cfg/c64-https-uci.cfg index 9fcbc74..49ca81f 100644 --- a/cfg/c64-https-uci.cfg +++ b/cfg/c64-https-uci.cfg @@ -3,12 +3,34 @@ # Target: Commodore Ultimate 64 / U64E using the host-visible UCI # ($DF1B-$DF1F) in place of ip65 + RR-Net. # -# MEMORY map mirrors the ip65 cfg so PRG offsets line up with the -# legacy build (Phase 8 can compact the layout later). The NET_CODE -# and NET_BSS regions are unused by UCI code but are kept as fill so -# the load image has the same shape as the ip65 PRG; NET_BSS is -# repurposed as a UCI-only BSS region (UCI_BSS segment) for the -# 256 B uci_host_buf reservation. +# Memory map: +# $0801-$1FFF : LOADER (BASIC stub + boot + http + net wrapper) +# $2000-$3FFF : NET_CODE (UCI adapter + LOADER_OVERFLOW tail + +# TLS_CODE + CRYPTO_AUX_CODE) +# $4000-$41FF : UCI_BSS (uci_host_buf + state, 512 B) +# $4200-$5FFF : CRYPTO_OVERLAY (7.5 KB swappable overlay slot — used +# by the external P-384 smoke test only) +# $6000-$BFFF : CRYPTO_RESIDENT (24 KB always-resident crypto + TLS + BSS) +# $C000-$CFFF : TCP_BUF (tcp_recv_buf, 4 KB ring) +# +# NOTE on UCI_BSS size: the plan's "256 B" target was optimistic — +# `src/net/uci/net.s` + `uci_cmd.s` allocate ~289 B (uci_host_buf 256 B + +# uci_ipaddr_resp 12 B + uci_socket_id/port/send/poll/... ~16 B + +# uci_resp control block 4 B). Rounded up to the next page (512 B) and +# the overlay slot trimmed accordingly (7.5 KB vs. 8 KB under ip65). +# +# CRYPTO_RESIDENT is 24 KB on UCI (vs 16 KB target on ip65) because UCI +# does not need $4000-$5FFF for backend BSS, letting CRYPTO_RESIDENT +# start at $6000 and claim the former NET_BSS space. +# +# --- x25519 overlay rollback note --- +# Phase C.1 (commit 6c9d2a3) integrated libs/x25519/ as a REU overlay +# and split CRYPTO_RESIDENT around a 1 KB sqtab hole. That integration +# deadlocked the TLS handshake at 48 MHz (x25519_scalarmult hung from +# TLS context) and was rolled back. The in-tree src/crypto/x25519.s + +# src/crypto/fe25519.s are now used under both backends. CRYPTO_OVERLAY +# still exists to serve the P-384 external smoke test (Phase C.3b, +# tools/test_p384_symbols.py). FEATURES { STARTADDRESS: default = $0801; @@ -18,47 +40,63 @@ MEMORY { ZP_CRYPTO: start = $0022, size = $001E, type = rw, define = yes; ZP_WIDE: start = $0040, size = $0040, type = rw, define = yes; - LOADADDR: start = $07FF, size = $0002, file = %O; - LOADER: start = $0801, size = $17FF, file = %O, define = yes, fill = yes, fillval = $00; - NET_CODE: start = $2000, size = $2000, file = %O, define = yes, fill = yes, fillval = $00; - NET_BSS: start = $4000, size = $2000, file = %O, define = yes, fill = yes, fillval = $00; - CRYPTO: start = $6000, size = $4000, file = %O, define = yes, fill = yes, fillval = $00; + LOADADDR: start = $07FF, size = $0002, file = %O; + LOADER: start = $0801, size = $17FF, file = %O, define = yes, fill = yes, fillval = $00; + NET_CODE: start = $2000, size = $2000, file = %O, define = yes, fill = yes, fillval = $00; + UCI_BSS_REGION: start = $4000, size = $0200, file = %O, define = yes, fill = yes, fillval = $00; + CRYPTO_OVERLAY: start = $4200, size = $1E00, file = %O, define = yes, fill = yes, fillval = $00; + CRYPTO_RESIDENT: start = $6000, size = $6000, file = %O, define = yes, fill = yes, fillval = $00; - SHADOW_BSS: start = $A000, size = $2000, type = rw, define = yes; TCP_BUF: start = $C000, size = $1000, type = rw, define = yes; } SEGMENTS { - ZEROPAGE: load = ZP_CRYPTO, type = zp, optional = yes; - ZP_WIDE: load = ZP_WIDE, type = zp, optional = yes; + ZEROPAGE: load = ZP_CRYPTO, type = zp, optional = yes; + ZP_WIDE: load = ZP_WIDE, type = zp, optional = yes; + + LOADADDR: load = LOADADDR, type = ro; + EXEHDR: load = LOADER, type = ro; + STARTUP: load = LOADER, type = ro, optional = yes; + CODE: load = LOADER, type = ro; + RODATA: load = CRYPTO_RESIDENT, type = ro; + INIT: load = LOADER, type = ro, optional = yes; - LOADADDR: load = LOADADDR, type = ro; - EXEHDR: load = LOADER, type = ro; - STARTUP: load = LOADER, type = ro, optional = yes; - CODE: load = LOADER, type = ro; - RODATA: load = CRYPTO, type = ro; - INIT: load = LOADER, type = ro, optional = yes; + # NET_CODE hosts the UCI adapter + the LOADER_OVERFLOW tail. + # Phase C.2 (UCI only): TLS_CODE and CRYPTO_AUX_CODE (SHA-256, + # HMAC-DRBG, ecdsa_verify dispatcher) are relocated here as well + # to open headroom in CRYPTO_RESIDENT. The UCI adapter is ~1.7 KB + # so NET_CODE has ~6.3 KB free after UCI_CODE + LOADER_OVERFLOW; + # TLS_CODE ~1.8 KB + CRYPTO_AUX_CODE ~3 KB fits with room to spare. + NET_CODE: load = NET_CODE, type = ro, optional = yes; + UCI_CODE: load = NET_CODE, type = ro, optional = yes; + LOADER_OVERFLOW: load = NET_CODE, type = ro, optional = yes; + TLS_CODE: load = NET_CODE, type = ro, optional = yes; + CRYPTO_AUX_CODE: load = NET_CODE, type = ro, optional = yes; + # Phase C.4: hmac_drbg.s moved to CRYPTO_AUX_CODE2 so the ip65 cfg + # can split it off into NET_CODE while sha256 stays in NET_BSS_TAIL. + # Under UCI both segments flow into NET_CODE identically. + CRYPTO_AUX_CODE2: load = NET_CODE, type = ro, optional = yes; + UCI_BSS: load = UCI_BSS_REGION, type = bss, optional = yes; - # NET_CODE / NET_BSS retained so physical layout matches the ip65 - # build. Under BACKEND=uci the UCI adapter code (src/net/uci/*.s) - # is placed in NET_CODE via the UCI_CODE segment so it doesn't - # squeeze the LOADER region. NET_BSS is reclaimed for the UCI- - # owned BSS (uci_host_buf lives here). - NET_CODE: load = NET_CODE, type = ro, optional = yes; - UCI_CODE: load = NET_CODE, type = ro, optional = yes; - # Overflow segment for LOADER-resident code that no longer fits in - # the $17FF-byte LOADER region. Rides in NET_CODE after the UCI - # adapter. See the matching entry in c64-https-ip65.cfg. - LOADER_OVERFLOW: load = NET_CODE, type = ro, optional = yes; - UCI_BSS: load = NET_BSS, type = bss, optional = yes; + # --- Overlay slot: used only by the P-384 external smoke test. --- + # The P-256 / P-384 OVERLAY segments are declared so ld65 has a + # valid load address even when nothing is currently linked. The + # production PRG does not embed any overlay image; the P-384 test + # harness DMAs a standalone overlay-p384.bin image into this slot + # at test time (see tools/test_p384_symbols.py). + OVERLAY_P256: load = CRYPTO_OVERLAY, type = ro, optional = yes; + OVERLAY_P384: load = CRYPTO_OVERLAY, type = ro, optional = yes; - CRYPTO_CODE: load = CRYPTO, type = ro; - CRYPTO_RODATA: load = CRYPTO, type = ro; - TLS_CODE: load = CRYPTO, type = ro; + # --- Resident crypto + TLS code / rodata. --- + CRYPTO_CODE: load = CRYPTO_RESIDENT, type = ro; + CRYPTO_RODATA: load = CRYPTO_RESIDENT, type = ro; + RESIDENT_RODATA: load = CRYPTO_RESIDENT, type = ro, optional = yes; + CRYPTO_INIT_CODE: load = CRYPTO_RESIDENT, type = ro, optional = yes; - BSS: load = SHADOW_BSS, type = bss; - CRYPTO_BSS: load = SHADOW_BSS, type = bss; - TABLES_BSS: load = CRYPTO, type = bss, align = $100; + # --- Resident BSS. --- + BSS: load = CRYPTO_RESIDENT, type = bss; + CRYPTO_BSS: load = CRYPTO_RESIDENT, type = bss; + TABLES_BSS: load = CRYPTO_RESIDENT, type = bss, align = $100; - TCP_RECV_BUF: load = TCP_BUF, type = bss, optional = yes; + TCP_RECV_BUF: load = TCP_BUF, type = bss, optional = yes; } diff --git a/cfg/p384-overlay.cfg b/cfg/p384-overlay.cfg new file mode 100644 index 0000000..5ddd902 --- /dev/null +++ b/cfg/p384-overlay.cfg @@ -0,0 +1,42 @@ +# cfg/p384-overlay.cfg — minimal ld65 config for extracting the P-384 +# OVERLAY image as a standalone binary, used only by +# `tools/integration/build_nistcurves_p384_bin.sh`. +# +# NOT USED by the main c64-https PRG build. The production PRG does NOT +# link the P-384 archive (Phase C.3b keeps P-384 external / smoke-test-only); +# this cfg exists purely so we can extract a padded 8 KB binary image plus +# a VICE-format labels file that `tools/test_p384_symbols.py` loads into +# REU at harness time. +# +# Layout: +# $4200-$61FF : OVERLAY_P384 region (8 KB, padded with $00). Matches the +# CRYPTO_OVERLAY base under the UCI cfg so the image DMAs +# into the live overlay slot cleanly at harness time. +# $C000-$CFFF : RESIDENT — holds the P-384 RW buffers (DATA / BSS). +# These addresses intentionally land inside TCP_BUF +# ($C000-$CFFF) because networking is NOT active during +# the P-384 smoke test — the TCP ring is free space. +# This avoids clashing with the main PRG's CRYPTO code +# segments at $7C00-$BFFF which remain live. + +FEATURES { + STARTADDRESS: default = $4200; +} + +MEMORY { + ZP: start = $0022, size = $001E, type = rw, define = yes; + OVERLAY_REGION: start = $4200, size = $2000, file = %O, define = yes, + fill = yes, fillval = $00; + RESIDENT: start = $C000, size = $1000, type = rw, define = yes; +} + +SEGMENTS { + ZEROPAGE: load = ZP, type = zp, optional = yes; + + OVERLAY_P384: load = OVERLAY_REGION, type = ro; + + # Resident RW buffers — we don't write them to the .bin, but they + # need real addresses so labels are correct. + DATA: load = RESIDENT, type = rw, optional = yes; + BSS: load = RESIDENT, type = bss, optional = yes; +} diff --git a/docs/crypto_abi_audit.md b/docs/crypto_abi_audit.md new file mode 100644 index 0000000..99a255f --- /dev/null +++ b/docs/crypto_abi_audit.md @@ -0,0 +1,148 @@ +# Crypto ABI Audit — Phase A + +**Date:** 2026-04-19 +**Scope:** 14 TLS-active ABI symbols + 3 P-384 symbols (17 total) + +--- + +## Question 1: Exact-Name Export Match + +All 17 ABI symbols have exact `.export` matches in their source libraries. + +| ABI Symbol | Source Library | File:Line | Export Status | Trampoline Needed? | +|---|---|---|---|---| +| `x25519_scalarmult` | c64-x25519 | src/x25519.s:22 | Exact match ✓ | No | +| `fe25519_mul` | c64-x25519 | src/fe25519.s:20 | Exact match ✓ | No | +| `fe25519_sqr` | c64-x25519 | src/fe25519.s:20 | Exact match ✓ | No | +| `fe25519_inv` | c64-x25519 | src/fe25519.s:21 | Exact match ✓ | No | +| `chacha20_encrypt` | c64-ChaCha20-Poly1305 | src/lib/chacha20_lib.s:27 | Exact match ✓ | No | +| `poly1305_init` | c64-ChaCha20-Poly1305 | src/lib/poly1305_lib.s:29 | Exact match ✓ | No | +| `poly1305_update` | c64-ChaCha20-Poly1305 | src/lib/poly1305_lib.s:31 | Exact match ✓ | No | +| `poly1305_final` | c64-ChaCha20-Poly1305 | src/lib/poly1305_lib.s:31 | Exact match ✓ | No | +| `aead_encrypt` | c64-ChaCha20-Poly1305 | src/lib/chacha20poly1305_lib.s:41 | Exact match ✓ | No | +| `aead_decrypt` | c64-ChaCha20-Poly1305 | src/lib/chacha20poly1305_lib.s:41 | Exact match ✓ | No | +| `ec_point_double` | c64-nist-curves | src/points256.s:15 | Exact match ✓ | No | +| `ec_point_add` | c64-nist-curves | src/points256.s:15 | Exact match ✓ | No | +| `ec_jacobian_to_affine` | c64-nist-curves | src/points256.s:17 | Exact match ✓ | No | +| `ec_point_double_384` | c64-nist-curves | src/points384.s:16 | Exact match ✓ | No | +| `ec_point_add_384` | c64-nist-curves | src/points384.s:16 | Exact match ✓ | No | +| `ec_jacobian_to_affine_384` | c64-nist-curves | src/points384.s:18 | Exact match ✓ | No | + +**Summary:** All 17 symbols export with exact names. No trampolines or `.export ` needed. + +--- + +## Question 2: Calling Convention Match + +Spot-checked all four primitives against the ABI contract: +- ABI header (`src/crypto_abi.inc`): "keys/IVs passed via fixed buffers in the crypto BSS" +- All implementations use ZP-resident pointer triplets (not A/X pointers) + +| Primitive | Library | Entry Point | Convention | Match? | +|---|---|---|---|---| +| `x25519_scalarmult` | c64-x25519 | src/x25519.s:83 | Reads from `fe25519_src1`, `x25_u` (fixed ZP/data buffers) | ✓ | +| `chacha20_encrypt` | c64-ChaCha20-Poly1305 | src/lib/chacha20_lib.s:27 | Reads from `cc20_data_ptr`, `cc20_remain` (fixed ZP) | ✓ | +| `ec_point_double` | c64-nist-curves | src/points256.s:59 | Loads pointers into `fp_src1`, `fp_src2` (fixed ZP); reads from `ec_p1` (fixed data) | ✓ | +| `ec_point_double_384` | c64-nist-curves | src/points384.s:63 | Loads pointers into `fp_src1`, `fp_src2` (fixed ZP); reads from `ec384_p1` (fixed data) | ✓ | + +**Calling Convention Verdict:** All four primitives match the ABI contract. No calling-convention mismatches. + +--- + +## Question 3: ZP `.ifndef` Hook Coverage + +Each canonical ZP equate is verified to be wrapped in `.ifndef` in its owning library's ZP-config file. + +### c64-x25519 (`src/constants.s`) + +| Canonical Name | Canonical Addr | Wrapped? | Upstream Default | Match? | +|---|---|---|---|---| +| `zp_tmp1` | $02 | ✓ line 47 | $02 | ✓ | +| `zp_tmp2` | $03 | ✓ line 50 | $03 | ✓ | +| `fe_src1` | $2C | ✓ line 55 | $1E (legacy) | ✗ **Default mismatch** | +| `fe_src2` | $2E | ✓ line 58 | $20 (legacy) | ✗ **Default mismatch** | +| `fe_dst` | $30 | ✓ line 61 | $22 (legacy) | ✗ **Default mismatch** | +| `fe_carry` | $32 | ✓ line 67 | $26 (legacy) | ✗ **Default mismatch** | +| `fe_loop` | $33 | ✓ line 70 | $27 (legacy) | ✗ **Default mismatch** | +| `fe_mul_i` | $34 | ✓ line 73 | $28 (legacy) | ✗ **Default mismatch** | +| `fe_mul_j` | $35 | ✓ line 76 | $29 (legacy) | ✗ **Default mismatch** | +| `x25_prev_bit` | $38 | ✓ line 81 | $2A (legacy) | ✗ **Default mismatch** | +| `x25_byte_idx` | $39 | ✓ line 87 | $2C (legacy) | ✗ **Default mismatch** | +| `x25_bit_mask` | $3A | ✓ line 90 | $2D (legacy) | ✗ **Default mismatch** | + +**Note:** c64-x25519's `src/constants.s` contains legacy ZP defaults that differ from the canonical addresses in c64-https. However, all equates are wrapped in `.ifndef`, so `--asm-define` override will work correctly. The defaults are **only used if no `-D` flag is passed**, which Phase C's `libs/x25519/build.sh` will not do — it will pass the full canonical ZP set. + +### c64-ChaCha20-Poly1305 (`src/lib/constants_lib.s`) + +| Canonical Name | Canonical Addr | Wrapped? | Upstream Default | Match? | +|---|---|---|---|---| +| `zp_tmp1` | $02 | ✓ line 12 | $02 | ✓ | +| `zp_tmp2` | $03 | ✓ line 15 | $03 | ✓ | +| `w32_src1` | $04 | ✓ line 20 | $04 | ✓ | +| `w32_src2` | $06 | ✓ line 23 | $06 | ✓ | +| `w32_dst` | $08 | ✓ line 26 | $08 | ✓ | +| `cc20_round` | $14 | ✓ line 31 | $14 | ✓ | +| `cc20_qr_idx` | $15 | ✓ line 34 | $15 | ✓ | +| `cc20_data_ptr` | $16 | ✓ line 37 | $16 | ✓ | +| `cc20_remain` | $18 | ✓ line 40 | $18 | ✓ | +| `cc20_buf_pos` | $19 | ✓ line 43 | $19 | ✓ | +| `poly_i` | $1A | ✓ line 74 | $1A | ✓ | +| `poly_j` | $1B | ✓ line 77 | $1B | ✓ | +| `poly_carry` | $1C | ✓ line 80 | $1C | ✓ | +| `poly_tmp` | $1D | ✓ line 83 | $1D | ✓ | + +**Verdict:** All 14 ChaCha20-Poly1305 ZP equates are wrapped and defaults match canonical values. ✓ + +### c64-nist-curves (`src/zp_config.s`) + +| Canonical Name | Canonical Addr | Wrapped? | Upstream Default | Match? | +|---|---|---|---|---| +| `zp_tmp1` | $02 | ✓ line 37 | $02 | ✓ | +| `zp_tmp2` | $03 | ✓ line 40 | $03 | ✓ | +| `fp_src1` | $22 | ✓ line 51 | $22 | ✓ | +| `fp_src2` | $24 | ✓ line 54 | $24 | ✓ | +| `fp_dst` | $26 | ✓ line 57 | $26 | ✓ | +| `fp_misc` | $28 | ✓ line 60 | $28 | ✓ | +| `fp_carry` | $2A | ✓ line 63 | $2A | ✓ | +| `fp_loop` | $2B | ✓ line 66 | $2B | ✓ | +| `fp_mul_i` | $39 | ✓ line 69 | $2C (wrong!) | ✗ **Default mismatch** | +| `fp_mul_j` | $3A | ✓ line 72 | $2D (wrong!) | ✗ **Default mismatch** | +| `ec_scalar_ptr` | $3B | ✓ line 77 | $3B | ✓ | +| `poly_i` | $1A | ✓ line 82 | $1A | ✓ | +| `poly_j` | $1B | ✓ line 85 | $1B | ✓ | +| `poly_carry` | $1C | ✓ line 88 | $1C | ✓ | +| `poly_tmp` | $1D | ✓ line 91 | $1D | ✓ | + +**Note:** c64-nist-curves has two slightly off defaults for `fp_mul_i` ($2C instead of $39) and `fp_mul_j` ($2D instead of $3A), but both are wrapped in `.ifndef` so `--asm-define` will override them correctly in Phase C. + +--- + +## Verdict Per Library + +### c64-x25519 +- **Status:** YELLOW — tiny workaround needed +- **Issue:** ZP defaults in `src/constants.s` do not match canonical addresses (e.g., `fe_src1` defaults to $1E, canonical is $2C) +- **Fix:** Phase C's `libs/x25519/build.sh` must pass the full canonical ZP set via `--asm-define` (as planned in the phase C template). All `.ifndef` hooks are present, so this will work without upstream patches. +- **Readiness:** Ready for Phase C with the standard `-D` flag set. + +### c64-ChaCha20-Poly1305 +- **Status:** GREEN — ready for Phase C +- **Details:** All 14 ZP equates are wrapped and defaults match canonical values exactly. No workarounds needed. +- **Readiness:** Fully ready for Phase C integration. + +### c64-nist-curves +- **Status:** YELLOW — tiny workaround needed +- **Issue:** `fp_mul_i` and `fp_mul_j` defaults are off by a few bytes ($2C/$2D vs canonical $39/$3A) +- **Fix:** Phase C's `libs/nistcurves/build.sh` must pass the full canonical ZP set via `--asm-define`. All `.ifndef` hooks are in place. +- **Readiness:** Ready for Phase C with the standard `-D` flag set; P-384 symbols are properly exported and calling conventions match. + +--- + +## Summary + +✓ All 17 symbols (14 TLS-active + 3 P-384) export with exact names. +✓ All four spot-checked primitives use the correct calling convention (ZP pointer triplets, not A/X). +⚠ Two libraries (x25519, nist-curves) have ZP default mismatches, but all are wrapped in `.ifndef` blocks; Phase C's build wrapper will pass canonical values via `--asm-define`, so **no upstream patches are required**. + +**Phase C Clearance:** APPROVED. All three sibling libraries are ready for Phase C integration. The user's recent prep work (`.ifndef` wrappers) is verified across all three libraries. + diff --git a/docs/crypto_cross_calls.md b/docs/crypto_cross_calls.md new file mode 100644 index 0000000..e4a1241 --- /dev/null +++ b/docs/crypto_cross_calls.md @@ -0,0 +1,63 @@ +# Phase B.5 — Cross-Library Call Audit + +## Objective + +Verify the REU-overlay design's core invariant: **no primitive in one sibling library may call a primitive in another**. The three overlay-hosted libraries (X25519, P-256, P-384) rotate through a single 8 KB RAM overlay slot; a call made while the wrong overlay is resident would execute unloaded code, corrupting the handshake. + +## Exports per Sibling + +### X25519 (c64-x25519/src) +``` +x25519_scalarmult (primary ABI entry) +x25519_base +fe25519_mul, fe25519_sqr, fe25519_inv +``` + +### ChaCha20-Poly1305 (c64-ChaCha20-Poly1305/src) +``` +chacha20_encrypt (primary ABI entry) +poly1305_init, poly1305_update, poly1305_final +aead_encrypt, aead_decrypt +``` + +### nist-curves (c64-nist-curves/src) +``` +ec_point_double, ec_point_add (P-256) +ec_jacobian_to_affine (P-256) +ec_point_double_384, ec_point_add_384 (P-384) +ec_jacobian_to_affine_384 (P-384) +``` + +## Cross-Library Calls Found + +Exhaustive search across all source files in each library for `jsr ` / `jmp ` / indirect JSR (`jsr (...)`): + +| Source Lib | Call Type | Target Lib | Status | +|---|---|---|---| +| X25519 | → ChaCha/AEAD | — | **NONE** ✓ | +| X25519 | → nist-curves | — | **NONE** ✓ | +| ChaCha20-Poly1305 | → X25519 | — | **NONE** ✓ | +| ChaCha20-Poly1305 | → nist-curves | — | **NONE** ✓ | +| nist-curves | → X25519 | — | **NONE** ✓ | +| nist-curves | → ChaCha20-Poly1305 | — | **NONE** ✓ | + +**Indirect calls** (`jsr (...)`): None detected. + +## Classification + +No inter-sibling calls exist. All **0** entries would fall into the SAFE category: +- Within-same-lib calls: Safe (same overlay slot). +- X25519/P-256/P-384 → ChaCha20-Poly1305 (AEAD, always-resident): Would be SAFE, but none found. +- ChaCha20-Poly1305 → X25519/P-256/P-384: Would be REVIEW (must invoke `crypto_swap_to_*` first), but none found. + +## Verdict + +**Phase C clear to proceed.** + +The overlay design's core invariant is **satisfied**: each library is self-contained, with no cross-library primitive calls. No composition adjustments or overlay-slot changes are required. + +--- + +**Audit date:** 2026-04-19 +**Executed:** grep -rn "jsr|jmp" across all source trees, filtering for known ABI symbols. +**Result:** Zero hazards. diff --git a/docs/phase_f_part1_root_cause.md b/docs/phase_f_part1_root_cause.md new file mode 100644 index 0000000..c282412 --- /dev/null +++ b/docs/phase_f_part1_root_cause.md @@ -0,0 +1,174 @@ +# Phase F Part 1 — x25519 REU-overlay hang under BACKEND=uci + +## Status (2026-04-20) + +Investigation in progress — **root cause narrowed but not fully +identified**. Hang bisected to inside `x25519_scalarmult` +(sibling-provided ladder) during TLS handshake. Isolated x25519 +(`tools/test_x25519.py`) passes, so the defect is stateful / +context-dependent. + +## Reproducer + +``` +BACKEND=uci make clean && make +python3 tools/uci/test_https_local.py # ~10 min timeout +``` + +Expected: ~110 s handshake, body = "HELLO FROM TLS SERVER". +Observed: 605 s timeout, `progress=0x03`, `tls_state=0x00` +(IDLE); `http_get`'s call to `tls_connect` never returns. + +HEAD at time of investigation: `7bb64e8`. + +## What was confirmed + +Three runs with progressively deeper instrumentation, all using +a single-byte sub-progress beacon in `tls_last_state` ($95D2 +SHADOW_BSS): + +| Probe value | Meaning | Observed | +|-----------------------|------------------------------------------|----------| +| `$82` | pre `jsr tls_ecdh_generate_keypair` | reached | +| `$91` | inside keypair, post privkey-copy | reached | +| `$92`, `$93` | pre fe25519_copy(basepoint), pre clamp | reached | +| **`$94`** | **pre `jsr x25519_scalarmult`** | **stuck**| +| `$9E` | post x25519_scalarmult (never seen) | never | + +So the Montgomery ladder enters and never returns. 605 s is +~1200x a healthy 500 ms scalarmult at 48 MHz — not merely +slow, genuinely stuck. + +Debug artifacts of the last probe run: +- `/tmp/uci_https_debug/20260420_*/tls_state_dump.json` — + `tls_last_state=0x94`, other TLS state zeroed. +- `/tmp/uci_https_debug/20260420_*/tail.txt` — last 2000 + CPU cycles captured; PC hotspots inside capture filter + ($2000-$3FFF, $6000-$9FFF, $DF1B-$DF1F) show stride-$2F + reads through the crypto tables region. +- Server side logs a successful TCP accept + (`client_addr = (..., 192.168.1.81:PORT)`) and an empty + request buffer — TCP is up; TLS never ClientHello'd. + +## What was ruled out + +- Not a DNS / TCP connect hang — `net_tcp_state = $01` + (UCI_TCP_CONNECTED) at hang; the local listener saw the + inbound connection. +- Not a DRBG hang — two `drbg_fill_bytes` calls complete + (probe `$82` reached). +- Not `fe25519_copy` / `x25519_clamp` — both complete + (probe `$94` reached). +- Not overlay-image rot — Phase E `tools/test_crypto_init.py` + still verifies overlay-slot-vs-PRG byte match at boot and + was passing as of the last green run. +- Not the UCI register polling pattern seen in + `uci_accesses.txt` — that's the **host-side bridge** + reading $DF1B-$DF1F for the test harness's memory-read + API, not CPU code polling. Confirmed by the 5-consecutive- + cycle stride (5 reads in 5 cycles cannot be CPU `lda abs` + which is 4 cycles each, capture filter only keeps the data + read cycle — 5 cpu `lda`s would be ~20 cycles apart). + +## Why test_x25519 passes but TLS hangs + +`tools/test_x25519.py` drives `x25519_scalarmult` directly +after boot's `crypto_init`. TLS runs scalarmult *after*: + +1. Boot's `sqtab_init` (legacy in-tree poly1305 + quarter-square init — runs **unconditionally**, not gated + by `USE_X25519_SIBLING`, see `src/boot.s:211`). The + sibling's sqtab was already built in `crypto_init` at + `$7800/$7A00`; the legacy routine rebuilds the same + quarter-square values to the same addresses. Redundant + but should be harmless. +2. Boot's `do_net_init` (UCI probe + DHCP read). +3. Menu SYS trigger. +4. `http_get` → DNS memcpy → `net_tcp_connect` (issues a + UCI TCP_CONNECT command, reads socket_id). +5. Hostname copy (32-byte memcpy). +6. `tls_connect` → two `drbg_fill_bytes` + (calls into HMAC-DRBG → SHA-256). + +## Promising next leads + +1. **ZP corruption from the legacy `sqtab_init` at boot + line 211.** The legacy routine uses module-local BSS + `sq_acc/sh/ad/i` (no exports), so it shouldn't collide + symbol-wise. However, if the ca65 linker places the + legacy's private BSS at addresses that happen to overlap + sibling data OR REU table backing regions, boot would + silently corrupt a 1 KB range. **Check the `.map` file + for `sq_acc`/`sq_sh` (legacy) placement vs sibling + tables.** If they overlap, the fix is to gate the legacy + call with `.ifndef USE_X25519_SIBLING` (same pattern as + `reu_mul_init` at line 218). + +2. **REU register state left in a bad config by something + between `crypto_overlay_stash_x25519` and the first + ladder mul.** The sibling's `fe25519_mul` relies on + `reu_clear_wide` re-establishing the mul-row fetch + config at entry, then issues bare `sta reu_command` DMAs + that inherit `reu_c64_lo/hi` + `reu_len_lo/hi` from the + clear-wide prelude. If SHA-256 or DRBG or HMAC touched + `$DFxx` (they shouldn't, but check), or if an interrupt + handler does, the first `fe25519_mul`'s REU fetch lands + in the wrong C64 RAM region, corrupting `fe_wide` + ($40-$7F). The ladder then multiplies indefinitely + with garbage — the ladder loop counter (`x25_byte_idx`, + `x25_bit_mask` in ZP $39-$3A) terminates correctly, but + each fe25519_mul may itself never terminate if `fe_mul_j` + ends up reading a table cell that doesn't decrement X to + 32. + +3. **IRQ running during scalarmult, clobbering ZP $33 + (`fe_loop`) or $34 (`fe_mul_i`).** `crypto_swap_to_x25519` + SEIs around its DMA but nothing in the crypto runtime + SEIs around the ladder. On U64E the default IRQ handler + is still KERNAL; at 48 MHz the per-frame IRQ fires with + a LOT of cycles between — but if it interrupts between + an `ldx fe_mul_i` and an `inx`/`cpx #32/bcc` group, the + handler could stomp ZP. **Look at whether + `src/boot.s` or `main.s` explicitly disables IRQs for + the handshake.** + +4. **UCI firmware is dispatching an async event (interrupt + or bus-stall) that corrupts the REU mid-DMA.** Less + likely because Phase E verified REU integrity at boot, + but worth ruling out with a mid-handshake overlay-byte + re-check (DMA-read `$4200-$5FFF` and compare against the + prg image). + +## Recommended immediate experiment + +**Gate the boot-level legacy `sqtab_init` call the same way +`reu_mul_init` is gated:** + +```diff + jsr crypto_init + +- ; build quarter-square multiply table (needed by Poly1305, fe25519, ECDSA) +- jsr sqtab_init ++ ; build quarter-square multiply table (needed by Poly1305, fe25519, ECDSA) ++ .ifndef USE_X25519_SIBLING ++ jsr sqtab_init ++ .endif +``` + +If the hang clears, the legacy routine was either stomping +sibling state or running against stale assumptions about +table contents. If the hang persists, move to lead #2 — +re-check REU registers before x25519_scalarmult. + +## Reverted instrumentation + +All `tls_last_state` probes added during this session have +been removed (`git diff --stat` returns clean). The probe +recipe is preserved above — reapply by patching +`src/tls13.s:123-143` and `src/tls_ecdh.s:49-70` as needed. + +## Budget used + +~60 minutes. Three U64E runs (10+ min each) consumed most +of the wall-clock. Follow-up agent should start from the +"Recommended immediate experiment" above. diff --git a/docs/phase_f_part1_trace_analysis.md b/docs/phase_f_part1_trace_analysis.md new file mode 100644 index 0000000..1aacedf --- /dev/null +++ b/docs/phase_f_part1_trace_analysis.md @@ -0,0 +1,171 @@ +# Phase F Part 1 — trace.bin analysis + +## TL;DR + +**The captured `trace.bin` artifacts from runs 20260420_151301, _152431, +_154548 and _160911 do NOT contain sufficient signal to pinpoint the +exact instruction where `x25519_scalarmult` hangs.** The 6510 bus tap, +as filtered by `test_https_local.py::_keep_cycle`, is a periodic sampler +(one sample every ~1/48th of a CPU cycle at 48 MHz turbo); what surfaces +in the PC histogram is a *cadence* artifact, not a hot-loop fingerprint. + +The hang itself is real and reproducible (handoff-doc probe byte +`tls_last_state = $94`, `tls_ecdhe_pubkey = 00...` in every snapshot), +but the trace *data* is not what will identify the specific loop. + +## What the trace actually shows + +1. **All captured cycles have PHI2 = 1** (CPU-phase), by construction. + `_keep_cycle` in `tools/uci/test_https_local.py:134` drops PHI2 = 0 + samples. Every window I sampled (cyc 0, 10k, 50k, 200k, 1M, …, 13M) + reports `CPU: 100% VIC: 0%`. +2. **A dominant address stride of exactly $2F** (47 bytes) across the + entire $2000-$9FFF filter window, for the entire 605 s duration. + Sample (cycles 13105079-13105200, late in the run, from + `20260420_154548/tail.txt`): + + ``` + 13105079 R $6ACB=1E 13105080 R $6AFA=25 13105081 R $6B29=91 + 13105082 R $6B58=40 13105083 R $6B87=31 ... + ``` + + Each consecutive sample is exactly $2F bytes further into memory, + one per reported cycle. A 6510 cannot walk memory at 1 byte per + cycle, let alone by $2F per cycle — this is **the sampling pattern + of the bus tap, not instruction flow**. +3. **PC-histogram ceiling artifact**: `summary.txt` shows 20 distinct + PCs all tied at exactly 3553 hits in the last-2M-cycle window + (`$2012, $2041, $2070, $209F, $20CE, $20FD, ...` — stride $2F, + covering the entire NET_CODE region). A genuine tight loop would + produce a handful of PCs with much higher counts; a uniform sweep + at 3553 hits across dozens of addresses is the packet cadence: + + ``` + packets: 1738174 over 605.4 s → 2871 pkts/s at 48 MHz + pkt → 360 cycles × 4 B → one sample every ~48 CPU cycles @48MHz + window 2M samples / 530 unique kept addresses ≈ 3800 per addr, + clamped by 2M / 563 ≈ 3553 per-addr cap (matches summary) + ``` +4. **All 124,270 `$DF1B-$DF1F` hits in `uci_accesses.txt` are + 5-consecutive-cycle $DF1B→$DF1C→$DF1D→$DF1E→$DF1F reads**, repeating + every ~570 cycles. Five absolute-mode loads in five *consecutive* + cycles is impossible for `lda abs` (4 cycles per op), so these are + the *host bridge* polling its own identification registers, not the + 6510 polling UCI status. Confirms the handoff doc's note that the + CPU is not executing UCI polling at all in the final seconds. +5. **No REU register accesses (`$DF00-$DF0F`) in the final 10,000 + cycles** (decoded window). The filter does not cover `$DFxx` outside + UCI, so this is expected, not evidence by itself. +6. The 16:09:11 run's `server_result.json` shows + `"error": "TimeoutError: timed out"` on accept — **that run failed + before the 6510 ever opened a TCP socket**, which matches the + handoff-doc note that Lead #1 (gating `sqtab_init`) regressed the + failure mode. Runs 15:13-15:56 (pre-regression, with probe) show + successful TCP accept (`client_addr = (..., 192.168.1.81:PORT)`), + no bytes received — TLS never sent ClientHello, consistent with + the hang inside `tls_ecdh_generate_keypair`'s `x25519_base` call. + +## What the trace does NOT show + +- **No identifiable hot-loop PC**. If the 6510 were stuck in a tight + 8-instruction loop inside `fe25519_mul`, I would expect those 8 PCs + to dominate the histogram by orders of magnitude. They do not. +- **No REU DMA command writes** in the tail. If `reu_clear_wide` or + `fe25519_mul`'s inline REU DMA were looping forever, we would see + writes to `$DF01` in the captured window. We do not — but the + filter excludes `$DF00-$DF0F`, so absence of REU writes in the + capture is not probative either way. +- **No jammed-CPU signature**. A KIL opcode would freeze the bus and + the tap would go silent; instead the tap reports continuous + samples. So the CPU is doing *something* — but the filter and + sampling rate don't tell us *what*. + +## Why the trace cannot settle the question + +The `_keep_cycle` filter (CPU cycles only, three address ranges) + the +360-cycle packet quantization + ~48:1 CPU-to-sample ratio at 48 MHz +means each packet provides **one sample per ~17,280 CPU cycles of +wall-clock**. A `fe25519_mul` run over 32 outer iterations × 32 inner +is on the order of ~10,000 cycles at 48 MHz — so a single mul lives +inside *one* sample. We cannot resolve finer-grained state from this +stream. + +To actually identify the hang, the investigation needs either: + +- **A cycle-accurate trace** (unfiltered, all PHI2 cycles in the hot + regions, written at the host bridge's native rate), or +- **In-ROM instrumentation**: add a set of `tls_last_state` bumps + *inside* `fe25519_mul` / `fe25519_sqr` (e.g., at entry, at outer-loop + top, at inner-loop top, at exit) and re-run once, then inspect the + post-hang byte to see which loop level is the offender, or +- **A jsr-level bisection harness** on the U64E at 48 MHz (drive + `x25519_scalarmult` directly from a 6502 stub after boot, like + `bench_ecdsa_u64e.py` does for ECDSA) to confirm whether the hang + reproduces in isolation or only after the full pre-handshake + sequence (DRBG × 2 + net init + DNS + TCP connect). + +## Evidence anchoring the prior agent's narrowed fault + +Independent of what the trace does not show, the `tls_state_dump.json` +snapshot itself is conclusive on *some* points: + +| Field | Value | Inference | +|-----------------------|-------------------------|----------------------------------------------| +| `tls_state` | `$00` (IDLE) | tls_connect never returned | +| `tls_last_state` | `$94` (probe beacon) | Execution reached the `jsr x25519_scalarmult` site but never the post-return beacon $9E | +| `tls_ecdhe_privkey` | populated (non-zero) | DRBG + clamp completed | +| `tls_ecdhe_pubkey` | `00 00 ...` (32 zero B) | `x25519_base` / `x25519_scalarmult` never wrote the result | +| `tls_client_random` | populated | Both DRBG calls completed | +| `net_tcp_state` | `$01` (CONNECTED) | UCI TCP_CONNECT returned a socket | +| `server_result.json` | `client_addr = (..., :)` (in probe runs) | TCP was established, never received a byte | + +All consistent with: **6510 entered `x25519_scalarmult` and did not +return within 605 s**, but the captured bus trace cannot differentiate +between "genuine 500× slowdown", "infinite loop at some PC", or +"periodic crash-into-BRK-handler-into-KERNAL" from what was captured. + +## Candidate root causes (leads unchanged from prior handoff) + +Given the trace does not distinguish them, the prior agent's leads +#2-4 remain open, with #1 already disproven: + +- **~~Lead #1 (sqtab gate)~~**: confirmed regressive (see run + 20260420_160911 — TCP connect never happened). +- **Lead #2 (REU register state)**: most plausible. `fe25519_mul` in + `libs/x25519/src/fe25519.s:357` relies on `reu_clear_wide` restoring + mul-row FETCH config at line 329-336, then 32 inline DMAs with a + short `asl / sta reu_reu_hi / adc #0 / sta reu_reu_bank / lda + #%10110001 / sta reu_command` sequence at lines 391-397. If any + intervening code between `crypto_overlay_stash_x25519` (boot) and + the first ladder iteration writes to any `$DF00-$DF0F` register, + the bare `sta reu_command` DMAs land in the wrong C64 RAM region + and `mul_dma_lo/mul_dma_hi` contain garbage, but each DMA still + terminates deterministically. **This alone would not cause an + infinite loop** — so either lead #2 needs refining (e.g., `fe_mul_i + / fe_mul_j` ZP clobber), or the true cause is a different axis. +- **Lead #3 (IRQ during ladder)**: no SEI around the ladder; KERNAL + IRQ handler runs; at 48 MHz the IRQ fires every ~20 ms wall-clock, + which over 605 s = 30,000+ IRQ events. Any single one that corrupts + ZP $33 `fe_loop` or $34 `fe_mul_i` during a critical window could + wedge the outer `cmp #32 / bcs @mul_done` comparison. This is + plausible but not verifiable from the current trace. +- **Lead #4 (UCI firmware async event)**: not supported by the + absence of CPU-issued UCI commands in `uci_accesses.txt`, and the + harness-side $DF1B-$DF1F polling (5 cycles / 570 cycles interval) + is host-originated bridge traffic, not 6510. + +## No fix applied + +I did not modify any source, and I did not run `test_https_local.py`. +Running one more 10-minute U64E test without a sharper hypothesis +would consume the hardware time without adding a single new signal +beyond what the four existing captures already show. The next step +needs in-ROM instrumentation (probe bytes inside `fe25519_mul` +itself) rather than another bus-tap capture — that is the only way +to move from "hang is somewhere inside the ladder" to "hang is on +outer iteration N, inner iteration M, at PC X". + +## Budget used + +~60 minutes. No U64E runs. No source modifications. Tree is at +`5d2d469`, clean. diff --git a/libs/nistcurves b/libs/nistcurves new file mode 160000 index 0000000..dfdfb59 --- /dev/null +++ b/libs/nistcurves @@ -0,0 +1 @@ +Subproject commit dfdfb59f982163a442a911aedead0a30495e873a diff --git a/src/boot.s b/src/boot.s index e4ea47e..f5b1d14 100644 --- a/src/boot.s +++ b/src/boot.s @@ -70,10 +70,11 @@ ; ---- exports: local BSS ---- .export net_initialized - ; ---- imports: entropy / DRBG / sqtab ---- + ; ---- imports: entropy / DRBG / sqtab / crypto init ---- .import entropy_init .import drbg_init_entropy .import sqtab_init + .import crypto_init ; ---- imports: network (backend adapter — ip65 or uci) ---- .import net_init @@ -191,6 +192,11 @@ start: jsr entropy_init jsr drbg_init_entropy + ; Shared crypto orchestrator. Currently calls only the stubbed + ; mul_tables_init — the in-tree x25519 sqtab_init / reu_mul_init + ; below run unconditionally under both backends. + jsr crypto_init + ; build quarter-square multiply table (needed by Poly1305, fe25519, ECDSA) jsr sqtab_init diff --git a/src/crypto/ecdsa_curve.s b/src/crypto/ecdsa_curve.s deleted file mode 100644 index 3761ae2..0000000 --- a/src/crypto/ecdsa_curve.s +++ /dev/null @@ -1,138 +0,0 @@ -; ecdsa_curve.s - P-256 curve parameters and operations -; Converted from ACME to ca65 in Phase 3 Batch A. -; -; Imported from c64-aes256-ecdsa for TLS 1.3 certificate verification. -; Test vectors stripped - not needed for verification-only use. -; ============================================================================= - -.include "constants.inc" - -; --- Externals (fp_* helpers from ecdsa_fp) --- -; Note: fp_misc and fp_src1 are zero-page equates in constants.inc. -.import fp_mod_mul -.import fp_copy -.import fp_r0 - -; --- Exports: curve constants --- -.export ec_p -.export ec_n -.export ec_a -.export ec_b -.export ec_gx -.export ec_gy - -; --- Exports: point scratch --- -.export ec_p1 -.export ec_p2 -.export ec_p3 -.export ec_t1 -.export ec_t2 -.export ec_t3 -.export ec_t4 -.export ec_t5 -.export ec_t6 - -; --- Exports: helpers --- -.export ec_set_modp -.export ec_set_modn -.export ec_mulp - -; ============================================================================= -; P-256 Curve Parameters -; ============================================================================= -.segment "CRYPTO_RODATA" - -ec_p: ; Field prime - .byte $FF, $FF, $FF, $FF, $00, $00, $00, $01 - .byte $00, $00, $00, $00, $00, $00, $00, $00 - .byte $00, $00, $00, $00, $FF, $FF, $FF, $FF - .byte $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF -ec_n: ; Group order - .byte $FF, $FF, $FF, $FF, $00, $00, $00, $00 - .byte $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF - .byte $BC, $E6, $FA, $AD, $A7, $17, $9E, $84 - .byte $F3, $B9, $CA, $C2, $FC, $63, $25, $51 -ec_a: ; Coefficient a = p - 3 - .byte $FF, $FF, $FF, $FF, $00, $00, $00, $01 - .byte $00, $00, $00, $00, $00, $00, $00, $00 - .byte $00, $00, $00, $00, $FF, $FF, $FF, $FF - .byte $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FC -ec_b: ; Coefficient b - .byte $5A, $C6, $35, $D8, $AA, $3A, $93, $E7 - .byte $B3, $EB, $BD, $55, $76, $98, $86, $BC - .byte $65, $1D, $06, $B0, $CC, $53, $B0, $F6 - .byte $3B, $CE, $3C, $3E, $27, $D2, $60, $4B -ec_gx: ; Generator x - .byte $6B, $17, $D1, $F2, $E1, $2C, $42, $47 - .byte $F8, $BC, $E6, $E5, $63, $A4, $40, $F2 - .byte $77, $03, $7D, $81, $2D, $EB, $33, $A0 - .byte $F4, $A1, $39, $45, $D8, $98, $C2, $96 -ec_gy: ; Generator y - .byte $4F, $E3, $42, $E2, $FE, $1A, $7F, $9B - .byte $8E, $E7, $EB, $4A, $7C, $0F, $9E, $16 - .byte $2B, $CE, $33, $57, $6B, $31, $5E, $CE - .byte $CB, $B6, $40, $68, $37, $BF, $51, $F5 - -; ============================================================================= -; Elliptic Curve Point Operations (Jacobian Coordinates) -; ============================================================================= -; Point = (X,Y,Z) each 32 bytes = 96 bytes total. Affine = X/Z^2, Y/Z^3. -; Point at infinity: Z = 0. -; All field arithmetic is mod ec_p. - -.segment "CRYPTO_BSS" - -; --- Point storage --- -ec_p1: .res 96, 0 ; working point (Jacobian) -ec_p2: .res 96, 0 ; second point (affine X,Y only used) -ec_p3: .res 96, 0 ; result point (Jacobian) - -; --- Temporaries for point math (mod p) --- -ec_t1: .res 32, 0 -ec_t2: .res 32, 0 -ec_t3: .res 32, 0 -ec_t4: .res 32, 0 -ec_t5: .res 32, 0 -ec_t6: .res 32, 0 - -; ============================================================================= -; Helpers -; ============================================================================= -.segment "CRYPTO_CODE" - -; --- Helper: set fp_misc = ec_p --- -ec_set_modp: - lda #<(ec_p) - sta fp_misc - lda #>(ec_p) - sta fp_misc+1 - rts - -; --- Helper: set fp_misc = ec_n --- -ec_set_modn: - lda #<(ec_n) - sta fp_misc - lda #>(ec_n) - sta fp_misc+1 - rts - -; --- Helper: modular multiply mod p, result -> (fp_dst) --- -; fp_src1, fp_src2 already set. Result goes through fp_r0 then copied to dst. -ec_mulp: - jsr ec_set_modp - jsr fp_mod_mul ; result in fp_r0 - ; Copy fp_r0 -> (fp_dst) - lda fp_src1 - pha - lda fp_src1+1 - pha - lda #<(fp_r0) - sta fp_src1 - lda #>(fp_r0) - sta fp_src1+1 - jsr fp_copy - pla - sta fp_src1+1 - pla - sta fp_src1 - rts diff --git a/src/crypto/ecdsa_curve_384.asm b/src/crypto/ecdsa_curve_384.asm deleted file mode 100644 index 0b305bc..0000000 --- a/src/crypto/ecdsa_curve_384.asm +++ /dev/null @@ -1,103 +0,0 @@ -; ============================================================================= -; ecdsa_curve_384.asm - P-384 curve parameters, point storage, helpers -; ============================================================================= - -; ============================================================================= -; P-384 Curve Parameters (48 bytes each, big-endian) -; ============================================================================= - -; P-384 field prime p -ec_p_384: - !byte $FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF - !byte $FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FE - !byte $FF,$FF,$FF,$FF,$00,$00,$00,$00,$00,$00,$00,$00,$FF,$FF,$FF,$FF - -; P-384 group order n -ec_n_384: - !byte $FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF - !byte $FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$C7,$63,$4D,$81,$F4,$37,$2D,$DF - !byte $58,$1A,$0D,$B2,$48,$B0,$A7,$7A,$EC,$EC,$19,$6A,$CC,$C5,$29,$73 - -; P-384 coefficient a = p - 3 -ec_a_384: - !byte $FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF - !byte $FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FE - !byte $FF,$FF,$FF,$FF,$00,$00,$00,$00,$00,$00,$00,$00,$FF,$FF,$FF,$FC - -; P-384 coefficient b -ec_b_384: - !byte $B3,$31,$2F,$A7,$E2,$3E,$E7,$E4,$98,$8E,$05,$6B,$E3,$F8,$2D,$19 - !byte $18,$1D,$9C,$6E,$FE,$81,$41,$12,$03,$14,$08,$8F,$50,$13,$87,$5A - !byte $C6,$56,$39,$8D,$8A,$2E,$D1,$9D,$2A,$85,$C8,$ED,$D3,$EC,$2A,$EF - -; P-384 generator point Gx -ec_gx_384: - !byte $AA,$87,$CA,$22,$BE,$8B,$05,$37,$8E,$B1,$C7,$1E,$F3,$20,$AD,$74 - !byte $6E,$1D,$3B,$62,$8B,$A7,$9B,$98,$59,$F7,$41,$E0,$82,$54,$2A,$38 - !byte $55,$02,$F2,$5D,$BF,$55,$29,$6C,$3A,$54,$5E,$38,$72,$76,$0A,$B7 - -; P-384 generator point Gy -ec_gy_384: - !byte $36,$17,$DE,$4A,$96,$26,$2C,$6F,$5D,$9E,$98,$BF,$92,$92,$DC,$29 - !byte $F8,$F4,$1D,$BD,$28,$9A,$14,$7C,$E9,$DA,$31,$13,$B5,$F0,$B8,$C0 - !byte $0A,$60,$B1,$CE,$1D,$7E,$81,$9D,$7A,$43,$1D,$7C,$90,$EA,$0E,$5F - -; ============================================================================= -; Point storage (144 bytes each = 3 x 48 Jacobian coordinates) -; ============================================================================= -ec_p1_384: !fill 144, 0 ; working point (Jacobian) -ec_p2_384: !fill 144, 0 ; second point (affine X,Y only used) -ec_p3_384: !fill 144, 0 ; result point (Jacobian) - -; ============================================================================= -; Temporaries for point math (48 bytes each) -; ============================================================================= -ec_t1_384: !fill 48, 0 -ec_t2_384: !fill 48, 0 -ec_t3_384: !fill 48, 0 -ec_t4_384: !fill 48, 0 -ec_t5_384: !fill 48, 0 -ec_t6_384: !fill 48, 0 - -; ============================================================================= -; Helper: set fp_misc = ec_p_384 -; ============================================================================= -ec_set_modp_384: - lda #ec_p_384 - sta fp_misc+1 - rts - -; ============================================================================= -; Helper: set fp_misc = ec_n_384 -; ============================================================================= -ec_set_modn_384: - lda #ec_n_384 - sta fp_misc+1 - rts - -; ============================================================================= -; ec_mulp_384: modular multiply mod p_384, result -> (fp_dst) -; fp_src1, fp_src2 already set. Result goes through fp_r0_384 then copied. -; ============================================================================= -ec_mulp_384: - jsr ec_set_modp_384 - jsr fp_mod_mul_384 ; result in fp_r0_384 - ; Copy fp_r0_384 -> (fp_dst) - lda fp_src1 - pha - lda fp_src1+1 - pha - lda #fp_r0_384 - sta fp_src1+1 - jsr fp_copy_384 - pla - sta fp_src1+1 - pla - sta fp_src1 - rts diff --git a/src/crypto/ecdsa_fp.s b/src/crypto/ecdsa_fp.s deleted file mode 100644 index db0544f..0000000 --- a/src/crypto/ecdsa_fp.s +++ /dev/null @@ -1,249 +0,0 @@ -; ecdsa_fp.s - P-256 field prime arithmetic -; Converted from ACME to ca65 in Phase 3 Batch A. -; -; Big-number primitives for ECDSA P-256. -; ZP pointers, fp_copy, fp_zero, fp_cmp, fp_add, fp_sub, fp_rshift1, -; fp_mul, fp_init_sqtab -; -; Imported from c64-aes256-ecdsa for TLS 1.3 certificate verification. -; ZP equates (fp_src1=$22 etc.) are in constants.inc. -; Quarter-square table at $7800 is shared with Poly1305. - - .include "constants.inc" - - .import sqtab_init - .import sqtab_lo, sqtab_hi - - .export fp_init_sqtab - .export fp_copy - .export fp_zero - .export fp_cmp - .export fp_add - .export fp_sub - .export fp_is_zero - .export fp_rshift1 - .export fp_mul - .export fp_a_byte - .export fp_b_byte - .export fp_s_hi - .export fp_p_lo - .export fp_p_hi - .export fp_wide - -; ============================================================================= -; fp_init_sqtab - quarter-square table at $7800-$7BFF -; NOT included here: sqtab_init in poly1305.asm already builds this table. -; Alias for callers that expect the ecdsa name: -; ============================================================================= -fp_init_sqtab = sqtab_init - - .segment "CRYPTO_CODE" - -; ============================================================================= -; fp_copy: copy 32 bytes from (fp_src1) to (fp_dst) -; ============================================================================= -fp_copy: - ldy #31 -@lp: lda (fp_src1),y - sta (fp_dst),y - dey - bpl @lp - rts - -; ============================================================================= -; fp_zero: zero 32 bytes at (fp_dst) -; ============================================================================= -fp_zero: - lda #0 - ldy #31 -@lp: sta (fp_dst),y - dey - bpl @lp - rts - -; ============================================================================= -; fp_cmp: compare (fp_src1) vs (fp_src2), 32 bytes big-endian -; Carry set if src1 >= src2, clear if src1 < src2. Zero if equal. -; ============================================================================= -fp_cmp: - ldy #0 -@lp: lda (fp_src1),y - cmp (fp_src2),y - bne @done - iny - cpy #32 - bne @lp -@done: rts - -; ============================================================================= -; fp_add: (fp_dst) = (fp_src1) + (fp_src2). Carry in fp_carry. -; ============================================================================= -fp_add: - clc - ldy #31 -@lp: lda (fp_src1),y - adc (fp_src2),y - sta (fp_dst),y - dey - bpl @lp - lda #0 - adc #0 - sta fp_carry - rts - -; ============================================================================= -; fp_sub: (fp_dst) = (fp_src1) - (fp_src2). Borrow in fp_carry (1=borrow). -; ============================================================================= -fp_sub: - sec - ldy #31 -@lp: lda (fp_src1),y - sbc (fp_src2),y - sta (fp_dst),y - dey - bpl @lp - lda #0 - adc #0 - eor #1 - sta fp_carry - rts - -; ============================================================================= -; fp_is_zero: test if (fp_src1) == 0. Z flag set if zero. -; ============================================================================= -fp_is_zero: - ldy #0 - lda #0 -@lp: ora (fp_src1),y - iny - cpy #32 - bne @lp - cmp #0 - rts - -; ============================================================================= -; fp_rshift1: right-shift (fp_src1) by 1 bit in place -; ============================================================================= -fp_rshift1: - clc - ldy #0 - ldx #32 -@lp: lda (fp_src1),y - ror - sta (fp_src1),y - iny - dex - bne @lp - rts - -; ============================================================================= -; fp_mul: 256x256 -> 512 bit multiply -; (fp_src1) * (fp_src2) -> fp_wide (64 bytes) -; Schoolbook with quarter-square 8x8 lookup. -; ============================================================================= -fp_mul: - ; Clear 64-byte result - ldy #63 - lda #0 -@clr: sta fp_wide,y - dey - bpl @clr - - lda #31 - sta fp_mul_i -@outer: - ldy fp_mul_i - lda (fp_src1),y - sta fp_a_byte - bne @do_inner - jmp @skip_o - -@do_inner: - lda #31 - sta fp_mul_j -@inner: - ldy fp_mul_j - lda (fp_src2),y - beq @skip_i - sta fp_b_byte - - ; a*b via quarter-square: sqtab[a+b] - sqtab[|a-b|] - lda fp_a_byte - clc - adc fp_b_byte - tax ; X = (a+b) low - lda #0 - adc #0 - sta fp_s_hi ; sum page (0 or 1) - - lda fp_a_byte - sec - sbc fp_b_byte - bcs :+ - eor #$ff - adc #1 -: tay ; Y = |a-b| (always page 0) - - lda fp_s_hi - beq @s0 - ; sum page 1 - lda sqtab_lo+256,x - sec - sbc sqtab_lo,y - sta fp_p_lo - lda sqtab_hi+256,x - sbc sqtab_hi,y - sta fp_p_hi - jmp @add_prod -@s0: lda sqtab_lo,x - sec - sbc sqtab_lo,y - sta fp_p_lo - lda sqtab_hi,x - sbc sqtab_hi,y - sta fp_p_hi - -@add_prod: - ; Add 16-bit product to fp_wide[i+j+1] (lo) and [i+j] (hi) - lda fp_mul_i - clc - adc fp_mul_j - tax - inx ; X = i+j+1 - - clc - lda fp_wide,x - adc fp_p_lo - sta fp_wide,x - dex ; X = i+j - lda fp_wide,x - adc fp_p_hi - sta fp_wide,x - bcc @skip_i - ; Propagate carry -@prop: dex - bmi @skip_i - lda fp_wide,x - adc #0 - sta fp_wide,x - bcs @prop - -@skip_i: - dec fp_mul_j - bmi @skip_o - jmp @inner -@skip_o: - dec fp_mul_i - bmi @mul_done - jmp @outer -@mul_done: - rts - - .segment "CRYPTO_BSS" - -fp_a_byte: .res 1 -fp_b_byte: .res 1 -fp_s_hi: .res 1 -fp_p_lo: .res 1 -fp_p_hi: .res 1 -fp_wide: .res 64 diff --git a/src/crypto/ecdsa_fp_384.asm b/src/crypto/ecdsa_fp_384.asm deleted file mode 100644 index 40f24ed..0000000 --- a/src/crypto/ecdsa_fp_384.asm +++ /dev/null @@ -1,238 +0,0 @@ -; ============================================================================= -; ecdsa_fp_384.asm - Big-number primitives for ECDSA P-384 -; ZP pointers (shared), fp_copy_384, fp_zero_384, fp_cmp_384, fp_add_384, -; fp_sub_384, fp_rshift1_384, fp_mul_384, fp_is_zero_384, fp_chk_one_384 -; -; Adapted from ecdsa_fp.asm (P-256) with 48-byte field elements. -; Uses the SAME ZP equates as P-256 — only data width changes. -; Quarter-square table (sqtab_lo/sqtab_hi) must already be built. -; ============================================================================= - -; --- Zero-page pointers (shared with P-256, already defined) --- -; fp_src1 = $22, fp_src2 = $24, fp_dst = $26, fp_misc = $28 -; fp_carry = $2a, fp_loop = $2b -; fp_mul_i = $39, fp_mul_j = $3a -; sqtab_lo = $7800, sqtab_hi = $7a00 - -; ============================================================================= -; fp_copy_384: copy 48 bytes from (fp_src1) to (fp_dst) -; ============================================================================= -fp_copy_384: - ldy #47 -@lp: lda (fp_src1),y - sta (fp_dst),y - dey - bpl @lp - rts - -; ============================================================================= -; fp_zero_384: zero 48 bytes at (fp_dst) -; ============================================================================= -fp_zero_384: - lda #0 - ldy #47 -@lp: sta (fp_dst),y - dey - bpl @lp - rts - -; ============================================================================= -; fp_cmp_384: compare (fp_src1) vs (fp_src2), 48 bytes big-endian -; Carry set if src1 >= src2, clear if src1 < src2. Zero if equal. -; ============================================================================= -fp_cmp_384: - ldy #0 -@lp: lda (fp_src1),y - cmp (fp_src2),y - bne @done - iny - cpy #48 - bne @lp -@done: rts - -; ============================================================================= -; fp_add_384: (fp_dst) = (fp_src1) + (fp_src2). Carry in fp_carry. -; ============================================================================= -fp_add_384: - clc - ldy #47 -@lp: lda (fp_src1),y - adc (fp_src2),y - sta (fp_dst),y - dey - bpl @lp - lda #0 - adc #0 - sta fp_carry - rts - -; ============================================================================= -; fp_sub_384: (fp_dst) = (fp_src1) - (fp_src2). Borrow in fp_carry (1=borrow). -; ============================================================================= -fp_sub_384: - sec - ldy #47 -@lp: lda (fp_src1),y - sbc (fp_src2),y - sta (fp_dst),y - dey - bpl @lp - lda #0 - adc #0 - eor #1 - sta fp_carry - rts - -; ============================================================================= -; fp_is_zero_384: test if (fp_src1) == 0. Z flag set if zero. -; ============================================================================= -fp_is_zero_384: - ldy #0 - lda #0 -@lp: ora (fp_src1),y - iny - cpy #48 - bne @lp - cmp #0 - rts - -; ============================================================================= -; fp_rshift1_384: right-shift (fp_src1) by 1 bit in place -; ============================================================================= -fp_rshift1_384: - clc - ldy #0 - ldx #48 -@lp: lda (fp_src1),y - ror - sta (fp_src1),y - iny - dex - bne @lp - rts - -; ============================================================================= -; fp_chk_one_384: check if (fp_src1) == 1. Z flag set if yes. -; ============================================================================= -fp_chk_one_384: - ldy #0 -@lp: lda (fp_src1),y - bne @no - iny - cpy #47 - bne @lp - lda (fp_src1),y - cmp #1 ; Z set if byte 47 == 1 - rts -@no: lda #$ff ; clear Z - rts - -; ============================================================================= -; fp_mul_384: 384x384 -> 768 bit multiply -; (fp_src1) * (fp_src2) -> fp_wide_384 (96 bytes) -; Schoolbook with quarter-square 8x8 lookup. -; ============================================================================= -fp_mul_384: - ; Clear 96-byte result - ldy #95 - lda #0 -@clr: sta fp_wide_384,y - dey - bpl @clr - - lda #47 - sta fp_mul_i -@outer: - ldy fp_mul_i - lda (fp_src1),y - sta fp_a_byte_384 - bne @do_inner - jmp @skip_o - -@do_inner: - lda #47 - sta fp_mul_j -@inner: - ldy fp_mul_j - lda (fp_src2),y - beq @skip_i - sta fp_b_byte_384 - - ; a*b via quarter-square: sqtab[a+b] - sqtab[|a-b|] - lda fp_a_byte_384 - clc - adc fp_b_byte_384 - tax ; X = (a+b) low - lda #0 - adc #0 - sta fp_s_hi_384 ; sum page (0 or 1) - - lda fp_a_byte_384 - sec - sbc fp_b_byte_384 - bcs + - eor #$ff - adc #1 -+ tay ; Y = |a-b| (always page 0) - - lda fp_s_hi_384 - beq @s0 - ; sum page 1 - lda sqtab_lo+256,x - sec - sbc sqtab_lo,y - sta fp_p_lo_384 - lda sqtab_hi+256,x - sbc sqtab_hi,y - sta fp_p_hi_384 - jmp @add_prod -@s0: lda sqtab_lo,x - sec - sbc sqtab_lo,y - sta fp_p_lo_384 - lda sqtab_hi,x - sbc sqtab_hi,y - sta fp_p_hi_384 - -@add_prod: - ; Add 16-bit product to fp_wide_384[i+j+1] (lo) and [i+j] (hi) - lda fp_mul_i - clc - adc fp_mul_j - tax - inx ; X = i+j+1 - - clc - lda fp_wide_384,x - adc fp_p_lo_384 - sta fp_wide_384,x - dex ; X = i+j - lda fp_wide_384,x - adc fp_p_hi_384 - sta fp_wide_384,x - bcc @skip_i - ; Propagate carry -@prop: dex - bmi @skip_i - lda fp_wide_384,x - adc #0 - sta fp_wide_384,x - bcs @prop - -@skip_i: - dec fp_mul_j - bmi @skip_o - jmp @inner -@skip_o: - dec fp_mul_i - bmi @mul_done - jmp @outer -@mul_done: - rts - -fp_a_byte_384: !byte 0 -fp_b_byte_384: !byte 0 -fp_s_hi_384: !byte 0 -fp_p_lo_384: !byte 0 -fp_p_hi_384: !byte 0 -fp_wide_384: !fill 96, 0 diff --git a/src/crypto/ecdsa_mod.s b/src/crypto/ecdsa_mod.s deleted file mode 100644 index 0b66409..0000000 --- a/src/crypto/ecdsa_mod.s +++ /dev/null @@ -1,511 +0,0 @@ -; ecdsa_mod.s - P-256 scalar modular arithmetic (mod n) -; Converted from ACME to ca65 in Phase 3 Batch A. -; ============================================================================= -; Modular arithmetic for ECDSA P-256 -; fp_mod_add, fp_mod_sub, fp_mod_reduce, fp_mod_mul, fp_mod_inv, -; result registers fp_r0-r3 -; -; Imported from c64-aes256-ecdsa for TLS 1.3 certificate verification. -; ============================================================================= - -.include "constants.inc" - -.import fp_add, fp_sub, fp_mul, fp_cmp, fp_copy, fp_zero, fp_rshift1 -.import fp_wide - -.export fp_mod_add -.export fp_mod_sub -.export fp_mod_reduce -.export fp_mod_mul -.export fp_mod_inv -.export fp_chk_one - -.export fp_rem -.export fp_bc -.export fp_bm -.export fp_inv_iter -.export fp_inv_u -.export fp_inv_v -.export fp_inv_x1 -.export fp_inv_x2 -.export fp_r0 -.export fp_r1 -.export fp_r2 -.export fp_r3 - -.segment "CRYPTO_CODE" - -; ============================================================================= -; fp_mod_add: (fp_dst) = ((fp_src1) + (fp_src2)) mod (fp_misc) -; ============================================================================= -fp_mod_add: - jsr fp_add - lda fp_carry - bne @reduce - - ; Compare dst with modulus - lda fp_src1 - pha - lda fp_src1+1 - pha - lda fp_src2 - pha - lda fp_src2+1 - pha - lda fp_dst - sta fp_src1 - lda fp_dst+1 - sta fp_src1+1 - lda fp_misc - sta fp_src2 - lda fp_misc+1 - sta fp_src2+1 - jsr fp_cmp - pla - sta fp_src2+1 - pla - sta fp_src2 - pla - sta fp_src1+1 - pla - sta fp_src1 - bcc @done - -@reduce: - ; dst -= modulus - lda fp_src1 - pha - lda fp_src1+1 - pha - lda fp_src2 - pha - lda fp_src2+1 - pha - lda fp_dst - sta fp_src1 - lda fp_dst+1 - sta fp_src1+1 - lda fp_misc - sta fp_src2 - lda fp_misc+1 - sta fp_src2+1 - jsr fp_sub - pla - sta fp_src2+1 - pla - sta fp_src2 - pla - sta fp_src1+1 - pla - sta fp_src1 -@done: rts - -; ============================================================================= -; fp_mod_sub: (fp_dst) = ((fp_src1) - (fp_src2)) mod (fp_misc) -; ============================================================================= -fp_mod_sub: - jsr fp_sub - lda fp_carry - beq @done - - ; Underflow: add modulus - lda fp_src1 - pha - lda fp_src1+1 - pha - lda fp_src2 - pha - lda fp_src2+1 - pha - lda fp_dst - sta fp_src1 - lda fp_dst+1 - sta fp_src1+1 - lda fp_misc - sta fp_src2 - lda fp_misc+1 - sta fp_src2+1 - jsr fp_add - pla - sta fp_src2+1 - pla - sta fp_src2 - pla - sta fp_src1+1 - pla - sta fp_src1 -@done: rts - -; ============================================================================= -; fp_mod_reduce: reduce 512-bit fp_wide mod (fp_misc) -> fp_r0 -; Binary long division: for each of 512 bits, shift into remainder -; and conditionally subtract modulus. -; ============================================================================= -fp_mod_reduce: - ; Clear 33-byte remainder - ldy #32 - lda #0 -@clr: sta fp_rem,y - dey - bpl @clr - - lda #0 - sta fp_bc ; byte counter in fp_wide - lda #$80 - sta fp_bm ; bit mask - -@bitlp: - ; Shift remainder left 1 - clc - ldy #32 -@shl: lda fp_rem,y - rol - sta fp_rem,y - dey - bpl @shl - - ; OR in next bit from fp_wide - ldy fp_bc - lda fp_wide,y - and fp_bm - beq @nobit - lda fp_rem+32 - ora #1 - sta fp_rem+32 -@nobit: - ; Compare remainder with modulus - lda fp_rem ; overflow byte - bne @dosub - - ldy #0 -@cmplp: lda fp_rem+1,y - cmp (fp_misc),y - bcc @nosub - bne @dosub - iny - cpy #32 - bne @cmplp - ; Equal: subtract - -@dosub: - sec - ldy #31 -@sublp: lda fp_rem+1,y - sbc (fp_misc),y - sta fp_rem+1,y - dey - bpl @sublp - lda fp_rem - sbc #0 - sta fp_rem - -@nosub: - ; Next bit - lsr fp_bm - bne @bitlp - lda #$80 - sta fp_bm - inc fp_bc - lda fp_bc - cmp #64 - bne @bitlp - - ; Copy result - ldy #0 -@cpy: lda fp_rem+1,y - sta fp_r0,y - iny - cpy #32 - bne @cpy - rts - -; ============================================================================= -; fp_mod_mul: fp_r0 = ((fp_src1) * (fp_src2)) mod (fp_misc) -; ============================================================================= -fp_mod_mul: - jsr fp_mul - jsr fp_mod_reduce - rts - -; ============================================================================= -; fp_mod_inv: fp_r0 = (fp_src1)^(-1) mod (fp_misc) -; Binary extended GCD algorithm. -; ============================================================================= -fp_mod_inv: - ; u = src1, v = mod, x1 = 1, x2 = 0 - lda #0 - sta fp_inv_iter - sta fp_inv_iter+1 - lda fp_dst - pha - lda fp_dst+1 - pha - - ; Copy u = src1 - lda #fp_inv_u - sta fp_dst+1 - jsr fp_copy - - ; Copy v = mod - lda fp_misc - sta fp_src1 - lda fp_misc+1 - sta fp_src1+1 - lda #fp_inv_v - sta fp_dst+1 - jsr fp_copy - - ; x1 = 1 - lda #fp_inv_x1 - sta fp_dst+1 - jsr fp_zero - lda #1 - sta fp_inv_x1+31 - - ; x2 = 0 - lda #fp_inv_x2 - sta fp_dst+1 - jsr fp_zero - - pla - sta fp_dst+1 - pla - sta fp_dst - -@mainlp: - inc fp_inv_iter - bne :+ - inc fp_inv_iter+1 -: - - ; Check u == 1 - lda #fp_inv_u - sta fp_src1+1 - jsr fp_chk_one - bne :+ - jmp @u_one -: - ; Check v == 1 - lda #fp_inv_v - sta fp_src1+1 - jsr fp_chk_one - bne :+ - jmp @v_one -: - - ; While u is even -@halfu: lda fp_inv_u+31 - and #1 - bne @halfv - - lda #fp_inv_u - sta fp_src1+1 - jsr fp_rshift1 - - lda fp_inv_x1+31 - and #1 - beq @x1ev_nocarry - ; x1 += mod - lda #fp_inv_x1 - sta fp_src1+1 - sta fp_dst+1 - lda fp_misc - sta fp_src2 - lda fp_misc+1 - sta fp_src2+1 - jsr fp_add - jmp @x1do_shift -@x1ev_nocarry: - lda #0 - sta fp_carry -@x1do_shift: - ; x1 >>= 1, with carry from fp_add shifted in as MSB - lda fp_carry ; carry from x1+mod (0 or 1) - lsr ; shift into 6502 carry flag - ldy #0 - ldx #32 -@x1sh: lda fp_inv_x1,y - ror ; rotate carry in from left - sta fp_inv_x1,y - iny - dex - bne @x1sh - jmp @halfu - - ; While v is even -@halfv: lda fp_inv_v+31 - and #1 - bne @comp - - lda #fp_inv_v - sta fp_src1+1 - jsr fp_rshift1 - - lda fp_inv_x2+31 - and #1 - beq @x2ev_nocarry - lda #fp_inv_x2 - sta fp_src1+1 - sta fp_dst+1 - lda fp_misc - sta fp_src2 - lda fp_misc+1 - sta fp_src2+1 - jsr fp_add - jmp @x2do_shift -@x2ev_nocarry: - lda #0 - sta fp_carry -@x2do_shift: - ; x2 >>= 1, with carry from fp_add shifted in as MSB - lda fp_carry - lsr ; into 6502 carry - ldy #0 - ldx #32 -@x2sh: lda fp_inv_x2,y - ror - sta fp_inv_x2,y - iny - dex - bne @x2sh - jmp @halfv - -@comp: - ; Compare u vs v - lda #fp_inv_u - sta fp_src1+1 - lda #fp_inv_v - sta fp_src2+1 - jsr fp_cmp - bcc @vbig - - ; u >= v: u -= v, x1 -= x2 mod m - lda #fp_inv_u - sta fp_dst+1 - jsr fp_sub - - lda #fp_inv_x1 - sta fp_src1+1 - lda #fp_inv_x2 - sta fp_src2+1 - lda #fp_inv_x1 - sta fp_dst+1 - jsr fp_mod_sub - jmp @mainlp - -@vbig: - ; v -= u, x2 -= x1 mod m - lda #fp_inv_v - sta fp_src1+1 - lda #fp_inv_u - sta fp_src2+1 - lda #fp_inv_v - sta fp_dst+1 - jsr fp_sub - - lda #fp_inv_x2 - sta fp_src1+1 - lda #fp_inv_x1 - sta fp_src2+1 - lda #fp_inv_x2 - sta fp_dst+1 - jsr fp_mod_sub - jmp @mainlp - -@u_one: ; Result = x1 - ldy #31 -@cu: lda fp_inv_x1,y - sta fp_r0,y - dey - bpl @cu - rts - -@v_one: ; Result = x2 - ldy #31 -@cv: lda fp_inv_x2,y - sta fp_r0,y - dey - bpl @cv - rts - -; Check if (fp_src1) == 1: Z flag set if yes -fp_chk_one: - ldy #0 -@lp: lda (fp_src1),y - bne @no - iny - cpy #31 - bne @lp - lda (fp_src1),y - cmp #1 ; Z set if byte 31 == 1 - rts -@no: lda #$ff ; clear Z - rts - -; ============================================================================= -; BSS / scratch -; ============================================================================= -.segment "CRYPTO_BSS" - -fp_rem: .res 33 -fp_bc: .res 1 -fp_bm: .res 1 - -fp_inv_iter: .res 2 - -fp_inv_u: .res 32 -fp_inv_v: .res 32 -fp_inv_x1: .res 32 -fp_inv_x2: .res 32 - -; Working registers -fp_r0: .res 32 ; primary result register -fp_r1: .res 32 -fp_r2: .res 32 -fp_r3: .res 32 diff --git a/src/crypto/ecdsa_mod_384.asm b/src/crypto/ecdsa_mod_384.asm deleted file mode 100644 index 429a7b3..0000000 --- a/src/crypto/ecdsa_mod_384.asm +++ /dev/null @@ -1,455 +0,0 @@ -; ============================================================================= -; ecdsa_mod_384.asm - Modular arithmetic for ECDSA P-384 -; fp_mod_add_384, fp_mod_sub_384, fp_mod_reduce_384, fp_mod_mul_384, -; fp_mod_inv_384, result registers fp_r0_384-fp_r3_384 -; -; Adapted from ecdsa_mod.asm (P-256) with 48-byte field elements. -; ============================================================================= - -; ============================================================================= -; fp_mod_add_384: (fp_dst) = ((fp_src1) + (fp_src2)) mod (fp_misc) -; ============================================================================= -fp_mod_add_384: - jsr fp_add_384 - lda fp_carry - bne @reduce - - ; Compare dst with modulus - lda fp_src1 - pha - lda fp_src1+1 - pha - lda fp_src2 - pha - lda fp_src2+1 - pha - lda fp_dst - sta fp_src1 - lda fp_dst+1 - sta fp_src1+1 - lda fp_misc - sta fp_src2 - lda fp_misc+1 - sta fp_src2+1 - jsr fp_cmp_384 - pla - sta fp_src2+1 - pla - sta fp_src2 - pla - sta fp_src1+1 - pla - sta fp_src1 - bcc @done - -@reduce: - ; dst -= modulus - lda fp_src1 - pha - lda fp_src1+1 - pha - lda fp_src2 - pha - lda fp_src2+1 - pha - lda fp_dst - sta fp_src1 - lda fp_dst+1 - sta fp_src1+1 - lda fp_misc - sta fp_src2 - lda fp_misc+1 - sta fp_src2+1 - jsr fp_sub_384 - pla - sta fp_src2+1 - pla - sta fp_src2 - pla - sta fp_src1+1 - pla - sta fp_src1 -@done: rts - -; ============================================================================= -; fp_mod_sub_384: (fp_dst) = ((fp_src1) - (fp_src2)) mod (fp_misc) -; ============================================================================= -fp_mod_sub_384: - jsr fp_sub_384 - lda fp_carry - beq @done - - ; Underflow: add modulus - lda fp_src1 - pha - lda fp_src1+1 - pha - lda fp_src2 - pha - lda fp_src2+1 - pha - lda fp_dst - sta fp_src1 - lda fp_dst+1 - sta fp_src1+1 - lda fp_misc - sta fp_src2 - lda fp_misc+1 - sta fp_src2+1 - jsr fp_add_384 - pla - sta fp_src2+1 - pla - sta fp_src2 - pla - sta fp_src1+1 - pla - sta fp_src1 -@done: rts - -; ============================================================================= -; fp_mod_reduce_384: reduce 768-bit fp_wide_384 mod (fp_misc) -> fp_r0_384 -; Binary long division: for each of 768 bits, shift into remainder -; and conditionally subtract modulus. -; ============================================================================= -fp_mod_reduce_384: - ; Clear 49-byte remainder (48 + 1 overflow byte) - ldy #48 - lda #0 -@clr: sta fp_rem_384,y - dey - bpl @clr - - lda #0 - sta fp_bc_384 ; byte counter in fp_wide_384 - lda #$80 - sta fp_bm_384 ; bit mask - -@bitlp: - ; Shift remainder left 1 - clc - ldy #48 -@shl: lda fp_rem_384,y - rol - sta fp_rem_384,y - dey - bpl @shl - - ; OR in next bit from fp_wide_384 - ldy fp_bc_384 - lda fp_wide_384,y - and fp_bm_384 - beq @nobit - lda fp_rem_384+48 - ora #1 - sta fp_rem_384+48 -@nobit: - ; Compare remainder with modulus - lda fp_rem_384 ; overflow byte - bne @dosub - - ldy #0 -@cmplp: lda fp_rem_384+1,y - cmp (fp_misc),y - bcc @nosub - bne @dosub - iny - cpy #48 - bne @cmplp - ; Equal: subtract - -@dosub: - sec - ldy #47 -@sublp: lda fp_rem_384+1,y - sbc (fp_misc),y - sta fp_rem_384+1,y - dey - bpl @sublp - lda fp_rem_384 - sbc #0 - sta fp_rem_384 - -@nosub: - ; Next bit - lsr fp_bm_384 - bne @bitlp - lda #$80 - sta fp_bm_384 - inc fp_bc_384 - lda fp_bc_384 - cmp #96 - bne @bitlp - - ; Copy result - ldy #0 -@cpy: lda fp_rem_384+1,y - sta fp_r0_384,y - iny - cpy #48 - bne @cpy - rts - -fp_rem_384: !fill 49, 0 -fp_bc_384: !byte 0 -fp_bm_384: !byte 0 - -; ============================================================================= -; fp_mod_mul_384: fp_r0_384 = ((fp_src1) * (fp_src2)) mod (fp_misc) -; ============================================================================= -fp_mod_mul_384: - jsr fp_mul_384 - jsr fp_mod_reduce_384 - rts - -; ============================================================================= -; fp_mod_inv_384: fp_r0_384 = (fp_src1)^(-1) mod (fp_misc) -; Binary extended GCD algorithm. -; ============================================================================= -fp_mod_inv_384: - ; u = src1, v = mod, x1 = 1, x2 = 0 - lda fp_dst - pha - lda fp_dst+1 - pha - - ; Copy u = src1 - lda #fp_inv_u_384 - sta fp_dst+1 - jsr fp_copy_384 - - ; Copy v = mod - lda fp_misc - sta fp_src1 - lda fp_misc+1 - sta fp_src1+1 - lda #fp_inv_v_384 - sta fp_dst+1 - jsr fp_copy_384 - - ; x1 = 1 - lda #fp_inv_x1_384 - sta fp_dst+1 - jsr fp_zero_384 - lda #1 - sta fp_inv_x1_384+47 - - ; x2 = 0 - lda #fp_inv_x2_384 - sta fp_dst+1 - jsr fp_zero_384 - - pla - sta fp_dst+1 - pla - sta fp_dst - -@mainlp: - ; Check u == 1 - lda #fp_inv_u_384 - sta fp_src1+1 - jsr fp_chk_one_384 - bne + - jmp @u_one -+ - ; Check v == 1 - lda #fp_inv_v_384 - sta fp_src1+1 - jsr fp_chk_one_384 - bne + - jmp @v_one -+ - - ; While u is even -@halfu: lda fp_inv_u_384+47 - and #1 - bne @halfv - - lda #fp_inv_u_384 - sta fp_src1+1 - jsr fp_rshift1_384 - - lda fp_inv_x1_384+47 - and #1 - beq @x1ev_nocarry - ; x1 += mod - lda #fp_inv_x1_384 - sta fp_src1+1 - sta fp_dst+1 - lda fp_misc - sta fp_src2 - lda fp_misc+1 - sta fp_src2+1 - jsr fp_add_384 - jmp @x1do_shift -@x1ev_nocarry: - lda #0 - sta fp_carry -@x1do_shift: - ; x1 >>= 1, with carry from fp_add shifted in as MSB - lda fp_carry ; carry from x1+mod (0 or 1) - lsr ; shift into 6502 carry flag - ldy #0 - ldx #48 -@x1sh: lda fp_inv_x1_384,y - ror ; rotate carry in from left - sta fp_inv_x1_384,y - iny - dex - bne @x1sh - jmp @halfu - - ; While v is even -@halfv: lda fp_inv_v_384+47 - and #1 - bne @comp - - lda #fp_inv_v_384 - sta fp_src1+1 - jsr fp_rshift1_384 - - lda fp_inv_x2_384+47 - and #1 - beq @x2ev_nocarry - lda #fp_inv_x2_384 - sta fp_src1+1 - sta fp_dst+1 - lda fp_misc - sta fp_src2 - lda fp_misc+1 - sta fp_src2+1 - jsr fp_add_384 - jmp @x2do_shift -@x2ev_nocarry: - lda #0 - sta fp_carry -@x2do_shift: - ; x2 >>= 1, with carry from fp_add shifted in as MSB - lda fp_carry - lsr ; into 6502 carry - ldy #0 - ldx #48 -@x2sh: lda fp_inv_x2_384,y - ror - sta fp_inv_x2_384,y - iny - dex - bne @x2sh - jmp @halfv - -@comp: - ; Compare u vs v - lda #fp_inv_u_384 - sta fp_src1+1 - lda #fp_inv_v_384 - sta fp_src2+1 - jsr fp_cmp_384 - bcc @vbig - - ; u >= v: u -= v, x1 -= x2 mod m - lda #fp_inv_u_384 - sta fp_dst+1 - jsr fp_sub_384 - - lda #fp_inv_x1_384 - sta fp_src1+1 - lda #fp_inv_x2_384 - sta fp_src2+1 - lda #fp_inv_x1_384 - sta fp_dst+1 - jsr fp_mod_sub_384 - jmp @mainlp - -@vbig: - ; v -= u, x2 -= x1 mod m - lda #fp_inv_v_384 - sta fp_src1+1 - lda #fp_inv_u_384 - sta fp_src2+1 - lda #fp_inv_v_384 - sta fp_dst+1 - jsr fp_sub_384 - - lda #fp_inv_x2_384 - sta fp_src1+1 - lda #fp_inv_x1_384 - sta fp_src2+1 - lda #fp_inv_x2_384 - sta fp_dst+1 - jsr fp_mod_sub_384 - jmp @mainlp - -@u_one: ; Result = x1 - ldy #47 -@cu: lda fp_inv_x1_384,y - sta fp_r0_384,y - dey - bpl @cu - rts - -@v_one: ; Result = x2 - ldy #47 -@cv: lda fp_inv_x2_384,y - sta fp_r0_384,y - dey - bpl @cv - rts - -fp_inv_u_384: !fill 48, 0 -fp_inv_v_384: !fill 48, 0 -fp_inv_x1_384: !fill 48, 0 -fp_inv_x2_384: !fill 48, 0 - -; ============================================================================= -; Working registers (48 bytes each) -; ============================================================================= -fp_r0_384: !fill 48, 0 ; primary result register -fp_r1_384: !fill 48, 0 -fp_r2_384: !fill 48, 0 -fp_r3_384: !fill 48, 0 diff --git a/src/crypto/ecdsa_points.s b/src/crypto/ecdsa_points.s deleted file mode 100644 index 4c33b79..0000000 --- a/src/crypto/ecdsa_points.s +++ /dev/null @@ -1,920 +0,0 @@ -; ecdsa_points.s — P-256 Jacobian point arithmetic -; Converted from ACME to ca65 in Phase 3 Batch A. -; -; ec_point_double, ec_point_add, ec_scalar_mul, ec_jacobian_to_affine -; -; Imported from c64-aes256-ecdsa for TLS 1.3 certificate verification. -; Debug output (chrout, print_decimal) stripped. - -.include "constants.inc" - -; ----------------------------------------------------------------------------- -; Imports from ecdsa_fp (fp_src1/fp_src2/fp_dst/ec_scalar_ptr come from -; constants.inc as zero-page equates). -; ----------------------------------------------------------------------------- -.import fp_r0 -.import fp_is_zero, fp_mod_add, fp_mod_sub, fp_mod_inv - -; ----------------------------------------------------------------------------- -; Imports from ecdsa_curve (curve constants + scratch + helpers) -; ----------------------------------------------------------------------------- -.import ec_set_modp, ec_mulp -.import ec_p1, ec_p2, ec_p3 -.import ec_t1, ec_t2, ec_t3, ec_t4, ec_t5, ec_t6 - -; ----------------------------------------------------------------------------- -; Exports -; ----------------------------------------------------------------------------- -.export ec_point_double -.export ec_point_add -.export ec_scalar_mul -.export ec_jacobian_to_affine -.export ec_affine_x -.export ec_affine_y -.export ec_sc_byte -.export ec_sc_mask - -; ----------------------------------------------------------------------------- -; Scratch / output RAM -; ----------------------------------------------------------------------------- -.segment "CRYPTO_BSS" - -ec_sc_byte: .res 1 -ec_sc_mask: .res 1 -ec_affine_x: .res 32 -ec_affine_y: .res 32 - -; ============================================================================= -; Code -; ============================================================================= -.segment "CRYPTO_CODE" - -; ============================================================================= -; ec_point_double: ec_p3 = 2 * ec_p1 (Jacobian) -; Formula for a = -3 (P-256): -; M = 3*(X1 - Z1^2)*(X1 + Z1^2) -; S = 4*X1*Y1^2 -; X3 = M^2 - 2*S -; Y3 = M*(S - X3) - 8*Y1^4 -; Z3 = 2*Y1*Z1 -; ============================================================================= -ec_point_double: - ; Check Z1 == 0 (point at infinity) - lda #<(ec_p1+64) - sta fp_src1 - lda #>(ec_p1+64) - sta fp_src1+1 - jsr fp_is_zero - bne @notinf - ; Result = infinity - ldy #95 - lda #0 -@ci: sta ec_p3,y - dey - bpl @ci - rts - -@notinf: - jsr ec_set_modp - - ; t1 = Z1^2 - lda #<(ec_p1+64) - sta fp_src1 - lda #>(ec_p1+64) - sta fp_src1+1 - lda #<(ec_p1+64) - sta fp_src2 - lda #>(ec_p1+64) - sta fp_src2+1 - lda #ec_t1 - sta fp_dst+1 - jsr ec_mulp ; t1 = Z1^2 - - ; t2 = X1 - t1 - lda #ec_p1 - sta fp_src1+1 - lda #ec_t1 - sta fp_src2+1 - lda #ec_t2 - sta fp_dst+1 - jsr fp_mod_sub ; t2 = X1 - Z1^2 - - ; t3 = X1 + t1 - lda #ec_p1 - sta fp_src1+1 - lda #ec_t1 - sta fp_src2+1 - lda #ec_t3 - sta fp_dst+1 - jsr fp_mod_add ; t3 = X1 + Z1^2 - - ; t4 = t2 * t3 = (X1-Z^2)(X1+Z^2) - lda #ec_t2 - sta fp_src1+1 - lda #ec_t3 - sta fp_src2+1 - lda #ec_t4 - sta fp_dst+1 - jsr ec_mulp ; t4 = X1^2 - Z1^4 - - ; M = 3*t4: t5 = 2*t4, t2 = t5+t4 = 3*t4 - lda #ec_t4 - sta fp_src1+1 - lda #ec_t4 - sta fp_src2+1 - lda #ec_t5 - sta fp_dst+1 - jsr fp_mod_add ; t5 = 2*t4 - - lda #ec_t5 - sta fp_src1+1 - lda #ec_t4 - sta fp_src2+1 - lda #ec_t2 - sta fp_dst+1 - jsr fp_mod_add ; t2 = M = 3*(X1^2 - Z1^4) - - ; t3 = Y1^2 - lda #<(ec_p1+32) - sta fp_src1 - lda #>(ec_p1+32) - sta fp_src1+1 - lda #<(ec_p1+32) - sta fp_src2 - lda #>(ec_p1+32) - sta fp_src2+1 - lda #ec_t3 - sta fp_dst+1 - jsr ec_mulp ; t3 = Y1^2 - - ; t4 = X1 * Y1^2 - lda #ec_p1 - sta fp_src1+1 - lda #ec_t3 - sta fp_src2+1 - lda #ec_t4 - sta fp_dst+1 - jsr ec_mulp ; t4 = X1*Y1^2 - - ; S = 4*X1*Y1^2 = 4*t4 - ; t5 = 2*t4 - lda #ec_t4 - sta fp_src1+1 - lda #ec_t4 - sta fp_src2+1 - lda #ec_t5 - sta fp_dst+1 - jsr fp_mod_add ; t5 = 2*X1*Y1^2 - - ; t1 = S = 2*t5 = 4*X1*Y1^2 - lda #ec_t5 - sta fp_src1+1 - lda #ec_t5 - sta fp_src2+1 - lda #ec_t1 - sta fp_dst+1 - jsr fp_mod_add ; t1 = S = 4*X1*Y1^2 - - ; X3 = M^2 - 2*S - ; t4 = M^2 - lda #ec_t2 - sta fp_src1+1 - lda #ec_t2 - sta fp_src2+1 - lda #ec_t4 - sta fp_dst+1 - jsr ec_mulp ; t4 = M^2 - - ; t5 = 2*S - lda #ec_t1 - sta fp_src1+1 - lda #ec_t1 - sta fp_src2+1 - lda #ec_t5 - sta fp_dst+1 - jsr fp_mod_add ; t5 = 2*S - - ; X3 = t4 - t5 - lda #ec_t4 - sta fp_src1+1 - lda #ec_t5 - sta fp_src2+1 - lda #ec_p3 - sta fp_dst+1 - jsr fp_mod_sub ; X3 = M^2 - 2S - - ; Y3 = M*(S - X3) - 8*Y1^4 - ; t4 = S - X3 - lda #ec_t1 - sta fp_src1+1 - lda #ec_p3 - sta fp_src2+1 - lda #ec_t4 - sta fp_dst+1 - jsr fp_mod_sub ; t4 = S - X3 - - ; t5 = M*(S-X3) - lda #ec_t2 - sta fp_src1+1 - lda #ec_t4 - sta fp_src2+1 - lda #ec_t5 - sta fp_dst+1 - jsr ec_mulp ; t5 = M*(S-X3) - - ; t4 = Y1^4 = (Y1^2)^2 = t3^2 - lda #ec_t3 - sta fp_src1+1 - lda #ec_t3 - sta fp_src2+1 - lda #ec_t4 - sta fp_dst+1 - jsr ec_mulp ; t4 = Y1^4 - - ; 8*Y1^4: t6 = 2*t4, t4 = 2*t6 = 4*Y1^4, t6 = 2*t4 = 8*Y1^4 - lda #ec_t4 - sta fp_src1+1 - lda #ec_t4 - sta fp_src2+1 - lda #ec_t6 - sta fp_dst+1 - jsr fp_mod_add ; t6 = 2*Y1^4 - - lda #ec_t6 - sta fp_src1+1 - lda #ec_t6 - sta fp_src2+1 - lda #ec_t4 - sta fp_dst+1 - jsr fp_mod_add ; t4 = 4*Y1^4 - - lda #ec_t4 - sta fp_src1+1 - lda #ec_t4 - sta fp_src2+1 - lda #ec_t6 - sta fp_dst+1 - jsr fp_mod_add ; t6 = 8*Y1^4 - - ; Y3 = t5 - t6 - lda #ec_t5 - sta fp_src1+1 - lda #ec_t6 - sta fp_src2+1 - lda #<(ec_p3+32) - sta fp_dst - lda #>(ec_p3+32) - sta fp_dst+1 - jsr fp_mod_sub ; Y3 = M*(S-X3) - 8*Y1^4 - - ; Z3 = 2*Y1*Z1 - ; t1 = Y1*Z1 - lda #<(ec_p1+32) - sta fp_src1 - lda #>(ec_p1+32) - sta fp_src1+1 - lda #<(ec_p1+64) - sta fp_src2 - lda #>(ec_p1+64) - sta fp_src2+1 - lda #ec_t1 - sta fp_dst+1 - jsr ec_mulp ; t1 = Y1*Z1 - - ; Z3 = 2*t1 - lda #ec_t1 - sta fp_src1+1 - lda #ec_t1 - sta fp_src2+1 - lda #<(ec_p3+64) - sta fp_dst - lda #>(ec_p3+64) - sta fp_dst+1 - jsr fp_mod_add ; Z3 = 2*Y1*Z1 - - rts - -; ============================================================================= -; ec_point_add: ec_p3 = ec_p1 + ec_p2 -; P1 is Jacobian (X1,Y1,Z1). P2 is AFFINE (X2,Y2, Z2 assumed 1). -; -; U2 = X2*Z1^2, S2 = Y2*Z1^3 -; H = U2 - X1, R = S2 - Y1 -; If H==0: if R==0 -> double, else -> infinity -; X3 = R^2 - H^3 - 2*X1*H^2 -; Y3 = R*(X1*H^2 - X3) - Y1*H^3 -; Z3 = H*Z1 -; ============================================================================= -ec_point_add: - ; If P1 is infinity (Z1==0): result = P2 with Z=1 - lda #<(ec_p1+64) - sta fp_src1 - lda #>(ec_p1+64) - sta fp_src1+1 - jsr fp_is_zero - bne @p1ok - - ; Copy P2 to P3 as Jacobian with Z=1 - ldy #31 -@cpx: lda ec_p2,y - sta ec_p3,y - dey - bpl @cpx - ldy #31 -@cpy: lda ec_p2+32,y - sta ec_p3+32,y - dey - bpl @cpy - ldy #31 - lda #0 -@clz: sta ec_p3+64,y - dey - bpl @clz - lda #1 - sta ec_p3+95 ; Z = 1 - rts - -@p1ok: - jsr ec_set_modp - - ; t1 = Z1^2 - lda #<(ec_p1+64) - sta fp_src1 - lda #>(ec_p1+64) - sta fp_src1+1 - lda #<(ec_p1+64) - sta fp_src2 - lda #>(ec_p1+64) - sta fp_src2+1 - lda #ec_t1 - sta fp_dst+1 - jsr ec_mulp ; t1 = Z1^2 - - ; t2 = X2*Z1^2 = U2 - lda #ec_p2 - sta fp_src1+1 - lda #ec_t1 - sta fp_src2+1 - lda #ec_t2 - sta fp_dst+1 - jsr ec_mulp ; t2 = U2 - - ; t3 = Z1^3 = Z1*t1 - lda #<(ec_p1+64) - sta fp_src1 - lda #>(ec_p1+64) - sta fp_src1+1 - lda #ec_t1 - sta fp_src2+1 - lda #ec_t3 - sta fp_dst+1 - jsr ec_mulp ; t3 = Z1^3 - - ; t4 = Y2*Z1^3 = S2 - lda #<(ec_p2+32) - sta fp_src1 - lda #>(ec_p2+32) - sta fp_src1+1 - lda #ec_t3 - sta fp_src2+1 - lda #ec_t4 - sta fp_dst+1 - jsr ec_mulp ; t4 = S2 - - ; H = U2 - X1 = t2 - X1 -> t1 - lda #ec_t2 - sta fp_src1+1 - lda #ec_p1 - sta fp_src2+1 - lda #ec_t1 - sta fp_dst+1 - jsr fp_mod_sub ; t1 = H = U2 - X1 - - ; R = S2 - Y1 = t4 - Y1 -> t2 - lda #ec_t4 - sta fp_src1+1 - lda #<(ec_p1+32) - sta fp_src2 - lda #>(ec_p1+32) - sta fp_src2+1 - lda #ec_t2 - sta fp_dst+1 - jsr fp_mod_sub ; t2 = R = S2 - Y1 - - ; Check H == 0 - lda #ec_t1 - sta fp_src1+1 - jsr fp_is_zero - bne @h_nonzero - - ; H == 0: check R - lda #ec_t2 - sta fp_src1+1 - jsr fp_is_zero - bne @set_inf - ; H==0, R==0: points are equal, double P1 - jmp ec_point_double - -@set_inf: - ; H==0, R!=0: inverse points, result = infinity - ldy #95 - lda #0 -@sinf: sta ec_p3,y - dey - bpl @sinf - rts - -@h_nonzero: - ; t3 = H^2 - lda #ec_t1 - sta fp_src1+1 - lda #ec_t1 - sta fp_src2+1 - lda #ec_t3 - sta fp_dst+1 - jsr ec_mulp ; t3 = H^2 - - ; t4 = H^3 = H*H^2 - lda #ec_t1 - sta fp_src1+1 - lda #ec_t3 - sta fp_src2+1 - lda #ec_t4 - sta fp_dst+1 - jsr ec_mulp ; t4 = H^3 - - ; t5 = X1*H^2 - lda #ec_p1 - sta fp_src1+1 - lda #ec_t3 - sta fp_src2+1 - lda #ec_t5 - sta fp_dst+1 - jsr ec_mulp ; t5 = X1*H^2 - - ; X3 = R^2 - H^3 - 2*X1*H^2 - ; t3 = R^2 - lda #ec_t2 - sta fp_src1+1 - lda #ec_t2 - sta fp_src2+1 - lda #ec_t3 - sta fp_dst+1 - jsr ec_mulp ; t3 = R^2 - - ; t3 = R^2 - H^3 - lda #ec_t3 - sta fp_src1+1 - lda #ec_t4 - sta fp_src2+1 - lda #ec_t3 - sta fp_dst+1 - jsr fp_mod_sub ; t3 = R^2 - H^3 - - ; t6 = 2*X1*H^2 - lda #ec_t5 - sta fp_src1+1 - lda #ec_t5 - sta fp_src2+1 - lda #ec_t6 - sta fp_dst+1 - jsr fp_mod_add ; t6 = 2*X1*H^2 - - ; X3 = t3 - t6 - lda #ec_t3 - sta fp_src1+1 - lda #ec_t6 - sta fp_src2+1 - lda #ec_p3 - sta fp_dst+1 - jsr fp_mod_sub ; X3 - - ; Y3 = R*(X1*H^2 - X3) - Y1*H^3 - ; t3 = X1*H^2 - X3 = t5 - X3 - lda #ec_t5 - sta fp_src1+1 - lda #ec_p3 - sta fp_src2+1 - lda #ec_t3 - sta fp_dst+1 - jsr fp_mod_sub ; t3 = X1*H^2 - X3 - - ; t5 = R * t3 - lda #ec_t2 - sta fp_src1+1 - lda #ec_t3 - sta fp_src2+1 - lda #ec_t5 - sta fp_dst+1 - jsr ec_mulp ; t5 = R*(X1*H^2 - X3) - - ; t6 = Y1*H^3 - lda #<(ec_p1+32) - sta fp_src1 - lda #>(ec_p1+32) - sta fp_src1+1 - lda #ec_t4 - sta fp_src2+1 - lda #ec_t6 - sta fp_dst+1 - jsr ec_mulp ; t6 = Y1*H^3 - - ; Y3 = t5 - t6 - lda #ec_t5 - sta fp_src1+1 - lda #ec_t6 - sta fp_src2+1 - lda #<(ec_p3+32) - sta fp_dst - lda #>(ec_p3+32) - sta fp_dst+1 - jsr fp_mod_sub ; Y3 - - ; Z3 = H*Z1 = t1*Z1 - lda #ec_t1 - sta fp_src1+1 - lda #<(ec_p1+64) - sta fp_src2 - lda #>(ec_p1+64) - sta fp_src2+1 - lda #<(ec_p3+64) - sta fp_dst - lda #>(ec_p3+64) - sta fp_dst+1 - jsr ec_mulp ; Z3 = H*Z1 - - rts - -; ============================================================================= -; ec_scalar_mul: ec_p3 = k * BasePoint -; k is a 32-byte scalar pointed to by ec_scalar_ptr. -; Uses double-and-add with the base point in ec_p2 (affine). -; Caller must load the affine base point into ec_p2 before calling. -; Result in ec_p3 (Jacobian). -; ============================================================================= -ec_scalar_mul: - ; Initialize ec_p1 = point at infinity (Z=0) - ldy #95 - lda #0 -@clr: sta ec_p1,y - dey - bpl @clr - - ; Process 256 bits of k, MSB first - lda #0 - sta ec_sc_byte ; byte index 0..31 - lda #$80 - sta ec_sc_mask ; bit mask - -@bitloop: - ; Double: ec_p1 = 2*ec_p1 (via ec_p3) - jsr ec_point_double ; ec_p3 = 2*ec_p1 - ; Copy ec_p3 -> ec_p1 - ldy #95 -@cp1: lda ec_p3,y - sta ec_p1,y - dey - bpl @cp1 - - ; Test bit of k - ldy ec_sc_byte - lda (ec_scalar_ptr),y - and ec_sc_mask - beq @nobit - - ; Add: ec_p1 = ec_p1 + ec_p2 (via ec_p3) - jsr ec_point_add ; ec_p3 = ec_p1 + G - ; Copy ec_p3 -> ec_p1 - ldy #95 -@cp2: lda ec_p3,y - sta ec_p1,y - dey - bpl @cp2 - -@nobit: - ; Advance to next bit - lsr ec_sc_mask - bne @bitloop - ; Next byte - lda #$80 - sta ec_sc_mask - inc ec_sc_byte - lda ec_sc_byte - cmp #32 - beq @done - jmp @bitloop - -@done: - ; Result is in ec_p1; copy to ec_p3 - ldy #95 -@cfin: lda ec_p1,y - sta ec_p3,y - dey - bpl @cfin - rts - -; ============================================================================= -; ec_jacobian_to_affine: convert ec_p3 (Jacobian) to affine (x,y) -; Result: ec_affine_x, ec_affine_y (32 bytes each) -; Computes x = X/Z^2, y = Y/Z^3 using modular inverse. -; ============================================================================= -ec_jacobian_to_affine: - jsr ec_set_modp - - ; Compute Z^(-1) - lda #<(ec_p3+64) - sta fp_src1 - lda #>(ec_p3+64) - sta fp_src1+1 - jsr fp_mod_inv ; fp_r0 = Z^(-1) - - ; Copy Z^(-1) to ec_t1 - ldy #31 -@czi: lda fp_r0,y - sta ec_t1,y - dey - bpl @czi - - ; t2 = Z^(-2) = Z^(-1) * Z^(-1) - lda #ec_t1 - sta fp_src1+1 - lda #ec_t1 - sta fp_src2+1 - lda #ec_t2 - sta fp_dst+1 - jsr ec_mulp ; t2 = Z^(-2) - - ; t3 = Z^(-3) = Z^(-2) * Z^(-1) - lda #ec_t2 - sta fp_src1+1 - lda #ec_t1 - sta fp_src2+1 - lda #ec_t3 - sta fp_dst+1 - jsr ec_mulp ; t3 = Z^(-3) - - ; x = X * Z^(-2) - lda #ec_p3 - sta fp_src1+1 - lda #ec_t2 - sta fp_src2+1 - lda #ec_affine_x - sta fp_dst+1 - jsr ec_mulp ; affine_x = X*Z^(-2) - - ; y = Y * Z^(-3) - lda #<(ec_p3+32) - sta fp_src1 - lda #>(ec_p3+32) - sta fp_src1+1 - lda #ec_t3 - sta fp_src2+1 - lda #ec_affine_y - sta fp_dst+1 - jsr ec_mulp ; affine_y = Y*Z^(-3) - - ; Copy affine result back to ec_p3 - ldy #31 -@cpx: lda ec_affine_x,y - sta ec_p3,y - dey - bpl @cpx - ldy #31 -@cpy2: lda ec_affine_y,y - sta ec_p3+32,y - dey - bpl @cpy2 - - rts diff --git a/src/crypto/ecdsa_points_384.asm b/src/crypto/ecdsa_points_384.asm deleted file mode 100644 index 759b774..0000000 --- a/src/crypto/ecdsa_points_384.asm +++ /dev/null @@ -1,886 +0,0 @@ -; ============================================================================= -; ecdsa_points_384.asm - Point operations for ECDSA P-384 -; ec_point_double_384, ec_point_add_384, ec_scalar_mul_384, -; ec_jacobian_to_affine_384 -; -; Adapted from ecdsa_points.asm (P-256) with 48-byte field elements. -; Jacobian point = 144 bytes (3 x 48). Y offset = +48, Z offset = +96. -; ============================================================================= - -; ============================================================================= -; ec_point_double_384: ec_p3_384 = 2 * ec_p1_384 (Jacobian) -; Formula for a = -3 (P-384): -; M = 3*(X1 - Z1^2)*(X1 + Z1^2) -; S = 4*X1*Y1^2 -; X3 = M^2 - 2*S -; Y3 = M*(S - X3) - 8*Y1^4 -; Z3 = 2*Y1*Z1 -; ============================================================================= -ec_point_double_384: - ; Check Z1 == 0 (point at infinity) - lda #<(ec_p1_384+96) - sta fp_src1 - lda #>(ec_p1_384+96) - sta fp_src1+1 - jsr fp_is_zero_384 - bne @notinf - ; Result = infinity - ldy #143 - lda #0 -@ci: sta ec_p3_384,y - dey - bpl @ci - rts - -@notinf: - jsr ec_set_modp_384 - - ; t1 = Z1^2 - lda #<(ec_p1_384+96) - sta fp_src1 - lda #>(ec_p1_384+96) - sta fp_src1+1 - lda #<(ec_p1_384+96) - sta fp_src2 - lda #>(ec_p1_384+96) - sta fp_src2+1 - lda #ec_t1_384 - sta fp_dst+1 - jsr ec_mulp_384 ; t1 = Z1^2 - - ; t2 = X1 - t1 - lda #ec_p1_384 - sta fp_src1+1 - lda #ec_t1_384 - sta fp_src2+1 - lda #ec_t2_384 - sta fp_dst+1 - jsr fp_mod_sub_384 ; t2 = X1 - Z1^2 - - ; t3 = X1 + t1 - lda #ec_p1_384 - sta fp_src1+1 - lda #ec_t1_384 - sta fp_src2+1 - lda #ec_t3_384 - sta fp_dst+1 - jsr fp_mod_add_384 ; t3 = X1 + Z1^2 - - ; t4 = t2 * t3 = (X1-Z^2)(X1+Z^2) - lda #ec_t2_384 - sta fp_src1+1 - lda #ec_t3_384 - sta fp_src2+1 - lda #ec_t4_384 - sta fp_dst+1 - jsr ec_mulp_384 ; t4 = X1^2 - Z1^4 - - ; M = 3*t4: t5 = 2*t4, t2 = t5+t4 = 3*t4 - lda #ec_t4_384 - sta fp_src1+1 - lda #ec_t4_384 - sta fp_src2+1 - lda #ec_t5_384 - sta fp_dst+1 - jsr fp_mod_add_384 ; t5 = 2*t4 - - lda #ec_t5_384 - sta fp_src1+1 - lda #ec_t4_384 - sta fp_src2+1 - lda #ec_t2_384 - sta fp_dst+1 - jsr fp_mod_add_384 ; t2 = M = 3*(X1^2 - Z1^4) - - ; t3 = Y1^2 - lda #<(ec_p1_384+48) - sta fp_src1 - lda #>(ec_p1_384+48) - sta fp_src1+1 - lda #<(ec_p1_384+48) - sta fp_src2 - lda #>(ec_p1_384+48) - sta fp_src2+1 - lda #ec_t3_384 - sta fp_dst+1 - jsr ec_mulp_384 ; t3 = Y1^2 - - ; t4 = X1 * Y1^2 - lda #ec_p1_384 - sta fp_src1+1 - lda #ec_t3_384 - sta fp_src2+1 - lda #ec_t4_384 - sta fp_dst+1 - jsr ec_mulp_384 ; t4 = X1*Y1^2 - - ; S = 4*X1*Y1^2 = 4*t4 - ; t5 = 2*t4 - lda #ec_t4_384 - sta fp_src1+1 - lda #ec_t4_384 - sta fp_src2+1 - lda #ec_t5_384 - sta fp_dst+1 - jsr fp_mod_add_384 ; t5 = 2*X1*Y1^2 - - ; t1 = S = 2*t5 = 4*X1*Y1^2 - lda #ec_t5_384 - sta fp_src1+1 - lda #ec_t5_384 - sta fp_src2+1 - lda #ec_t1_384 - sta fp_dst+1 - jsr fp_mod_add_384 ; t1 = S = 4*X1*Y1^2 - - ; X3 = M^2 - 2*S - ; t4 = M^2 - lda #ec_t2_384 - sta fp_src1+1 - lda #ec_t2_384 - sta fp_src2+1 - lda #ec_t4_384 - sta fp_dst+1 - jsr ec_mulp_384 ; t4 = M^2 - - ; t5 = 2*S - lda #ec_t1_384 - sta fp_src1+1 - lda #ec_t1_384 - sta fp_src2+1 - lda #ec_t5_384 - sta fp_dst+1 - jsr fp_mod_add_384 ; t5 = 2*S - - ; X3 = t4 - t5 - lda #ec_t4_384 - sta fp_src1+1 - lda #ec_t5_384 - sta fp_src2+1 - lda #ec_p3_384 - sta fp_dst+1 - jsr fp_mod_sub_384 ; X3 = M^2 - 2S - - ; Y3 = M*(S - X3) - 8*Y1^4 - ; t4 = S - X3 - lda #ec_t1_384 - sta fp_src1+1 - lda #ec_p3_384 - sta fp_src2+1 - lda #ec_t4_384 - sta fp_dst+1 - jsr fp_mod_sub_384 ; t4 = S - X3 - - ; t5 = M*(S-X3) - lda #ec_t2_384 - sta fp_src1+1 - lda #ec_t4_384 - sta fp_src2+1 - lda #ec_t5_384 - sta fp_dst+1 - jsr ec_mulp_384 ; t5 = M*(S-X3) - - ; t4 = Y1^4 = (Y1^2)^2 = t3^2 - lda #ec_t3_384 - sta fp_src1+1 - lda #ec_t3_384 - sta fp_src2+1 - lda #ec_t4_384 - sta fp_dst+1 - jsr ec_mulp_384 ; t4 = Y1^4 - - ; 8*Y1^4: t6 = 2*t4, t4 = 2*t6 = 4*Y1^4, t6 = 2*t4 = 8*Y1^4 - lda #ec_t4_384 - sta fp_src1+1 - lda #ec_t4_384 - sta fp_src2+1 - lda #ec_t6_384 - sta fp_dst+1 - jsr fp_mod_add_384 ; t6 = 2*Y1^4 - - lda #ec_t6_384 - sta fp_src1+1 - lda #ec_t6_384 - sta fp_src2+1 - lda #ec_t4_384 - sta fp_dst+1 - jsr fp_mod_add_384 ; t4 = 4*Y1^4 - - lda #ec_t4_384 - sta fp_src1+1 - lda #ec_t4_384 - sta fp_src2+1 - lda #ec_t6_384 - sta fp_dst+1 - jsr fp_mod_add_384 ; t6 = 8*Y1^4 - - ; Y3 = t5 - t6 - lda #ec_t5_384 - sta fp_src1+1 - lda #ec_t6_384 - sta fp_src2+1 - lda #<(ec_p3_384+48) - sta fp_dst - lda #>(ec_p3_384+48) - sta fp_dst+1 - jsr fp_mod_sub_384 ; Y3 = M*(S-X3) - 8*Y1^4 - - ; Z3 = 2*Y1*Z1 - ; t1 = Y1*Z1 - lda #<(ec_p1_384+48) - sta fp_src1 - lda #>(ec_p1_384+48) - sta fp_src1+1 - lda #<(ec_p1_384+96) - sta fp_src2 - lda #>(ec_p1_384+96) - sta fp_src2+1 - lda #ec_t1_384 - sta fp_dst+1 - jsr ec_mulp_384 ; t1 = Y1*Z1 - - ; Z3 = 2*t1 - lda #ec_t1_384 - sta fp_src1+1 - lda #ec_t1_384 - sta fp_src2+1 - lda #<(ec_p3_384+96) - sta fp_dst - lda #>(ec_p3_384+96) - sta fp_dst+1 - jsr fp_mod_add_384 ; Z3 = 2*Y1*Z1 - - rts - -; ============================================================================= -; ec_point_add_384: ec_p3_384 = ec_p1_384 + ec_p2_384 -; P1 is Jacobian (X1,Y1,Z1). P2 is AFFINE (X2,Y2, Z2 assumed 1). -; -; U2 = X2*Z1^2, S2 = Y2*Z1^3 -; H = U2 - X1, R = S2 - Y1 -; If H==0: if R==0 -> double, else -> infinity -; X3 = R^2 - H^3 - 2*X1*H^2 -; Y3 = R*(X1*H^2 - X3) - Y1*H^3 -; Z3 = H*Z1 -; ============================================================================= -ec_point_add_384: - ; If P1 is infinity (Z1==0): result = P2 with Z=1 - lda #<(ec_p1_384+96) - sta fp_src1 - lda #>(ec_p1_384+96) - sta fp_src1+1 - jsr fp_is_zero_384 - bne @p1ok - - ; Copy P2 to P3 as Jacobian with Z=1 - ldy #47 -@cpx: lda ec_p2_384,y - sta ec_p3_384,y - dey - bpl @cpx - ldy #47 -@cpy: lda ec_p2_384+48,y - sta ec_p3_384+48,y - dey - bpl @cpy - ldy #47 - lda #0 -@clz: sta ec_p3_384+96,y - dey - bpl @clz - lda #1 - sta ec_p3_384+143 ; Z = 1 - rts - -@p1ok: - jsr ec_set_modp_384 - - ; t1 = Z1^2 - lda #<(ec_p1_384+96) - sta fp_src1 - lda #>(ec_p1_384+96) - sta fp_src1+1 - lda #<(ec_p1_384+96) - sta fp_src2 - lda #>(ec_p1_384+96) - sta fp_src2+1 - lda #ec_t1_384 - sta fp_dst+1 - jsr ec_mulp_384 ; t1 = Z1^2 - - ; t2 = X2*Z1^2 = U2 - lda #ec_p2_384 - sta fp_src1+1 - lda #ec_t1_384 - sta fp_src2+1 - lda #ec_t2_384 - sta fp_dst+1 - jsr ec_mulp_384 ; t2 = U2 - - ; t3 = Z1^3 = Z1*t1 - lda #<(ec_p1_384+96) - sta fp_src1 - lda #>(ec_p1_384+96) - sta fp_src1+1 - lda #ec_t1_384 - sta fp_src2+1 - lda #ec_t3_384 - sta fp_dst+1 - jsr ec_mulp_384 ; t3 = Z1^3 - - ; t4 = Y2*Z1^3 = S2 - lda #<(ec_p2_384+48) - sta fp_src1 - lda #>(ec_p2_384+48) - sta fp_src1+1 - lda #ec_t3_384 - sta fp_src2+1 - lda #ec_t4_384 - sta fp_dst+1 - jsr ec_mulp_384 ; t4 = S2 - - ; H = U2 - X1 = t2 - X1 -> t1 - lda #ec_t2_384 - sta fp_src1+1 - lda #ec_p1_384 - sta fp_src2+1 - lda #ec_t1_384 - sta fp_dst+1 - jsr fp_mod_sub_384 ; t1 = H = U2 - X1 - - ; R = S2 - Y1 = t4 - Y1 -> t2 - lda #ec_t4_384 - sta fp_src1+1 - lda #<(ec_p1_384+48) - sta fp_src2 - lda #>(ec_p1_384+48) - sta fp_src2+1 - lda #ec_t2_384 - sta fp_dst+1 - jsr fp_mod_sub_384 ; t2 = R = S2 - Y1 - - ; Check H == 0 - lda #ec_t1_384 - sta fp_src1+1 - jsr fp_is_zero_384 - bne @h_nonzero - - ; H == 0: check R - lda #ec_t2_384 - sta fp_src1+1 - jsr fp_is_zero_384 - bne @set_inf - ; H==0, R==0: points are equal, double P1 - jmp ec_point_double_384 - -@set_inf: - ; H==0, R!=0: inverse points, result = infinity - ldy #143 - lda #0 -@sinf: sta ec_p3_384,y - dey - bpl @sinf - rts - -@h_nonzero: - ; t3 = H^2 - lda #ec_t1_384 - sta fp_src1+1 - lda #ec_t1_384 - sta fp_src2+1 - lda #ec_t3_384 - sta fp_dst+1 - jsr ec_mulp_384 ; t3 = H^2 - - ; t4 = H^3 = H*H^2 - lda #ec_t1_384 - sta fp_src1+1 - lda #ec_t3_384 - sta fp_src2+1 - lda #ec_t4_384 - sta fp_dst+1 - jsr ec_mulp_384 ; t4 = H^3 - - ; t5 = X1*H^2 - lda #ec_p1_384 - sta fp_src1+1 - lda #ec_t3_384 - sta fp_src2+1 - lda #ec_t5_384 - sta fp_dst+1 - jsr ec_mulp_384 ; t5 = X1*H^2 - - ; X3 = R^2 - H^3 - 2*X1*H^2 - ; t3 = R^2 - lda #ec_t2_384 - sta fp_src1+1 - lda #ec_t2_384 - sta fp_src2+1 - lda #ec_t3_384 - sta fp_dst+1 - jsr ec_mulp_384 ; t3 = R^2 - - ; t3 = R^2 - H^3 - lda #ec_t3_384 - sta fp_src1+1 - lda #ec_t4_384 - sta fp_src2+1 - lda #ec_t3_384 - sta fp_dst+1 - jsr fp_mod_sub_384 ; t3 = R^2 - H^3 - - ; t6 = 2*X1*H^2 - lda #ec_t5_384 - sta fp_src1+1 - lda #ec_t5_384 - sta fp_src2+1 - lda #ec_t6_384 - sta fp_dst+1 - jsr fp_mod_add_384 ; t6 = 2*X1*H^2 - - ; X3 = t3 - t6 - lda #ec_t3_384 - sta fp_src1+1 - lda #ec_t6_384 - sta fp_src2+1 - lda #ec_p3_384 - sta fp_dst+1 - jsr fp_mod_sub_384 ; X3 - - ; Y3 = R*(X1*H^2 - X3) - Y1*H^3 - ; t3 = X1*H^2 - X3 = t5 - X3 - lda #ec_t5_384 - sta fp_src1+1 - lda #ec_p3_384 - sta fp_src2+1 - lda #ec_t3_384 - sta fp_dst+1 - jsr fp_mod_sub_384 ; t3 = X1*H^2 - X3 - - ; t5 = R * t3 - lda #ec_t2_384 - sta fp_src1+1 - lda #ec_t3_384 - sta fp_src2+1 - lda #ec_t5_384 - sta fp_dst+1 - jsr ec_mulp_384 ; t5 = R*(X1*H^2 - X3) - - ; t6 = Y1*H^3 - lda #<(ec_p1_384+48) - sta fp_src1 - lda #>(ec_p1_384+48) - sta fp_src1+1 - lda #ec_t4_384 - sta fp_src2+1 - lda #ec_t6_384 - sta fp_dst+1 - jsr ec_mulp_384 ; t6 = Y1*H^3 - - ; Y3 = t5 - t6 - lda #ec_t5_384 - sta fp_src1+1 - lda #ec_t6_384 - sta fp_src2+1 - lda #<(ec_p3_384+48) - sta fp_dst - lda #>(ec_p3_384+48) - sta fp_dst+1 - jsr fp_mod_sub_384 ; Y3 - - ; Z3 = H*Z1 = t1*Z1 - lda #ec_t1_384 - sta fp_src1+1 - lda #<(ec_p1_384+96) - sta fp_src2 - lda #>(ec_p1_384+96) - sta fp_src2+1 - lda #<(ec_p3_384+96) - sta fp_dst - lda #>(ec_p3_384+96) - sta fp_dst+1 - jsr ec_mulp_384 ; Z3 = H*Z1 - - rts - -; ============================================================================= -; ec_scalar_mul_384: ec_p3_384 = k * ec_p2_384 -; k is a 48-byte scalar pointed to by ec_scalar_ptr (ZP $3b). -; ec_p2_384 must be set by caller to the affine point (X,Y) to multiply. -; Uses double-and-add with ec_p2_384 (affine). -; Result in ec_p3_384 (Jacobian). -; ============================================================================= -ec_scalar_mul_384: - ; Initialize ec_p1_384 = point at infinity (Z=0) - ldy #143 - lda #0 -@clr: sta ec_p1_384,y - dey - bpl @clr - - ; Caller must set ec_p2_384 before calling - - ; Process 384 bits of k, MSB first - lda #0 - sta ec_sc_byte_384 ; byte index 0..47 - lda #$80 - sta ec_sc_mask_384 ; bit mask - -@bitloop: - ; Double: ec_p1_384 = 2*ec_p1_384 (via ec_p3_384) - jsr ec_point_double_384 ; ec_p3_384 = 2*ec_p1_384 - ; Copy ec_p3_384 -> ec_p1_384 - ldy #143 -@cp1: lda ec_p3_384,y - sta ec_p1_384,y - dey - bpl @cp1 - - ; Test bit of k - ldy ec_sc_byte_384 - lda (ec_scalar_ptr),y - and ec_sc_mask_384 - beq @nobit - - ; Add: ec_p1_384 = ec_p1_384 + ec_p2_384 (via ec_p3_384) - jsr ec_point_add_384 ; ec_p3_384 = ec_p1_384 + G - ; Copy ec_p3_384 -> ec_p1_384 - ldy #143 -@cp2: lda ec_p3_384,y - sta ec_p1_384,y - dey - bpl @cp2 - -@nobit: - ; Advance to next bit - lsr ec_sc_mask_384 - bne @bitloop - ; Next byte - lda #$80 - sta ec_sc_mask_384 - inc ec_sc_byte_384 - lda ec_sc_byte_384 - cmp #48 - beq @done - jmp @bitloop - -@done: - ; Result is in ec_p1_384; copy to ec_p3_384 - ldy #143 -@cfin: lda ec_p1_384,y - sta ec_p3_384,y - dey - bpl @cfin - rts - -ec_sc_byte_384: !byte 0 -ec_sc_mask_384: !byte 0 - -; ============================================================================= -; ec_jacobian_to_affine_384: convert ec_p3_384 (Jacobian) to affine (x,y) -; Result: ec_affine_x_384, ec_affine_y_384 (48 bytes each) -; Computes x = X/Z^2, y = Y/Z^3 using modular inverse. -; ============================================================================= -ec_affine_x_384: !fill 48, 0 -ec_affine_y_384: !fill 48, 0 - -ec_jacobian_to_affine_384: - jsr ec_set_modp_384 - - ; Compute Z^(-1) - lda #<(ec_p3_384+96) - sta fp_src1 - lda #>(ec_p3_384+96) - sta fp_src1+1 - jsr fp_mod_inv_384 ; fp_r0_384 = Z^(-1) - - ; Copy Z^(-1) to ec_t1_384 - ldy #47 -@czi: lda fp_r0_384,y - sta ec_t1_384,y - dey - bpl @czi - - ; t2 = Z^(-2) = Z^(-1) * Z^(-1) - lda #ec_t1_384 - sta fp_src1+1 - lda #ec_t1_384 - sta fp_src2+1 - lda #ec_t2_384 - sta fp_dst+1 - jsr ec_mulp_384 ; t2 = Z^(-2) - - ; t3 = Z^(-3) = Z^(-2) * Z^(-1) - lda #ec_t2_384 - sta fp_src1+1 - lda #ec_t1_384 - sta fp_src2+1 - lda #ec_t3_384 - sta fp_dst+1 - jsr ec_mulp_384 ; t3 = Z^(-3) - - ; x = X * Z^(-2) - lda #ec_p3_384 - sta fp_src1+1 - lda #ec_t2_384 - sta fp_src2+1 - lda #ec_affine_x_384 - sta fp_dst+1 - jsr ec_mulp_384 ; affine_x = X*Z^(-2) - - ; y = Y * Z^(-3) - lda #<(ec_p3_384+48) - sta fp_src1 - lda #>(ec_p3_384+48) - sta fp_src1+1 - lda #ec_t3_384 - sta fp_src2+1 - lda #ec_affine_y_384 - sta fp_dst+1 - jsr ec_mulp_384 ; affine_y = Y*Z^(-3) - - ; Copy affine result back to ec_p3_384 - ldy #47 -@cpx: lda ec_affine_x_384,y - sta ec_p3_384,y - dey - bpl @cpx - ldy #47 -@cpy2: lda ec_affine_y_384,y - sta ec_p3_384+48,y - dey - bpl @cpy2 - - rts diff --git a/src/crypto/ecdsa_verify.s b/src/crypto/ecdsa_verify.s index 0811eda..7f3b1f1 100644 --- a/src/crypto/ecdsa_verify.s +++ b/src/crypto/ecdsa_verify.s @@ -1,456 +1,112 @@ -; ecdsa_verify.s - P-256 (and P-384 dispatch stub) ECDSA signature verification -; Converted from ACME to ca65 in Phase 3 Batch A. +; ============================================================================= +; ecdsa_verify.s - Thin TLS-facing dispatcher over sibling c64-nist-curves. +; +; Phase C.4 rewrite. The in-tree P-256 primitives +; (ecdsa_{points,fp,mod,curve}.s) have been removed from the link; this file +; now just packs the TLS layer's BE state buffers into the 160-byte flat +; struct expected by the sibling's `ecdsa_verify_256` and forwards its +; carry flag. DER signature parsing remains in-tree (same format as before). ; -; Verifies ECDSA signatures as required for TLS 1.3 CertificateVerify -; (P-256/SHA-256) and certificate chain verification (P-384). +; Inputs provided by TLS cert handler (big-endian, 32 bytes each): +; ecdsa_hash, ecdsa_sig_r, ecdsa_sig_s, ecdsa_pubkey_x, ecdsa_pubkey_y +; Plus: ecdsa_curve_id (0 = P-256, 1 = P-384), ecdsa_sig_len, ecdsa_hash_len. ; -; Input: ecdsa_curve_id (0=P-256, 1=P-384) -; ecdsa_hash (32 or 48 bytes) = hash of message (z) -; ecdsa_sig_r (32 or 48 bytes) = signature r -; ecdsa_sig_s (32 or 48 bytes) = signature s -; ecdsa_pubkey_x/y (32 or 48 bytes each) = public key Q -; Output: C=0 signature valid, C=1 invalid +; Output: C=0 signature VALID, C=1 INVALID or unsupported curve. ; -; NOTE: P-384 dispatch is currently stubbed (returns error). A full -; P-384 verify body existed in an earlier revision — this post-fix file -; only keeps the dispatch stub and the DER parser remains curve-agnostic. -; P-384-suffixed symbols are still declared as `.import` below so future -; restoration links cleanly once ecdsa_*_384.s exist. +; P-384 dispatch remains stubbed (see project_p384_stubbed memory note). ; ============================================================================= .include "constants.inc" -; --- Externals: fp / ec helpers (ecdsa_fp, ecdsa_mod, ecdsa_curve) --- -.import fp_copy -.import fp_zero -.import fp_cmp -.import fp_is_zero -.import fp_sub -.import fp_mod_mul -.import fp_mod_inv -.import fp_r0 - -.import ec_set_modn -.import ec_set_modp -.import ec_scalar_mul -.import ec_point_add -.import ec_jacobian_to_affine - -; --- Externals: P-256 curve data / scratch points (ecdsa_curve / ecdsa_points) --- -.import ec_p1 -.import ec_p2 -.import ec_p3 -.import ec_gx -.import ec_gy -.import ec_n - -; --- Externals: P-384 symbols (currently unresolved; preserved for later restore) --- -.import fp_copy_384 -.import fp_zero_384 -.import fp_cmp_384 -.import fp_is_zero_384 -.import fp_sub_384 -.import fp_mod_mul_384 -.import fp_mod_inv_384 -.import fp_r0_384 - -.import ec_set_modn_384 -.import ec_set_modp_384 -.import ec_scalar_mul_384 -.import ec_point_add_384 -.import ec_jacobian_to_affine_384 - -.import ec_p1_384 -.import ec_p2_384 -.import ec_p3_384 -.import ec_gx_384 -.import ec_gy_384 -.import ec_n_384 - -; --- Externals: mutable data buffers (data.asm) --- +; --- External sibling (c64-nist-curves) entry points --- +.import ecdsa_verify_256 +.import ec_scalar_mul_var +.import ec_gx256, ec_gy256 +.import ec_base_x, ec_base_y + +; --- State buffers (in-tree data.s) --- .import ecdsa_curve_id .import ecdsa_hash -.import ecdsa_hash_len .import ecdsa_sig_r .import ecdsa_sig_s .import ecdsa_sig_len .import ecdsa_pubkey_x .import ecdsa_pubkey_y -.import ecdsa_verify_tmp - -.import ev_u1 -.import ev_u2 -.import ev_point_save - -.import ev_u1_384 -.import ev_u2_384 -.import ev_point_save_384 +; 160-byte packed struct (SHADOW_BSS via CRYPTO_BSS below) +; ev_der_int_len / ev_der_copy_cnt retained for DER parser .import ev_der_int_len .import ev_der_copy_cnt ; --- Exports --- .export ecdsa_verify -.export ecdsa_verify_256 -.export ecdsa_verify_384 .export ecdsa_parse_der_sig +.export ec_scalar_mul ; shim for sibling's Lim-Lee slot + .segment "CRYPTO_CODE" ; ============================================================================= -; Curve dispatch +; ecdsa_verify - curve dispatch (called from tls_cert.s) ; ============================================================================= ecdsa_verify: - ; Ensure BASIC ROM is banked out — ECDSA data buffers live at $A000+ - lda $01 - and #%11111110 ; clear LORAM (bit 0) -> BASIC ROM off - sta $01 - + ; Boot banks out BASIC ROM before any crypto runs (src/boot.s + ; writes $36 to $01); the legacy defensive re-bank here was + ; redundant and has been removed to save bytes. lda ecdsa_curve_id - bne @p384 - jmp ecdsa_verify_256 -@p384: - ; TODO: restore P-384 dispatch — see project memory project_p384_stubbed.md - sec - rts - -; ============================================================================= -; ecdsa_verify_256 - P-256 signature verification -; ============================================================================= -ecdsa_verify_256: - ; --------------------------------------------------------------- - ; Step 1: Validate r and s are in [1, n-1] - ; --------------------------------------------------------------- - - ; Check r != 0 - lda #ecdsa_sig_r - sta fp_src1+1 - jsr fp_is_zero - beq @256_invalid ; r == 0 -> invalid - - ; Check r < n - lda #ecdsa_sig_r - sta fp_src1+1 - lda #ec_n - sta fp_src2+1 - jsr fp_cmp - bcs @256_invalid ; r >= n -> invalid - - ; Check s != 0 - lda #ecdsa_sig_s - sta fp_src1+1 - jsr fp_is_zero - beq @256_invalid ; s == 0 -> invalid - - ; Check s < n - lda #ecdsa_sig_s - sta fp_src1+1 - lda #ec_n - sta fp_src2+1 - jsr fp_cmp - bcs @256_invalid ; s >= n -> invalid - jmp @256_step2 - -@256_invalid: + beq @p256 + ; P-384 verify still stubbed (project_p384_stubbed). sec rts - ; --------------------------------------------------------------- - ; Step 2: w = s^(-1) mod n - ; --------------------------------------------------------------- -@256_step2: - jsr ec_set_modn ; fp_misc = ec_n - lda #ecdsa_sig_s - sta fp_src1+1 - jsr fp_mod_inv ; fp_r0 = s^(-1) mod n - - ; Copy w = fp_r0 -> ecdsa_verify_tmp - lda #fp_r0 - sta fp_src1+1 - lda #ecdsa_verify_tmp - sta fp_dst+1 - jsr fp_copy ; ecdsa_verify_tmp = w - - ; --------------------------------------------------------------- - ; Step 3: u1 = z * w mod n - ; --------------------------------------------------------------- - jsr ec_set_modn ; fp_misc = ec_n - lda #ecdsa_hash - sta fp_src1+1 - lda #ecdsa_verify_tmp - sta fp_src2+1 - jsr fp_mod_mul ; fp_r0 = z * w mod n - - ; Copy u1 = fp_r0 -> ev_u1 - lda #fp_r0 - sta fp_src1+1 - lda #ev_u1 - sta fp_dst+1 - jsr fp_copy ; ev_u1 = u1 - - ; --------------------------------------------------------------- - ; Step 4: u2 = r * w mod n - ; --------------------------------------------------------------- - jsr ec_set_modn ; fp_misc = ec_n - lda #ecdsa_sig_r - sta fp_src1+1 - lda #ecdsa_verify_tmp - sta fp_src2+1 - jsr fp_mod_mul ; fp_r0 = r * w mod n - - ; Copy u2 = fp_r0 -> ev_u2 - lda #fp_r0 - sta fp_src1+1 - lda #ev_u2 - sta fp_dst+1 - jsr fp_copy ; ev_u2 = u2 - - ; --------------------------------------------------------------- - ; Step 5a: Compute u1 * G - ; Load generator G into ec_p2 (affine base point for scalar mul) - ; --------------------------------------------------------------- - - ; ec_p2.X = ec_gx - lda #ec_gx - sta fp_src1+1 - lda #ec_p2 - sta fp_dst+1 - jsr fp_copy - - ; ec_p2.Y = ec_gy - lda #ec_gy - sta fp_src1+1 - lda #<(ec_p2+32) - sta fp_dst - lda #>(ec_p2+32) - sta fp_dst+1 - jsr fp_copy - - ; Set scalar pointer to u1 - lda #ev_u1 - sta ec_scalar_ptr+1 - - ; ec_p3 = u1 * G (result in ec_p3, Jacobian) - jsr ec_scalar_mul - - ; Save u1*G result from ec_p3 to ev_point_save (96 bytes) - ldx #95 -@save_u1g: - lda ec_p3,x - sta ev_point_save,x - dex - bpl @save_u1g - - ; --------------------------------------------------------------- - ; Step 5b: Compute u2 * Q - ; Load public key Q into ec_p2 (affine base point for scalar mul) - ; --------------------------------------------------------------- - - ; ec_p2.X = ecdsa_pubkey_x - lda #ecdsa_pubkey_x - sta fp_src1+1 - lda #ec_p2 - sta fp_dst+1 - jsr fp_copy - - ; ec_p2.Y = ecdsa_pubkey_y - lda #ecdsa_pubkey_y - sta fp_src1+1 - lda #<(ec_p2+32) - sta fp_dst - lda #>(ec_p2+32) - sta fp_dst+1 - jsr fp_copy - - ; Set scalar pointer to u2 - lda #ev_u2 - sta ec_scalar_ptr+1 - - ; ec_p3 = u2 * Q (result in ec_p3, Jacobian) - jsr ec_scalar_mul - - ; --------------------------------------------------------------- - ; Step 5c: R = u1*G + u2*Q (point addition) - ; ec_p1 = u1*G (restore from save), ec_p2 = u2*Q (from ec_p3) - ; --------------------------------------------------------------- - - ; Copy u1*G from save into ec_p1 - ldx #95 -@restore_u1g: - lda ev_point_save,x - sta ec_p1,x - dex - bpl @restore_u1g - - ; Copy u2*Q from ec_p3 into ec_p2 - ; ec_point_add uses ec_p2 in affine (X,Y) but we have Jacobian. - ; Convert u2*Q to affine first, then load into ec_p2. - jsr ec_jacobian_to_affine ; converts ec_p3 in-place -> affine X,Y - - ldx #31 -@copy_u2q_x: - lda ec_p3,x ; affine X - sta ec_p2,x - dex - bpl @copy_u2q_x - - ldx #31 -@copy_u2q_y: - lda ec_p3+32,x ; affine Y - sta ec_p2+32,x - dex - bpl @copy_u2q_y - - ; ec_p3 = ec_p1 + ec_p2 (Jacobian + affine -> Jacobian) - jsr ec_point_add - - ; --------------------------------------------------------------- - ; Step 6: Convert R to affine - ; --------------------------------------------------------------- - jsr ec_jacobian_to_affine ; converts ec_p3 in-place - - ; Check R is not point at infinity (Z was 0 before conversion) - ; After affine conversion, if Z was 0 the result is undefined. - ; ec_jacobian_to_affine should flag this; we check X for zero - ; as a sanity check (astronomically unlikely for valid sig). - - ; --------------------------------------------------------------- - ; Step 7: Check R.x mod n == r - ; R.x is already reduced mod p. We need R.x mod n. - ; Since p and n are close for P-256, R.x mod n may just be R.x, - ; but we must check: if R.x >= n, subtract n. - ; --------------------------------------------------------------- - - ; Compare R.x (in ec_p3) with n - lda #ec_p3 - sta fp_src1+1 - lda #ec_n - sta fp_src2+1 - jsr fp_cmp - bcc @256_no_reduce ; R.x < n, no reduction needed - - ; R.x >= n: compute R.x - n -> ev_u1 (reuse buffer) - lda #ec_p3 - sta fp_src1+1 - lda #ec_n - sta fp_src2+1 - lda #ev_u1 - sta fp_dst+1 - jsr fp_sub - - ; Compare ev_u1 with r - lda #ev_u1 - sta fp_src1+1 - jmp @256_final_cmp - -@256_no_reduce: - ; Compare R.x directly with r - lda #ec_p3 - sta fp_src1+1 - -@256_final_cmp: +@p256: + ; The TLS-populated ecdsa_sig_r, ecdsa_sig_s, ecdsa_hash, + ; ecdsa_pubkey_x, ecdsa_pubkey_y are laid out contiguously at 32 B + ; each (see src/data.s), matching the r|s|h|Qx|Qy BE struct that + ; the sibling's ecdsa_verify_256 ingests. Just hand it the base. lda #ecdsa_sig_r - sta fp_src2+1 - jsr fp_cmp - bne @256_mismatch + ldx #>ecdsa_sig_r + jmp ecdsa_verify_256 ; carry = verify result, returns to caller - ; R.x mod n == r -> signature valid - clc - rts - -@256_mismatch: - sec - rts ; ============================================================================= -; ecdsa_verify_384 - P-384 signature verification +; ec_scalar_mul - shim providing the symbol name the sibling's +; ecdsa_verify_256 uses for fixed-base u1*G. The sibling ships a real +; Lim-Lee 8-way comb implementation that depends on a 16 KB REU bank-2 +; precompute table (built at boot by ec_precompute_256). That precompute +; is not wired into c64-https; we skip it by redirecting the fixed-base +; call to the variable-base primitive with G pre-loaded as the base point. +; Slower (double-and-add instead of windowed comb), but correct and avoids +; dragging ~22 KB of Lim-Lee infrastructure into the PRG. +; +; ec_gx256/ec_gy256 and ec_base_x/ec_base_y are each declared as +; contiguous 32-byte slots (X then Y) in the sibling's data segments, so +; a single 64-byte copy loop suffices for both coordinates. ; ============================================================================= -ecdsa_verify_384: - ; STUBBED — see project_p384_stubbed.md - ; Full P-384 verify body removed to save space; dispatch in ecdsa_verify - ; returns error for non-P-256 curves before reaching this label. - sec - rts +ec_scalar_mul: + ldy #63 +@cp_g: lda ec_gx256,y ; also covers ec_gy256 at +32 + sta ec_base_x,y ; also covers ec_base_y at +32 + dey + bpl @cp_g + jmp ec_scalar_mul_var + ; ============================================================================= -; DER signature parsing -; ============================================================================= -; ecdsa_parse_der_sig - Parse DER-encoded ECDSA signature into r, s +; ecdsa_parse_der_sig - Parse ASN.1 DER ECDSA signature into ecdsa_sig_r/s. ; -; Input: zp_ptr ($FB-$FC) = pointer to DER signature data -; zp_count ($FE) = total signature length -; ecdsa_sig_len = expected component length (32 or 48) -; Output: ecdsa_sig_r, ecdsa_sig_s filled, C=0 ok, C=1 parse error +; Input: zp_ptr points at SEQUENCE start of the DER signature. +; ecdsa_sig_len = 32 (P-256). +; Output: ecdsa_sig_r, ecdsa_sig_s filled (right-aligned, zero-padded BE). +; C=0 on success, C=1 on malformed DER. ; ; DER format: SEQUENCE { INTEGER r, INTEGER s } ; 30 02 02 ; INTEGERs may have a leading 0x00 padding byte if high bit is set. +; +; Body lifted verbatim from the pre-Phase-C.4 file; only `jsr fp_zero` +; calls replaced with an inline 48-byte clear (48 covers both P-256 and +; a future P-384 restore). ; ============================================================================= ecdsa_parse_der_sig: ldy #0 @@ -460,55 +116,40 @@ ecdsa_parse_der_sig: cmp #$30 beq :+ jmp @der_error -: - iny +: iny - ; Skip SEQUENCE length byte (we trust the outer length) + ; Skip SEQUENCE length byte iny ; --- Parse first INTEGER (r) --- - ; Expect INTEGER tag (0x02) lda (zp_ptr),y cmp #$02 beq :+ jmp @der_error -: - iny +: iny - ; Read r length lda (zp_ptr),y sta ev_der_int_len iny - ; Clear ecdsa_sig_r. fp_zero clobbers Y (uses it as a counter), - ; so save/restore around the call — we need Y to still point at the - ; first byte of r after the clear. - lda #ecdsa_sig_r - sta fp_dst+1 + ; Clear ecdsa_sig_r (32 bytes). Preserve Y (DER cursor). tya pha - lda ecdsa_sig_len - cmp #48 - beq @clr_r_384 - jsr fp_zero - pla - tay - jmp @parse_r -@clr_r_384: - jsr fp_zero ; STUBBED — dead code for P-256 only + ldx #31 + lda #0 +@clr_r: sta ecdsa_sig_r,x + dex + bpl @clr_r pla tay @parse_r: - ; Handle leading zero padding: if int_len > sig_len, skip leading 0x00 lda ev_der_int_len cmp ecdsa_sig_len beq @r_no_pad bcc @r_short - ; int_len > sig_len: skip (int_len - sig_len) leading zeros + ; int_len > sig_len: skip leading zeros lda ev_der_int_len sec sbc ecdsa_sig_len @@ -522,16 +163,13 @@ ecdsa_parse_der_sig: jmp @r_no_pad @r_short: - ; int_len < sig_len: right-align in buffer - ; dest offset = sig_len - int_len - ; Handled by the copy below (starts at offset) + ; int_len < sig_len: right-align (handled by dest offset below). @r_no_pad: - ; Copy r bytes, right-aligned in ecdsa_sig_r lda ecdsa_sig_len sec sbc ev_der_int_len - tax ; X = dest offset + tax lda ev_der_int_len sta ev_der_copy_cnt @r_copy: @@ -544,41 +182,28 @@ ecdsa_parse_der_sig: dec ev_der_copy_cnt jmp @r_copy - ; --- Parse second INTEGER (s) --- @parse_s_tag: lda (zp_ptr),y cmp #$02 bne @der_error iny - ; Read s length lda (zp_ptr),y sta ev_der_int_len iny - ; Clear ecdsa_sig_s. Same Y-preservation dance as for r above: - ; fp_zero clobbers Y but we need Y to continue pointing at the - ; first byte of s after the clear. - lda #ecdsa_sig_s - sta fp_dst+1 + ; Clear ecdsa_sig_s (32 bytes). tya pha - lda ecdsa_sig_len - cmp #48 - beq @clr_s_384 - jsr fp_zero - pla - tay - jmp @parse_s -@clr_s_384: - jsr fp_zero ; STUBBED — dead code for P-256 only + ldx #31 + lda #0 +@clr_s: sta ecdsa_sig_s,x + dex + bpl @clr_s pla tay @parse_s: - ; Handle leading zero padding lda ev_der_int_len cmp ecdsa_sig_len beq @s_no_pad @@ -599,7 +224,6 @@ ecdsa_parse_der_sig: @s_short: @s_no_pad: - ; Copy s bytes, right-aligned in ecdsa_sig_s lda ecdsa_sig_len sec sbc ev_der_int_len @@ -623,3 +247,11 @@ ecdsa_parse_der_sig: @der_error: sec rts + + +; ============================================================================= +; The 160-byte packed r|s|h|Qx|Qy struct required by ecdsa_verify_256 is +; composed in-place by the contiguous `ecdsa_sig_r`, `ecdsa_sig_s`, +; `ecdsa_hash`, `ecdsa_pubkey_x`, `ecdsa_pubkey_y` declarations in +; src/data.s (32 bytes each, in that order). See data.s Phase C.4 note. +; ============================================================================= diff --git a/src/crypto/hmac_drbg.s b/src/crypto/hmac_drbg.s index 61cf0ba..4b032b6 100644 --- a/src/crypto/hmac_drbg.s +++ b/src/crypto/hmac_drbg.s @@ -53,7 +53,11 @@ .import sha256_block .import sha256_hash -.segment "CRYPTO_CODE" +; Phase C.4 fit: hmac_drbg occupies a separate aux-code segment so the +; ip65 cfg can route it to NET_CODE tail while sha256 (larger) rides +; NET_BSS_TAIL alongside TLS_CODE. Under UCI both segments flow into +; NET_CODE via identical cfg rules, preserving Phase C.2's behavior. +.segment "CRYPTO_AUX_CODE2" ; ============================================================================= ; hmac_sha256 - compute HMAC-SHA256 diff --git a/src/crypto/p384_force_link.s b/src/crypto/p384_force_link.s new file mode 100644 index 0000000..26bf1cd --- /dev/null +++ b/src/crypto/p384_force_link.s @@ -0,0 +1,35 @@ +; ============================================================================= +; p384_force_link.s - Force linker to pull P-384 archive members +; +; Phase C.3 (Option B): the c64-nist-curves sibling at libs/nistcurves/ +; supplies three variable-base P-384 primitives (ec_point_double_384, +; ec_point_add_384, ec_jacobian_to_affine_384). None of the production +; TLS call sites reference them yet — tools/test_p384_symbols.py is the +; sole caller at the moment. Without a visible .import, ld65 would omit +; the archive members entirely and the symbols would not appear in +; build/labels.txt. +; +; This stub emits a three-word reference table in CRYPTO_AUX_CODE (UCI +; backend) so each archive member is forced into the link. The table +; itself is tiny (6 bytes) and unreachable at runtime. +; +; Inert when USE_NISTCURVES_P384 is undefined (ip65 backend), so the +; same file can sit unconditionally in CRYPTO_SRCS without affecting +; non-UCI builds. +; ============================================================================= + + .setcpu "6502" + +.ifdef USE_NISTCURVES_P384 + + .import ec_point_double_384 + .import ec_point_add_384 + .import ec_jacobian_to_affine_384 + + .segment "CRYPTO_AUX_CODE" +p384_force_link_refs: + .word ec_point_double_384 + .word ec_point_add_384 + .word ec_jacobian_to_affine_384 + +.endif diff --git a/src/crypto/sha256.s b/src/crypto/sha256.s index b0662f3..6efaba3 100644 --- a/src/crypto/sha256.s +++ b/src/crypto/sha256.s @@ -79,7 +79,7 @@ sha256_k: ; ============================================================================= ; SHA-256 Implementation (code) ; ============================================================================= -.segment "CRYPTO_CODE" +.segment "CRYPTO_AUX_CODE" ; ============================================================================= ; sha256_init - initialize hash state diff --git a/src/crypto/shared/crypto_init.s b/src/crypto/shared/crypto_init.s new file mode 100644 index 0000000..6c4ac3a --- /dev/null +++ b/src/crypto/shared/crypto_init.s @@ -0,0 +1,40 @@ +; ============================================================================= +; crypto_init.s - Top-level crypto init orchestrator +; +; Single entry point called from `src/boot.s` right after the entropy seed +; and before the menu loop. Subsequent phases (C.3) will hang more +; per-library init routines off this file at the insertion marker below. +; +; Phase C.1 (rolled back) attempted to add x25519 sibling REU mul-table +; init here under BACKEND=uci. The integration deadlocked the TLS +; handshake at 48 MHz — see the rollback commit. The in-tree x25519 +; under `src/crypto/x25519.s` provides its own init (`reu_mul_init` +; in `src/boot.s`), driven directly from `boot.s` rather than through +; this orchestrator. +; +; Dispatch order (executed once at boot): +; 1. mul_tables_init (shared 8x8 sqtab) [Phase C.0: stub] +; 2. poly1305_shoup_init (Profile A Shoup r_tab) [Phase C.2] +; 3. ec_precompute_256 (P-256 scalar precompute) [Phase C.3] +; 4. ec_precompute_384 (P-384 scalar precompute) [Phase C.3] +; ============================================================================= + + .include "constants.inc" ; reu_* register equates + + .export crypto_init + .import mul_tables_init + +; ----------------------------------------------------------------------------- +; crypto_init - call each crypto module's init once at boot. +; ----------------------------------------------------------------------------- +.segment "CODE" + +crypto_init: + jsr mul_tables_init + + ; === Phase C.2-.3 insertion point === + ; Add `jsr _init` lines BELOW this marker (one per Phase C + ; agent) in the dispatch order documented at the top of this file. + ; ==================================== + + rts diff --git a/src/crypto/shared/crypto_swap.s b/src/crypto/shared/crypto_swap.s new file mode 100644 index 0000000..9237057 --- /dev/null +++ b/src/crypto/shared/crypto_swap.s @@ -0,0 +1,143 @@ +; ============================================================================= +; crypto_swap.s - Crypto overlay DMA dispatcher +; +; Pages one of two 8 KB overlay images (P-256, P-384) from REU +; bank 2 into the live CRYPTO_OVERLAY region. Call sites prefix each +; overlay-targeting primitive with `jsr crypto_swap_to_`. +; +; Idempotent: re-entering with the same overlay already resident is a +; single-byte compare + rts (no DMA). +; +; Interrupt discipline: SEI around the DMA window; restores original I +; flag on exit. ~8 ms DMA latency at any CPU speed (REU bus runs at +; ~1 MHz regardless of turbo). +; +; Phase C.1 rollback note: the x25519 overlay integration was removed +; after it broke the TLS handshake at 48 MHz UCI. `crypto_swap_to_x25519` +; no longer exists; in-tree x25519 in `src/crypto/x25519.s` is +; always-resident. The remaining swap entry points exist for the +; external P-384 smoke test (tools/test_p384_symbols.py). +; +; `current_overlay`: 1 byte in CRYPTO_BSS (SHADOW_BSS-resident). +; 0 = none (uninitialized / swap_none) +; 2 = p256 +; 3 = p384 +; +; `CRYPTO_OVERLAY_START` is defined by the linker (cfg `MEMORY { }` +; `define = yes` on the CRYPTO_OVERLAY region — see cfg/c64-https-*.cfg). +; ============================================================================= + + .include "constants.inc" ; reu_* register equates + .include "reu_layout.inc" + + .export crypto_swap_to_p256 + .export crypto_swap_to_p384 + .export crypto_swap_none + .export current_overlay + + ; Export REU layout equates once (guarded against multi-include). + .export REU_OVERLAY_P256 + .export REU_OVERLAY_P384 + .export OVERLAY_SIZE + + ; Live overlay slot start address (from the cfg's MEMORY{} define). + .import __CRYPTO_OVERLAY_START__ + +; ----------------------------------------------------------------------------- +; Overlay IDs — must stay in sync with `current_overlay` comments. +; ----------------------------------------------------------------------------- +OV_NONE = 0 +OV_P256 = 2 +OV_P384 = 3 + +; REU command: execute REU->C64 stash (bit 7 = start, bits 1-0 = direction +; 01 = REU-to-C64). Matches the DMA issue used elsewhere in the codebase. +REU_CMD_REU_TO_C64 = $91 + +; ----------------------------------------------------------------------------- +; crypto_swap_to_p256 / _p384 +; ----------------------------------------------------------------------------- +.segment "LOADER_OVERFLOW" + +crypto_swap_to_p256: + lda #OV_P256 + cmp current_overlay + beq swap_done_fast + pha + lda #REU_OVERLAY_P256 + ldy #^REU_OVERLAY_P256 + jsr do_swap + pla + sta current_overlay + rts + +crypto_swap_to_p384: + lda #OV_P384 + cmp current_overlay + beq swap_done_fast + pha + lda #REU_OVERLAY_P384 + ldy #^REU_OVERLAY_P384 + jsr do_swap + pla + sta current_overlay + rts + +crypto_swap_none: + lda #OV_NONE + sta current_overlay + rts + +swap_done_fast: + rts + +; ----------------------------------------------------------------------------- +; do_swap - issue the REU -> C64 DMA of 8 KB into CRYPTO_OVERLAY +; IN: A = REU source low byte +; X = REU source middle byte +; Y = REU source bank byte +; Clobbers A, X, Y. Saves / restores original I flag. +; ----------------------------------------------------------------------------- +do_swap: + ; Save current I flag on the stack (bit 2 of P). + php + sei + + ; REU source: bank + high/low address + sta reu_reu_lo + stx reu_reu_hi + sty reu_reu_bank + + ; C64 target: CRYPTO_OVERLAY_START, 8 KB window + lda #<__CRYPTO_OVERLAY_START__ + sta reu_c64_lo + lda #>__CRYPTO_OVERLAY_START__ + sta reu_c64_hi + + ; 8 KB = $2000 + lda #OVERLAY_SIZE + sta reu_len_hi + + ; Normal autoincrement on both sides. + lda #0 + sta reu_addr_ctrl + + ; Issue REU -> C64 DMA (command $91: bit7=start, 01=REU->C64). + lda #REU_CMD_REU_TO_C64 + sta reu_command + + ; Restore original I flag. + plp + rts + +; ----------------------------------------------------------------------------- +; current_overlay - single-byte state tracking which overlay is resident. +; Lives in SHADOW_BSS-resident CRYPTO_BSS (via BSS segment) so it survives +; across calls without polluting ZP. +; ----------------------------------------------------------------------------- +.segment "BSS" +current_overlay: .res 1 diff --git a/src/crypto/shared/mul_tables.s b/src/crypto/shared/mul_tables.s new file mode 100644 index 0000000..71a444f --- /dev/null +++ b/src/crypto/shared/mul_tables.s @@ -0,0 +1,46 @@ +; ============================================================================= +; mul_tables.s - Shared 8x8 quarter-square multiply tables (STUBBED) +; +; Phase C.0: this file is a stub. `mul_tables_init` returns immediately; +; `sqtab_lo` / `sqtab_hi` labels are *not* defined here yet — the in-tree +; `src/data.s` still owns the definitions (512-byte tables containing the +; full quarter-square formula) and the in-tree init in +; `src/crypto/*_sqtab_init.s` still populates them. +; +; Phase C.1/.2/.3: the first sibling lib to integrate pulls the canonical +; `sqtab_lo` / `sqtab_hi` labels + their init into this file, under +; `.ifdef CANONICAL_SQTAB`. Subsequent lib integrations redirect their +; internal `sqtab_init` to the shared entry point here. +; +; Public API: +; mul_tables_init - build the 256x256 quarter-square tables +; (currently: stub; returns immediately). +; sqtab_lo, sqtab_hi - 256-byte tables, page-aligned, in TABLES_BSS +; (currently: defined by src/data.s; re-homed here +; under CANONICAL_SQTAB in a later phase). +; ============================================================================= + + .export mul_tables_init + +; ----------------------------------------------------------------------------- +; mul_tables_init - stub for Phase C.0. Existing in-tree `sqtab_init` +; (imported by boot.s) still handles table population. A later phase +; redirects boot.s to call `mul_tables_init` instead. +; ----------------------------------------------------------------------------- +.segment "CODE" + +mul_tables_init: + rts + +; ----------------------------------------------------------------------------- +; Table labels — only defined under CANONICAL_SQTAB to avoid duplicating +; the legacy `src/data.s` definitions in Phase C.0. +; ----------------------------------------------------------------------------- +.ifdef CANONICAL_SQTAB + .export sqtab_lo + .export sqtab_hi + +.segment "TABLES_BSS" +sqtab_lo: .res 256 +sqtab_hi: .res 256 +.endif diff --git a/src/crypto/shared/reu_layout.inc b/src/crypto/shared/reu_layout.inc new file mode 100644 index 0000000..fc193ec --- /dev/null +++ b/src/crypto/shared/reu_layout.inc @@ -0,0 +1,60 @@ +; ============================================================================= +; reu_layout.inc - Authoritative REU (Ram Expansion Unit) bank map +; +; The REU is the backing store for all oversize crypto tables plus the +; three swappable crypto overlays (x25519, P-256, P-384). +; +; Bank map (REU 24-bit address): +; $00000-$1FFFF (banks 0-1, 128 KB) x25519 mul tables +; $20000-$2FFFF (bank 2, 64 KB) overlay store (3x 8 KB slots + slack) +; $30000-$3FFFF (bank 3, 64 KB) P-256 precompute +; $40000-$7FFFF (banks 4-7, 256 KB) P-384 precompute + headroom +; +; Each overlay image is 8 KB. The live overlay slot (CRYPTO_OVERLAY +; region, 8 KB on ip65 / 7.75 KB on UCI) is loaded by `crypto_swap_to_*` +; from the overlay store bank. +; +; Downstream libs pick up their bases via `--asm-define REU_BASE_=…` +; or by including this file. +; ============================================================================= + +; --- x25519 mul tables (2 banks, used by in-tree reu_mul_init in boot.s) --- +.ifndef REU_X25519_MUL_TABLES_BASE +REU_X25519_MUL_TABLES_BASE = $00000 +.endif + +; --- overlay storage (single bank; 8 KB slots for P-256 and P-384) --- +; The x25519 overlay slot was removed after the Phase C.1 integration +; was rolled back — in-tree x25519 is always-resident. Offset $0100 +; base kept for the remaining slots to match the external P-384 test +; harness's stash layout. +.ifndef REU_OVERLAY_STORE_BASE +REU_OVERLAY_STORE_BASE = $20100 +.endif +.ifndef REU_OVERLAY_P256 +REU_OVERLAY_P256 = $22100 +.endif +.ifndef REU_OVERLAY_P384 +REU_OVERLAY_P384 = $24100 +.endif + +; --- P-256 precompute (1 bank) --- +.ifndef REU_P256_PRECOMPUTE_BASE +REU_P256_PRECOMPUTE_BASE = $30000 +.endif + +; --- P-384 precompute (4 banks) --- +.ifndef REU_P384_PRECOMPUTE_BASE +REU_P384_PRECOMPUTE_BASE = $40000 +.endif + +; --- overlay slot size (bytes) --- +; Each overlay image occupies exactly this many bytes in the REU store and +; is DMA'd into the live CRYPTO_OVERLAY region at runtime. +.ifndef OVERLAY_SIZE +OVERLAY_SIZE = $2000 ; 8 KB +.endif + +; Note: `.export` of these equates happens once in +; `src/crypto/shared/crypto_swap.s` (guarded by `.ifndef REU_LAYOUT_EXPORTED` +; there) — including this file in multiple TUs does not double-export. diff --git a/src/crypto/shared/zp_canon.inc b/src/crypto/shared/zp_canon.inc new file mode 100644 index 0000000..7a3b69f --- /dev/null +++ b/src/crypto/shared/zp_canon.inc @@ -0,0 +1,160 @@ +; ============================================================================= +; zp_canon.inc - Canonical zero-page map for c64-https and sibling crypto libs +; +; Authoritative copy of the crypto zero-page assignments. Sibling libraries +; (`c64-x25519`, `c64-ChaCha20-Poly1305`, `c64-nist-curves`) wrap every ZP +; equate in `.ifndef`/`.endif` so this file can be the single source of truth +; passed via `ca65 --asm-define =` or via `.include` at build +; time. +; +; Each equate is wrapped in `.ifndef` so downstream users may override +; individual addresses by pre-defining them on the ca65 command line. +; +; Layout mirrors `src/constants.inc:44-107` verbatim. When `constants.inc` +; is pruned in Phase G, this file remains. +; ============================================================================= + +; --- Shared tmp (used by both crypto and general code) --- +.ifndef zp_tmp1 +zp_tmp1 = $02 ; general temp +.endif +.ifndef zp_tmp2 +zp_tmp2 = $03 ; general temp +.endif + +; --- word32 pointers (ChaCha20 / Poly1305 via wireguard) --- +.ifndef w32_src1 +w32_src1 = $04 ; 2 bytes ($04-$05) +.endif +.ifndef w32_src2 +w32_src2 = $06 ; 2 bytes ($06-$07) +.endif +.ifndef w32_dst +w32_dst = $08 ; 2 bytes ($08-$09) +.endif + +; --- SHA-256 accumulators --- +.ifndef sha_temp1 +sha_temp1 = $0a ; 4 bytes ($0A-$0D) +.endif +.ifndef sha_temp2 +sha_temp2 = $0e ; 4 bytes ($0E-$11) +.endif +.ifndef sha256_round +sha256_round = $12 ; 1 byte +.endif + +; --- ChaCha20 state / mult66 pointers (time-shared: fe25519 and ChaCha20 never run simultaneously) --- +.ifndef cc20_round +cc20_round = $14 ; 1 byte +.endif +.ifndef cc20_qr_idx +cc20_qr_idx = $15 ; 1 byte +.endif +.ifndef cc20_data_ptr +cc20_data_ptr = $16 ; 2 bytes ($16-$17) +.endif +.ifndef cc20_remain +cc20_remain = $18 ; low byte of 16-bit ChaCha20/AEAD length +.endif +.ifndef cc20_buf_pos +cc20_buf_pos = $19 ; 1 byte +.endif + +; --- mult66 indirect-indexed multiply pointers (time-shared with ChaCha20) --- +.ifndef lmul0 +lmul0 = $14 ; 2 bytes ($14-$15) — sqtab lookup pointer +.endif +.ifndef lmul1 +lmul1 = $16 ; 2 bytes ($16-$17) — sqtab_hi lookup pointer +.endif + +; --- Poly1305 state --- +.ifndef poly_i +poly_i = $1a ; 1 byte +.endif +.ifndef poly_j +poly_j = $1b ; 1 byte +.endif +.ifndef poly_carry +poly_carry = $1c ; 1 byte +.endif +.ifndef poly_tmp +poly_tmp = $1d ; 1 byte +.endif + +; --- TLS record layer --- +.ifndef tls_rec_ptr +tls_rec_ptr = $1e ; 2 bytes ($1E-$1F) +.endif +.ifndef tls_rec_idx +tls_rec_idx = $20 ; 1 byte +.endif +.ifndef tls_direction +tls_direction = $21 ; 1 byte +.endif + +; --- ECDSA P-256/P-384 bignum arithmetic --- +; Overlaps with x25519 at $39-$3A but the two never run simultaneously. +.ifndef fp_src1 +fp_src1 = $22 ; 2 bytes ($22-$23) +.endif +.ifndef fp_src2 +fp_src2 = $24 ; 2 bytes ($24-$25) +.endif +.ifndef fp_dst +fp_dst = $26 ; 2 bytes ($26-$27) +.endif +.ifndef fp_misc +fp_misc = $28 ; 2 bytes ($28-$29) +.endif +.ifndef fp_carry +fp_carry = $2a ; 1 byte +.endif +.ifndef fp_loop +fp_loop = $2b ; 1 byte +.endif +.ifndef fp_mul_i +fp_mul_i = $39 ; 1 byte (shares with x25_byte_idx) +.endif +.ifndef fp_mul_j +fp_mul_j = $3a ; 1 byte (shares with x25_bit_mask) +.endif +.ifndef ec_scalar_ptr +ec_scalar_ptr = $3b ; 2 bytes ($3B-$3C) +.endif + +; --- fe25519 field arithmetic --- +.ifndef fe_src1 +fe_src1 = $2c ; 2 bytes ($2C-$2D) +.endif +.ifndef fe_src2 +fe_src2 = $2e ; 2 bytes ($2E-$2F) +.endif +.ifndef fe_dst +fe_dst = $30 ; 2 bytes ($30-$31) +.endif +.ifndef fe_carry +fe_carry = $32 ; 1 byte +.endif +.ifndef fe_loop +fe_loop = $33 ; 1 byte +.endif +.ifndef fe_mul_i +fe_mul_i = $34 ; 1 byte +.endif +.ifndef fe_mul_j +fe_mul_j = $35 ; 1 byte +.endif +; $36-$37 reserved (fe25519 uses fe_tmp1..4 as 32-byte data labels) + +; --- x25519 state --- +.ifndef x25_prev_bit +x25_prev_bit = $38 ; 1 byte — previous k_t for swap +.endif +.ifndef x25_byte_idx +x25_byte_idx = $39 ; 1 byte — byte index in scalar +.endif +.ifndef x25_bit_mask +x25_bit_mask = $3a ; 1 byte — current bit mask +.endif diff --git a/src/crypto/shared/zp_define.mk b/src/crypto/shared/zp_define.mk new file mode 100644 index 0000000..2ce14d1 --- /dev/null +++ b/src/crypto/shared/zp_define.mk @@ -0,0 +1,61 @@ +# ============================================================================= +# zp_define.mk - Canonical ca65 --asm-define flag set for the crypto ZP map +# +# Shared by c64-https in-tree builds (Makefile can include this if any source +# assembles sibling-lib .s files directly) and by each sibling lib's +# `libs//build.sh` wrapper (Phase C.1-.3). Keeps the canonical ZP +# addresses DRY — change a ZP location in `zp_canon.inc` and update here +# in lock-step. +# +# Usage (sibling lib build.sh): +# include $(C64_HTTPS)/src/crypto/shared/zp_define.mk +# ca65 $(CRYPTO_ZP_DEFINES) -o foo.o foo.s +# +# The $$ in each flag is literal `$` passed through to the shell that will +# then read `$02` etc. as a hex literal for ca65 (ca65 accepts `$NN` on the +# command line via --asm-define when wrapped correctly by the invoking +# shell). +# ============================================================================= + +CRYPTO_ZP_DEFINES := \ + --asm-define zp_tmp1=\$$02 \ + --asm-define zp_tmp2=\$$03 \ + --asm-define w32_src1=\$$04 \ + --asm-define w32_src2=\$$06 \ + --asm-define w32_dst=\$$08 \ + --asm-define sha_temp1=\$$0a \ + --asm-define sha_temp2=\$$0e \ + --asm-define sha256_round=\$$12 \ + --asm-define cc20_round=\$$14 \ + --asm-define cc20_qr_idx=\$$15 \ + --asm-define cc20_data_ptr=\$$16 \ + --asm-define cc20_remain=\$$18 \ + --asm-define cc20_buf_pos=\$$19 \ + --asm-define lmul0=\$$14 \ + --asm-define lmul1=\$$16 \ + --asm-define poly_i=\$$1a \ + --asm-define poly_j=\$$1b \ + --asm-define poly_carry=\$$1c \ + --asm-define poly_tmp=\$$1d \ + --asm-define tls_rec_ptr=\$$1e \ + --asm-define tls_rec_idx=\$$20 \ + --asm-define tls_direction=\$$21 \ + --asm-define fp_src1=\$$22 \ + --asm-define fp_src2=\$$24 \ + --asm-define fp_dst=\$$26 \ + --asm-define fp_misc=\$$28 \ + --asm-define fp_carry=\$$2a \ + --asm-define fp_loop=\$$2b \ + --asm-define fp_mul_i=\$$39 \ + --asm-define fp_mul_j=\$$3a \ + --asm-define ec_scalar_ptr=\$$3b \ + --asm-define fe_src1=\$$2c \ + --asm-define fe_src2=\$$2e \ + --asm-define fe_dst=\$$30 \ + --asm-define fe_carry=\$$32 \ + --asm-define fe_loop=\$$33 \ + --asm-define fe_mul_i=\$$34 \ + --asm-define fe_mul_j=\$$35 \ + --asm-define x25_prev_bit=\$$38 \ + --asm-define x25_byte_idx=\$$39 \ + --asm-define x25_bit_mask=\$$3a diff --git a/src/data.s b/src/data.s index 6fe2d40..223f7f1 100644 --- a/src/data.s +++ b/src/data.s @@ -18,8 +18,6 @@ .export sqtab2_lo .export sqtab2_hi -.export mul38_lo_tab -.export mul38_hi_tab ; --- mult66 second quarter-square table --- sqtab2_lo: @@ -35,6 +33,8 @@ sqtab2_hi: .endrepeat ; --- mul_by_38 lookup tables --- +.export mul38_lo_tab +.export mul38_hi_tab mul38_lo_tab: .byte 0 .repeat 255, I @@ -496,7 +496,11 @@ x25_e: .res 32 .export mul_cached_a .export mul_src2_buf mul_cached_a: .res 1 ; cached src1[i] for inlined multiply -mul_src2_buf: .res 32 ; absolute copy of src2 for fast indexed access +mul_src2_buf: .res 35 ; absolute copy of src2 for fast indexed access + ; (32 bytes + 3 pad zeros so the sibling + ; c64-nist-curves fp256 4x-unrolled mul can + ; over-read past j=31 into zeros for its + ; fast-skip fast path — Phase C.4) ; ----------------------------------------------------------------------------- ; ECDSA signature verification @@ -509,30 +513,28 @@ mul_src2_buf: .res 32 ; absolute copy of src2 for fast indexed access .export ecdsa_sig_s .export ecdsa_pubkey_x .export ecdsa_pubkey_y -.export ecdsa_verify_tmp ecdsa_curve_id: .res 1 ; 0=P-256, 1=P-384 -ecdsa_hash: .res 48 ; message hash (32 for P-256, 48 for P-384) -ecdsa_sig_r: .res 48 ; signature r component -ecdsa_sig_s: .res 48 ; signature s component -ecdsa_pubkey_x: .res 48 ; public key Q.x -ecdsa_pubkey_y: .res 48 ; public key Q.y -ecdsa_verify_tmp: .res 48 ; temporary for w - -; --- P-256 working buffers --- -.export ev_u1 -.export ev_u2 -.export ev_point_save -ev_u1: .res 32 ; u1 = z * w mod n -ev_u2: .res 32 ; u2 = r * w mod n -ev_point_save: .res 96 ; saved Jacobian point (u1*G) - -; --- P-384 working buffers --- -.export ev_u1_384 -.export ev_u2_384 -.export ev_point_save_384 -ev_u1_384: .res 48 ; u1 = z * w mod n (P-384) -ev_u2_384: .res 48 ; u2 = r * w mod n (P-384) -ev_point_save_384: .res 144 ; saved Jacobian point (u1*G, P-384) + +; Phase C.4: for P-256, these five 32-byte BE buffers are laid out +; contiguously in memory (r|s|h|Qx|Qy) so the dispatcher can hand +; `ecdsa_sig_r` directly to the sibling's ecdsa_verify_256 as a 160-byte +; packed input struct without an explicit pack step. Reshrunk from 48 B +; to 32 B each; only P-256 is wired right now (P-384 is stubbed — when it +; is re-integrated via a second sibling archive it will own its own +; 240-byte BE input struct, likely reusing this layout scaled to 48 B). +ecdsa_sig_r: .res 32 ; signature r component (BE, struct +0) +ecdsa_sig_s: .res 32 ; signature s component (BE, struct +32) +ecdsa_hash: .res 32 ; message hash (BE, struct +64) +ecdsa_pubkey_x: .res 32 ; public key Q.x (BE, struct +96) +ecdsa_pubkey_y: .res 32 ; public key Q.y (BE, struct +128) + +; --- Legacy in-tree ECDSA scratch (ecdsa_verify_tmp, ev_u1, ev_u2, +; ev_point_save, ev_u1_384, ev_u2_384, ev_point_save_384) was +; reclaimed in Phase C.4. The sibling c64-nist-curves +; `ecdsa_verify_256` now owns its own scratch inside the archive's +; BSS (ecdsa_w/u1/u2/u1_be/u2_be/u1g_x/u1g_y, fp_rev_buf), so the +; legacy ev_* labels are unused. Phase G can reclaim the matching +; P-384 slots when ecdsa_verify_384 is re-integrated the same way. ; --- DER parsing temporaries --- .export ev_der_int_len diff --git a/src/exports.s b/src/exports.s index 3e3e3c2..93dcc75 100644 --- a/src/exports.s +++ b/src/exports.s @@ -16,3 +16,25 @@ .include "constants.inc" .export tcp_recv_buf + +; Promote the fe25519 ZP equates so tools/test_x25519.py can resolve +; them via labels.txt. +.export fe_src1 +.export fe_src2 +.export fe_dst + +; Phase F fallout: tools/test_crypto.py resolves these ZP equates via +; labels.txt and failed under both backends because the equates were +; never linker-visible. Promote them here — they have stable addresses +; across in-tree and canonical-ZP layouts. +.export cc20_data_ptr +.export cc20_remain +.export zp_ptr + +; Phase C.4: c64-nist-curves fp256.s references a handful of REU DMA +; registers via `.import` (it was written to live in a linker-visible +; symbol world). Promote the numeric equates from constants.inc so ld65 +; can resolve the sibling's imports. +.export reu_reu_hi +.export reu_reu_bank +.export reu_command diff --git a/tools/integration/_merge_allow.py b/tools/integration/_merge_allow.py new file mode 100755 index 0000000..7826594 --- /dev/null +++ b/tools/integration/_merge_allow.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Merge allow/deny patterns into a .claude/settings.local.json file. + +Usage: + _merge_allow.py --allow PATTERN [...] [--deny PATTERN [...]] + +Creates the file with a minimal skeleton if missing. Merges new patterns +into permissions.allow / permissions.deny, dedupes against existing entries, +preserves the order of existing entries, and appends new entries at the end. +Preserves any other top-level keys and any other keys under permissions. +Writes back with 2-space indent and a trailing newline. +""" +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + + +def merge(existing: list, new: list) -> tuple[list, list]: + """Return (merged_list, added_patterns). Preserves existing order.""" + seen = set(existing) + added: list = [] + out = list(existing) + for p in new: + if p not in seen: + out.append(p) + added.append(p) + seen.add(p) + return out, added + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("path", type=Path) + ap.add_argument("--allow", nargs="*", default=[]) + ap.add_argument("--deny", nargs="*", default=[]) + args = ap.parse_args() + + if args.path.exists(): + with args.path.open() as f: + data = json.load(f) + else: + data = {"permissions": {"allow": [], "deny": []}} + + perms = data.setdefault("permissions", {}) + cur_allow = perms.get("allow", []) + cur_deny = perms.get("deny", []) + + new_allow, added_allow = merge(cur_allow, args.allow) + new_deny, added_deny = merge(cur_deny, args.deny) + + perms["allow"] = new_allow + if args.deny or cur_deny: + perms["deny"] = new_deny + + args.path.parent.mkdir(parents=True, exist_ok=True) + with args.path.open("w") as f: + json.dump(data, f, indent=2) + f.write("\n") + + # Report what was added for transparency. + print(f"[{args.path}]") + print(f" allow: +{len(added_allow)} new / {len(new_allow)} total") + for p in added_allow: + print(f" + {p}") + print(f" deny: +{len(added_deny)} new / {len(new_deny)} total") + for p in added_deny: + print(f" + {p}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/integration/allowlist_preseed.sh b/tools/integration/allowlist_preseed.sh new file mode 100755 index 0000000..fa6ae27 --- /dev/null +++ b/tools/integration/allowlist_preseed.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Pre-seed the three sibling repos' .claude/settings.local.json files with +# the Bash/permission patterns downstream integration agents need, so cross- +# repo dispatches do not stall on approval prompts. +# +# Merges into permissions.allow (dedupe against existing; preserve order; +# append new entries at the end) and ensures a core set of deny patterns. +# Creates the file if missing. Never touches c64-https's own settings. +# +# Invokes tools/integration/_merge_allow.py. +set -euo pipefail + +HERE="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +MERGER="$HERE/_merge_allow.py" + +SIBLINGS=( + "/home/someone/c64-x25519" + "/home/someone/c64-ChaCha20-Poly1305" + "/home/someone/c64-nist-curves" +) + +ALLOW=( + "Bash(python3 *)" + "Bash(make *)" + "Bash(make)" + "Bash(make clean)" + "Bash(make run)" + "Bash(ca65 *)" + "Bash(ld65 *)" + "Bash(ar65 *)" + "Bash(x64sc *)" + "Bash(timeout *)" + "Bash(git submodule *)" + "Bash(git worktree *)" + "Bash(git add *)" + "Bash(git commit *)" + "Bash(git checkout *)" + "Bash(bash *.sh)" + "Bash(bash tools/integration/*.sh *)" +) + +DENY=( + "Bash(rm -rf *)" + "Bash(git push --force *)" + "Bash(killall *)" +) + +for repo in "${SIBLINGS[@]}"; do + if [[ ! -d "$repo" ]]; then + echo "FATAL: sibling repo missing: $repo" >&2 + exit 1 + fi + mkdir -p "$repo/.claude" + settings="$repo/.claude/settings.local.json" + python3 "$MERGER" "$settings" \ + --allow "${ALLOW[@]}" \ + --deny "${DENY[@]}" +done diff --git a/tools/integration/build_chacha20poly1305.sh b/tools/integration/build_chacha20poly1305.sh new file mode 100755 index 0000000..6d91036 --- /dev/null +++ b/tools/integration/build_chacha20poly1305.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Build libs/chacha20poly1305 as build/lib/chacha20poly1305.a (Profile A). +# Phase C will implement this. +set -euo pipefail +echo "Phase C will implement this" +exit 0 diff --git a/tools/integration/build_nistcurves.sh b/tools/integration/build_nistcurves.sh new file mode 100755 index 0000000..2df1f7b --- /dev/null +++ b/tools/integration/build_nistcurves.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Build libs/nistcurves as build/lib/nistcurves.a (P-256 + P-384). +# Phase C will implement this. +set -euo pipefail +echo "Phase C will implement this" +exit 0 diff --git a/tools/integration/build_nistcurves_p256.sh b/tools/integration/build_nistcurves_p256.sh new file mode 100755 index 0000000..d0e2e73 --- /dev/null +++ b/tools/integration/build_nistcurves_p256.sh @@ -0,0 +1,355 @@ +#!/usr/bin/env bash +# ============================================================================= +# tools/integration/build_nistcurves_p256.sh - Build c64-nist-curves P-256 +# ECDSA verify primitives as a resident .a archive linked into the main PRG. +# +# Phase C.4 of the sibling-lib integration. Produces build/lib/nistcurves-p256.a +# containing the P-256 field arithmetic, modular arithmetic, variable-base +# scalar multiply, Jacobian->affine conversion, and packaged ECDSA verify +# (ecdsa_verify_256). No overlay mechanism; all code always-resident. +# +# Excluded (to fit the budget + avoid REU precompute): +# - ec_scalar_mul - Lim-Lee fixed-base comb. Needs a 16 KB REU bank-2 +# precompute table built by ec_precompute_256 at boot. +# Replaced by a shim in src/crypto/ecdsa_verify.s that +# copies G into ec_base_x/y and tail-calls +# ec_scalar_mul_var. The dispatcher is the ONLY caller +# of ecdsa_verify_256, so the shim covers the sole +# in-PRG use of ec_scalar_mul. +# - ec_precompute_256 - builds the Lim-Lee anchor table into REU bank 2. +# Only useful with ec_scalar_mul. +# - Lim-Lee anchor tables (ec_anchor1_x..ec_anchor8_y, cm_k, ec_aff2g_256_*) +# and all sm256_reu_* REU DMA helpers that service them. +# - All P-384 data/arith (fp384_*, ec384_*, ecdsa384_*, cm_k_384, anchors). +# Lives in nistcurves-p384.a under the separate Phase C.3b smoke test. +# - Shared mul infrastructure (mul_cached_a, mul_src2_buf, mul_dma_lo/hi, +# mul_8x8, sqtab_init, sqtab_lo/hi, poly_prod_lo/hi, reu_fetch_mul_row) - +# the in-tree src/data.s + src/crypto/poly1305.s + src/boot.s already +# provide these and they are shared across fe25519 + P-256 via the REU +# DMA row-fetch pipeline. Adding the sibling's copies would collide. +# - ecdsa_inputs_256, ecdsa_result_256 test-driver scratch - only used by +# the nist-curves PRG's own test harness. +# +# The script stages the sibling's .s files in build/lib/nistcurves_p256_staging/, +# applies sed patches to strip Lim-Lee bodies + provide a minimal P-256-only +# data.s, and assembles with canonical ZP equates passed via -D. +# +# Usage (from top-level Makefile): +# bash tools/integration/build_nistcurves_p256.sh +# Produces: +# build/lib/nistcurves-p256.a +# build/lib/nistcurves-p256.sizes.txt (per-source byte counts) +# ============================================================================= +set -eo pipefail + +# --- Paths --- +PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +LIB_SRC="$PROJECT_ROOT/libs/nistcurves/src" +STAGING="$PROJECT_ROOT/build/lib/nistcurves_p256_staging" +OUT_DIR="$PROJECT_ROOT/build/lib" +ARCHIVE="$OUT_DIR/nistcurves-p256.a" +SIZES="$OUT_DIR/nistcurves-p256.sizes.txt" + +CA65="${CA65:-ca65}" +AR65="${AR65:-ar65}" + +# --- Canonical ZP defines --- +# Mirrors the P-384 build's -D flag set. zp_ptr2 is relocated into +# $3D-$3E (inside ZP_CRYPTO, otherwise unused) because the sibling's +# default ($fd-$fe) overlaps with c64-https's zp_temp/zp_count used +# by der_decode.s during cert parsing. ecdsa_verify_256 runs AFTER +# DER parsing completes, so the clobber would be fine in practice, +# but the relocation keeps the lifetime isolation explicit. +ZP_DEFINES=( + '-Dproc_port=$01' + '-Dzp_tmp1=$02' + '-Dzp_tmp2=$03' + '-Dzp_ptr1=$fb' + '-Dzp_ptr2=$3d' + '-Dfp_src1=$22' + '-Dfp_src2=$24' + '-Dfp_dst=$26' + '-Dfp_misc=$28' + '-Dfp_carry=$2a' + '-Dfp_loop=$2b' + '-Dfp_mul_i=$39' + '-Dfp_mul_j=$3a' + '-Dec_scalar_ptr=$3b' + '-Dpoly_i=$1a' + '-Dpoly_j=$1b' + '-Dpoly_carry=$1c' + '-Dpoly_tmp=$1d' +) + +# --- Stage sources --- +rm -rf "$STAGING" +mkdir -p "$STAGING" + +cp "$LIB_SRC"/constants.s "$STAGING/" +cp "$LIB_SRC"/zp_config.s "$STAGING/" +cp "$LIB_SRC"/fp256.s "$STAGING/fp256_raw.s" +cp "$LIB_SRC"/mod256.s "$STAGING/mod256_raw.s" +cp "$LIB_SRC"/points256.s "$STAGING/points256_raw.s" +cp "$LIB_SRC"/ecdsa256.s "$STAGING/ecdsa256_raw.s" + +# --- Strip points256.s of the Lim-Lee / REU precompute bodies --- +# Lines 762-1458 in the upstream file cover: +# - sm256_reu_stash_affine / sm256_reu_fetch_affine / sm256_calc_offset_64 +# / sm256_reu_restore (REU DMA helpers for bank-2 anchor table) +# - ec_precompute_256 and its internal helpers (load_G_jac, successive-double +# helpers, anchor accumulate) +# - ec_scalar_mul (Lim-Lee 8-way fixed-base comb) and its anchor-loader +# helpers + anchor base-address table + cm_* / sm256_* state vars +# Keeps ec_point_double (line 60-410), ec_point_add (411-761), +# ec_scalar_mul_var (1459-1609), ec_jacobian_to_affine (1610-end). +sed -i '762,1458d' "$STAGING/points256_raw.s" + +# Strip exports + imports that only the removed bodies used. +sed -i '/^\.export ec_precompute_256, ec_scalar_mul, ec_scalar_mul_var$/c\ +.export ec_scalar_mul_var' "$STAGING/points256_raw.s" +# Anchor + Lim-Lee state imports +sed -i '/^\.import ec_aff2g_256_x, ec_aff2g_256_y$/d' "$STAGING/points256_raw.s" +sed -i '/^\.import ec_anchor[1-8]_x, ec_anchor[1-8]_x, ec_anchor[1-8]_x, ec_anchor[1-8]_x$/d' "$STAGING/points256_raw.s" +sed -i '/^\.import ec_anchor[1-8]_y, ec_anchor[1-8]_y, ec_anchor[1-8]_y, ec_anchor[1-8]_y$/d' "$STAGING/points256_raw.s" +sed -i '/^\.import ec_anchor.*$/d' "$STAGING/points256_raw.s" +sed -i '/^\.import cm_k, mul_dma_lo$/d' "$STAGING/points256_raw.s" +sed -i '/^\.import ec_sc_byte, ec_sc_mask$/d' "$STAGING/points256_raw.s" +# REU DMA register imports (only used by stripped REU anchor helpers) +sed -i '/^\.import reu_c64_lo, reu_c64_hi, reu_reu_lo, reu_reu_hi$/d' "$STAGING/points256_raw.s" +sed -i '/^\.import reu_reu_bank, reu_len_lo, reu_len_hi$/d' "$STAGING/points256_raw.s" +sed -i '/^\.import reu_addr_ctrl, reu_command$/d' "$STAGING/points256_raw.s" +# ec_mulp / ec_sqrp are used by all three retained bodies - keep. +# fp_tmp1 is used by ec_scalar_mul_var - keep. + +# Sanity: no leftover non-comment references to stripped symbols. +# Filter out comment lines (first non-blank char is `;`) before checking. +if grep -v '^\s*;' "$STAGING/points256_raw.s" \ + | grep -qE '\bec_anchor[0-9]+_|\bcm_k\b|\bec_aff2g_256|\bec_sc_byte\b|\bec_sc_mask\b|\bsm256_reu|\bec_scalar_mul\b[^_]'; then + echo "ERROR: stripped points256 still references removed-body symbols" >&2 + grep -v '^\s*;' "$STAGING/points256_raw.s" \ + | grep -nE '\bec_anchor[0-9]+_|\bcm_k\b|\bec_aff2g_256|\bec_sc_byte\b|\bec_sc_mask\b|\bsm256_reu|\bec_scalar_mul\b[^_]' \ + | head -5 >&2 + exit 1 +fi + +# --- Strip curve256.s to ec_a256, ec_b256, ec_gx256, ec_gy256 only --- +# The test vector constants (ecdsa_test_*) are used only by the sibling's +# own test PRG and would add ~256 B of dead rodata here. +cat > "$STAGING/curve256_raw.s" <<'CURVE_EOF' +.setcpu "6502" + +; ============================================================================= +; curve256_raw.s - P-256 curve parameters for c64-https Phase C.4. +; Hand-trimmed from libs/nistcurves/src/curve256.s: test vectors dropped +; (only used by the sibling's standalone test harness). +; ============================================================================= + +.segment "RODATA" + +.export ec_a256, ec_b256, ec_gx256, ec_gy256 + +; Coefficient a = p - 3 +ec_a256: + .byte $FC, $FF, $FF, $FF, $FF, $FF, $FF, $FF + .byte $FF, $FF, $FF, $FF, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $01, $00, $00, $00, $FF, $FF, $FF, $FF + +; Coefficient b +ec_b256: + .byte $4B, $60, $D2, $27, $3E, $3C, $CE, $3B + .byte $F6, $B0, $53, $CC, $B0, $06, $1D, $65 + .byte $BC, $86, $98, $76, $55, $BD, $EB, $B3 + .byte $E7, $93, $3A, $AA, $D8, $35, $C6, $5A + +; Generator x coordinate (LE) +ec_gx256: + .byte $96, $C2, $98, $D8, $45, $39, $A1, $F4 + .byte $A0, $33, $EB, $2D, $81, $7D, $03, $77 + .byte $F2, $40, $A4, $63, $E5, $E6, $BC, $F8 + .byte $47, $42, $2C, $E1, $F2, $D1, $17, $6B + +; Generator y coordinate (LE) +ec_gy256: + .byte $F5, $51, $BF, $37, $68, $40, $B6, $CB + .byte $CE, $5E, $31, $6B, $57, $33, $CE, $2B + .byte $16, $9E, $0F, $7C, $4A, $EB, $E7, $8E + .byte $9B, $7F, $1A, $FE, $E2, $42, $E3, $4F +CURVE_EOF + +# --- Emit minimal data_p256_raw.s --- +# Keeps only the RW buffers that fp256 / mod256 / points256 (post-strip) / +# ecdsa256 reference. Shared mul infrastructure (mul_cached_a, mul_src2_buf, +# mul_dma_lo, mul_dma_hi) is provided by in-tree src/data.s. P-384 data and +# Lim-Lee anchors are excluded. +cat > "$STAGING/data_p256_raw.s" <<'DATA_EOF' +.setcpu "6502" + +; ============================================================================= +; data_p256_raw.s - Minimal P-256 RW buffers for c64-https / c64-nist-curves +; integration (Phase C.4). Hand-extracted from the sibling's +; data.s so in-tree shared mul buffers remain unclobbered and +; P-384 / Lim-Lee state is omitted. +; +; All exports here are P-256-exclusive. +; ============================================================================= + +.segment "DATA" + +; --- P-256 field arithmetic working buffers (32 bytes each) --- +; fp_tmp2/3/4 and fp_r1/2/3 are declared by the sibling's full data.s +; but never .importe'd from the retained fp256/mod256/points256/ecdsa256 +; bodies; pruned here to save BSS (~192 B). +.export fp_wide +fp_wide: .res 64, 0 ; 512-bit product from multiply +.export fp_tmp1 +fp_tmp1: .res 32, 0 + +; --- P-256 result registers (only fp_r0 referenced) --- +.export fp_r0 +fp_r0: .res 32, 0 + +; --- P-256 modular inverse working space --- +.export fp_inv_u +fp_inv_u: .res 32, 0 +.export fp_inv_v +fp_inv_v: .res 32, 0 +.export fp_inv_x1 +fp_inv_x1: .res 32, 0 +.export fp_inv_x2 +fp_inv_x2: .res 32, 0 + +; --- P-256 point storage (Jacobian: X=32 + Y=32 + Z=32 = 96 bytes) --- +.export ec_p1 +ec_p1: .res 96, 0 +.export ec_p2 +ec_p2: .res 96, 0 +.export ec_p3 +ec_p3: .res 96, 0 + +; --- P-256 point math temporaries --- +.export ec_t1 +ec_t1: .res 32, 0 +.export ec_t2 +ec_t2: .res 32, 0 +.export ec_t3 +ec_t3: .res 32, 0 +.export ec_t4 +ec_t4: .res 32, 0 +.export ec_t5 +ec_t5: .res 32, 0 +.export ec_t6 +ec_t6: .res 32, 0 + +; --- P-256 affine output --- +.export ec_affine_x +ec_affine_x: .res 32, 0 +.export ec_affine_y +ec_affine_y: .res 32, 0 + +; --- Variable-base scalar-mul input (affine, 32 bytes each, LE). --- +.export ec_base_x +ec_base_x: .res 32, 0 +.export ec_base_y +ec_base_y: .res 32, 0 + +; --- Solinas reduction scratch (33 bytes: 32 + carry) --- +.export fp_red_tmp +fp_red_tmp: .res 33, 0 + +; --- ECDSA verify scratch (P-256). All 32-byte little-endian unless noted. --- +.export ecdsa_r +ecdsa_r: .res 32, 0 ; LE r (byte-reversed from BE input) +.export ecdsa_s +ecdsa_s: .res 32, 0 ; LE s +.export ecdsa_h +ecdsa_h: .res 32, 0 ; LE message hash +.export ecdsa_qx +ecdsa_qx: .res 32, 0 ; LE public-key affine X +.export ecdsa_qy +ecdsa_qy: .res 32, 0 ; LE public-key affine Y +.export ecdsa_w +ecdsa_w: .res 32, 0 ; LE w = s^-1 mod n +.export ecdsa_u1 +ecdsa_u1: .res 32, 0 ; LE u1 = h*w mod n +.export ecdsa_u2 +ecdsa_u2: .res 32, 0 ; LE u2 = r*w mod n +.export ecdsa_u1_be +ecdsa_u1_be: .res 32, 0 ; BE u1 (scalar_mul input) +.export ecdsa_u2_be +ecdsa_u2_be: .res 32, 0 ; BE u2 (scalar_mul_var input) +.export ecdsa_u1g_x +ecdsa_u1g_x: .res 32, 0 ; LE affine X of u1*G +.export ecdsa_u1g_y +ecdsa_u1g_y: .res 32, 0 ; LE affine Y of u1*G + +; --- fp_reverse32 staging buffer (one 32-byte scratch). --- +.export fp_rev_buf +fp_rev_buf: .res 32, 0 +DATA_EOF + +# --- Route CODE segments in the raw .s files to CRYPTO_CODE. --- +# The sibling uses `.segment "CODE"`, which under c64-https's cfg is the +# LOADER region ($0801-$1FFF). We want this code in CRYPTO_RESIDENT. +for src in fp256_raw mod256_raw points256_raw ecdsa256_raw; do + sed -i 's/^\.segment "CODE"/.segment "CRYPTO_CODE"/' "$STAGING/$src.s" +done + +# --- Route DATA segment in data_p256_raw.s to CRYPTO_BSS. --- +# The c64-https cfg has no "DATA" segment slot; our minimal data file +# only contains `.res` (zero-init) declarations, so CRYPTO_BSS is the +# right home. Don't accidentally match anything inside a string or +# comment: the data_p256_raw.s we emit has exactly one such directive. +sed -i 's/^\.segment "DATA"$/.segment "CRYPTO_BSS"/' "$STAGING/data_p256_raw.s" + +# Sanity: no leftover `.segment "CODE"` hunks outside the expected +# pattern (the raw files should only have one CODE segment each). +for src in fp256_raw mod256_raw points256_raw ecdsa256_raw; do + if grep -qE '^\.segment "CODE"$' "$STAGING/$src.s"; then + echo "ERROR: leftover .segment \"CODE\" in $src.s" >&2 + exit 1 + fi +done + +# --- Assemble each staged .s file --- +OBJ_DIR="$STAGING/obj" +rm -rf "$OBJ_DIR" +mkdir -p "$OBJ_DIR" "$OUT_DIR" + +# zp_config.s is the single point of truth for ZP equates; we apply -D +# overrides so sibling defaults get replaced with c64-https's canonical map. +"$CA65" \ + -I "$STAGING" \ + -I "$PROJECT_ROOT/src/crypto/shared" \ + "${ZP_DEFINES[@]}" \ + -o "$OBJ_DIR/zp_config.o" "$STAGING/zp_config.s" + +for src in fp256_raw mod256_raw points256_raw ecdsa256_raw curve256_raw data_p256_raw; do + "$CA65" \ + -I "$STAGING" \ + -I "$PROJECT_ROOT/src/crypto/shared" \ + -o "$OBJ_DIR/$src.o" "$STAGING/$src.s" +done + +# --- Archive --- +rm -f "$ARCHIVE" +"$AR65" a "$ARCHIVE" \ + "$OBJ_DIR/zp_config.o" \ + "$OBJ_DIR/fp256_raw.o" \ + "$OBJ_DIR/mod256_raw.o" \ + "$OBJ_DIR/points256_raw.o" \ + "$OBJ_DIR/ecdsa256_raw.o" \ + "$OBJ_DIR/curve256_raw.o" \ + "$OBJ_DIR/data_p256_raw.o" + +# --- Per-source byte counts --- +{ + echo "# nistcurves-p256.a per-source byte counts (ca65 .o file sizes)" + for src in zp_config fp256_raw mod256_raw points256_raw ecdsa256_raw curve256_raw data_p256_raw; do + bytes=$(wc -c < "$OBJ_DIR/$src.o") + printf '%-24s %d bytes (.o)\n' "$src" "$bytes" + done +} > "$SIZES" + +echo "built $ARCHIVE" +cat "$SIZES" diff --git a/tools/integration/build_nistcurves_p384.sh b/tools/integration/build_nistcurves_p384.sh new file mode 100755 index 0000000..2caf4d9 --- /dev/null +++ b/tools/integration/build_nistcurves_p384.sh @@ -0,0 +1,297 @@ +#!/usr/bin/env bash +# ============================================================================= +# tools/integration/build_nistcurves_p384.sh - Build c64-nist-curves P-384 +# primitives as a REU overlay .a archive for the UCI backend. +# +# Phase C.3 of the sibling-lib integration. Produces build/lib/nistcurves-p384.a +# containing ONLY the three variable-base P-384 primitives used by TLS +# (ec_point_double_384, ec_point_add_384, ec_jacobian_to_affine_384) +# plus their fp/mod helpers. +# +# Segment layout: +# OVERLAY_P384 - all P-384 runtime code (fp384 + mod384 + points384). +# Paged into the live CRYPTO_OVERLAY slot via REU DMA. +# CRYPTO_RESIDENT - P-384 RW data (ec384_* points, fp384_* tmps, etc.) +# routed through the DATA / BSS segments. +# +# Excluded (upstream JC-000/c64-nist-curves#17 tracks what's missing): +# - ec_scalar_mul_384 - fixed-base-only (Lim-Lee comb over precomputed +# anchors). Not useful without variable-base mul. +# - ec_precompute_384 - builds the Lim-Lee comb table; needs REU bank 2 +# layout that conflicts with the overlay store. +# - Lim-Lee anchor tables (ec_anchor1_384_x..ec_anchor8_384_y) and +# comb-scalar state (cm_k_384, ec384_sc_byte/mask, ec384_precomp_i). +# - P-256 modules (fp256/mod256/curve256/points256/inv256). P-256 stays +# in-tree (see src/crypto/ecdsa_*.s); Phase C.3 does not swap it out. +# - curve384.s (ec_a384/b384/gx384/gy384constants). Only imported by the +# stripped ec_precompute_384 / ec_scalar_mul_384. +# +# The mul_8x8 runtime + mul_dma_lo/hi tables + reu_fetch_mul_row come +# from the already-linked c64-x25519 sibling archive (build/lib/x25519.a). +# P-384's fp_mul_384 / fp_sqr_384 reuse those REU-backed product tables; +# the table layout (a*512 offset, 256 lo + 256 hi bytes per row) matches +# between the two siblings. +# +# The script stages the sibling's .s files in build/lib/nistcurves_p384_staging/, +# applies a sed-patch to each to override their `.segment "CODE"` / "DATA" +# directives, and assembles with canonical ZP equates passed via -D. +# +# Usage (from top-level Makefile): +# bash tools/integration/build_nistcurves_p384.sh +# Produces: +# build/lib/nistcurves-p384.a +# build/lib/nistcurves-p384.sizes.txt (per-source byte counts) +# ============================================================================= +set -eo pipefail + +# --- Paths --- +PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +LIB_SRC="$PROJECT_ROOT/libs/nistcurves/src" +STAGING="$PROJECT_ROOT/build/lib/nistcurves_p384_staging" +OUT_DIR="$PROJECT_ROOT/build/lib" +ARCHIVE="$OUT_DIR/nistcurves-p384.a" +SIZES="$OUT_DIR/nistcurves-p384.sizes.txt" + +CA65="${CA65:-ca65}" +AR65="${AR65:-ar65}" + +# --- Canonical ZP defines --- +# The sibling's zp_config.s wraps every ZP equate in .ifndef, so command-line +# -D values win over the defaults. We pin the sibling to c64-https's +# canonical ZP map (src/crypto/shared/zp_canon.inc) so the archive's +# absolute ZP references line up with TLS call-site expectations. +# +# Note: fp_mul_i / fp_mul_j overlap with x25_byte_idx / x25_bit_mask at +# $39/$3a. This is fine because x25519 and P-384 run at different times +# (different overlays; only one resident at a time) and the canonical +# map documents the time-sharing. +ZP_DEFINES=( + '-Dproc_port=$01' + '-Dzp_tmp1=$02' + '-Dzp_tmp2=$03' + '-Dzp_ptr1=$fb' + '-Dzp_ptr2=$fd' + '-Dfp_src1=$22' + '-Dfp_src2=$24' + '-Dfp_dst=$26' + '-Dfp_misc=$28' + '-Dfp_carry=$2a' + '-Dfp_loop=$2b' + '-Dfp_mul_i=$39' + '-Dfp_mul_j=$3a' + '-Dec_scalar_ptr=$3b' + '-Dpoly_i=$1a' + '-Dpoly_j=$1b' + '-Dpoly_carry=$1c' + '-Dpoly_tmp=$1d' +) + +# --- Stage sources --- +rm -rf "$STAGING" +mkdir -p "$STAGING" + +# The sibling's constants.s is pulled in via -I; we don't stage it here +# (it has no segment directives we'd rewrite, and it's .include'd by +# zp_config.s / data.s transitively). +cp "$LIB_SRC"/constants.s "$STAGING/" +cp "$LIB_SRC"/zp_config.s "$STAGING/" +cp "$LIB_SRC"/fp384.s "$STAGING/fp384_raw.s" +cp "$LIB_SRC"/mod384.s "$STAGING/mod384_raw.s" +cp "$LIB_SRC"/points384.s "$STAGING/points384_raw.s" +cp "$LIB_SRC"/data.s "$STAGING/data_raw.s" + +# --- Strip points384.s of ec_precompute_384 and ec_scalar_mul_384 --- +# Those live between lines 787 (just before ec_precompute_384:) and +# 1489 (just before the ec_jacobian_to_affine_384: header). +# We also strip the `.export ec_precompute_384, ec_scalar_mul_384` line +# so the archive doesn't advertise symbols whose bodies were removed. +# The remaining three `.export` symbols (ec_point_double_384, +# ec_point_add_384, ec_jacobian_to_affine_384) stay. +# +# Imports that the removed bodies relied on (anchors, cm_k_384, sc_byte, +# sc_mask, precomp_i, ec_gx384, ec_gy384, ec_set_modp_384... wait ec_set_modp +# is still used by double/add) — we remove ONLY the anchor + comb-state +# imports since everything else is used by the retained primitives. +sed -i '787,1489d' "$STAGING/points384_raw.s" +sed -i '/^\.export ec_precompute_384, ec_scalar_mul_384$/d' "$STAGING/points384_raw.s" +# Strip imports only used by the removed bodies. Patterns are anchored +# to avoid accidentally deleting unrelated lines. +sed -i '/^\.import ec_gx384, ec_gy384$/d' "$STAGING/points384_raw.s" +sed -i '/^\.import ec_anchor[1-8]_384_x/d' "$STAGING/points384_raw.s" +sed -i '/^\.import ec_anchor[1-8]_384_y/d' "$STAGING/points384_raw.s" +sed -i '/^\.import cm_k_384, mul_dma_lo$/d' "$STAGING/points384_raw.s" +sed -i '/^\.import ec384_sc_byte, ec384_sc_mask, ec384_precomp_i$/d' "$STAGING/points384_raw.s" + +# --- Strip data_raw.s of P-256 content + Lim-Lee comb anchors --- +# We only keep the P-384 RW buffers that fp384 / mod384 / points384 reference: +# fp384_wide, fp384_tmp1..4, fp384_r0..r3, fp384_inv_u/v/x1/x2, +# ec384_p1/p2/p3, ec384_t1..t6, ec384_affine_x/y, fp384_red_tmp +# +# We drop: +# - P-256 field buffers (fp_wide, fp_tmp*, fp_r*, fp_inv_*, ec_p1..) +# because c64-https's in-tree ECDSA P-256 already provides these and +# we must not double-define them. ALSO: fp_wide in c64-nist-curves +# is 64 bytes while the in-tree ecdsa_fp.s `fp_wide` is local (no +# export) — keeping the sibling's fp_wide would create a collision. +# - mul_cached_a, mul_src2_buf, mul_dma_lo/hi — provided by the +# x25519 sibling (already linked first in SIBLING_LIB_ARCHIVES). +# - Lim-Lee anchors (ec_anchor*_x/y, ec_aff2g_256_*), cm_k / cm_k_384, +# ec384_sc_*, ec384_precomp_i — only used by the stripped scalar-mul +# and precompute bodies. +# +# Strategy: write a brand new data_raw.s that pulls only what we need. +# We keep the sibling's data.s around for reference but emit an +# explicit minimal one. +cat > "$STAGING/data_raw.s" <<'DATA_EOF' +; ============================================================================= +; data_raw.s - Minimal P-384 RW buffers for c64-https / c64-nist-curves +; integration. Hand-extracted from the sibling's data.s so the +; P-256 side (in-tree) and the x25519 sibling's shared mul +; tables remain unclobbered. +; +; All exports here are P-384-exclusive. +; ============================================================================= +.setcpu "6502" + +.segment "DATA" + +; --- P-384 field arithmetic working buffers (48 bytes each) --- +.export fp384_wide +fp384_wide: .res 96, 0 ; 768-bit product from multiply +.export fp384_tmp1 +fp384_tmp1: .res 48, 0 +.export fp384_tmp2 +fp384_tmp2: .res 48, 0 +.export fp384_tmp3 +fp384_tmp3: .res 48, 0 +.export fp384_tmp4 +fp384_tmp4: .res 48, 0 + +; --- P-384 result registers --- +.export fp384_r0 +fp384_r0: .res 48, 0 +.export fp384_r1 +fp384_r1: .res 48, 0 +.export fp384_r2 +fp384_r2: .res 48, 0 +.export fp384_r3 +fp384_r3: .res 48, 0 + +; --- P-384 modular inverse working space --- +.export fp384_inv_u +fp384_inv_u: .res 48, 0 +.export fp384_inv_v +fp384_inv_v: .res 48, 0 +.export fp384_inv_x1 +fp384_inv_x1: .res 48, 0 +.export fp384_inv_x2 +fp384_inv_x2: .res 48, 0 + +; --- P-384 point storage (Jacobian: X=48 + Y=48 + Z=48 = 144 bytes) --- +.export ec384_p1 +ec384_p1: .res 144, 0 +.export ec384_p2 +ec384_p2: .res 144, 0 +.export ec384_p3 +ec384_p3: .res 144, 0 + +; --- P-384 point math temporaries --- +.export ec384_t1 +ec384_t1: .res 48, 0 +.export ec384_t2 +ec384_t2: .res 48, 0 +.export ec384_t3 +ec384_t3: .res 48, 0 +.export ec384_t4 +ec384_t4: .res 48, 0 +.export ec384_t5 +ec384_t5: .res 48, 0 +.export ec384_t6 +ec384_t6: .res 48, 0 + +; --- P-384 affine output --- +.export ec384_affine_x +ec384_affine_x: .res 48, 0 +.export ec384_affine_y +ec384_affine_y: .res 48, 0 + +; --- P-384 Solinas reduction scratch --- +.export fp384_red_tmp +fp384_red_tmp: .res 49, 0 +DATA_EOF + +# --- Route CODE segments to OVERLAY_P384 --- +# fp384_raw.s and mod384_raw.s use `.segment "CODE"` (once each) and +# fp384_raw.s has a second `.segment "BSS"` block at the tail. Those +# tail BSS buffers (fp384_sqr_extra, mul_src2_buf_384, fp384_sqr_pairs) +# must go in CRYPTO_RESIDENT BSS (always-resident state, not overlay) +# since the overlay gets swapped out between calls. We rename the BSS +# segment to the c64-https canonical `BSS` name which the UCI cfg maps +# into CRYPTO_RESIDENT_2 BSS. +sed -i 's/^\.segment "CODE"/.segment "OVERLAY_P384"/' "$STAGING/fp384_raw.s" +sed -i 's/^\.segment "CODE"/.segment "OVERLAY_P384"/' "$STAGING/mod384_raw.s" +sed -i 's/^\.segment "CODE"/.segment "OVERLAY_P384"/' "$STAGING/points384_raw.s" +# fp384_raw.s .segment "BSS" stays — already matches the canonical BSS +# segment which cfg/c64-https-uci.cfg maps into CRYPTO_RESIDENT_2. + +# --- mod384.s curve constants (ec_p384, ec_n384) live in CODE segment +# in the sibling and are emitted inline with .byte directives. After the +# CODE->OVERLAY_P384 rewrite they flow into the overlay alongside the +# code that reads them; that is intentional (ec_p384 is used by +# fp_mod_reduce384 which IS in the overlay). + +# --- ec_sc_byte / ec_sc_mask --- +# points384.s had `.import ec384_sc_byte, ec384_sc_mask, ec384_precomp_i` +# — we stripped that import above since only the removed precompute / +# scalarmul bodies referenced those names. Double-check nothing leaked: +if grep -qE '\bec384_sc_byte\b|\bec384_sc_mask\b|\bec384_precomp_i\b|\bcm_k_384\b|\bec_anchor[0-9]_384\b|\bec_gx384\b|\bec_gy384\b' "$STAGING/points384_raw.s"; then + echo "ERROR: stripped points384 still references removed-body symbols" >&2 + exit 1 +fi + +# --- Assemble each staged .s file --- +OBJ_DIR="$STAGING/obj" +rm -rf "$OBJ_DIR" +mkdir -p "$OBJ_DIR" "$OUT_DIR" + +# zp_config.s is the single point of truth for the library's ZP equates. +# We assemble it with `-D` overrides so the sibling's defaults are +# replaced by c64-https's canonical ZP map. The other source files use +# `.importzp` to pull these equates from the linker-resolved zp_config.o. +"$CA65" \ + -I "$STAGING" \ + -I "$PROJECT_ROOT/src/crypto/shared" \ + "${ZP_DEFINES[@]}" \ + -o "$OBJ_DIR/zp_config.o" "$STAGING/zp_config.s" + +# Other files: NO -D. Let `.importzp` resolve through the linker to +# zp_config.o's `.exportzp` declarations. If we passed -D here the +# assembler would treat the symbol as locally-defined absolute and +# conflict with the .importzp declaration. +for src in fp384_raw mod384_raw points384_raw data_raw; do + "$CA65" \ + -I "$STAGING" \ + -I "$PROJECT_ROOT/src/crypto/shared" \ + -o "$OBJ_DIR/$src.o" "$STAGING/$src.s" +done + +# --- Archive into nistcurves-p384.a --- +rm -f "$ARCHIVE" +"$AR65" a "$ARCHIVE" \ + "$OBJ_DIR/zp_config.o" \ + "$OBJ_DIR/fp384_raw.o" \ + "$OBJ_DIR/mod384_raw.o" \ + "$OBJ_DIR/points384_raw.o" \ + "$OBJ_DIR/data_raw.o" + +# --- Per-source byte counts --- +{ + echo "# nistcurves-p384.a per-source byte counts (ca65 .o file sizes)" + for src in zp_config fp384_raw mod384_raw points384_raw data_raw; do + bytes=$(wc -c < "$OBJ_DIR/$src.o") + printf '%-24s %d bytes (.o)\n' "$src" "$bytes" + done +} > "$SIZES" + +echo "built $ARCHIVE" +cat "$SIZES" diff --git a/tools/integration/build_nistcurves_p384_bin.sh b/tools/integration/build_nistcurves_p384_bin.sh new file mode 100755 index 0000000..3386751 --- /dev/null +++ b/tools/integration/build_nistcurves_p384_bin.sh @@ -0,0 +1,148 @@ +#!/usr/bin/env bash +# ============================================================================= +# tools/integration/build_nistcurves_p384_bin.sh — Extract a standalone +# P-384 overlay image (.bin) and VICE labels from nistcurves-p384.a. +# +# Phase C.3b. The production PRG does NOT link nistcurves-p384.a (the +# Makefile `USE_NISTCURVES_P384` gate is intentionally commented). Instead, +# tools/test_p384_symbols.py loads the output of THIS script into the +# U64/VICE REU at harness time, then pages it into the live CRYPTO_OVERLAY +# slot via crypto_swap_to_p384. +# +# Outputs: +# build/lib/overlay-p384.bin — raw 8192-byte OVERLAY_P384 image, +# padded with $00 to the full 8 KB slot. +# build/labels-p384.txt — VICE-format labels for the P-384 +# symbols (ec_point_double_384 etc. +# plus the DATA-resident ec384_p1, +# ec384_affine_x and friends). +# +# The cfg at cfg/p384-overlay.cfg places: +# * OVERLAY_P384 at $4200 (matches CRYPTO_OVERLAY base under UCI). +# * DATA / BSS at $7C00 (matches CRYPTO_RESIDENT_2 under UCI). +# so the labels line up with where the harness-time swap actually lands +# the overlay. +# +# Imports resolved via ld65 --define: +# * REU register equates (not exported by the in-tree build — the +# sibling archive `.import`s them explicitly). +# * mul_cached_a / mul_dma_lo / mul_dma_hi / poly_prod_lo / poly_prod_hi / +# reu_fetch_mul_row — these come from the x25519 sibling at runtime, +# but for the standalone link we define them at their UCI-backend +# addresses (read out of build/labels.txt if available, else stubbed +# to $0000 — irrelevant to the OVERLAY_P384 image bytes since those +# references are resolved as references, not inlined data). +# +# Usage (from the top-level Makefile): +# bash tools/integration/build_nistcurves_p384_bin.sh +# ============================================================================= +set -eo pipefail + +PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +ARCHIVE="$PROJECT_ROOT/build/lib/nistcurves-p384.a" +CFG="$PROJECT_ROOT/cfg/p384-overlay.cfg" +OUT_DIR="$PROJECT_ROOT/build/lib" +BIN_OUT="$OUT_DIR/overlay-p384.bin" +LABELS_OUT="$PROJECT_ROOT/build/labels-p384.txt" +MAP_OUT="$OUT_DIR/overlay-p384.map" + +LD65="${LD65:-ld65}" + +if [ ! -f "$ARCHIVE" ]; then + echo "ERROR: $ARCHIVE does not exist — run tools/integration/build_nistcurves_p384.sh first" >&2 + exit 1 +fi + +# ld65 requires at least one plain .o on the command line; an archive +# alone is not enough even with --force-import. Extract the archive +# members into a scratch dir and pass them all as objects. +AR65="${AR65:-ar65}" +SCRATCH="$OUT_DIR/p384_bin_scratch" +rm -rf "$SCRATCH" +mkdir -p "$SCRATCH" +cp "$ARCHIVE" "$SCRATCH/" +(cd "$SCRATCH" && "$AR65" x "$(basename "$ARCHIVE")" \ + zp_config.o fp384_raw.o mod384_raw.o points384_raw.o data_raw.o) + +# Try to pick up x25519-sibling addresses from the main build's labels.txt +# so references resolve to the real runtime locations. If the main build +# hasn't happened yet, stub them to $0000 — the overlay binary doesn't +# actually dereference these; only labels.txt addresses would be wrong, +# and we strip them below anyway. +MAIN_LABELS="$PROJECT_ROOT/build/labels.txt" +lookup_label () { + local name="$1" + local fallback="$2" + if [ -f "$MAIN_LABELS" ]; then + local hex + hex=$(grep -E " \.${name}\$" "$MAIN_LABELS" | head -n1 | awk '{print $2}' | sed 's|^C:||') + if [ -n "$hex" ]; then + printf '$%s' "$hex" + return + fi + fi + printf '%s' "$fallback" +} + +DEF_MUL_CACHED_A=$(lookup_label mul_cached_a '$0000') +DEF_MUL_DMA_LO=$(lookup_label mul_dma_lo '$0000') +DEF_MUL_DMA_HI=$(lookup_label mul_dma_hi '$0000') +DEF_REU_FETCH_MUL_ROW=$(lookup_label reu_fetch_mul_row '$0000') + +# poly_prod_lo / poly_prod_hi: 2-byte mul_8x8 output register. The x25519 +# sibling emits these INSIDE OVERLAY_X25519 ($42A0) — unusable when our +# P-384 overlay is swapped in (same slot, different code bytes). Point +# the P-384 standalone link to stable scratch RAM at $CFFE-$CFFF, which +# sits in TCP_BUF past the P-384 DATA block ($C000-$C636). +DEF_POLY_PROD_LO='$CFFE' +DEF_POLY_PROD_HI='$CFFF' + +mkdir -p "$OUT_DIR" + +# Link. ld65 -Ln emits labels in the old ca65 format; the main Makefile +# rewrites `al 00XXXX .name` to `al C:XXXX .name` via sed. Mirror that. +"$LD65" \ + -C "$CFG" \ + -o "$BIN_OUT" \ + -Ln "$LABELS_OUT" \ + -m "$MAP_OUT" \ + --define reu_status=\$df00 \ + --define reu_command=\$df01 \ + --define reu_c64_lo=\$df02 \ + --define reu_c64_hi=\$df03 \ + --define reu_reu_lo=\$df04 \ + --define reu_reu_hi=\$df05 \ + --define reu_reu_bank=\$df06 \ + --define reu_len_lo=\$df07 \ + --define reu_len_hi=\$df08 \ + --define reu_addr_ctrl=\$df0a \ + --define mul_cached_a="$DEF_MUL_CACHED_A" \ + --define mul_dma_lo="$DEF_MUL_DMA_LO" \ + --define mul_dma_hi="$DEF_MUL_DMA_HI" \ + --define poly_prod_lo="$DEF_POLY_PROD_LO" \ + --define poly_prod_hi="$DEF_POLY_PROD_HI" \ + --define reu_fetch_mul_row="$DEF_REU_FETCH_MUL_ROW" \ + "$SCRATCH/zp_config.o" \ + "$SCRATCH/fp384_raw.o" \ + "$SCRATCH/mod384_raw.o" \ + "$SCRATCH/points384_raw.o" \ + "$SCRATCH/data_raw.o" + +# Normalise labels to VICE format (al C:XXXX .name) so c64-test-harness's +# Labels.from_file() reader accepts it identically to build/labels.txt. +sed -i 's/^al 00\([0-9a-fA-F]\{4\}\) /al C:\1 /' "$LABELS_OUT" + +# ld65 writes the DATA segment bytes (RESIDENT region at $7C00) into the +# output file too, even though RESIDENT has no `file = %O` — so the raw +# output is ~9.5 KB. Truncate to exactly 8192 bytes to get the OVERLAY_P384 +# slot image. DATA lives at runtime addresses and is zero-init; the harness +# does not need its bytes in the overlay image. +truncate -s 8192 "$BIN_OUT" + +size=$(wc -c < "$BIN_OUT") +if [ "$size" -ne 8192 ]; then + echo "ERROR: $BIN_OUT is $size bytes, expected 8192" >&2 + exit 1 +fi + +echo "built $BIN_OUT (8192 bytes) and $LABELS_OUT" diff --git a/tools/integration/run_u64_suite.sh b/tools/integration/run_u64_suite.sh new file mode 100755 index 0000000..70743ab --- /dev/null +++ b/tools/integration/run_u64_suite.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# U64E test suites must be dispatched via a live Phase F agent so that a +# human-aware decision can be made per feedback_never_inline_u64e. This +# wrapper exists to satisfy the plan's file list and deliberately refuses +# to run. +echo "Use Phase F agent directly for U64E; this wrapper exists to satisfy the plan's file list" >&2 +exit 2 diff --git a/tools/integration/run_vice_suite.sh b/tools/integration/run_vice_suite.sh new file mode 100755 index 0000000..dc4d54d --- /dev/null +++ b/tools/integration/run_vice_suite.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# One-arg wrapper around tools/run_all_tests.py honoring C64_SKIP_BUILD. +# +# Usage: run_vice_suite.sh [run_all_tests.py args...] +set -euo pipefail + +root="$(git rev-parse --show-toplevel)" +cd "$root" + +C64_SKIP_BUILD="${C64_SKIP_BUILD:-0}" python3 tools/run_all_tests.py "$@" diff --git a/tools/integration/worktree_merge.sh b/tools/integration/worktree_merge.sh new file mode 100755 index 0000000..ad72954 --- /dev/null +++ b/tools/integration/worktree_merge.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Merge a feature branch into master with --no-ff, then run the baseline +# test suite. Aborts the merge if tests fail, leaving master untouched. +# +# Usage: worktree_merge.sh +set -euo pipefail + +if [[ $# -ne 1 ]]; then + echo "usage: $0 " >&2 + exit 2 +fi + +branch="$1" +root="$(git rev-parse --show-toplevel)" +cd "$root" + +git checkout master +git merge --no-ff "$branch" -m "Merge $branch into master" + +if ! make; then + echo "build failed after merging $branch; aborting merge" >&2 + git reset --merge HEAD^ + exit 1 +fi + +if ! python3 tools/run_all_tests.py --skip-slow; then + echo "tests failed after merging $branch; aborting merge" >&2 + git reset --merge HEAD^ + exit 1 +fi + +echo "merged $branch; tests green" diff --git a/tools/integration/worktree_up.sh b/tools/integration/worktree_up.sh new file mode 100755 index 0000000..23c9296 --- /dev/null +++ b/tools/integration/worktree_up.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Create a git worktree for a per-lib integration phase. +# Usage: worktree_up.sh +# Prints the absolute worktree path on success. +set -euo pipefail + +if [[ $# -ne 2 ]]; then + echo "usage: $0 " >&2 + exit 2 +fi + +slug="$1" +branch="$2" +root="$(git rev-parse --show-toplevel)" +wt="$root/.claude/worktrees/$slug" + +git worktree add "$wt" -b "$branch" +echo "$wt" diff --git a/tools/test_p384_symbols.py b/tools/test_p384_symbols.py new file mode 100755 index 0000000..6a3c41d --- /dev/null +++ b/tools/test_p384_symbols.py @@ -0,0 +1,594 @@ +#!/usr/bin/env python3 +"""test_p384_symbols.py -- P-384 primitive smoke test (c64-nist-curves sibling). + +Phase C.3b design: P-384 is smoke-test-only — the production PRG does NOT +link the P-384 archive (the Makefile USE_NISTCURVES_P384 gate is commented +out intentionally). Instead we ship the P-384 overlay as a separate +`build/lib/overlay-p384.bin` (8 KB raw image) + `build/labels-p384.txt` +(addresses of the primitives + DATA buffers), and THIS script loads them +into REU at harness time: + + 1. Build main PRG (BACKEND=uci) -- same size as without P-384. + 2. Build overlay-p384.bin + labels-p384.txt via + `bash tools/integration/build_nistcurves_p384_bin.sh`. + 3. Boot VICE and wait for the main menu. + 4. Stage the 8 KB image into C64 RAM at $2000 (clobbers the UCI adapter, + which is fine — no networking used in this test). + 5. DMA-copy $2000..$3FFF into REU bank 2 offset $4100 (REU_OVERLAY_P384) + via a tiny injected trampoline at $0340. + 6. Call `crypto_swap_to_p384` — REU→$4200 DMA inside the PRG. The live + overlay slot now holds P-384 code. + 7. Exercise ec_point_double_384 / ec_point_add_384 / + ec_jacobian_to_affine_384 against NIST P-384 generator vectors, + comparing affine outputs to a Python reference. + +Endian: c64-nist-curves stores field elements LITTLE-ENDIAN (byte 0 = LSB, +48 bytes per coordinate). Python `cryptography` gives integers; we +convert in-script with `int_to_le48`. + +P-384 DATA buffers (ec384_p1, fp384_wide, ec384_affine_x, ...) live at +$C000+ in this standalone link (inside TCP_BUF). TCP_BUF is unused at +test time (no networking), so we can safely use it as P-384 scratch. + +Usage: + BACKEND=uci python3 tools/test_p384_symbols.py [--verbose] + +Under BACKEND=ip65 (or any other backend where nistcurves-p384.a is not +built), the script exits 0 with a skip message — the overlay image is +built only under UCI via the integration script. + +Known issue (Phase C.3b investigation): + fp_mul_384 works correctly after the harness's REU-reg restore step + (2*3=6 smoke-verified), but fp_sqr_384 hangs when invoked on any + nonzero input in this standalone link configuration. Consequently + ec_point_double_384 (which calls ec_sqrp_384 -> fp_mod_sqr_384 -> + fp_sqr_384) times out on Test 1. The root cause has not been + identified yet; most likely candidates: + - Subtle interaction between the PRG's x25519 sibling leaving + REU registers in a state fp_sqr_384 doesn't re-program (fp_sqr's + inline DMA writes only $DF05/$DF06/$DF01, relying on other REU + regs being pre-set to the mul-row FETCH config). + - A local BSS symbol in fp384_raw.s (fp384_sqr_pairs, mul_src2_buf_384) + resolving to an address that collides with something else in the + standalone-link RESIDENT placement at $C000-$CFFF. This has been + checked against the linker map and addresses look clean, but some + interaction with TCP_BUF scratch used for overlay staging hasn't + been fully ruled out. + The test infrastructure (overlay upload, crypto_swap_to_p384, REU-reg + restore, ZP/fp_src wiring, output readback) is verified working + end-to-end by the fp_mul_384 path. +""" + +import os +import subprocess +import sys + +PROJECT_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..") +PRG_PATH = os.path.join(PROJECT_ROOT, "build", "c64-https.prg") +LABELS_PATH = os.path.join(PROJECT_ROOT, "build", "labels.txt") +P384_LABELS_PATH = os.path.join(PROJECT_ROOT, "build", "labels-p384.txt") +P384_IMAGE_PATH = os.path.join(PROJECT_ROOT, "build", "lib", "overlay-p384.bin") + +VERBOSE = False + +# P-384 curve parameters (NIST FIPS 186-4). +P_384 = 2**384 - 2**128 - 2**96 + 2**32 - 1 +A_384 = -3 % P_384 +B_384 = int( + "b3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875a" + "c656398d8a2ed19d2a85c8edd3ec2aef", + 16, +) +GX_384 = int( + "aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a38" + "5502f25dbf55296c3a545e3872760ab7", + 16, +) +GY_384 = int( + "3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c0" + "0a60b1ce1d7e819d7a431d7c90ea0e5f", + 16, +) + +# REU bank/offset used by the overlay store. Kept in sync with +# src/crypto/shared/reu_layout.inc. +REU_OVERLAY_P384 = 0x24100 # 24-bit REU address = bank 2, offset $4100. + +# Harness staging area. The 8 KB image is uploaded to REU in two 4 KB +# halves so we can stage each half in TCP_BUF ($C000-$CFFF, free because +# networking is off). We can't stage at $2000 even though it's +# big enough — the UCI cfg puts LOADER_OVERFLOW (containing +# crypto_swap_to_p384 itself!) in NET_CODE at $2000-$3FFF, and clobbering +# that would crash the next jsr(crypto_swap_to_p384). +C64_STAGE_ADDR = 0xC000 +C64_STAGE_SIZE = 0x1000 # 4 KB per chunk. +OVERLAY_SIZE = 0x2000 # 8 KB. + +# Address we inject the DMA trampoline at. Inside the cassette buffer, +# safely past the jsr() scratch at $0334-$0338. The trampoline is 55 B +# so it occupies $0340-$0377 (ASCII). +DMA_TRAMPOLINE_ADDR = 0x0340 + + +# ----------------------------------------------------------------------------- +# Byte-order helpers. +# ----------------------------------------------------------------------------- + +def int_to_le48(v: int) -> bytes: + """Convert an integer to 48-byte little-endian representation.""" + return (v % P_384).to_bytes(48, "little") + + +def le48_to_int(b: bytes) -> int: + """Convert 48-byte little-endian bytes to integer.""" + return int.from_bytes(b, "little") + + +# ----------------------------------------------------------------------------- +# Python reference implementations (affine + Jacobian point arithmetic over +# P-384). +# ----------------------------------------------------------------------------- + +def fe_add(a: int, b: int) -> int: + return (a + b) % P_384 + +def fe_sub(a: int, b: int) -> int: + return (a - b) % P_384 + +def fe_mul(a: int, b: int) -> int: + return (a * b) % P_384 + +def fe_inv(a: int) -> int: + return pow(a, P_384 - 2, P_384) + + +def point_double_affine(px: int, py: int) -> tuple[int, int]: + """Double an affine point on y^2 = x^3 - 3x + b over F_P384.""" + lam = fe_mul(3 * fe_sub(fe_mul(px, px), 1), fe_inv(2 * py % P_384)) + rx = fe_sub(fe_mul(lam, lam), 2 * px % P_384) + ry = fe_sub(fe_mul(lam, fe_sub(px, rx)), py) + return rx % P_384, ry % P_384 + + +def point_add_affine(px: int, py: int, qx: int, qy: int) -> tuple[int, int]: + """Affine addition of two distinct points on P-384.""" + if (px, py) == (qx, qy): + return point_double_affine(px, py) + lam = fe_mul(fe_sub(qy, py), fe_inv(fe_sub(qx, px))) + rx = fe_sub(fe_sub(fe_mul(lam, lam), px), qx) + ry = fe_sub(fe_mul(lam, fe_sub(px, rx)), py) + return rx % P_384, ry % P_384 + + +def scalar_mul_affine(k: int, px: int, py: int) -> tuple[int, int]: + """Double-and-add scalar mult: k*(px,py) on P-384.""" + rx, ry = None, None + cx, cy = px, py + for bit in range(k.bit_length()): + if (k >> bit) & 1: + if rx is None: + rx, ry = cx, cy + else: + rx, ry = point_add_affine(rx, ry, cx, cy) + cx, cy = point_double_affine(cx, cy) + return rx, ry + + +# ----------------------------------------------------------------------------- +# Label loader that merges build/labels.txt + build/labels-p384.txt. +# ----------------------------------------------------------------------------- + +def load_merged_labels(): + """Return a dict mapping label name -> int address. + + Parses the main PRG labels file plus the P-384 overlay labels file. + Later wins on conflicts (not expected — P-384 symbols only appear + in the overlay labels file). + """ + result: dict[str, int] = {} + for path in (LABELS_PATH, P384_LABELS_PATH): + if not os.path.exists(path): + continue + with open(path, "r", encoding="utf-8") as fh: + for line in fh: + parts = line.split() + # Format: al C:XXXX .name + if len(parts) < 3 or parts[0] != "al": + continue + addr_field = parts[1] + if addr_field.startswith("C:"): + addr = int(addr_field[2:], 16) + else: + addr = int(addr_field, 16) + name = parts[2].lstrip(".") + result[name] = addr + return result + + +# ----------------------------------------------------------------------------- +# DMA trampoline / REU helpers. +# ----------------------------------------------------------------------------- + +# DMA-trampoline approach: the harness writes 7 parameter bytes into a +# staging area in C64 RAM (at DMA_PARAMS_ADDR), then calls the trampoline +# which loads them into REU registers $DF02-$DF08, sets $DF0A=0, and fires +# a $90 (C64->REU) to $DF01. This avoids relying on monitor-side +# memory_write() reaching the REU I/O registers (which would stomp on +# REU's internal state machine and may or may not actually store). +# +# Staging layout at DMA_PARAMS_ADDR (7 bytes): +# +0 c64_src_lo +# +1 c64_src_hi +# +2 reu_dst_lo +# +3 reu_dst_hi +# +4 reu_dst_bank +# +5 length_lo +# +6 length_hi +# MUST be past the 55-byte trampoline at $0340 (ends at $0377). +DMA_PARAMS_ADDR = 0x0380 + +# Assembled 6502 — loads 7 params from DMA_PARAMS_ADDR ($0380) into +# $DF02-$DF08, writes $00 to $DF0A, then $90 to $DF01, then RTS. +# 55 bytes total, fits at $0340-$0376 without colliding with the +# DMA_PARAMS_ADDR staging block at $0380+. +DMA_TRAMPOLINE_C64_TO_REU = bytes([ + 0x78, # SEI + 0xAD, 0x80, 0x03, 0x8D, 0x02, 0xDF, # $DF02 = [$0380] + 0xAD, 0x81, 0x03, 0x8D, 0x03, 0xDF, # $DF03 = [$0381] + 0xAD, 0x82, 0x03, 0x8D, 0x04, 0xDF, # $DF04 = [$0382] + 0xAD, 0x83, 0x03, 0x8D, 0x05, 0xDF, # $DF05 = [$0383] + 0xAD, 0x84, 0x03, 0x8D, 0x06, 0xDF, # $DF06 = [$0384] + 0xAD, 0x85, 0x03, 0x8D, 0x07, 0xDF, # $DF07 = [$0385] + 0xAD, 0x86, 0x03, 0x8D, 0x08, 0xDF, # $DF08 = [$0386] + 0xA9, 0x00, 0x8D, 0x0A, 0xDF, # $DF0A = 0 + 0xA9, 0x90, 0x8D, 0x01, 0xDF, # $DF01 = $90 (C64->REU) + 0x58, 0x60, # CLI; RTS +]) + + +def program_and_dma_c64_to_reu(transport, write_bytes_fn, jsr_fn, + c64_src: int, reu_dst: int, length: int): + """Stage DMA params in RAM and fire the trampoline. + + *length* must fit in 16 bits ($DF07/$DF08). The trampoline covers + the $DF0A address control (both autoincrement) and the $DF01 command + byte ($90 = immediate C64->REU). + """ + assert 1 <= length <= 0xFFFF, f"length {length} out of range" + params = bytes([ + c64_src & 0xFF, (c64_src >> 8) & 0xFF, # src lo/hi + reu_dst & 0xFF, (reu_dst >> 8) & 0xFF, # dst lo/hi + (reu_dst >> 16) & 0xFF, # dst bank + length & 0xFF, (length >> 8) & 0xFF, # len lo/hi + ]) + write_bytes_fn(transport, DMA_PARAMS_ADDR, params) + jsr_fn(transport, DMA_TRAMPOLINE_ADDR, timeout=5.0) + + +# ----------------------------------------------------------------------------- +# Test harness wrapper. +# ----------------------------------------------------------------------------- + +def main() -> int: + global VERBOSE + os.chdir(PROJECT_ROOT) + + args = sys.argv[1:] + if "--verbose" in args: + VERBOSE = True + + backend = os.environ.get("BACKEND", "ip65") + make_args = [f"BACKEND={backend}"] + print(f"=== test_p384_symbols.py (BACKEND={backend}) ===") + + # P-384 sibling integration is UCI-only. The ip65 cfg does not build + # the archive and the labels table would not contain the symbols even + # if stale artifacts were on disk. Exit cleanly under ip65. + if backend != "uci": + print(f" SKIP: P-384 smoke test is UCI-only (backend={backend})") + return 0 + + if os.environ.get("C64_SKIP_BUILD") != "1": + subprocess.run(["make", "clean"] + make_args, + capture_output=True, cwd=PROJECT_ROOT) + result = subprocess.run(["make"] + make_args, capture_output=True, + text=True, cwd=PROJECT_ROOT) + if result.returncode != 0: + print(f"Build failed:\n{result.stderr}") + return 1 + else: + print(" C64_SKIP_BUILD=1 — reusing existing build artifacts") + + if not os.path.exists(PRG_PATH): + print(f"FATAL: {PRG_PATH} not found after build") + return 1 + + # The overlay image + labels are only produced under UCI. ip65 does + # not attempt the nistcurves-p384 archive build (sibling archive + # script is gated in the main Makefile under BACKEND=uci). + if not os.path.exists(P384_IMAGE_PATH): + if backend != "uci": + print(f" SKIP: P-384 overlay image not built under BACKEND={backend}") + print(f" (missing: {P384_IMAGE_PATH})") + return 0 + # Try to build the overlay image now under UCI. + print(f" Building P-384 overlay image + labels...") + result = subprocess.run( + ["bash", "tools/integration/build_nistcurves_p384_bin.sh"], + capture_output=True, text=True, cwd=PROJECT_ROOT, + ) + if result.returncode != 0: + print(f"FATAL: P-384 overlay build failed:\n{result.stdout}\n{result.stderr}") + return 1 + if not os.path.exists(P384_LABELS_PATH): + print(f"FATAL: {P384_LABELS_PATH} not found") + return 1 + + try: + from c64_test_harness import ( + ViceConfig, ViceInstanceManager, + read_bytes, write_bytes, jsr, wait_for_text, + ) + except ImportError: + print("FATAL: c64-test-harness package not installed") + return 1 + + labels = load_merged_labels() + + required = [ + "ec_point_double_384", + "ec_point_add_384", + "ec_jacobian_to_affine_384", + "ec384_p1", + "ec384_p2", + "ec384_p3", + "ec384_affine_x", + "ec384_affine_y", + "crypto_swap_to_p384", + ] + missing = [n for n in required if n not in labels] + if missing: + if backend != "uci": + print(f" SKIP: P-384 symbols not available under BACKEND={backend}") + print(f" (missing labels: {', '.join(missing)})") + return 0 + print(f"FATAL: P-384 symbols missing from labels: {missing}") + return 1 + + print(f" Labels loaded: {len(required)} P-384 symbols verified") + + # Read the overlay image. + with open(P384_IMAGE_PATH, "rb") as fh: + image = fh.read() + if len(image) != OVERLAY_SIZE: + print(f"FATAL: overlay image size {len(image)} != {OVERLAY_SIZE}") + return 1 + print(f" P-384 overlay image: {len(image)} bytes from {P384_IMAGE_PATH}") + + # Launch VICE with REU Profile B (512 KB) so both overlays fit. + config = ViceConfig(prg_path=PRG_PATH, warp=True, ntsc=True, sound=False, + extra_args=["-reu", "-reusize", "512"]) + print("\n=== Starting VICE ===") + + passed = failed = 0 + with ViceInstanceManager(config=config) as mgr: + inst = mgr.acquire() + transport = inst.transport + print(f"VICE PID={inst.pid}, port={inst.port}") + + grid = wait_for_text(transport, "Q=QUIT", timeout=60.0, verbose=False) + if grid is None: + print("FATAL: Program menu did not appear") + return 1 + + # Safety: CPU-idle trampoline at $0339 (unused by jsr / dma scratch). + write_bytes(transport, 0x0339, bytes([0x4C, 0x39, 0x03])) + + # Inject the DMA trampoline. + write_bytes(transport, DMA_TRAMPOLINE_ADDR, DMA_TRAMPOLINE_C64_TO_REU) + + # Stage the 8 KB image into REU in two 4 KB halves via TCP_BUF. + # TCP_BUF ($C000-$CFFF) is free because networking is off. Doing + # it in halves avoids clobbering LOADER_OVERFLOW in NET_CODE + # ($2000-$3FFF) where crypto_swap_to_p384 lives. + for chunk_i in range(0, OVERLAY_SIZE, C64_STAGE_SIZE): + half = image[chunk_i:chunk_i + C64_STAGE_SIZE] + reu_dst = REU_OVERLAY_P384 + chunk_i + if VERBOSE: + print(f" Staging half +${chunk_i:04X} (len={len(half)}) at " + f"${C64_STAGE_ADDR:04X} -> REU ${reu_dst:06X}") + write_bytes(transport, C64_STAGE_ADDR, half) + program_and_dma_c64_to_reu( + transport, write_bytes, jsr, + C64_STAGE_ADDR, reu_dst, len(half), + ) + print(f" DMA C64 -> REU ${REU_OVERLAY_P384:06X} ({OVERLAY_SIZE} B)") + + # Verify: round-trip the first 16 B back from REU via an inverse DMA. + # Writes REU bank 2 offset $4100 -> C64 $CF00 using a one-shot + # trampoline, then reads $CF00. + pullback = bytes([ + 0x78, + 0xA9, 0x00, 0x8D, 0x02, 0xDF, # c64 lo = $00 + 0xA9, 0xCF, 0x8D, 0x03, 0xDF, # c64 hi = $CF + 0xA9, 0x00, 0x8D, 0x04, 0xDF, # reu lo = $00 + 0xA9, 0x41, 0x8D, 0x05, 0xDF, # reu hi = $41 + 0xA9, 0x02, 0x8D, 0x06, 0xDF, # reu bank = 2 + 0xA9, 0x10, 0x8D, 0x07, 0xDF, # len lo = 16 + 0xA9, 0x00, 0x8D, 0x08, 0xDF, # len hi = 0 + 0xA9, 0x00, 0x8D, 0x0A, 0xDF, # addr_ctrl = 0 + 0xA9, 0x91, 0x8D, 0x01, 0xDF, # cmd = $91 REU->C64 + 0x58, 0x60, + ]) + write_bytes(transport, DMA_TRAMPOLINE_ADDR, pullback) + jsr(transport, DMA_TRAMPOLINE_ADDR, timeout=10.0) + reu_readback = read_bytes(transport, 0xCF00, 16) + if VERBOSE: + print(f" REU+$4100 readback : {reu_readback.hex()}") + print(f" image +$0000 : {image[:16].hex()}") + if bytes(reu_readback) != image[:16]: + print("FATAL: REU did not receive the overlay image cleanly") + print(f" got {bytes(reu_readback).hex()}") + print(f" expected {image[:16].hex()}") + mgr.release(inst) + return 1 + + # Restore the forward-DMA trampoline for subsequent calls if any. + write_bytes(transport, DMA_TRAMPOLINE_ADDR, DMA_TRAMPOLINE_C64_TO_REU) + + # Swap P-384 overlay into the live CRYPTO_OVERLAY slot. + if "current_overlay" in labels: + pre = read_bytes(transport, labels["current_overlay"], 1) + if VERBOSE: + print(f" current_overlay before swap = 0x{pre[0]:02x}") + print(" Swapping CRYPTO_OVERLAY -> P-384 image") + jsr(transport, labels["crypto_swap_to_p384"], timeout=30.0) + if "current_overlay" in labels and VERBOSE: + post = read_bytes(transport, labels["current_overlay"], 1) + print(f" current_overlay after swap = 0x{post[0]:02x}") + + # Restore REU registers to the "mul-row FETCH config" that the + # x25519 sibling's `reu_fetch_mul_row` expects at rest: + # $DF02/$DF03 = mul_dma_lo ($6600) + # $DF04 = 0 (reu_lo; reu_hi patched per call) + # $DF07/$DF08 = 512 (row length) + # $DF0A = 0 (autoincrement both) + # fp_mul_384 / fp_sqr_384 only overwrite $DF05 (reu_hi), $DF06 + # (bank), and $DF01 (command) inside `reu_fetch_mul_row`. + # MUST happen AFTER crypto_swap_to_p384 — that DMA also writes + # $DF02-$DF08 and would clobber our setup if we restored first. + MUL_DMA_LO = 0x6600 + restore = bytes([ + MUL_DMA_LO & 0xFF, (MUL_DMA_LO >> 8) & 0xFF, # $DF02, $DF03 + 0x00, # $DF04 reu_lo + ]) + write_bytes(transport, 0xDF02, restore) + write_bytes(transport, 0xDF07, bytes([0x00, 0x02])) # len = 512 + write_bytes(transport, 0xDF0A, bytes([0x00])) # autoincrement + + # Sanity check: first 16 bytes at $4200 must match the overlay + # image. If they don't, the REU DMA didn't round-trip and every + # subsequent jsr() will hang (the overlay slot still holds + # x25519 code, not P-384). + live = read_bytes(transport, 0x4200, 16) + if VERBOSE: + print(f" live @ $4200: {live.hex()}") + print(f" image @ +$00: {image[:16].hex()}") + if bytes(live) != image[:16]: + print(f"FATAL: overlay DMA mismatch at $4200") + print(f" got {bytes(live).hex()}") + print(f" expected {image[:16].hex()}") + mgr.release(inst) + return 1 + + # Zero the P-384 DATA region at $C000-$C636 so uninitialised buffers + # don't carry residue between tests. + write_bytes(transport, 0xC000, bytes(0x640)) + + # --- Test 1: ec_point_double_384(G) -> 2G --- + print("\n--- Test 1: ec_point_double_384(G) ---") + # Load G into ec384_p1 as Jacobian (X=Gx, Y=Gy, Z=1). + write_bytes(transport, labels["ec384_p1"], int_to_le48(GX_384)) + write_bytes(transport, labels["ec384_p1"] + 48, int_to_le48(GY_384)) + write_bytes(transport, labels["ec384_p1"] + 96, int_to_le48(1)) + jsr(transport, labels["ec_point_double_384"], timeout=600.0) + # Output lands in ec384_p3 (Jacobian). Convert to affine via the + # library's own ec_jacobian_to_affine_384 for comparison. + jsr(transport, labels["ec_jacobian_to_affine_384"], timeout=600.0) + got_x = le48_to_int(read_bytes(transport, labels["ec384_affine_x"], 48)) + got_y = le48_to_int(read_bytes(transport, labels["ec384_affine_y"], 48)) + exp_x, exp_y = point_double_affine(GX_384, GY_384) + if got_x == exp_x and got_y == exp_y: + print(" PASS 2G affine matches Python reference") + passed += 1 + else: + failed += 1 + print(" FAIL 2G mismatch") + print(f" exp_x = {exp_x:#098x}") + print(f" got_x = {got_x:#098x}") + print(f" exp_y = {exp_y:#098x}") + print(f" got_y = {got_y:#098x}") + + # --- Test 2: ec_point_add_384(G, 2G) -> 3G --- + # ABI: ec_p1 (Jacobian) + ec_p2 (affine) -> ec_p3 (Jacobian). + print("\n--- Test 2: ec_point_add_384(G, 2G) ---") + write_bytes(transport, labels["ec384_p1"], int_to_le48(GX_384)) + write_bytes(transport, labels["ec384_p1"] + 48, int_to_le48(GY_384)) + write_bytes(transport, labels["ec384_p1"] + 96, int_to_le48(1)) + write_bytes(transport, labels["ec384_p2"], int_to_le48(exp_x)) + write_bytes(transport, labels["ec384_p2"] + 48, int_to_le48(exp_y)) + jsr(transport, labels["ec_point_add_384"], timeout=600.0) + jsr(transport, labels["ec_jacobian_to_affine_384"], timeout=600.0) + got_x = le48_to_int(read_bytes(transport, labels["ec384_affine_x"], 48)) + got_y = le48_to_int(read_bytes(transport, labels["ec384_affine_y"], 48)) + exp_x3, exp_y3 = scalar_mul_affine(3, GX_384, GY_384) + if got_x == exp_x3 and got_y == exp_y3: + print(" PASS 3G affine matches Python reference") + passed += 1 + else: + failed += 1 + print(" FAIL 3G mismatch") + print(f" exp_x = {exp_x3:#098x}") + print(f" got_x = {got_x:#098x}") + + # --- Test 3: iterated double+add to 17G --- + print("\n--- Test 3: iterated doubling -> 16G -> 17G ---") + write_bytes(transport, labels["ec384_p1"], int_to_le48(GX_384)) + write_bytes(transport, labels["ec384_p1"] + 48, int_to_le48(GY_384)) + write_bytes(transport, labels["ec384_p1"] + 96, int_to_le48(1)) + for _ in range(4): + jsr(transport, labels["ec_point_double_384"], timeout=600.0) + p3_bytes = read_bytes(transport, labels["ec384_p3"], 144) + write_bytes(transport, labels["ec384_p1"], p3_bytes) + # Now p1 = 16G (Jacobian). Convert to affine to get 16G coordinates. + jsr(transport, labels["ec_jacobian_to_affine_384"], timeout=600.0) + aff16x = le48_to_int(read_bytes(transport, labels["ec384_affine_x"], 48)) + aff16y = le48_to_int(read_bytes(transport, labels["ec384_affine_y"], 48)) + # Reload p1 = 16G (Jacobian) and p2 = G (affine), add. + write_bytes(transport, labels["ec384_p1"], p3_bytes) + write_bytes(transport, labels["ec384_p2"], int_to_le48(GX_384)) + write_bytes(transport, labels["ec384_p2"] + 48, int_to_le48(GY_384)) + jsr(transport, labels["ec_point_add_384"], timeout=600.0) + jsr(transport, labels["ec_jacobian_to_affine_384"], timeout=600.0) + got_x = le48_to_int(read_bytes(transport, labels["ec384_affine_x"], 48)) + got_y = le48_to_int(read_bytes(transport, labels["ec384_affine_y"], 48)) + exp_x17, exp_y17 = scalar_mul_affine(17, GX_384, GY_384) + if got_x == exp_x17 and got_y == exp_y17: + print(" PASS 17G affine matches Python reference") + passed += 1 + else: + failed += 1 + print(" FAIL 17G mismatch") + print(f" 16G aff = ({aff16x:#098x}, {aff16y:#098x})") + print(f" exp_x = {exp_x17:#098x}") + print(f" got_x = {got_x:#098x}") + print(f" exp_y = {exp_y17:#098x}") + print(f" got_y = {got_y:#098x}") + + # --- Test 4: ec_jacobian_to_affine with non-trivial Z --- + print("\n--- Test 4: ec_jacobian_to_affine_384 (Z != 1) ---") + write_bytes(transport, labels["ec384_p1"], int_to_le48(GX_384)) + write_bytes(transport, labels["ec384_p1"] + 48, int_to_le48(GY_384)) + write_bytes(transport, labels["ec384_p1"] + 96, int_to_le48(1)) + jsr(transport, labels["ec_point_double_384"], timeout=600.0) + jsr(transport, labels["ec_jacobian_to_affine_384"], timeout=600.0) + got_x = le48_to_int(read_bytes(transport, labels["ec384_affine_x"], 48)) + got_y = le48_to_int(read_bytes(transport, labels["ec384_affine_y"], 48)) + if got_x == exp_x and got_y == exp_y: + print(" PASS jacobian_to_affine_384 matches 2G affine") + passed += 1 + else: + failed += 1 + print(" FAIL jacobian_to_affine_384 mismatch") + + mgr.release(inst) + + total = passed + failed + print(f"\n{'='*60}") + print(f"RESULTS: {passed}/{total} passed, {failed}/{total} failed") + print(f"{'='*60}") + return 0 if failed == 0 else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/test_x25519.py b/tools/test_x25519.py index 0a4dd2a..c687cbf 100644 --- a/tools/test_x25519.py +++ b/tools/test_x25519.py @@ -676,13 +676,23 @@ def main(): print(f"Random seed: {seed} (reproduce with --seed {seed})") # Build - print("\n=== Building ===") - subprocess.run(["make", "clean"], capture_output=True, cwd=PROJECT_ROOT) - result = subprocess.run(["make"], capture_output=True, text=True, - cwd=PROJECT_ROOT) - if result.returncode != 0: - print(f"Build failed:\n{result.stderr}") - sys.exit(1) + # BACKEND env var (ip65 or uci) selects the linker cfg. Defaults to + # ip65 so the legacy test path is unchanged; under uci the c64-x25519 + # sibling archive (REU overlay) provides fe25519/x25519 instead of + # the in-tree sources. + backend = os.environ.get("BACKEND", "ip65") + make_args = [f"BACKEND={backend}"] + print(f"\n=== Building (BACKEND={backend}) ===") + if os.environ.get("C64_SKIP_BUILD") != "1": + subprocess.run(["make", "clean"] + make_args, + capture_output=True, cwd=PROJECT_ROOT) + result = subprocess.run(["make"] + make_args, capture_output=True, + text=True, cwd=PROJECT_ROOT) + if result.returncode != 0: + print(f"Build failed:\n{result.stderr}") + sys.exit(1) + else: + print(" C64_SKIP_BUILD=1 — reusing existing build artifacts") assert os.path.exists(PRG_PATH), f"{PRG_PATH} not found after build" print(f" Build OK: {PRG_PATH}") diff --git a/tools/uci/bench_ecdsa_u64e.py b/tools/uci/bench_ecdsa_u64e.py index 4e69b77..b5ff513 100644 --- a/tools/uci/bench_ecdsa_u64e.py +++ b/tools/uci/bench_ecdsa_u64e.py @@ -50,6 +50,7 @@ from pathlib import Path from c64_test_harness.backends.device_lock import DeviceLock +from c64_test_harness.labels import Labels from c64_test_harness.backends.ultimate64 import Ultimate64Transport from c64_test_harness.backends.ultimate64_client import Ultimate64Client from c64_test_harness.backends.ultimate64_helpers import ( @@ -97,14 +98,9 @@ def _load_labels() -> dict[str, int]: - labels: dict[str, int] = {} - for line in LABELS_PATH.read_text().splitlines(): - parts = line.split() - if len(parts) >= 3 and parts[0] == "al" and parts[2].startswith("."): - name = parts[2][1:] - _, hex_addr = parts[1].split(":", 1) - labels[name] = int(hex_addr, 16) - return labels + # c64-test-harness Labels is a Mapping since 0.12.4 (JC-000/c64-test-harness#64) + # and parses both C: and non-C (REU/bank) label lines since #62. + return dict(Labels.from_file(LABELS_PATH)) def _build_stub(labels: dict[str, int]) -> bytes: diff --git a/tools/uci/test_https_local.py b/tools/uci/test_https_local.py index 1440ced..c52434d 100644 --- a/tools/uci/test_https_local.py +++ b/tools/uci/test_https_local.py @@ -80,6 +80,7 @@ ) from c64_test_harness.uci_network import enable_uci, disable_uci from c64_test_harness.keyboard import send_text +from c64_test_harness.labels import Labels DEBUG_CAPTURE_ENABLED = os.environ.get("DEBUG_CAPTURE", "1") != "0" @@ -239,14 +240,9 @@ def _run_https_server(srv: socket.socket, ctx: ssl.SSLContext, def _load_labels() -> dict[str, int]: - labels: dict[str, int] = {} - for line in LABELS_PATH.read_text().splitlines(): - parts = line.split() - if len(parts) >= 3 and parts[0] == "al" and parts[2].startswith("."): - name = parts[2][1:] - _, hex_addr = parts[1].split(":", 1) - labels[name] = int(hex_addr, 16) - return labels + # c64-test-harness Labels is a Mapping since 0.12.4 (JC-000/c64-test-harness#64) + # and parses both C: and non-C (REU/bank) label lines since #62. + return dict(Labels.from_file(LABELS_PATH)) def _build_http_routine(labels: dict[str, int], port: int) -> tuple[bytes, int]: