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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 69 additions & 45 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -496,7 +496,7 @@ The scripts:
`EXTERNAL_PORT`, default 4433) skips the
inline listener + repo-cert load and points
the C64 at an out-of-band server — e.g. the
packaged `dist/c64-https-listener.zip`
packaged `dist/c64-https-listener.py`
listener; pass criteria then come from
C64-side state only. Default OFF.
Each run writes a timestamped artifact dir
Expand DownExpand Up@@ -1350,50 +1350,74 @@ ld65 and ca65 edge cases; they are intentional and should stay:

## Packaging

`make package` builds the release artifacts into `dist/` (gitignored):

- `c64-https-uci-reu.prg` — default REU profile (`make BACKEND=uci`).
Requires REU hardware/enabled; fastest
at stock 1 MHz (the REU profile is the
right default below ~7 MHz).
- `c64-https-uci-onchip.prg` — `USE_NISTCURVES_ONCHIP=1`. **No REU
required** — for stock machines without
an REU (~3.9x the verify CPU work; at
1 MHz expect ~23 min for the ECDSA
verify alone).
- `c64-https.d64` — both PRGs on one 1541 image
(`HTTPS-REU`, `HTTPS-NOREU`), built
with VICE's `c1541`.
- `c64-https-listener.zip` — self-contained Python TLS 1.3 test
listener (source: `tools/package/
listener/`): `run.sh` creates a venv,
installs `cryptography`, **generates
fresh P-256 certs** (`gen_certs.py`),
and serves the canonical response.
Requires an OpenSSL 1.1.1+/3.x python
(refuses LibreSSL, e.g. macOS system
python, with a clear error).
- `MANIFEST.txt` — sizes, git HEAD, sha256 checksums.

Scripts live in `tools/package/` (`build_prgs.sh`, `build_d64.sh`,
`build_listener_zip.sh`); each variant build does `make clean` first
(flag changes are not tracked by make). Builds are deterministic —
`make package` reproduces the validated hashes at the same HEAD.

**ip65 is not packaged yet, but it now LINKS.** The historical blocker
(`BSS overflows CRYPTO_COLD_SHADOW by 1406 bytes`) was closed by the
#68 refit — the overflow was exactly `LIB_NISTCURVES_P256_BSS`, which
now time-shares `cert_buf`'s RAM via the `SCRATCH_UNION` region (their
lifetimes are disjoint; see the cfg comment block and the lifetime
contract at `cert_buf` in `src/der_decode.s`). Both ip65 profiles
build, and the REU-less ip65+onchip image is validated end-to-end in
VICE (see "ip65 / stock-C64 wall-clock"). Adding it to `make package`
is a live option — it is the only artifact that serves a stock C64 +
RR-Net cartridge, which today has no shipped PRG at all. Note
c64-nist-curves#54 is CLOSED-as-completed and already inside our pin,
so it is not headroom in reserve. The comb profile stays deliberately excluded (REU
bank 2 residency + ~40 min boot precompute at 1 MHz make it wrong for
a general release).
`make package` builds the release artifacts into `dist/` (gitignored).
**All four backend x profile combinations ship**, `make clean` between
every one:

- `c64-https-uci-reu.prg` `make BACKEND=uci`
- `c64-https-uci-onchip.prg` `+ USE_NISTCURVES_ONCHIP=1`
- `c64-https-ip65-reu.prg` `make BACKEND=ip65`
- `c64-https-ip65-onchip.prg` `+ USE_NISTCURVES_ONCHIP=1`

