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
35 changes: 28 additions & 7 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -163,7 +163,8 @@ to the ip65 layout.

### UCI test scripts

Scripts under `tools/uci/` require a U64E at 192.168.1.81 and use
Scripts under `tools/uci/` require a U64E (default 192.168.1.81,
overridable via the `U64_HOST` environment variable) and use
`DeviceLock` + `enable_uci`/`disable_uci`:

- `boot_check.py` — verify UCI firmware detection and boot banner
Expand All@@ -175,10 +176,15 @@ Scripts under `tools/uci/` require a U64E at 192.168.1.81 and use
- `test_https_local.py` — HTTPS e2e scaffolding against a local TLS 1.3
listener (ECDSA-P256 cert from
`tools/https_e2e/certs/`). DMAs a 6502 stub
that calls `http_get`, flips the U64E to 48
MHz turbo, and captures full diagnostics on
pass or timeout. `DEBUG_CAPTURE=1` enables a
bounded 6510 bus stream for post-mortem.
that calls `http_get`, flips the U64E to the
CPU speed selected by `TURBO_MHZ` (default
48; `TURBO_MHZ=1` runs at stock 1 MHz with
all wall-clock budgets auto-scaled by 48x
and has been validated end-to-end on real
U64E hardware), and captures full
diagnostics on pass or timeout.
`DEBUG_CAPTURE=1` enables a bounded 6510
bus stream for post-mortem.
Each run writes a timestamped artifact dir
under `$UCI_DEBUG_DIR` (default
`/tmp/uci_https_debug/<ISO>/`) containing:
Expand All@@ -199,8 +205,11 @@ Scripts under `tools/uci/` require a U64E at 192.168.1.81 and use
### End-to-end HTTPS status

The TLS 1.3 handshake now completes end-to-end against the local test
listener (ECDSA-P256 cert, `tools/https_e2e/certs/`). The flow that
works on real U64E hardware at 48 MHz turbo:
listener (ECDSA-P256 cert, `tools/https_e2e/certs/`) on **both** backends:
UCI/U64E at 48 MHz turbo and stock 1 MHz, and ip65/VICE at stock 1 MHz
no-WARP (after the 255-byte TCP RX clamp fix in `src/net/ip65/net.s`;
see `tests/test_phase3_https_1mhz.py`). The flow, identical across both
backends:

