Repository navigation
c64-https v0.6.0
For demonstration and educational purposes only. It is not cryptographically secure.
Built from the commit tagged v0.6.0. Submodule pins (tools/check_upstream_pins.py):
libs/mlkemv0.6.0 (new);libs/nistcurvesv0.15.0 (unchanged);libs/x25519v0.16.0 (unchanged);- ip65
25a9c5a(untagged, unchanged).
This release covers the merged PRs since v0.5.1: #303, #306-#308, #310, #311 (build, rig and docs fixes), #312-#315 (the post-quantum hybrid key exchange, below) and this release's packaging change. The main change: both UCI images now offer the hybrid key exchange X25519MLKEM768. They have completed fetches of the Wikipedia Commodore 64 article over it on hardware. ip65-onchip stays classic and is not byte-identical to v0.5.1 (see below).
No classic UCI image ships any more. uci-onchip and uci-comb are the same products as before with PQ_HYBRID=1 added to their make arguments. To build the v0.5.1-style X25519-only UCI image, omit it: make BACKEND=uci USE_NISTCURVES_ONCHIP=1 TRUST_STORE=1 (or USE_NISTCURVES_ONCHIP_COMB=1).
Assets
| image | for | PRG sha256 |
|---|---|---|
c64-https-ip65-onchip |
stock C64 + RR-Net, no REU, no turbo | e69bfce7b552e765df02cb253b4ff12cecaca3e978669376b7af5ffad9343dce |
c64-https-uci-onchip |
U64E / C64 Ultimate, turbo, no REU | 3b4dd718cbdc296f2083c90ce4d89a25e1fbd032e849c66f25227ef4d0ac4d5a |
c64-https-uci-comb |
U64E / C64 Ultimate, turbo + REU (fastest) | 41202de9fa6b9da769fce87a3223e7d1c74630f873b4b64a940afacddb7cbeb6 |
c64-https-uci-m3-demo |
U64E with the TLS-socket (ESP32) firmware, see below | 916d41628a17b9436bbf58a71f7350c721a06e459cf5c33e5baeb1ad61af9a24 (carried forward from v0.5.1, unchanged) |
| disk | sha256 |
|---|---|
c64-https-ip65-onchip.d64 |
30c5ba3974eed49a248e0041c1bf1e3e2e9584d772c710b086f50dc18a840e38 |
c64-https-uci-onchip.d64 |
70356cd1a7334c009ac37641d6cc7f4954550fa9d530e0ab913dda7a2b5d2302 |
c64-https-uci-comb.d64 |
293883c77a30364674425a17b3121a1adab08d21d4e2f7f8c72362e494916faa |
c64-https-uci-m3-demo.d64 |
ae2cc5b7d40190ddfc911cb40e7a7d62e406c134f304b3fe9e2f04ea540ff498 (carried forward) |
Each image ships as a .prg and as a bootable .d64. The full asset set is:
- the three
.prgand three.d64above; MANIFEST.txt,build-info.txt,c64-https-listener.py;- the M3 demo's
c64-https-uci-m3-demo.prg,c64-https-uci-m3-demo.d64,c64-https-uci-m3-demo-README.txtandc64-https-uci-m3-demo-SHA256SUMS(v0.5.1's files, unchanged); - a top-level
SHA256SUMScovering every other asset, generated bytools/package/write_release_sums.sh <assets-dir>(it refuses a missing file and verifies its own output).
The M3 demo's own checksum file names its README README.txt, the name inside the demo package; rename the asset to that before running shasum -c on it.
The 1541 filenames (ip65-noreu, uci-noreu, uci-comb) and the PRG sizes (47,105 B, 47,105 B, 51,201 B) are the same as v0.5.1.
Post-quantum hybrid key exchange
-
The ClientHello offers
X25519MLKEM768(0x11EC: ML-KEM-768 encapsulation key || X25519 share) first andX25519(0x001D) second; the server picks. A 0x11EC answer feedsmlkem_ss || x25519_ssinto the handshake secret (draft-ietf-tls-ecdhe-mlkem, ML-KEM first). A 0x001D answer completes with X25519 alone, as before. -
A server that picks X25519 does not get the v0.5.1 client. KeyGen still runs on every connection; the ClientHello is about 1.2 KB larger and is sent in several sections; and the ServerHello is read by a stricter streaming reader (an empty session-id echo, exactly the two extensions we asked for, a malformed or oversize record header while waiting for the ServerHello is now fatal).
-
ML-KEM-768 comes from
libs/mlkem(c64-mlkem v0.6.0). Its code is staged in the PRG at$A000and copied to$E000at boot; the polynomial area is$D000-$DFFF(RAM under I/O) and the decapsulation-key scratch is the first 2,400 B of the TCP ring. Intermediates are scrubbed after KeyGen and after Decaps, and both shared secrets are zeroed once the handshake secret exists.PQ_HYBRIDis refused on ip65, uci-m3 and the uci REU profile. -
Cost (U64E at 48 MHz, local OpenSSL 3.6.5 listener started with
-groups X25519for the classic image and-groups X25519MLKEM768for the PQ image (the group differs per column and cannot be avoided: the classic client offers no hybrid group), release arguments plusHTTPS_SNI, time from the SYS trigger to client Finished, 0.5 s poll, n=3 each; the classic image is the same arguments withoutPQ_HYBRID):image classic (PQ_HYBRID off) hybrid difference uci-onchip 52.4, 52.1, 52.6 s (mean 52.4) 55.2, 54.8, 54.6 s (mean 54.9) +2.5 s (+4.8%) uci-comb 41.0, 41.3, 40.6 s (mean 41.0) 43.9, 43.1, 43.4 s (mean 43.5) +2.5 s (+6.1%) Whole Wikipedia fetch with the release PRGs, rig wall-clock: 191-192 s onchip, 220-222 s comb; the classic comb control took 218 s (n=1).
-
What it protects, and what it does not. The hybrid changes the key exchange only: a passive recorder who later has a quantum computer cannot recover a session's keys from the recorded handshake unless it can also break X25519 or ML-KEM-768 (harvest now, decrypt later). Authentication is unchanged and classical: the server proves itself with ECDSA P-256, and the UCI images' only pin is trust-on-first-use of that leaf key. And the protection is only as good as its secrets: this client's DRBG is seeded from SID voice 3 and CIA timer noise, which is weak, and the ML-KEM seed and X25519 private key both come from it. Read this as a demonstration of the protocol path, not as quantum safety.
-
Servers: OpenSSL 3.6.5
s_serverand en.wikipedia.org negotiate 11EC with our hello (tables below); browserleaks.com did in an earlier live run. github.com and lwn.net do not offer 11EC (openssl s_client -groups X25519MLKEM768getshandshake_failure); auci-combfetch of github.com/robots.txt falls back to X25519 and completes.
Other changes since v0.5.1
- PQ stages, in order: the sectioned ClientHello with 16-bit lengths and HelloRetryRequest refusal (#312);
libs/mlkempinned and placed behindPQ_HYBRID=1, link-only (#313); the streaming hybrid ServerHello, KeyGen intls_connect, Decaps (#314); the hybrid key schedule (#315). - The linker output no longer depends on the build locale (#311), a linker-config edit relinks the PRG (#306), a bare pytest is green on a uci or an ip65 build (#308), and the rig trampolines set the trust-store mode (#307). Docs: #303 (where the UCI images keep the trust store), #310 (M3 demo provenance).
tools/uci/rig_https_banner.pyprintstls_kex_group;REQUIRE_KEX=ECmakes any other group part of the exit decision (decide_exit).
ip65-onchip is not the v0.5.1 binary
c64-https-ip65-onchip.prg is e69bfce7…; v0.5.1's was 810f1158…. Rebuilt at each step: 1c377d34… after #311 (link order only), then e69bfce7… from #312 on, unchanged by #313-#315. #312 changed ip65 code: it added the HelloRetryRequest check to the ip65 ServerHello parser and changed the shared ClientHello builder. It does not offer the hybrid.
Validated in emulation, not on silicon. The hardware-free ip65 end-to-end (tests/rig_vice_https_macos.py, E2E_PROFILE=onchip, E2E_NO_WARP=1) PASSED on these bytes: ethernet-capable VICE 3.10 x64sc (the patched pcap build at ~/opt/vice-eth/bin/x64sc) on a feth/pcap link with the rig's dnsmasq, a host TLS 1.3 listener on port 24530, honest 1 MHz, 2,328 s wall (DHCP OK at +35 s; after G: TCP connected +3 s, CH +475 s, SH +931 s, ServerHello processed (PROC) +950 s, last flight record received +2,266 s, server Finished +2,269 s, request sent +2,287 s, connection closed +2,293 s), http_status=200, body TLS13 OK FROM C64 TEST (22 B, as expected). The image under test was built with the exact release arguments plus the rig's port: make BACKEND=ip65 USE_NISTCURVES_ONCHIP=1 TRUST_RELEASE=1 HTTPS_PORT=24530, sha256 9b1ef4b1582242ef0820a33499c60f1f6a69db803f45de6431451fe34a4c7463. It differs from the release PRG e69bfce7… in exactly 4 bytes, the two HTTPS_PORT words (443 = $01BB vs 24530 = $5FD2, each stored twice); nothing else. What this does NOT cover: real RR-Net/cs8900a silicon (the physical rig was not run), server name validation (the ip65 image has none and accepts any certificate whose signature verifies), and anything faster than 1 MHz.
M3 demo
Carried forward from v0.5.1 unchanged (PRG, .d64, README, checksum file). The M3 build does not link the 6510 TLS or crypto code, and building BACKEND=uci-m3 HTTPS_HOST=en.wikipedia.org HTTPS_PATH=/wiki/Commodore_64 at this release reproduces the v0.5.1 PRG byte for byte (916d4162…), so the hardware validation recorded for it still describes these bytes. It was not re-run for this release, and tools/package/build_m3_demo.sh was not used: it would rewrite the README around this commit and re-quote firmware provenance that nobody re-verified.
Validated
Built on macOS 27.0.1, GNU Make 3.81, ca65/ld65 V2.18, LANG=en_US.UTF-8; the PRG hashes are locale-independent since #311. make package-verify passed 14/14 with no SKIP (PRG reproducibility from a second clean build, load ranges off the I/O page, .d64 read-back byte-compare and VICE boot to the banner for all three images, listener selftest).
Hardware, exact release PRGs (U64E, fw 3.15 branch build, 48 MHz, rig_https_banner.py: menu path, G, typed host en.wikipedia.org, path /wiki/Commodore_64; completeness by the response's own framing; REQUIRE_KEX=EC makes any other group a failure). The device's real /USB1/TRUST.A|B were used as users run it; no key-change prompt appeared. Every run:
| # | image (PRG sha256 prefix) | tls_kex_group | HTTP | body | elapsed |
|---|---|---|---|---|---|
| 1 | uci-comb (41202de9) |
$EC | 200 | 760,609 B == Content-Length, PASS | 220 s |
| 2 | uci-comb (41202de9) |
$EC | 200 | 760,609 B == Content-Length, PASS | 222 s |
| 3 | uci-comb (41202de9) |
$EC | 200 | 760,609 B == Content-Length, PASS | 222 s |
| 4 | uci-onchip (3b4dd718), device REU enabled (16 MB) |
$EC | 200 | 760,609 B == Content-Length, PASS | 191 s |
| 5 | uci-onchip (3b4dd718), device REU enabled (16 MB) |
$EC | 200 | 760,609 B == Content-Length, PASS | 192 s |
| 6 | uci-onchip (3b4dd718), device REU Disabled |
$EC | 200 | 760,609 B == Content-Length, PASS | 190 s |
| control | BACKEND=uci USE_NISTCURVES_ONCHIP_COMB=1 TRUST_STORE=1 TRUST_RELEASE=1 on this tree, PQ_HYBRID off (fee4d5b1) |
n/a | 200 | 760,609 B == Content-Length, PASS | 218 s |
| fallback | uci-comb (41202de9), github.com /robots.txt |
$1D | 200 | 14,489 B == Content-Length, PASS | 101 s |
All 8 runs completed; no truncation was seen. The silent-truncation defect #211 was closed on 2026-09-24 (PR #238); these runs are evidence for that fix on the PQ images, not a statement that it is intermittent today. The article is 760,609 B now (759,171 B at v0.5.1). Elapsed is the whole rig run including boot (comb adds its ~45 s table build).
Local OpenSSL 3.6.5 listener (tools/uci/rig_https_local.py, EXTERNAL_LISTENER=1; it dials the listener's IP, so a release PRG cannot pass the name check, and the images were built with the release arguments plus HTTPS_SNI=www.foo.invalid: uci-onchip 46398f0b…, uci-comb fdb7386e…):
| image | server -groups |
tls_kex_group | result | oracle (C64 application traffic secrets vs the server's keylog) |
|---|---|---|---|---|
| uci-onchip | X25519MLKEM768 | $EC | full HTTPS PASS | MATCH |
| uci-onchip | X25519 | $1D | full HTTPS PASS | n/a (not hybrid) |
| uci-comb | X25519MLKEM768 | $EC | full HTTPS PASS | MATCH |
| uci-comb | X25519 | $1D | full HTTPS PASS | n/a (not hybrid) |
boot_check.py passed on the release uci-onchip and uci-comb PRGs.
Trust store on the PQ images (the store stages in the TCP ring, which now also hosts the ML-KEM dk scratch). The rigs refuse any directory but /<volume>/c64https-test, so they ran on the release arguments plus TRUST_STORE_DIR=/USB1/c64https-test HTTPS_PORT=24520 (uci-onchip a0d26e3d…, uci-comb e8adb55d…); the real /USB1/TRUST.A|B (80 B / 144 B) were unchanged afterwards:
rig_trust_store.py(disk I/O: empty, save, load, torn write, corruption, 32 records, missing directory): exit 0 on both, 24 and 25 checks, 0 failures; phase 13 runs only at 1 MHz and was not run.rig_trust_policy.py, local mode, through the menu against a Python TLS 1.3 listener (it can only choose X25519, so these fetches took 0x001D): exit 0 on both, 30 passed, 0 failed each: first use records the key (KEY RECORDED), a matching key isKNOWN KEYwith the store untouched, a changed key showsKEY CHANGEDand the full hash and sends no client Finished, accept-on-retry recordsACCEPTED/K2, andYat the prompt runs oneUNPINNEDfetch that records nothing. Live mode (RIG_MODE=live) needs the REU viewer demo, which does not link with PQ (below), so it was not run. Disclosure: the first-use and changed-key trust paths were therefore exercised on the PQ images with X25519 fetches only (KeyGen and the dk scratch in the ring still ran on every connect); with an 11EC server only the known-key path has been run, in the Wikipedia runs above.
Memory (tools/measure_margins.py --build, the exact release arguments, free bytes): uci-onchip LOADER 157, NET_CODE 105, CRYPTO_OVERLAY 372, CRYPTO_HOT 196; uci-comb LOADER 342, NET_CODE 134, CRYPTO_OVERLAY 55, CRYPTO_HOT 379, COLD_IMAGE 1,697, COLD_RUN_TRUST 75.
Suites, on this tree: BACKEND=uci python3 tools/run_all_tests.py 455/455 (the net and ip65_exports suites are accounted skips: they test the ip65 adapter). That aggregate runs the non-PQ REU profile; no VICE suite runs the release images, which are covered by make package-verify's VICE boot, the hardware runs above, and the PQ suites (test_pq_*, test_mlkem_*, 72 mutants, per the review of #315). Bare python3 -m pytest -q 510 passed.
What this release does NOT do
- No certificate chain validation on the three 6510 images (unchanged). The released UCI images pin by trust-on-first-use (
TRUST.A/TRUST.Bon/USB1); a build-time pin or a signed bundle needs a custom build. If the store cannot be used, they offer to continue unpinned. The ip65 image accepts any certificate whose signature verifies. On the M3 demo, the ESP32 verifies certificates against its own Mozilla CA store. - Server name validation is UCI-only.
- Entropy is weak (see above).
- The Wikipedia REU viewer demo does not link with
PQ_HYBRID=1.make BACKEND=uci USE_NISTCURVES_ONCHIP_COMB=1 PQ_HYBRID=1 HTTPS_HOST=en.wikipedia.org HTTPS_PATH=/wiki/Commodore_64 HTTPS_BODY_TO_REU=1overflows CRYPTO_HOT by 507 B. Build the demo withoutPQ_HYBRID. - A hybrid ServerHello sent as one record cannot be read (the record buffer is 548 B); the client refuses it at once, leaving
tls_sh_err=$0E(not shown on screen). Servers that honour max_fragment_length (OpenSSL, Wikipedia, browserleaks) split it. Front ends that ignore it, such as Cloudflare's and BoringSSL-based ones, are out of scope (Cloudflare also enforces a ~15 s deadline). ip65-onchipwas validated in VICE only (one e2e PASS at honest 1 MHz, see above); not on real RR-Net silicon at this release, and it has no server name validation.- The C64 Ultimate was not validated for this release. The last C64U run (v0.5.1,
uci-onchipagainst Wikipedia at 64 MHz) did not reach a connection:- Its trust store could not be used, so the image printed
TRUST FAIL 02and askedCONTINUE UNPINNED? Y/N. The UCI images keep their trust store (TRUST.A/TRUST.B) on/USB1, and that C64 Ultimate has no/USB1volume. That fits, but the cause was not investigated. - The path the rig typed arrived as
/wiki/.ommodore_64. - No socket was opened ("NOT DIALLED").
- Its trust store could not be used, so the image printed