ip65 is now packaged (it was previously excluded on a link failure that
the #68 refit closed) — it is the only artifact that serves a stock C64
+ RR-Net cartridge, and `ip65-onchip` is the only image a bone-stock
machine with no REU can run at all.

The REU-vs-onchip guidance in `MANIFEST.txt` is the measured **~18 MHz**
crossover, not the older ~7 MHz figure: the REU profile carries a
wall-clock floor (DMA anchored to the ~1 MHz bus) that turbo cannot
touch, the onchip profile has none, and on a U64E the sign flips between
the 16 and 20 MHz CPU-speed settings. See the ECDSA wall-clock section.

Disk images:

- `c64-https-<variant>.d64` x4 — one PRG each, `LOAD"*",8,1`
- `c64-https-uci.d64`, `c64-https-ip65.d64` — both of that backend's
profiles on one disk

There is deliberately **no all-in-one image**: the four PRGs total 868
blocks against a .d64's 664 free. Each backend's pair does fit (UCI 496,
ip65 372), which makes the per-backend disk the largest useful bundle.

`c64-https-listener.py` is a **single self-extracting Python file** (was
a zip + `run.sh` + venv + pip). It has **no third-party dependency at
all**: `cryptography` was only ever used to mint the self-signed P-256
cert, and `tools/package/listener/gen_certs.py` now does that in pure
Python (P-256 point arithmetic + minimal DER encoder + ECDSA-SHA256).
TLS was always stdlib `ssl`. What remains is a property of the
*interpreter*, not an installable package — an `ssl` with TLS 1.3
(OpenSSL 1.1.1+); macOS's `/usr/bin/python3` is LibreSSL 2.8.3 and
cannot serve this client at any price. That is detected at startup and
reported in one line (never a traceback, `--debug` restores it), and it
is stated in `MANIFEST.txt` rather than left to be discovered.
`--selftest` proves the whole path with no C64: mint cert, serve on
loopback, drive it with a Python `ssl` client, then again with `openssl
s_client -ciphersuites TLS_CHACHA20_POLY1305_SHA256` — the C64's only
suite, which the stdlib client can never force because CPython exposes
no API to restrict TLS 1.3 suites.

Scripts live in `tools/package/`: `_common.sh` (the variant matrix — one
line per shipped PRG, every other script derives from it),
`build_prgs.sh`, `build_d64.sh`, `build_listener.py`, `write_manifest.sh`.
Nothing is version-specific; re-running `make package` after a submodule
bump regenerates every artifact with zero edits.

`make package-verify` is the acceptance gate (`tools/package/
verify_release.py`): rebuilds every variant and compares **PRG** hashes
(object hashes are not evidence — ca65 stamps build time into every
`.o`), reads each PRG back out of its .d64 with `c1541` and
byte-compares, boots every image in VICE asserting the banner, and runs
the listener selftest. `SKIP_REBUILD` / `SKIP_VICE` / `SKIP_LISTENER`
narrow it.

Booting a .d64 in VICE needs `-trapdevice8 +drive8truedrive`: under true
drive emulation the ~250-block load never completes inside any sane
budget, and the symptom is a screen stuck on `LOADING` that looks like a
bad image rather than a slow one. `verify_release.py` passes both flags.

The comb profile stays deliberately excluded (REU bank 2 residency +
~40 min boot precompute at 1 MHz make it wrong for a general release).

Validation record (2026-07-27, HEAD cb6eab4):
- onchip PRG passes the 3-vector ECDSA KAT in VICE **without** REU
Expand Down
35 changes: 23 additions & 12 deletions Makefile
Original file line numberDiff line numberDiff line change
Expand Up@@ -231,7 +231,7 @@ ALL_OBJS := $(TOP_OBJS) $(CRYPTO_OBJS) $(CRYPTO_SHARED_OBJS) $(NET_OBJS)
PRG := build/c64-https.prg
LABELS := build/labels.txt

.PHONY: all link run clean ip65-libs ip65-blob package
.PHONY: all link run clean ip65-libs ip65-blob package package-verify

all: $(PRG)

Expand DownExpand Up@@ -443,17 +443,28 @@ run: $(PRG)
clean:
rm -rf build

# Release packaging: build the PRG matrix, bundle both UCI PRGs onto a D64,
# and write dist/MANIFEST.txt (sizes, git HEAD, sha256 checksums). The scripts
# run `make clean` between flag combinations themselves, so `package` does not
# depend on any build artifact. build_listener_zip.sh is skipped gracefully
# when absent so a partial checkout can still package the PRGs.
# Release packaging: build all four PRG variants (both backends x both crypto
# profiles), put each on its own D64 plus a per-backend D64, generate the
# single-file test listener, and write dist/MANIFEST.txt.
#
# The scripts run `make clean` between flag combinations themselves, so
# `package` deliberately depends on no build artifact — and re-running it after
# a submodule bump regenerates everything with no edits anywhere. Order
# matters: build_prgs.sh writes dist/build-info.txt and build_d64.sh writes
# dist/d64-listings.txt, both of which write_manifest.sh consumes.
#
# PACKAGE_PYTHON must be an interpreter that can run the listener's own
# selftest — see `make package-verify`.
PACKAGE_PYTHON ?= python3
package:
bash tools/package/build_prgs.sh
@if [ -x tools/package/build_listener_zip.sh ]; then \
echo "[package] running tools/package/build_listener_zip.sh"; \
bash tools/package/build_listener_zip.sh; \
else \
echo "[package] tools/package/build_listener_zip.sh absent — skipping listener zip"; \
fi
bash tools/package/build_d64.sh
$(PACKAGE_PYTHON) tools/package/build_listener.py
bash tools/package/write_manifest.sh

# Acceptance gate for the release artifacts: rebuild every PRG a second time
# and compare PRG hashes, boot every D64 in VICE and assert the banner, and run
# the built listener end to end against a Python ssl client. Measures; does not
# assert. Run it after `make package`.
package-verify:
$(PACKAGE_PYTHON) tools/package/verify_release.py
25 changes: 25 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,31 @@ An HTTPS client for the Commodore 64 in 6502 assembly. Implements TLS 1.3 over T

**For demonstration and educational purposes only — not cryptographically secure.**

## I just want to run it

Grab a release: every build is prebuilt, as a `.prg` and as a bootable `.d64`.
No assembler, no cc65, no Python packages, no build step. Two questions pick
your image, and `MANIFEST.txt` in the release walks through them:

| | REU present | no REU |
|---|---|---|
| **Ultimate 64 / C64 Ultimate** | `c64-https-uci-reu` | `c64-https-uci-onchip` |
| **stock C64 + RR-Net** | `c64-https-ip65-reu` | `c64-https-ip65-onchip` |

The `reu` images are faster below roughly 18 MHz — which is every real stock
C64 — because they offload the ECDSA verify to REU DMA. The `onchip` images
need no REU at all and win above that crossover, so they are the right pick
for Ultimate turbo modes. `ip65-onchip` is the only image a bone-stock
machine with no expansion RAM can run end to end.

`c64-https-listener.py` in the same release is a single self-extracting file
that stands up the server side to point the C64 at: it mints its own
certificate and needs nothing installed, only a `python3` whose `ssl` has
TLS 1.3. Run `python3 c64-https-listener.py --selftest` to check that before
involving a C64.

To build these yourself: `make package && make package-verify`.

## Architecture

```
Expand Down
73 changes: 73 additions & 0 deletions tools/package/_common.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
# =============================================================================
# tools/package/_common.sh — shared definitions for the release packaging
# scripts. Sourced, never executed.
#
# The single source of truth for the release variant matrix lives here so that
# build_prgs.sh, build_d64.sh and write_manifest.sh cannot drift apart.
# =============================================================================

# --- Variant matrix -----------------------------------------------------------
# One line per shipped PRG:
# <key>|<prg basename>|<make args>|<1541 filename>|<backend>|<one-line guidance>
#
# 1541 filenames are <=16 chars and lowercase here because c1541 uppercases
# into PETSCII on write. They are the same on the single-variant disk and on
# the per-backend disk, so a user only ever learns one name. Keep them stable
# across releases — people type them.
#
# Nothing here is version-specific: adding a profile or a backend is one line,
# and every downstream script picks it up with no further edits.
PACKAGE_VARIANTS=(
"uci-reu|c64-https-uci-reu.prg|BACKEND=uci|uci-reu|uci|Ultimate 64 / C64 Ultimate (UCI networking). Needs the REU enabled. Faster below ~18 MHz — the right pick at stock 1 MHz."
"uci-onchip|c64-https-uci-onchip.prg|BACKEND=uci USE_NISTCURVES_ONCHIP=1|uci-noreu|uci|Ultimate 64 / C64 Ultimate (UCI networking). No REU required. Faster above ~18 MHz — the right pick at 32/48/64 MHz turbo."
"ip65-reu|c64-https-ip65-reu.prg|BACKEND=ip65|ip65-reu|ip65|Stock C64 + RR-Net / cs8900a cartridge. Needs an REU. Faster below ~18 MHz, i.e. at any speed a real stock C64 runs at."
"ip65-onchip|c64-https-ip65-onchip.prg|BACKEND=ip65 USE_NISTCURVES_ONCHIP=1|ip65-noreu|ip65|Stock C64 + RR-Net / cs8900a cartridge, no REU at all. The only image a bone-stock C64 can run end to end; slowest (~36 min per handshake at 1 MHz)."
)

# Backends, in matrix order, deduplicated. Used for the per-backend disks.
package_backends() {
local line
for line in "${PACKAGE_VARIANTS[@]}"; do
printf '%s\n' "$(variant_field "$line" 5)"
done | awk 'NF && !seen[$0]++'
}

# Field accessors — `variant_field <line> <1-based index>`.
variant_field() { printf '%s' "$1" | cut -d'|' -f"$2"; }

# --- Paths --------------------------------------------------------------------
# PROJECT_ROOT must be set by the caller before sourcing (it knows its own $0).
: "${PROJECT_ROOT:?_common.sh: PROJECT_ROOT must be set before sourcing}"

DIST="$PROJECT_ROOT/dist"
BUILT_PRG="$PROJECT_ROOT/build/c64-https.prg"
BUILD_INFO="$DIST/build-info.txt" # machine-readable; write_manifest.sh reads it
MANIFEST="$DIST/MANIFEST.txt"

# --- sha256, portably ---------------------------------------------------------
if command -v sha256sum >/dev/null 2>&1; then
sha256_of() { sha256sum "$1" | cut -d' ' -f1; }
else
sha256_of() { shasum -a 256 "$1" | cut -d' ' -f1; }
fi

# --- Submodule pins (offline) -------------------------------------------------
# Deliberately NOT `git submodule status`: it renders versions via `git
# describe` *without* `--tags`, so a lightweight tag (c64-x25519 v0.6.0 is one)
# is invisible and the pin reads as "5 commits past v0.5.0". Read the gitlink
# from the tree and resolve the tag inside the submodule with --tags.
# Prints "<path> <sha> <tag-or-(untagged)>" per submodule.
submodule_pins() {
git -C "$PROJECT_ROOT" config --file .gitmodules \
--get-regexp '^submodule\..*\.path$' 2>/dev/null \
| awk '{print $2}' | sort | while read -r sub; do
local sha tag
sha="$(git -C "$PROJECT_ROOT" ls-tree HEAD "$sub" | awk '{print $3}')"
[ -n "$sha" ] || continue
tag="$(git -C "$PROJECT_ROOT/$sub" describe --tags --exact-match "$sha" \
2>/dev/null || true)"
[ -n "$tag" ] || tag="(untagged)"
printf '%s %s %s\n' "$sub" "$sha" "$tag"
done
}
Loading