- ClientHello → ServerHello (X25519 key share)
- EncryptedExtensions, Certificate, CertificateVerify (ECDSA-P256
Expand DownExpand Up@@ -242,6 +251,14 @@ Five latent bugs and three new ones were cleared to get here:
9. `http_recv_response` state transitions fall through instead of
returning @not_done, so one dispatch walks status + headers +
body when they all fit in a single TLS record (`fbc7d10`).
10. ip65 TCP RX callback 255-byte clamp removed (PR #27). The old
callback truncated `cb_remaining` to 255 when the high byte was
non-zero while ip65 ACKed the full `tcp_inbound_data_length`,
silently dropping bytes 256+. Any TLS record &gt;255 B (Certificate
in particular, ~369 B in the local listener setup) got partially
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.

### Known issues

Expand All@@ -259,6 +276,10 @@ Five latent bugs and three new ones were cleared to get here:
behavior. Under UCI it says "ULTIMATE 64 ELITE (UCI)".
- The delay-loop fence adds ~2.5 ms overhead per UCI register access
at 1 MHz (negligible for networking, but visible in tight loops).
- `http_resp_buf` is rendered as raw ASCII on the C64 screen instead
of being translated to screen codes, so the response body displays
as graphics characters. Response bytes themselves are correct —
purely cosmetic. Tracked as issue #28.

### ECDSA P-256 verify wall-clock

Expand Down
21 changes: 21 additions & 0 deletions LICENSE
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 JC-000

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
37 changes: 27 additions & 10 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,11 +18,11 @@ An HTTPS client for the Commodore 64 in 6502 assembly. Implements TLS 1.3 over T
│ │ Record Layer │ Handshake Proto │ │ tls_record.asm, tls_handshake.asm
│ └──────┬───────┴────────┬─────────┘ │
│ │ │ │
│ ┌──────┴───────┐ ┌─────┴──────────┐
│ │ AEAD │ │ Key Schedule │ │ (crypto modules)
│ │ ChaCha20- │ │ HKDF-SHA256 │ │ hkdf.asm
│ │ Poly1305 │ │ ECDHE P-256 │ │
│ └──────────────┘ └────────────────┘ │
│ ┌──────┴───────┐ ┌─────┴─────────
│ │ AEAD │ │ Key Schedule │ │ (crypto modules)
│ │ ChaCha20- │ │ HKDF-SHA256 │ │ hkdf.asm
│ │ Poly1305 │ │ ECDHE P-256 │
│ └──────────────┘ └────────────────┘
├─────────────────────────────────────────┤
│ Network ABI (src/net_abi.inc) │ net_init / net_tcp_* / net_dns_*
├──────────────────────┬──────────────────┤
Expand DownExpand Up@@ -114,7 +114,12 @@ The Makefile automatically builds ip65 from the submodule into a flat binary blo

## Project Status

Current status (40 KB binary, 537 labels):
Current status:

- 38 KB binary (ip65 build), 1738 labels
- 38 KB binary (uci build), 1816 labels

Progress:

- [x] Project structure and build system
- [x] ip65 submodule integration — 6.8 KB binary blob at $2000 (TCP/UDP/DNS/DHCP/ARP + RR-Net CS8900a)
Expand All@@ -131,11 +136,13 @@ Current status (40 KB binary, 537 labels):
- [x] X.509 certificate parsing — DER parser extracts TBS, public key, signature (r,s), curve ID for P-256 and P-384
- [x] ECDSA signature verification — P-256 and P-384, full verify (s⁻¹, scalar mul, point add, Jacobian→affine)
- [x] HTTP/1.1 GET request — build GET, parse response (status + headers + body), plain HTTP end-to-end
- [x] **End-to-end HTTPS GET demo (UCI backend)** — TLS 1.3 handshake + HTTP GET completes against a local Python TLS listener (ECDSA-P256 cert) on real Ultimate 64 Elite hardware at 48 MHz turbo. Returns `http_status=200`, body `"HELLO FROM TLS SERVER"`. See `tools/uci/test_https_local.py`. The equivalent end-to-end HTTPS path over VICE/ip65 is not yet proven — only DHCP and plain HTTP currently pass on the bridge rig (see End-to-End Bridge Tests below).
- [x] **End-to-end HTTPS GET demo (both backends)** — TLS 1.3 handshake + HTTP GET completes against a local Python TLS listener (ECDSA-P256 cert). Returns `http_status=200`, body `"HELLO FROM TLS SERVER"`.
- UCI: real Ultimate 64 Elite hardware at both 48 MHz turbo and stock 1 MHz. See `tools/uci/test_https_local.py` (supports `TURBO_MHZ` env var).
- ip65: VICE + RR-Net + bridge rig at stock 1 MHz (no WARP). See `tests/test_phase3_https_1mhz.py`. Wall-clock ~2-3 h end-to-end, dominated by ECDSA-P256 verify.

### Known Issues

- **ECDSA P-256 verify** runs ~85 s/op on the U64E at 48 MHz turbo (UCI backend). Sufficient for the local listener (which holds the connection open), but exceeds typical 10-30 s real-world server handshake windows. Real-internet ECDSA-P256 targets need a sibling-style optimized P-256 implementation (parallel to the `c64-x25519` work). The ip65/VICE path has not been measured end-to-end because HTTPS over the bridge rig is not yet proven.
- **ECDSA P-256 verify** runs ~85 s/op on the U64E at 48 MHz turbo (UCI backend) and scales to ~60-70 min at stock 1 MHz (both backends). Sufficient for the local listener (which holds the connection open), but exceeds typical 10-30 s real-world server handshake windows. Real-internet ECDSA-P256 targets need a sibling-style optimized P-256 implementation (parallel to the `c64-x25519` work).
- **P-384** ECDSA is currently stubbed (both backends). Cert chains requiring P-384 will not verify until restored.
- **Live internet HTTP GET (UCI backend)** has not been re-verified since the FPGA-fence rework; only the local multi-segment listener is exercised regularly.
- **VICE 3.9** previously appeared to crash on chained HMAC-SHA256 calls (backend-independent — affects the crypto-only test suites), but this was caused by hardcoded port numbers bypassing the test harness port allocator. With proper `ViceInstanceManager` usage (no hardcoded ports), all N=1..10 chained calls succeed reliably.
Expand DownExpand Up@@ -180,7 +187,7 @@ sudo PYTHONPATH=tools python3 tests/test_phase2_http.py # Plain HTTP GET over

### End-to-End Bridge Tests (ip65 backend)

Full end-to-end tests that drive the real c64-https binary in VICE over a Linux bridge with RR-Net ethernet (the same pattern used by [`c64-test-harness` bridge networking](../c64-test-harness/docs/bridge_networking.md)). These exercise the **ip65/RR-Net path only** — HTTPS end-to-end on this rig is not yet proven; only DHCP (phase1) and plain HTTP (phase2) pass today. VICE runs at **normal speed** (warp breaks RR-Net DHCP), so these tests need generous timeouts (~90-120s per phase).
Full end-to-end tests that drive the real c64-https binary in VICE over a Linux bridge with RR-Net ethernet (the same pattern used by [`c64-test-harness` bridge networking](../c64-test-harness/docs/bridge_networking.md)). These exercise the **ip65/RR-Net path only**. All three phases pass: DHCP (phase1), plain HTTP (phase2), and HTTPS (phase3 via `tests/test_phase3_https_1mhz.py`). VICE runs at **normal speed** (warp breaks RR-Net DHCP), so these tests need generous timeouts (~90-120s per phase; the phase3 HTTPS run is ~2-3 h at 1 MHz).

**Setup:**

Expand All@@ -205,7 +212,7 @@ The setup script creates `br-c64` with `tap-c64-0`/`tap-c64-1`, assigns `10.0.65

### Ultimate 64 Elite Hardware Tests (UCI backend)

Scripts under `tools/uci/` drive a real Ultimate 64 Elite over the network (default `192.168.1.81`), exercising the **UCI backend only** (built with `make BACKEND=uci`). They DMA the PRG into RAM, run the boot, and snapshot UCI/TLS state on completion or timeout. These scripts do not run under VICE.
Scripts under `tools/uci/` drive a real Ultimate 64 Elite over the network (default `192.168.1.81`, overridable via the `U64_HOST` environment variable), exercising the **UCI backend only** (built with `make BACKEND=uci`). They DMA the PRG into RAM, run the boot, and snapshot UCI/TLS state on completion or timeout. These scripts do not run under VICE.

```bash
python3 tools/uci/boot_check.py # UCI firmware detection
Expand All@@ -217,6 +224,16 @@ python3 tools/uci/test_https_local.py # HTTPS GET (TLS 1.3 + ECDSA-P256)

`test_https_local.py` is the end-to-end HTTPS demo (UCI backend only): it boots the U64E at 48 MHz turbo, connects to a local Python TLS listener using the test cert under `tools/https_e2e/certs/`, and confirms a full TLS 1.3 handshake + HTTP GET. With `DEBUG_CAPTURE=1`, each run writes a timestamped artifact directory under `$UCI_DEBUG_DIR` (default `/tmp/uci_https_debug/`) with raw 6510 bus trace, TLS state snapshot, and listener result.

Environment variables honored by `test_https_local.py`:

- `U64_HOST` (default `192.168.1.81`) — U64E address
- `TURBO_MHZ` (default `48`) — C64 CPU speed. `TURBO_MHZ=1` runs the test at stock 1 MHz with every wall-clock budget auto-scaled by 48x (~2-3 h total, validated end-to-end on real U64E hardware).
- `HTTPS_PORT` (default `443`, falls back to `4433` if the bind fails)
- `SENTINEL_POLL_TIMEOUT`, `ACCEPT_TIMEOUT` — per-test overrides in seconds; default to `600 * (48 / TURBO_MHZ)`.
- `DEBUG_CAPTURE` (default `1`) — set to `0` to disable the bounded 6510 bus stream.
- `KEEP_DEBUG_ON_PASS` (default `0`) — set to `1` to preserve artifacts on PASS runs.
- `UCI_DEBUG_DIR` (default `/tmp/uci_https_debug`) — base directory for run artifacts.

## Related Projects

- [c64-aes256-ecdsa](../c64-aes256-ecdsa) — AES-256, SHA-256, ECDSA P-256, HMAC-DRBG
Expand Down
41 changes: 24 additions & 17 deletions src/net/ip65/net.s
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,9 +10,15 @@
; The callback must NOT touch crypto state — it only copies received data
; into tcp_recv_buf (a ring buffer) for later processing by the TLS layer.
;
; The d973531 fix (clamp cb_remaining to 255 per callback invocation) is
; preserved verbatim. The ZP $02-$1B save/restore around every ip65 call
; is load-bearing — do not remove.
; The ZP $02-$1B save/restore around every ip65 call is load-bearing —
; do not remove.
;
; The former d973531 clamp (cb_remaining -> 255 bytes per callback) was
; a data-loss bug: ip65 ACKs the full tcp_inbound_data_length regardless
; of how many bytes the callback consumes, so any byte past #255 was
; silently dropped. TLS records larger than 255 B (e.g. a Certificate
; record) got corrupted. The callback now copies the full 16-bit length
; and advances the SMC source high-byte when the 8-bit X index wraps.

.include "constants.inc"
.include "ip65_symbols.inc"
Expand DownExpand Up@@ -316,8 +322,11 @@ net_recv_byte:
; SMC instructions below so we can read those ip65 variables using absolute
; addressing (no ZP indirection needed).
;
; d973531 fix (preserved): cb_remaining is clamped to 255 bytes per callback
; invocation so the 8-bit X index cannot wrap and re-read the inbound buffer.
; 16-bit copy: cb_remaining carries the full inbound length (up to 1460 B
; for a full MSS segment). The inner loop uses X as an 8-bit source index
; and the SMC source base (cb_copy_byte+1/+2). When X wraps 0->0 (256 B
; consumed) we advance the high byte of the SMC source so successive
; 256-byte windows of the inbound buffer are copied correctly.
; =============================================================================
net_tcp_recv_cb:
; --- Read inbound data length (16-bit) ---
Expand All@@ -340,17 +349,10 @@ cb_load_ptr_hi:
lda $ffff ; SMC: patched to addr of tcp_inbound_data_ptr+1
sta cb_copy_byte+2 ; patch high byte of LDA abs,x source

; Clamp cb_remaining to 255 bytes max per callback to prevent
; 8-bit X-index wrap which would re-read source byte 0 onwards
; and overwrite previously-copied ring bytes. (d973531)
lda cb_remaining+1
beq :+
lda #255
sta cb_remaining
lda #0
sta cb_remaining+1
:
; Copy loop: X = source index; ring store uses SMC on cb_store
; Copy loop: X = source index (wraps every 256 B; when it wraps
; we advance the high byte of the SMC source pointer so the next
; 256-byte window is read from the correct address). The ring
; store uses SMC on cb_store, repatched per byte.
ldx #0
cb_loop:
; Check 16-bit remaining count
Expand All@@ -359,7 +361,7 @@ cb_loop:
bne :+
jmp cb_done
:
; --- Overflow check: if ((tail+1) & $3FF) == head, ring is full ---
; --- Overflow check: if ((tail+1) & TCP_RECV_MASK) == head, ring is full ---
lda tcp_recv_tail+0
clc
adc #1
Expand DownExpand Up@@ -395,6 +397,11 @@ cb_copy_byte:
cb_store:
sta $ffff ; SMC: patched to tcp_recv_buf + tail
inx
bne cb_src_ok
; X wrapped $FF -> $00: advance SMC source high byte to read the
; next 256-byte window of the inbound buffer.
inc cb_copy_byte+2
cb_src_ok:

; tail = next (already computed above)
lda cb_next_lo
Expand Down
Loading