From e9a3a8de426e6562c97d6e5145626bb1d2cabf72 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:39:03 -0500 Subject: [PATCH 01/22] Switch all tests back to wait_for_text() now that harness PR #29 is merged The harness wait_for_text() now calls transport.resume() between polls internally, so the inline polling loops are no longer needed. This replaces 13 copies of the same ~10-line loop with single wait_for_text() calls, reducing total code by 120 lines. Co-Authored-By: Claude Opus 4.6 (1M context) --- tools/_diag_carry.py | 15 +++------------ tools/run_all_tests.py | 14 +++----------- tools/test_chained_hmac.py | 14 +++----------- tools/test_crypto.py | 16 +++------------- tools/test_entropy.py | 16 +++------------- tools/test_hkdf.py | 15 +++------------ tools/test_http.py | 18 ++++-------------- tools/test_keyschedule_steps.py | 16 +++------------- tools/test_net.py | 17 ++++------------- tools/test_sha256.py | 15 +++------------ tools/test_tls_handshake.py | 14 ++------------ tools/test_tls_record.py | 14 ++------------ tools/test_x509.py | 14 +++----------- 13 files changed, 39 insertions(+), 159 deletions(-) diff --git a/tools/_diag_carry.py b/tools/_diag_carry.py index a8a053f..81750b9 100644 --- a/tools/_diag_carry.py +++ b/tools/_diag_carry.py @@ -4,8 +4,8 @@ os.chdir(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) from c64_test_harness import ( - Labels, ViceConfig, ViceInstanceManager, ScreenGrid, - read_bytes, write_bytes, goto, jsr, + Labels, ViceConfig, ViceInstanceManager, + read_bytes, write_bytes, goto, jsr, wait_for_text, ) import subprocess @@ -86,16 +86,7 @@ def jsr_with_carry_diag(transport, addr, timeout=60.0, poll_interval=0.5): t = inst.transport print(f"VICE PID={inst.pid}, port={inst.port}", flush=True) - # Binary monitor: resume CPU between screen polls - grid = None - deadline = time.monotonic() + 180.0 - while time.monotonic() < deadline: - g = ScreenGrid.from_transport(t) - if "Q=QUIT" in g.continuous_text().upper(): - grid = g - break - t.resume() - time.sleep(1.0) + grid = wait_for_text(t, "Q=QUIT", timeout=180.0, verbose=False) if grid is None: print("FATAL: menu not found") sys.exit(1) diff --git a/tools/run_all_tests.py b/tools/run_all_tests.py index a881549..6df9b48 100644 --- a/tools/run_all_tests.py +++ b/tools/run_all_tests.py @@ -13,8 +13,8 @@ os.chdir(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) from c64_test_harness import ( - Labels, ViceConfig, ViceInstanceManager, ScreenGrid, - read_bytes, write_bytes, jsr, + Labels, ViceConfig, ViceInstanceManager, + read_bytes, write_bytes, jsr, wait_for_text, ) PRG_PATH = os.path.join("build", "c64-https.prg") @@ -119,15 +119,7 @@ def main(): # Wait for all instances to boot (binary monitor: resume CPU between polls) for i, inst in enumerate(instances): - grid = None - deadline = time.monotonic() + 120.0 - while time.monotonic() < deadline: - g = ScreenGrid.from_transport(inst.transport) - if "Q=QUIT" in g.continuous_text().upper(): - grid = g - break - inst.transport.resume() - time.sleep(1.0) + grid = wait_for_text(inst.transport, "Q=QUIT", timeout=120.0, verbose=False) if grid is None: print(f" Worker {i}: FATAL - menu did not appear") sys.exit(1) diff --git a/tools/test_chained_hmac.py b/tools/test_chained_hmac.py index c273d94..05394ac 100644 --- a/tools/test_chained_hmac.py +++ b/tools/test_chained_hmac.py @@ -19,13 +19,13 @@ Labels, ViceConfig, ViceInstanceManager, - ScreenGrid, read_bytes, write_bytes, set_breakpoint, delete_breakpoint, goto, wait_for_pc, + wait_for_text, ) # --------------------------------------------------------------------------- @@ -91,16 +91,8 @@ def main(): transport = inst.transport print(f" N={n}: VICE PID={inst.pid}, port={inst.port}") - # Wait for program menu (binary monitor: resume CPU between polls) - grid = None - deadline = time.time() + 60 - while time.time() < deadline: - g = ScreenGrid.from_transport(transport) - if "Q=QUIT" in g.continuous_text().upper(): - grid = g - break - transport.resume() - time.sleep(1.0) + # Wait for program menu + grid = wait_for_text(transport, "Q=QUIT", timeout=60, verbose=False) if grid is None: print(f" N={n}: FAIL - main menu did not appear") results.append((n, False, 0.0, True)) diff --git a/tools/test_crypto.py b/tools/test_crypto.py index 171eb6b..31cb65b 100644 --- a/tools/test_crypto.py +++ b/tools/test_crypto.py @@ -14,10 +14,9 @@ import struct import subprocess import sys -import time from c64_test_harness import ( - Labels, ViceConfig, ViceInstanceManager, ScreenGrid, - read_bytes, write_bytes, jsr, + Labels, ViceConfig, ViceInstanceManager, + read_bytes, write_bytes, jsr, wait_for_text, ) PROJECT_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..") @@ -763,16 +762,7 @@ def main(): transport = inst.transport print(f"VICE PID={inst.pid}, port={inst.port}") - # Binary monitor: resume CPU between screen polls - grid = None - deadline = time.monotonic() + 60.0 - while time.monotonic() < deadline: - g = ScreenGrid.from_transport(transport) - if "Q=QUIT" in g.continuous_text().upper(): - grid = g - break - transport.resume() - time.sleep(1.0) + grid = wait_for_text(transport, "Q=QUIT", timeout=60.0, verbose=False) if grid is None: print("FATAL: Program menu did not appear") sys.exit(1) diff --git a/tools/test_entropy.py b/tools/test_entropy.py index 4c5c3be..ce063d4 100644 --- a/tools/test_entropy.py +++ b/tools/test_entropy.py @@ -14,13 +14,10 @@ import os import subprocess import sys -import time - from c64_test_harness import ( Labels, ViceConfig, ViceInstanceManager, - ScreenGrid, read_bytes, write_bytes, jsr, @@ -28,6 +25,7 @@ delete_breakpoint, goto, wait_for_pc, + wait_for_text, ) # --------------------------------------------------------------------------- @@ -388,17 +386,9 @@ def main(): transport = inst.transport print(f" VICE PID={inst.pid}, port={inst.port}") - # Wait for main menu (binary monitor: resume CPU between screen polls) + # Wait for main menu print(" Waiting for main menu...") - grid = None - deadline = time.monotonic() + 60.0 - while time.monotonic() < deadline: - g = ScreenGrid.from_transport(transport) - if "Q=QUIT" in g.continuous_text().upper(): - grid = g - break - transport.resume() - time.sleep(1.0) + grid = wait_for_text(transport, "Q=QUIT", timeout=60.0, verbose=False) if grid is None: print("FATAL: Main menu did not appear") sys.exit(1) diff --git a/tools/test_hkdf.py b/tools/test_hkdf.py index eb5d934..d363d8d 100644 --- a/tools/test_hkdf.py +++ b/tools/test_hkdf.py @@ -26,12 +26,11 @@ Labels, ViceConfig, ViceInstanceManager, - ScreenGrid, read_bytes, write_bytes, jsr, + wait_for_text, ) -import time # --------------------------------------------------------------------------- # Constants @@ -473,17 +472,9 @@ def main(): transport = inst.transport print(f" VICE PID={inst.pid}, port={inst.port}") - # Wait for main menu (binary monitor: resume CPU between polls) + # Wait for main menu print(" Waiting for main menu...") - grid = None - deadline = time.monotonic() + 60.0 - while time.monotonic() < deadline: - g = ScreenGrid.from_transport(transport) - if "Q=QUIT" in g.continuous_text().upper(): - grid = g - break - transport.resume() - time.sleep(1.0) + grid = wait_for_text(transport, "Q=QUIT", timeout=60.0, verbose=False) if grid is None: print("FATAL: Main menu did not appear") sys.exit(1) diff --git a/tools/test_http.py b/tools/test_http.py index 273a2c9..f81206b 100755 --- a/tools/test_http.py +++ b/tools/test_http.py @@ -13,11 +13,9 @@ import struct import subprocess import sys -import time - from c64_test_harness import ( - Labels, ViceConfig, ViceInstanceManager, ScreenGrid, - read_bytes, write_bytes, jsr, + Labels, ViceConfig, ViceInstanceManager, + read_bytes, write_bytes, jsr, wait_for_text, ) PROJECT_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..") @@ -454,16 +452,8 @@ def main(): transport = inst.transport print(f" VICE PID={inst.pid}, port={inst.port}") - # Wait for menu to appear (binary monitor: resume CPU between polls) - grid = None - deadline = time.monotonic() + 60.0 - while time.monotonic() < deadline: - g = ScreenGrid.from_transport(transport) - if "Q=QUIT" in g.continuous_text().upper(): - grid = g - break - transport.resume() - time.sleep(1.0) + # Wait for menu to appear + grid = wait_for_text(transport, "Q=QUIT", timeout=60.0, verbose=False) if grid is None: print(" FATAL: Program menu did not appear") sys.exit(1) diff --git a/tools/test_keyschedule_steps.py b/tools/test_keyschedule_steps.py index 0fc1ad6..905d88b 100644 --- a/tools/test_keyschedule_steps.py +++ b/tools/test_keyschedule_steps.py @@ -18,16 +18,14 @@ import subprocess import sys -import time - from c64_test_harness import ( Labels, ViceConfig, ViceInstanceManager, - ScreenGrid, read_bytes, write_bytes, jsr, + wait_for_text, ) # --------------------------------------------------------------------------- @@ -395,17 +393,9 @@ def main(): transport = inst.transport print(f" VICE PID={inst.pid}, port={inst.port}") - # Wait for main menu (binary monitor: resume CPU between polls) + # Wait for main menu print(" Waiting for main menu...") - grid = None - deadline = time.monotonic() + 60.0 - while time.monotonic() < deadline: - g = ScreenGrid.from_transport(transport) - if "Q=QUIT" in g.continuous_text().upper(): - grid = g - break - transport.resume() - time.sleep(1.0) + grid = wait_for_text(transport, "Q=QUIT", timeout=60.0, verbose=False) if grid is None: print("FATAL: Main menu did not appear") sys.exit(1) diff --git a/tools/test_net.py b/tools/test_net.py index 458ff4e..00fcbec 100644 --- a/tools/test_net.py +++ b/tools/test_net.py @@ -13,10 +13,9 @@ import struct import subprocess import sys -import time from c64_test_harness import ( - Labels, ViceConfig, ViceInstanceManager, ScreenGrid, - read_bytes, write_bytes, jsr, + Labels, ViceConfig, ViceInstanceManager, + read_bytes, write_bytes, jsr, wait_for_text, ) PROJECT_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..") @@ -481,16 +480,8 @@ def main(): transport = inst.transport print(f" VICE PID={inst.pid}, port={inst.port}") - # Wait for menu to appear (binary monitor: resume CPU between polls) - grid = None - deadline = time.monotonic() + 60.0 - while time.monotonic() < deadline: - g = ScreenGrid.from_transport(transport) - if "Q=QUIT" in g.continuous_text().upper(): - grid = g - break - transport.resume() - time.sleep(1.0) + # Wait for menu to appear + grid = wait_for_text(transport, "Q=QUIT", timeout=60.0, verbose=False) if grid is None: print(" FATAL: Program menu did not appear") sys.exit(1) diff --git a/tools/test_sha256.py b/tools/test_sha256.py index 3866bf3..836dfe6 100644 --- a/tools/test_sha256.py +++ b/tools/test_sha256.py @@ -17,15 +17,14 @@ import struct import subprocess import sys -import time from c64_test_harness import ( Labels, ViceConfig, ViceInstanceManager, - ScreenGrid, read_bytes, write_bytes, jsr, + wait_for_text, ) # --------------------------------------------------------------------------- @@ -325,17 +324,9 @@ def main(): transport = inst.transport print(f" VICE PID={inst.pid}, port={inst.port}") - # Wait for main menu (binary monitor: resume CPU between screen polls) + # Wait for main menu print(" Waiting for main menu...") - grid = None - deadline = time.monotonic() + 60.0 - while time.monotonic() < deadline: - g = ScreenGrid.from_transport(transport) - if "Q=QUIT" in g.continuous_text().upper(): - grid = g - break - transport.resume() - time.sleep(1.0) + grid = wait_for_text(transport, "Q=QUIT", timeout=60.0, verbose=False) if grid is None: print("FATAL: Main menu did not appear") sys.exit(1) diff --git a/tools/test_tls_handshake.py b/tools/test_tls_handshake.py index cd5df74..5c1b3ba 100644 --- a/tools/test_tls_handshake.py +++ b/tools/test_tls_handshake.py @@ -20,13 +20,10 @@ import subprocess import sys -import time as _time - from c64_test_harness import ( Labels, ViceConfig, ViceInstanceManager, - ScreenGrid, read_bytes, write_bytes, jsr, @@ -34,6 +31,7 @@ delete_breakpoint, goto, wait_for_pc, + wait_for_text, ) # --------------------------------------------------------------------------- @@ -1272,15 +1270,7 @@ def main(): # Wait for main menu (binary monitor: resume CPU between polls) print(" Waiting for main menu...") - grid = None - deadline = _time.monotonic() + 60.0 - while _time.monotonic() < deadline: - g = ScreenGrid.from_transport(transport) - if "Q=QUIT" in g.continuous_text().upper(): - grid = g - break - transport.resume() - _time.sleep(1.0) + grid = wait_for_text(transport, "Q=QUIT", timeout=60.0, verbose=False) if grid is None: print("FATAL: Main menu did not appear") sys.exit(1) diff --git a/tools/test_tls_record.py b/tools/test_tls_record.py index a87e0ef..df8b527 100644 --- a/tools/test_tls_record.py +++ b/tools/test_tls_record.py @@ -19,13 +19,10 @@ from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 -import time - from c64_test_harness import ( Labels, ViceConfig, ViceInstanceManager, - ScreenGrid, read_bytes, write_bytes, jsr, @@ -33,6 +30,7 @@ delete_breakpoint, goto, wait_for_pc, + wait_for_text, ) # --------------------------------------------------------------------------- @@ -777,15 +775,7 @@ def main(): # Wait for main menu (binary monitor: resume CPU between polls) print(" Waiting for main menu...") - grid = None - deadline = time.monotonic() + 60.0 - while time.monotonic() < deadline: - g = ScreenGrid.from_transport(transport) - if "Q=QUIT" in g.continuous_text().upper(): - grid = g - break - transport.resume() - time.sleep(1.0) + grid = wait_for_text(transport, "Q=QUIT", timeout=60.0, verbose=False) if grid is None: print("FATAL: Main menu did not appear") sys.exit(1) diff --git a/tools/test_x509.py b/tools/test_x509.py index 370bfeb..41a5518 100644 --- a/tools/test_x509.py +++ b/tools/test_x509.py @@ -30,11 +30,11 @@ Labels, ViceConfig, ViceInstanceManager, - ScreenGrid, read_bytes, write_bytes, jsr, goto, + wait_for_text, ) # --------------------------------------------------------------------------- @@ -771,17 +771,9 @@ def main(): print(f"\n=== Starting VICE ===") print(f" VICE PID={inst.pid}, port={inst.port}") - # Wait for main menu (binary monitor: resume CPU between polls) + # Wait for main menu print(" Waiting for main menu...") - grid = None - deadline = time.monotonic() + 60.0 - while time.monotonic() < deadline: - g = ScreenGrid.from_transport(transport) - if "Q=QUIT" in g.continuous_text().upper(): - grid = g - break - transport.resume() - time.sleep(1.0) + grid = wait_for_text(transport, "Q=QUIT", timeout=60.0, verbose=False) if grid is None: print("FATAL: Main menu did not appear") sys.exit(1) From 6cb9f54e04c1a349c1f2af1c2db081d2152ac5c2 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Sat, 28 Mar 2026 20:11:35 -0500 Subject: [PATCH 02/22] Fix parallel test runner: all 10 suites, instance-per-suite, no worker reuse The parallel runner had two bugs: (1) as_completed() didn't see futures added mid-iteration, so only the first N suites were collected, and (2) reusing VICE instances across suites caused state contamination (HKDF 0/12 on reused workers). Fix: allocate a fresh VICE instance per suite via run_suite_in_own_instance(). Add all 10 suites (was 5). Add --skip-slow and --seed flags. 193/193 pass. Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 11 ++- tools/run_all_tests.py | 178 ++++++++++++++++++++--------------------- 2 files changed, 92 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index 25bc431..58674f1 100644 --- a/README.md +++ b/README.md @@ -121,13 +121,15 @@ Current status (24.8 KB binary, 487 labels): ## Test Automation -193 tests across 9 suites + 2 diagnostic suites, using the [`c64-test-harness`](../c64-test-harness) package to drive VICE via its binary monitor protocol. All tests log VICE PID and port for multi-agent safety. +193 tests across 10 suites (+ 1 standalone diagnostic), using the [`c64-test-harness`](../c64-test-harness) package to drive VICE via its binary monitor protocol. The parallel runner allocates a fresh VICE instance per suite to avoid state contamination. All tests log VICE PID and port for multi-agent safety. ```bash pip install -e ../c64-test-harness -# Run all suites in parallel (5 VICE instances, ~2.5 min wall time) -python3 tools/run_all_tests.py --workers 5 +# Run all 10 suites in parallel (one VICE instance per suite, ~5 min with ECDSA) +python3 tools/run_all_tests.py +python3 tools/run_all_tests.py --skip-slow # Skip x509/ECDSA (~5s wall time) +python3 tools/run_all_tests.py --workers 6 # Limit concurrent VICE instances # Individual suites python3 tools/test_net.py # 60 tests: ip65 integration, ZP save/restore, ring buffer, TCP recv callback @@ -139,7 +141,8 @@ python3 tools/test_x509.py # 11 tests: DER parse P-256/P-384, ECDSA ver python3 tools/test_tls_handshake.py # 21 tests: transcript hash, ClientHello, ServerHello, key schedule (RFC 8448), Finished MAC python3 tools/test_keyschedule_steps.py # 9 tests: key schedule step-by-step (RFC 8448 vectors) python3 tools/test_entropy.py # 7 tests: SID/CIA hardware init, DRBG seeding, output quality -python3 tools/test_chained_hmac.py # 10 tests: chained HMAC-SHA256 stability (N=1..10) +python3 tools/test_http.py # 27 tests: HTTP/1.1 GET builder, response parser, status codes +python3 tools/test_chained_hmac.py # 10 tests: chained HMAC-SHA256 stability (N=1..10, standalone) ``` ## Related Projects diff --git a/tools/run_all_tests.py b/tools/run_all_tests.py index 6df9b48..783733c 100644 --- a/tools/run_all_tests.py +++ b/tools/run_all_tests.py @@ -2,13 +2,15 @@ """Run all c64-https test suites in parallel using ViceInstanceManager. Usage: - python3 tools/run_all_tests.py [--workers N] + python3 tools/run_all_tests.py [--workers N] [--seed S] [--skip-slow] """ import os +import random import subprocess import sys import time +from concurrent.futures import ThreadPoolExecutor, as_completed os.chdir(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) @@ -35,48 +37,26 @@ def build(): return Labels.from_file(LABELS_PATH) -def run_test_suite(name, transport, labels, port, pid): +def run_test_suite(name, transport, labels, seed): """Run a single test suite, return (name, passed, failed, duration).""" + # Ensure CPU is running before each suite (previous suite leaves it paused + # after jsr() returns at a breakpoint) + transport.resume() start = time.time() passed = failed = 0 try: if name == "net": - from test_net import test_build_integrity, test_ip65_jump_table - from test_net import test_zp_save_restore, test_recv_ring_buffer - from test_net import test_ip65_init_without_hardware - - p, f = test_build_integrity(labels) - passed += p; failed += f - p, f = test_ip65_jump_table(transport) - passed += p; failed += f - p, f = test_zp_save_restore(transport, labels) - passed += p; failed += f - p, f = test_recv_ring_buffer(transport, labels) - passed += p; failed += f - p, f = test_ip65_init_without_hardware(transport, labels) - passed += p; failed += f + from test_net import run_tests as net_run + passed, failed = net_run(transport, labels) elif name == "sha256": from test_sha256 import run_tests as sha256_run passed, failed = sha256_run(transport, labels, iterations=5) elif name == "crypto": - from test_crypto import (test_sqtab_init, test_chacha20_block_rfc, - test_chacha20_encrypt_rfc, test_poly1305_mac_rfc, - test_aead_encrypt_rfc, test_aead_decrypt_roundtrip, - test_aead_random) - import random - rng = random.Random(42) - for fn in [test_sqtab_init, test_chacha20_block_rfc, - test_chacha20_encrypt_rfc, test_poly1305_mac_rfc, - test_aead_encrypt_rfc]: - p, f = fn(transport, labels) - passed += p; failed += f - p, f = test_aead_decrypt_roundtrip(transport, labels, rng) - passed += p; failed += f - p, f = test_aead_random(transport, labels, rng) - passed += p; failed += f + from test_crypto import run_tests as crypto_run + passed, failed = crypto_run(transport, labels, seed=seed) elif name == "hkdf": from test_hkdf import run_tests as hkdf_run @@ -84,10 +64,32 @@ def run_test_suite(name, transport, labels, port, pid): elif name == "tls_record": from test_tls_record import run_tests as record_run - passed, failed = record_run(transport, labels, seed=42) + passed, failed = record_run(transport, labels, seed=seed) + + elif name == "tls_handshake": + from test_tls_handshake import run_tests as handshake_run + passed, failed = handshake_run(transport, labels, seed=seed) + + elif name == "keyschedule": + from test_keyschedule_steps import run_tests as ks_run + passed, failed = ks_run(transport, labels) + + elif name == "entropy": + from test_entropy import run_tests as entropy_run + passed, failed = entropy_run(transport, labels) + + elif name == "http": + from test_http import run_tests as http_run + passed, failed = http_run(transport, labels) + + elif name == "x509": + from test_x509 import run_tests as x509_run + passed, failed = x509_run(transport, labels) except Exception as e: + import traceback print(f" [{name}] EXCEPTION: {e}") + traceback.print_exc() failed += 1 duration = time.time() - start @@ -95,64 +97,66 @@ def run_test_suite(name, transport, labels, port, pid): def main(): - workers = 3 - for i, arg in enumerate(sys.argv[1:]): - if arg == "--workers": - workers = int(sys.argv[i + 2]) + workers = 4 + seed = random.randint(0, 2**32 - 1) + skip_slow = False + + args = sys.argv[1:] + i = 0 + while i < len(args): + if args[i] == "--workers": + workers = int(args[i + 1]) + i += 2 + elif args[i] == "--seed": + seed = int(args[i + 1]) + i += 2 + elif args[i] == "--skip-slow": + skip_slow = True + i += 1 + else: + i += 1 + + print(f"Random seed: {seed} (reproduce with --seed {seed})") labels = build() - suites = ["net", "sha256", "crypto", "hkdf", "tls_record"] + # x509 is by far the slowest (~5 min for ECDSA verify), so start it first. + # Entropy uses manual breakpoints sensitive to CPU state, so start it early + # on a fresh worker. Remaining fast suites fill in around them. + suites = ["entropy", "net", "sha256", "crypto", "hkdf", + "keyschedule", "http", "tls_record", "tls_handshake"] + if not skip_slow: + suites.insert(0, "x509") config = ViceConfig(prg_path=PRG_PATH, warp=True, ntsc=True, sound=False) + num_instances = min(workers, len(suites)) - print(f"\n=== Starting {workers} VICE instances (staggered 100ms) ===") + print(f"\n=== Launching {len(suites)} suites across " + f"{num_instances} concurrent VICE instances ===") - with ViceInstanceManager(config=config) as mgr: - instances = [] - for i in range(min(workers, len(suites))): - inst = mgr.acquire() - print(f" Worker {i}: VICE PID={inst.pid}, port={inst.port}") - instances.append(inst) - if i < workers - 1: - time.sleep(0.1) # 100ms stagger per PATTERNS.md - - # Wait for all instances to boot (binary monitor: resume CPU between polls) - for i, inst in enumerate(instances): - grid = wait_for_text(inst.transport, "Q=QUIT", timeout=120.0, verbose=False) + def run_suite_in_own_instance(mgr, suite_name): + """Acquire a fresh VICE instance, run one suite, release.""" + inst = mgr.acquire() + try: + grid = wait_for_text(inst.transport, "Q=QUIT", timeout=120.0, + verbose=False) if grid is None: - print(f" Worker {i}: FATAL - menu did not appear") - sys.exit(1) + return suite_name, 0, 1, 0.0 # Safety loop: JMP $0339 prevents crash when BASIC ROM banked out write_bytes(inst.transport, 0x0339, bytes([0x4C, 0x39, 0x03])) - print(f" Worker {i}: ready") - - # Each suite gets its own worker — suites run in parallel - # If more suites than workers, extra suites wait for a free worker - from concurrent.futures import ThreadPoolExecutor, as_completed - - def worker_fn(suite_name, inst): - return run_test_suite(suite_name, inst.transport, labels, - inst.port, inst.pid) - - results = [] - print(f"\n=== Running {len(suites)} test suites across " - f"{len(instances)} workers ===\n") - - # Map suites to workers 1:1 (first batch), then reuse freed workers - with ThreadPoolExecutor(max_workers=len(instances)) as pool: - futures = {} - inst_queue = list(instances) - pending_suites = list(suites) - active = {} - - # Submit up to N suites (one per worker) - while pending_suites and inst_queue: - suite = pending_suites.pop(0) - inst = inst_queue.pop(0) - fut = pool.submit(worker_fn, suite, inst) - futures[fut] = suite - active[fut] = inst + + return run_test_suite(suite_name, inst.transport, labels, seed) + finally: + mgr.release(inst) + + results = [] + + with ViceInstanceManager(config=config) as mgr: + with ThreadPoolExecutor(max_workers=num_instances) as pool: + futures = { + pool.submit(run_suite_in_own_instance, mgr, suite): suite + for suite in suites + } for fut in as_completed(futures): name, passed, failed, duration = fut.result() @@ -161,18 +165,6 @@ def worker_fn(suite_name, inst): print(f" [{status}] {name}: {passed}/{passed+failed} " f"({duration:.1f}s)") - # Return this worker's instance and submit next suite - freed_inst = active.pop(fut) - if pending_suites: - suite = pending_suites.pop(0) - new_fut = pool.submit(worker_fn, suite, freed_inst) - futures[new_fut] = suite - active[new_fut] = freed_inst - - # Release instances - for inst in instances: - mgr.release(inst) - # Summary total_passed = sum(r[1] for r in results) total_failed = sum(r[2] for r in results) @@ -183,7 +175,7 @@ def worker_fn(suite_name, inst): f"{total_failed} failed") for name, passed, failed, duration in sorted(results): status = "OK" if failed == 0 else "FAIL" - print(f" {status:4s} {name:15s} {passed:3d}/{passed+failed:3d} " + print(f" {status:4s} {name:20s} {passed:3d}/{passed+failed:3d} " f"({duration:.1f}s)") print(f"{'='*60}") From 7dc6721c372c6b5a00b4fb418855ccd7ea054609 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:02:06 -0500 Subject: [PATCH 03/22] Fix A/X register clobbering in net.asm and add DNS + HTTP integration tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit net_dns_resolve and net_set_tcp_dest both passed A/X parameters through net_save_zp, which uses X as a loop counter and clobbers both registers. This caused DNS resolution and TCP destination setup to receive garbage pointers instead of the caller's intended addresses. Fixed by pushing A/X to the stack before the ZP save and restoring after. Added test_dns.py (4 tests) exercising net_dns_resolve over TAP with dnsmasq, and test_http_integration.py (5 tests) for end-to-end plain HTTP GET (DNS → TCP → request → response). Both use ViceInstanceManager with ethernet_mode="rrnet" and run unprivileged (only dnsmasq via sudo). Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 6 +- build/c64-https.prg | Bin 39573 -> 39573 bytes build/labels.txt | 164 ++++++++-------- src/net.asm | 12 ++ tools/test_dns.py | 344 +++++++++++++++++++++++++++++++++ tools/test_http_integration.py | 329 +++++++++++++++++++++++++++++++ tools/test_server.py | 110 +++++++++++ 7 files changed, 882 insertions(+), 83 deletions(-) create mode 100644 tools/test_dns.py create mode 100644 tools/test_http_integration.py create mode 100644 tools/test_server.py diff --git a/README.md b/README.md index 58674f1..ea9ee7a 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ Current status (24.8 KB binary, 487 labels): - [x] Entropy/DRBG initialization — SID voice 3 noise + CIA timer seeding at boot, DRBG fills for TLS random values - [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) -- [ ] HTTP/1.1 GET request +- [x] HTTP/1.1 GET request — build GET, parse response (status + headers + body), plain HTTP end-to-end - [ ] End-to-end HTTPS GET demo ### Known Issues @@ -143,6 +143,10 @@ python3 tools/test_keyschedule_steps.py # 9 tests: key schedule step-by-step (RF python3 tools/test_entropy.py # 7 tests: SID/CIA hardware init, DRBG seeding, output quality python3 tools/test_http.py # 27 tests: HTTP/1.1 GET builder, response parser, status codes python3 tools/test_chained_hmac.py # 10 tests: chained HMAC-SHA256 stability (N=1..10, standalone) + +# Integration tests (require tap-c64 interface, dnsmasq; see scripts/setup-tap-networking.sh in c64-test-harness) +python3 tools/test_dns.py # 4 tests: DNS resolution via ip65 over TAP (known host, second host, unknown host) +python3 tools/test_http_integration.py # 5 tests: end-to-end plain HTTP GET over TAP (DNS + TCP + request/response) ``` ## Related Projects diff --git a/build/c64-https.prg b/build/c64-https.prg index 36784959929be54a74d7fa76529e91d1d7c030f1..39f1ff62924681edb17d5c01fdd72790e8efe242 100644 GIT binary patch delta 2593 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 8) & 0xFF + result_lo = CARRY_RESULT_ADDR & 0xFF + result_hi = (CARRY_RESULT_ADDR >> 8) & 0xFF + return bytes([ + 0xA9, hostname_lo, # LDA #hostname_lo + 0xA2, hostname_hi, # LDX #hostname_hi + 0x20, dns_lo, dns_hi, # JSR net_dns_resolve + 0xA9, 0x00, # LDA #$00 (success) + 0x90, 0x02, # BCC +2 (branch if carry clear = success) + 0xA9, 0x01, # LDA #$01 (failure) + 0x8D, result_lo, result_hi, # STA CARRY_RESULT_ADDR + 0x60, # RTS + ]) + + +def do_dns_resolve(transport, write_bytes, read_bytes, jsr_fn, + hostname_str, dns_resolve_addr): + """Write hostname to scratch RAM, build trampoline, call it, return + (carry_result, ip_bytes). + + carry_result: 0 = success (carry clear), 1 = failure (carry set) + ip_bytes: 4-byte list from ip65_dns_ip_addr + """ + # Write null-terminated hostname to scratch RAM + hostname = hostname_str.encode("ascii") + b"\x00" + write_bytes(transport, HOSTNAME_ADDR, hostname) + + # Clear carry result location + write_bytes(transport, CARRY_RESULT_ADDR, [0xFF]) + + hostname_lo = HOSTNAME_ADDR & 0xFF + hostname_hi = (HOSTNAME_ADDR >> 8) & 0xFF + + trampoline = build_dns_trampoline(hostname_lo, hostname_hi, dns_resolve_addr) + write_bytes(transport, TRAMPOLINE_ADDR, trampoline) + + # Execute the trampoline + jsr_fn(transport, TRAMPOLINE_ADDR, timeout=30.0) + + # Read carry result + carry_bytes = read_bytes(transport, CARRY_RESULT_ADDR, 1) + carry_result = carry_bytes[0] + + # Read resolved IP (4 bytes) + ip_bytes = read_bytes(transport, IP65_DNS_IP_ADDR, 4) + + return carry_result, ip_bytes + + +# --------------------------------------------------------------------------- +# Main test +# --------------------------------------------------------------------------- + +def main(): + os.chdir(PROJECT_ROOT) + + if not check_prerequisites(): + sys.exit(0) + + # Late imports -- only needed if prerequisites are met + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + from c64_test_harness import ( + Labels, ViceConfig, ViceInstanceManager, + read_bytes, write_bytes, jsr, wait_for_text, + ) + + passed = 0 + failed = 0 + dnsmasq_proc = None + mgr = None + inst = None + + try: + # ---- 1. Build -------------------------------------------------------- + print("\n=== Building ===") + 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) + print(" Build OK") + + labels = Labels.from_file(LABELS_PATH) + print(f" Labels loaded, {len(labels)} symbols") + + # ---- Test: test_dns_labels ------------------------------------------- + print("\n=== test_dns_labels ===") + dns_resolve_addr = labels.address("net_dns_resolve") + if dns_resolve_addr is not None: + print(f" PASS: net_dns_resolve found @ ${dns_resolve_addr:04X}") + passed += 1 + else: + print(" FAIL: net_dns_resolve label not found") + failed += 1 + raise RuntimeError("Required label net_dns_resolve not found") + + # ---- 2. Start dnsmasq ------------------------------------------------ + print("\n=== Starting dnsmasq ===") + dnsmasq_proc = start_dnsmasq() + + # ---- 3. Launch VICE -------------------------------------------------- + print("\n=== Starting VICE ===") + config = ViceConfig( + prg_path=PRG_PATH, + warp=False, # warp causes timing issues with ethernet + ntsc=True, + sound=False, + ethernet=True, + ethernet_mode="rrnet", + ethernet_driver="tuntap", + ethernet_interface="tap-c64", + ) + + mgr = ViceInstanceManager(config=config) + inst = mgr.acquire() + transport = inst.transport + print(f" VICE PID={inst.pid}, port={inst.port}") + + # ---- 4. Wait for boot menu ------------------------------------------ + print("\n=== Waiting for boot menu ===") + grid = wait_for_text(transport, "Q=QUIT", timeout=60.0, verbose=False) + if grid is None: + print(" FATAL: Program menu did not appear") + failed += 1 + raise RuntimeError("Boot menu timeout") + print(" Boot menu appeared") + + # ---- 5. Network init (DHCP) ----------------------------------------- + print("\n=== Network init (pressing I for init) ===") + transport.resume() # CPU paused after wait_for_text screen read + transport.inject_keys([0x49]) # 'I' + + grid = wait_for_text(transport, "DHCP OK", timeout=60.0, verbose=False) + if grid is None: + print(" FAIL: DHCP did not complete within 60 seconds") + if dnsmasq_proc: + dnsmasq_proc.terminate() + _, stderr = dnsmasq_proc.communicate(timeout=5) + print(f" dnsmasq stderr:\n{stderr.decode()}") + dnsmasq_proc = None + failed += 1 + raise RuntimeError("DHCP timeout") + print(" DHCP OK") + + # ---- Test: test_dns_resolve_known_host ------------------------------- + print("\n=== test_dns_resolve_known_host ===") + carry, ip = do_dns_resolve( + transport, write_bytes, read_bytes, jsr, + "c64test.local", dns_resolve_addr, + ) + expected_ip = [10, 0, 65, 1] + if carry == 0 and list(ip) == expected_ip: + print(f" PASS: resolved c64test.local -> {'.'.join(str(b) for b in ip)}" + f", carry=0") + passed += 1 + else: + print(f" FAIL: c64test.local -> {list(ip)}, carry={carry}" + f" (expected {expected_ip}, carry=0)") + failed += 1 + + # ---- Test: test_dns_resolve_second_host ------------------------------ + print("\n=== test_dns_resolve_second_host ===") + carry, ip = do_dns_resolve( + transport, write_bytes, read_bytes, jsr, + "second.local", dns_resolve_addr, + ) + if carry == 0 and list(ip) == expected_ip: + print(f" PASS: resolved second.local -> {'.'.join(str(b) for b in ip)}" + f", carry=0") + passed += 1 + else: + print(f" FAIL: second.local -> {list(ip)}, carry={carry}" + f" (expected {expected_ip}, carry=0)") + failed += 1 + + # ---- Test: test_dns_resolve_unknown_host ----------------------------- + print("\n=== test_dns_resolve_unknown_host ===") + carry, ip = do_dns_resolve( + transport, write_bytes, read_bytes, jsr, + "nonexistent.invalid", dns_resolve_addr, + ) + if carry == 1: + print(f" PASS: nonexistent.invalid -> carry=1 (failure, as expected)") + passed += 1 + else: + print(f" FAIL: nonexistent.invalid -> carry={carry}, ip={list(ip)}" + f" (expected carry=1)") + failed += 1 + + except RuntimeError as e: + print(f"\n Test aborted: {e}") + except Exception as e: + print(f"\n Unexpected error: {e}") + import traceback + traceback.print_exc() + failed += 1 + finally: + # ---- Teardown -------------------------------------------------------- + print("\n=== Teardown ===") + + if mgr is not None: + try: + if inst is not None: + mgr.release(inst) + mgr.shutdown() + print(" VICE released") + except Exception as e: + print(f" VICE cleanup error: {e}") + + if dnsmasq_proc is not None: + try: + dnsmasq_proc.terminate() + try: + _, stderr = dnsmasq_proc.communicate(timeout=5) + print(f" dnsmasq stopped (exit={dnsmasq_proc.returncode})") + if failed > 0: + print(f" dnsmasq stderr:\n{stderr.decode()}") + except subprocess.TimeoutExpired: + dnsmasq_proc.kill() + dnsmasq_proc.wait() + print(" dnsmasq killed (did not terminate cleanly)") + except Exception as e: + print(f" dnsmasq cleanup error: {e}") + + # ---- Summary ------------------------------------------------------------- + total = passed + failed + print(f"\n{'='*60}") + print(f"RESULTS: {passed}/{total} passed, {failed}/{total} failed") + print(f"{'='*60}") + sys.exit(0 if failed == 0 else 1) + + +if __name__ == "__main__": + main() diff --git a/tools/test_http_integration.py b/tools/test_http_integration.py new file mode 100644 index 0000000..904e3a6 --- /dev/null +++ b/tools/test_http_integration.py @@ -0,0 +1,329 @@ +#!/usr/bin/env python3 +"""test_http_integration.py -- End-to-end HTTP integration test for c64-https. + +Exercises the C64's http_get_plain routine over real networking via the TAP +interface. The network architecture is: + + VICE (C64, 10.0.65.2) <--tap-c64 L2--> Host (10.0.65.1) + |-- dnsmasq (DHCP + DNS) + |-- HTTP server :80 + +Prerequisites: + - tap-c64 interface exists and is configured (10.0.65.1) + - x64sc (VICE) is on PATH + - dnsmasq is on PATH + +Usage: + python3 tools/test_http_integration.py +""" + +import os +import shutil +import subprocess +import sys +import time + +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") + +# --------------------------------------------------------------------------- +# Skip checks +# --------------------------------------------------------------------------- + +def check_prerequisites(): + """Return True if all prerequisites are met, else print skip and return False.""" + if not os.path.exists("/sys/class/net/tap-c64"): + print("SKIP: tap-c64 interface not found") + return False + if shutil.which("x64sc") is None: + print("SKIP: x64sc not on PATH") + return False + if shutil.which("dnsmasq") is None: + print("SKIP: dnsmasq not on PATH") + return False + if shutil.which("sudo") is None: + print("SKIP: sudo not on PATH") + return False + return True + + +# --------------------------------------------------------------------------- +# dnsmasq helper +# --------------------------------------------------------------------------- + +def start_dnsmasq(): + """Start dnsmasq providing DHCP and DNS on tap-c64. Returns Popen.""" + cmd = [ + "sudo", "dnsmasq", + "--no-daemon", + "--interface=tap-c64", + "--bind-interfaces", + "--listen-address=10.0.65.1", + "--dhcp-range=10.0.65.2,10.0.65.10,255.255.255.0,5m", + "--address=/c64test.local/10.0.65.1", + "--dhcp-option=6,10.0.65.1", + "--log-queries", + "--no-resolv", + ] + print(f" dnsmasq cmd: {' '.join(cmd)}") + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + # Give it a moment to bind + time.sleep(0.5) + if proc.poll() is not None: + _, stderr = proc.communicate() + raise RuntimeError(f"dnsmasq failed to start: {stderr.decode()}") + print(f" dnsmasq PID={proc.pid}") + return proc + + +# --------------------------------------------------------------------------- +# Main test +# --------------------------------------------------------------------------- + +def main(): + os.chdir(PROJECT_ROOT) + + if not check_prerequisites(): + sys.exit(0) + + # Late imports -- only needed if prerequisites are met + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + from test_server import TestHTTPServer + from c64_test_harness import ( + Labels, ViceConfig, ViceInstanceManager, + read_bytes, write_bytes, jsr, wait_for_text, + ) + + passed = 0 + failed = 0 + dnsmasq_proc = None + server = None + mgr = None + inst = None + + try: + # ---- 1. Build -------------------------------------------------------- + print("\n=== Building ===") + 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) + print(" Build OK") + + labels = Labels.from_file(LABELS_PATH) + print(f" Labels loaded, {len(labels)} symbols") + + # Verify key labels exist + required_labels = [ + "http_get_plain", "http_host_ptr", "http_host_len", + "http_path_ptr", "http_path_len", "http_port", + "http_parse_state", "http_line_idx", "http_hdr_match", + "http_resp_len", "http_resp_buf", "http_status", + ] + for name in required_labels: + if labels.address(name) is None: + print(f" FATAL: required label '{name}' not found") + sys.exit(1) + + # ---- 2. Start dnsmasq ------------------------------------------------ + print("\n=== Starting dnsmasq ===") + dnsmasq_proc = start_dnsmasq() + + # ---- 3. Start HTTP test server --------------------------------------- + print("\n=== Starting HTTP test server ===") + server = TestHTTPServer(host="10.0.65.1", port=8080) + server.start() + print(" HTTP server listening on 10.0.65.1:8080") + + # ---- 4. Launch VICE -------------------------------------------------- + print("\n=== Starting VICE ===") + config = ViceConfig( + prg_path=PRG_PATH, + warp=False, # warp causes timing issues with ethernet + ntsc=True, + sound=False, + ethernet=True, + ethernet_mode="rrnet", + ethernet_driver="tuntap", + ethernet_interface="tap-c64", + ) + + mgr = ViceInstanceManager(config=config) + inst = mgr.acquire() + transport = inst.transport + print(f" VICE PID={inst.pid}, port={inst.port}") + + # ---- 5. Wait for boot menu ------------------------------------------ + print("\n=== Waiting for boot menu ===") + grid = wait_for_text(transport, "Q=QUIT", timeout=60.0, verbose=False) + if grid is None: + print(" FATAL: Program menu did not appear") + failed += 1 + raise RuntimeError("Boot menu timeout") + print(" Boot menu appeared") + + # ---- 6. Network init (DHCP) ----------------------------------------- + print("\n=== Network init (pressing I for init) ===") + transport.resume() # CPU paused after wait_for_text screen read + transport.inject_keys([0x49]) # 'I' + + grid = wait_for_text(transport, "DHCP OK", timeout=60.0, verbose=False) + if grid is None: + print(" FAIL: DHCP did not complete within 60 seconds") + # Dump dnsmasq stderr for debugging + if dnsmasq_proc: + dnsmasq_proc.terminate() + _, stderr = dnsmasq_proc.communicate(timeout=5) + print(f" dnsmasq stderr:\n{stderr.decode()}") + dnsmasq_proc = None + failed += 1 + raise RuntimeError("DHCP timeout") + print(" DHCP OK") + passed += 1 + + # ---- 7. Set up HTTP parameters in C64 memory ------------------------- + print("\n=== Setting up HTTP parameters ===") + + # Write hostname to scratch RAM at $C000 + hostname = b"c64test.local\x00" + write_bytes(transport, 0xC000, hostname) + write_bytes(transport, labels.address("http_host_ptr"), [0x00, 0xC0]) + write_bytes(transport, labels.address("http_host_len"), [13]) + + # Write path to $C080 + path = b"/\x00" + write_bytes(transport, 0xC080, path) + write_bytes(transport, labels.address("http_path_ptr"), [0x80, 0xC0]) + write_bytes(transport, labels.address("http_path_len"), [1]) + + # Set port to 8080 (little-endian 16-bit: 0x1F90) + write_bytes(transport, labels.address("http_port"), [0x90, 0x1F]) + + # Initialize parser state + write_bytes(transport, labels.address("http_parse_state"), [0]) + write_bytes(transport, labels.address("http_line_idx"), [0]) + write_bytes(transport, labels.address("http_hdr_match"), [0]) + write_bytes(transport, labels.address("http_resp_len"), [0, 0]) + + print(" Parameters written to C64 memory") + + # ---- 8. Call http_get_plain ------------------------------------------ + print("\n=== Calling http_get_plain ===") + http_get_plain = labels.address("http_get_plain") + print(f" http_get_plain @ ${http_get_plain:04X}") + + try: + jsr(transport, http_get_plain, timeout=60.0) + print(" http_get_plain returned") + except TimeoutError: + print(" FAIL: http_get_plain timed out after 60 seconds") + failed += 1 + raise RuntimeError("http_get_plain timeout") + + # ---- 9. Read results ------------------------------------------------- + print("\n=== Checking results ===") + + # Check http_status (2 bytes, little-endian) + status_bytes = read_bytes(transport, labels.address("http_status"), 2) + status = status_bytes[0] | (status_bytes[1] << 8) + if status == 200: + print(f" PASS: http_status = {status}") + passed += 1 + else: + print(f" FAIL: http_status = {status}, expected 200 " + f"(bytes: ${status_bytes[0]:02X} ${status_bytes[1]:02X})") + failed += 1 + + # Check http_resp_len (2 bytes, little-endian) + resp_len_bytes = read_bytes(transport, labels.address("http_resp_len"), 2) + resp_len = resp_len_bytes[0] | (resp_len_bytes[1] << 8) + if resp_len == 9: + print(f" PASS: http_resp_len = {resp_len}") + passed += 1 + else: + print(f" FAIL: http_resp_len = {resp_len}, expected 9") + failed += 1 + + # Check response body + resp_body = read_bytes(transport, labels.address("http_resp_buf"), resp_len) + if resp_body == b"HELLO C64": + print(f" PASS: response body = 'HELLO C64'") + passed += 1 + else: + print(f" FAIL: response body = {resp_body!r}, expected b'HELLO C64'") + failed += 1 + + # ---- 10. Verify server received a well-formed request ---------------- + print("\n=== Checking server-side request log ===") + if len(server.requests) >= 1: + req = server.requests[0] + if req["method"] == "GET" and req["path"] == "/": + print(f" PASS: server received GET / " + f"(Host: {req['headers'].get('Host', '')})") + passed += 1 + else: + print(f" FAIL: server received {req['method']} {req['path']}, " + f"expected GET /") + failed += 1 + else: + print(f" FAIL: server received 0 requests, expected >= 1") + failed += 1 + + except RuntimeError as e: + print(f"\n Test aborted: {e}") + except Exception as e: + print(f"\n Unexpected error: {e}") + import traceback + traceback.print_exc() + failed += 1 + finally: + # ---- Teardown -------------------------------------------------------- + print("\n=== Teardown ===") + + if mgr is not None: + try: + if inst is not None: + mgr.release(inst) + mgr.shutdown() + print(" VICE released") + except Exception as e: + print(f" VICE cleanup error: {e}") + + if server is not None: + try: + server.stop() + print(" HTTP server stopped") + except Exception as e: + print(f" HTTP server cleanup error: {e}") + + if dnsmasq_proc is not None: + try: + dnsmasq_proc.terminate() + try: + _, stderr = dnsmasq_proc.communicate(timeout=5) + print(f" dnsmasq stopped (exit={dnsmasq_proc.returncode})") + if failed > 0: + print(f" dnsmasq stderr:\n{stderr.decode()}") + except subprocess.TimeoutExpired: + dnsmasq_proc.kill() + dnsmasq_proc.wait() + print(" dnsmasq killed (did not terminate cleanly)") + except Exception as e: + print(f" dnsmasq cleanup error: {e}") + + # ---- Summary ------------------------------------------------------------- + total = passed + failed + print(f"\n{'='*60}") + print(f"RESULTS: {passed}/{total} passed, {failed}/{total} failed") + print(f"{'='*60}") + sys.exit(0 if failed == 0 else 1) + + +if __name__ == "__main__": + main() diff --git a/tools/test_server.py b/tools/test_server.py new file mode 100644 index 0000000..b6675d6 --- /dev/null +++ b/tools/test_server.py @@ -0,0 +1,110 @@ +"""Reusable HTTP test server for C64 HTTPS integration testing.""" + +import threading +from http.server import HTTPServer, BaseHTTPRequestHandler + +DEFAULT_HOST = "10.0.65.1" +DEFAULT_PORT = 80 + + +class _ReusableHTTPServer(HTTPServer): + """HTTPServer subclass that sets SO_REUSEADDR before bind.""" + + allow_reuse_address = True + +RESPONSE_BODY = "HELLO C64" + + +class _RequestHandler(BaseHTTPRequestHandler): + """Handles HTTP requests, recording them for test assertions.""" + + def do_GET(self): + if self.path == "/": + body = RESPONSE_BODY.encode("ascii") + self.send_response(200) + self.send_header("Content-Type", "text/plain") + self.send_header("Content-Length", str(len(body))) + self.send_header("Connection", "close") + self.end_headers() + self.wfile.write(body) + else: + body = b"Not Found" + self.send_response(404) + self.send_header("Content-Type", "text/plain") + self.send_header("Content-Length", str(len(body))) + self.send_header("Connection", "close") + self.end_headers() + self.wfile.write(body) + + self.server.record_request(self.command, self.path, dict(self.headers)) + + def log_message(self, format, *args): + """Suppress default stderr logging during tests.""" + pass + + +class TestHTTPServer: + """HTTP server that runs in a background daemon thread. + + Attributes: + requests: list of dicts recording each received request + (keys: method, path, headers). + """ + + def __init__(self, host=DEFAULT_HOST, port=DEFAULT_PORT, ssl_context=None): + self.host = host + self.port = port + self.ssl_context = ssl_context + self.requests = [] + self._lock = threading.Lock() + + self._httpd = _ReusableHTTPServer((host, port), _RequestHandler) + + if ssl_context is not None: + self._httpd.socket = ssl_context.wrap_socket( + self._httpd.socket, server_side=True + ) + + # Give the handler a way to record requests back to us. + self._httpd.record_request = self._record_request + + self._thread = None + + # ---- public API -------------------------------------------------------- + + def start(self): + """Start serving in a daemon thread.""" + self._thread = threading.Thread(target=self._httpd.serve_forever) + self._thread.daemon = True + self._thread.start() + + def stop(self): + """Shut down the server and wait for the thread to exit.""" + self._httpd.shutdown() + if self._thread is not None: + self._thread.join() + + # ---- internals --------------------------------------------------------- + + def _record_request(self, method, path, headers): + with self._lock: + self.requests.append( + {"method": method, "path": path, "headers": headers} + ) + + +def start_test_server(host=DEFAULT_HOST, port=DEFAULT_PORT, ssl_context=None): + """Create, start, and return a TestHTTPServer instance.""" + server = TestHTTPServer(host=host, port=port, ssl_context=ssl_context) + server.start() + return server + + +if __name__ == "__main__": + srv = start_test_server() + print(f"Test server listening on {srv.host}:{srv.port}") + try: + srv._thread.join() + except KeyboardInterrupt: + print("\nShutting down.") + srv.stop() From 7df7d59c80d4dbc824bd2ed85f4f43a5875bbe41 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Mon, 30 Mar 2026 19:14:57 -0500 Subject: [PATCH 04/22] Import optimized X25519 from c64-x25519 with unit tests and benchmark Replace baseline fe25519/x25519 with optimized versions from the c64-x25519 performance tuning project, achieving ~30% speedup (12,782 jiffies / 3.6 min per key generation vs 18,005 baseline). Optimizations imported: - REU DMA multiplication tables (128KB REU, 4 cyc/product vs mul_8x8) - mult66 indirect-indexed quarter-square multiply for fe_sqr - Self-modifying accumulation addresses in fe_mul/fe_sqr inner loops - 4x unrolled constant-time fe_cswap (38 cyc/byte vs 49) - Shift-before-accumulate for fe_sqr cross terms - mul_by_38 lookup tables for fe_reduce_wide Key integration fixes: - Optimization tables (mul_dma, sqtab2, mul38) placed early in data.asm to stay below $A000 and avoid BASIC ROM shadow region - BASIC ROM banked out at boot and kept off during runtime (data buffers at $A000+ need direct RAM access) - VICE launched with -reu -reusize 512 for all test suites - Zero page lmul0/lmul1 pointers time-shared with ChaCha20 vars New files: - tools/test_x25519.py: 71 unit tests (fe25519 field ops + x25519_clamp + optional --slow RFC 7748 scalarmult vectors) - tools/bench_x25519.py: key generation benchmark with jiffy clock timing and Python X25519 verification Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 24 +- build/c64-https.prg | Bin 39573 -> 41278 bytes build/labels.txt | 1065 +++++++++++++++++++++------------------- src/boot.asm | 140 ++++++ src/constants.asm | 18 +- src/crypto/fe25519.asm | 666 ++++++++++++++++++++++--- src/crypto/x25519.asm | 97 ++-- src/data.asm | 45 ++ src/http.asm | 148 +++++- tools/bench_x25519.py | 208 ++++++++ tools/run_all_tests.py | 10 +- tools/test_x25519.py | 743 ++++++++++++++++++++++++++++ 12 files changed, 2470 insertions(+), 694 deletions(-) create mode 100644 tools/bench_x25519.py create mode 100644 tools/test_x25519.py diff --git a/README.md b/README.md index ea9ee7a..4175125 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Target: **TLS_CHACHA20_POLY1305_SHA256** (0x1303) - **AEAD:** ChaCha20-Poly1305 (from [c64-wireguard](../c64-wireguard)) - **Hash:** SHA-256 (from [c64-aes256-ecdsa](../c64-aes256-ecdsa)) -- **Key exchange:** ECDHE with secp256r1 / P-256 (from c64-aes256-ecdsa) +- **Key exchange:** ECDHE with X25519 (optimized: REU DMA multiply, self-mod code, ~3.6 min/op) - **Key derivation:** HKDF-SHA256 (new, built from HMAC-SHA256) - **PRNG:** HMAC-DRBG seeded from SID+CIA entropy (from c64-aes256-ecdsa) @@ -47,7 +47,8 @@ The crypto modules and ip65 overlap on zero page $02-$1B. Rather than relocating $02-$03 Shared tmp (save/restore around ip65 calls) $04-$09 word32 pointers (ChaCha20) $0A-$12 SHA-256 accumulators -$14-$1D ChaCha20 + Poly1305 vars +$14-$17 mult66 pointers (fe25519) / ChaCha20 vars (time-shared) +$18-$1D ChaCha20 + Poly1305 vars $22-$3C ECDSA bignum / field arithmetic $FB-$FE General pointers (save/restore around ip65 calls) ``` @@ -68,8 +69,10 @@ $4000-$5FFF Crypto: ChaCha20, Poly1305, AEAD (~8 KB) $6000-$6FFF Crypto: SHA-256, HMAC-SHA256, HKDF (~4 KB) $7000-$77FF Crypto: ECDSA/ECDH P-256 (~2 KB) $7800-$7BFF Quarter-square multiply table (1 KB, runtime-generated) -$7C00-$9FFF Data buffers: TLS state, record buffers (~9 KB) -$A000-$BFFF BASIC ROM (banked out for RAM if needed) +$7C00-$8DFF Code: ECDSA verify, DER decode, TLS cert, ECDH (~4.5 KB) +$8E00-$93FF Optimization tables: REU DMA, sqtab2, mul38 (~1.5 KB, below ROM) +$9400-$BFFF Data buffers: TLS state, crypto state, record buffers (~11 KB) + ($A000-$BFFF under BASIC ROM, banked out at boot) $C000-$CFFF Free RAM (4 KB, overflow buffers) $DE00-$DE0F RR-Net CS8900a I/O registers (directly accessed by ip65) ``` @@ -97,12 +100,13 @@ The Makefile automatically builds ip65 from the submodule into a flat binary blo ## Project Status -Current status (24.8 KB binary, 487 labels): +Current status (40 KB binary, 537 labels): - [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) - [x] Network wrapper with ZP time-sharing — save/restore $02-$1B around ip65 calls -- [x] Crypto primitives — ChaCha20, Poly1305, AEAD (from c64-wireguard), SHA-256, HMAC-DRBG (from c64-aes256-ecdsa), x25519/fe25519 (from c64-wireguard) +- [x] Crypto primitives — ChaCha20, Poly1305, AEAD (from c64-wireguard), SHA-256, HMAC-DRBG (from c64-aes256-ecdsa) +- [x] Optimized X25519/fe25519 — REU DMA multiply tables, mult66 quarter-square, self-mod code, 4x-unrolled cswap (~30% faster, 12,782 jiffies / 3.6 min per keygen) - [x] HKDF-SHA256 — Extract, Expand, Expand-Label, Derive-Secret (RFC 5869 + TLS 1.3) - [x] TLS 1.3 record layer — encrypt/decrypt with ChaCha20-Poly1305, nonce construction, sequence numbers - [x] TLS 1.3 handshake — ClientHello builder (x25519 key_share, SNI), ServerHello parser, streaming transcript hash @@ -121,12 +125,12 @@ Current status (24.8 KB binary, 487 labels): ## Test Automation -193 tests across 10 suites (+ 1 standalone diagnostic), using the [`c64-test-harness`](../c64-test-harness) package to drive VICE via its binary monitor protocol. The parallel runner allocates a fresh VICE instance per suite to avoid state contamination. All tests log VICE PID and port for multi-agent safety. +253 tests across 11 suites (+ 1 standalone diagnostic), using the [`c64-test-harness`](../c64-test-harness) package to drive VICE via its binary monitor protocol. The parallel runner allocates a fresh VICE instance per suite (with REU support for x25519) to avoid state contamination. All tests log VICE PID and port for multi-agent safety. ```bash pip install -e ../c64-test-harness -# Run all 10 suites in parallel (one VICE instance per suite, ~5 min with ECDSA) +# Run all 11 suites in parallel (one VICE instance per suite, ~5 min with ECDSA) python3 tools/run_all_tests.py python3 tools/run_all_tests.py --skip-slow # Skip x509/ECDSA (~5s wall time) python3 tools/run_all_tests.py --workers 6 # Limit concurrent VICE instances @@ -142,8 +146,12 @@ python3 tools/test_tls_handshake.py # 21 tests: transcript hash, ClientHello, Se python3 tools/test_keyschedule_steps.py # 9 tests: key schedule step-by-step (RFC 8448 vectors) python3 tools/test_entropy.py # 7 tests: SID/CIA hardware init, DRBG seeding, output quality python3 tools/test_http.py # 27 tests: HTTP/1.1 GET builder, response parser, status codes +python3 tools/test_x25519.py # 71 tests: fe25519 field ops, x25519_clamp, scalarmult (--slow for RFC 7748 vectors) python3 tools/test_chained_hmac.py # 10 tests: chained HMAC-SHA256 stability (N=1..10, standalone) +# Benchmark +python3 tools/bench_x25519.py # X25519 key generation (~3.6 min C64 time, ~8s warp) + # Integration tests (require tap-c64 interface, dnsmasq; see scripts/setup-tap-networking.sh in c64-test-harness) python3 tools/test_dns.py # 4 tests: DNS resolution via ip65 over TAP (known host, second host, unknown host) python3 tools/test_http_integration.py # 5 tests: end-to-end plain HTTP GET over TAP (DNS + TCP + request/response) diff --git a/build/c64-https.prg b/build/c64-https.prg index 39f1ff62924681edb17d5c01fdd72790e8efe242..1b33b3808c64095dc77aa6f9a0678e1019dfee4e 100644 GIT binary patch literal 41278 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 literal 39573 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 S$$z_oe~Z<3h5iRU@c#fZ8o diff --git a/build/labels.txt b/build/labels.txt index 16e5daa..fe04bb2 100644 --- a/build/labels.txt +++ b/build/labels.txt @@ -47,6 +47,7 @@ al C:d414 .sid_v3_sr al C:0014 .TLS_CT_CHANGE_CIPHER al C:ffc9 .chkout al C:2031 .ip65_vt_tcp_snd_len +al C:df00 .reu_status al C:000d .TLS_EXT_SIG_ALGORITHMS al C:0017 .TLS_GROUP_SECP256R1 al C:0015 .TLS_CT_ALERT @@ -55,6 +56,7 @@ al C:d800 .color_ram al C:0001 .TLS_ALERT_WARNING al C:0002 .TLS_STATE_SERVER_HELLO +al C:df0a .reu_addr_ctrl al C:0035 .fe_mul_j al C:0034 .fe_mul_i al C:0004 .TLS_STATE_CERTIFICATE @@ -70,6 +72,10 @@ al C:d412 .sid_v3_ctrl al C:0005 .TLS_STATE_CERT_VERIFY al C:d41b .sid_osc3 al C:0008 .TLS_HS_ENCRYPTED_EXT +al C:df05 .reu_reu_hi +al C:df04 .reu_reu_lo +al C:0014 .lmul0 +al C:0016 .lmul1 al C:002a .fp_carry al C:0004 .w32_src1 al C:002c .fe_src1 @@ -96,10 +102,12 @@ al C:2012 .ip65_tcp_close al C:4073 .ip65_dns_ip_addr al C:001a .poly_i al C:001b .poly_j +al C:df01 .reu_command al C:000a .sha_temp1 al C:0032 .fe_carry al C:000e .sha_temp2 al C:0039 .x25_byte_idx +al C:0100 .i al C:00ff .TLS_STATE_ERROR al C:00fb .zp_ptr al C:0001 .TLS_HS_CLIENT_HELLO @@ -117,6 +125,7 @@ al C:2009 .ip65_dns_resolve al C:001e .tls_rec_ptr al C:00fd .zp_temp al C:0038 .x25_prev_bit +al C:df06 .reu_reu_bank al C:001d .poly_tmp al C:0022 .fp_src1 al C:0024 .fp_src2 @@ -127,7 +136,11 @@ al C:000f .http_host_zimmers_len al C:0018 .cc20_remain al C:4f48 .ip65_tcp_snd_len al C:0002 .ip65_zp_start +al C:df08 .reu_len_hi +al C:df02 .reu_c64_lo al C:0002 .TLS_HS_SERVER_HELLO +al C:df07 .reu_len_lo +al C:df03 .reu_c64_hi al C:dc04 .cia1_ta_lo al C:3a8a .ip65_cfg_ip al C:0028 .fp_misc @@ -145,568 +158,584 @@ al C:000d .http_host_apple_len al C:2003 .ip65_process al C:202f .ip65_vt_tcp_in_len -al C:8f9b .tls_hs_write_iv -al C:637c .ec_point_add -al C:661e .ec_sc_byte +al C:9623 .tls_hs_write_iv +al C:672e .ec_point_add +al C:69d0 .ec_sc_byte al C:80af .der_skip_tlv -al C:50e8 .drbg_fill_bytes -al C:1db0 .lbl_derived -al C:4d87 .sha256_shr3 -al C:4d6c .sha256_rotr22 -al C:51a9 .fe_mul -al C:467e .sha256_h1_init +al C:51bf .drbg_fill_bytes +al C:1e8a .lbl_derived +al C:4e5e .sha256_shr3 +al C:4e43 .sha256_rotr22 +al C:5353 .fe_mul +al C:4755 .sha256_h1_init al C:83b0 .cert_sig_s -al C:4d7b .sha256_rotr25 -al C:571f .x25519_ladder_step +al C:4e52 .sha256_rotr25 +al C:5b19 .x25519_ladder_step al C:831f .cert_pubkey al C:8380 .cert_sig_r -al C:0a7f .menu_msg -al C:9d4f .aead_scratch -al C:6886 .fp_mod_add_384 -al C:5106 .fe_zero -al C:9411 .hkdf_context_len -al C:5110 .fe_one -al C:0de8 .net_send_ptr -al C:0aa1 .init_msg -al C:8fd3 .tls_app_write_key -al C:a041 .ecdsa_sig_s -al C:6620 .ec_affine_x -al C:6822 .fp_b_byte_384 -al C:5a0c .fp_s_hi -al C:479a .sha256_init -al C:a011 .ecdsa_sig_r -al C:6640 .ec_affine_y -al C:0d95 .cb_remaining -al C:467a .sha256_h0_init -al C:5a0f .fp_wide -al C:65c1 .ec_scalar_mul -al C:1df4 .tls_c_hs_secret -al C:1361 .tls_record_send_plaintext +al C:0a91 .menu_msg +al C:a3d7 .aead_scratch +al C:6c38 .fp_mod_add_384 +al C:51dd .fe_zero +al C:9a99 .hkdf_context_len +al C:51e7 .fe_one +al C:0ec2 .net_send_ptr +al C:0ab3 .init_msg +al C:965b .tls_app_write_key +al C:9300 .mul38_hi_tab +al C:a6ea .ecdsa_sig_s +al C:69d2 .ec_affine_x +al C:6bd4 .fp_b_byte_384 +al C:5dbe .fp_s_hi +al C:a6ba .ecdsa_sig_r +al C:4871 .sha256_init +al C:69f2 .ec_affine_y +al C:0e6f .cb_remaining +al C:4751 .sha256_h0_init +al C:5dc1 .fp_wide +al C:6973 .ec_scalar_mul +al C:1ece .tls_c_hs_secret +al C:143b .tls_record_send_plaintext al C:8d36 .tls_ecdh_compute_shared -al C:70a0 .ec_t6_384 -al C:45cc .aead_compute_tag -al C:671e .fp_add_384 -al C:5ae2 .fp_mod_reduce -al C:5b86 .fp_mod_mul -al C:6710 .fp_cmp_384 -al C:13a1 .tls_build_client_hello -al C:4686 .sha256_h3_init -al C:1d0b .tls_compute_finished -al C:98f5 .sha256_block -al C:41f1 .chacha20_encrypt -al C:1e14 .tls_s_hs_secret -al C:0bdc .send_ok_msg -al C:9273 .tls_hs_buf -al C:3fbc .copy32 -al C:3f8b .rotl32_12 -al C:a0a2 .ecdsa_pubkey_y -al C:a072 .ecdsa_pubkey_x -al C:58f8 .fp_copy -al C:1915 .hkdf_expand_label -al C:8f3b .tls_transcript -al C:430d .sq_ad -al C:7100 .ec_point_double_384 +al C:7452 .ec_t6_384 +al C:46a3 .aead_compute_tag +al C:6ad0 .fp_add_384 +al C:5e94 .fp_mod_reduce +al C:6ac2 .fp_cmp_384 +al C:5f38 .fp_mod_mul +al C:147b .tls_build_client_hello +al C:475d .sha256_h3_init +al C:1de5 .tls_compute_finished +al C:9f7d .sha256_block +al C:42c8 .chacha20_encrypt +al C:1eee .tls_s_hs_secret +al C:0bee .send_ok_msg +al C:98fb .tls_hs_buf +al C:4093 .copy32 +al C:4062 .rotl32_12 +al C:a74b .ecdsa_pubkey_y +al C:a71b .ecdsa_pubkey_x +al C:5caa .fp_copy +al C:19ef .hkdf_expand_label +al C:95c3 .tls_transcript +al C:43e4 .sq_ad al C:83e0 .cert_sig_len -al C:0adb .dhcp_msg -al C:6bf0 .fp_inv_x2_384 -al C:4682 .sha256_h2_init +al C:74b2 .ec_point_double_384 +al C:0aed .dhcp_msg +al C:6fa2 .fp_inv_x2_384 +al C:4759 .sha256_h2_init al C:7db8 .ecdsa_verify_384 -al C:9405 .hkdf_info_len -al C:0831 .main_loop -al C:7672 .ec_jacobian_to_affine_384 -al C:0c68 .net_tcp_connect -al C:98a8 .input_length -al C:9478 .http_path_len -al C:9041 .tls_rec_len -al C:736e .ec_point_add_384 -al C:7610 .ec_sc_byte_384 -al C:430a .sq_sh +al C:9a8d .hkdf_info_len +al C:0843 .main_loop +al C:7a24 .ec_jacobian_to_affine_384 +al C:0c1b .reu_mul_init +al C:0d42 .net_tcp_connect +al C:9f30 .input_length +al C:9b00 .http_path_len +al C:96c9 .tls_rec_len +al C:43e1 .sq_sh +al C:7720 .ec_point_add_384 +al C:79c2 .ec_sc_byte_384 al C:831d .cert_tbs_len -al C:1260 .tls_record_read -al C:45b5 .aead_setup_chacha -al C:6660 .ec_jacobian_to_affine -al C:7070 .ec_t5_384 -al C:0f06 .tls_recv_server_hello -al C:5d29 .fp_inv_iter -al C:a0d2 .ecdsa_verify_tmp -al C:461b .aead_process_padded -al C:9c7b .cc20_key -al C:468e .sha256_h5_init +al C:133a .tls_record_read +al C:468c .aead_setup_chacha +al C:6a12 .ec_jacobian_to_affine +al C:a668 .mul_src2_buf +al C:7422 .ec_t5_384 +al C:0fe0 .tls_recv_server_hello +al C:60db .fp_inv_iter +al C:a77b .ecdsa_verify_tmp +al C:46f2 .aead_process_padded +al C:a303 .cc20_key +al C:4765 .sha256_h5_init al C:8d11 .cert_data_ptr -al C:8e7a .tls_state -al C:9b59 .drbg_seed -al C:5ebe .ec_gx -al C:0ec3 .tls_close -al C:8edb .tls_ecdhe_pubkey -al C:5ede .ec_gy -al C:1855 .entropy_init -al C:3dbd .add32 +al C:9502 .tls_state +al C:a1e1 .drbg_seed +al C:6270 .ec_gx +al C:0f9d .tls_close +al C:9563 .tls_ecdhe_pubkey +al C:6290 .ec_gy +al C:192f .entropy_init +al C:3e94 .add32 al C:83e2 .cert_buf -al C:6776 .fp_mul_384 -al C:6bc0 .fp_inv_x1_384 +al C:6b28 .fp_mul_384 +al C:6f72 .fp_inv_x1_384 al C:8c2e .tls_handle_cert_verify -al C:468a .sha256_h4_init -al C:4b7a .sha256_ch -al C:3da6 .http_conn_hdr -al C:1e74 .tls_finished_key +al C:4761 .sha256_h4_init +al C:4c51 .sha256_ch +al C:3e7d .http_conn_hdr +al C:1f4e .tls_finished_key al C:83e1 .cert_curve_id -al C:957f .http_resp_buf -al C:69cc .fp_bm_384 -al C:7040 .ec_t4_384 +al C:9c07 .http_resp_buf +al C:6d7e .fp_bm_384 +al C:8e00 .mul_dma_lo +al C:73f2 .ec_t4_384 al C:7c00 .ecdsa_verify -al C:0bee .failed_msg -al C:9bbb .cc20_state -al C:4696 .sha256_h7_init +al C:0c00 .failed_msg +al C:a243 .cc20_state +al C:476d .sha256_h7_init al C:8d14 .cert_parse_pos -al C:4271 .sqtab_init -al C:9d2d .aead_nonce -al C:16d1 .tls_transcript_block -al C:940b .hkdf_ikm_len -al C:4313 .mul_8x8 -al C:5607 .fe_inv_sqr_cnt +al C:4348 .sqtab_init +al C:8f00 .mul_dma_hi +al C:a3b5 .aead_nonce +al C:17ab .tls_transcript_block +al C:43ea .mul_8x8 +al C:9a93 .hkdf_ikm_len +al C:5a05 .fe_inv_sqr_cnt al C:82f5 .oid_ec_pubkey -al C:940c .hkdf_label_ptr -al C:9d3f .aead_tag -al C:4692 .sha256_h6_init -al C:957d .http_req_len -al C:9a55 .sha256_len -al C:4363 .poly1305_multiply -al C:0bf6 .done_msg -al C:6919 .fp_mod_reduce_384 -al C:69cd .fp_mod_mul_384 -al C:9412 .hkdf_out_len -al C:3f09 .rotl32_8 -al C:9f3f .x25_b -al C:5d9e .fp_inv_x2 -al C:9e3f .x25_scalar -al C:6e00 .ec_p1_384 -al C:7010 .ec_t3_384 -al C:0d4e .net_tcp_recv_cb -al C:9f1f .x25_a -al C:5d7e .fp_inv_x1 -al C:9f7f .x25_cb +al C:9a94 .hkdf_label_ptr +al C:a3c7 .aead_tag +al C:a667 .mul_cached_a +al C:4769 .sha256_h6_init +al C:9c05 .http_req_len +al C:a0dd .sha256_len +al C:443a .poly1305_multiply +al C:0c08 .done_msg +al C:6ccb .fp_mod_reduce_384 +al C:6d7f .fp_mod_mul_384 +al C:9a9a .hkdf_out_len +al C:3fe0 .rotl32_8 +al C:a5c7 .x25_b +al C:6150 .fp_inv_x2 +al C:a4c7 .x25_scalar +al C:71b2 .ec_p1_384 +al C:73c2 .ec_t3_384 +al C:0e28 .net_tcp_recv_cb +al C:a5a7 .x25_a +al C:6130 .fp_inv_x1 +al C:a607 .x25_cb al C:8d18 .cert_end_lo -al C:a1a2 .ev_u1_384 -al C:6c80 .fp_r2_384 -al C:08a7 .print_string -al C:0c81 .net_set_tcp_dest -al C:9f9f .x25_e +al C:a84b .ev_u1_384 +al C:7032 .fp_r2_384 +al C:08b9 .print_string +al C:0d5b .net_set_tcp_dest +al C:a627 .x25_e al C:8d19 .cert_end_hi -al C:3ddc .add32_to_dst -al C:9bba .drbg_buf_idx -al C:0c3d .net_dhcp -al C:0c09 .http_host_zimmers -al C:8fc7 .tls_hs_read_iv -al C:3ee4 .rotl32_1 -al C:5a0a .fp_a_byte -al C:9b38 .hmac_data_len +al C:3eb3 .add32_to_dst +al C:a242 .drbg_buf_idx +al C:0d17 .net_dhcp +al C:0ccf .reu_fetch_mul_row +al C:0ce3 .http_host_zimmers +al C:964f .tls_hs_read_iv +al C:3fbb .rotl32_1 +al C:5dbc .fp_a_byte +al C:a1c0 .hmac_data_len +al C:9200 .mul38_lo_tab al C:8cec .cv_label -al C:506d .extra_sid_lo -al C:4271 .fp_init_sqtab -al C:1d66 .tls_verify_finished -al C:9e1f .fe_p +al C:5144 .extra_sid_lo +al C:4348 .fp_init_sqtab +al C:1e40 .tls_verify_finished +al C:a4a7 .fe_p al C:809f .der_skip -al C:8ebb .tls_ecdhe_privkey -al C:1714 .tls_transcript_save -al C:3f2c .rotl32_4 -al C:a142 .ev_point_save -al C:1222 .tls_record_write -al C:3fb6 .rotl32_7 -al C:506e .extra_sid_hi -al C:9b99 .drbg_seed_len -al C:44c7 .poly1305_final -al C:5a0b .fp_b_byte -al C:1dc3 .lbl_s_hs_traffic +al C:9543 .tls_ecdhe_privkey +al C:12fc .tls_record_write +al C:17ee .tls_transcript_save +al C:4003 .rotl32_4 +al C:a7eb .ev_point_save +al C:408d .rotl32_7 +al C:5145 .extra_sid_hi +al C:a221 .drbg_seed_len +al C:459e .poly1305_final +al C:5dbd .fp_b_byte +al C:1e9d .lbl_s_hs_traffic al C:3b27 .http_get -al C:6fe0 .ec_t2_384 -al C:610e .ec_point_double -al C:1dec .lbl_finished -al C:595f .fp_mul -al C:0e6c .tls_send -al C:1734 .tls_transcript_init -al C:1db7 .lbl_c_hs_traffic +al C:64c0 .ec_point_double +al C:7392 .ec_t2_384 +al C:1ec6 .lbl_finished +al C:5d11 .fp_mul +al C:0f46 .tls_send +al C:180e .tls_transcript_init +al C:1e91 .lbl_c_hs_traffic al C:8d0e .cert_list_len_lo -al C:8fa7 .tls_hs_read_key -al C:6d10 .ec_n_384 -al C:6cb0 .fp_r3_384 -al C:0d78 .cb_copy_byte -al C:1d90 .empty_hash -al C:0ec9 .tls_send_client_hello -al C:9e5f .x25_u -al C:0a30 .banner_msg -al C:69d4 .fp_mod_inv_384 +al C:962f .tls_hs_read_key +al C:70c2 .ec_n_384 +al C:7062 .fp_r3_384 +al C:0e52 .cb_copy_byte +al C:1e6a .empty_hash +al C:5571 .mul38_in +al C:0fa3 .tls_send_client_hello +al C:a4e7 .x25_u +al C:0a42 .banner_msg +al C:6d86 .fp_mod_inv_384 al C:8d0d .cert_list_len_hi -al C:3dbb .http_bg_idx -al C:97a4 .tls_app_ptr -al C:1685 .tls_parse_encrypted_extensions -al C:0aba .net_fail_msg -al C:0b64 .dns_fail_msg -al C:9d3e .aead_data_len -al C:0c19 .http_host_apple -al C:3dfb .xor32 -al C:0b0b .no_net_msg -al C:0d2f .net_recv_ready -al C:0aef .dhcp_fail_msg -al C:9040 .tls_rec_type -al C:9cab .poly_h -al C:43d4 .poly1305_reduce -al C:4ce3 .sha256_rotr1 -al C:6f20 .ec_p3_384 -al C:6fb0 .ec_t1_384 -al C:4d18 .sha256_rotr2 -al C:4a2e .sha256_load_word -al C:197f .tls_derive_secret -al C:0acf .net_ok_msg -al C:0a19 .print_resp_body -al C:0b78 .dns_ok_msg -al C:6c20 .fp_r0_384 -al C:16d0 .tls_hostname_len -al C:4f53 .hmac_drbg_update -al C:70d0 .ec_set_modp_384 -al C:3e19 .xor32_in_place -al C:4d1e .sha256_rotr6 -al C:6754 .fp_rshift1_384 -al C:9406 .hkdf_salt_ptr -al C:4238 .poly1305_clamp -al C:55e9 .fe_inv_dst -al C:0b94 .tcp_ok_msg -al C:4dbd .hmac_sha256 -al C:4d27 .sha256_rotr7 -al C:98e9 .sha_temp3 -al C:9d39 .aead_aad_ptr -al C:58de .x25519_base -al C:4d05 .sha256_rotr8 -al C:6746 .fp_is_zero_384 -al C:0c29 .net_init -al C:5a0e .fp_p_hi -al C:661f .ec_sc_mask -al C:8e78 .tcp_recv_head -al C:50c0 .drbg_random_byte +al C:3e92 .http_bg_idx +al C:9e2c .tls_app_ptr +al C:175f .tls_parse_encrypted_extensions +al C:0acc .net_fail_msg +al C:0b76 .dns_fail_msg +al C:a3c6 .aead_data_len +al C:0cf3 .http_host_apple +al C:3ed2 .xor32 +al C:0b1d .no_net_msg +al C:0e09 .net_recv_ready +al C:0b01 .dhcp_fail_msg +al C:a333 .poly_h +al C:44ab .poly1305_reduce +al C:4dba .sha256_rotr1 +al C:96c8 .tls_rec_type +al C:7362 .ec_t1_384 +al C:72d2 .ec_p3_384 +al C:4b05 .sha256_load_word +al C:4def .sha256_rotr2 +al C:1a59 .tls_derive_secret +al C:0ae1 .net_ok_msg +al C:0a2b .print_resp_body +al C:0b8a .dns_ok_msg +al C:6fd2 .fp_r0_384 +al C:17aa .tls_hostname_len +al C:502a .hmac_drbg_update +al C:7482 .ec_set_modp_384 +al C:3ef0 .xor32_in_place +al C:4df5 .sha256_rotr6 +al C:6b06 .fp_rshift1_384 +al C:9a8e .hkdf_salt_ptr +al C:430f .poly1305_clamp +al C:59e7 .fe_inv_dst +al C:0ba6 .tcp_ok_msg +al C:4e94 .hmac_sha256 +al C:4dfe .sha256_rotr7 +al C:9f71 .sha_temp3 +al C:a3c1 .aead_aad_ptr +al C:5c90 .x25519_base +al C:4ddc .sha256_rotr8 +al C:6af8 .fp_is_zero_384 +al C:0d03 .net_init +al C:5dc0 .fp_p_hi +al C:69d1 .ec_sc_mask +al C:9500 .tcp_recv_head +al C:5197 .drbg_random_byte al C:837f .cert_pubkey_len -al C:6da0 .ec_gx_384 +al C:7152 .ec_gx_384 al C:80d1 .x509_parse_cert -al C:0bb9 .tls_ok_msg -al C:0d91 .cb_done -al C:4cf4 .sha256_rotl1 -al C:174e .tls_transcript_update -al C:5a0d .fp_p_lo -al C:0afc .dhcp_ok_msg -al C:8f7b .tls_hs_write_key -al C:4584 .aead_derive_otk -al C:6e90 .ec_p2_384 -al C:6763 .fp_chk_one_384 -al C:3da0 .http_host_hdr -al C:a1d2 .ev_u2_384 -al C:0dd2 .net_save_zp -al C:6c50 .fp_r1_384 -al C:5608 .x25519_clamp -al C:5942 .fp_is_zero -al C:47da .sha256_update +al C:0bcb .tls_ok_msg +al C:0e6b .cb_done +al C:4dcb .sha256_rotl1 +al C:1828 .tls_transcript_update +al C:5dbf .fp_p_lo +al C:0b0e .dhcp_ok_msg +al C:9603 .tls_hs_write_key +al C:7242 .ec_p2_384 +al C:465b .aead_derive_otk +al C:6b15 .fp_chk_one_384 +al C:3e77 .http_host_hdr +al C:a87b .ev_u2_384 +al C:0eac .net_save_zp +al C:7002 .fp_r1_384 +al C:5a06 .x25519_clamp +al C:5cf4 .fp_is_zero +al C:48b1 .sha256_update al C:8d10 .cert_data_len_lo -al C:4664 .aead_verify_tag -al C:9ccc .poly_s -al C:506f .drbg_init_entropy -al C:9cbc .poly_r -al C:4a3d .sha256_load_word_to_temp2 -al C:4b96 .sha256_maj -al C:0b2b .http_get_msg -al C:9473 .http_host_ptr +al C:473b .aead_verify_tag +al C:a354 .poly_s +al C:5146 .drbg_init_entropy +al C:a344 .poly_r +al C:4b14 .sha256_load_word_to_temp2 +al C:4c6d .sha256_maj +al C:0b3d .http_get_msg +al C:9afb .http_host_ptr al C:8d0f .cert_data_len_hi -al C:0ddd .net_restore_zp -al C:6dd0 .ec_gy_384 -al C:a292 .ev_der_int_len -al C:9bfb .cc20_work -al C:4362 .mul_s_pg -al C:a293 .ev_der_copy_cnt -al C:0b48 .https_get_msg -al C:5022 .hmac_drbg_instantiate -al C:9453 .tls_master_secret -al C:4a4c .sha256_add_temp2_to_temp1 -al C:469a .sha256_k -al C:511a .fe_add +al C:0eb7 .net_restore_zp +al C:7182 .ec_gy_384 +al C:a93b .ev_der_int_len +al C:a283 .cc20_work +al C:4439 .mul_s_pg +al C:a93c .ev_der_copy_cnt +al C:0b5a .https_get_msg +al C:50f9 .hmac_drbg_instantiate +al C:9adb .tls_master_secret +al C:4b23 .sha256_add_temp2_to_temp1 +al C:4771 .sha256_k +al C:51f1 .fe_add al C:89e4 .tls_handle_certificate -al C:4d2d .sha256_rotr11 -al C:9b9a .drbg_output -al C:6b90 .fp_inv_v_384 -al C:199b .tls_derive_handshake_keys -al C:4311 .poly_prod_lo -al C:4d39 .sha256_rotr13 -al C:4888 .sha256_process_block -al C:1690 .tls_hostname -al C:70d9 .ec_set_modn_384 -al C:9784 .http_line_buf +al C:4e04 .sha256_rotr11 +al C:a222 .drbg_output +al C:6f42 .fp_inv_v_384 +al C:43e8 .poly_prod_lo +al C:1a75 .tls_derive_handshake_keys +al C:495f .sha256_process_block +al C:4e10 .sha256_rotr13 +al C:176a .tls_hostname +al C:748b .ec_set_modn_384 al C:8d1a .cert_bs_len -al C:5b8d .fp_mod_inv -al C:0c91 .net_tcp_send -al C:4471 .poly1305_update -al C:1010 .tls_select_keys -al C:4312 .poly_prod_hi -al C:3db9 .http_crlf -al C:4d48 .sha256_rotr17 -al C:4d51 .sha256_rotr18 -al C:4d5d .sha256_rotr19 -al C:5141 .fe_sub -al C:6821 .fp_a_byte_384 -al C:1186 .tls_record_decrypt +al C:9e0c .http_line_buf +al C:5f3f .fp_mod_inv +al C:43e9 .poly_prod_hi +al C:0d6b .net_tcp_send +al C:4548 .poly1305_update +al C:10ea .tls_select_keys +al C:3e90 .http_crlf +al C:4e1f .sha256_rotr17 +al C:4e28 .sha256_rotr18 +al C:4e34 .sha256_rotr19 +al C:5218 .fe_sub +al C:6bd3 .fp_a_byte_384 +al C:1260 .tls_record_decrypt al C:831b .cert_tbs_ptr -al C:9476 .http_path_ptr -al C:902b .tls_write_seq -al C:0c49 .net_poll -al C:8f5b .tls_transcript_h0 -al C:5a4f .fp_mod_add -al C:4bba .sha256_add_to_hash -al C:8f5f .tls_transcript_h1 -al C:9ca7 .cc20_counter -al C:8f63 .tls_transcript_h2 -al C:8d78 .tcp_recv_buf -al C:451c .aead_encrypt -al C:8f67 .tls_transcript_h3 -al C:940f .hkdf_context_ptr -al C:9cdc .poly_product -al C:9782 .http_hdr_match -al C:8f6b .tls_transcript_h4 -al C:6d70 .ec_b_384 -al C:9373 .tls_hs_len -al C:1db0 .empty_context -al C:53d5 .fe_inv -al C:8f6f .tls_transcript_h5 -al C:9fbf .x25_basepoint -al C:5950 .fp_rshift1 -al C:8f73 .tls_transcript_h6 -al C:0d97 .net_init_cb_addrs -al C:8f77 .tls_transcript_h7 -al C:0867 .do_net_init -al C:9dbf .fe_tmp2 -al C:52bd .fe_sqr -al C:9ddf .fe_tmp3 -al C:a071 .ecdsa_sig_len -al C:9c3b .cc20_keystream -al C:5aaf .fp_mod_sub -al C:0b80 .tcp_fail_msg -al C:9d9f .fe_tmp1 -al C:0dea .net_send_len -al C:156a .tls_parse_server_hello -al C:9935 .sha256_w -al C:5b63 .fp_rem -al C:9043 .tls_rec_buf -al C:1293 .tls_recv_record -al C:9375 .hkdf_prk -al C:9dff .fe_tmp4 -al C:1de7 .lbl_key -al C:66fc .fp_copy_384 -al C:5225 .fe_reduce_wide -al C:9ebf .x25_z2 -al C:9edf .x25_x3 -al C:93b5 .hkdf_info_buf -al C:9e9f .x25_x2 -al C:9eff .x25_z3 -al C:08b8 .do_http_get -al C:139f .tls_recv_count -al C:0c53 .net_dns_resolve -al C:3e5c .rotr32_8 -al C:17af .tls_transcript_hash -al C:9a57 .hmac_key -al C:0bea .ok_msg -al C:0f60 .tls_recv_encrypted -al C:9a97 .hmac_opad_block -al C:9d5f .fe_wide -al C:6d40 .ec_a_384 -al C:98e5 .sha_h +al C:9afe .http_path_ptr +al C:96b3 .tls_write_seq +al C:0d23 .net_poll +al C:95e3 .tls_transcript_h0 +al C:5e01 .fp_mod_add +al C:4c91 .sha256_add_to_hash +al C:95e7 .tls_transcript_h1 +al C:a32f .cc20_counter +al C:95eb .tls_transcript_h2 +al C:9400 .tcp_recv_buf +al C:45f3 .aead_encrypt +al C:95ef .tls_transcript_h3 +al C:9a97 .hkdf_context_ptr +al C:a364 .poly_product +al C:9e0a .http_hdr_match +al C:95f3 .tls_transcript_h4 +al C:7122 .ec_b_384 +al C:99fb .tls_hs_len +al C:1e8a .empty_context +al C:57d3 .fe_inv +al C:95f7 .tls_transcript_h5 +al C:a647 .x25_basepoint +al C:5d02 .fp_rshift1 +al C:95fb .tls_transcript_h6 +al C:0e71 .net_init_cb_addrs +al C:95ff .tls_transcript_h7 +al C:0879 .do_net_init +al C:0ccd .reu_init_a +al C:a447 .fe_tmp2 +al C:5574 .fe_sqr +al C:a467 .fe_tmp3 +al C:a71a .ecdsa_sig_len +al C:a2c3 .cc20_keystream +al C:5e61 .fp_mod_sub +al C:0b92 .tcp_fail_msg +al C:a427 .fe_tmp1 +al C:0cce .reu_init_b +al C:0ec4 .net_send_len +al C:1644 .tls_parse_server_hello +al C:9fbd .sha256_w +al C:5f15 .fp_rem +al C:96cb .tls_rec_buf +al C:136d .tls_recv_record +al C:99fd .hkdf_prk +al C:a487 .fe_tmp4 +al C:1ec1 .lbl_key +al C:6aae .fp_copy_384 +al C:546f .fe_reduce_wide +al C:a547 .x25_z2 +al C:a567 .x25_x3 +al C:9a3d .hkdf_info_buf +al C:a527 .x25_x2 +al C:a587 .x25_z3 +al C:08ca .do_http_get +al C:1479 .tls_recv_count +al C:0d2d .net_dns_resolve +al C:3f33 .rotr32_8 +al C:1889 .tls_transcript_hash +al C:9100 .sqtab2_hi +al C:a0df .hmac_key +al C:0bfc .ok_msg +al C:103a .tls_recv_encrypted +al C:a11f .hmac_opad_block +al C:a3e7 .fe_wide +al C:70f2 .ec_a_384 +al C:9f6d .sha_h +al C:9000 .sqtab2_lo al C:8d1b .cv_sig_len -al C:8fff .tls_app_read_key -al C:68e6 .fp_mod_sub_384 -al C:60de .ec_set_modp -al C:0dec .tls_connect -al C:9781 .http_parse_state -al C:503e .hmac_drbg_generate -al C:506c .extra_sid_count -al C:98d9 .sha_e -al C:940e .hkdf_label_len -al C:3f91 .rotr32_1 -al C:98d5 .sha_d -al C:0d5f .cb_load_ptr_lo -al C:9409 .hkdf_ikm_ptr -al C:98e1 .sha_g -al C:98dd .sha_f -al C:5e3e .ec_p -al C:98c9 .sha_a -al C:3e82 .rotr32_4 -al C:4360 .mul_a -al C:9e7f .x25_result -al C:1387 .tls_record_recv_and_decrypt -al C:901f .tls_app_read_iv -al C:0d65 .cb_load_ptr_hi -al C:4361 .mul_b -al C:98d1 .sha_c -al C:52c8 .fe_mul_a24 -al C:9fe0 .ecdsa_hash +al C:9687 .tls_app_read_key +al C:6c98 .fp_mod_sub_384 +al C:6490 .ec_set_modp +al C:0ec6 .tls_connect +al C:9e09 .http_parse_state +al C:5115 .hmac_drbg_generate +al C:5143 .extra_sid_count +al C:9f61 .sha_e +al C:9a96 .hkdf_label_len +al C:4068 .rotr32_1 +al C:9f5d .sha_d +al C:0e39 .cb_load_ptr_lo +al C:9a91 .hkdf_ikm_ptr +al C:9f69 .sha_g +al C:9f65 .sha_f +al C:61f0 .ec_p +al C:9f51 .sha_a +al C:3f59 .rotr32_4 +al C:4437 .mul_a +al C:a507 .x25_result +al C:1461 .tls_record_recv_and_decrypt +al C:96a7 .tls_app_read_iv +al C:0e3f .cb_load_ptr_hi +al C:4438 .mul_b +al C:56cc .fe_mul_a24 +al C:9f59 .sha_c +al C:a689 .ecdsa_hash al C:89e2 .cert_buf_len al C:803f .der_read_tag -al C:1711 .tls_transcript_block_len -al C:5902 .fp_zero -al C:7612 .ec_affine_x_384 -al C:98cd .sha_b -al C:3ee1 .rotr32_7 -al C:4da0 .sha256_shr10 -al C:1712 .tls_transcript_total_lo -al C:50fc .fe_copy -al C:6b60 .fp_inv_u_384 +al C:17eb .tls_transcript_block_len +al C:5cb4 .fp_zero +al C:79c4 .ec_affine_x_384 +al C:9f55 .sha_b +al C:3fb8 .rotr32_7 +al C:4e77 .sha256_shr10 +al C:17ec .tls_transcript_total_lo +al C:51d3 .fe_copy +al C:6f12 .fp_inv_u_384 al C:8319 .der_len -al C:4850 .sha256_final -al C:1995 .hkdf_tls13_prefix -al C:4012 .chacha20_init -al C:5177 .fe_reduce_final -al C:6731 .fp_sub_384 +al C:4927 .sha256_final +al C:1a6f .hkdf_tls13_prefix +al C:40e9 .chacha20_init +al C:524e .fe_reduce_final +al C:6ae3 .fp_sub_384 al C:82fc .oid_prime256v1 -al C:977f .http_resp_len -al C:3d91 .http_get_verb -al C:5dfe .fp_r2 -al C:1713 .tls_transcript_total_hi -al C:561b .x25519_scalarmult -al C:5e1e .fp_r3 -al C:5e5e .ec_n -al C:6ce0 .ec_p_384 -al C:5dbe .fp_r0 -al C:3d25 .http_get_plain -al C:5dde .fp_r1 -al C:8e7b .tls_client_random -al C:60e7 .ec_set_modn -al C:10bc .tls_seq_increment -al C:6825 .fp_p_hi_384 -al C:5e9e .ec_b -al C:7611 .ec_sc_mask_384 +al C:9e07 .http_resp_len +al C:3e68 .http_get_verb +al C:5512 .mul_by_38 +al C:61b0 .fp_r2 +al C:17ed .tls_transcript_total_hi +al C:5a19 .x25519_scalarmult +al C:61d0 .fp_r3 +al C:6210 .ec_n +al C:7092 .ec_p_384 +al C:6170 .fp_r0 +al C:3dfc .http_get_plain +al C:6190 .fp_r1 +al C:9503 .tls_client_random +al C:6499 .ec_set_modn +al C:1196 .tls_seq_increment +al C:6bd7 .fp_p_hi_384 +al C:79c3 .ec_sc_mask_384 al C:7f63 .ecdsa_parse_der_sig -al C:0c27 .http_path_root -al C:5e7e .ec_a -al C:9033 .tls_read_seq -al C:3fd2 .zero32 -al C:947d .http_req_buf -al C:9479 .http_port -al C:6823 .fp_s_hi_384 -al C:7642 .ec_affine_y_384 -al C:97a8 .input_buffer -al C:3dbc .http_bg_src -al C:590c .fp_cmp -al C:9b39 .hmac_result -al C:9f5f .x25_da -al C:a102 .ev_u1 -al C:3b2c .http_build_get +al C:6250 .ec_b +al C:0d01 .http_path_root +al C:6230 .ec_a +al C:96bb .tls_read_seq +al C:40a9 .zero32 +al C:9b05 .http_req_buf +al C:9b01 .http_port +al C:6bd5 .fp_s_hi_384 +al C:79f4 .ec_affine_y_384 +al C:9e30 .input_buffer +al C:3e93 .http_bg_src +al C:5cbe .fp_cmp +al C:a1c1 .hmac_result +al C:a5e7 .x25_da +al C:a7ab .ev_u1 +al C:3c03 .http_build_get al C:8d13 .cert_data_offset -al C:699a .fp_rem_384 -al C:0d3b .net_recv_byte -al C:a010 .ecdsa_hash_len -al C:0bcb .send_fail_msg -al C:9cfd .poly1305_tag -al C:a122 .ev_u2 -al C:9d3c .aead_data_ptr -al C:4184 .chacha20_block -al C:9c9b .cc20_nonce -al C:1e54 .tls_verify_data -al C:9ad7 .hmac_data_buf -al C:4a66 .sha256_sig0 +al C:6d4c .fp_rem_384 +al C:0e15 .net_recv_byte +al C:a6b9 .ecdsa_hash_len +al C:0bdd .send_fail_msg +al C:a385 .poly1305_tag +al C:a7cb .ev_u2 +al C:a3c4 .aead_data_ptr +al C:425b .chacha20_block +al C:a323 .cc20_nonce +al C:1f2e .tls_verify_data +al C:a15f .hmac_data_buf +al C:4b3d .sha256_sig0 al C:8a69 .x509_extract_pubkey -al C:55eb .fe_inv_sqrn_tmp2 -al C:97a6 .tls_app_len -al C:4aab .sha256_sig1 -al C:0cb5 .net_tcp_close -al C:60f0 .ec_mulp -al C:4b35 .sha256_big_sig1 -al C:454b .aead_decrypt -al C:4af0 .sha256_big_sig0 -al C:6826 .fp_wide_384 +al C:59e9 .fe_inv_sqrn_tmp2 +al C:9e2e .tls_app_len +al C:4b82 .sha256_sig1 +al C:0d8f .net_tcp_close +al C:64a2 .ec_mulp +al C:4c0c .sha256_big_sig1 +al C:4622 .aead_decrypt +al C:4bc7 .sha256_big_sig0 +al C:6bd8 .fp_wide_384 al C:8d5e .zp_save_buf -al C:10d4 .tls_record_encrypt -al C:8e9b .tls_server_random -al C:9395 .hkdf_okm -al C:69cb .fp_bc_384 -al C:591a .fp_add -al C:9783 .http_line_idx -al C:1088 .tls_build_nonce -al C:8efb .tls_server_pubkey -al C:18d6 .hkdf_expand -al C:70e2 .ec_mulp_384 -al C:1dcf .lbl_c_ap_traffic -al C:1ddb .lbl_s_ap_traffic -al C:0d4e .cb_load_len_lo +al C:11ae .tls_record_encrypt +al C:9523 .tls_server_random +al C:9a1d .hkdf_okm +al C:6d7d .fp_bc_384 +al C:5ccc .fp_add +al C:9e0b .http_line_idx +al C:1162 .tls_build_nonce +al C:9583 .tls_server_pubkey +al C:5573 .mul38_hi +al C:19b0 .hkdf_expand +al C:7494 .ec_mulp_384 +al C:1ea9 .lbl_c_ap_traffic +al C:1eb5 .lbl_s_ap_traffic +al C:5572 .mul38_lo +al C:0e28 .cb_load_len_lo al C:7c11 .ecdsa_verify_256 -al C:0fd8 .tls_send_finished -al C:1264 .tls_enc_aead_len -al C:0ba3 .tls_fail_msg -al C:1b65 .tls_derive_traffic_keys +al C:10b2 .tls_send_finished +al C:133e .tls_enc_aead_len +al C:0bb5 .tls_fail_msg +al C:1c3f .tls_derive_traffic_keys al C:80b8 .der_match_oid al C:8d1c .tls_ecdh_generate_keypair -al C:903b .tls_rec_header -al C:139e .tls_recv_state -al C:a202 .ev_point_save_384 -al C:592d .fp_sub -al C:0d54 .cb_load_len_hi -al C:3d95 .http_version -al C:1dea .lbl_iv -al C:08a6 .net_initialized -al C:9267 .tls_nonce -al C:9d0d .aead_key -al C:3ff2 .cc20_qr_table -al C:1e34 .tls_derived_tmp -al C:60be .ec_t6 -al C:9413 .tls_early_secret -al C:609e .ec_t5 -al C:3e37 .rotr32_16 -al C:3c0d .http_recv_response -al C:5b85 .fp_bm -al C:607e .ec_t4 -al C:5efe .ec_p1 -al C:605e .ec_t3 -al C:8e79 .tcp_recv_tail -al C:603e .ec_t2 -al C:3e7f .rotr32_12 -al C:5fbe .ec_p3 -al C:601e .ec_t1 -al C:518d .fe_cswap -al C:0e9a .tls_recv -al C:5d2b .fp_chk_one -al C:5f5e .ec_p2 -al C:0cbf .net_print_ip +al C:96c3 .tls_rec_header +al C:1478 .tls_recv_state +al C:a8ab .ev_point_save_384 +al C:5cdf .fp_sub +al C:0e2e .cb_load_len_hi +al C:3e6c .http_version +al C:1ec4 .lbl_iv +al C:08b8 .net_initialized +al C:98ef .tls_nonce +al C:a395 .aead_key +al C:40c9 .cc20_qr_table +al C:1f0e .tls_derived_tmp +al C:6470 .ec_t6 +al C:9a9b .tls_early_secret +al C:6450 .ec_t5 +al C:3f0e .rotr32_16 +al C:3ce4 .http_recv_response +al C:5f37 .fp_bm +al C:6430 .ec_t4 +al C:62b0 .ec_p1 +al C:6410 .ec_t3 +al C:9501 .tcp_recv_tail +al C:63f0 .ec_t2 +al C:3f56 .rotr32_12 +al C:6370 .ec_p3 +al C:63d0 .ec_t1 +al C:5264 .fe_cswap +al C:0f74 .tls_recv +al C:60dd .fp_chk_one +al C:6310 .ec_p2 +al C:0d99 .net_print_ip al C:8309 .oid_sha256_ecdsa -al C:5d5e .fp_inv_v -al C:090c .do_https_get -al C:4452 .poly1305_block -al C:8f1b .tls_shared_secret -al C:5d3e .fp_inv_u +al C:6110 .fp_inv_v +al C:091e .do_https_get +al C:4529 .poly1305_block +al C:95a3 .tls_shared_secret +al C:60f0 .fp_inv_u al C:8304 .oid_secp384r1 -al C:947b .http_status -al C:98ed .sha_t1 -al C:5b84 .fp_bc -al C:4307 .sq_acc -al C:3fe2 .cc20_constants -al C:98f1 .sha_t2 -al C:9fdf .ecdsa_curve_id -al C:9475 .http_host_len -al C:75b3 .ec_scalar_mul_384 -al C:9433 .tls_handshake_secret -al C:4227 .poly1305_init -al C:98ad .sha256_h1 +al C:9b03 .http_status +al C:9f75 .sha_t1 +al C:5f36 .fp_bc +al C:43de .sq_acc +al C:40b9 .cc20_constants +al C:9f79 .sha_t2 +al C:a688 .ecdsa_curve_id +al C:7965 .ec_scalar_mul_384 +al C:9afd .http_host_len +al C:9abb .tls_handshake_secret +al C:42fe .poly1305_init +al C:9f35 .sha256_h1 al C:8d17 .cert_ext_len_lo -al C:98a9 .sha256_h0 -al C:8ff3 .tls_app_write_iv -al C:98b5 .sha256_h3 -al C:9a77 .hmac_val -al C:403f .chacha20_quarter_round -al C:98b1 .sha256_h2 -al C:1266 .tls_send_record -al C:430f .sq_i -al C:98bd .sha256_h5 -al C:9408 .hkdf_salt_len -al C:98b9 .sha256_h4 -al C:6706 .fp_zero_384 +al C:9f31 .sha256_h0 +al C:967b .tls_app_write_iv +al C:9f3d .sha256_h3 +al C:a0ff .hmac_val +al C:4116 .chacha20_quarter_round +al C:9f39 .sha256_h2 +al C:1340 .tls_send_record +al C:43e6 .sq_i +al C:9f45 .sha256_h5 +al C:9a90 .hkdf_salt_len +al C:6ab8 .fp_zero_384 al C:8d16 .cert_ext_len_hi -al C:98c5 .sha256_h7 -al C:98c1 .sha256_h6 -al C:9a35 .sha256_hash -al C:6824 .fp_p_lo_384 -al C:0d70 .cb_loop -al C:137c .tls_record_send_encrypted -al C:9d3b .aead_aad_len +al C:9f41 .sha256_h4 +al C:9f4d .sha256_h7 +al C:9f49 .sha256_h6 +al C:a0bd .sha256_hash +al C:6bd6 .fp_p_lo_384 +al C:0e4a .cb_loop +al C:1456 .tls_record_send_encrypted +al C:a3c3 .aead_aad_len al C:8311 .oid_sha384_ecdsa -al C:5163 .fe_cmp_p -al C:186d .hkdf_extract +al C:523a .fe_cmp_p +al C:1947 .hkdf_extract al C:804a .der_read_length diff --git a/src/boot.asm b/src/boot.asm index 47abc5f..a8e9cd9 100644 --- a/src/boot.asm +++ b/src/boot.asm @@ -37,11 +37,28 @@ jsr entropy_init jsr drbg_init_entropy + ; build quarter-square multiply table (needed by Poly1305, fe25519, ECDSA) + jsr sqtab_init + + ; pre-compute REU multiply rows (depends on sqtab being populated) + ; Ensure BASIC ROM is off — data buffers and REU DMA targets live at $A000+ + lda $01 + and #%11111110 + sta $01 + jsr reu_mul_init + ; print menu lda #menu_msg jsr print_string + ; Ensure BASIC ROM stays off for all runtime operation. + ; Data buffers (fe_wide, x25_*, ECDSA) live at $A000-$BFFF. + ; The C64 writes to RAM under ROM, but reads hit ROM unless banked out. + lda $01 + and #%11111110 + sta $01 + ; enter main loop jmp main_loop @@ -488,6 +505,129 @@ done_msg: !text "CONNECTION CLOSED" !byte $0d, 0 +; ============================================================================= +; REU multiply table initialization (from c64-x25519 optimizations) +; ============================================================================= + +; ============================================================================= +; reu_mul_init - Generate 256 full multiplication rows and stash in REU +; +; For each a = 0..255, computes a*b for b = 0..255 and stashes: +; 256 lo bytes at REU offset a*512 +; 256 hi bytes at REU offset a*512+256 +; +; Uses mul_dma_lo/mul_dma_hi as staging buffers. +; Uses mul_8x8 (requires sqtab to be initialized first). +; Clobbers: A, X, Y +; ============================================================================= +reu_mul_init: + lda #0 + sta reu_init_a ; outer counter (multiplier a) + +@outer: + ; For current a, compute a*b for all b=0..255 + lda #0 + sta reu_init_b ; inner counter (multiplicand b) + +@inner: + lda reu_init_a + ldx reu_init_b + jsr mul_8x8 ; poly_prod_lo/hi = a * b + + ldx reu_init_b + lda poly_prod_lo + sta mul_dma_lo,x + lda poly_prod_hi + sta mul_dma_hi,x + + inc reu_init_b + bne @inner ; loop b = 0..255 + + ; Stash lo table (256 bytes) to REU at offset a*512 + lda #mul_dma_lo + sta reu_c64_hi + lda #0 + sta reu_reu_lo ; REU offset low = 0 + lda reu_init_a + asl ; A = a * 2 (high byte of offset) + sta reu_reu_hi + lda #0 + adc #0 ; carry into bank if a >= 128 + sta reu_reu_bank + lda #0 + sta reu_len_lo + lda #1 + sta reu_len_hi ; length = 256 + lda #0 + sta reu_addr_ctrl ; both addresses increment + lda #%10110000 ; execute + autoload + STASH (C64->REU) + sta reu_command + + ; Stash hi table (256 bytes) to REU at offset a*512+256 + lda #mul_dma_hi + sta reu_c64_hi + lda #0 + sta reu_reu_lo + lda reu_init_a + asl ; a*2 (carry = bit 7 of a) + lda #0 + adc #0 ; bank = a >> 7 + sta reu_reu_bank + lda reu_init_a + asl ; a*2 + ora #1 ; +1 for hi page (a*2 is even, so OR works) + sta reu_reu_hi + lda #0 + sta reu_len_lo + lda #1 + sta reu_len_hi ; length = 256 + lda #0 + sta reu_addr_ctrl + lda #%10110000 ; execute + autoload + STASH + sta reu_command + + inc reu_init_a + beq @init_done ; if wrapped to 0, done + jmp @outer +@init_done: + ; Pre-configure constant REU registers for fetch routine + lda #mul_dma_lo + sta reu_c64_hi + lda #0 + sta reu_reu_lo + sta reu_len_lo + sta reu_addr_ctrl + lda #2 + sta reu_len_hi ; length high = 2 (512 bytes) + rts + +reu_init_a: !byte 0 +reu_init_b: !byte 0 + +; ============================================================================= +; reu_fetch_mul_row - DMA a multiplication table row from REU to C64 +; +; Input: mul_cached_a = multiplier value (0-255) +; Fetches 512 bytes: 256 lo bytes to mul_dma_lo, 256 hi bytes to mul_dma_hi +; Clobbers: A +; ============================================================================= +reu_fetch_mul_row: + lda mul_cached_a + asl ; A = multiplier * 2, carry = bit 7 + sta reu_reu_hi + lda #0 + adc #0 ; bank = carry from shift + sta reu_reu_bank + lda #%10110001 ; execute + autoload + FETCH (REU->C64) + sta reu_command + rts + ; ============================================================================= ; hostname and path data ; ============================================================================= diff --git a/src/constants.asm b/src/constants.asm index 67a3bb3..3deaf67 100644 --- a/src/constants.asm +++ b/src/constants.asm @@ -52,13 +52,17 @@ sha_temp1 = $0a ; 4 bytes ($0A-$0D) sha_temp2 = $0e ; 4 bytes ($0E-$11) sha256_round = $12 ; 1 byte -; --- ChaCha20 state --- +; --- ChaCha20 state / mult66 pointers (time-shared: fe25519 and ChaCha20 never run simultaneously) --- cc20_round = $14 ; 1 byte cc20_qr_idx = $15 ; 1 byte cc20_data_ptr = $16 ; 2 bytes ($16-$17) cc20_remain = $18 ; 1 byte (also poly1305_update counter) cc20_buf_pos = $19 ; 1 byte +; --- mult66 indirect-indexed multiply pointers (time-shared with ChaCha20) --- +lmul0 = $14 ; 2 bytes ($14-$15) — sqtab lookup pointer +lmul1 = $16 ; 2 bytes ($16-$17) — sqtab_hi lookup pointer + ; --- Poly1305 state --- poly_i = $1a ; 1 byte poly_j = $1b ; 1 byte @@ -106,6 +110,18 @@ zp_count = $fe ; 1 byte sqtab_lo = $7800 ; 512 bytes: floor(n^2/4) low bytes sqtab_hi = $7a00 ; 512 bytes: floor(n^2/4) high bytes +; --- REU (Ram Expansion Unit) registers --- +reu_status = $df00 ; status register +reu_command = $df01 ; command register +reu_c64_lo = $df02 ; C64 base address low +reu_c64_hi = $df03 ; C64 base address high +reu_reu_lo = $df04 ; REU base address low +reu_reu_hi = $df05 ; REU base address high +reu_reu_bank = $df06 ; REU bank +reu_len_lo = $df07 ; transfer length low +reu_len_hi = $df08 ; transfer length high +reu_addr_ctrl = $df0a ; address control + ; --- SID voice 3 setup for noise (entropy collection) --- sid_base = $d400 sid_v3_freq_lo = $d40e diff --git a/src/crypto/fe25519.asm b/src/crypto/fe25519.asm index 5c1ffc9..9d1c016 100644 --- a/src/crypto/fe25519.asm +++ b/src/crypto/fe25519.asm @@ -1,6 +1,13 @@ ; ============================================================================= ; fe25519.asm - Field arithmetic mod p = 2^255 - 19 ; +; Optimized version imported from c64-x25519 project. +; Key optimizations over baseline: +; - fe_mul: REU DMA table lookup + 2x inner loop unroll (no mul_8x8 calls) +; - fe_sqr: Dedicated squaring with mult66 quarter-square + symmetry exploit +; - fe_reduce_wide: mul38 lookup tables instead of mul_8x8 calls +; - fe_cswap: Self-modifying abs,Y + 4x unroll (38 cyc/byte vs 49) +; ; 32-byte little-endian field elements. ; Uses ZP pointers fe_src1, fe_src2, fe_dst for operands. ; Reuses mul_8x8 and sqtab from poly1305.asm for multiplication. @@ -8,11 +15,12 @@ ; Key design: ; - Little-endian throughout (matches 6502 carry propagation and X25519 wire) ; - DEX/DEY for all carry-dependent loops (CPX/CPY clobber carry) -; - Reduction mod p: 2^256 = 38 mod p, so multiply overflow by 38 and add +; - Reduction mod p: 2^256 ≡ 38 mod p, so multiply overflow by 38 and add ; -; Adapted from c64-wireguard for c64-https TLS 1.3 ECDH. -; ZP equates (fe_src1, fe_src2, fe_dst, etc.) defined in constants.asm. -; Data labels (fe_wide, fe_tmp1..4, fe_p) defined in data.asm. +; ZP equates (fe_src1, fe_src2, fe_dst, lmul0, lmul1) defined in constants.asm. +; Data labels (fe_wide, fe_tmp1..4, fe_p, mul_cached_a, mul_src2_buf, +; mul_dma_lo, mul_dma_hi, sqtab2_lo, sqtab2_hi, mul38_lo_tab, +; mul38_hi_tab) defined in data.asm. ; ============================================================================= ; ============================================================================= @@ -69,7 +77,7 @@ fe_add: iny dex ; DEX doesn't affect carry bne @add_loop - bcs @must_reduce ; carry out -> result >= 2^256 > p + bcs @must_reduce ; carry out → result >= 2^256 > p ; Check if result >= p jsr fe_cmp_p @@ -107,7 +115,7 @@ fe_sub: iny dex bne @sub_loop - bcs @done ; no borrow -> done + bcs @done ; no borrow → done ; Borrow: add p clc @@ -139,7 +147,7 @@ fe_cmp_p: bne @greater dey bpl @cmp_loop - sec ; equal -> >= p + sec ; equal → >= p rts @less: clc @@ -175,21 +183,129 @@ fe_reduce_final: ; ; Input: A = swap mask (0x00 = no swap, 0xFF = swap) ; Clobbers: A, X, Y +; +; Self-modifying code: patches absolute,Y addresses into the inner loop +; to replace indirect-indexed (zp),Y loads/stores (4-5 cyc vs 5-6 cyc each). +; Eliminates redundant re-read of src1 by keeping value in X register. +; Unrolled 4x to reduce loop overhead (32 bytes / 4 = 8 iterations). +; +; Per byte: lda abs,Y(4) + tax(2) + eor abs,Y(4) + and zp(3) + sta zp(3) +; + txa(2) + eor zp(3) + sta abs,Y(5) + lda abs,Y(4) + eor zp(3) +; + sta abs,Y(5) = 38 cycles/byte +; Old: 49 cycles/byte (indirect-indexed + redundant re-read) +; Savings: ~11 cyc/byte * 32 bytes * 512 calls = ~180k cycles ; ============================================================================= fe_cswap: sta fe_carry ; save mask + + ; Patch src1 address into lda/sta abs,Y instructions (8 patches) + lda fe_src1 + sta @ld_a1+1 + sta @st_a1+1 + sta @ld_a2+1 + sta @st_a2+1 + sta @ld_a3+1 + sta @st_a3+1 + sta @ld_a4+1 + sta @st_a4+1 + lda fe_src1+1 + sta @ld_a1+2 + sta @st_a1+2 + sta @ld_a2+2 + sta @st_a2+2 + sta @ld_a3+2 + sta @st_a3+2 + sta @ld_a4+2 + sta @st_a4+2 + + ; Patch src2 address into eor/lda/sta abs,Y instructions (12 patches) + lda fe_src2 + sta @eor_b1+1 + sta @ld_b1+1 + sta @st_b1+1 + sta @eor_b2+1 + sta @ld_b2+1 + sta @st_b2+1 + sta @eor_b3+1 + sta @ld_b3+1 + sta @st_b3+1 + sta @eor_b4+1 + sta @ld_b4+1 + sta @st_b4+1 + lda fe_src2+1 + sta @eor_b1+2 + sta @ld_b1+2 + sta @st_b1+2 + sta @eor_b2+2 + sta @ld_b2+2 + sta @st_b2+2 + sta @eor_b3+2 + sta @ld_b3+2 + sta @st_b3+2 + sta @eor_b4+2 + sta @ld_b4+2 + sta @st_b4+2 + ldy #31 @loop: - lda (fe_src1),y - eor (fe_src2),y ; diff = a ^ b - and fe_carry ; mask it - sta fe_loop ; temp - lda (fe_src1),y + ; --- Byte at Y --- +@ld_a1: lda $ffff,y ; a[y] (patched) + tax ; X = a[y] +@eor_b1:eor $ffff,y ; a[y] ^ b[y] (patched) + and fe_carry ; diff + sta fe_loop ; save diff + txa ; A = a[y] + eor fe_loop ; a[y] ^ diff +@st_a1: sta $ffff,y ; store new a[y] (patched) +@ld_b1: lda $ffff,y ; b[y] (patched) + eor fe_loop ; b[y] ^ diff +@st_b1: sta $ffff,y ; store new b[y] (patched) + + dey + + ; --- Byte at Y --- +@ld_a2: lda $ffff,y + tax +@eor_b2:eor $ffff,y + and fe_carry + sta fe_loop + txa eor fe_loop - sta (fe_src1),y - lda (fe_src2),y +@st_a2: sta $ffff,y +@ld_b2: lda $ffff,y + eor fe_loop +@st_b2: sta $ffff,y + + dey + + ; --- Byte at Y --- +@ld_a3: lda $ffff,y + tax +@eor_b3:eor $ffff,y + and fe_carry + sta fe_loop + txa + eor fe_loop +@st_a3: sta $ffff,y +@ld_b3: lda $ffff,y eor fe_loop - sta (fe_src2),y +@st_b3: sta $ffff,y + + dey + + ; --- Byte at Y --- +@ld_a4: lda $ffff,y + tax +@eor_b4:eor $ffff,y + and fe_carry + sta fe_loop + txa + eor fe_loop +@st_a4: sta $ffff,y +@ld_b4: lda $ffff,y + eor fe_loop +@st_b4: sta $ffff,y + dey bpl @loop rts @@ -197,8 +313,11 @@ fe_cswap: ; ============================================================================= ; fe_mul - (fe_dst) = (fe_src1) * (fe_src2) mod p ; -; Schoolbook 32x32->64-byte multiply using mul_8x8 (quarter-square table). -; Then reduce mod p. +; Combined REU DMA table lookup + 2x inner loop unroll. +; Each outer iteration: DMA fetches 512-byte mul row for src1[i], +; then inner loop does direct table lookup (mul_dma_lo/hi,Y) instead of +; mult66 quarter-square. Inner loop unrolled 2x to reduce branch overhead. +; ; Clobbers: A, X, Y ; ============================================================================= fe_mul: @@ -210,62 +329,163 @@ fe_mul: dex bpl @zero_wide - ; 2. Schoolbook multiply: src1[i] * src2[j] + ; 2. Copy src2 to absolute buffer (needed for indexed access) + ldy #31 +@copy_src2: + lda (fe_src2),y + sta mul_src2_buf,y + dey + bpl @copy_src2 + + ; 3. Schoolbook multiply with REU DMA lookup + self-mod accumulation lda #0 sta fe_mul_i @mul_outer: ldy fe_mul_i lda (fe_src1),y - beq @skip_zero ; skip if src1[i] == 0 + bne @nonzero_i + jmp @skip_zero +@nonzero_i: + sta mul_cached_a ; cache src1[i] for inner loop + + ; DMA the multiplication row for src1[i] from REU + jsr reu_fetch_mul_row + + ; Self-mod: patch accumulation addresses to base = fe_wide + i + ; Patch BOTH copies of the unrolled inner loop + lda #fe_wide + adc #0 ; handle page crossing + sta @accum_ld1+2 + sta @accum_st1+2 + sta @accum_ld1_b+2 + sta @accum_st1_b+2 + ; For +1 accesses (high byte of product), base is fe_wide + i + 1 + lda #<(fe_wide+1) + clc + adc fe_mul_i + sta @accum_ld2+1 + sta @accum_st2+1 + sta @accum_ld2_b+1 + sta @accum_st2_b+1 + lda #>(fe_wide+1) + adc #0 + sta @accum_ld2+2 + sta @accum_st2+2 + sta @accum_ld2_b+2 + sta @accum_st2_b+2 lda #0 sta fe_mul_j + + ; ===== UNROLLED 2x INNER LOOP ===== + ; First copy processes j, second copy processes j+1 + ; Loop exit check only after second copy + @mul_inner: - ldy fe_mul_i - lda (fe_src1),y ; A = src1[i] - pha - ldy fe_mul_j - lda (fe_src2),y ; A = src2[j] - beq @skip_j_zero ; skip if zero - tax ; X = src2[j] - pla ; A = src1[i] - jsr mul_8x8 ; poly_prod_lo/hi = result + ; --- First copy: process src2[j] --- + ldx fe_mul_j + ldy mul_src2_buf,x ; Y = src2[j] + beq @next_j_first ; skip if zero + + ; --- REU table lookup: mul_cached_a * Y --- + lda mul_dma_lo,y ; lo byte of product (4 cycles) + sta poly_prod_lo + lda mul_dma_hi,y ; hi byte of product (4 cycles) + sta poly_prod_hi ; Add 16-bit product to fe_wide[i+j] + ldx fe_mul_j + + clc +@accum_ld1: + lda fe_wide,x ; patched to fe_wide+i base + adc poly_prod_lo +@accum_st1: + sta fe_wide,x +@accum_ld2: + lda fe_wide+1,x ; patched to fe_wide+i+1 base + adc poly_prod_hi +@accum_st2: + sta fe_wide+1,x + bcc @next_j_first + + ; Propagate carry (rare path) lda fe_mul_i clc adc fe_mul_j - tax ; X = i+j - clc + adc #2 + tax +@prop_carry_a: + cpx #64 + bcs @next_j_first + sec lda fe_wide,x - adc poly_prod_lo + adc #0 sta fe_wide,x inx - lda fe_wide,x - adc poly_prod_hi + bcs @prop_carry_a + +@next_j_first: + inc fe_mul_j ; advance j, no exit check + + ; --- Second copy: process src2[j+1] --- + ldx fe_mul_j + ldy mul_src2_buf,x ; Y = src2[j] + beq @next_j ; skip if zero + + ; --- REU table lookup: mul_cached_a * Y --- + lda mul_dma_lo,y ; lo byte of product (4 cycles) + sta poly_prod_lo + lda mul_dma_hi,y ; hi byte of product (4 cycles) + sta poly_prod_hi + + ; Add 16-bit product to fe_wide[i+j] + ldx fe_mul_j + + clc +@accum_ld1_b: + lda fe_wide,x ; patched to fe_wide+i base + adc poly_prod_lo +@accum_st1_b: sta fe_wide,x +@accum_ld2_b: + lda fe_wide+1,x ; patched to fe_wide+i+1 base + adc poly_prod_hi +@accum_st2_b: + sta fe_wide+1,x bcc @next_j - ; Propagate carry -@prop_carry: - inx + ; Propagate carry (rare path) + lda fe_mul_i + clc + adc fe_mul_j + clc + adc #2 + tax +@prop_carry_b: cpx #64 bcs @next_j sec lda fe_wide,x adc #0 sta fe_wide,x - bcs @prop_carry - jmp @next_j + inx + bcs @prop_carry_b -@skip_j_zero: - pla ; discard src1[i] @next_j: inc fe_mul_j lda fe_mul_j cmp #32 - bcc @mul_inner + bcs @skip_zero + jmp @mul_inner @skip_zero: inc fe_mul_i @@ -275,7 +495,7 @@ fe_mul: jmp @mul_outer @mul_done: - ; 3. Reduce mod p + ; 4. Reduce mod p jsr fe_reduce_wide ; Copy result to (fe_dst) @@ -293,6 +513,7 @@ fe_mul: ; fe_reduce_wide - Reduce fe_wide[0..63] mod p into fe_wide[0..31] ; ; fe_wide[32..63] * 38 + fe_wide[0..31], with second pass for overflow. +; Uses mul38 lookup tables for speed. ; Clobbers: A, X, Y ; ============================================================================= fe_reduce_wide: @@ -301,13 +522,14 @@ fe_reduce_wide: sta fe_carry ldx #0 @reduce1: - lda fe_wide+32,x + ldy fe_wide+32,x ; Y = byte value (table index) beq @reduce1_zero - stx fe_loop ; save byte index - ldx #38 - jsr mul_8x8 ; poly_prod_lo/hi = byte * 38 - ldx fe_loop ; restore byte index + ; Table lookup: Y * 38 + lda mul38_lo_tab,y + sta poly_prod_lo + lda mul38_hi_tab,y + sta poly_prod_hi ; Add product + running carry to fe_wide[x] clc @@ -348,8 +570,11 @@ fe_reduce_wide: ; If carry remains, multiply by 38 and add to bottom lda fe_carry beq @done - ldx #38 - jsr mul_8x8 + tay ; Y = carry value + lda mul38_lo_tab,y + sta poly_prod_lo + lda mul38_hi_tab,y + sta poly_prod_hi clc lda fe_wide @@ -387,16 +612,312 @@ fe_reduce_wide: @done: rts +; ============================================================================= +; mul_by_38 - Multiply A by 38, result in poly_prod_hi:poly_prod_lo +; +; Uses shift-and-add: 38 = 32 + 4 + 2 +; Input: A = multiplicand (0-255) +; Output: poly_prod_lo/poly_prod_hi = A * 38 (16-bit, max 9690=$25DA) +; Clobbers: A, Y +; Preserves: X +; ============================================================================= +mul_by_38: + sta mul38_in ; save input + ; 16-bit shift register starts as A + lda mul38_in + sta mul38_lo + lda #0 + sta mul38_hi + + ; shift left 1 -> A*2, add to prod + asl mul38_lo + rol mul38_hi + lda mul38_lo + sta poly_prod_lo + lda mul38_hi + sta poly_prod_hi ; prod = A*2 + + ; shift left 1 more -> A*4, add to prod + asl mul38_lo + rol mul38_hi ; mul38 = A*4 + clc + lda poly_prod_lo + adc mul38_lo + sta poly_prod_lo + lda poly_prod_hi + adc mul38_hi + sta poly_prod_hi ; prod = A*2 + A*4 = A*6 + + ; shift left 3 more -> A*32, add to prod + asl mul38_lo + rol mul38_hi ; A*8 + asl mul38_lo + rol mul38_hi ; A*16 + asl mul38_lo + rol mul38_hi ; A*32 + clc + lda poly_prod_lo + adc mul38_lo + sta poly_prod_lo + lda poly_prod_hi + adc mul38_hi + sta poly_prod_hi ; prod = A*6 + A*32 = A*38 + rts + +mul38_in: !byte 0 +mul38_lo: !byte 0 +mul38_hi: !byte 0 + ; ============================================================================= ; fe_sqr - (fe_dst) = (fe_src1)^2 mod p +; +; Dedicated squaring: exploits symmetry a[i]*a[j] = a[j]*a[i]. +; Uses mult66 indirect-indexed multiply + self-modifying accumulation +; (same technique as fe_mul). Cross terms added twice to fuse doubling. +; 1. Cross terms: accumulate 2*a[i]*a[j] for i < j (inline mult66, shift-before-accum) +; 2. Diagonal: add a[i]^2 at position 2*i (inline mult66) +; 3. Reduce mod p +; ; Clobbers: A, X, Y ; ============================================================================= fe_sqr: - lda fe_src1 - sta fe_src2 - lda fe_src1+1 - sta fe_src2+1 - jmp fe_mul + ; 1. Zero the 64-byte product buffer + ldx #63 + lda #0 +@zero_wide: + sta fe_wide,x + dex + bpl @zero_wide + + ; 2. Copy src1 to absolute buffer (src1==src2 for squaring) + ldy #31 +@copy_src: + lda (fe_src1),y + sta mul_src2_buf,y + dey + bpl @copy_src + + ; 3. Set up ZP pointers for mult66 indirect-indexed multiply + lda #>sqtab_lo + sta lmul0+1 + lda #>sqtab_hi + sta lmul1+1 + + ; 4. Cross terms with mult66 + self-mod, shift-before-accumulate + lda #0 + sta fe_mul_i +@sqr_outer: + ldy fe_mul_i + lda (fe_src1),y + bne @sqr_nonzero_i + jmp @sqr_skip_i +@sqr_nonzero_i: + sta mul_cached_a ; cache a[i] for inner loop + + ; Self-mod: patch accumulation addresses to base = fe_wide + i + lda #fe_wide + adc #0 ; handle page crossing + sta @sqr_accum_ld1+2 + sta @sqr_accum_st1+2 + ; For +1 accesses (high byte of product) + lda #<(fe_wide+1) + clc + adc fe_mul_i + sta @sqr_accum_ld2+1 + sta @sqr_accum_st2+1 + lda #>(fe_wide+1) + adc #0 + sta @sqr_accum_ld2+2 + sta @sqr_accum_st2+2 + + ; Set up ZP pointer low byte = a[i] once per outer loop + lda mul_cached_a + sta lmul0 ; lmul0 = sqtab_lo + a[i] + sta lmul1 ; lmul1 = sqtab_hi + a[i] + + ; j starts at i+1 + lda fe_mul_i + clc + adc #1 + sta fe_mul_j + +@sqr_inner: + ldx fe_mul_j + ldy mul_src2_buf,x ; Y = a[j] + bne @sqr_nonzero_j ; skip if zero + jmp @sqr_next_j +@sqr_nonzero_j: + + ; --- mult66 inline: a[i] * a[j] --- + tya ; A = a[j] + sec + sbc mul_cached_a ; A = a[j] - a[i] + tax ; X = difference (or wrapped) + + ; (lmul0),Y = sqtab_lo[a[i] + a[j]] + lda (lmul0),y + bcc @sqr_neg_diff ; branch if a[j] < a[i] + + ; Positive difference path (carry SET): + sbc sqtab_lo,x + sta poly_prod_lo + lda (lmul1),y + sbc sqtab_hi,x + sta poly_prod_hi + jmp @sqr_accum + +@sqr_neg_diff: + ; Negative difference path (carry CLEAR): + sbc sqtab2_lo,x + sta poly_prod_lo + lda (lmul1),y + sbc sqtab2_hi,x + sta poly_prod_hi + ; --- END mult66 --- + +@sqr_accum: + ; Double the product (shift-before-accumulate replaces second addition) + asl poly_prod_lo + rol poly_prod_hi + lda #0 + adc #0 ; A = carry from ROL (0 or 1) + sta poly_carry ; save 17th bit + + ; Single addition of doubled product to fe_wide[i+j] + ldx fe_mul_j + + clc +@sqr_accum_ld1: + lda fe_wide,x ; patched to fe_wide+i base + adc poly_prod_lo +@sqr_accum_st1: + sta fe_wide,x +@sqr_accum_ld2: + lda fe_wide+1,x ; patched to fe_wide+i+1 base + adc poly_prod_hi +@sqr_accum_st2: + sta fe_wide+1,x + + ; Capture accumulation carry and combine with shift carry + lda #0 + adc poly_carry ; A = accum_carry + shift_carry (0, 1, or 2) + beq @sqr_next_j ; if both zero, skip + + ; Add combined carries to fe_wide[i+j+2] + ldx fe_mul_i + tay ; Y = combined carry value + txa + clc + adc fe_mul_j + clc + adc #2 + tax + tya ; A = combined carry value + clc + adc fe_wide,x + sta fe_wide,x + bcc @sqr_next_j + ; Propagate further carries +@sqr_prop1: + inx + cpx #64 + bcs @sqr_next_j + sec + lda fe_wide,x + adc #0 + sta fe_wide,x + bcs @sqr_prop1 + +@sqr_next_j: + inc fe_mul_j + lda fe_mul_j + cmp #32 + bcs @sqr_skip_i + jmp @sqr_inner + +@sqr_skip_i: + inc fe_mul_i + lda fe_mul_i + cmp #31 ; i goes 0..30 (j needs room for i+1) + bcs @sqr_cross_done + jmp @sqr_outer +@sqr_cross_done: + + ; 5. Add diagonal terms: a[i]^2 at position 2*i (inline mult66) + ; For self-multiply: diff=0, sqtab[0]=0, so result = sqtab[2*a[i]] + ; With lmul0 = a[i], Y = a[i]: (lmul0),Y = sqtab[2*a[i]] + lda #0 + sta fe_mul_i +@diag_outer: + ldy fe_mul_i + lda (fe_src1),y + beq @diag_skip ; skip if a[i] == 0 + + ; Set up mult66 pointers for self-multiply + sta lmul0 ; lmul0 low = a[i] + sta lmul1 ; lmul1 low = a[i] + tay ; Y = a[i] + + ; (lmul0),Y = sqtab_lo[a[i] + a[i]] = sqtab_lo[2*a[i]] + ; (lmul1),Y = sqtab_hi[a[i] + a[i]] = sqtab_hi[2*a[i]] + ; diff = 0, sqtab[0] = 0, no subtraction needed + lda (lmul0),y ; lo byte of a[i]^2 + sta poly_prod_lo + lda (lmul1),y ; hi byte of a[i]^2 + sta poly_prod_hi + + ; Add to fe_wide[2*i] + lda fe_mul_i + asl ; A = 2*i + tax + + clc + lda fe_wide,x + adc poly_prod_lo + sta fe_wide,x + inx + lda fe_wide,x + adc poly_prod_hi + sta fe_wide,x + bcc @diag_skip + + ; Propagate carry +@diag_prop: + inx + cpx #64 + bcs @diag_skip + sec + lda fe_wide,x + adc #0 + sta fe_wide,x + bcs @diag_prop + +@diag_skip: + inc fe_mul_i + lda fe_mul_i + cmp #32 + bcs @sqr_reduce + jmp @diag_outer + +@sqr_reduce: + ; 6. Reduce mod p (same as fe_mul) + jsr fe_reduce_wide + + ; Copy result to (fe_dst) + ldy #31 +@copy_result: + lda fe_wide,y + sta (fe_dst),y + dey + bpl @copy_result + + jsr fe_reduce_final + rts ; ============================================================================= ; fe_mul_a24 - (fe_dst) = (fe_src1) * 121665 mod p @@ -421,7 +942,7 @@ fe_mul_a24: lda (fe_src1),y beq @skip_zero_a24 - ; src1[i] * $41 -> add at offset i + ; src1[i] * $41 → add at offset i ldx #$41 jsr mul_8x8 ldx fe_mul_i @@ -437,7 +958,7 @@ fe_mul_a24: bne + inc fe_wide+3,x + - ; src1[i] * $DB -> add at offset i+1 + ; src1[i] * $DB → add at offset i+1 ldy fe_mul_i lda (fe_src1),y ldx #$db @@ -455,7 +976,7 @@ fe_mul_a24: bne + inc fe_wide+4,x + - ; src1[i] * $01 -> add at offset i+2 + ; src1[i] * $01 → add at offset i+2 ldy fe_mul_i lda (fe_src1),y ldx fe_mul_i @@ -473,11 +994,10 @@ fe_mul_a24: cpx #32 bcc @outer - ; Reduce: fe_wide[32..34] * 38 -> add to fe_wide[0..31] + ; Reduce: fe_wide[32..34] * 38 → add to fe_wide[0..31] lda fe_wide+32 beq @r_b33 - ldx #38 - jsr mul_8x8 + jsr mul_by_38 clc lda fe_wide adc poly_prod_lo @@ -497,8 +1017,7 @@ fe_mul_a24: @r_b33: lda fe_wide+33 beq @r_b34 - ldx #38 - jsr mul_8x8 + jsr mul_by_38 clc lda fe_wide+1 adc poly_prod_lo @@ -518,8 +1037,7 @@ fe_mul_a24: @r_b34: lda fe_wide+34 beq @r_done_a24 - ldx #38 - jsr mul_8x8 + jsr mul_by_38 clc lda fe_wide+2 adc poly_prod_lo @@ -581,7 +1099,7 @@ fe_inv: sta fe_dst+1 jsr fe_copy ; fe_tmp1 = z - ; --- z2 = z^2 -> fe_tmp2 --- + ; --- z2 = z^2 → fe_tmp2 --- lda #fe_tmp1 @@ -592,7 +1110,7 @@ fe_inv: sta fe_dst+1 jsr fe_sqr ; fe_tmp2 = z^2 - ; --- z4 = z2^2 -> fe_tmp3 --- + ; --- z4 = z2^2 → fe_tmp3 --- lda #fe_tmp2 @@ -603,7 +1121,7 @@ fe_inv: sta fe_dst+1 jsr fe_sqr ; fe_tmp3 = z^4 - ; --- z8 = z4^2 -> fe_tmp3 --- + ; --- z8 = z4^2 → fe_tmp3 --- lda #fe_tmp3 @@ -614,7 +1132,7 @@ fe_inv: sta fe_dst+1 jsr fe_sqr ; fe_tmp3 = z^8 - ; --- z9 = z8 * z -> fe_tmp3 --- + ; --- z9 = z8 * z → fe_tmp3 --- lda #fe_tmp3 @@ -629,7 +1147,7 @@ fe_inv: sta fe_dst+1 jsr fe_mul ; fe_tmp3 = z^9 - ; --- z11 = z9 * z2 -> x25_a (saved for final step) --- + ; --- z11 = z9 * z2 → x25_a (saved for final step) --- lda #fe_tmp3 @@ -644,7 +1162,7 @@ fe_inv: sta fe_dst+1 jsr fe_mul ; x25_a = z^11 - ; --- z22 = z11^2 -> fe_tmp2 --- + ; --- z22 = z11^2 → fe_tmp2 --- lda #x25_a @@ -655,7 +1173,7 @@ fe_inv: sta fe_dst+1 jsr fe_sqr ; fe_tmp2 = z^22 - ; --- z_5_0 = z22 * z9 = z^31 -> fe_tmp2 --- + ; --- z_5_0 = z22 * z9 = z^31 → fe_tmp2 --- lda #fe_tmp2 @@ -684,7 +1202,7 @@ fe_inv: lda #5 jsr fe_inv_sqrn_tmp2 ; fe_tmp2 = z_5_0^(2^5) - ; --- z_10_0 = fe_tmp2 * fe_tmp3 -> x25_b (saved) --- + ; --- z_10_0 = fe_tmp2 * fe_tmp3 → x25_b (saved) --- lda #fe_tmp2 @@ -755,7 +1273,7 @@ fe_inv: sta fe_dst+1 jsr fe_mul ; fe_tmp2 = z^(2^40-1) - ; --- z_50_0: square 10x, multiply with z_10_0 -> x25_da (saved) --- + ; --- z_50_0: square 10x, multiply with z_10_0 → x25_da (saved) --- lda #10 jsr fe_inv_sqrn_tmp2 ; fe_tmp2 = z_40_0^(2^10) @@ -773,7 +1291,7 @@ fe_inv: sta fe_dst+1 jsr fe_mul ; x25_da = z^(2^50-1) - ; --- z_100_0: copy z_50_0 to tmp2, square 50x, multiply -> x25_cb (saved) --- + ; --- z_100_0: copy z_50_0 to tmp2, square 50x, multiply → x25_cb (saved) --- lda #x25_da diff --git a/src/crypto/x25519.asm b/src/crypto/x25519.asm index 130785b..41a2a46 100644 --- a/src/crypto/x25519.asm +++ b/src/crypto/x25519.asm @@ -4,6 +4,10 @@ ; Montgomery ladder scalar multiplication on Curve25519. ; Uses fe25519.asm field arithmetic. ; +; Optimized version imported from c64-x25519 project: +; - Streamlined bit extraction (single read of scalar byte, no double-read) +; - RFC 7748 u-coordinate high-bit masking in scalarmult +; ; API: ; x25519_clamp - Clamp 32-byte scalar per RFC 7748 ; x25519_scalarmult - Montgomery ladder: result = scalar * u-point @@ -14,12 +18,10 @@ ; ; ZP equates (x25_prev_bit, x25_byte_idx, x25_bit_mask) in constants.asm. ; Data labels (x25_scalar, x25_u, x25_result, etc.) in data.asm. -; -; Adapted from c64-wireguard for c64-https TLS 1.3 ECDH. ; ============================================================================= ; ============================================================================= -; x25519_clamp - Clamp scalar per RFC 7748 S5 +; x25519_clamp - Clamp scalar per RFC 7748 §5 ; ; Clear bits 0, 1, 2 of byte 0 ; Clear bit 7 of byte 31 @@ -71,7 +73,10 @@ x25519_scalarmult: sta fe_dst+1 jsr fe_zero - ; x_3 = u + ; x_3 = u (mask high bit per RFC 7748 decodeUCoordinate) + lda x25_u+31 + and #$7f + sta x25_u+31 lda #x25_u @@ -102,7 +107,7 @@ x25519_scalarmult: sta x25_bit_mask @bit_loop: - ; Get current bit k_t + ; Get current bit k_t (single extraction) ldx x25_byte_idx lda x25_scalar,x and x25_bit_mask @@ -110,20 +115,11 @@ x25519_scalarmult: lda #1 @bit_zero: ; A = k_t (0 or 1) - ; swap = k_t XOR prev_bit - eor x25_prev_bit - ; Save k_t for next iteration - pha - ldx x25_byte_idx - lda x25_scalar,x - and x25_bit_mask - beq @save_zero - lda #1 -@save_zero: - sta x25_prev_bit - pla ; A = swap flag (0 or 1) + tax ; X = k_t (save for prev_bit update) + eor x25_prev_bit ; A = swap = k_t XOR old prev_bit + stx x25_prev_bit ; update prev_bit = k_t - ; Convert to mask: 0 -> $00, 1 -> $FF + ; Convert to mask: 0 → $00, 1 → $FF beq @no_swap_mask lda #$ff @no_swap_mask: @@ -248,7 +244,7 @@ x25519_scalarmult: ; Clobbers: A, X, Y, all fe_* ZP vars ; ============================================================================= x25519_ladder_step: - ; A = x_2 + z_2 -> x25_a + ; A = x_2 + z_2 → x25_a lda #x25_x2 @@ -263,22 +259,15 @@ x25519_ladder_step: sta fe_dst+1 jsr fe_add - ; B = x_2 - z_2 -> x25_b - lda #x25_x2 - sta fe_src1+1 - lda #x25_z2 - sta fe_src2+1 + ; B = x_2 - z_2 → x25_b + ; fe_src1=x25_x2, fe_src2=x25_z2 still set from fe_add above lda #x25_b sta fe_dst+1 jsr fe_sub - ; AA = A^2 -> fe_tmp3 + ; AA = A^2 → fe_tmp3 lda #x25_a @@ -289,7 +278,7 @@ x25519_ladder_step: sta fe_dst+1 jsr fe_sqr ; fe_tmp3 = AA - ; BB = B^2 -> fe_tmp4 + ; BB = B^2 → fe_tmp4 lda #x25_b @@ -300,7 +289,7 @@ x25519_ladder_step: sta fe_dst+1 jsr fe_sqr ; fe_tmp4 = BB - ; E = AA - BB -> x25_e + ; E = AA - BB → x25_e lda #fe_tmp3 @@ -315,7 +304,7 @@ x25519_ladder_step: sta fe_dst+1 jsr fe_sub ; x25_e = E = AA - BB - ; C = x_3 + z_3 -> fe_tmp1 (temp) + ; C = x_3 + z_3 → fe_tmp1 (temp) lda #x25_x3 @@ -330,22 +319,15 @@ x25519_ladder_step: sta fe_dst+1 jsr fe_add ; fe_tmp1 = C - ; D = x_3 - z_3 -> fe_tmp2 (temp) - lda #x25_x3 - sta fe_src1+1 - lda #x25_z3 - sta fe_src2+1 + ; D = x_3 - z_3 → fe_tmp2 (temp) + ; fe_src1=x25_x3, fe_src2=x25_z3 still set from fe_add above lda #fe_tmp2 sta fe_dst+1 jsr fe_sub ; fe_tmp2 = D - ; DA = D * A -> x25_da + ; DA = D * A → x25_da lda #fe_tmp2 @@ -360,7 +342,7 @@ x25519_ladder_step: sta fe_dst+1 jsr fe_mul ; x25_da = D * A - ; CB = C * B -> x25_cb + ; CB = C * B → x25_cb lda #fe_tmp1 @@ -389,14 +371,11 @@ x25519_ladder_step: lda #>x25_x3 sta fe_dst+1 jsr fe_add ; x25_x3 = DA + CB + ; fe_dst=x25_x3 still set; copy to fe_src1 for squaring lda #x25_x3 sta fe_src1+1 - lda #x25_x3 - sta fe_dst+1 jsr fe_sqr ; x25_x3 = (DA + CB)^2 ; z_3 = x_1 * (DA - CB)^2 @@ -414,16 +393,14 @@ x25519_ladder_step: lda #>x25_z3 sta fe_dst+1 jsr fe_sub ; x25_z3 = DA - CB + ; fe_dst=x25_z3 still set; copy to fe_src1 for squaring lda #x25_z3 sta fe_src1+1 - lda #x25_z3 - sta fe_dst+1 jsr fe_sqr ; x25_z3 = (DA - CB)^2 ; Now z_3 = x_1 * (DA-CB)^2 + ; fe_dst=x25_z3 still set from fe_sqr above lda #x25_u @@ -432,10 +409,6 @@ x25519_ladder_step: sta fe_src2 lda #>x25_z3 sta fe_src2+1 - lda #x25_z3 - sta fe_dst+1 jsr fe_mul ; x25_z3 = x_1 * (DA - CB)^2 ; x_2 = AA * BB @@ -454,7 +427,7 @@ x25519_ladder_step: jsr fe_mul ; x25_x2 = AA * BB ; z_2 = E * (AA + a24*E) - ; First: a24*E -> fe_tmp1 + ; First: a24*E → fe_tmp1 lda #x25_e @@ -465,7 +438,8 @@ x25519_ladder_step: sta fe_dst+1 jsr fe_mul_a24 ; fe_tmp1 = a24 * E - ; AA + a24*E -> fe_tmp1 + ; AA + a24*E → fe_tmp1 + ; fe_dst=fe_tmp1 still set from fe_mul_a24 above lda #fe_tmp3 @@ -474,21 +448,14 @@ x25519_ladder_step: sta fe_src2 lda #>fe_tmp1 sta fe_src2+1 - lda #fe_tmp1 - sta fe_dst+1 jsr fe_add ; fe_tmp1 = AA + a24*E ; z_2 = E * (AA + a24*E) + ; fe_src2=fe_tmp1 still set from fe_add above lda #x25_e sta fe_src1+1 - lda #fe_tmp1 - sta fe_src2+1 lda #x25_z2 diff --git a/src/data.asm b/src/data.asm index 2f95a7d..8f4067b 100644 --- a/src/data.asm +++ b/src/data.asm @@ -7,6 +7,45 @@ ; ============================================================================= zp_save_buf: !fill 26, 0 ; saves $02-$1B during ip65 calls +; ============================================================================= +; fe25519/x25519 optimization tables — MUST live below $A000 to avoid +; BASIC ROM shadow. REU DMA and CPU reads need direct RAM access. +; Placed here (early in data section) to guarantee addresses < $A000. +; ============================================================================= + +; --- REU DMA target buffers (page-aligned for LDA abs,Y without penalty) --- + !align 255, 0 ; align to next page boundary +mul_dma_lo: + !fill 256, 0 ; DMA target: lo bytes of a*b for current a +mul_dma_hi: + !fill 256, 0 ; DMA target: hi bytes of a*b for current a + +; --- mult66 second quarter-square table --- +sqtab2_lo: + !byte 0 + !for i, 1, 255 { + !byte <(((256-i)*(256-i))/4 - 1) + } + +sqtab2_hi: + !byte 0 + !for i, 1, 255 { + !byte >(((256-i)*(256-i))/4 - 1) + } + +; --- mul_by_38 lookup tables --- +mul38_lo_tab: + !byte 0 + !for i, 1, 255 { + !byte <(i * 38) + } + +mul38_hi_tab: + !byte 0 + !for i, 1, 255 { + !byte >(i * 38) + } + ; ============================================================================= ; Network layer buffers ; ============================================================================= @@ -230,6 +269,12 @@ x25_basepoint: !byte 9 !fill 31, 0 +; --- fe_mul optimization buffers (from c64-x25519 optimizations) --- +mul_cached_a: + !byte 0 ; cached src1[i] for inlined multiply +mul_src2_buf: + !fill 32, 0 ; absolute copy of src2 for fast indexed access + ; ============================================================================= ; ECDSA signature verification (moved from ecdsa_verify.asm to avoid ; $7800-$7BFF sqtab memory collision) diff --git a/src/http.asm b/src/http.asm index c9e2ede..40e7f23 100644 --- a/src/http.asm +++ b/src/http.asm @@ -16,43 +16,139 @@ ; Output: C=0 success (response in http_resp_buf), C=1 failure ; ============================================================================= http_get: - ; 1. DNS resolve hostname - ; jsr net_dns_resolve - ; bcs @error + ; --- 1. DNS resolve hostname --- + lda http_host_ptr + ldx http_host_ptr+1 + jsr net_dns_resolve + bcc @dns_ok + jmp @error +@dns_ok: + + ; --- 2. Set TCP destination IP --- + lda #ip65_dns_ip_addr + jsr net_set_tcp_dest + + ; --- 3. TCP connect on http_port --- + lda http_port + ldx http_port+1 + jsr net_tcp_connect + bcc @tcp_ok + jmp @error +@tcp_ok: - ; 2. TCP connect to resolved IP on port 443 - ; jsr net_tcp_connect - ; bcs @error + ; --- 4. Copy hostname to tls_hostname for SNI --- + lda http_host_ptr + sta zp_ptr + lda http_host_ptr+1 + sta zp_ptr+1 + ldy #0 +@copy_host: + cpy http_host_len + beq @copy_host_done + lda (zp_ptr),y + sta tls_hostname,y + iny + bne @copy_host ; always branches (hostname < 256) +@copy_host_done: + lda #0 + sta tls_hostname,y ; null-terminate + sty tls_hostname_len - ; 3. TLS handshake - ; jsr tls_connect - ; bcs @error + ; --- 5. TLS handshake --- + jsr tls_connect + bcc @tls_ok + jmp @tls_error +@tls_ok: - ; 4. Build GET request + ; --- 6. Build HTTP GET request --- jsr http_build_get - ; bcs @error - ; 5. Send via TLS - ; lda #http_req_buf - ; ... set length ... - ; jsr tls_send - ; bcs @error + ; --- 7. Send request via TLS --- + lda #http_req_buf + sta tls_app_ptr+1 + lda http_req_len + sta tls_app_len + lda http_req_len+1 + sta tls_app_len+1 + jsr tls_send + bcs @close_error - ; 6. Receive response via TLS - ; jsr http_recv_response - ; bcs @error + ; --- 8. Receive response via TLS --- + ; Initialise parser state + lda #0 + sta http_parse_state + sta http_line_idx + sta http_hdr_match + sta http_resp_len + sta http_resp_len+1 - ; 7. Close TLS + TCP - ; jsr tls_close + ; Poll + receive loop + lda #0 + sta @recv_timeout + sta @recv_timeout+1 +@recv_loop: + jsr net_poll + jsr tls_recv + bcs @recv_no_data + ; Got decrypted data in tls_app_ptr / tls_app_len + ; Copy tls_app_ptr to ZP for indirect addressing + lda tls_app_ptr + sta zp_ptr + lda tls_app_ptr+1 + sta zp_ptr+1 + + ; Feed decrypted bytes into the TCP ring buffer + ldy #0 +@feed_loop: + cpy tls_app_len ; low byte only (TLS records < 256) + beq @feed_done + lda (zp_ptr),y + ldx tcp_recv_tail + sta tcp_recv_buf,x + inx + stx tcp_recv_tail + iny + bne @feed_loop ; always branches +@feed_done: + ; Parse from ring buffer + jsr http_recv_response + bcc @recv_complete ; C=0 means parsing complete + ; Reset timeout counter on progress + lda #0 + sta @recv_timeout + sta @recv_timeout+1 + jmp @recv_loop + +@recv_no_data: + inc @recv_timeout + bne @recv_loop + inc @recv_timeout+1 + bne @recv_loop + ; Timeout — accept whatever we have + +@recv_complete: + jsr tls_close + jsr net_tcp_close clc rts -; @error: -; jsr tls_close -; sec -; rts +@recv_timeout: !word 0 + +@tls_error: + jsr net_tcp_close +@error: + sec + rts + +@close_error: + jsr tls_close + jsr net_tcp_close + sec + rts ; ============================================================================= ; http_build_get - construct HTTP/1.1 GET request in http_req_buf diff --git a/tools/bench_x25519.py b/tools/bench_x25519.py new file mode 100644 index 0000000..26ce696 --- /dev/null +++ b/tools/bench_x25519.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +"""bench_x25519.py -- X25519 key generation benchmark on C64. + +Runs x25519_base (scalar * basepoint 9) on the C64 and measures +wall-clock and jiffy-clock time. Verifies result against RFC 7748. + +Usage: + python3 tools/bench_x25519.py [--no-verify] [--no-blank] +""" + +import os +import subprocess +import sys +import time + +from c64_test_harness import ( + Labels, ViceConfig, ViceInstanceManager, + read_bytes, write_bytes, jsr, wait_for_text, +) + +try: + from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey + from cryptography.hazmat.primitives.serialization import ( + Encoding, PublicFormat, + ) + HAS_CRYPTO = True +except ImportError: + HAS_CRYPTO = False + +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") + +NTSC_HZ = 60 +NTSC_CYCLES_PER_SEC = 1_022_727 + +# Trampoline and result storage in cassette buffer area +TRAMPOLINE_ADDR = 0x0360 +BENCH_TICKS_ADDR = 0x0350 # 3 bytes for jiffy clock snapshot + +# Test scalar for basepoint multiply (x25519_base clamps this internally) +BENCH_SCALAR = bytes.fromhex( + "a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4" +) + + +def compute_expected_pubkey(scalar_bytes): + """Compute expected public key = clamp(scalar) * basepoint(9) via Python.""" + if not HAS_CRYPTO: + return None + # X25519PrivateKey.from_private_bytes applies clamping internally + privkey = X25519PrivateKey.from_private_bytes(scalar_bytes) + pubkey_bytes = privkey.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) + return pubkey_bytes + + +def build_trampoline(labels, blank=True): + """Build 6502 trampoline: zero jiffy, [blank VIC], jsr x25519_base, + snap jiffy, [unblank], rts.""" + code = bytearray() + + # SEI; zero jiffy clock ($A0-$A2, big-endian) + code += bytes([0x78]) # SEI + code += bytes([0xA9, 0x00]) # LDA #$00 + code += bytes([0x85, 0xA0]) # STA $A0 + code += bytes([0x85, 0xA1]) # STA $A1 + code += bytes([0x85, 0xA2]) # STA $A2 + code += bytes([0x58]) # CLI + + # Blank VIC-II (disable DEN bit 4 of $D011) for ~20-25% speedup + if blank: + code += bytes([0xAD, 0x11, 0xD0]) # LDA $D011 + code += bytes([0x29, 0xEF]) # AND #$EF + code += bytes([0x8D, 0x11, 0xD0]) # STA $D011 + + # JSR x25519_base + addr = labels["x25519_base"] + code += bytes([0x20, addr & 0xFF, addr >> 8]) + + # SEI; snapshot jiffy clock to BENCH_TICKS_ADDR + bt = BENCH_TICKS_ADDR + code += bytes([0x78]) # SEI + code += bytes([0xA5, 0xA0]) # LDA $A0 + code += bytes([0x8D, bt & 0xFF, bt >> 8]) # STA bench_ticks+0 + code += bytes([0xA5, 0xA1]) # LDA $A1 + code += bytes([0x8D, (bt+1) & 0xFF, (bt+1) >> 8]) # STA bench_ticks+1 + code += bytes([0xA5, 0xA2]) # LDA $A2 + code += bytes([0x8D, (bt+2) & 0xFF, (bt+2) >> 8]) # STA bench_ticks+2 + code += bytes([0x58]) # CLI + + # Unblank VIC-II + if blank: + code += bytes([0xAD, 0x11, 0xD0]) # LDA $D011 + code += bytes([0x09, 0x10]) # ORA #$10 + code += bytes([0x8D, 0x11, 0xD0]) # STA $D011 + + code += bytes([0x60]) # RTS + return bytes(code) + + +def jiffies_to_str(ticks): + secs = ticks / NTSC_HZ + if secs < 60: + return f"{ticks} jiffies ({secs:.1f}s)" + mins = secs / 60 + return f"{ticks} jiffies ({mins:.1f} min / {secs:.0f}s)" + + +def main(): + os.chdir(PROJECT_ROOT) + + verify = True + blank = True + for arg in sys.argv[1:]: + if arg == "--no-verify": + verify = False + elif arg == "--no-blank": + blank = False + + # Build + print("Building...") + 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) + + labels = Labels.from_file(LABELS_PATH) + + for name in ["x25519_base", "x25_scalar", "x25_result"]: + if labels.address(name) is None: + print(f"FATAL: '{name}' label not found") + sys.exit(1) + + trampoline = build_trampoline(labels, blank=blank) + + config = ViceConfig(prg_path=PRG_PATH, warp=True, ntsc=True, sound=False, + extra_args=["-reu", "-reusize", "512"]) + + print(f"Trampoline: {len(trampoline)} bytes at ${TRAMPOLINE_ADDR:04X}") + print(f"VIC-II blanking: {'ON' if blank else 'OFF'}") + + 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=120.0, verbose=False) + if grid is None: + print("FATAL: Boot menu did not appear") + sys.exit(1) + + # Safety loop at $0339 + write_bytes(transport, 0x0339, bytes([0x4C, 0x39, 0x03])) + + # Compute expected result + expected = compute_expected_pubkey(BENCH_SCALAR) + + # Write scalar and trampoline + write_bytes(transport, labels["x25_scalar"], BENCH_SCALAR) + write_bytes(transport, TRAMPOLINE_ADDR, trampoline) + + print(f"\n{'='*60}") + print(f" X25519 key generation: scalar * basepoint(9)") + print(f" Scalar: {BENCH_SCALAR[:16].hex()}...") + print(f"{'='*60}") + print(f"\n Running... (expect ~2-5 min wall clock in warp mode)") + + wall_start = time.time() + jsr(transport, TRAMPOLINE_ADDR, timeout=7200.0) + wall_elapsed = time.time() - wall_start + + # Read jiffy ticks (3 bytes, big-endian) + ticks_data = read_bytes(transport, BENCH_TICKS_ADDR, 3) + ticks = (ticks_data[0] << 16) | (ticks_data[1] << 8) | ticks_data[2] + + # Read result + result_bytes = read_bytes(transport, labels["x25_result"], 32) + + c64_secs = ticks / NTSC_HZ + est_cycles = c64_secs * NTSC_CYCLES_PER_SEC + + print(f"\n--- Results ---") + print(f" Jiffy clock: {jiffies_to_str(ticks)}") + print(f" Wall clock: {wall_elapsed:.1f}s ({wall_elapsed/60:.1f} min)") + if wall_elapsed > 0: + print(f" Warp factor: {c64_secs/wall_elapsed:.1f}x") + print(f" Est. cycles: {est_cycles:,.0f}") + print(f" C64 real-time: {c64_secs:.0f}s ({c64_secs/60:.1f} min)") + + if verify: + if expected is None: + print(f" Correctness: SKIPPED (pip install cryptography)") + print(f" result: {result_bytes.hex()}") + elif result_bytes == expected: + print(f" Correctness: PASS (matches Python X25519)") + else: + print(f" Correctness: FAIL") + print(f" expected: {expected.hex()}") + print(f" got: {result_bytes.hex()}") + + mgr.release(inst) + + print("\nDone.") + + +if __name__ == "__main__": + main() diff --git a/tools/run_all_tests.py b/tools/run_all_tests.py index 783733c..cf5403f 100644 --- a/tools/run_all_tests.py +++ b/tools/run_all_tests.py @@ -86,6 +86,10 @@ def run_test_suite(name, transport, labels, seed): from test_x509 import run_tests as x509_run passed, failed = x509_run(transport, labels) + elif name == "x25519": + from test_x25519 import run_tests as x25519_run + passed, failed = x25519_run(transport, labels, seed=seed) + except Exception as e: import traceback print(f" [{name}] EXCEPTION: {e}") @@ -124,11 +128,13 @@ def main(): # Entropy uses manual breakpoints sensitive to CPU state, so start it early # on a fresh worker. Remaining fast suites fill in around them. suites = ["entropy", "net", "sha256", "crypto", "hkdf", - "keyschedule", "http", "tls_record", "tls_handshake"] + "keyschedule", "http", "tls_record", "tls_handshake", + "x25519"] if not skip_slow: suites.insert(0, "x509") - config = ViceConfig(prg_path=PRG_PATH, warp=True, ntsc=True, sound=False) + config = ViceConfig(prg_path=PRG_PATH, warp=True, ntsc=True, sound=False, + extra_args=["-reu", "-reusize", "512"]) num_instances = min(workers, len(suites)) print(f"\n=== Launching {len(suites)} suites across " diff --git a/tools/test_x25519.py b/tools/test_x25519.py new file mode 100644 index 0000000..0a4dd2a --- /dev/null +++ b/tools/test_x25519.py @@ -0,0 +1,743 @@ +#!/usr/bin/env python3 +"""test_x25519.py -- fe25519 field arithmetic and X25519 key exchange tests. + +Tests fe_add, fe_sub, fe_mul, fe_sqr, fe_inv, fe_cswap, fe_mul_a24, +fe_copy, fe_zero, fe_one, x25519_clamp, and (with --slow) x25519_scalarmult +against Python reference implementations and RFC 7748 test vectors. + +Uses the binary monitor test harness -- jsr() is event-based via +checkpoints, so no polling or retry wrappers are needed. + +Usage: + python3 tools/test_x25519.py [--seed S] [--verbose] [--slow] +""" + +import os +import random +import subprocess +import sys + +from c64_test_harness import ( + Labels, ViceConfig, ViceInstanceManager, + read_bytes, write_bytes, jsr, wait_for_text, +) + +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") + +VERBOSE = False +SLOW = False + +# p = 2^255 - 19 +P = (1 << 255) - 19 + + +# ============================================================================ +# Python reference implementations +# ============================================================================ + +def fe_add_ref(a, b): + return (a + b) % P + +def fe_sub_ref(a, b): + return (a - b) % P + +def fe_mul_ref(a, b): + return (a * b) % P + +def fe_sqr_ref(a): + return (a * a) % P + +def fe_inv_ref(a): + return pow(a, P - 2, P) + +def fe_mul_a24_ref(a): + return (a * 121665) % P + +def int_to_le32(val): + """Convert integer to 32-byte little-endian bytes.""" + return (val % P).to_bytes(32, "little") + +def le32_to_int(data): + """Convert 32-byte little-endian bytes to integer.""" + return int.from_bytes(data, "little") + +def rand_fe(rng): + """Generate a random field element in [0, p-1].""" + return rng.randint(0, P - 1) + +def clamp_ref(scalar): + """Clamp scalar per RFC 7748.""" + s = bytearray(scalar) + s[0] &= 0xF8 + s[31] = (s[31] & 0x7F) | 0x40 + return bytes(s) + + +# RFC 7748 Section 6.1 test vectors +SCALAR_1 = bytes.fromhex( + "a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4") +U_1 = bytes.fromhex( + "e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c") +EXPECTED_1 = bytes.fromhex( + "c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552") + +SCALAR_2 = bytes.fromhex( + "4b66e9d4d1b4673c5ad22691957d6af5c11b6421e0ea01d42ca4169e7918ba0d") +U_2 = bytes.fromhex( + "e5210f12786811d3f4b7959d0538ae2c31dbe7106fc03c3efc4cd549c715a493") +EXPECTED_2 = bytes.fromhex( + "95cbde9476e8907d7aade45cb4b873f88b595a68799fa152e6f8f7647aac7957") + + +# ============================================================================ +# C64 helper functions +# ============================================================================ + +def set_fe_ptrs(transport, labels, src1=None, src2=None, dst=None): + """Set fe_src1, fe_src2, fe_dst zero-page pointers.""" + if src1 is not None: + write_bytes(transport, labels["fe_src1"], + bytes([src1 & 0xFF, src1 >> 8])) + if src2 is not None: + write_bytes(transport, labels["fe_src2"], + bytes([src2 & 0xFF, src2 >> 8])) + if dst is not None: + write_bytes(transport, labels["fe_dst"], + bytes([dst & 0xFF, dst >> 8])) + + +def write_fe(transport, addr, val): + """Write a field element (integer) to C64 memory as 32-byte LE.""" + write_bytes(transport, addr, int_to_le32(val)) + + +def read_fe(transport, addr): + """Read a 32-byte LE field element from C64 memory, return as integer.""" + return le32_to_int(read_bytes(transport, addr, 32)) + + +def c64_fe_add(transport, labels, a, b): + """Compute a + b mod p on C64.""" + write_fe(transport, labels["fe_tmp1"], a) + write_fe(transport, labels["fe_tmp2"], b) + set_fe_ptrs(transport, labels, + src1=labels["fe_tmp1"], + src2=labels["fe_tmp2"], + dst=labels["fe_tmp3"]) + jsr(transport, labels["fe_add"]) + return read_fe(transport, labels["fe_tmp3"]) + + +def c64_fe_sub(transport, labels, a, b): + """Compute a - b mod p on C64.""" + write_fe(transport, labels["fe_tmp1"], a) + write_fe(transport, labels["fe_tmp2"], b) + set_fe_ptrs(transport, labels, + src1=labels["fe_tmp1"], + src2=labels["fe_tmp2"], + dst=labels["fe_tmp3"]) + jsr(transport, labels["fe_sub"]) + return read_fe(transport, labels["fe_tmp3"]) + + +def c64_fe_mul(transport, labels, a, b): + """Compute a * b mod p on C64.""" + write_fe(transport, labels["fe_tmp1"], a) + write_fe(transport, labels["fe_tmp2"], b) + set_fe_ptrs(transport, labels, + src1=labels["fe_tmp1"], + src2=labels["fe_tmp2"], + dst=labels["fe_tmp3"]) + jsr(transport, labels["fe_mul"], timeout=120.0) + return read_fe(transport, labels["fe_tmp3"]) + + +def c64_fe_sqr(transport, labels, a): + """Compute a^2 mod p on C64.""" + write_fe(transport, labels["fe_tmp1"], a) + set_fe_ptrs(transport, labels, + src1=labels["fe_tmp1"], + dst=labels["fe_tmp3"]) + jsr(transport, labels["fe_sqr"], timeout=120.0) + return read_fe(transport, labels["fe_tmp3"]) + + +def c64_fe_inv(transport, labels, a): + """Compute a^(p-2) mod p on C64.""" + write_fe(transport, labels["fe_tmp1"], a) + set_fe_ptrs(transport, labels, + src1=labels["fe_tmp1"], + dst=labels["fe_tmp3"]) + # fe_inv takes ~253 squarings + 11 muls -- very slow + jsr(transport, labels["fe_inv"], timeout=600.0) + return read_fe(transport, labels["fe_tmp3"]) + + +def c64_fe_mul_a24(transport, labels, a): + """Compute a * 121665 mod p on C64.""" + write_fe(transport, labels["fe_tmp1"], a) + set_fe_ptrs(transport, labels, + src1=labels["fe_tmp1"], + dst=labels["fe_tmp3"]) + jsr(transport, labels["fe_mul_a24"], timeout=60.0) + return read_fe(transport, labels["fe_tmp3"]) + + +def c64_fe_copy(transport, labels, a): + """Copy a field element via fe_copy.""" + write_fe(transport, labels["fe_tmp1"], a) + set_fe_ptrs(transport, labels, + src1=labels["fe_tmp1"], + dst=labels["fe_tmp3"]) + jsr(transport, labels["fe_copy"]) + return read_fe(transport, labels["fe_tmp3"]) + + +def c64_fe_zero(transport, labels): + """Zero a field element via fe_zero.""" + # Write nonzero first to prove it gets zeroed + write_fe(transport, labels["fe_tmp3"], P - 1) + set_fe_ptrs(transport, labels, dst=labels["fe_tmp3"]) + jsr(transport, labels["fe_zero"]) + return read_fe(transport, labels["fe_tmp3"]) + + +def c64_fe_one(transport, labels): + """Set a field element to 1 via fe_one.""" + write_fe(transport, labels["fe_tmp3"], P - 1) + set_fe_ptrs(transport, labels, dst=labels["fe_tmp3"]) + jsr(transport, labels["fe_one"]) + return read_fe(transport, labels["fe_tmp3"]) + + +def c64_x25519_clamp(transport, labels, scalar): + """Clamp a scalar on C64. Returns clamped scalar bytes.""" + write_bytes(transport, labels["x25_scalar"], scalar) + jsr(transport, labels["x25519_clamp"]) + return read_bytes(transport, labels["x25_scalar"], 32) + + +def c64_x25519_scalarmult(transport, labels, scalar, u): + """Compute scalar * u on C64. Returns 32-byte result.""" + write_bytes(transport, labels["x25_scalar"], scalar) + write_bytes(transport, labels["x25_u"], u) + jsr(transport, labels["x25519_scalarmult"], timeout=7200.0) + return read_bytes(transport, labels["x25_result"], 32) + + +# ============================================================================ +# Test functions -- fe25519 field operations +# ============================================================================ + +def test_fe_copy_zero_one(transport, labels): + """Test fe_copy, fe_zero, fe_one.""" + passed = failed = 0 + + # fe_zero + result = c64_fe_zero(transport, labels) + if result == 0: + passed += 1 + if VERBOSE: + print(" PASS fe_zero") + else: + failed += 1 + print(f" FAIL fe_zero: got {result}") + + # fe_one + result = c64_fe_one(transport, labels) + if result == 1: + passed += 1 + if VERBOSE: + print(" PASS fe_one") + else: + failed += 1 + print(f" FAIL fe_one: got {result}") + + # fe_copy + test_val = 0xDEADBEEF_CAFEBABE_12345678_9ABCDEF0 + result = c64_fe_copy(transport, labels, test_val) + if result == test_val: + passed += 1 + if VERBOSE: + print(" PASS fe_copy") + else: + failed += 1 + print(f" FAIL fe_copy: expected {test_val:#x}, got {result:#x}") + + return passed, failed + + +def test_fe_add(transport, labels, rng): + """Test fe_add with boundary cases and random inputs.""" + passed = failed = 0 + + cases = [ + ("0+0", 0, 0), + ("0+1", 0, 1), + ("1+1", 1, 1), + ("p-1+1", P - 1, 1), + ("p-1+p-1", P - 1, P - 1), + ("large+large", P - 10, 15), + ] + for i in range(6): + a, b = rand_fe(rng), rand_fe(rng) + cases.append((f"random #{i}", a, b)) + + for name, a, b in cases: + expected = fe_add_ref(a, b) + result = c64_fe_add(transport, labels, a, b) + if result == expected: + passed += 1 + if VERBOSE: + print(f" PASS add {name}") + else: + failed += 1 + print(f" FAIL add {name}: expected {expected}, got {result}") + + return passed, failed + + +def test_fe_sub(transport, labels, rng): + """Test fe_sub with boundary cases and random inputs.""" + passed = failed = 0 + + cases = [ + ("0-0", 0, 0), + ("1-0", 1, 0), + ("1-1", 1, 1), + ("0-1", 0, 1), + ("10-20", 10, 20), + ("p-1-0", P - 1, 0), + ] + for i in range(6): + a, b = rand_fe(rng), rand_fe(rng) + cases.append((f"random #{i}", a, b)) + + for name, a, b in cases: + expected = fe_sub_ref(a, b) + result = c64_fe_sub(transport, labels, a, b) + if result == expected: + passed += 1 + if VERBOSE: + print(f" PASS sub {name}") + else: + failed += 1 + print(f" FAIL sub {name}: expected {expected}, got {result}") + + return passed, failed + + +def test_fe_mul(transport, labels, rng): + """Test fe_mul with identity, zero, and random inputs.""" + passed = failed = 0 + + cases = [ + ("0*0", 0, 0), + ("0*1", 0, 1), + ("1*1", 1, 1), + ("2*3", 2, 3), + ("a*0", rand_fe(rng), 0), + ("1*a", 1, rand_fe(rng)), + ] + for i in range(4): + a, b = rand_fe(rng), rand_fe(rng) + cases.append((f"random #{i}", a, b)) + + for name, a, b in cases: + expected = fe_mul_ref(a, b) + result = c64_fe_mul(transport, labels, a, b) + if result == expected: + passed += 1 + if VERBOSE: + print(f" PASS mul {name}") + else: + failed += 1 + print(f" FAIL mul {name}:") + print(f" a = {a}") + print(f" b = {b}") + print(f" expected = {expected}") + print(f" got = {result}") + + return passed, failed + + +def test_fe_sqr(transport, labels, rng): + """Test fe_sqr against Python reference.""" + passed = failed = 0 + + cases = [0, 1, 2, P - 1, rand_fe(rng), rand_fe(rng), rand_fe(rng)] + + for i, a in enumerate(cases): + expected = fe_sqr_ref(a) + result = c64_fe_sqr(transport, labels, a) + if result == expected: + passed += 1 + if VERBOSE: + print(f" PASS sqr #{i}") + else: + failed += 1 + print(f" FAIL sqr #{i}: a={a}, expected={expected}, got={result}") + + return passed, failed + + +def test_fe_inv(transport, labels, rng): + """Test fe_inv: inv(1)==1, inv(2)*2==1. + + Full fe_inv takes ~10 minutes per call in VICE. Test inv(1) which is + fast, plus inv(2) as a second case (small value, verifiable). + """ + passed = failed = 0 + + cases = [1, 2] + + for i, a in enumerate(cases): + print(f" inv test #{i} (a={a:#x})...", end="", flush=True) + inv_a = c64_fe_inv(transport, labels, a) + expected = fe_inv_ref(a) + + if inv_a == expected: + passed += 1 + print(" PASS" if VERBOSE else " ok") + else: + failed += 1 + print(" FAIL") + print(f" expected inv = {expected}") + print(f" got inv = {inv_a}") + product = (a * inv_a) % P + print(f" a * got_inv mod p = {product}") + + return passed, failed + + +def test_fe_cswap(transport, labels, rng): + """Test fe_cswap constant-time swap with mask=$00 and mask=$FF.""" + passed = failed = 0 + + a = rand_fe(rng) + b = rand_fe(rng) + + cswap_addr = labels["fe_cswap"] + trampoline = labels["input_buffer"] + + # No-swap test (mask = $00) + write_fe(transport, labels["fe_tmp1"], a) + write_fe(transport, labels["fe_tmp2"], b) + set_fe_ptrs(transport, labels, + src1=labels["fe_tmp1"], + src2=labels["fe_tmp2"]) + write_bytes(transport, trampoline, bytes([ + 0xA9, 0x00, # LDA #$00 + 0x4C, cswap_addr & 0xFF, cswap_addr >> 8, # JMP fe_cswap + ])) + jsr(transport, trampoline) + r_a = read_fe(transport, labels["fe_tmp1"]) + r_b = read_fe(transport, labels["fe_tmp2"]) + + if r_a == a and r_b == b: + passed += 1 + if VERBOSE: + print(" PASS cswap no-swap") + else: + failed += 1 + print(f" FAIL cswap no-swap: a changed={r_a != a}, b changed={r_b != b}") + + # Swap test (mask = $FF) + write_fe(transport, labels["fe_tmp1"], a) + write_fe(transport, labels["fe_tmp2"], b) + set_fe_ptrs(transport, labels, + src1=labels["fe_tmp1"], + src2=labels["fe_tmp2"]) + write_bytes(transport, trampoline, bytes([ + 0xA9, 0xFF, # LDA #$FF + 0x4C, cswap_addr & 0xFF, cswap_addr >> 8, # JMP fe_cswap + ])) + jsr(transport, trampoline) + r_a = read_fe(transport, labels["fe_tmp1"]) + r_b = read_fe(transport, labels["fe_tmp2"]) + + if r_a == b and r_b == a: + passed += 1 + if VERBOSE: + print(" PASS cswap swap") + else: + failed += 1 + print(f" FAIL cswap swap: expected ({b:#x},{a:#x}), " + f"got ({r_a:#x},{r_b:#x})") + + return passed, failed + + +def test_fe_mul_a24(transport, labels, rng): + """Test fe_mul_a24 (multiply by 121665).""" + passed = failed = 0 + + cases = [0, 1, 2, 121665, P - 1, + rand_fe(rng), rand_fe(rng), rand_fe(rng)] + + for i, a in enumerate(cases): + expected = fe_mul_a24_ref(a) + result = c64_fe_mul_a24(transport, labels, a) + if result == expected: + passed += 1 + if VERBOSE: + print(f" PASS mul_a24 #{i}") + else: + failed += 1 + print(f" FAIL mul_a24 #{i}: a={a}, expected={expected}, " + f"got={result}") + + return passed, failed + + +def test_fe_add_sub_inverse(transport, labels, rng): + """Test that (a + b) - b == a (add/sub are inverses).""" + passed = failed = 0 + + for i in range(5): + a = rand_fe(rng) + b = rand_fe(rng) + sum_ab = c64_fe_add(transport, labels, a, b) + result = c64_fe_sub(transport, labels, sum_ab, b) + if result == a: + passed += 1 + if VERBOSE: + print(f" PASS add_sub_inverse #{i}") + else: + failed += 1 + print(f" FAIL add_sub_inverse #{i}: expected {a}, got {result}") + + return passed, failed + + +# ============================================================================ +# Test functions -- x25519 +# ============================================================================ + +def test_x25519_clamp(transport, labels, rng): + """Test x25519_clamp against reference implementation.""" + passed = failed = 0 + + # Fixed cases + cases = [ + bytes(range(32)), + bytes([0xFF] * 32), + bytes([0x00] * 32), + bytes([0xA5] * 32), + ] + # Random cases + for _ in range(6): + cases.append(bytes(rng.getrandbits(8) for _ in range(32))) + + for i, scalar in enumerate(cases): + expected = clamp_ref(scalar) + result = c64_x25519_clamp(transport, labels, scalar) + if result == expected: + passed += 1 + if VERBOSE: + print(f" PASS clamp #{i}") + else: + failed += 1 + print(f" FAIL clamp #{i}:") + print(f" input: {scalar.hex()}") + print(f" expected: {expected.hex()}") + print(f" got: {result.hex()}") + # Show which bytes differ + for j in range(32): + if expected[j] != result[j]: + print(f" byte[{j}]: expected 0x{expected[j]:02x}, " + f"got 0x{result[j]:02x}") + + return passed, failed + + +def test_x25519_rfc7748_vector1(transport, labels): + """RFC 7748 Section 6.1 test vector 1.""" + passed = failed = 0 + + print(" RFC 7748 vector 1...", end="", flush=True) + result = c64_x25519_scalarmult(transport, labels, SCALAR_1, U_1) + + if result == EXPECTED_1: + passed += 1 + print(" PASS") + else: + failed += 1 + print(" FAIL") + print(f" expected: {EXPECTED_1.hex()}") + print(f" got: {result.hex()}") + + return passed, failed + + +def test_x25519_rfc7748_vector2(transport, labels): + """RFC 7748 Section 6.1 test vector 2.""" + passed = failed = 0 + + print(" RFC 7748 vector 2...", end="", flush=True) + result = c64_x25519_scalarmult(transport, labels, SCALAR_2, U_2) + + if result == EXPECTED_2: + passed += 1 + print(" PASS") + else: + failed += 1 + print(" FAIL") + print(f" expected: {EXPECTED_2.hex()}") + print(f" got: {result.hex()}") + + return passed, failed + + +# ============================================================================ +# Main +# ============================================================================ + +def run_tests(transport, labels, seed): + """Run all test groups. Returns (passed, failed).""" + rng = random.Random(seed) + total_passed = 0 + total_failed = 0 + + test_groups = [ + ("fe_copy/zero/one", + lambda: test_fe_copy_zero_one(transport, labels)), + ("fe_add", + lambda: test_fe_add(transport, labels, rng)), + ("fe_sub", + lambda: test_fe_sub(transport, labels, rng)), + ("fe_add/sub inverse", + lambda: test_fe_add_sub_inverse(transport, labels, rng)), + ("fe_mul", + lambda: test_fe_mul(transport, labels, rng)), + ("fe_sqr", + lambda: test_fe_sqr(transport, labels, rng)), + ("fe_mul_a24", + lambda: test_fe_mul_a24(transport, labels, rng)), + ("fe_cswap", + lambda: test_fe_cswap(transport, labels, rng)), + ("fe_inv", + lambda: test_fe_inv(transport, labels, rng)), + ("x25519_clamp", + lambda: test_x25519_clamp(transport, labels, rng)), + ] + + if SLOW: + test_groups += [ + ("x25519 RFC 7748 vector 1", + lambda: test_x25519_rfc7748_vector1(transport, labels)), + ("x25519 RFC 7748 vector 2", + lambda: test_x25519_rfc7748_vector2(transport, labels)), + ] + else: + print("\n (x25519 scalarmult tests skipped -- " + "use --slow to enable, ~100 min each)") + + for name, test_fn in test_groups: + print(f"\n--- {name} ---") + try: + p, f = test_fn() + total_passed += p + total_failed += f + status = "OK" if f == 0 else "FAIL" + print(f" {status}: {p}/{p + f} passed") + except Exception as e: + total_failed += 1 + print(f" ERROR: {e}") + import traceback + traceback.print_exc() + + return total_passed, total_failed + + +def main(): + global VERBOSE, SLOW + os.chdir(PROJECT_ROOT) + + seed = random.randint(0, 2**32 - 1) + args = sys.argv[1:] + i = 0 + while i < len(args): + if args[i] == "--seed" and i + 1 < len(args): + seed = int(args[i + 1]) + i += 2 + elif args[i] == "--verbose": + VERBOSE = True + i += 1 + elif args[i] == "--slow": + SLOW = True + i += 1 + else: + i += 1 + + random.seed(seed) + 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) + + assert os.path.exists(PRG_PATH), f"{PRG_PATH} not found after build" + print(f" Build OK: {PRG_PATH}") + + # Load labels + labels = Labels.from_file(LABELS_PATH) + + required = [ + "fe_src1", "fe_src2", "fe_dst", + "fe_copy", "fe_zero", "fe_one", + "fe_add", "fe_sub", "fe_mul", "fe_sqr", "fe_inv", + "fe_cswap", "fe_mul_a24", + "fe_tmp1", "fe_tmp2", "fe_tmp3", + "x25519_clamp", "x25519_scalarmult", + "x25_scalar", "x25_u", "x25_result", + "input_buffer", + ] + for name in required: + if labels.address(name) is None: + print(f"FATAL: '{name}' label not found in {LABELS_PATH}") + sys.exit(1) + + print(f" Labels loaded: {len(required)} required labels verified") + + # Launch VICE + config = ViceConfig(prg_path=PRG_PATH, warp=True, ntsc=True, sound=False, + extra_args=["-reu", "-reusize", "512"]) + print("\n=== Starting VICE ===") + + 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") + sys.exit(1) + + print(" VICE ready, running tests...") + + # Safety: write JMP $0339 at $0339 so CPU loops harmlessly + # after jsr() returns (prevents crash when BASIC ROM is banked out) + write_bytes(transport, 0x0339, bytes([0x4C, 0x39, 0x03])) + + passed, failed = run_tests(transport, labels, seed) + + mgr.release(inst) + + total = passed + failed + print(f"\n{'='*60}") + print(f"RESULTS: {passed}/{total} passed, {failed}/{total} failed") + print(f"{'='*60}") + sys.exit(0 if failed == 0 else 1) + + +if __name__ == "__main__": + main() From 001d3a7d57b7b735b098b5c5db211231011b38ad Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Thu, 2 Apr 2026 19:48:41 -0500 Subject: [PATCH 05/22] Add consolidated network test environment with context manager Introduces tools/net_test_env.py with NetworkTestEnv context manager that handles TAP interface, dnsmasq, and optional HTTPS server lifecycle. Replaces duplicated inline setup/teardown code across network test files and guarantees cleanup via __exit__, signal handlers, and atexit. Migrates test_dns.py as proof-of-concept. Includes 14 unit tests. Co-Authored-By: Claude Opus 4.6 (1M context) --- tools/net_test_env.py | 544 +++++++++++++++++++++++++++++++++++++ tools/test_dns.py | 343 +++++++++-------------- tools/test_net_test_env.py | 231 ++++++++++++++++ 3 files changed, 907 insertions(+), 211 deletions(-) create mode 100644 tools/net_test_env.py create mode 100644 tools/test_net_test_env.py diff --git a/tools/net_test_env.py b/tools/net_test_env.py new file mode 100644 index 0000000..6938ebb --- /dev/null +++ b/tools/net_test_env.py @@ -0,0 +1,544 @@ +#!/usr/bin/env python3 +"""net_test_env.py -- Consolidated network test environment for C64 VICE emulator tests. + +Provides a NetworkTestEnv context manager that handles TAP interface setup, +dnsmasq lifecycle, and optional HTTP/HTTPS server startup. Replaces the +duplicated inline setup/teardown code across test_dns.py, test_http_integration.py, +and test_https_integration.py. + +Usage as context manager: + with NetworkTestEnv(dns_records={"c64test.local": "10.0.65.1"}) as env: + # env.dnsmasq_proc is running + # env.server is running if http_server=True + run_tests(...) + +Usage as CLI: + python3 tools/net_test_env.py --dns-record c64test.local=10.0.65.1 + python3 tools/net_test_env.py --wrap python3 tools/test_dns.py +""" + +from __future__ import annotations + +import argparse +import atexit +import os +import shutil +import signal +import ssl +import subprocess +import sys +import time +from typing import Optional + +# Allow importing test_server from the same directory. +sys.path.insert(0, os.path.dirname(__file__)) +from test_server import TestHTTPServer + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +TAP_SYSFS = "/sys/class/net/{iface}" +SETUP_TAP_SCRIPT = os.path.join( + os.path.dirname(__file__), "..", "..", + "c64-test-harness", "scripts", "setup-tap-networking.sh", +) +# Resolve to absolute path +SETUP_TAP_SCRIPT = os.path.normpath(SETUP_TAP_SCRIPT) + +DEFAULT_DNS_RECORDS: dict[str, str] = {"c64test.local": "10.0.65.1"} + + +# --------------------------------------------------------------------------- +# Standalone helpers +# --------------------------------------------------------------------------- + +def skip_if_no_network(tap_interface: str = "tap-c64") -> bool: + """Check if network test prerequisites are missing. + + Returns True if tests should be skipped (i.e., something is missing). + Prints a SKIP message for the first missing prerequisite found. + """ + if not os.path.exists(TAP_SYSFS.format(iface=tap_interface)): + print(f"SKIP: {tap_interface} interface not found") + return True + if shutil.which("x64sc") is None: + print("SKIP: x64sc not on PATH") + return True + if shutil.which("dnsmasq") is None: + print("SKIP: dnsmasq not on PATH") + return True + if shutil.which("sudo") is None: + print("SKIP: sudo not on PATH") + return True + return False + + +def start_dnsmasq( + tap_interface: str = "tap-c64", + tap_address: str = "10.0.65.1", + dhcp_range: tuple[str, str] = ("10.0.65.2", "10.0.65.10"), + dns_records: dict[str, str] | None = None, + extra_args: list[str] | None = None, + verbose: bool = True, +) -> subprocess.Popen: + """Start dnsmasq providing DHCP and DNS on a TAP interface. + + Args: + tap_interface: Network interface to bind to. + tap_address: Listen address for dnsmasq. + dhcp_range: (start, end) IP range for DHCP leases. + dns_records: Mapping of hostname -> IP for --address entries. + extra_args: Additional command-line arguments for dnsmasq. + verbose: Print the command and PID. + + Returns: + The Popen object for the dnsmasq process. + + Raises: + RuntimeError: If dnsmasq exits immediately after launch. + """ + if dns_records is None: + dns_records = dict(DEFAULT_DNS_RECORDS) + + range_start, range_end = dhcp_range + cmd = [ + "sudo", "dnsmasq", + "--no-daemon", + f"--interface={tap_interface}", + "--bind-interfaces", + f"--listen-address={tap_address}", + f"--dhcp-range={range_start},{range_end},255.255.255.0,5m", + f"--dhcp-option=6,{tap_address}", + "--log-queries", + "--no-resolv", + ] + for hostname, ip in dns_records.items(): + cmd.append(f"--address=/{hostname}/{ip}") + if extra_args: + cmd.extend(extra_args) + + if verbose: + print(f" dnsmasq cmd: {' '.join(cmd)}") + + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + # Give it a moment to bind ports. + time.sleep(0.5) + if proc.poll() is not None: + _, stderr = proc.communicate() + raise RuntimeError(f"dnsmasq failed to start: {stderr.decode()}") + + if verbose: + print(f" dnsmasq PID={proc.pid}") + return proc + + +def stop_dnsmasq(proc: subprocess.Popen, timeout: int = 5) -> None: + """Terminate a dnsmasq process gracefully, killing it if necessary. + + Args: + proc: The Popen object returned by start_dnsmasq(). + timeout: Seconds to wait for graceful termination before killing. + """ + if proc.poll() is not None: + return # Already exited. + try: + proc.terminate() + try: + proc.communicate(timeout=timeout) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait() + except OSError: + pass # Process already gone. + + +def _kill_stale_dnsmasq() -> None: + """Kill any leftover dnsmasq processes. Errors are silently ignored.""" + try: + subprocess.run( + ["sudo", "killall", "dnsmasq"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except OSError: + pass + + +# --------------------------------------------------------------------------- +# NetworkTestEnv context manager +# --------------------------------------------------------------------------- + +class NetworkTestEnv: + """Context manager that sets up and tears down the full network test environment. + + Manages: + - TAP interface creation (optional, delegates to setup-tap-networking.sh) + - dnsmasq lifecycle (DHCP + DNS) + - Optional HTTP/HTTPS server via TestHTTPServer + + Example:: + + with NetworkTestEnv(http_server=True, http_port=8080) as env: + assert env.dnsmasq_proc.poll() is None # running + assert env.server is not None + # ... run VICE tests ... + """ + + def __init__( + self, + tap_interface: str = "tap-c64", + tap_address: str = "10.0.65.1", + dhcp_range: tuple[str, str] = ("10.0.65.2", "10.0.65.10"), + dns_records: dict[str, str] | None = None, + extra_dnsmasq_args: list[str] | None = None, + setup_tap: bool = True, + teardown_tap: bool = False, + http_server: bool = False, + http_host: str = "10.0.65.1", + http_port: int = 80, + ssl_context: ssl.SSLContext | None = None, + verbose: bool = True, + ): + self.tap_interface = tap_interface + self.tap_address = tap_address + self.dhcp_range = dhcp_range + self.dns_records = dns_records if dns_records is not None else dict(DEFAULT_DNS_RECORDS) + self.extra_dnsmasq_args = extra_dnsmasq_args + self.setup_tap = setup_tap + self.teardown_tap = teardown_tap + self.http_server_enabled = http_server + self.http_host = http_host + self.http_port = http_port + self.ssl_context = ssl_context + self.verbose = verbose + + self._dnsmasq_proc: subprocess.Popen | None = None + self._server: TestHTTPServer | None = None + self._torn_down = False + self._prev_sigint = None + self._prev_sigterm = None + + # ---- Properties -------------------------------------------------------- + + @property + def dnsmasq_proc(self) -> subprocess.Popen | None: + """The running dnsmasq Popen object, or None if not started.""" + return self._dnsmasq_proc + + @property + def server(self) -> TestHTTPServer | None: + """The running TestHTTPServer instance, or None if not started.""" + return self._server + + # ---- Prerequisite check ------------------------------------------------ + + def check_prerequisites(self) -> list[str]: + """Return a list of missing prerequisites. Empty list means all OK.""" + missing: list[str] = [] + if not self.setup_tap and not os.path.exists( + TAP_SYSFS.format(iface=self.tap_interface) + ): + missing.append(f"{self.tap_interface} interface not found (and setup_tap=False)") + if shutil.which("dnsmasq") is None: + missing.append("dnsmasq not on PATH") + if shutil.which("sudo") is None: + missing.append("sudo not on PATH") + if self.setup_tap and not os.path.isfile(SETUP_TAP_SCRIPT): + missing.append(f"TAP setup script not found: {SETUP_TAP_SCRIPT}") + return missing + + # ---- Setup / teardown -------------------------------------------------- + + def setup(self) -> "NetworkTestEnv": + """Set up the network test environment. + + 1. Create TAP interface if needed. + 2. Kill stale dnsmasq processes. + 3. Start dnsmasq. + 4. Start HTTP server if requested. + + Returns self for chaining. + """ + # Install signal handlers and atexit for safety. + self._install_signal_handlers() + atexit.register(self.teardown) + + # 1. TAP interface. + tap_exists = os.path.exists(TAP_SYSFS.format(iface=self.tap_interface)) + if self.setup_tap and not tap_exists: + if self.verbose: + print(f" Setting up TAP interface {self.tap_interface}...") + result = subprocess.run( + ["sudo", SETUP_TAP_SCRIPT], + capture_output=True, + text=True, + ) + if result.returncode != 0: + raise RuntimeError( + f"TAP setup failed (exit {result.returncode}):\n{result.stderr}" + ) + if self.verbose: + print(f" TAP interface {self.tap_interface} created") + elif tap_exists: + if self.verbose: + print(f" TAP interface {self.tap_interface} already exists") + else: + if self.verbose: + print(f" Skipping TAP setup (setup_tap=False)") + + # 2. Kill stale dnsmasq. + _kill_stale_dnsmasq() + + # 3. Start dnsmasq. + if self.verbose: + print(" Starting dnsmasq...") + self._dnsmasq_proc = start_dnsmasq( + tap_interface=self.tap_interface, + tap_address=self.tap_address, + dhcp_range=self.dhcp_range, + dns_records=self.dns_records, + extra_args=self.extra_dnsmasq_args, + verbose=self.verbose, + ) + + # 4. HTTP server. + if self.http_server_enabled: + if self.verbose: + proto = "HTTPS" if self.ssl_context else "HTTP" + print(f" Starting {proto} server on {self.http_host}:{self.http_port}...") + self._server = TestHTTPServer( + host=self.http_host, + port=self.http_port, + ssl_context=self.ssl_context, + ) + self._server.start() + if self.verbose: + proto = "HTTPS" if self.ssl_context else "HTTP" + print(f" {proto} server listening on {self.http_host}:{self.http_port}") + + return self + + def teardown(self) -> None: + """Tear down the network test environment. Idempotent.""" + if self._torn_down: + return + self._torn_down = True + + if self.verbose: + print(" NetworkTestEnv teardown...") + + # Stop HTTP server. + if self._server is not None: + try: + self._server.stop() + if self.verbose: + proto = "HTTPS" if self.ssl_context else "HTTP" + print(f" {proto} server stopped") + except Exception as e: + print(f" WARNING: HTTP server stop failed: {e}") + self._server = None + + # Stop dnsmasq. + if self._dnsmasq_proc is not None: + try: + stop_dnsmasq(self._dnsmasq_proc) + if self.verbose: + print(f" dnsmasq stopped (exit={self._dnsmasq_proc.returncode})") + except Exception as e: + print(f" WARNING: dnsmasq stop failed: {e}") + self._dnsmasq_proc = None + + # Teardown TAP if requested. + if self.teardown_tap: + try: + subprocess.run( + ["sudo", "ip", "link", "delete", self.tap_interface], + capture_output=True, + ) + if self.verbose: + print(f" TAP interface {self.tap_interface} removed") + except Exception as e: + print(f" WARNING: TAP teardown failed: {e}") + + # Restore signal handlers. + self._restore_signal_handlers() + + # Unregister atexit (best-effort; atexit doesn't support unregister, + # but the idempotent guard above prevents double-teardown). + + # ---- Context manager protocol ------------------------------------------ + + def __enter__(self) -> "NetworkTestEnv": + return self.setup() + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + self.teardown() + + # ---- Signal handling --------------------------------------------------- + + def _install_signal_handlers(self) -> None: + """Install SIGINT/SIGTERM handlers that trigger teardown.""" + def _handler(signum, frame): + self.teardown() + # Re-raise with default handler so the process exits with the + # correct signal status. + signal.signal(signum, signal.SIG_DFL) + os.kill(os.getpid(), signum) + + try: + self._prev_sigint = signal.signal(signal.SIGINT, _handler) + self._prev_sigterm = signal.signal(signal.SIGTERM, _handler) + except (OSError, ValueError): + # signal.signal can fail if not on the main thread. + pass + + def _restore_signal_handlers(self) -> None: + """Restore previous signal handlers.""" + try: + if self._prev_sigint is not None: + signal.signal(signal.SIGINT, self._prev_sigint) + self._prev_sigint = None + if self._prev_sigterm is not None: + signal.signal(signal.SIGTERM, self._prev_sigterm) + self._prev_sigterm = None + except (OSError, ValueError): + pass + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + +def _parse_dns_record(value: str) -> tuple[str, str]: + """Parse a 'host=ip' string into a (host, ip) tuple.""" + if "=" not in value: + raise argparse.ArgumentTypeError( + f"DNS record must be in host=ip format, got: {value!r}" + ) + host, ip = value.split("=", 1) + return host.strip(), ip.strip() + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Set up network test environment for C64 VICE emulator tests.", + ) + parser.add_argument( + "--setup-tap", action="store_true", default=True, + help="Set up TAP interface if it doesn't exist (default: True)", + ) + parser.add_argument( + "--no-setup-tap", action="store_false", dest="setup_tap", + help="Skip TAP interface setup", + ) + parser.add_argument( + "--teardown-tap", action="store_true", default=False, + help="Tear down TAP interface on exit", + ) + parser.add_argument( + "--dns-record", action="append", type=_parse_dns_record, + metavar="HOST=IP", dest="dns_records", + help="DNS record (repeatable). Default: c64test.local=10.0.65.1", + ) + parser.add_argument( + "--http-port", type=int, default=None, + help="Start an HTTP server on this port", + ) + parser.add_argument( + "--https-port", type=int, default=None, + help="Start an HTTPS server on this port (generates self-signed cert)", + ) + parser.add_argument( + "--wrap", nargs=argparse.REMAINDER, metavar="CMD", + help="Run CMD with the environment set up, then teardown and exit", + ) + parser.add_argument( + "--quiet", action="store_true", default=False, + help="Suppress verbose output", + ) + + args = parser.parse_args() + + # Build dns_records dict. + dns_records: dict[str, str] | None = None + if args.dns_records: + dns_records = dict(args.dns_records) + + # Determine HTTP/HTTPS settings. + http_server = args.http_port is not None or args.https_port is not None + http_port = args.https_port or args.http_port or 80 + ssl_ctx: ssl.SSLContext | None = None + + if args.https_port is not None: + import tempfile + cert_dir = tempfile.mkdtemp(prefix="c64tls_") + cert_path = os.path.join(cert_dir, "cert.pem") + key_path = os.path.join(cert_dir, "key.pem") + subprocess.run([ + "openssl", "req", "-new", "-x509", + "-newkey", "ec", "-pkeyopt", "ec_paramgen_curve:prime256v1", + "-keyout", key_path, "-out", cert_path, + "-days", "1", "-nodes", + "-subj", "/CN=c64test.local", + ], check=True, capture_output=True) + ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ssl_ctx.minimum_version = ssl.TLSVersion.TLSv1_3 + ssl_ctx.maximum_version = ssl.TLSVersion.TLSv1_3 + ssl_ctx.load_cert_chain(cert_path, key_path) + if not args.quiet: + print(f"Generated self-signed TLS cert in {cert_dir}") + + verbose = not args.quiet + + env = NetworkTestEnv( + dns_records=dns_records, + extra_dnsmasq_args=None, + setup_tap=args.setup_tap, + teardown_tap=args.teardown_tap, + http_server=http_server, + http_port=http_port, + ssl_context=ssl_ctx, + verbose=verbose, + ) + + # Check prerequisites before doing anything. + missing = env.check_prerequisites() + if missing: + for m in missing: + print(f"ERROR: {m}") + return 1 + + if args.wrap: + # --wrap mode: setup, run command, teardown, exit with command's code. + if not args.wrap: + parser.error("--wrap requires a command") + with env: + if verbose: + print(f"\n Running: {' '.join(args.wrap)}") + result = subprocess.run(args.wrap) + return result.returncode + else: + # Interactive mode: setup, print status, wait for Ctrl+C. + with env: + proto = "HTTPS" if ssl_ctx else "HTTP" if http_server else None + print(f"\n{'='*60}") + print(f"Network test environment is running.") + print(f" TAP interface: {env.tap_interface}") + print(f" dnsmasq PID: {env.dnsmasq_proc.pid}") + if env.server is not None: + print(f" {proto} server: {env.http_host}:{env.http_port}") + print(f" DNS records: {env.dns_records}") + print(f"{'='*60}") + print(f"Press Ctrl+C to stop.\n") + try: + while True: + time.sleep(1.0) + except KeyboardInterrupt: + print("\nInterrupted.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/test_dns.py b/tools/test_dns.py index 1ae6a22..5817a9b 100644 --- a/tools/test_dns.py +++ b/tools/test_dns.py @@ -13,15 +13,16 @@ """ import os -import shutil import subprocess import sys -import time 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") +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from net_test_env import NetworkTestEnv, skip_if_no_network + # ip65_dns_ip_addr: 4 bytes storing the resolved IP address IP65_DNS_IP_ADDR = 0x4073 @@ -31,64 +32,6 @@ CARRY_RESULT_ADDR = 0xC0F0 -# --------------------------------------------------------------------------- -# Skip checks -# --------------------------------------------------------------------------- - -def check_prerequisites(): - """Return True if all prerequisites are met, else print skip and return False.""" - if not os.path.exists("/sys/class/net/tap-c64"): - print("SKIP: tap-c64 interface not found") - return False - if shutil.which("x64sc") is None: - print("SKIP: x64sc not on PATH") - return False - if shutil.which("dnsmasq") is None: - print("SKIP: dnsmasq not on PATH") - return False - if shutil.which("sudo") is None: - print("SKIP: sudo not on PATH (needed for dnsmasq)") - return False - return True - - -# --------------------------------------------------------------------------- -# dnsmasq helper -# --------------------------------------------------------------------------- - -def start_dnsmasq(): - """Start dnsmasq providing DHCP and DNS on tap-c64. Returns Popen. - - dnsmasq needs root for port 53, so we launch it via sudo. - """ - cmd = [ - "sudo", "dnsmasq", - "--no-daemon", - "--interface=tap-c64", - "--bind-interfaces", - "--listen-address=10.0.65.1", - "--dhcp-range=10.0.65.2,10.0.65.10,255.255.255.0,5m", - "--address=/c64test.local/10.0.65.1", - "--address=/second.local/10.0.65.1", - "--dhcp-option=6,10.0.65.1", - "--log-queries", - "--no-resolv", - ] - print(f" dnsmasq cmd: {' '.join(cmd)}") - proc = subprocess.Popen( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - # Give it a moment to bind - time.sleep(0.5) - if proc.poll() is not None: - _, stderr = proc.communicate() - raise RuntimeError(f"dnsmasq failed to start: {stderr.decode()}") - print(f" dnsmasq PID={proc.pid}") - return proc - - # --------------------------------------------------------------------------- # DNS resolve helper # --------------------------------------------------------------------------- @@ -164,11 +107,10 @@ def do_dns_resolve(transport, write_bytes, read_bytes, jsr_fn, def main(): os.chdir(PROJECT_ROOT) - if not check_prerequisites(): + if skip_if_no_network(): sys.exit(0) # Late imports -- only needed if prerequisites are met - sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from c64_test_harness import ( Labels, ViceConfig, ViceInstanceManager, read_bytes, write_bytes, jsr, wait_for_text, @@ -176,161 +118,140 @@ def main(): passed = 0 failed = 0 - dnsmasq_proc = None mgr = None inst = None - try: - # ---- 1. Build -------------------------------------------------------- - print("\n=== Building ===") - 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) - print(" Build OK") - - labels = Labels.from_file(LABELS_PATH) - print(f" Labels loaded, {len(labels)} symbols") - - # ---- Test: test_dns_labels ------------------------------------------- - print("\n=== test_dns_labels ===") - dns_resolve_addr = labels.address("net_dns_resolve") - if dns_resolve_addr is not None: - print(f" PASS: net_dns_resolve found @ ${dns_resolve_addr:04X}") - passed += 1 - else: - print(" FAIL: net_dns_resolve label not found") - failed += 1 - raise RuntimeError("Required label net_dns_resolve not found") - - # ---- 2. Start dnsmasq ------------------------------------------------ - print("\n=== Starting dnsmasq ===") - dnsmasq_proc = start_dnsmasq() - - # ---- 3. Launch VICE -------------------------------------------------- - print("\n=== Starting VICE ===") - config = ViceConfig( - prg_path=PRG_PATH, - warp=False, # warp causes timing issues with ethernet - ntsc=True, - sound=False, - ethernet=True, - ethernet_mode="rrnet", - ethernet_driver="tuntap", - ethernet_interface="tap-c64", - ) - - mgr = ViceInstanceManager(config=config) - inst = mgr.acquire() - transport = inst.transport - print(f" VICE PID={inst.pid}, port={inst.port}") - - # ---- 4. Wait for boot menu ------------------------------------------ - print("\n=== Waiting for boot menu ===") - grid = wait_for_text(transport, "Q=QUIT", timeout=60.0, verbose=False) - if grid is None: - print(" FATAL: Program menu did not appear") - failed += 1 - raise RuntimeError("Boot menu timeout") - print(" Boot menu appeared") - - # ---- 5. Network init (DHCP) ----------------------------------------- - print("\n=== Network init (pressing I for init) ===") - transport.resume() # CPU paused after wait_for_text screen read - transport.inject_keys([0x49]) # 'I' - - grid = wait_for_text(transport, "DHCP OK", timeout=60.0, verbose=False) - if grid is None: - print(" FAIL: DHCP did not complete within 60 seconds") - if dnsmasq_proc: - dnsmasq_proc.terminate() - _, stderr = dnsmasq_proc.communicate(timeout=5) - print(f" dnsmasq stderr:\n{stderr.decode()}") - dnsmasq_proc = None - failed += 1 - raise RuntimeError("DHCP timeout") - print(" DHCP OK") - - # ---- Test: test_dns_resolve_known_host ------------------------------- - print("\n=== test_dns_resolve_known_host ===") - carry, ip = do_dns_resolve( - transport, write_bytes, read_bytes, jsr, - "c64test.local", dns_resolve_addr, - ) - expected_ip = [10, 0, 65, 1] - if carry == 0 and list(ip) == expected_ip: - print(f" PASS: resolved c64test.local -> {'.'.join(str(b) for b in ip)}" - f", carry=0") - passed += 1 - else: - print(f" FAIL: c64test.local -> {list(ip)}, carry={carry}" - f" (expected {expected_ip}, carry=0)") - failed += 1 - - # ---- Test: test_dns_resolve_second_host ------------------------------ - print("\n=== test_dns_resolve_second_host ===") - carry, ip = do_dns_resolve( - transport, write_bytes, read_bytes, jsr, - "second.local", dns_resolve_addr, - ) - if carry == 0 and list(ip) == expected_ip: - print(f" PASS: resolved second.local -> {'.'.join(str(b) for b in ip)}" - f", carry=0") - passed += 1 - else: - print(f" FAIL: second.local -> {list(ip)}, carry={carry}" - f" (expected {expected_ip}, carry=0)") - failed += 1 - - # ---- Test: test_dns_resolve_unknown_host ----------------------------- - print("\n=== test_dns_resolve_unknown_host ===") - carry, ip = do_dns_resolve( - transport, write_bytes, read_bytes, jsr, - "nonexistent.invalid", dns_resolve_addr, - ) - if carry == 1: - print(f" PASS: nonexistent.invalid -> carry=1 (failure, as expected)") - passed += 1 - else: - print(f" FAIL: nonexistent.invalid -> carry={carry}, ip={list(ip)}" - f" (expected carry=1)") + with NetworkTestEnv( + dns_records={"c64test.local": "10.0.65.1", "second.local": "10.0.65.1"}, + setup_tap=False, + ) as env: + try: + # ---- 1. Build -------------------------------------------------------- + print("\n=== Building ===") + 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) + print(" Build OK") + + labels = Labels.from_file(LABELS_PATH) + print(f" Labels loaded, {len(labels)} symbols") + + # ---- Test: test_dns_labels ------------------------------------------- + print("\n=== test_dns_labels ===") + dns_resolve_addr = labels.address("net_dns_resolve") + if dns_resolve_addr is not None: + print(f" PASS: net_dns_resolve found @ ${dns_resolve_addr:04X}") + passed += 1 + else: + print(" FAIL: net_dns_resolve label not found") + failed += 1 + raise RuntimeError("Required label net_dns_resolve not found") + + # ---- 2. Launch VICE -------------------------------------------------- + print("\n=== Starting VICE ===") + config = ViceConfig( + prg_path=PRG_PATH, + warp=False, # warp causes timing issues with ethernet + ntsc=True, + sound=False, + ethernet=True, + ethernet_mode="rrnet", + ethernet_driver="tuntap", + ethernet_interface="tap-c64", + ) + + mgr = ViceInstanceManager(config=config) + inst = mgr.acquire() + transport = inst.transport + print(f" VICE PID={inst.pid}, port={inst.port}") + + # ---- 3. Wait for boot menu ------------------------------------------ + print("\n=== Waiting for boot menu ===") + grid = wait_for_text(transport, "Q=QUIT", timeout=60.0, verbose=False) + if grid is None: + print(" FATAL: Program menu did not appear") + failed += 1 + raise RuntimeError("Boot menu timeout") + print(" Boot menu appeared") + + # ---- 4. Network init (DHCP) ----------------------------------------- + print("\n=== Network init (pressing I for init) ===") + transport.resume() # CPU paused after wait_for_text screen read + transport.inject_keys([0x49]) # 'I' + + grid = wait_for_text(transport, "DHCP OK", timeout=60.0, verbose=False) + if grid is None: + print(" FAIL: DHCP did not complete within 60 seconds") + failed += 1 + raise RuntimeError("DHCP timeout") + print(" DHCP OK") + + # ---- Test: test_dns_resolve_known_host ------------------------------- + print("\n=== test_dns_resolve_known_host ===") + carry, ip = do_dns_resolve( + transport, write_bytes, read_bytes, jsr, + "c64test.local", dns_resolve_addr, + ) + expected_ip = [10, 0, 65, 1] + if carry == 0 and list(ip) == expected_ip: + print(f" PASS: resolved c64test.local -> {'.'.join(str(b) for b in ip)}" + f", carry=0") + passed += 1 + else: + print(f" FAIL: c64test.local -> {list(ip)}, carry={carry}" + f" (expected {expected_ip}, carry=0)") + failed += 1 + + # ---- Test: test_dns_resolve_second_host ------------------------------ + print("\n=== test_dns_resolve_second_host ===") + carry, ip = do_dns_resolve( + transport, write_bytes, read_bytes, jsr, + "second.local", dns_resolve_addr, + ) + if carry == 0 and list(ip) == expected_ip: + print(f" PASS: resolved second.local -> {'.'.join(str(b) for b in ip)}" + f", carry=0") + passed += 1 + else: + print(f" FAIL: second.local -> {list(ip)}, carry={carry}" + f" (expected {expected_ip}, carry=0)") + failed += 1 + + # ---- Test: test_dns_resolve_unknown_host ----------------------------- + print("\n=== test_dns_resolve_unknown_host ===") + carry, ip = do_dns_resolve( + transport, write_bytes, read_bytes, jsr, + "nonexistent.invalid", dns_resolve_addr, + ) + if carry == 1: + print(f" PASS: nonexistent.invalid -> carry=1 (failure, as expected)") + passed += 1 + else: + print(f" FAIL: nonexistent.invalid -> carry={carry}, ip={list(ip)}" + f" (expected carry=1)") + failed += 1 + + except RuntimeError as e: + print(f"\n Test aborted: {e}") + except Exception as e: + print(f"\n Unexpected error: {e}") + import traceback + traceback.print_exc() failed += 1 + finally: + # ---- Teardown (VICE only -- dnsmasq handled by NetworkTestEnv) ------- + print("\n=== Teardown ===") - except RuntimeError as e: - print(f"\n Test aborted: {e}") - except Exception as e: - print(f"\n Unexpected error: {e}") - import traceback - traceback.print_exc() - failed += 1 - finally: - # ---- Teardown -------------------------------------------------------- - print("\n=== Teardown ===") - - if mgr is not None: - try: - if inst is not None: - mgr.release(inst) - mgr.shutdown() - print(" VICE released") - except Exception as e: - print(f" VICE cleanup error: {e}") - - if dnsmasq_proc is not None: - try: - dnsmasq_proc.terminate() + if mgr is not None: try: - _, stderr = dnsmasq_proc.communicate(timeout=5) - print(f" dnsmasq stopped (exit={dnsmasq_proc.returncode})") - if failed > 0: - print(f" dnsmasq stderr:\n{stderr.decode()}") - except subprocess.TimeoutExpired: - dnsmasq_proc.kill() - dnsmasq_proc.wait() - print(" dnsmasq killed (did not terminate cleanly)") - except Exception as e: - print(f" dnsmasq cleanup error: {e}") + if inst is not None: + mgr.release(inst) + mgr.shutdown() + print(" VICE released") + except Exception as e: + print(f" VICE cleanup error: {e}") # ---- Summary ------------------------------------------------------------- total = passed + failed diff --git a/tools/test_net_test_env.py b/tools/test_net_test_env.py new file mode 100644 index 0000000..13546dd --- /dev/null +++ b/tools/test_net_test_env.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +"""Unit tests for net_test_env.py. + +All external dependencies (TAP interfaces, dnsmasq, VICE, subprocess) are mocked. +No sudo, no network, no VICE required. +""" + +import subprocess +import unittest +from unittest.mock import MagicMock, patch, call + + +class TestSkipIfNoNetwork(unittest.TestCase): + """Tests for the skip_if_no_network() helper.""" + + @patch("net_test_env.shutil.which", return_value="/usr/bin/thing") + @patch("net_test_env.os.path.exists", return_value=False) + def test_skip_if_no_network_missing_tap(self, mock_exists, mock_which): + from net_test_env import skip_if_no_network + self.assertTrue(skip_if_no_network()) + mock_exists.assert_called_once_with("/sys/class/net/tap-c64") + + @patch("net_test_env.shutil.which", return_value="/usr/bin/thing") + @patch("net_test_env.os.path.exists", return_value=True) + def test_skip_if_no_network_all_present(self, mock_exists, mock_which): + from net_test_env import skip_if_no_network + self.assertFalse(skip_if_no_network()) + + @patch("net_test_env.shutil.which") + @patch("net_test_env.os.path.exists", return_value=True) + def test_skip_if_no_network_missing_dnsmasq(self, mock_exists, mock_which): + from net_test_env import skip_if_no_network + + def which_side_effect(name): + if name == "dnsmasq": + return None + return "/usr/bin/" + name + + mock_which.side_effect = which_side_effect + self.assertTrue(skip_if_no_network()) + + +class TestCheckPrerequisites(unittest.TestCase): + """Tests for NetworkTestEnv.check_prerequisites().""" + + @patch("net_test_env.shutil.which", return_value="/usr/bin/thing") + @patch("net_test_env.os.path.exists", return_value=False) + @patch("net_test_env.os.path.isfile", return_value=True) + def test_check_prerequisites_missing_tap(self, mock_isfile, mock_exists, mock_which): + from net_test_env import NetworkTestEnv + env = NetworkTestEnv(setup_tap=False) + missing = env.check_prerequisites() + self.assertTrue(any("interface not found" in m for m in missing)) + + @patch("net_test_env.shutil.which") + @patch("net_test_env.os.path.exists", return_value=True) + @patch("net_test_env.os.path.isfile", return_value=True) + def test_check_prerequisites_missing_dnsmasq(self, mock_isfile, mock_exists, mock_which): + from net_test_env import NetworkTestEnv + + def which_side_effect(name): + if name == "dnsmasq": + return None + return "/usr/bin/" + name + + mock_which.side_effect = which_side_effect + env = NetworkTestEnv(setup_tap=False) + missing = env.check_prerequisites() + self.assertTrue(any("dnsmasq" in m for m in missing)) + + +class TestStartDnsmasq(unittest.TestCase): + """Tests for start_dnsmasq() command construction.""" + + @patch("net_test_env.time.sleep") + @patch("net_test_env.subprocess.Popen") + def test_start_dnsmasq_command_construction(self, mock_popen_cls, mock_sleep): + from net_test_env import start_dnsmasq + + mock_proc = MagicMock() + mock_proc.poll.return_value = None + mock_proc.pid = 12345 + mock_popen_cls.return_value = mock_proc + + dns = {"example.local": "10.0.65.1", "other.local": "10.0.65.2"} + start_dnsmasq(dns_records=dns, verbose=False) + + cmd = mock_popen_cls.call_args[0][0] + self.assertIn("--address=/example.local/10.0.65.1", cmd) + self.assertIn("--address=/other.local/10.0.65.2", cmd) + self.assertIn("--interface=tap-c64", cmd) + self.assertIn("sudo", cmd) + + @patch("net_test_env.time.sleep") + @patch("net_test_env.subprocess.Popen") + def test_start_dnsmasq_extra_args(self, mock_popen_cls, mock_sleep): + from net_test_env import start_dnsmasq + + mock_proc = MagicMock() + mock_proc.poll.return_value = None + mock_proc.pid = 12345 + mock_popen_cls.return_value = mock_proc + + start_dnsmasq(extra_args=["--port=5353", "--bogus-priv"], verbose=False) + + cmd = mock_popen_cls.call_args[0][0] + self.assertIn("--port=5353", cmd) + self.assertIn("--bogus-priv", cmd) + + +class TestStopDnsmasq(unittest.TestCase): + """Tests for stop_dnsmasq().""" + + def test_stop_dnsmasq_already_exited(self): + from net_test_env import stop_dnsmasq + + mock_proc = MagicMock() + mock_proc.poll.return_value = 0 + stop_dnsmasq(mock_proc) + mock_proc.terminate.assert_not_called() + + def test_stop_dnsmasq_graceful(self): + from net_test_env import stop_dnsmasq + + mock_proc = MagicMock() + mock_proc.poll.return_value = None + mock_proc.communicate.return_value = (b"", b"") + stop_dnsmasq(mock_proc) + mock_proc.terminate.assert_called_once() + mock_proc.kill.assert_not_called() + + def test_stop_dnsmasq_timeout_kills(self): + from net_test_env import stop_dnsmasq + + mock_proc = MagicMock() + mock_proc.poll.return_value = None + mock_proc.communicate.side_effect = subprocess.TimeoutExpired(cmd="dnsmasq", timeout=5) + stop_dnsmasq(mock_proc, timeout=5) + mock_proc.terminate.assert_called_once() + mock_proc.kill.assert_called_once() + + +class TestContextManager(unittest.TestCase): + """Tests for NetworkTestEnv as a context manager.""" + + @patch("net_test_env.TestHTTPServer") + @patch("net_test_env._kill_stale_dnsmasq") + @patch("net_test_env.start_dnsmasq") + @patch("net_test_env.stop_dnsmasq") + @patch("net_test_env.os.path.exists", return_value=True) + def test_context_manager_teardown_on_exception( + self, mock_exists, mock_stop, mock_start, mock_kill, mock_http_cls + ): + from net_test_env import NetworkTestEnv + + mock_proc = MagicMock() + mock_proc.returncode = 0 + mock_start.return_value = mock_proc + + try: + with NetworkTestEnv(setup_tap=False, verbose=False) as env: + raise ValueError("boom") + except ValueError: + pass + + mock_stop.assert_called_once_with(mock_proc) + + @patch("net_test_env.TestHTTPServer") + @patch("net_test_env._kill_stale_dnsmasq") + @patch("net_test_env.start_dnsmasq") + @patch("net_test_env.stop_dnsmasq") + @patch("net_test_env.os.path.exists", return_value=True) + def test_teardown_idempotent( + self, mock_exists, mock_stop, mock_start, mock_kill, mock_http_cls + ): + from net_test_env import NetworkTestEnv + + mock_proc = MagicMock() + mock_proc.returncode = 0 + mock_start.return_value = mock_proc + + env = NetworkTestEnv(setup_tap=False, verbose=False) + env.setup() + env.teardown() + env.teardown() # second call should be a no-op + + mock_stop.assert_called_once_with(mock_proc) + + +class TestDnsRecordsDefault(unittest.TestCase): + """Test default DNS records.""" + + def test_dns_records_default(self): + from net_test_env import NetworkTestEnv + env = NetworkTestEnv() + self.assertEqual(env.dns_records, {"c64test.local": "10.0.65.1"}) + + +class TestHTTPServerStarted(unittest.TestCase): + """Test that HTTP server is started when http_server=True.""" + + @patch("net_test_env.TestHTTPServer") + @patch("net_test_env._kill_stale_dnsmasq") + @patch("net_test_env.start_dnsmasq") + @patch("net_test_env.os.path.exists", return_value=True) + def test_http_server_started_when_enabled( + self, mock_exists, mock_start, mock_kill, mock_http_cls + ): + from net_test_env import NetworkTestEnv + + mock_proc = MagicMock() + mock_proc.poll.return_value = None + mock_proc.pid = 99 + mock_start.return_value = mock_proc + + mock_server = MagicMock() + mock_http_cls.return_value = mock_server + + env = NetworkTestEnv(setup_tap=False, http_server=True, verbose=False) + env.setup() + + mock_http_cls.assert_called_once_with( + host="10.0.65.1", port=80, ssl_context=None + ) + mock_server.start.assert_called_once() + + env.teardown() + + +if __name__ == "__main__": + unittest.main() From a7b676af130a79bf46a770da525b737b1afe0e86 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Sun, 12 Apr 2026 13:10:51 -0500 Subject: [PATCH 06/22] Add end-to-end bridge tests for DHCP and HTTP GET Vendor the c64-test-harness bridge networking scripts (setup + cleanup) and extend them with dnsmasq (DHCP + DNS overrides). Build a reusable tools/https_e2e/ library with BridgeEnv context manager, single-VICE launcher, boot menu helpers, and HTTP listener. Two passing e2e tests drive the real c64-https binary in VICE over RR-Net at normal speed: Phase 1 verifies DHCP, Phase 2 verifies plain HTTP GET to a local listener. HTTPS (Phase 3) to follow. Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 2 + README.md | 29 ++++ scripts/cleanup-bridge-tap.sh | 161 ++++++++++++++++++ scripts/setup-bridge-tap.sh | 169 +++++++++++++++++++ tests/test_phase1_dhcp.py | 135 +++++++++++++++ tests/test_phase2_http.py | 270 ++++++++++++++++++++++++++++++ tools/https_e2e/__init__.py | 30 ++++ tools/https_e2e/c64_menu.py | 80 +++++++++ tools/https_e2e/env.py | 183 ++++++++++++++++++++ tools/https_e2e/http_listener.py | 78 +++++++++ tools/https_e2e/vice_on_bridge.py | 162 ++++++++++++++++++ 11 files changed, 1299 insertions(+) create mode 100755 scripts/cleanup-bridge-tap.sh create mode 100755 scripts/setup-bridge-tap.sh create mode 100644 tests/test_phase1_dhcp.py create mode 100644 tests/test_phase2_http.py create mode 100644 tools/https_e2e/__init__.py create mode 100644 tools/https_e2e/c64_menu.py create mode 100644 tools/https_e2e/env.py create mode 100644 tools/https_e2e/http_listener.py create mode 100644 tools/https_e2e/vice_on_bridge.py diff --git a/.gitignore b/.gitignore index dc26f26..28b2cfe 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ __pycache__/ ip65-build/*.o ip65-build/*.bin ip65-build/*.map +.claude/ +.serena/ diff --git a/README.md b/README.md index 4175125..4eb9506 100644 --- a/README.md +++ b/README.md @@ -155,8 +155,37 @@ python3 tools/bench_x25519.py # X25519 key generation (~3.6 min C64 time # Integration tests (require tap-c64 interface, dnsmasq; see scripts/setup-tap-networking.sh in c64-test-harness) python3 tools/test_dns.py # 4 tests: DNS resolution via ip65 over TAP (known host, second host, unknown host) python3 tools/test_http_integration.py # 5 tests: end-to-end plain HTTP GET over TAP (DNS + TCP + request/response) + +# End-to-end bridge tests (require br-c64 bridge, RR-Net; see below) +sudo PYTHONPATH=tools python3 tests/test_phase1_dhcp.py # DHCP over RR-Net bridge +sudo PYTHONPATH=tools python3 tests/test_phase2_http.py # Plain HTTP GET over bridge ``` +### End-to-End Bridge Tests + +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)). VICE runs at **normal speed** (warp breaks RR-Net DHCP), so these tests need generous timeouts (~90-120s per phase). + +**Setup:** + +```bash +# Create the bridge, TAP interfaces, and start dnsmasq (DHCP + DNS) +sudo ./scripts/setup-bridge-tap.sh + +# Tear down (also handles stale VICE processes, legacy tap-c64, vicerc files) +sudo ./scripts/cleanup-bridge-tap.sh +``` + +The setup script creates `br-c64` with `tap-c64-0`/`tap-c64-1`, assigns `10.0.65.1/24` to the bridge, and starts dnsmasq providing DHCP (pool 10.0.65.50-150) with DNS overrides (`zimmers.net` and `apple.com` → `10.0.65.1`). The `BridgeEnv` context manager in `tools/https_e2e/env.py` wraps both scripts for use in tests. + +**Library:** `tools/https_e2e/` exposes a reusable public API: + +| Module | Public API | +|--------|-----------| +| `env.py` | `BridgeEnv` (context manager), `check_prerequisites()` | +| `vice_on_bridge.py` | `launch_vice_on_bridge()` → `ViceHandle`, `shutdown_vice()` | +| `c64_menu.py` | `press_key()`, `wait_for_screen_text()`, `get_screen_text()` | +| `http_listener.py` | `start_http_listener()` → `HttpListenerHandle`, `stop_http_listener()` | + ## Related Projects - [c64-aes256-ecdsa](../c64-aes256-ecdsa) — AES-256, SHA-256, ECDSA P-256, HMAC-DRBG diff --git a/scripts/cleanup-bridge-tap.sh b/scripts/cleanup-bridge-tap.sh new file mode 100755 index 0000000..fbf561f --- /dev/null +++ b/scripts/cleanup-bridge-tap.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +# cleanup-bridge-tap.sh -- Tear down the bridge+dnsmasq env set up by +# setup-bridge-tap.sh. Idempotent -- safe to run if already torn down. +# +# Kills any leftover VICE processes, tears down the br-c64 bridge and +# its tap-c64-0/tap-c64-1 interfaces, removes the iptables FORWARD rules, +# and cleans up stale /tmp/vice_eth_*.rc files. +# +# Usage: +# sudo ./scripts/cleanup-bridge-tap.sh + +set -u # don't set -e: we want to keep going through all cleanup steps + +BRIDGE="br-c64" +TAP0="tap-c64-0" +TAP1="tap-c64-1" +TAP_LEGACY="tap-c64" +DNSMASQ_PIDFILE="/tmp/c64-https-dnsmasq.pid" + +echo "=== c64-https bridge networking cleanup ===" +echo + +# --- 1. Kill any leftover x64sc processes ------------------------------------ +echo "[1/6] Killing any leftover x64sc processes..." +if pgrep -x x64sc > /dev/null 2>&1; then + pgrep -a x64sc | while read -r pid cmd; do + echo " killing PID $pid: $cmd" + done + pkill -TERM x64sc 2>/dev/null || true + sleep 1 + if pgrep -x x64sc > /dev/null 2>&1; then + pkill -KILL x64sc 2>/dev/null || true + sleep 1 + fi + if pgrep -x x64sc > /dev/null 2>&1; then + echo " WARNING: x64sc still running after SIGKILL" + else + echo " all x64sc processes killed" + fi +else + echo " no x64sc processes running" +fi +echo + +# --- 2. Kill dnsmasq (pidfile + /proc scan) ---------------------------------- +echo "[2/6] Killing dnsmasq processes..." +found_dns=0 + +# 2a. Primary path: pidfile +if [[ -f "$DNSMASQ_PIDFILE" ]]; then + PID="$(cat "$DNSMASQ_PIDFILE" 2>/dev/null || true)" + if [[ -n "$PID" ]] && kill -0 "$PID" 2>/dev/null; then + if grep -q dnsmasq "/proc/$PID/comm" 2>/dev/null; then + kill "$PID" 2>/dev/null || true + for _ in 1 2 3 4 5; do + kill -0 "$PID" 2>/dev/null || break + sleep 0.2 + done + kill -9 "$PID" 2>/dev/null || true + echo " [killed] dnsmasq pid=$PID (via pidfile)" + found_dns=1 + else + echo " [ok] pidfile pid $PID is not dnsmasq, skipping" + fi + else + echo " [ok] dnsmasq pidfile pid $PID already gone" + fi + rm -f "$DNSMASQ_PIDFILE" +fi + +# 2b. Fallback: scan /proc cmdlines for dnsmasq bound to our TAPs/bridge +if command -v pgrep > /dev/null; then + while read -r pid; do + if [[ -n "$pid" ]]; then + cmdline=$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null || echo "") + if echo "$cmdline" | grep -qE "(tap-c64-|br-c64|tap-c64)"; then + echo " [killed] dnsmasq pid=$pid (via /proc scan): $cmdline" + kill -TERM "$pid" 2>/dev/null || true + found_dns=1 + fi + fi + done < <(pgrep -x dnsmasq 2>/dev/null) +fi + +if [[ "$found_dns" == "0" ]]; then + echo " no dnsmasq processes found" +fi +echo + +# --- 3. Remove iptables FORWARD rules ---------------------------------------- +echo "[3/6] Removing iptables FORWARD rules..." +removed=0 +for DEV in "$BRIDGE" "$TAP0" "$TAP1" "$TAP_LEGACY"; do + if iptables -D FORWARD -i "$DEV" -j ACCEPT 2>/dev/null; then + echo " [removed] FORWARD -i $DEV" + removed=$((removed + 1)) + fi + if iptables -D FORWARD -o "$DEV" -j ACCEPT 2>/dev/null; then + echo " [removed] FORWARD -o $DEV" + removed=$((removed + 1)) + fi +done +if [[ "$removed" == "0" ]]; then + echo " no FORWARD rules to remove" +fi +echo + +# --- 4. Tear down TAP interfaces and bridge ----------------------------------- +echo "[4/6] Tearing down TAP interfaces and bridge..." +for TAP_DEV in "$TAP0" "$TAP1" "$TAP_LEGACY"; do + if ip link show "$TAP_DEV" > /dev/null 2>&1; then + ip link set "$TAP_DEV" down 2>/dev/null || true + ip tuntap del dev "$TAP_DEV" mode tap 2>/dev/null + if ip link show "$TAP_DEV" > /dev/null 2>&1; then + echo " WARNING: $TAP_DEV still exists" + else + echo " [removed] $TAP_DEV" + fi + else + echo " [ok] $TAP_DEV already absent" + fi +done + +if ip link show "$BRIDGE" > /dev/null 2>&1; then + ip link set "$BRIDGE" down 2>/dev/null || true + ip link del "$BRIDGE" type bridge 2>/dev/null + if ip link show "$BRIDGE" > /dev/null 2>&1; then + echo " WARNING: $BRIDGE still exists" + else + echo " [removed] $BRIDGE" + fi +else + echo " [ok] $BRIDGE already absent" +fi +echo + +# --- 5. Remove stale temp vicerc files ---------------------------------------- +echo "[5/6] Removing stale /tmp/vice_eth_*.rc files..." +shopt -s nullglob +rc_files=(/tmp/vice_eth_*.rc) +if [[ ${#rc_files[@]} -gt 0 ]]; then + for f in "${rc_files[@]}"; do + rm -f "$f" && echo " [removed] $f" + done +else + echo " no stale vicerc files" +fi +shopt -u nullglob +echo + +# --- 6. Remove stale dnsmasq pidfile (if not already cleaned) ----------------- +echo "[6/6] Final pidfile cleanup..." +if [[ -f "$DNSMASQ_PIDFILE" ]]; then + rm -f "$DNSMASQ_PIDFILE" + echo " [removed] $DNSMASQ_PIDFILE" +else + echo " [ok] no stale pidfile" +fi +echo + +echo "=== Cleanup complete ===" diff --git a/scripts/setup-bridge-tap.sh b/scripts/setup-bridge-tap.sh new file mode 100755 index 0000000..c9a52a9 --- /dev/null +++ b/scripts/setup-bridge-tap.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env bash +# setup-bridge-tap.sh -- Bridge + TAP + dnsmasq for c64-https end-to-end tests. +# +# Vendored and extended from c64-test-harness/scripts/setup-bridge-tap.sh. +# Creates br-c64 with tap-c64-0 and tap-c64-1, host IP 10.0.65.1/24, iptables +# FORWARD rules, and then starts a dnsmasq bound to br-c64 that: +# - serves DHCP leases on 10.0.65.50-10.0.65.150 (1h) +# - pushes default gw + DNS = 10.0.65.1 +# - overrides zimmers.net and apple.com to 10.0.65.1 +# +# Idempotent -- safe to run twice. Run via sudo. Pair with cleanup-bridge-tap.sh. +# +# Usage: +# sudo ./scripts/setup-bridge-tap.sh + +set -euo pipefail + +BRIDGE="br-c64" +BRIDGE_ADDR="10.0.65.1/24" +BRIDGE_IP="${BRIDGE_ADDR%/*}" +TAP0="tap-c64-0" +TAP1="tap-c64-1" +TAP_USER="${SUDO_USER:-$USER}" + +DNSMASQ_PIDFILE="/tmp/c64-https-dnsmasq.pid" +DNSMASQ_LOGFILE="/tmp/c64-https-dnsmasq.log" +DHCP_RANGE_START="10.0.65.50" +DHCP_RANGE_END="10.0.65.150" +DHCP_LEASE="1h" + +echo "Bridge: $BRIDGE ($BRIDGE_ADDR)" +echo "TAP devices: $TAP0, $TAP1 (owner: $TAP_USER)" +echo "dnsmasq: pid=$DNSMASQ_PIDFILE log=$DNSMASQ_LOGFILE" +echo + +# --- Bridge ------------------------------------------------------------------ + +if ip link show "$BRIDGE" &>/dev/null; then + echo "[ok] $BRIDGE already exists" +else + ip link add name "$BRIDGE" type bridge + echo "[created] $BRIDGE" +fi + +if [[ -f "/sys/devices/virtual/net/$BRIDGE/bridge/stp_state" ]]; then + if [[ "$(cat /sys/devices/virtual/net/$BRIDGE/bridge/stp_state)" != "0" ]]; then + ip link set "$BRIDGE" type bridge stp_state 0 + echo "[disabled] STP on $BRIDGE" + fi +fi + +if ip addr show "$BRIDGE" | grep -q "$BRIDGE_IP"; then + echo "[ok] $BRIDGE has $BRIDGE_ADDR" +else + ip addr add "$BRIDGE_ADDR" dev "$BRIDGE" + echo "[addr] $BRIDGE_ADDR assigned" +fi + +if ip link show "$BRIDGE" | grep -q 'state UP'; then + echo "[ok] $BRIDGE is UP" +else + ip link set "$BRIDGE" up + echo "[up] $BRIDGE" +fi + +# --- TAP interfaces ---------------------------------------------------------- + +for TAP_DEV in "$TAP0" "$TAP1"; do + if ip link show "$TAP_DEV" &>/dev/null; then + echo "[ok] $TAP_DEV already exists" + else + ip tuntap add dev "$TAP_DEV" mode tap user "$TAP_USER" + echo "[created] $TAP_DEV" + fi + + if ip link show "$TAP_DEV" 2>/dev/null | grep -q "master $BRIDGE"; then + echo "[ok] $TAP_DEV already bridged" + else + ip link set "$TAP_DEV" master "$BRIDGE" + echo "[bridge] $TAP_DEV added to $BRIDGE" + fi + + if ip link show "$TAP_DEV" | grep -q 'state UP'; then + echo "[ok] $TAP_DEV is UP" + else + ip link set "$TAP_DEV" up + echo "[up] $TAP_DEV" + fi +done + +# --- iptables FORWARD rules -------------------------------------------------- + +for DEV in "$BRIDGE" "$TAP0" "$TAP1"; do + if ! iptables -C FORWARD -i "$DEV" -j ACCEPT 2>/dev/null; then + iptables -A FORWARD -i "$DEV" -j ACCEPT + echo "[added] FORWARD: $DEV inbound" + fi + if ! iptables -C FORWARD -o "$DEV" -j ACCEPT 2>/dev/null; then + iptables -A FORWARD -o "$DEV" -j ACCEPT + echo "[added] FORWARD: $DEV outbound" + fi +done + +# --- dnsmasq ----------------------------------------------------------------- +# Stop any stale dnsmasq we previously started. + +if [[ -f "$DNSMASQ_PIDFILE" ]]; then + OLD_PID="$(cat "$DNSMASQ_PIDFILE" 2>/dev/null || true)" + if [[ -n "$OLD_PID" ]] && kill -0 "$OLD_PID" 2>/dev/null; then + # Only kill if it's actually a dnsmasq process + if grep -q dnsmasq "/proc/$OLD_PID/comm" 2>/dev/null; then + kill "$OLD_PID" 2>/dev/null || true + sleep 0.3 + kill -9 "$OLD_PID" 2>/dev/null || true + echo "[killed] stale dnsmasq pid=$OLD_PID" + fi + fi + rm -f "$DNSMASQ_PIDFILE" +fi + +if ! command -v dnsmasq >/dev/null 2>&1; then + echo "ERROR: dnsmasq not installed" >&2 + exit 1 +fi + +# Start dnsmasq as a daemon with its own pidfile. --bind-interfaces + listen +# on the bridge ip so we don't clash with a system resolver on other ifaces. +: >"$DNSMASQ_LOGFILE" +dnsmasq \ + --keep-in-foreground \ + --pid-file="$DNSMASQ_PIDFILE" \ + --interface="$BRIDGE" \ + --bind-interfaces \ + --listen-address="$BRIDGE_IP" \ + --no-resolv \ + --no-hosts \ + --dhcp-range="$DHCP_RANGE_START,$DHCP_RANGE_END,255.255.255.0,$DHCP_LEASE" \ + --dhcp-option=3,"$BRIDGE_IP" \ + --dhcp-option=6,"$BRIDGE_IP" \ + --address=/zimmers.net/"$BRIDGE_IP" \ + --address=/apple.com/"$BRIDGE_IP" \ + --log-queries \ + --log-dhcp \ + >>"$DNSMASQ_LOGFILE" 2>&1 & +DNSMASQ_PID=$! +disown "$DNSMASQ_PID" 2>/dev/null || true + +# dnsmasq in --keep-in-foreground does NOT write the pidfile itself, so we +# write the child PID manually. +echo "$DNSMASQ_PID" >"$DNSMASQ_PIDFILE" + +# Wait briefly for it to bind. +for _ in 1 2 3 4 5 6 7 8 9 10; do + if ! kill -0 "$DNSMASQ_PID" 2>/dev/null; then + echo "ERROR: dnsmasq exited early. Log tail:" >&2 + tail -20 "$DNSMASQ_LOGFILE" >&2 || true + exit 1 + fi + if ss -lnup 2>/dev/null | grep -q "$BRIDGE_IP:53" \ + && ss -lnup 2>/dev/null | grep -q "$BRIDGE_IP:67"; then + break + fi + sleep 0.2 +done +echo "[dnsmasq] pid=$DNSMASQ_PID bound to $BRIDGE_IP (DHCP $DHCP_RANGE_START-$DHCP_RANGE_END)" + +echo +echo "Done. Bridge $BRIDGE ready, dnsmasq serving DHCP+DNS on $BRIDGE_IP." +echo "Tear down with: sudo ./scripts/cleanup-bridge-tap.sh" diff --git a/tests/test_phase1_dhcp.py b/tests/test_phase1_dhcp.py new file mode 100644 index 0000000..ad12dc3 --- /dev/null +++ b/tests/test_phase1_dhcp.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""Phase 1 e2e test: boot c64-https.prg in VICE, press I, see DHCP OK. + +This test runs the real c64-https binary in VICE on a Linux bridge with +RR-Net ethernet and a host-side dnsmasq. It exercises ip65's net_dhcp +end-to-end. It touches NO TLS/HTTP logic -- it only asserts that the +boot menu appears and that pressing 'I' produces the 'DHCP OK' banner. + +Run: + PYTHONPATH=tools python3 tests/test_phase1_dhcp.py + +Exit codes: + 0 -- PASS + 0 -- SKIP (clearly printed) + 1 -- FAIL +""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys + +_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +_TOOLS = os.path.join(_REPO_ROOT, "tools") +if _TOOLS not in sys.path: + sys.path.insert(0, _TOOLS) + +PRG_PATH = os.path.join(_REPO_ROOT, "build", "c64-https.prg") + +# Exact literal from src/boot.asm (menu_msg @ line 424-426). +MENU_NEEDLE = "Q=QUIT" +# dhcp_ok_msg @ boot.asm:448 is "DHCP OK - IP: ". Match the load-bearing prefix. +DHCP_OK_NEEDLE = "DHCP OK" + +MENU_TIMEOUT = 90.0 +DHCP_TIMEOUT = 90.0 + + +def _skip(reason: str) -> int: + print(f"SKIP: {reason}") + return 0 + + +def _ensure_built() -> bool: + if os.path.isfile(PRG_PATH): + return True + print("[build] c64-https.prg missing, running make...") + r = subprocess.run(["make"], cwd=_REPO_ROOT, capture_output=True, text=True) + if r.returncode != 0: + print(f" make failed (exit {r.returncode}):\n{r.stderr}") + return False + return os.path.isfile(PRG_PATH) + + +def main() -> int: + # ---- Prerequisite / skip gating ---------------------------------------- + from https_e2e import ( + BridgeEnv, + check_prerequisites, + launch_vice_on_bridge, + shutdown_vice, + press_key, + wait_for_screen_text, + get_screen_text, + ) + + missing = check_prerequisites() + if missing: + return _skip("missing prerequisites: " + "; ".join(missing)) + + if not _ensure_built(): + return _skip("c64-https.prg could not be built") + + # ---- Run the test ------------------------------------------------------ + handle = None + try: + with BridgeEnv() as env: + try: + print(f"\n=== Launching VICE on {env.tap0} with {PRG_PATH} ===") + handle = launch_vice_on_bridge( + prg_path=PRG_PATH, + tap=env.tap0, + ready_timeout=90.0, + ) + transport = handle.transport + + print(f"\n=== Waiting for boot menu ({MENU_NEEDLE!r}) ===") + try: + wait_for_screen_text( + transport, MENU_NEEDLE, timeout=MENU_TIMEOUT + ) + except TimeoutError as e: + print(f"FAIL: boot menu did not appear\n{e}") + return 1 + print(" boot menu OK") + + print("\n=== Pressing 'I' for DHCP init ===") + press_key(transport, "I") + + print(f"\n=== Waiting up to {DHCP_TIMEOUT:.0f}s for {DHCP_OK_NEEDLE!r} ===") + try: + final = wait_for_screen_text( + transport, DHCP_OK_NEEDLE, timeout=DHCP_TIMEOUT + ) + except TimeoutError as e: + print(f"FAIL: DHCP did not complete\n{e}") + dnsmasq_log = "/tmp/c64-https-dnsmasq.log" + if os.path.isfile(dnsmasq_log): + print(f"\n--- tail of {dnsmasq_log} ---") + with open(dnsmasq_log, "rb") as f: + data = f.read()[-4000:] + print(data.decode("utf-8", errors="replace")) + return 1 + + print("\n=== PASS: DHCP OK seen on screen ===") + snippet = "\n".join(final.splitlines()[:25]) + print(f"--- final screen (first 25 lines) ---\n{snippet}") + return 0 + finally: + # Shut VICE down BEFORE BridgeEnv tears down the TAPs. + if handle is not None: + try: + shutdown_vice(handle) + except Exception as e: # noqa: BLE001 + print(f" shutdown_vice: {e}") + handle = None + except Exception as e: + print(f"FAIL: unexpected error: {e}") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_phase2_http.py b/tests/test_phase2_http.py new file mode 100644 index 0000000..6375b0b --- /dev/null +++ b/tests/test_phase2_http.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 +"""Phase 2 e2e test: boot c64-https.prg, do DHCP, then plain HTTP GET. + +This test extends Phase 1 by pressing 'H' after DHCP succeeds, which +triggers a plain HTTP GET to zimmers.net (resolved via dnsmasq to the +host bridge IP 10.0.65.1). A Python HTTP server on 10.0.65.1:80 serves +a known response body. + +Run: + sudo PYTHONPATH=tools python3 tests/test_phase2_http.py + +Exit codes: + 0 -- PASS + 0 -- SKIP (clearly printed) + 1 -- FAIL +""" + +from __future__ import annotations + +import os +import subprocess +import sys +import time + +_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +_TOOLS = os.path.join(_REPO_ROOT, "tools") +if _TOOLS not in sys.path: + sys.path.insert(0, _TOOLS) + +PRG_PATH = os.path.join(_REPO_ROOT, "build", "c64-https.prg") + +# Screen needles (from src/boot.asm string labels). +MENU_NEEDLE = "Q=QUIT" +DHCP_OK_NEEDLE = "DHCP OK" +# Response body served by our test HTTP server. +RESPONSE_BODY = "HELLO FROM TEST SERVER" + +MENU_TIMEOUT = 90.0 +DHCP_TIMEOUT = 90.0 +HTTP_TIMEOUT = 120.0 + + +def _skip(reason: str) -> int: + print(f"SKIP: {reason}") + return 0 + + +def _ensure_built() -> bool: + if os.path.isfile(PRG_PATH): + return True + print("[build] c64-https.prg missing, running make...") + r = subprocess.run(["make"], cwd=_REPO_ROOT, capture_output=True, text=True) + if r.returncode != 0: + print(f" make failed (exit {r.returncode}):\n{r.stderr}") + return False + return os.path.isfile(PRG_PATH) + + +def _dump_diagnostics(transport=None) -> None: + """Print dnsmasq log and host-side connectivity checks for post-mortem.""" + dnsmasq_log = "/tmp/c64-https-dnsmasq.log" + if os.path.isfile(dnsmasq_log): + print(f"\n--- tail of {dnsmasq_log} ---") + with open(dnsmasq_log, "rb") as f: + data = f.read()[-4000:] + print(data.decode("utf-8", errors="replace")) + + # Host-side DNS check + try: + r = subprocess.run( + ["dig", "+short", "@10.0.65.1", "www.zimmers.net"], + capture_output=True, text=True, timeout=5, + ) + print(f"\n dig @10.0.65.1 www.zimmers.net -> {r.stdout.strip()}") + except Exception as e: + print(f" dig check failed: {e}") + + # Host-side HTTP check + try: + import urllib.request + resp = urllib.request.urlopen("http://10.0.65.1:80/", timeout=3) + print(f" HTTP from host: {resp.status} {resp.read()[:100]}") + except Exception as e: + print(f" HTTP from host failed: {e}") + + # ip65 error code from C64 memory + if transport is not None: + try: + transport.resume() + err_data = transport.read_memory(0x4CEA, 1) + print(f" ip65_error at $4CEA = 0x{err_data[0]:02X}") + except Exception as e: + print(f" ip65_error read failed: {e}") + + +def main() -> int: + from https_e2e import ( + BridgeEnv, + check_prerequisites, + launch_vice_on_bridge, + shutdown_vice, + press_key, + wait_for_screen_text, + get_screen_text, + start_http_listener, + stop_http_listener, + ) + + missing = check_prerequisites() + if missing: + return _skip("missing prerequisites: " + "; ".join(missing)) + + if not _ensure_built(): + return _skip("c64-https.prg could not be built") + + handle = None + listener = None + try: + with BridgeEnv() as env: + try: + # --- Start HTTP listener on bridge IP --- + print(f"\n=== Starting HTTP listener on {env.bridge_ip}:80 ===") + listener = start_http_listener( + host=env.bridge_ip, + port=80, + response_body=RESPONSE_BODY, + ) + print(f" listener ready on {listener.host}:{listener.port}") + + # --- Launch VICE --- + print(f"\n=== Launching VICE on {env.tap0} with {PRG_PATH} ===") + handle = launch_vice_on_bridge( + prg_path=PRG_PATH, + tap=env.tap0, + ready_timeout=90.0, + ) + transport = handle.transport + + # --- Wait for boot menu --- + print(f"\n=== Waiting for boot menu ({MENU_NEEDLE!r}) ===") + try: + wait_for_screen_text(transport, MENU_NEEDLE, timeout=MENU_TIMEOUT) + except TimeoutError as e: + print(f"FAIL: boot menu did not appear\n{e}") + return 1 + print(" boot menu OK") + + # --- DHCP init --- + print("\n=== Pressing 'I' for DHCP init ===") + press_key(transport, "I") + + print(f"\n=== Waiting up to {DHCP_TIMEOUT:.0f}s for {DHCP_OK_NEEDLE!r} ===") + try: + wait_for_screen_text(transport, DHCP_OK_NEEDLE, timeout=DHCP_TIMEOUT) + except TimeoutError as e: + print(f"FAIL: DHCP did not complete\n{e}") + _dump_diagnostics(transport) + return 1 + print(" DHCP OK") + + # --- HTTP GET --- + print("\n=== Pressing 'H' for plain HTTP GET ===") + press_key(transport, "H") + + print(f"\n=== Waiting up to {HTTP_TIMEOUT:.0f}s for HTTP OK ===") + # After pressing H, the C64 prints: + # "HTTP GET WWW.ZIMMERS.NET..." + # then on success: "OK" followed by response body, + # or on failure: "FAILED". + # + # We cannot simply wait_for_screen_text("OK") because + # "DHCP OK" is already on screen. Instead we poll and + # look for "OK" appearing *after* the "HTTP GET" line, + # or for "FAILED" after it, or for the response body. + deadline = time.monotonic() + HTTP_TIMEOUT + final = "" + http_started = False + result = None # "pass" | "fail" + + while time.monotonic() < deadline: + try: + transport.resume() + except Exception: + pass + time.sleep(2.0) + try: + final = get_screen_text(transport) + except Exception: + continue + + upper = final.upper() + + # Check if the HTTP GET banner appeared + idx_get = upper.find("HTTP GET") + if idx_get < 0: + continue + if not http_started: + print(" HTTP GET initiated") + http_started = True + + after_get = upper[idx_get:] + + # Check for FAILED after HTTP GET + if "FAILED" in after_get: + result = "fail" + break + + # Check for OK after HTTP GET line (not DHCP OK). + lines_after = after_get.split("\n") + for line in lines_after[1:]: # skip "HTTP GET..." line + stripped = line.strip() + if stripped == "OK" or stripped.startswith("OK"): + result = "pass" + break + + # Also check for response body as a success indicator. + if RESPONSE_BODY[:12].upper() in upper: + result = "pass" + + if result: + break + + if result == "fail" or result != "pass": + reason = ("HTTP GET reported FAILED" if result == "fail" + else f"HTTP GET did not complete within {HTTP_TIMEOUT:.0f}s") + print(f"FAIL: {reason}") + if result != "fail": + try: + final = get_screen_text(transport) + except Exception: + pass + print(f"\n--- final screen ---\n{final}") + _dump_diagnostics(transport) + return 1 + + print("\n=== PASS: HTTP GET OK seen on screen ===") + snippet = "\n".join(final.splitlines()[:25]) + print(f"--- final screen (first 25 lines) ---\n{snippet}") + + # Check for response body on screen. + body_upper = RESPONSE_BODY.upper() + if body_upper in final.upper(): + print(f" response body verified: {RESPONSE_BODY!r}") + else: + # Not a hard failure -- the body might have scrolled off. + print(f" (response body not found on screen, may have scrolled)") + + return 0 + finally: + if listener is not None: + try: + stop_http_listener(listener) + except Exception as e: + print(f" stop_http_listener: {e}") + listener = None + if handle is not None: + try: + shutdown_vice(handle) + except Exception as e: + print(f" shutdown_vice: {e}") + handle = None + except Exception as e: + print(f"FAIL: unexpected error: {e}") + import traceback + traceback.print_exc() + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/https_e2e/__init__.py b/tools/https_e2e/__init__.py new file mode 100644 index 0000000..20e3943 --- /dev/null +++ b/tools/https_e2e/__init__.py @@ -0,0 +1,30 @@ +"""https_e2e -- End-to-end test helpers for the c64-https program. + +Public API used by tests/test_phase1_dhcp.py and (later) higher phases: + + from https_e2e import ( + BridgeEnv, + launch_vice_on_bridge, shutdown_vice, + press_key, wait_for_screen_text, + check_prerequisites, + ) + +Internals live in underscored helpers in each submodule. +""" + +from .env import BridgeEnv, check_prerequisites +from .vice_on_bridge import launch_vice_on_bridge, shutdown_vice +from .c64_menu import press_key, wait_for_screen_text, get_screen_text +from .http_listener import start_http_listener, stop_http_listener + +__all__ = [ + "BridgeEnv", + "check_prerequisites", + "launch_vice_on_bridge", + "shutdown_vice", + "press_key", + "wait_for_screen_text", + "get_screen_text", + "start_http_listener", + "stop_http_listener", +] diff --git a/tools/https_e2e/c64_menu.py b/tools/https_e2e/c64_menu.py new file mode 100644 index 0000000..bcc8336 --- /dev/null +++ b/tools/https_e2e/c64_menu.py @@ -0,0 +1,80 @@ +"""Keyboard / screen helpers for interacting with the c64-https boot menu. + +Everything goes through the canonical c64-test-harness entry points: +- keyboard input uses transport.inject_keys() -- the same path as + harness.send_key() +- screen reads use ScreenGrid.from_transport(); between polls we call + transport.resume() so the binary monitor's memory read does not leave + the CPU paused. +""" + +from __future__ import annotations + +import time +from typing import Optional + +from c64_test_harness.backends.vice_binary import BinaryViceTransport +from c64_test_harness.screen import ScreenGrid + + +def press_key(transport: BinaryViceTransport, ch: str | int) -> None: + """Press a single ASCII/PETSCII key on the C64. + + Accepts either a one-character str (upper- or lower-case) or an int + (raw PETSCII / screen code). For letters, we send the uppercase ASCII + value -- the boot menu reads $49 etc. via CHRIN which handles this. + """ + if isinstance(ch, str): + if len(ch) != 1: + raise ValueError(f"press_key: expected 1 char, got {ch!r}") + code = ord(ch.upper()) + else: + code = int(ch) & 0xFF + # Ensure CPU isn't paused from a prior screen read. + try: + transport.resume() + except Exception: # noqa: BLE001 + pass + transport.inject_keys([code]) + + +def get_screen_text(transport: BinaryViceTransport) -> str: + """Read the current C64 screen as a flat string.""" + grid = ScreenGrid.from_transport(transport) + return grid.continuous_text() + + +def wait_for_screen_text( + transport: BinaryViceTransport, + needle: str, + timeout: float = 90.0, + poll_interval: float = 0.75, + verbose: bool = False, +) -> str: + """Poll the screen until `needle` appears (case-insensitive). + + Returns the final screen text on success. Raises TimeoutError on + failure, with the last screen text in the exception message. + """ + needle_upper = needle.upper() + deadline = time.monotonic() + timeout + last_text = "" + last_err: Exception | None = None + while time.monotonic() < deadline: + try: + # Binary monitor pauses CPU on reads -- resume each iteration. + transport.resume() + time.sleep(poll_interval) + last_text = get_screen_text(transport) + if needle_upper in last_text.upper(): + if verbose: + print(f"[screen] matched {needle!r}") + return last_text + except Exception as e: # noqa: BLE001 + last_err = e + time.sleep(0.3) + raise TimeoutError( + f"screen text {needle!r} not seen within {timeout:.0f}s.\n" + f"Last screen text:\n{last_text!r}\n" + f"Last poll error: {last_err}" + ) diff --git a/tools/https_e2e/env.py b/tools/https_e2e/env.py new file mode 100644 index 0000000..f94aa79 --- /dev/null +++ b/tools/https_e2e/env.py @@ -0,0 +1,183 @@ +"""BridgeEnv -- context manager wrapping scripts/setup-bridge-tap.sh. + +Runs the vendored setup script (br-c64 + tap-c64-0/1 + dnsmasq) on __enter__ +and the cleanup script on __exit__. Polls until dnsmasq is listening on +10.0.65.1:53 (DNS UDP) and :67 (DHCP). Tolerates repeated entry by letting +the setup script itself be idempotent. +""" + +from __future__ import annotations + +import os +import shutil +import socket +import subprocess +import time +from contextlib import contextmanager + +_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +_SETUP_SCRIPT = os.path.join(_REPO_ROOT, "scripts", "setup-bridge-tap.sh") +_CLEANUP_SCRIPT = os.path.join(_REPO_ROOT, "scripts", "cleanup-bridge-tap.sh") + +BRIDGE_IP = "10.0.65.1" +BRIDGE_IFACE = "br-c64" +TAP0 = "tap-c64-0" +TAP1 = "tap-c64-1" + + +def check_prerequisites() -> list[str]: + """Return a list of missing prereqs. Empty list means all OK.""" + missing: list[str] = [] + for tool in ("x64sc", "dnsmasq", "sudo", "ip", "iptables"): + if shutil.which(tool) is None: + missing.append(f"{tool} not on PATH") + if not os.path.isfile(_SETUP_SCRIPT): + missing.append(f"setup script missing: {_SETUP_SCRIPT}") + if not os.path.isfile(_CLEANUP_SCRIPT): + missing.append(f"cleanup script missing: {_CLEANUP_SCRIPT}") + # sudo without password? + try: + r = subprocess.run( + ["sudo", "-n", "true"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + timeout=5, + ) + if r.returncode != 0: + missing.append("sudo requires a password (NOPASSWD not configured)") + except (OSError, subprocess.TimeoutExpired) as e: + missing.append(f"sudo probe failed: {e}") + return missing + + +def _port_open_udp(host: str, port: int) -> bool: + """Crude UDP 'is something listening' probe -- check /proc/net/udp.""" + # UDP sockets don't accept connections, so best to scan /proc/net/udp. + try: + with open("/proc/net/udp", "r") as f: + lines = f.read().splitlines()[1:] + except OSError: + return False + # Format: sl local_address rem_address st ... + # local_address is HEX_IP:HEX_PORT where HEX_IP is little-endian for IPv4. + try: + packed = socket.inet_aton(host) + hex_ip = "".join(f"{b:02X}" for b in reversed(packed)) + except OSError: + return False + needle = f"{hex_ip}:{port:04X}" + for line in lines: + parts = line.split() + if len(parts) >= 2 and parts[1].upper() == needle: + return True + return False + + +_DNSMASQ_PIDFILE = "/tmp/c64-https-dnsmasq.pid" + + +def _pid_alive(pid: int) -> bool: + try: + os.kill(pid, 0) + except PermissionError: + return True # process exists but owned by another user (e.g. nobody) + except (OSError, ProcessLookupError): + return False + return True + + +def _wait_for_dnsmasq(timeout: float = 10.0) -> None: + """Wait until dnsmasq is serving DNS on 10.0.65.1:53. + + dnsmasq's DHCP listener uses a raw packet socket (not a regular UDP + socket bound to :67), so we only check :53 for the UDP listener and + rely on the pidfile + process liveness as the DHCP-ready signal. + """ + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + dns_ok = _port_open_udp(BRIDGE_IP, 53) + pid_ok = False + if os.path.isfile(_DNSMASQ_PIDFILE): + try: + with open(_DNSMASQ_PIDFILE) as f: + pid = int(f.read().strip()) + pid_ok = _pid_alive(pid) + except (OSError, ValueError): + pid_ok = False + if dns_ok and pid_ok: + return + time.sleep(0.2) + raise RuntimeError( + f"dnsmasq not ready within {timeout}s " + f"(dns_on_{BRIDGE_IP}:53={_port_open_udp(BRIDGE_IP, 53)})" + ) + + +def _run_sudo_script(script: str) -> subprocess.CompletedProcess: + return subprocess.run( + ["sudo", script], + capture_output=True, + text=True, + ) + + +class BridgeEnv: + """Context manager that brings up br-c64 + taps + dnsmasq. + + Usage:: + + with BridgeEnv() as env: + # env.tap0 / env.bridge_ip available + ... + """ + + bridge_ip = BRIDGE_IP + bridge_iface = BRIDGE_IFACE + tap0 = TAP0 + tap1 = TAP1 + + def __init__(self, verbose: bool = True): + self.verbose = verbose + self._entered = False + + def __enter__(self) -> "BridgeEnv": + # Clean any stale state first so repeated entry is safe. + if self.verbose: + print(f"[BridgeEnv] cleanup stale state...") + _run_sudo_script(_CLEANUP_SCRIPT) # errors ignored + + if self.verbose: + print(f"[BridgeEnv] running setup: {_SETUP_SCRIPT}") + r = _run_sudo_script(_SETUP_SCRIPT) + if r.returncode != 0: + raise RuntimeError( + f"setup-bridge-tap.sh failed (exit {r.returncode}):\n" + f"STDOUT:\n{r.stdout}\nSTDERR:\n{r.stderr}" + ) + if self.verbose: + # Show a compact tail + tail = "\n".join(r.stdout.splitlines()[-6:]) + print(f"[BridgeEnv] setup ok:\n{tail}") + + if not os.path.isdir(f"/sys/class/net/{self.bridge_iface}"): + raise RuntimeError(f"{self.bridge_iface} not up after setup") + for t in (self.tap0, self.tap1): + if not os.path.isdir(f"/sys/class/net/{t}"): + raise RuntimeError(f"{t} not up after setup") + + _wait_for_dnsmasq(timeout=10.0) + if self.verbose: + print(f"[BridgeEnv] dnsmasq bound to {BRIDGE_IP}:53/67") + + self._entered = True + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + if self.verbose: + print(f"[BridgeEnv] cleanup...") + r = _run_sudo_script(_CLEANUP_SCRIPT) + if r.returncode != 0 and self.verbose: + print( + f"[BridgeEnv] cleanup non-zero exit={r.returncode}\n" + f"STDOUT:\n{r.stdout}\nSTDERR:\n{r.stderr}" + ) diff --git a/tools/https_e2e/http_listener.py b/tools/https_e2e/http_listener.py new file mode 100644 index 0000000..d559a28 --- /dev/null +++ b/tools/https_e2e/http_listener.py @@ -0,0 +1,78 @@ +"""Simple HTTP listener for e2e testing. + +Runs a background HTTP server on a specified host:port. Every GET request +returns a fixed 200 OK with a short body. The server runs in a daemon +thread so the test can drive VICE in the main thread. + +Binding to port 80 requires root. The test already runs under sudo +(BridgeEnv needs it), so no special handling is needed here. + +Public API: + start_http_listener(host, port) -> HttpListenerHandle + stop_http_listener(handle) +""" + +from __future__ import annotations + +import threading +from dataclasses import dataclass +from http.server import BaseHTTPRequestHandler, HTTPServer + +# Fixed response body served for every GET. +DEFAULT_RESPONSE_BODY = "HELLO FROM TEST SERVER" + + +class _Handler(BaseHTTPRequestHandler): + """Serves a canned 200 OK response for any GET.""" + + # Class-level attribute set before server starts. + response_body: str = DEFAULT_RESPONSE_BODY + + def do_GET(self) -> None: # noqa: N802 + body = self.response_body.encode("ascii") + self.send_response(200) + self.send_header("Content-Type", "text/plain") + self.send_header("Content-Length", str(len(body))) + self.send_header("Connection", "close") + self.end_headers() + self.wfile.write(body) + + # Silence per-request log lines. + def log_message(self, format: str, *args: object) -> None: # noqa: A002 + pass + + +@dataclass +class HttpListenerHandle: + """Returned by start_http_listener; pass to stop_http_listener.""" + server: HTTPServer + thread: threading.Thread + host: str + port: int + + +def start_http_listener( + host: str = "10.0.65.1", + port: int = 80, + response_body: str = DEFAULT_RESPONSE_BODY, +) -> HttpListenerHandle: + """Start an HTTP server in a daemon thread. Returns a handle.""" + # Set the response body on the handler class before creating the server. + _Handler.response_body = response_body + + server = HTTPServer((host, port), _Handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + return HttpListenerHandle(server=server, thread=thread, host=host, port=port) + + +def stop_http_listener(handle: HttpListenerHandle) -> None: + """Shut the server down cleanly.""" + try: + handle.server.shutdown() + except Exception: # noqa: BLE001 + pass + try: + handle.server.server_close() + except Exception: # noqa: BLE001 + pass diff --git a/tools/https_e2e/vice_on_bridge.py b/tools/https_e2e/vice_on_bridge.py new file mode 100644 index 0000000..d805f5b --- /dev/null +++ b/tools/https_e2e/vice_on_bridge.py @@ -0,0 +1,162 @@ +"""Launch a single VICE instance on the c64-https bridge. + +Mirrors the single-instance half of c64-test-harness's bridge_vice_pair +fixture. Normal-speed RR-Net, CS8900a initialised, MAC programmed, PRG +autoloaded via ViceConfig.prg_path. +""" + +from __future__ import annotations + +import time +from dataclasses import dataclass +from typing import Optional + +from c64_test_harness.backends.vice_binary import BinaryViceTransport +from c64_test_harness.backends.vice_lifecycle import ViceConfig, ViceProcess +from c64_test_harness.backends.vice_manager import PortAllocator +from c64_test_harness.ethernet import set_cs8900a_mac +from c64_test_harness.execute import jsr, load_code +from c64_test_harness.memory import read_bytes +from c64_test_harness.screen import ScreenGrid +from c64_test_harness.bridge_ping import ( + cs8900a_rxctl_code, + cs8900a_read_linectl_code, + cs8900a_write_linectl_code, +) + +DEFAULT_MAC = bytes.fromhex("02C6400000A1") # 02:C6:40:00:00:A1 -- c64-https + + +@dataclass +class ViceHandle: + """Everything a test needs to drive and shut down a VICE instance.""" + process: ViceProcess + transport: BinaryViceTransport + allocator: PortAllocator + port: int + + +def _connect(port: int, proc: ViceProcess, timeout: float = 30.0) -> BinaryViceTransport: + deadline = time.monotonic() + timeout + last: Exception | None = None + while time.monotonic() < deadline: + try: + return BinaryViceTransport(port=port) + except Exception as e: # noqa: BLE001 + last = e + if proc._proc is not None and proc._proc.poll() is not None: + raise RuntimeError(f"VICE on port {port} exited early") from e + time.sleep(0.25) + raise RuntimeError(f"could not connect to VICE on port {port}: {last}") + + +def _wait_for_ready(transport: BinaryViceTransport, timeout: float = 60.0) -> None: + """Wait for either BASIC READY (no PRG autoload) or for an autostarted + program to have taken over the screen. We poll continuous_text() for + either 'READY' or common c64-https banner text. + """ + deadline = time.monotonic() + timeout + last_text = "" + while time.monotonic() < deadline: + try: + transport.resume() + time.sleep(0.5) + grid = ScreenGrid.from_transport(transport) + text = grid.continuous_text().upper() + last_text = text + if "READY" in text or "C64-HTTPS" in text or "Q=QUIT" in text: + return + except Exception: # noqa: BLE001 + time.sleep(0.3) + raise RuntimeError( + f"BASIC READY / banner not seen within {timeout}s. Last text:\n{last_text}" + ) + + +def _init_cs8900a(transport: BinaryViceTransport, code: int = 0xC000, scratch: int = 0xC1E0) -> None: + load_code(transport, code, cs8900a_rxctl_code()) + jsr(transport, code, timeout=5.0) + load_code(transport, code, cs8900a_read_linectl_code(scratch)) + jsr(transport, code, timeout=5.0) + linectl = read_bytes(transport, scratch, 2) + load_code(transport, code, cs8900a_write_linectl_code(linectl[0] | 0xC0, linectl[1])) + jsr(transport, code, timeout=5.0) + + +def launch_vice_on_bridge( + prg_path: str, + tap: str = "tap-c64-0", + mac: bytes = DEFAULT_MAC, + port_range: tuple[int, int] = (6560, 6580), + ready_timeout: float = 60.0, + verbose: bool = True, +) -> ViceHandle: + """Start one VICE on the bridge, autoload prg_path, init CS8900a. + + The program's own code is running by the time this returns -- because + we use -autostart, ip65 boots as soon as BASIC runs it. The CS8900a + init is NOT performed on c64-https (it takes over the chip itself); + we only run it here to match the harness pattern's "known-good" init + before the program grabs the chip. In practice c64-https re-initialises + the chip on its own so this is harmless. + + Returns a ViceHandle. Call shutdown_vice() to stop cleanly. + """ + allocator = PortAllocator(port_range_start=port_range[0], port_range_end=port_range[1]) + port = allocator.allocate() + res = allocator.take_socket(port) + if res is not None: + res.close() + + config = ViceConfig( + port=port, + prg_path=prg_path, + warp=False, # load-bearing: warp breaks DHCP + sound=False, + minimize=False, + ethernet=True, + ethernet_mode="rrnet", + ethernet_interface=tap, + ethernet_driver="tuntap", + extra_args=["-reu", "-reusize", "512"], # boot.asm uses REU for mul tables + ) + + proc = ViceProcess(config) + proc.start() + if verbose: + pid = proc._proc.pid if proc._proc is not None else "?" + print(f"[vice] started pid={pid} port={port} tap={tap}") + + try: + transport = _connect(port, proc, timeout=20.0) + _wait_for_ready(transport, timeout=ready_timeout) + # Best-effort: program a MAC via the harness helper. c64-https + # may overwrite this on its own init pass; that's fine. + try: + set_cs8900a_mac(transport, mac) + except Exception as e: # noqa: BLE001 + if verbose: + print(f"[vice] set_cs8900a_mac skipped: {e}") + except Exception: + # Clean up on failure. + proc.stop() + allocator.release(port) + raise + + return ViceHandle(process=proc, transport=transport, allocator=allocator, port=port) + + +def shutdown_vice(handle: ViceHandle) -> None: + """Close transport, stop VICE process, release port.""" + try: + handle.transport.close() + except Exception: # noqa: BLE001 + pass + try: + handle.process.stop() + except Exception: # noqa: BLE001 + pass + try: + handle.allocator.release(handle.port) + except Exception: # noqa: BLE001 + pass From 1c75ed9dbcf47c2ba8b89bd1e8adcc273bac38f6 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Tue, 14 Apr 2026 08:40:13 -0500 Subject: [PATCH 07/22] Relocate crypto above ip65 BSS + multiple TLS receive-path fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original CPU JAM at $4DE0 was caused by ip65 writing DHCP packets on top of sha256_rotr8 code: c64-https crypto code streamed from $3B28 right through ip65's BSS window at $4000-$5FFF. Fix the layout by inserting `* = $6000` before word32.asm so all of word32/chacha/poly/aead/sha256/ hmac_drbg/fe25519/x25519/ecdsa + ecdsa_verify/der/tls_cert/tls_ecdh land above the BSS. Requires three adjacent compromises to fit in the space below $7C00: - Relocate sqtab from the hardcoded $7800/$7A00 equates to labels inside data.asm BSS, freeing the $7800-$7BFF region - Stub P-384 ECDSA (src/crypto/ecdsa_verify.asm) to return an error instead of dispatching. P-384 isn't needed for the self-signed P-256 cert used in tests but MUST be restored before real CA chains. Tech debt tracked in project_p384_stubbed memory - Remove the `* = $7C00` barrier now that sqtab is gone With the layout fix, the CPU JAM is gone entirely and the TLS receive path progresses far enough to expose several follow-on bugs that a clean walkthrough diagnosed and fixed in sequence: - tls_record_io.asm: plaintext/encrypted dispatch was comparing against TLS_STATE_SERVER_HELLO ($02) but tls_state is already $02 when the ServerHello arrives, causing the code to try to AEAD-decrypt a plaintext record. Change to TLS_STATE_ENCRYPTED_EXT ($03) - net.asm: TCP recv callback had an 8-bit X register wrap when ip65 delivered packets >255 bytes, causing re-reads of source bytes. Clamp cb_remaining to 255 per invocation - tls_keyschedule.asm: tls_derive_handshake_keys fell off the end of its final loop without a clc, leaving the carry flag set from the last hmac_sha256 call. Caller's `bcs @error` then fired on success. Add clc before rts - tls13.asm: tls_ecdh_compute_shared was defined in tls_ecdh.asm but never called anywhere in the codebase, so tls_shared_secret stayed zeros. HKDF then derived handshake keys from zeros. Add the jsr call in tls_recv_server_hello right after tls_parse_server_hello succeeds - tls_record_io.asm: RFC 8446 §5 says TLS 1.3 clients MUST ignore CCS records during handshake. Add a retry that skips CCS content type Rename the demo URL from www.apple.com to www.foo.bar throughout boot.asm, dnsmasq DNS overrides, HTTPS listener cert subject/SAN, and test screen text expectations, to avoid any risk of real-world impact if the test environment escapes the bridge. Bridge setup/cleanup scripts hardened: no blanket pkill, specific PID tracking, idempotent re-entry, handling of stale vice_eth rc files and legacy tap-c64 interfaces from older test harness setups. New test tests/test_phase3_https.py drives the full Phase 3 e2e flow: BridgeEnv + HTTPS listener + VICE + DHCP + HTTPS GET. Captures granular post-mortem diagnostics (tls_state, tls_last_state, tls_recv_progress, tls_recv_sub_progress, tls_rec_header, tls_rec_buf, tls_hs_buf, tcp_recv_buf, tls_recv_state/count) via dynamic label lookup from build/labels.txt so diagnostic addresses track the current build. Instrumentation added to src/data.asm (tls_last_state, tls_recv_progress, tls_recv_sub_progress, tls_recv_poll_count) and to tls_recv_server_hello, tls_record_recv_and_decrypt, tls_recv_record for precise failure-site identification. These are permanent diagnostic helpers. Current status: handshake reaches tls_state=$03 (ENCRYPTED_EXT) and dies trying to receive the first encrypted handshake record (EncryptedExtensions). Two remaining architectural bugs require multi-file refactoring and are tracked as follow-on work: 1. tcp_recv_buf is 256 bytes with 8-bit indices. TLS 1.3 Certificate records (~374 bytes) don't fit. Needs 2KB ring with 16-bit indices and producer overflow check in net_tcp_recv_cb 2. aead_data_len is 1 byte. Caps AEAD decrypt at 255 bytes, so the ~353-byte Certificate ciphertext body is truncated and tag always fails. Needs widening to 16-bit with updated ChaCha20/Poly1305 loops Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 3 + README.md | 2 +- build/c64-https.prg | Bin 41278 -> 45891 bytes build/labels.txt | 1059 ++++++++++++++--------------- scripts/cleanup-bridge-tap.sh | 44 +- scripts/setup-bridge-tap.sh | 4 +- src/boot.asm | 20 +- src/constants.asm | 3 +- src/crypto/ecdsa_verify.asm | 308 +-------- src/crypto/poly1305.asm | 4 +- src/data.asm | 10 + src/main.asm | 20 +- src/net.asm | 10 + src/tls13.asm | 22 + src/tls_keyschedule.asm | 1 + src/tls_record_io.asm | 52 +- tests/test_phase3_https.py | 549 +++++++++++++++ tools/https_e2e/__init__.py | 3 + tools/https_e2e/https_listener.py | 162 +++++ 19 files changed, 1360 insertions(+), 916 deletions(-) create mode 100644 tests/test_phase3_https.py create mode 100644 tools/https_e2e/https_listener.py diff --git a/.gitignore b/.gitignore index 28b2cfe..e6e44a4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ ip65-build/*.bin ip65-build/*.map .claude/ .serena/ +tools/https_e2e/certs/ +tools/diag_4de0_*.py +tools/diag_read_live.py diff --git a/README.md b/README.md index 4eb9506..15a0143 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ sudo ./scripts/setup-bridge-tap.sh sudo ./scripts/cleanup-bridge-tap.sh ``` -The setup script creates `br-c64` with `tap-c64-0`/`tap-c64-1`, assigns `10.0.65.1/24` to the bridge, and starts dnsmasq providing DHCP (pool 10.0.65.50-150) with DNS overrides (`zimmers.net` and `apple.com` → `10.0.65.1`). The `BridgeEnv` context manager in `tools/https_e2e/env.py` wraps both scripts for use in tests. +The setup script creates `br-c64` with `tap-c64-0`/`tap-c64-1`, assigns `10.0.65.1/24` to the bridge, and starts dnsmasq providing DHCP (pool 10.0.65.50-150) with DNS overrides (`zimmers.net` and `foo.bar` → `10.0.65.1`). The `BridgeEnv` context manager in `tools/https_e2e/env.py` wraps both scripts for use in tests. **Library:** `tools/https_e2e/` exposes a reusable public API: diff --git a/build/c64-https.prg b/build/c64-https.prg index 1b33b3808c64095dc77aa6f9a0678e1019dfee4e..cb876bf66f7051aa8b5cef5739a6df9369ad89d6 100644 GIT binary patch literal 45891 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 g3Z?B#|DX3j)c|J5G&Up!KaXaE2J literal 41278 zcmeHw34BvkzJJnmNz*NbLQ~dD*;?LJMD9{3FFKvNwL&8#Rfv}H)ilP`~kP`|@ zr!9t*lmNGS`%-6oW7Q~vLW{s$b>`{3(TXBaRBG4+ZK40~x%Vaw1!s9P^WOWsKlcOo zp7Yz!@BGes?`f3k2(?DBdwAODkqU*vrA)qQRj%b;U_NT&CRJ*fI`utFY63GgkIBxI zkWBYTZo7z)@}^8R|GL_%V){lgU$=QLH%t>knRRM_8?uo)CZ@8G>J?65dBTS-Ww_F+ zG;_ZZr7)_f5;b>ROq;nc#V2<`Ld8>*vN8f@n7L^2$&^QNO7gnetgyB`+ghopZSjV} z*m~gyGt3awTu+&Ny!hl!la>lDlC)NEG2})CSZ`KvQKXHg6uY>YM7fJokf2>m>v*VA zR7Q%bS|wG3H63K$C&|3*ml<^jnQu~=O2~97JeMQAYWw2&rc)Dz%bWr!TXj5l*%`?U zr6qerHg$_6cD2*M+#kig=~Ob4D0+vC-X}hpBhe)h+-p*LTa=mmKuW5{aUo>LZf+MD zx|&=;?M-I)N zHEU)8lWCeZC3hAxCoN@IRMad}0W&OR1e3~SW*hNuc-qjJtm)oiBhp4i<>wF0oid9_ z$}C79o0euwW~R)_o{|qxRFq=c_-VP*W-(0mcq)`(vczWr!{m+6n>|gW0DhX$H0{1= zxmgUDe!=F?NJ&YFQpjmW6fo5|%`|0lJCL0brO2O>H+xFKEXp@IJ9DPUL=%jUSj@09 zn4!$HnR-SsB|o3d*ZXuZx$G=awy2!S7A?#|Nq+H*U!>eOEhlG6enASX5Cuy<@nMZK zXPTy@WU@Ju(%b?je@X#snlq(cH(=DrS+FOQ&CQ*XIm?d=fKk2Fx7o(r$pzWQ8SMgY zg^Hp;S1=`avR{tu01BKHrJy9flFJ&WvAL*5wt#9u6r6~}?PMf>BC_UWB=b!s?tD|G zU18(T*k;NOcT*)0ElG?~WohoWl$j0w=yNu=ht z=$Ru?Tnd^P|9pFLYkQK2NwEzQb+l8FetuC&aFgkHRPiD-Xm$3M@H8+NQG|vf&O{Y! zPAP=2SJcr5H6?z$NM%;=ik&BvJH0_VbGlhhhgW+PFE%T7ilDYOvOJo{eMNNC@@VgH znk*0jv!MGGF-2kmP7SjUj_*nj%wa7Ra<{voqnfAp3*lnD#o7o(IX!AezLu zMME`cC#SNua67H79K*bm$7Kd&m>z^fm%NjyigD*4RFFCqmJDk z%c()=QY9F}h03X}jG>o1mYLYY9m%|^b%$|bK>wk5Au`Q@_B+8@lw^g{4&?^P1Hxwcoa0twY-?NwT1HOEj_ zuQZgFfADM5`>|r8REH`D=u`@#La491Dy;AZ%uoF5LYpeN*oq;=2YCBqr z?ruS>Gs5$Infx&zesmGDB+A{D`v5>n z{9ZI=l)$uKT^lE?X1YX|s7yoRYSzWs50^HtixX7MkHzup;u=DP)BIy`LL+~eTF<{% z+MvRSmlDUn?@t{qrNuc?io!rLOplQ$8cG}bLuyJK`nk1nFlAjFfzXE1D1?h1D&3Pv zuQ(mF6n}u3-(zvb2!6g7dX?=h0VbSiC>1^e_QbJL;h3p+9PL=w#!15D-o%$pH~)-# zsCTG8gZ*xDM@w!FVa%OqJ5Cqdv7%DJDQS-b9j#^LMj5$TR$pJwsVI#8#{^onlO~!@ zF~Sv2A<>HWAo1Q=yo>ZcN$oW4DQ<8;TB4TtrF3urRuhm8giG|21w8|3q5{&l(@PLZ zi^Mx3fPO$gx?ey#P(DN9@WaA(Kk(J@UX$dEb|wII#UUN?fr>@-ppxY+786b1RmanX ziO45cML=4Fl%=0fCCTNeqoi`AgHcIHqq17dwzg1kYq_~nK|AjtMB@GNs6kv#MO{mo zz3)z`>X5Fl_6B2d(TZe4XCbMcK2-uEJ{5wFKCLS89z^3|ZajG~-n6GH1iFGkRD#OH z##53Ok)&0;-xTjR>RmxF%NpbkI-)}I%#HTsO*z>j^l-H{3>LadS|6s24Z*^^`sTS* ziK|5z4g3fp4RB$ai0vsD=~KuMbBr)TF49K1NX?>ouA4%)eK%m;p02cTc<^)Myu{yIv*Bnpj+F3Y-bE3%c6Lfhs$NU12t;aLg#zYe;_e`;5Lv> zf8e7(3s)vzBhjCk`H~86GHV&la&QmxVlE7LcaQx>Ic%YBx=}9jUPH4Z z72e3Y@f9KqsX!;GL=sRsaTZx?Dk`Ygz}~ZhvZG4ISMU`TWI_dB3bIf#u>ynn@^0mZ ziu!}0!c}KD{~|SMCrko`H(IRV@fGz=$4jArFR!qVuV~_bO=-#~P5CV}<)E>V3X?kl zbBbSAa63s^1;>!`3Q|$Qe4k)mvvl>a5sX;=wB7cZyI~WTMr;+_ZnBLMmQ-L@@Oi$w_kJ>6}|H| z(Qk^PxBEp$LNs%4H%ZXRUJ!)jn-x+GaP~XXHOq7-d&zX~`sq~E)GdBf6>ta2COSmY zp(>Sh_<7&+^9J34H&W(}patzK^EUq%^rm*?{X6usl$VC1-!cDC-N+$FxnLF0fpMACT+MVhCky^U{AE>3rJ9_%>h>qtv^8OvQv|p?x?ug$T>N~{KEwPwO z8xE>9x3|>GAbEJFecm2G5wrpQQ3@rb0pN=~x-e5%gQ`6#z2~--g#=lH=)~=!E65v^ zjqT-mldxrDqQrxeSH1jXP@cKB-4jUrh$D;=>tr? zQ%(EQOrNBhydzENjAgEd<< zn<6kuXvk8w9(Rj)Unbs{vzQ##Mt-OfI@gPjNa+zGKFsfH1l5-JBg{l$uqh4cF3kmn z!K!gZTu>UU5oR4_U8&iw0rBk*iWZ?;#5*Ttq=&iM%eXX<>WUyr!!d6sol9epL|dq6 z(`g1)Lj~pxtUby=d!05?XmF}LHvP1=hBmLNxzJ#rYv{Q{Z=Vlz6Wu;CnE7zSP>o?| zgkfl;p`lICI796!%B(Zj6dF9?hMsc`pU*c4A;+|r(jp)X+~L4qV;cLoBPv5`HRUS9 zW^7sUL52pUH-dl2Q1h^Xxt!#l&8$gco=RdqOT+*jl?83lS7N!abS&#^M zTlDrWSx?Oh~;T-qY%L}M%WgL>HK}^X6PJd{CtaE zFdfrg<@z{{%B;*PfJLW+dCgJ<|;K0)uCw!9LC48D+3fGgD2i#?_=;p8SL@! z^~aUraC4OCW-gR@J_!yAqYhPPacU9{ZxT%wcJ(5%u-zA#WhO<2N(Ht{qiZ$WBkP49 z&qkb$I)**4B7;{Wf{qs%@N&tiLN#pB(?C&@VgvW2vmd#dYx^OWo9t9D6Ov2tLQ6pw z81{w#2}Eh}s=n63T5N-J5MeqYF?_u2h%7gau+Z04SZH0C#bQO!zn!Gv402_^d; zt3%>7IaRtX#Y+1{)Eu42Up0{*Oyru0TsJvm>^}nflN4a#IUsNzMDcVv$Aq7_h2&2; z+%x1qbGQQX=Nx1|%b^NMORlXom&-)zMy~BPHZ_<=PZ1cfo051kp)B;4+U=D#xgls?aumlo17`zB@6+C@5*XO)=V+(Kdrs zZ~sYXWGa)Pf5Rvt+BBlSXW%4}8p#Hw&>dM~(|eL8k!^ZU@+4BC_Y9guw(BWcC82kS z=oduviz0fbhz^y|r6T&*BDzdOmy77p61qY}?-J23iD)}TZ_yP8tsgMS9b9eN)sIp4 zp*0rb4Z(IB0z-*PH&eHaBA@3V*O&wN7dhN;aw^9eZ2L0jm}i;6{uT8UH=3NKuycS_ z=JmV%kH2od)7H^kADJ7+sP(7HWcwz^IJ&)zo$9bDheq zLB_4?3|k*FY*iT1^wkZi-rxqk5YntL3c(F2LeDMP{5nI;V+Q*ZG*s6aimGjo8F+=! z8*b`10Kn#ThWe&7t?5jE&*KJfC<5dOLzB+TJ&UMAUlC-aYg}%*bD-1^oTHjo8TeHO zxiKh7D;4{=fjK^af1I|6n>61hwul=YS_I!}K&+2!2==16o@kRB#|?w`(~~z1LOnz@ zKW>1OhA^$lv}geLv=GGYp=P+1I<<2Y)%t95l9_+hP_x$1RIDJ68f&l3hTjV7&HETqXYut6bwi-P}XvbR8x z{U?*X)r9KT`T|?jEj11Jzf>;<{xxnC@M0^G1K%Br?zz=KcL39k$uSMsr$p}q0IxR^ zCGBrjruZa&oDtC;pX7}6j52y7bz9btG8QYV-K$H!Z}N-=l_woHVNnwqT1ira#@$a>sadZwx_fLE zuG>{cQ@^C9Q^8*BhDw=JDrxtFaq63^^w{d$t2aAC5a~ZoMHhi29YflXzGfz&w2KbO z!uyeo!dy#miKY7=ESit9Els9yb@loP<9hH2*Q$#w=S(^0O=f0mI=WOV?NXWFrZYDg z=IKFrSJ0|=Yx#I1wq3$`9URgSiVmi^$z+O5Y(8%?l@FpX?D}SqZ%wxEOn5t&e&Jiw zmJ8p}ewoCwWAF6o}d-^eAsviO_1q-PcvOLTOI z?2|X!q{tzq^h)8rsoGkTMw4>WJxcU^k$jTAjtLpVE2!FA`N_%>7^kJp?G+PH^kx*n~e69U~HHo6!1Nay5eZkmF2q|VOu=Q zhZ)l^AgeZBh-cGAU}l5f&mauK>_+I%_caP(d_NrDMc2fz3%dBQlfPYFiFFvgH~=)Cug#I98_y?x4b(khB>T5`kavX0_?QAxU=-+KrvoRt@;H zHtO;uG{vE4#omE@66=krNn($A2CyfFif!>K+F~M0s`Z|JEO}YaaA=FXj-?At_$R`M z9>o(&C#i^abf*CPIJN1!Fr#n+$WLYR@dOl^e2 zfEFw2j&*@~H>UnDFZNWBw~tft`2g12t?9cYR=C=n#M%e2Jj2=(S-uZz?}xUTF4Be{ zqh#n%uBV%~(1s2E_yeASl-|>q(tG+-`lijoHz$+8R;j8>X3OKVHe<@5hOscIM;2^F ztEH`w$!boao*&BcL)n@XR%(doiue?7XnEhPdf_TsX%9N>RnQ4=cPXdfKy3YpQP(uU(B9cJ~K`3sjQ%uA=UB$u_ZEC`3u;9kX&9G1*t z$!wN+F^SA!ZNFfd&Z!+{;>r`lSns#oEGH}Wo0uvvZ!idvS*-mYRveQSv$iFogd)~f z%wib-S?R!;sOHHmnZiQGQr142rIPfJbdfJ&Yl>MioVAT$`NeF_5|)f)ZKGIb^DurD zTdX82S(M$jg8D09t-uds8-j!ZKz+A@#adw{D}36#iUmspoeND*g+f?FR|4n^L5#kO zZnn^*${QwPb!P6K_5#8!C?k4bYAog{lUZAUxeu`R34Wu${dSWw z-C`-3%G$CdpDPZKY!)BF)M_yQ$3!){IU9Y?56#oqvk@j+8b6h-$zmmXsZf#FN~NV0 zE7?~vKAWwXCKgPGf=M~4XtIDwtS$2&uayXK1t)sd%q^0gN}Y=lgqhEHeHA6p;=dnI zd?78cqI?18w;B~TiLJ?$Jd!Gpxs%zNDe~w&n!RQ89?sIydjv~+u8|lyv-nXMLNSIsq=R=&oCCOQ~L9}BSoJEG* zGU88WF>Z^){S>kLzh$f+oMjtA$1d3AAL~VPb;U_{8tX%)vECn`Tz`zZbf&Vj-FJvv zboezXB7$GS)+}b3^$hLO(L2|qN#J6(rpO0ITn7))8@)pq%W(GqzNfKaB*x=j#)eLq z9C$}c4*wNm1$#UBEGb}_qbYR84g46el&DN^jHDI?3H_<(>V_HZ4`3pt{egR+y}6*R zFu|#q7!PBqOf3{b`kNzs0z}SHM*AX`@|!8Y*UxYEmrh_YMZwtI-Dr*A#~PY$}rOPMUUuaF2Msl&ZQ3ujjz2A@I(~Ku_S>ZlZ z&k2?l@;R|L4W-7=mAucRbiBpRApFATvNbGA#GaH-F_Og?v5aI1D|**gLZ8A;(B%^{ zL8_Jsw^R!k*RczOqS3V1rcy_-RKrLX#u4!wJv~IQj?KS#xw|+}`t$y=n z?e|<9gjO211~=mQN^oN`wxhUUq|tn%nCC}JTI=r7zkRPB+4}l}50+fuI#Gg;QFYIc z-hy`04_Q8>F*(YcR1@CFs~e@eHF{HwRl6ycsT|F}u76+4*Xhs1y4B2%(I_3}-qoW) zifu7IoZf{?*c#Jba7FKn!8!wMTm#0B$VOXKqf5IlmP9w&Vj3Z;Qq85y`ji3{wwZox z24S_4MBRvaAPH~u3DcR)Sd<4f+Ek5Z65MDDX*6>|lsULD+yH=idP(S6EnY3hnyHRy zZ3pU~#jN(<^=CBJNd9;F^9pMewmY`QH1aWx4I^kZY>jTjo>WL9JhnBY@!hs}@pbr* z+|y1KQBe7qAVfFPO4QOy3@32AqVAZ!Jh&0>KTED+6bC;xVH>N_DMvOYD``*SJVinq zZDG`N_&1$ic%BcjM$?DF8pD62e?OZ4Sbru4?rRz&xvzN)u+(waspBpKF8dB{-xAKi zo=P=Uf$!FV+bmqhpr(dHc~v7^0<8u~E6hIN zuIT9{!5o7Llgz^%gc^7mE>Sd^seYdi1<8jWNo()nF@1==b1|)sUPc9Yzp2K!f$4ov+Y@K9Anlu z40rc$NOJdR=;iL*5a-rl14C&(uL;+h&&P!`=JUP6)6D0S!VTv0!^3A%X}0HCO^WoN z{EDr4p0#c919X;Ou>kU0Sj+QlHUBHT6*({#0FVT*`Pgr#=Ej*_ADkGFA#|#2VUx>Ua(?TR`RVayI1K*B^`H>36@1 z^2PC6tK;br4wdPL4CXJK*>EpL!wug}B6btaZW6W|=P&@cl0mL!&@Xs#b|bC*TVs4> zg71rCYFlcQJMD@Wn{3LRShX`>k1OfoV=`4|G+)UO4mV%T5Z;!KbLhUvKw2QBzRXf zM5=Znfwx0lbi8U8-mIy1@xi-_am-=YF;n01d{;_Z?Z<0w!Do)=<0)Qefs}Sej6dPD zPBptt*Qp`yU)E@F07uX?L*D<2qIfznvo(NyC z;yls&(U@)QqTjX-hqBw^bN&RfU;>R#gr25({sepnzF>mTj41O58$AcZ&?7Oa!*DuC zlf8WP8anJFViy%mo8sj7^R8QC=~R?Z}7^{?dtkFJ;e9I>XpG1eTN5P zpf)&9V!r)BvFbdD_x6W~&8`fpRl{lOqROD{>Pls;I=fN@h>}87+to4=;FL%t21+CW zy+p!{m`D|Aa9M+Biq06UDk_Y^LNGZ73vx0X8f7>Yg`5nBUKvhJL5{*hkTydDb(4CK z=!OpW5|~!24u>pk9#Q*giC{X_SV<)TTM29tHA5s3^#xI4st=aRHo#KKhnx(TZ3djm zK~9E4qG%h+D9NXA$HFAPv4U>7p_ih@*8BdM z3)ilgzibgRV))Pn>(&%9OBBKCNM%TPR8XiUS``)%qX^N&C_^J6gTmBNs^IWwXN;%L zV9#`72*5Whsw#Vdm{fUEtL&@AWQ3=0mHkngH1F%;IcCskbj2I3!JM-gV=o{LGDIyx z!kOPp2mrQ|9q&Nqoxns-1f>t&PWeFxN>QK>*iPoN#HSYm7a0D@7M}WNTSyFMeD0a) zv~Q#iC9zcWiIfVSv6J~RlhZGxPj&}SYb(Y9im{WuaUR9G^dA*#NmHgr@%WF5$H@6ivq!Pk z4|`08L9CO3;}%iJp=P`7>*g6z<;_L1yO_-0*WEL@$~&^U2*(Cp-E@WBt9715RTi8c zTvXLWQ#e7msH!Bc=~N$gOgaF?RGQms+Bv!CY<(tkPA+IvGG0gZ0>ERC9+s%;|wy$(kyS72l=}vUYd%M9S42DV?k5 z`ZZNFgOH%P!XEw%ic;7UnnYZWrc<#cZ<(zr-CAlTalWfNUO8_3mmr{MiG zh}X}e-U9VyF>AM0iDYKx{;8ORq*fth)2hg@Dl)u^**cXuZ{XjnvUcU)ud>GTN2;tk zrhXDrHIU@Vr)K?@N z7<+NY#Ao)t+~dJNei~L?)@kGY%6`MFZ_kcge|Xi~bJuKWti8GGv(WwT?z*_<>s`g~ ze*bpXrkHo_NBTwXIX?9Kkmb|YJX`&MoBQmMA2rj3-jWVlOG=VJB>A^d!O{2A(qyH^r+5-n$@!&S*&^U=|c@EJ5~*`I^Sv9 zbD-NpGY-voe#OzHc~u|oXiE5x^3Z-)qDNV^iz`#zAX4%I&Jae<`;~k#)S$3ntQ z9QHp}dU720KUIoWo58mpe4<5HIjyymt5f>GlS;BXfAFB(|3kb@tVf| zOR>B^P)v+WHy#+zFC9ChsmvchNRe7Uo2wI&=$SMMs;K^8uyExp^fVaIajrnAsO5PM zYWZvjwftTOwS2CFT0Y-FEiZIX%ZnY##iGAhR6&qYlgC4pQLYA8}Y7bC7%o|4WBu zorB~#B>!MJ*kd}g-Gmvv2bOAN`{k^!IA)zIsxTJY85#yQdOHrCHFyxJNSY|oMNC!W{VJUEs5e|O1!;Pa(9K6wCneQM*5M?_oMGlhf;IkZ-haDu#!B2Hq9&(VW4t|QmQs^L4 zKxA@QmO6;Z!Ow75mN>`^2S43mS?nOw9sD$hrPx8HfoP_~vcf@TI(XJ$S?(aLgU@wX zmN`hSgU@kTEDn<6Fi+Jy6!B2xLs2E6E=~9bL}O^wfJp4tX#j}ApIC`9yP~pPkqF!f zge|^yE&>hnMvm!?9Okne=7Sv52Q(qw0dGf%SU&-!kyrrtvy16lvH>!we38p%W5=gC z&@POlnyF$@q&p3hFDj>v7Q1Z*`)5*0?9wUX0ekN~O*mO_nUhjL;M`h=4&15W5AN|U z{SLQ>J@fYXhJf3sDhY3T)Hj)eSZS0vxCa9RMwvGq(Qi3YZoTXHHyy=6KyeDo z8xEW!gH+Q8ent#0Bcc57=I>zJ-z|ckT0=)xX@6IA&sAi4%LNy%-pNDsSQs4|8R05k z57-@e4@b)+jffqk(OPM%#6b{=UO}GP!}oD|dt#WyL*w~!kA1_QCR6529l*tXa8ksy za3)8e^aPuN>iPnMB6Qs<$akhhq-c{UsNe^0B~wCQ2URugs{VczC=R9yl0}#*;Nwn7 z_jP8_%nn8EV4*$HNsDm_j_Tv2)O0PR;Jg1JC-^h_mwWikJa4jX75&=>nR)Ksb(8a~ zEp=1!tgTxg%d6~O+u~Hrg{!{YQ>myo-Je$(x>>kE#l3^WH4yjXOgtw!@i~AKE96c# zK5y&4TmAz|+Y){*){n3m) zlioDdVrD+i!(UQTY)_-RXHKSe$jX%MX;^st;0|H*`6b$P5t{bn{=Fjjs#yXIx#%O) zN_=VS{1i}=_NV=OOTvV(&Dtw;KM`BXca&uv?ixbQ=aXysL{&gy3rNobGPr<@DIik{ zTtmq>`Q&;&2`(U=3P`U4GNgc{7m%q1t`u@1pZt_h+VV+Q0f{Rh2?eB20ZAz!BML}H z0m&{PIR&m%(wt9P@`<8=s0&D!0@AI3Bo>g=0y46Ij4L413P^52W$(JSw(5J^+WHT- zj@U9~M0r~q0OE7=KNq7d#l8n3>l~IYKfy&QQtGtPqrXGgk5ipeDf%<@@q{kbX{gx8ted_<&mxKr^~ z*k14OI>iprHdWTe$6%9 zn{(h+QDWK2xhh^PJ3-k8JA-`XE$L0AUDWX?{{RqSSuJ~_Bt*oR$;#RM%GqSqY_z%6 zv!hqdPFX#hUo{)H@~daVRtZ8awn)(PnU(;Ag#K9~v9|~ekRft_thILG9<#>7LB2p) zFb9DmMM?KLq|Y2NK6=a{iF2gL=r@N9l7Ri^kim2Cr;qT?t}Cj!SSiN^qU{%R%F-`9 zysx@8t|az`xUo~)^@g~wV~Ut_Ls5caQNVUuu%^tE^O>S=d)zUGI7ML(Z~uKG4{VVm zsZ6*2TotORM2i)dJHj7SG^XTm_;j`y4!sc>FT$fz;k;w1r3Y%`MD0-R8mYndkS}PP zXbRgycNFXku9b6c3oa*4Pk{*y4=;d4FM1a`jo5$MH?m!Ldy7!tJYOZKZhlim8>aKm zG97{jdsIzTp7Wv&=c`K8AOYRCA{|7nKVM}J1n(P31INZAsQWRUX4BAuh+6_M(iwE~ zT$Lb3*bltgM=0t-fbya)0-=#kRRCeQj}UlBJYU6sLyaQeP{QDL!du&zD~+7H*ENhZ z-OCNMrg3|%!KP%cXbt(lFa1bVm~^zhGv23k`fAeHk`7Zf1rlPRJ0Ly!yV zfZ7j~>*v%-)ILi<-O&=*8^WBwHvlfN9Bo%{w1Wb$XaZS|!dw^3{mWY70$CoGOBld% zN0H~+tvGkP6~hBXo|CNz1Is?iGhtxa7vLEfz9^6-3@rO@HT;eu1FXEoSNkQE{JDFr zX?)YYqEhraB;-E7JAJHcW}ghXPiQhL_@jH1ual#D`LlbIpTdVXXZM=L*rAnqE>H9! zO@5xohq{uVBB3sh7MzCD!?G_>is$4c7!=lYuD5k8_TlyW#(IwK?XUL+amwl`>6_A# zXv?+^7vqrz2z5H@$u*o=lwSmRmh92z0^m%-{ZsYAYupB_{>WG@@A@wZ@kv9<@@9OVSs2$L$f;SFM%KQu3($53%BZ6OWspPdoGd#GC>m1KNLr7OEvu#W zcBFbvrh?k%Bmq7uTH&&tw2f04K9H|ea`PVu6bDVCobNeNH^lml^C4M-<`V|uQd?Cd&kx$lDFn!ApPxJ z?wf($niDxeV)?A&%Ggo?*@;1$;U+~QJy-Im|uD7f2RmwYaO=bAPmExk;k zQ1NBT!MU!nxPpOZ_04wm%Xan8b`7v5x&~VNx{|E@T*=n{%-atXS^F1R z`xROH7FiQZ2LDCkFK!_=GhaZy_b;;?zm=tuK^1hFN2@?VM$g0Abj&3vOQ?MSE+&qvzpV5LRO#EMGBS3Z%cmt4!=%=EVmwpCx}blh4eq0oksB*__up zfHPYMGSv^jcZu*_U+nn~;O2`UE=^AU_5r*|Vdrx*{rMv6z#{8_=0BV90`D_3#3VvY zU*>bnfWwE2t6)8-@CubO^4k583k`BXsGa!J%38)-(H&vQ2Ll_6&6&FL3{madO;*fghP? zEQ$Z{(w9RfWnF*k?3smo4VNBOt$%vxw<9XA>=`@d)3QUQ?CD8g-R$<%F59_pr#}(1 zyX)YSBYv@D*n=lO_;*9mn0NB7Qrqrc|93rr)}Mr?j=yf8MPdOGOvxf9eV#Pra4Y1V zuo@!<9gCzsX94=gD-Q?}mFVZnS1iQ9LAk{42q>{bS%3~jK;O6!U9oc!=Y|}L640S6 z1j;L;=0A8FN(1eATt0y-WPxy$k84E- zfk1g>foZZCK7lY^D|GE+=U9t~X$XrxcpyyGpzrZ!K16Rqd#{W!g2(s~l zTrRiFrNnUW1x}8(0T^qv3&n7|Mu(t< zYeg%&Lymx-2}H_8QEpft81$%>G@xi8(t=TxPgi?g(pJ>nS9>&r{k%m1t;%2LKHmh` zB*==j9jXZCx`Cq3_Mv9Bm&9kTS9W%QK&@+Vg1Iwg)Jz$*LKcW{@NumOK+#4Y zz(rBC(FX|hnW2@rX0}@>$i}Ay2$UPW&kUcg5^%}7C>N~;FSw|$-7?C#$SnHPy+E4Y z3dNX-ept*xN*8*8Yrr&d4G5BsYe0a0ZVi|w_||}Fwe4#_dES`X zK5rB&?b9W+(o*GR;m1qRJU&>0yl+|fhb8>SOLUe$ERmLlf27&;3x8PRTNeItN&B+! zKbFYL!cUf%Dv*|zg%m0-3qM)nUlxA4#PT0Y?4K?<=0WNcnv#}Ouqar)AuvzgH&Hwj$AaEsmhxAQ(1u7bK2fQD2bYafZSH=we z;+}3%uV%aVKla^s35$=vpS;>naOXrv`K`Yhmb(9|3Fp2pvxO`58-o7wrf`;N>ROSYtZXdh&)zwq&QgTAkR?pWg6&xee$)gJA2nc>$g>zT3O`Te`f z?!9I_^~|h6>9d0_&3$Kf@>2b*2h&$QqWRtE_{Qm_1BNW0ySwb29XmctzJ8^rC#fRt ziSz$Ctn+K$*AHsUwI_Q%`LzA9%$`q_d=owIzqdB-YP;)!f2{|&9a!kEpez1Ea)Nng z4fB*mUh#h<4`MYK?4=by9lus#n7h6hEBaU=ms91B)quDJk1qC2*^IBiwGw9dbOl)WU(=NcfolSa9ANy8+l^l!8^7Ro<9+4w3H%7Rz8Wfbu!YS7e_zP_9EV*WXaC{B|GagNynwUoMwV7s}->c19nXRWj-hPTT<$jnP+8Fd9<6f}zYlPkkk$ ze7b;=%S_AF-B)`wqcXj^CZJXM==|me*yMBD9c+3PHc@B$P>CocZ5(j6&)glbi8jOl zfrsRh_$t+i?)CrGU`2Wt&@TyNK)gEn=5`4)V{aQ-}bJoliJ>YL+StZ z{{5fSApMFbzj+@Te(V#b*Vl7zpbGYh8|q%YFC*^UVH*o?^xXZqZ`vtzzDyv6%5>io z_!hK$kq?P|B;U?G_c&^FKPtQW0ZQ;INfe;{0PT;Bem~*Mw-Cx!-HGr6KcUDgZ8S<7 zan%oSbdQOG(m+M`<^X!xngM0wnAOG3d7c#u?ceUlF*B|^2={|f+fFz`1_khb5Xk%O ze!F`CJ-pUwG0v_*l6E0-NR}+Lr_m&~?|>7{@@>~jUMv~V5eTa!$KZA6%aUHx{`G#U zH{hUsVg*QlPt{j5=&}Q@ja0bji0KZCv9jhsTkV=#Y(RoXw!z6%cPhGQ^dqxJWY6s1Ls}G=>3fJLN z@Q+s6Up~-ussx{^`zk7c+gDKm+>ZxxzkI;{>PnHD>Gzm3l}ekmidI#sCup^O6-}bw z1x_qK?VH;DgsSp&STd0Fl^I(MRPzCAUsL{5ekxy89a=0^72ZO(i2e5X4fXg5IPq*Q zlK3&W46YvaH~4H@Hg>L(+vGdXjDlnOT z_Y@N87c#hNAjvq6AIEv4@H;$j?5l4&IgT^E{}fdtn0-3FxlKpwU)9ll!<}_}BkM@s zKtK4VWs^QV7n5l*Bw-`IiRiXb1~xp6KVkli7aw#00kCM2OhV7d21!4DCxW|er1mCk zq;QoCN7|CGkshSAblYfcH8T@8Fw-}f92>}c8*J}yc=pET+Lk}HwF#m4)jWFY6z}-I z!Xa9af>Lz)_s@XA)k3;&bhVNm8*c%pyIL-E-$;4_b)m;b(rcrR@3|5B0O5OWghmPy zEg%=45ltYMo}m&dg>Xn^92-nCo|Y52Pc#3v;g)}a2Hc`n7I|3~c|{Z%ih*6UmAmSE zrZS9qeS@HZ0?OEqY6VIS6#l+)uybQ&2u*eY8{GTlM$SN<+KA%=r#70-J&ma-AZ7|l z1BAc)3=Ya(c}DUJ{aHCtS9|q|K^iQ=rK;mkZM5E`(DL3J>)*SH-}!U36yB7oWuJ(! z_ukke-F1?<~FB`6NW*XvQLf=Qi>0iGHmeE!HPsN6RZl7pQ``KLHVxkyB@gf zfx8~K>w&u-_$PP(KNN_cp^U&t`|uY0IH&l-mdnI@5xtWrB0i$UhcHXb#7HnLsmC7* zZy~V+A5@o-PGoCm!W3^Qp`!TCL}$?wYY>jX#t^H@6-45QOGUbny+KrLS4jkZ^_tEU z`B;LPK_`O0BZnUu03Xp2WWh-ih^zHP0ug`9cIWf1mJ>Px1v{S?=OlFUv-Nq*3>(7y zsX_kKCVwj1pE~JJjrXVMG((a?{|KCvYL~3`hT#`nOfr0s@Ca46Nmx!F*+LP0oD}BM z$9UmB`cMf4^q~|?^bv+{aTR3qKFqx^Kd;+YZ~C=p@L*vmML#VJppRgoH+|4wJH;bW zpg-FJp0suzY2IVnv<1KTpev+5=B+4fr9UPqxhGsWC;G;H3j8W>ILObQZ8f<>T~FhO zE}rqml5V8l)I=e^l$;bb@28rBg-Xd}*#1Z|) z;cvF`ZTJaQDWxbEXL*1r%OyxqIq6QspJOjab$2J$o1$x&g-?}GUivGe6FCNFk8xB> zJoW7PZmW0_xeu0i3bNaQ1s{HN&VjSZy>N6Hf0_dy_6pR5S48vp6)n3uE9Z0vJzUYo=4<;MQ?OJaU=&yA{S z-d|5VS2FfANk6?UqiJX6#Y(pA-^{Uxf0^{>ZJF2kg$crQ6R)|8`+i`m(A^a-u>vrFrj?m(Cde{-Jf*z@OE=*+wUt^W_D?MbKAo7sMB>N z4^NEuHdL*@XGF}m^{$Qc4ZYjGIJjqHVfNsd%b$9xezoEk69;O4JbmKzs%?)K&CVRg z#I${X>chjY9oV&P<66tYduL`%9FvyZx2LXiOoTc_rBt*jCQh6%Vf^@U<1#Yz`mtlv z)5nY%J$lrrQ6oo=7%_bKuwiLwX{o6xDMN=29WrF_;K73iB_}5*B@G-naKM26{rmUp z*SBxq#Kgotefls=@7}$8_3G8LXU`rzdUWsJy<4|#2?+^0T=DVoUAuPef-5dAt~0Jq zxMFc>wb~eZMdOOX6-loMT=?6G8cjI8)Z!H;UZLU@B3;4KrSe@t{!1Bn{gsezQw%Vz zDtr5DP1>9%ob}Cddh@1NKDyp(^5T+z|12mecTL5?vysE^d2;VDZ`W}PHrM>&CuZu> z9j`YA51RSNONW}GN8Yz#-|XccJRUY}SQ~HC?RtrD$m{er@?SKA{>z5oPyG-_>9Y#jd>cmBB7MEDdF{o~O(VE9agP>C<8VYq8bP=aOY}li;X-)P3xP&2gdDvPe7F#W^kSq7fk`ifC@#b*z4}QP!WY+H ziI2PJyB@gffx8~K>w&u-_`m6aH*Yx#`G2#FcT08G19v@e*8_JwaMuI>bPq89bcuHb v-t|BS4_vtOVI3dzsSf0Kv;WN=2>)-^hmVr~)f)fh3jWmvxGVo(?ScOX2IFR| diff --git a/build/labels.txt b/build/labels.txt index fe04bb2..4b3a7d5 100644 --- a/build/labels.txt +++ b/build/labels.txt @@ -44,7 +44,6 @@ al C:0020 .tls_rec_idx al C:0001 .TLS_MAX_FRAG_512 al C:0400 .screen_ram al C:d414 .sid_v3_sr -al C:0014 .TLS_CT_CHANGE_CIPHER al C:ffc9 .chkout al C:2031 .ip65_vt_tcp_snd_len al C:df00 .reu_status @@ -85,14 +84,12 @@ al C:200c .ip65_tcp_connect al C:0003 .zp_tmp2 al C:0002 .zp_tmp1 al C:0014 .cc20_round -al C:7a00 .sqtab_hi al C:000f .TLS_HS_CERT_VERIFY al C:003a .fp_mul_j al C:2006 .ip65_dhcp_init al C:0039 .fp_mul_i al C:001a .ip65_zp_size al C:0019 .cc20_buf_pos -al C:7800 .sqtab_lo al C:201e .ip65_set_tcp_dest al C:001c .poly_carry al C:003a .x25_bit_mask @@ -137,6 +134,7 @@ al C:0018 .cc20_remain al C:4f48 .ip65_tcp_snd_len al C:0002 .ip65_zp_start al C:df08 .reu_len_hi +al C:000b .http_host_foo_len al C:df02 .reu_c64_lo al C:0002 .TLS_HS_SERVER_HELLO al C:df07 .reu_len_lo @@ -153,589 +151,538 @@ al C:0021 .tls_direction al C:0033 .fe_loop al C:003b .ec_scalar_ptr al C:0014 .TLS_HS_FINISHED +al C:0014 .TLS_CT_CHANGE_CIPHER al C:201b .ip65_set_tcp_cb -al C:000d .http_host_apple_len al C:2003 .ip65_process al C:202f .ip65_vt_tcp_in_len -al C:9623 .tls_hs_write_iv -al C:672e .ec_point_add -al C:69d0 .ec_sc_byte -al C:80af .der_skip_tlv -al C:51bf .drbg_fill_bytes -al C:1e8a .lbl_derived -al C:4e5e .sha256_shr3 -al C:4e43 .sha256_rotr22 -al C:5353 .fe_mul -al C:4755 .sha256_h1_init -al C:83b0 .cert_sig_s -al C:4e52 .sha256_rotr25 -al C:5b19 .x25519_ladder_step -al C:831f .cert_pubkey -al C:8380 .cert_sig_r +al C:a828 .tls_hs_write_iv +al C:889a .ec_point_add +al C:8b3c .ec_sc_byte +al C:8f1f .der_skip_tlv +al C:732b .drbg_fill_bytes +al C:1f15 .lbl_derived +al C:6fca .sha256_shr3 +al C:6faf .sha256_rotr22 +al C:74bf .fe_mul +al C:68c1 .sha256_h1_init +al C:9220 .cert_sig_s +al C:6fbe .sha256_rotr25 +al C:7c85 .x25519_ladder_step +al C:918f .cert_pubkey +al C:91f0 .cert_sig_r al C:0a91 .menu_msg -al C:a3d7 .aead_scratch -al C:6c38 .fp_mod_add_384 -al C:51dd .fe_zero -al C:9a99 .hkdf_context_len -al C:51e7 .fe_one -al C:0ec2 .net_send_ptr +al C:b5dc .aead_scratch +al C:7349 .fe_zero +al C:ac9e .hkdf_context_len +al C:7353 .fe_one +al C:0ecd .net_send_ptr al C:0ab3 .init_msg -al C:965b .tls_app_write_key -al C:9300 .mul38_hi_tab -al C:a6ea .ecdsa_sig_s -al C:69d2 .ec_affine_x -al C:6bd4 .fp_b_byte_384 -al C:5dbe .fp_s_hi -al C:a6ba .ecdsa_sig_r -al C:4871 .sha256_init -al C:69f2 .ec_affine_y -al C:0e6f .cb_remaining -al C:4751 .sha256_h0_init -al C:5dc1 .fp_wide -al C:6973 .ec_scalar_mul -al C:1ece .tls_c_hs_secret -al C:143b .tls_record_send_plaintext -al C:8d36 .tls_ecdh_compute_shared -al C:7452 .ec_t6_384 -al C:46a3 .aead_compute_tag -al C:6ad0 .fp_add_384 -al C:5e94 .fp_mod_reduce -al C:6ac2 .fp_cmp_384 -al C:5f38 .fp_mod_mul -al C:147b .tls_build_client_hello -al C:475d .sha256_h3_init -al C:1de5 .tls_compute_finished -al C:9f7d .sha256_block -al C:42c8 .chacha20_encrypt -al C:1eee .tls_s_hs_secret -al C:0bee .send_ok_msg -al C:98fb .tls_hs_buf -al C:4093 .copy32 -al C:4062 .rotl32_12 -al C:a74b .ecdsa_pubkey_y -al C:a71b .ecdsa_pubkey_x -al C:5caa .fp_copy -al C:19ef .hkdf_expand_label -al C:95c3 .tls_transcript -al C:43e4 .sq_ad -al C:83e0 .cert_sig_len -al C:74b2 .ec_point_double_384 +al C:a860 .tls_app_write_key +al C:a100 .mul38_hi_tab +al C:b8ef .ecdsa_sig_s +al C:8b3e .ec_affine_x +al C:7f2a .fp_s_hi +al C:b8bf .ecdsa_sig_r +al C:69dd .sha256_init +al C:8b5e .ec_affine_y +al C:0e7a .cb_remaining +al C:68bd .sha256_h0_init +al C:7f2d .fp_wide +al C:8adf .ec_scalar_mul +al C:1f59 .tls_c_hs_secret +al C:14aa .tls_record_send_plaintext +al C:9ba6 .tls_ecdh_compute_shared +al C:680f .aead_compute_tag +al C:8000 .fp_mod_reduce +al C:80a4 .fp_mod_mul +al C:1505 .tls_build_client_hello +al C:68c9 .sha256_h3_init +al C:1e70 .tls_compute_finished +al C:b182 .sha256_block +al C:6434 .chacha20_encrypt +al C:1f79 .tls_s_hs_secret +al C:0bec .send_ok_msg +al C:ab00 .tls_hs_buf +al C:61ff .copy32 +al C:61ce .rotl32_12 +al C:b950 .ecdsa_pubkey_y +al C:b920 .ecdsa_pubkey_x +al C:7e16 .fp_copy +al C:1a79 .hkdf_expand_label +al C:a7c8 .tls_transcript +al C:6550 .sq_ad +al C:9250 .cert_sig_len al C:0aed .dhcp_msg -al C:6fa2 .fp_inv_x2_384 -al C:4759 .sha256_h2_init -al C:7db8 .ecdsa_verify_384 -al C:9a8d .hkdf_info_len +al C:68c5 .sha256_h2_init +al C:8dd1 .ecdsa_verify_384 +al C:ac92 .hkdf_info_len al C:0843 .main_loop -al C:7a24 .ec_jacobian_to_affine_384 -al C:0c1b .reu_mul_init -al C:0d42 .net_tcp_connect -al C:9f30 .input_length -al C:9b00 .http_path_len -al C:96c9 .tls_rec_len -al C:43e1 .sq_sh -al C:7720 .ec_point_add_384 -al C:79c2 .ec_sc_byte_384 -al C:831d .cert_tbs_len -al C:133a .tls_record_read -al C:468c .aead_setup_chacha -al C:6a12 .ec_jacobian_to_affine -al C:a668 .mul_src2_buf -al C:7422 .ec_t5_384 -al C:0fe0 .tls_recv_server_hello -al C:60db .fp_inv_iter -al C:a77b .ecdsa_verify_tmp -al C:46f2 .aead_process_padded -al C:a303 .cc20_key -al C:4765 .sha256_h5_init -al C:8d11 .cert_data_ptr -al C:9502 .tls_state -al C:a1e1 .drbg_seed -al C:6270 .ec_gx -al C:0f9d .tls_close -al C:9563 .tls_ecdhe_pubkey -al C:6290 .ec_gy -al C:192f .entropy_init -al C:3e94 .add32 -al C:83e2 .cert_buf -al C:6b28 .fp_mul_384 -al C:6f72 .fp_inv_x1_384 -al C:8c2e .tls_handle_cert_verify -al C:4761 .sha256_h4_init -al C:4c51 .sha256_ch +al C:0c19 .reu_mul_init +al C:0d3e .net_tcp_connect +al C:b135 .input_length +al C:ad05 .http_path_len +al C:a8ce .tls_rec_len +al C:654d .sq_sh +al C:918d .cert_tbs_len +al C:1376 .tls_record_read +al C:67f8 .aead_setup_chacha +al C:8b7e .ec_jacobian_to_affine +al C:b86d .mul_src2_buf +al C:0ff1 .tls_recv_server_hello +al C:8247 .fp_inv_iter +al C:b980 .ecdsa_verify_tmp +al C:685e .aead_process_padded +al C:b508 .cc20_key +al C:68d1 .sha256_h5_init +al C:9b81 .cert_data_ptr +al C:a702 .tls_state +al C:b3e6 .drbg_seed +al C:83dc .ec_gx +al C:0fae .tls_close +al C:a768 .tls_ecdhe_pubkey +al C:83fc .ec_gy +al C:19b9 .entropy_init +al C:6000 .add32 +al C:9252 .cert_buf +al C:a706 .tls_recv_poll_count +al C:9a9e .tls_handle_cert_verify +al C:68cd .sha256_h4_init +al C:6dbd .sha256_ch al C:3e7d .http_conn_hdr -al C:1f4e .tls_finished_key -al C:83e1 .cert_curve_id -al C:9c07 .http_resp_buf -al C:6d7e .fp_bm_384 -al C:8e00 .mul_dma_lo -al C:73f2 .ec_t4_384 -al C:7c00 .ecdsa_verify -al C:0c00 .failed_msg -al C:a243 .cc20_state -al C:476d .sha256_h7_init -al C:8d14 .cert_parse_pos -al C:4348 .sqtab_init -al C:8f00 .mul_dma_hi -al C:a3b5 .aead_nonce -al C:17ab .tls_transcript_block -al C:43ea .mul_8x8 -al C:9a93 .hkdf_ikm_len -al C:5a05 .fe_inv_sqr_cnt -al C:82f5 .oid_ec_pubkey -al C:9a94 .hkdf_label_ptr -al C:a3c7 .aead_tag -al C:a667 .mul_cached_a -al C:4769 .sha256_h6_init -al C:9c05 .http_req_len -al C:a0dd .sha256_len -al C:443a .poly1305_multiply -al C:0c08 .done_msg -al C:6ccb .fp_mod_reduce_384 -al C:6d7f .fp_mod_mul_384 -al C:9a9a .hkdf_out_len -al C:3fe0 .rotl32_8 -al C:a5c7 .x25_b -al C:6150 .fp_inv_x2 -al C:a4c7 .x25_scalar -al C:71b2 .ec_p1_384 -al C:73c2 .ec_t3_384 -al C:0e28 .net_tcp_recv_cb -al C:a5a7 .x25_a -al C:6130 .fp_inv_x1 -al C:a607 .x25_cb -al C:8d18 .cert_end_lo -al C:a84b .ev_u1_384 -al C:7032 .fp_r2_384 +al C:1fd9 .tls_finished_key +al C:9251 .cert_curve_id +al C:ae0c .http_resp_buf +al C:9c00 .mul_dma_lo +al C:8c1a .ecdsa_verify +al C:0bfe .failed_msg +al C:b448 .cc20_state +al C:68d9 .sha256_h7_init +al C:9b84 .cert_parse_pos +al C:64b4 .sqtab_init +al C:9d00 .mul_dma_hi +al C:b5ba .aead_nonce +al C:1835 .tls_transcript_block +al C:6556 .mul_8x8 +al C:ac98 .hkdf_ikm_len +al C:7b71 .fe_inv_sqr_cnt +al C:9165 .oid_ec_pubkey +al C:ac99 .hkdf_label_ptr +al C:b5cc .aead_tag +al C:b86c .mul_cached_a +al C:68d5 .sha256_h6_init +al C:ae0a .http_req_len +al C:b2e2 .sha256_len +al C:65a6 .poly1305_multiply +al C:0c06 .done_msg +al C:ac9f .hkdf_out_len +al C:614c .rotl32_8 +al C:b7cc .x25_b +al C:82bc .fp_inv_x2 +al C:b6cc .x25_scalar +al C:0e24 .net_tcp_recv_cb +al C:b7ac .x25_a +al C:829c .fp_inv_x1 +al C:b80c .x25_cb +al C:9b88 .cert_end_lo +al C:ba50 .ev_u1_384 al C:08b9 .print_string -al C:0d5b .net_set_tcp_dest -al C:a627 .x25_e -al C:8d19 .cert_end_hi -al C:3eb3 .add32_to_dst -al C:a242 .drbg_buf_idx -al C:0d17 .net_dhcp -al C:0ccf .reu_fetch_mul_row -al C:0ce3 .http_host_zimmers -al C:964f .tls_hs_read_iv -al C:3fbb .rotl32_1 -al C:5dbc .fp_a_byte -al C:a1c0 .hmac_data_len -al C:9200 .mul38_lo_tab -al C:8cec .cv_label -al C:5144 .extra_sid_lo -al C:4348 .fp_init_sqtab -al C:1e40 .tls_verify_finished -al C:a4a7 .fe_p -al C:809f .der_skip -al C:9543 .tls_ecdhe_privkey -al C:12fc .tls_record_write -al C:17ee .tls_transcript_save -al C:4003 .rotl32_4 -al C:a7eb .ev_point_save -al C:408d .rotl32_7 -al C:5145 .extra_sid_hi -al C:a221 .drbg_seed_len -al C:459e .poly1305_final -al C:5dbd .fp_b_byte -al C:1e9d .lbl_s_hs_traffic +al C:0d57 .net_set_tcp_dest +al C:b82c .x25_e +al C:9b89 .cert_end_hi +al C:601f .add32_to_dst +al C:b447 .drbg_buf_idx +al C:0d13 .net_dhcp +al C:0ccd .reu_fetch_mul_row +al C:0ce1 .http_host_zimmers +al C:a854 .tls_hs_read_iv +al C:6127 .rotl32_1 +al C:7f28 .fp_a_byte +al C:b3c5 .hmac_data_len +al C:a000 .mul38_lo_tab +al C:9b5c .cv_label +al C:72b0 .extra_sid_lo +al C:64b4 .fp_init_sqtab +al C:1ecb .tls_verify_finished +al C:b6ac .fe_p +al C:8f0f .der_skip +al C:a748 .tls_ecdhe_privkey +al C:1338 .tls_record_write +al C:1878 .tls_transcript_save +al C:616f .rotl32_4 +al C:b9f0 .ev_point_save +al C:61f9 .rotl32_7 +al C:72b1 .extra_sid_hi +al C:b426 .drbg_seed_len +al C:670a .poly1305_final +al C:a400 .sqtab_hi +al C:7f29 .fp_b_byte +al C:1f28 .lbl_s_hs_traffic al C:3b27 .http_get -al C:64c0 .ec_point_double -al C:7392 .ec_t2_384 -al C:1ec6 .lbl_finished -al C:5d11 .fp_mul -al C:0f46 .tls_send -al C:180e .tls_transcript_init -al C:1e91 .lbl_c_hs_traffic -al C:8d0e .cert_list_len_lo -al C:962f .tls_hs_read_key -al C:70c2 .ec_n_384 -al C:7062 .fp_r3_384 -al C:0e52 .cb_copy_byte -al C:1e6a .empty_hash -al C:5571 .mul38_in -al C:0fa3 .tls_send_client_hello -al C:a4e7 .x25_u +al C:862c .ec_point_double +al C:1f51 .lbl_finished +al C:7e7d .fp_mul +al C:0f57 .tls_send +al C:1898 .tls_transcript_init +al C:1f1c .lbl_c_hs_traffic +al C:9b7e .cert_list_len_lo +al C:a834 .tls_hs_read_key +al C:0e5d .cb_copy_byte +al C:1ef5 .empty_hash +al C:a200 .sqtab_lo +al C:a704 .tls_recv_progress +al C:76dd .mul38_in +al C:0fb4 .tls_send_client_hello +al C:b6ec .x25_u al C:0a42 .banner_msg -al C:6d86 .fp_mod_inv_384 -al C:8d0d .cert_list_len_hi +al C:9b7d .cert_list_len_hi al C:3e92 .http_bg_idx -al C:9e2c .tls_app_ptr -al C:175f .tls_parse_encrypted_extensions +al C:b031 .tls_app_ptr +al C:17e9 .tls_parse_encrypted_extensions al C:0acc .net_fail_msg -al C:0b76 .dns_fail_msg -al C:a3c6 .aead_data_len -al C:0cf3 .http_host_apple -al C:3ed2 .xor32 +al C:0b74 .dns_fail_msg +al C:b5cb .aead_data_len +al C:603e .xor32 al C:0b1d .no_net_msg -al C:0e09 .net_recv_ready +al C:0e05 .net_recv_ready al C:0b01 .dhcp_fail_msg -al C:a333 .poly_h -al C:44ab .poly1305_reduce -al C:4dba .sha256_rotr1 -al C:96c8 .tls_rec_type -al C:7362 .ec_t1_384 -al C:72d2 .ec_p3_384 -al C:4b05 .sha256_load_word -al C:4def .sha256_rotr2 -al C:1a59 .tls_derive_secret +al C:b538 .poly_h +al C:6617 .poly1305_reduce +al C:6f26 .sha256_rotr1 +al C:a8cd .tls_rec_type +al C:6c71 .sha256_load_word +al C:6f5b .sha256_rotr2 +al C:1ae3 .tls_derive_secret al C:0ae1 .net_ok_msg al C:0a2b .print_resp_body -al C:0b8a .dns_ok_msg -al C:6fd2 .fp_r0_384 -al C:17aa .tls_hostname_len -al C:502a .hmac_drbg_update -al C:7482 .ec_set_modp_384 -al C:3ef0 .xor32_in_place -al C:4df5 .sha256_rotr6 -al C:6b06 .fp_rshift1_384 -al C:9a8e .hkdf_salt_ptr -al C:430f .poly1305_clamp -al C:59e7 .fe_inv_dst -al C:0ba6 .tcp_ok_msg -al C:4e94 .hmac_sha256 -al C:4dfe .sha256_rotr7 -al C:9f71 .sha_temp3 -al C:a3c1 .aead_aad_ptr -al C:5c90 .x25519_base -al C:4ddc .sha256_rotr8 -al C:6af8 .fp_is_zero_384 -al C:0d03 .net_init -al C:5dc0 .fp_p_hi -al C:69d1 .ec_sc_mask -al C:9500 .tcp_recv_head -al C:5197 .drbg_random_byte -al C:837f .cert_pubkey_len -al C:7152 .ec_gx_384 -al C:80d1 .x509_parse_cert -al C:0bcb .tls_ok_msg -al C:0e6b .cb_done -al C:4dcb .sha256_rotl1 -al C:1828 .tls_transcript_update -al C:5dbf .fp_p_lo +al C:0b88 .dns_ok_msg +al C:1834 .tls_hostname_len +al C:7196 .hmac_drbg_update +al C:605c .xor32_in_place +al C:6f61 .sha256_rotr6 +al C:ac93 .hkdf_salt_ptr +al C:647b .poly1305_clamp +al C:7b53 .fe_inv_dst +al C:0ba4 .tcp_ok_msg +al C:7000 .hmac_sha256 +al C:6f6a .sha256_rotr7 +al C:b176 .sha_temp3 +al C:b5c6 .aead_aad_ptr +al C:7dfc .x25519_base +al C:6f48 .sha256_rotr8 +al C:0cff .net_init +al C:0cf1 .http_host_foo +al C:7f2c .fp_p_hi +al C:8b3d .ec_sc_mask +al C:a700 .tcp_recv_head +al C:7303 .drbg_random_byte +al C:91ef .cert_pubkey_len +al C:8f41 .x509_parse_cert +al C:0bc9 .tls_ok_msg +al C:0e76 .cb_done +al C:6f37 .sha256_rotl1 +al C:18b2 .tls_transcript_update +al C:a705 .tls_recv_sub_progress +al C:7f2b .fp_p_lo al C:0b0e .dhcp_ok_msg -al C:9603 .tls_hs_write_key -al C:7242 .ec_p2_384 -al C:465b .aead_derive_otk -al C:6b15 .fp_chk_one_384 +al C:a808 .tls_hs_write_key +al C:67c7 .aead_derive_otk al C:3e77 .http_host_hdr -al C:a87b .ev_u2_384 -al C:0eac .net_save_zp -al C:7002 .fp_r1_384 -al C:5a06 .x25519_clamp -al C:5cf4 .fp_is_zero -al C:48b1 .sha256_update -al C:8d10 .cert_data_len_lo -al C:473b .aead_verify_tag -al C:a354 .poly_s -al C:5146 .drbg_init_entropy -al C:a344 .poly_r -al C:4b14 .sha256_load_word_to_temp2 -al C:4c6d .sha256_maj +al C:ba80 .ev_u2_384 +al C:0eb7 .net_save_zp +al C:7b72 .x25519_clamp +al C:7e60 .fp_is_zero +al C:6a1d .sha256_update +al C:9b80 .cert_data_len_lo +al C:68a7 .aead_verify_tag +al C:b559 .poly_s +al C:72b2 .drbg_init_entropy +al C:b549 .poly_r +al C:6c80 .sha256_load_word_to_temp2 +al C:6dd9 .sha256_maj al C:0b3d .http_get_msg -al C:9afb .http_host_ptr -al C:8d0f .cert_data_len_hi -al C:0eb7 .net_restore_zp -al C:7182 .ec_gy_384 -al C:a93b .ev_der_int_len -al C:a283 .cc20_work -al C:4439 .mul_s_pg -al C:a93c .ev_der_copy_cnt +al C:ad00 .http_host_ptr +al C:9b7f .cert_data_len_hi +al C:0ec2 .net_restore_zp +al C:bb40 .ev_der_int_len +al C:b488 .cc20_work +al C:65a5 .mul_s_pg +al C:bb41 .ev_der_copy_cnt al C:0b5a .https_get_msg -al C:50f9 .hmac_drbg_instantiate -al C:9adb .tls_master_secret -al C:4b23 .sha256_add_temp2_to_temp1 -al C:4771 .sha256_k -al C:51f1 .fe_add -al C:89e4 .tls_handle_certificate -al C:4e04 .sha256_rotr11 -al C:a222 .drbg_output -al C:6f42 .fp_inv_v_384 -al C:43e8 .poly_prod_lo -al C:1a75 .tls_derive_handshake_keys -al C:495f .sha256_process_block -al C:4e10 .sha256_rotr13 -al C:176a .tls_hostname -al C:748b .ec_set_modn_384 -al C:8d1a .cert_bs_len -al C:9e0c .http_line_buf -al C:5f3f .fp_mod_inv -al C:43e9 .poly_prod_hi -al C:0d6b .net_tcp_send -al C:4548 .poly1305_update -al C:10ea .tls_select_keys +al C:7265 .hmac_drbg_instantiate +al C:ace0 .tls_master_secret +al C:6c8f .sha256_add_temp2_to_temp1 +al C:68dd .sha256_k +al C:735d .fe_add +al C:9854 .tls_handle_certificate +al C:6f70 .sha256_rotr11 +al C:b427 .drbg_output +al C:6554 .poly_prod_lo +al C:1aff .tls_derive_handshake_keys +al C:6acb .sha256_process_block +al C:6f7c .sha256_rotr13 +al C:17f4 .tls_hostname +al C:9b8a .cert_bs_len +al C:b011 .http_line_buf +al C:80ab .fp_mod_inv +al C:6555 .poly_prod_hi +al C:0d67 .net_tcp_send +al C:66b4 .poly1305_update +al C:1126 .tls_select_keys al C:3e90 .http_crlf -al C:4e1f .sha256_rotr17 -al C:4e28 .sha256_rotr18 -al C:4e34 .sha256_rotr19 -al C:5218 .fe_sub -al C:6bd3 .fp_a_byte_384 -al C:1260 .tls_record_decrypt -al C:831b .cert_tbs_ptr -al C:9afe .http_path_ptr -al C:96b3 .tls_write_seq -al C:0d23 .net_poll -al C:95e3 .tls_transcript_h0 -al C:5e01 .fp_mod_add -al C:4c91 .sha256_add_to_hash -al C:95e7 .tls_transcript_h1 -al C:a32f .cc20_counter -al C:95eb .tls_transcript_h2 -al C:9400 .tcp_recv_buf -al C:45f3 .aead_encrypt -al C:95ef .tls_transcript_h3 -al C:9a97 .hkdf_context_ptr -al C:a364 .poly_product -al C:9e0a .http_hdr_match -al C:95f3 .tls_transcript_h4 -al C:7122 .ec_b_384 -al C:99fb .tls_hs_len -al C:1e8a .empty_context -al C:57d3 .fe_inv -al C:95f7 .tls_transcript_h5 -al C:a647 .x25_basepoint -al C:5d02 .fp_rshift1 -al C:95fb .tls_transcript_h6 -al C:0e71 .net_init_cb_addrs -al C:95ff .tls_transcript_h7 +al C:6f8b .sha256_rotr17 +al C:6f94 .sha256_rotr18 +al C:6fa0 .sha256_rotr19 +al C:7384 .fe_sub +al C:129c .tls_record_decrypt +al C:918b .cert_tbs_ptr +al C:ad03 .http_path_ptr +al C:a8b8 .tls_write_seq +al C:0d1f .net_poll +al C:a7e8 .tls_transcript_h0 +al C:7f6d .fp_mod_add +al C:6dfd .sha256_add_to_hash +al C:a7ec .tls_transcript_h1 +al C:b534 .cc20_counter +al C:a7f0 .tls_transcript_h2 +al C:a600 .tcp_recv_buf +al C:675f .aead_encrypt +al C:a7f4 .tls_transcript_h3 +al C:ac9c .hkdf_context_ptr +al C:b569 .poly_product +al C:b00f .http_hdr_match +al C:a7f8 .tls_transcript_h4 +al C:ac00 .tls_hs_len +al C:1f15 .empty_context +al C:793f .fe_inv +al C:a7fc .tls_transcript_h5 +al C:b84c .x25_basepoint +al C:7e6e .fp_rshift1 +al C:a800 .tls_transcript_h6 +al C:0e7c .net_init_cb_addrs +al C:a804 .tls_transcript_h7 al C:0879 .do_net_init -al C:0ccd .reu_init_a -al C:a447 .fe_tmp2 -al C:5574 .fe_sqr -al C:a467 .fe_tmp3 -al C:a71a .ecdsa_sig_len -al C:a2c3 .cc20_keystream -al C:5e61 .fp_mod_sub -al C:0b92 .tcp_fail_msg -al C:a427 .fe_tmp1 -al C:0cce .reu_init_b -al C:0ec4 .net_send_len -al C:1644 .tls_parse_server_hello -al C:9fbd .sha256_w -al C:5f15 .fp_rem -al C:96cb .tls_rec_buf -al C:136d .tls_recv_record -al C:99fd .hkdf_prk -al C:a487 .fe_tmp4 -al C:1ec1 .lbl_key -al C:6aae .fp_copy_384 -al C:546f .fe_reduce_wide -al C:a547 .x25_z2 -al C:a567 .x25_x3 -al C:9a3d .hkdf_info_buf -al C:a527 .x25_x2 -al C:a587 .x25_z3 +al C:0ccb .reu_init_a +al C:b64c .fe_tmp2 +al C:76e0 .fe_sqr +al C:b66c .fe_tmp3 +al C:b91f .ecdsa_sig_len +al C:b4c8 .cc20_keystream +al C:7fcd .fp_mod_sub +al C:0b90 .tcp_fail_msg +al C:b62c .fe_tmp1 +al C:0ccc .reu_init_b +al C:0ecf .net_send_len +al C:16ce .tls_parse_server_hello +al C:b1c2 .sha256_w +al C:8081 .fp_rem +al C:a8d0 .tls_rec_buf +al C:13a9 .tls_recv_record +al C:ac02 .hkdf_prk +al C:b68c .fe_tmp4 +al C:1f4c .lbl_key +al C:75db .fe_reduce_wide +al C:b74c .x25_z2 +al C:b76c .x25_x3 +al C:ac42 .hkdf_info_buf +al C:b72c .x25_x2 +al C:b78c .x25_z3 al C:08ca .do_http_get -al C:1479 .tls_recv_count -al C:0d2d .net_dns_resolve -al C:3f33 .rotr32_8 -al C:1889 .tls_transcript_hash -al C:9100 .sqtab2_hi -al C:a0df .hmac_key -al C:0bfc .ok_msg -al C:103a .tls_recv_encrypted -al C:a11f .hmac_opad_block -al C:a3e7 .fe_wide -al C:70f2 .ec_a_384 -al C:9f6d .sha_h -al C:9000 .sqtab2_lo -al C:8d1b .cv_sig_len -al C:9687 .tls_app_read_key -al C:6c98 .fp_mod_sub_384 -al C:6490 .ec_set_modp -al C:0ec6 .tls_connect -al C:9e09 .http_parse_state -al C:5115 .hmac_drbg_generate -al C:5143 .extra_sid_count -al C:9f61 .sha_e -al C:9a96 .hkdf_label_len -al C:4068 .rotr32_1 -al C:9f5d .sha_d -al C:0e39 .cb_load_ptr_lo -al C:9a91 .hkdf_ikm_ptr -al C:9f69 .sha_g -al C:9f65 .sha_f -al C:61f0 .ec_p -al C:9f51 .sha_a -al C:3f59 .rotr32_4 -al C:4437 .mul_a -al C:a507 .x25_result -al C:1461 .tls_record_recv_and_decrypt -al C:96a7 .tls_app_read_iv -al C:0e3f .cb_load_ptr_hi -al C:4438 .mul_b -al C:56cc .fe_mul_a24 -al C:9f59 .sha_c -al C:a689 .ecdsa_hash -al C:89e2 .cert_buf_len -al C:803f .der_read_tag -al C:17eb .tls_transcript_block_len -al C:5cb4 .fp_zero -al C:79c4 .ec_affine_x_384 -al C:9f55 .sha_b -al C:3fb8 .rotr32_7 -al C:4e77 .sha256_shr10 -al C:17ec .tls_transcript_total_lo -al C:51d3 .fe_copy -al C:6f12 .fp_inv_u_384 -al C:8319 .der_len -al C:4927 .sha256_final -al C:1a6f .hkdf_tls13_prefix -al C:40e9 .chacha20_init -al C:524e .fe_reduce_final -al C:6ae3 .fp_sub_384 -al C:82fc .oid_prime256v1 -al C:9e07 .http_resp_len +al C:1503 .tls_recv_count +al C:0d29 .net_dns_resolve +al C:609f .rotr32_8 +al C:1913 .tls_transcript_hash +al C:9f00 .sqtab2_hi +al C:b2e4 .hmac_key +al C:0bfa .ok_msg +al C:1076 .tls_recv_encrypted +al C:b324 .hmac_opad_block +al C:b5ec .fe_wide +al C:b172 .sha_h +al C:9e00 .sqtab2_lo +al C:9b8b .cv_sig_len +al C:a88c .tls_app_read_key +al C:85fc .ec_set_modp +al C:0ed1 .tls_connect +al C:b00e .http_parse_state +al C:7281 .hmac_drbg_generate +al C:72af .extra_sid_count +al C:b166 .sha_e +al C:ac9b .hkdf_label_len +al C:61d4 .rotr32_1 +al C:b162 .sha_d +al C:0e35 .cb_load_ptr_lo +al C:ac96 .hkdf_ikm_ptr +al C:b16e .sha_g +al C:b16a .sha_f +al C:835c .ec_p +al C:b156 .sha_a +al C:60c5 .rotr32_4 +al C:65a3 .mul_a +al C:b70c .x25_result +al C:14d0 .tls_record_recv_and_decrypt +al C:a8ac .tls_app_read_iv +al C:0e3b .cb_load_ptr_hi +al C:65a4 .mul_b +al C:7838 .fe_mul_a24 +al C:b15e .sha_c +al C:b88e .ecdsa_hash +al C:9852 .cert_buf_len +al C:8eaf .der_read_tag +al C:1875 .tls_transcript_block_len +al C:7e20 .fp_zero +al C:b15a .sha_b +al C:6124 .rotr32_7 +al C:6fe3 .sha256_shr10 +al C:1876 .tls_transcript_total_lo +al C:733f .fe_copy +al C:9189 .der_len +al C:6a93 .sha256_final +al C:1af9 .hkdf_tls13_prefix +al C:6255 .chacha20_init +al C:73ba .fe_reduce_final +al C:916c .oid_prime256v1 +al C:b00c .http_resp_len al C:3e68 .http_get_verb -al C:5512 .mul_by_38 -al C:61b0 .fp_r2 -al C:17ed .tls_transcript_total_hi -al C:5a19 .x25519_scalarmult -al C:61d0 .fp_r3 -al C:6210 .ec_n -al C:7092 .ec_p_384 -al C:6170 .fp_r0 +al C:767e .mul_by_38 +al C:831c .fp_r2 +al C:1877 .tls_transcript_total_hi +al C:7b85 .x25519_scalarmult +al C:833c .fp_r3 +al C:837c .ec_n +al C:82dc .fp_r0 al C:3dfc .http_get_plain -al C:6190 .fp_r1 -al C:9503 .tls_client_random -al C:6499 .ec_set_modn -al C:1196 .tls_seq_increment -al C:6bd7 .fp_p_hi_384 -al C:79c3 .ec_sc_mask_384 -al C:7f63 .ecdsa_parse_der_sig -al C:6250 .ec_b -al C:0d01 .http_path_root -al C:6230 .ec_a -al C:96bb .tls_read_seq -al C:40a9 .zero32 -al C:9b05 .http_req_buf -al C:9b01 .http_port -al C:6bd5 .fp_s_hi_384 -al C:79f4 .ec_affine_y_384 -al C:9e30 .input_buffer +al C:82fc .fp_r1 +al C:a708 .tls_client_random +al C:8605 .ec_set_modn +al C:11d2 .tls_seq_increment +al C:8dd3 .ecdsa_parse_der_sig +al C:83bc .ec_b +al C:0cfd .http_path_root +al C:839c .ec_a +al C:a8c0 .tls_read_seq +al C:6215 .zero32 +al C:ad0a .http_req_buf +al C:ad06 .http_port +al C:b035 .input_buffer al C:3e93 .http_bg_src -al C:5cbe .fp_cmp -al C:a1c1 .hmac_result -al C:a5e7 .x25_da -al C:a7ab .ev_u1 +al C:7e2a .fp_cmp +al C:b3c6 .hmac_result +al C:b7ec .x25_da +al C:b9b0 .ev_u1 al C:3c03 .http_build_get -al C:8d13 .cert_data_offset -al C:6d4c .fp_rem_384 -al C:0e15 .net_recv_byte -al C:a6b9 .ecdsa_hash_len -al C:0bdd .send_fail_msg -al C:a385 .poly1305_tag -al C:a7cb .ev_u2 -al C:a3c4 .aead_data_ptr -al C:425b .chacha20_block -al C:a323 .cc20_nonce -al C:1f2e .tls_verify_data -al C:a15f .hmac_data_buf -al C:4b3d .sha256_sig0 -al C:8a69 .x509_extract_pubkey -al C:59e9 .fe_inv_sqrn_tmp2 -al C:9e2e .tls_app_len -al C:4b82 .sha256_sig1 -al C:0d8f .net_tcp_close -al C:64a2 .ec_mulp -al C:4c0c .sha256_big_sig1 -al C:4622 .aead_decrypt -al C:4bc7 .sha256_big_sig0 -al C:6bd8 .fp_wide_384 -al C:8d5e .zp_save_buf -al C:11ae .tls_record_encrypt -al C:9523 .tls_server_random -al C:9a1d .hkdf_okm -al C:6d7d .fp_bc_384 -al C:5ccc .fp_add -al C:9e0b .http_line_idx -al C:1162 .tls_build_nonce -al C:9583 .tls_server_pubkey -al C:5573 .mul38_hi -al C:19b0 .hkdf_expand -al C:7494 .ec_mulp_384 -al C:1ea9 .lbl_c_ap_traffic -al C:1eb5 .lbl_s_ap_traffic -al C:5572 .mul38_lo -al C:0e28 .cb_load_len_lo -al C:7c11 .ecdsa_verify_256 -al C:10b2 .tls_send_finished -al C:133e .tls_enc_aead_len -al C:0bb5 .tls_fail_msg -al C:1c3f .tls_derive_traffic_keys -al C:80b8 .der_match_oid -al C:8d1c .tls_ecdh_generate_keypair -al C:96c3 .tls_rec_header -al C:1478 .tls_recv_state -al C:a8ab .ev_point_save_384 -al C:5cdf .fp_sub -al C:0e2e .cb_load_len_hi +al C:9b83 .cert_data_offset +al C:0e11 .net_recv_byte +al C:b8be .ecdsa_hash_len +al C:0bdb .send_fail_msg +al C:b58a .poly1305_tag +al C:b9d0 .ev_u2 +al C:b5c9 .aead_data_ptr +al C:63c7 .chacha20_block +al C:b528 .cc20_nonce +al C:1fb9 .tls_verify_data +al C:b364 .hmac_data_buf +al C:6ca9 .sha256_sig0 +al C:98d9 .x509_extract_pubkey +al C:7b55 .fe_inv_sqrn_tmp2 +al C:b033 .tls_app_len +al C:6cee .sha256_sig1 +al C:0d8b .net_tcp_close +al C:860e .ec_mulp +al C:6d78 .sha256_big_sig1 +al C:678e .aead_decrypt +al C:6d33 .sha256_big_sig0 +al C:9bce .zp_save_buf +al C:11ea .tls_record_encrypt +al C:a728 .tls_server_random +al C:ac22 .hkdf_okm +al C:7e38 .fp_add +al C:b010 .http_line_idx +al C:119e .tls_build_nonce +al C:a788 .tls_server_pubkey +al C:76df .mul38_hi +al C:1a3a .hkdf_expand +al C:1f34 .lbl_c_ap_traffic +al C:1f40 .lbl_s_ap_traffic +al C:76de .mul38_lo +al C:0e24 .cb_load_len_lo +al C:8c2a .ecdsa_verify_256 +al C:10ee .tls_send_finished +al C:137a .tls_enc_aead_len +al C:0bb3 .tls_fail_msg +al C:1cca .tls_derive_traffic_keys +al C:8f28 .der_match_oid +al C:9b8c .tls_ecdh_generate_keypair +al C:a8c8 .tls_rec_header +al C:1502 .tls_recv_state +al C:bab0 .ev_point_save_384 +al C:7e4b .fp_sub +al C:0e2a .cb_load_len_hi al C:3e6c .http_version -al C:1ec4 .lbl_iv +al C:1f4f .lbl_iv al C:08b8 .net_initialized -al C:98ef .tls_nonce -al C:a395 .aead_key -al C:40c9 .cc20_qr_table -al C:1f0e .tls_derived_tmp -al C:6470 .ec_t6 -al C:9a9b .tls_early_secret -al C:6450 .ec_t5 -al C:3f0e .rotr32_16 +al C:aaf4 .tls_nonce +al C:b59a .aead_key +al C:6235 .cc20_qr_table +al C:1f99 .tls_derived_tmp +al C:85dc .ec_t6 +al C:aca0 .tls_early_secret +al C:85bc .ec_t5 +al C:607a .rotr32_16 al C:3ce4 .http_recv_response -al C:5f37 .fp_bm -al C:6430 .ec_t4 -al C:62b0 .ec_p1 -al C:6410 .ec_t3 -al C:9501 .tcp_recv_tail -al C:63f0 .ec_t2 -al C:3f56 .rotr32_12 -al C:6370 .ec_p3 -al C:63d0 .ec_t1 -al C:5264 .fe_cswap -al C:0f74 .tls_recv -al C:60dd .fp_chk_one -al C:6310 .ec_p2 -al C:0d99 .net_print_ip -al C:8309 .oid_sha256_ecdsa -al C:6110 .fp_inv_v +al C:80a3 .fp_bm +al C:859c .ec_t4 +al C:841c .ec_p1 +al C:857c .ec_t3 +al C:a701 .tcp_recv_tail +al C:855c .ec_t2 +al C:60c2 .rotr32_12 +al C:84dc .ec_p3 +al C:853c .ec_t1 +al C:73d0 .fe_cswap +al C:0f85 .tls_recv +al C:8249 .fp_chk_one +al C:847c .ec_p2 +al C:0d95 .net_print_ip +al C:9179 .oid_sha256_ecdsa +al C:827c .fp_inv_v al C:091e .do_https_get -al C:4529 .poly1305_block -al C:95a3 .tls_shared_secret -al C:60f0 .fp_inv_u -al C:8304 .oid_secp384r1 -al C:9b03 .http_status -al C:9f75 .sha_t1 -al C:5f36 .fp_bc -al C:43de .sq_acc -al C:40b9 .cc20_constants -al C:9f79 .sha_t2 -al C:a688 .ecdsa_curve_id -al C:7965 .ec_scalar_mul_384 -al C:9afd .http_host_len -al C:9abb .tls_handshake_secret -al C:42fe .poly1305_init -al C:9f35 .sha256_h1 -al C:8d17 .cert_ext_len_lo -al C:9f31 .sha256_h0 -al C:967b .tls_app_write_iv -al C:9f3d .sha256_h3 -al C:a0ff .hmac_val -al C:4116 .chacha20_quarter_round -al C:9f39 .sha256_h2 -al C:1340 .tls_send_record -al C:43e6 .sq_i -al C:9f45 .sha256_h5 -al C:9a90 .hkdf_salt_len -al C:6ab8 .fp_zero_384 -al C:8d16 .cert_ext_len_hi -al C:9f41 .sha256_h4 -al C:9f4d .sha256_h7 -al C:9f49 .sha256_h6 -al C:a0bd .sha256_hash -al C:6bd6 .fp_p_lo_384 -al C:0e4a .cb_loop -al C:1456 .tls_record_send_encrypted -al C:a3c3 .aead_aad_len -al C:8311 .oid_sha384_ecdsa -al C:523a .fe_cmp_p -al C:1947 .hkdf_extract -al C:804a .der_read_length +al C:6695 .poly1305_block +al C:a7a8 .tls_shared_secret +al C:825c .fp_inv_u +al C:9174 .oid_secp384r1 +al C:ad08 .http_status +al C:b17a .sha_t1 +al C:80a2 .fp_bc +al C:654a .sq_acc +al C:6225 .cc20_constants +al C:b17e .sha_t2 +al C:b88d .ecdsa_curve_id +al C:ad02 .http_host_len +al C:acc0 .tls_handshake_secret +al C:646a .poly1305_init +al C:b13a .sha256_h1 +al C:9b87 .cert_ext_len_lo +al C:b136 .sha256_h0 +al C:a880 .tls_app_write_iv +al C:b142 .sha256_h3 +al C:b304 .hmac_val +al C:6282 .chacha20_quarter_round +al C:b13e .sha256_h2 +al C:137c .tls_send_record +al C:6552 .sq_i +al C:b14a .sha256_h5 +al C:ac95 .hkdf_salt_len +al C:9b86 .cert_ext_len_hi +al C:b146 .sha256_h4 +al C:b152 .sha256_h7 +al C:b14e .sha256_h6 +al C:a703 .tls_last_state +al C:b2c2 .sha256_hash +al C:0e55 .cb_loop +al C:14c5 .tls_record_send_encrypted +al C:b5c8 .aead_aad_len +al C:9181 .oid_sha384_ecdsa +al C:73a6 .fe_cmp_p +al C:19d1 .hkdf_extract +al C:8eba .der_read_length diff --git a/scripts/cleanup-bridge-tap.sh b/scripts/cleanup-bridge-tap.sh index fbf561f..12088f9 100755 --- a/scripts/cleanup-bridge-tap.sh +++ b/scripts/cleanup-bridge-tap.sh @@ -2,9 +2,14 @@ # cleanup-bridge-tap.sh -- Tear down the bridge+dnsmasq env set up by # setup-bridge-tap.sh. Idempotent -- safe to run if already torn down. # -# Kills any leftover VICE processes, tears down the br-c64 bridge and -# its tap-c64-0/tap-c64-1 interfaces, removes the iptables FORWARD rules, -# and cleans up stale /tmp/vice_eth_*.rc files. +# Tears down the br-c64 bridge and its tap-c64-0/tap-c64-1 interfaces, +# removes the iptables FORWARD rules, kills the project's dnsmasq, and +# cleans up stale /tmp/vice_eth_*.rc files. +# +# NOTE: Does NOT kill x64sc processes. Our test-owned VICE instances are +# managed per-instance by ViceProcess.stop() (see ViceInstanceManager / +# shutdown_vice()), and sibling projects on this host may have their own +# x64sc processes that MUST NOT be clobbered. # # Usage: # sudo ./scripts/cleanup-bridge-tap.sh @@ -20,30 +25,12 @@ DNSMASQ_PIDFILE="/tmp/c64-https-dnsmasq.pid" echo "=== c64-https bridge networking cleanup ===" echo -# --- 1. Kill any leftover x64sc processes ------------------------------------ -echo "[1/6] Killing any leftover x64sc processes..." -if pgrep -x x64sc > /dev/null 2>&1; then - pgrep -a x64sc | while read -r pid cmd; do - echo " killing PID $pid: $cmd" - done - pkill -TERM x64sc 2>/dev/null || true - sleep 1 - if pgrep -x x64sc > /dev/null 2>&1; then - pkill -KILL x64sc 2>/dev/null || true - sleep 1 - fi - if pgrep -x x64sc > /dev/null 2>&1; then - echo " WARNING: x64sc still running after SIGKILL" - else - echo " all x64sc processes killed" - fi -else - echo " no x64sc processes running" -fi +# --- 1. (skipped) x64sc kill -- managed per-instance by ViceProcess ---------- +echo "[1/5] (skipping x64sc kill -- managed per-instance by ViceProcess)" echo # --- 2. Kill dnsmasq (pidfile + /proc scan) ---------------------------------- -echo "[2/6] Killing dnsmasq processes..." +echo "[2/5] Killing dnsmasq processes..." found_dns=0 # 2a. Primary path: pidfile @@ -88,7 +75,7 @@ fi echo # --- 3. Remove iptables FORWARD rules ---------------------------------------- -echo "[3/6] Removing iptables FORWARD rules..." +echo "[3/5] Removing iptables FORWARD rules..." removed=0 for DEV in "$BRIDGE" "$TAP0" "$TAP1" "$TAP_LEGACY"; do if iptables -D FORWARD -i "$DEV" -j ACCEPT 2>/dev/null; then @@ -106,7 +93,7 @@ fi echo # --- 4. Tear down TAP interfaces and bridge ----------------------------------- -echo "[4/6] Tearing down TAP interfaces and bridge..." +echo "[4/5] Tearing down TAP interfaces and bridge..." for TAP_DEV in "$TAP0" "$TAP1" "$TAP_LEGACY"; do if ip link show "$TAP_DEV" > /dev/null 2>&1; then ip link set "$TAP_DEV" down 2>/dev/null || true @@ -135,7 +122,7 @@ fi echo # --- 5. Remove stale temp vicerc files ---------------------------------------- -echo "[5/6] Removing stale /tmp/vice_eth_*.rc files..." +echo "[5/5] Removing stale /tmp/vice_eth_*.rc files and final pidfile cleanup..." shopt -s nullglob rc_files=(/tmp/vice_eth_*.rc) if [[ ${#rc_files[@]} -gt 0 ]]; then @@ -148,8 +135,7 @@ fi shopt -u nullglob echo -# --- 6. Remove stale dnsmasq pidfile (if not already cleaned) ----------------- -echo "[6/6] Final pidfile cleanup..." +# --- 5b. Remove stale dnsmasq pidfile (if not already cleaned) ---------------- if [[ -f "$DNSMASQ_PIDFILE" ]]; then rm -f "$DNSMASQ_PIDFILE" echo " [removed] $DNSMASQ_PIDFILE" diff --git a/scripts/setup-bridge-tap.sh b/scripts/setup-bridge-tap.sh index c9a52a9..d690dbd 100755 --- a/scripts/setup-bridge-tap.sh +++ b/scripts/setup-bridge-tap.sh @@ -6,7 +6,7 @@ # FORWARD rules, and then starts a dnsmasq bound to br-c64 that: # - serves DHCP leases on 10.0.65.50-10.0.65.150 (1h) # - pushes default gw + DNS = 10.0.65.1 -# - overrides zimmers.net and apple.com to 10.0.65.1 +# - overrides zimmers.net and foo.bar to 10.0.65.1 # # Idempotent -- safe to run twice. Run via sudo. Pair with cleanup-bridge-tap.sh. # @@ -138,7 +138,7 @@ dnsmasq \ --dhcp-option=3,"$BRIDGE_IP" \ --dhcp-option=6,"$BRIDGE_IP" \ --address=/zimmers.net/"$BRIDGE_IP" \ - --address=/apple.com/"$BRIDGE_IP" \ + --address=/foo.bar/"$BRIDGE_IP" \ --log-queries \ --log-dhcp \ >>"$DNSMASQ_LOGFILE" 2>&1 & diff --git a/src/boot.asm b/src/boot.asm index a8e9cd9..38efe6c 100644 --- a/src/boot.asm +++ b/src/boot.asm @@ -240,11 +240,11 @@ do_https_get: jsr print_string ; --- set HTTP host/path/port --- - lda #http_host_apple + lda #>http_host_foo sta http_host_ptr+1 - lda #http_host_apple_len + lda #http_host_foo_len sta http_host_len lda #http_host_apple + lda #http_host_foo jsr net_dns_resolve bcc @dns_ok @@ -458,7 +458,7 @@ http_get_msg: !byte $0d, 0 https_get_msg: - !text "HTTPS GET WWW.APPLE.COM..." + !text "HTTPS GET WWW.FOO.BAR..." !byte $0d, 0 dns_fail_msg: @@ -636,10 +636,10 @@ http_host_zimmers: !byte 0 http_host_zimmers_len = 15 -http_host_apple: - !text "www.apple.com" +http_host_foo: + !text "www.foo.bar" !byte 0 -http_host_apple_len = 13 +http_host_foo_len = 11 http_path_root: !text "/" diff --git a/src/constants.asm b/src/constants.asm index 3deaf67..23669a2 100644 --- a/src/constants.asm +++ b/src/constants.asm @@ -107,8 +107,7 @@ zp_temp = $fd ; 1 byte zp_count = $fe ; 1 byte ; --- Quarter-square multiply table (shared by Poly1305 and ECDSA) --- -sqtab_lo = $7800 ; 512 bytes: floor(n^2/4) low bytes -sqtab_hi = $7a00 ; 512 bytes: floor(n^2/4) high bytes +; sqtab_lo/sqtab_hi now defined as labels in data.asm — moved out of $7800 to free code space ; --- REU (Ram Expansion Unit) registers --- reu_status = $df00 ; status register diff --git a/src/crypto/ecdsa_verify.asm b/src/crypto/ecdsa_verify.asm index 5ed5e75..cdfbcd1 100644 --- a/src/crypto/ecdsa_verify.asm +++ b/src/crypto/ecdsa_verify.asm @@ -44,7 +44,9 @@ ecdsa_verify: bne @p384 jmp ecdsa_verify_256 @p384: - jmp ecdsa_verify_384 + ; TODO: restore P-384 dispatch — see project memory project_p384_stubbed.md + sec + rts ; ============================================================================= ; ecdsa_verify_256 - P-256 signature verification @@ -364,303 +366,9 @@ ecdsa_verify_256: ; ecdsa_verify_384 - P-384 signature verification ; ============================================================================= ecdsa_verify_384: - ; --------------------------------------------------------------- - ; 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_384 - beq @384_invalid ; r == 0 -> invalid - - ; Check r < n - lda #ecdsa_sig_r - sta fp_src1+1 - lda #ec_n_384 - sta fp_src2+1 - jsr fp_cmp_384 - bcs @384_invalid ; r >= n -> invalid - - ; Check s != 0 - lda #ecdsa_sig_s - sta fp_src1+1 - jsr fp_is_zero_384 - beq @384_invalid ; s == 0 -> invalid - - ; Check s < n - lda #ecdsa_sig_s - sta fp_src1+1 - lda #ec_n_384 - sta fp_src2+1 - jsr fp_cmp_384 - bcs @384_invalid ; s >= n -> invalid - jmp @384_step2 - -@384_invalid: - sec - rts - - ; --------------------------------------------------------------- - ; Step 2: w = s^(-1) mod n - ; --------------------------------------------------------------- -@384_step2: - jsr ec_set_modn_384 ; fp_misc = ec_n_384 - lda #ecdsa_sig_s - sta fp_src1+1 - jsr fp_mod_inv_384 ; fp_r0_384 = s^(-1) mod n - - ; Copy w = fp_r0_384 -> ecdsa_verify_tmp - lda #fp_r0_384 - sta fp_src1+1 - lda #ecdsa_verify_tmp - sta fp_dst+1 - jsr fp_copy_384 ; ecdsa_verify_tmp = w (48 bytes) - - ; --------------------------------------------------------------- - ; Step 3: u1 = z * w mod n - ; --------------------------------------------------------------- - jsr ec_set_modn_384 - lda #ecdsa_hash - sta fp_src1+1 - lda #ecdsa_verify_tmp - sta fp_src2+1 - jsr fp_mod_mul_384 ; fp_r0_384 = z * w mod n - - ; Copy u1 to ev_u1_384 - lda #fp_r0_384 - sta fp_src1+1 - lda #ev_u1_384 - sta fp_dst+1 - jsr fp_copy_384 - - ; --------------------------------------------------------------- - ; Step 4: u2 = r * w mod n - ; --------------------------------------------------------------- - jsr ec_set_modn_384 - lda #ecdsa_sig_r - sta fp_src1+1 - lda #ecdsa_verify_tmp - sta fp_src2+1 - jsr fp_mod_mul_384 ; fp_r0_384 = r * w mod n - - ; Copy u2 to ev_u2_384 - lda #fp_r0_384 - sta fp_src1+1 - lda #ev_u2_384 - sta fp_dst+1 - jsr fp_copy_384 - - ; --------------------------------------------------------------- - ; Step 5a: Compute u1 * G (P-384 generator) - ; Load G into ec_p2_384 as affine point (X=Gx, Y=Gy) - ; ec_scalar_mul_384 initializes ec_p1_384 internally - ; --------------------------------------------------------------- - - ; ec_p2_384.X = ec_gx_384 - lda #ec_gx_384 - sta fp_src1+1 - lda #ec_p2_384 - sta fp_dst+1 - jsr fp_copy_384 - - ; ec_p2_384.Y = ec_gy_384 - lda #ec_gy_384 - sta fp_src1+1 - lda #<(ec_p2_384+48) - sta fp_dst - lda #>(ec_p2_384+48) - sta fp_dst+1 - jsr fp_copy_384 - - ; Set scalar pointer to u1 - lda #ev_u1_384 - sta ec_scalar_ptr+1 - - ; ec_p3_384 = u1 * G - jsr ec_scalar_mul_384 - - ; Save u1*G result from ec_p3_384 to ev_point_save_384 (144 bytes) - ldx #0 -@save_384_lp: - lda ec_p3_384,x - sta ev_point_save_384,x - inx - cpx #144 - bne @save_384_lp - - ; --------------------------------------------------------------- - ; Step 5b: Compute u2 * Q (P-384 public key) - ; ec_scalar_mul_384 initializes ec_p1_384 internally - ; --------------------------------------------------------------- - - ; ec_p2_384.X = ecdsa_pubkey_x - lda #ecdsa_pubkey_x - sta fp_src1+1 - lda #ec_p2_384 - sta fp_dst+1 - jsr fp_copy_384 - - ; ec_p2_384.Y = ecdsa_pubkey_y - lda #ecdsa_pubkey_y - sta fp_src1+1 - lda #<(ec_p2_384+48) - sta fp_dst - lda #>(ec_p2_384+48) - sta fp_dst+1 - jsr fp_copy_384 - - ; Set scalar pointer to u2 - lda #ev_u2_384 - sta ec_scalar_ptr+1 - - ; ec_p3_384 = u2 * Q - jsr ec_scalar_mul_384 - - ; --------------------------------------------------------------- - ; Step 5c: R = u1*G + u2*Q (point addition) - ; --------------------------------------------------------------- - - ; Copy u1*G from save into ec_p1_384 - ldx #0 -@restore_384_lp: - lda ev_point_save_384,x - sta ec_p1_384,x - inx - cpx #144 - bne @restore_384_lp - - ; Convert u2*Q (ec_p3_384) to affine, load into ec_p2_384 - jsr ec_jacobian_to_affine_384 - - ldx #47 -@copy_384_u2q_x: - lda ec_p3_384,x - sta ec_p2_384,x - dex - bpl @copy_384_u2q_x - - ldx #47 -@copy_384_u2q_y: - lda ec_p3_384+48,x - sta ec_p2_384+48,x - dex - bpl @copy_384_u2q_y - - ; ec_p3_384 = ec_p1_384 + ec_p2_384 - jsr ec_point_add_384 - - ; --------------------------------------------------------------- - ; Step 6: Convert R to affine - ; --------------------------------------------------------------- - jsr ec_jacobian_to_affine_384 - - ; --------------------------------------------------------------- - ; Step 7: Check R.x mod n == r - ; Compare R.x (in ec_p3_384) with n_384 - ; --------------------------------------------------------------- - lda #ec_p3_384 - sta fp_src1+1 - lda #ec_n_384 - sta fp_src2+1 - jsr fp_cmp_384 - bcc @384_no_reduce ; R.x < n, no reduction needed - - ; R.x >= n: compute R.x - n -> ev_u1_384 (reuse buffer) - lda #ec_p3_384 - sta fp_src1+1 - lda #ec_n_384 - sta fp_src2+1 - lda #ev_u1_384 - sta fp_dst+1 - jsr fp_sub_384 - - ; Compare ev_u1_384 with r - lda #ev_u1_384 - sta fp_src1+1 - jmp @384_final_cmp - -@384_no_reduce: - lda #ec_p3_384 - sta fp_src1+1 - -@384_final_cmp: - lda #ecdsa_sig_r - sta fp_src2+1 - jsr fp_cmp_384 - bne @384_mismatch - - ; R.x mod n == r -> signature valid - clc - rts - -@384_mismatch: + ; 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 @@ -717,7 +425,7 @@ ecdsa_parse_der_sig: jsr fp_zero jmp @parse_r @clr_r_384: - jsr fp_zero_384 + jsr fp_zero ; STUBBED — dead code for P-256 only @parse_r: ; Handle leading zero padding: if int_len > sig_len, skip leading 0x00 @@ -785,7 +493,7 @@ ecdsa_parse_der_sig: jsr fp_zero jmp @parse_s @clr_s_384: - jsr fp_zero_384 + jsr fp_zero ; STUBBED — dead code for P-256 only @parse_s: ; Handle leading zero padding diff --git a/src/crypto/poly1305.asm b/src/crypto/poly1305.asm index 28a6106..be1b48f 100644 --- a/src/crypto/poly1305.asm +++ b/src/crypto/poly1305.asm @@ -12,9 +12,7 @@ ; Identity: a*b = floor((a+b)^2/4) - floor((a-b)^2/4) ; ============================================================================= -; Quarter-square table addresses (page-aligned for speed) -sqtab_lo = $7800 ; 512 bytes: low bytes of floor(n^2/4) -sqtab_hi = $7a00 ; 512 bytes: high bytes of floor(n^2/4) +; sqtab_lo/sqtab_hi now defined as labels in data.asm — moved out of $7800 to free code space ; ============================================================================= ; poly1305_init - Initialize Poly1305 state diff --git a/src/data.asm b/src/data.asm index 8f4067b..f622009 100644 --- a/src/data.asm +++ b/src/data.asm @@ -46,6 +46,12 @@ mul38_hi_tab: !byte >(i * 38) } +; --- Quarter-square tables (runtime-generated by sqtab_init in poly1305.asm). +; 512 bytes each (indexed 0..511). Moved from $7800/$7A00 to free up that code region. + !align 255, 0 +sqtab_lo: !fill 512, 0 +sqtab_hi: !fill 512, 0 + ; ============================================================================= ; Network layer buffers ; ============================================================================= @@ -57,6 +63,10 @@ tcp_recv_tail: !byte 0 ; write position (updated by ip65 callback) ; TLS state ; ============================================================================= tls_state: !byte 0 ; current TLS state machine state +tls_last_state: !byte 0 ; preserves tls_state before error handler overwrites it +tls_recv_progress: !byte 0 ; granular progress within tls_recv_server_hello +tls_recv_sub_progress: !byte 0 ; granular progress within tls_record_recv_and_decrypt +tls_recv_poll_count: !word 0 ; number of sh_wait poll loop iterations (16-bit) tls_client_random: !fill 32, 0 ; client random (32 bytes) tls_server_random: !fill 32, 0 ; server random (32 bytes) diff --git a/src/main.asm b/src/main.asm index af486dc..83dd49e 100644 --- a/src/main.asm +++ b/src/main.asm @@ -45,10 +45,17 @@ ; ============================================================================= ; Crypto modules (from c64-wireguard and c64-aes256-ecdsa) ; ============================================================================= + +; Relocate all crypto (word32 through ecdsa) out of ip65 BSS window ($4000-$5FFF). +; ip65's DHCP output_buffer at $4CED clobbers any code linked in that range. +; This puts word32/chacha/poly/aead + sha256/hmac/drbg + fe25519/x25519 + +; ecdsa P-256 + ecdsa_verify/der/tls_cert/tls_ecdh all contiguously at $6000+. +* = $6000 !source "crypto/word32.asm" !source "crypto/chacha20.asm" !source "crypto/poly1305.asm" !source "crypto/aead.asm" + !source "crypto/sha256.asm" !source "crypto/hmac_drbg.asm" !source "crypto/fe25519.asm" @@ -61,15 +68,10 @@ !source "crypto/ecdsa_points.asm" ; --- ECDSA P-384 (for CA certificate verification) --- -!source "crypto/ecdsa_fp_384.asm" -!source "crypto/ecdsa_mod_384.asm" -!source "crypto/ecdsa_curve_384.asm" -!source "crypto/ecdsa_points_384.asm" - -; --- Skip past quarter-square multiply table region ($7800-$7BFF) --- -; The sqtab_lo/sqtab_hi tables are runtime-generated at $7800-$7BFF. -; ECDSA P-384 code pushes past $7800, so we jump to $7C00. -* = $7C00 +; !source "crypto/ecdsa_fp_384.asm" ; STUBBED — see project_p384_stubbed.md +; !source "crypto/ecdsa_mod_384.asm" ; STUBBED — see project_p384_stubbed.md +; !source "crypto/ecdsa_curve_384.asm" ; STUBBED — see project_p384_stubbed.md +; !source "crypto/ecdsa_points_384.asm" ; STUBBED — see project_p384_stubbed.md ; --- ECDSA signature verification (P-256 + P-384) --- !source "crypto/ecdsa_verify.asm" diff --git a/src/net.asm b/src/net.asm index d87d5fd..54c8a69 100644 --- a/src/net.asm +++ b/src/net.asm @@ -274,6 +274,16 @@ 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. + lda cb_remaining+1 + beq + + lda #255 + sta cb_remaining + lda #0 + sta cb_remaining+1 ++ ; Copy loop: X = source index, Y = ring buffer tail ldx #0 ldy tcp_recv_tail diff --git a/src/tls13.asm b/src/tls13.asm index bf39481..5bc3197 100644 --- a/src/tls13.asm +++ b/src/tls13.asm @@ -112,6 +112,8 @@ tls_connect: rts @error: + lda tls_state ; preserve last attempted state + sta tls_last_state lda #TLS_STATE_ERROR sta tls_state sec @@ -239,10 +241,18 @@ tls_send_client_hello: ; Output: C=0 success, C=1 timeout or parse error ; ============================================================================= tls_recv_server_hello: + lda #$01 + sta tls_recv_progress lda #0 sta @sh_timeout sta @sh_timeout+1 + sta tls_recv_poll_count + sta tls_recv_poll_count+1 @sh_wait: + inc tls_recv_poll_count + bne + + inc tls_recv_poll_count+1 ++ jsr net_poll jsr tls_record_recv_and_decrypt bcc @sh_got_record @@ -254,10 +264,14 @@ tls_recv_server_hello: sec rts @sh_got_record: + lda #$02 + sta tls_recv_progress ; verify content type is handshake lda tls_rec_type cmp #TLS_CT_HANDSHAKE bne @sh_error + lda #$03 + sta tls_recv_progress ; copy tls_rec_buf to tls_hs_buf (tls_rec_len bytes) ldy #0 @@ -273,10 +287,18 @@ tls_recv_server_hello: sta tls_hs_len lda tls_rec_len+1 sta tls_hs_len+1 + lda #$04 + sta tls_recv_progress ; parse ServerHello jsr tls_parse_server_hello bcs @sh_error + lda #$05 + sta tls_recv_progress + + ; compute ECDH shared secret now that tls_server_pubkey is populated + jsr tls_ecdh_compute_shared + clc ; update transcript with ServerHello lda #= 24: invalid +@store_continue: + ; increment tls_recv_count (16-bit) inc tls_recv_count bne + @@ -99,6 +116,8 @@ tls_recv_record: bne @read_header ; (shouldn't happen, but safe) ; --- Parse header --- + lda #$03 + sta tls_recv_sub_progress ; tls_rec_type = header[0] lda tls_rec_header sta tls_rec_type @@ -113,6 +132,8 @@ tls_recv_record: beq + jmp @error + + lda #$04 + sta tls_recv_sub_progress ; tls_rec_len = header[3] * 256 + header[4] (big-endian) lda tls_rec_header+4 ; low byte @@ -132,6 +153,8 @@ tls_recv_record: jmp @error ; low byte >= $25: too big @len_ok: + lda #$05 + sta tls_recv_sub_progress ; Switch to state 1, reset count lda #1 sta tls_recv_state @@ -148,6 +171,8 @@ tls_recv_record: ; --- State 1: reading payload bytes --- @read_payload: + lda #$06 + sta tls_recv_sub_progress jsr net_recv_byte bcs @incomplete ; no data available @@ -187,6 +212,8 @@ tls_recv_record: ; --- Record complete --- @complete: + lda #$07 + sta tls_recv_sub_progress ; Reset state machine for next record lda #0 sta tls_recv_state @@ -274,21 +301,38 @@ tls_record_send_encrypted: ; handles both plaintext and encrypted records based on tls_state. ; ============================================================================= tls_record_recv_and_decrypt: +@retry: + lda #$01 + sta tls_recv_sub_progress ; Try to receive a complete record jsr tls_recv_record bcs @recv_incomplete + ; RFC 8446 Section 5: TLS 1.3 clients MUST ignore ChangeCipherSpec + ; records sent during the handshake for middlebox compatibility. + lda tls_rec_type + cmp #TLS_CT_CHANGE_CIPHER + beq @retry + ; Record received. Check if decryption is needed. - ; After ServerHello (state >= TLS_STATE_SERVER_HELLO), records are encrypted. + ; After ServerHello (state >= TLS_STATE_ENCRYPTED_EXT), records are encrypted. + ; The ServerHello record itself is plaintext even though tls_state is + ; set to SERVER_HELLO during its receipt. lda tls_state - cmp #TLS_STATE_SERVER_HELLO - bcc @plaintext ; state < SERVER_HELLO: no decryption + cmp #TLS_STATE_ENCRYPTED_EXT + bcc @plaintext ; state < ENCRYPTED_EXT: no decryption ; Decrypt the record in-place + lda #$08 + sta tls_recv_sub_progress jsr tls_record_decrypt bcs @aead_fail ; AEAD verification failed + lda #$09 + sta tls_recv_sub_progress @plaintext: + lda #$0A + sta tls_recv_sub_progress clc rts diff --git a/tests/test_phase3_https.py b/tests/test_phase3_https.py new file mode 100644 index 0000000..3133a15 --- /dev/null +++ b/tests/test_phase3_https.py @@ -0,0 +1,549 @@ +#!/usr/bin/env python3 +"""Phase 3 e2e test: boot c64-https.prg, do DHCP, then HTTPS GET. + +This test extends Phase 2 by pressing 'G' after DHCP succeeds, which +triggers an HTTPS GET to www.foo.bar (resolved via dnsmasq to the +host bridge IP 10.0.65.1). A Python HTTPS server (TLS 1.3, self-signed +P-256 ECDSA cert, CN=www.foo.bar) on 10.0.65.1:443 serves a known +response body. + +The C64 X25519 keygen is slow (~3.6 min at normal speed), so the TLS +phase gets a generous 5-minute timeout. Total test runtime is typically +6-8 minutes. + +Run: + sudo PYTHONPATH=tools python3 tests/test_phase3_https.py + +Exit codes: + 0 -- PASS + 0 -- SKIP (clearly printed) + 1 -- FAIL +""" + +from __future__ import annotations + +import os +import subprocess +import sys +import time + +_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +_TOOLS = os.path.join(_REPO_ROOT, "tools") +if _TOOLS not in sys.path: + sys.path.insert(0, _TOOLS) + +PRG_PATH = os.path.join(_REPO_ROOT, "build", "c64-https.prg") + +# Screen needles (from src/boot.asm string labels). +MENU_NEEDLE = "Q=QUIT" +DHCP_OK_NEEDLE = "DHCP OK" +# Primary success indicator: the C64 prints this after the whole HTTPS +# exchange completes. +SUCCESS_NEEDLE = "CONNECTION CLOSED" +# Failure needles (any one of these means the C64 bailed out). +FAIL_NEEDLES = ( + "DNS RESOLVE FAILED", + "TCP CONNECT FAILED", + "TLS HANDSHAKE FAILED", + "TLS SEND FAILED", +) +# Progress needles we use to report how far we got on failure. +PROGRESS_NEEDLES = ( + "HTTPS GET", + "DNS OK", + "TCP CONNECTED", + "TLS HANDSHAKE OK", + "REQUEST SENT", + "CONNECTION CLOSED", +) +# Response body served by our test HTTPS server. +RESPONSE_BODY = "TLS13 OK FROM C64 TEST" + +MENU_TIMEOUT = 90.0 +DHCP_TIMEOUT = 90.0 +# TLS handshake dominates: X25519 keygen ~3.6 min PLUS X25519 shared secret +# ~3.6 min PLUS HKDF (many HMAC-SHA256) ~2 min PLUS ECDSA P-256 verify ~2 min. +# Budget 15 minutes total. +HTTPS_TIMEOUT = 900.0 + + +def _skip(reason: str) -> int: + print(f"SKIP: {reason}") + return 0 + + +def _ensure_built() -> bool: + if os.path.isfile(PRG_PATH): + return True + print("[build] c64-https.prg missing, running make...") + r = subprocess.run(["make"], cwd=_REPO_ROOT, capture_output=True, text=True) + if r.returncode != 0: + print(f" make failed (exit {r.returncode}):\n{r.stderr}") + return False + return os.path.isfile(PRG_PATH) + + +def _last_progress_seen(upper_screen: str) -> str: + """Return the latest progress marker seen on screen, or '(none)'.""" + last = "(none)" + last_idx = -1 + for needle in PROGRESS_NEEDLES: + idx = upper_screen.rfind(needle) + if idx > last_idx: + last_idx = idx + last = needle + return last + + +_LABELS_CACHE = None + +def _label_addr(name: str): + """Look up a label address in build/labels.txt; return int or None.""" + global _LABELS_CACHE + if _LABELS_CACHE is None: + _LABELS_CACHE = {} + try: + with open("/home/someone/c64-https/build/labels.txt") as f: + for line in f: + # format: "al C:xxxx .name" + parts = line.split() + if len(parts) >= 3 and parts[0] == "al": + addr_s = parts[1].split(":")[-1] + lbl = parts[2].lstrip(".") + try: + _LABELS_CACHE[lbl] = int(addr_s, 16) + except ValueError: + pass + except Exception: + pass + return _LABELS_CACHE.get(name) + + +def _dump_diagnostics(transport=None) -> None: + """Print dnsmasq log and host-side connectivity checks for post-mortem.""" + dnsmasq_log = "/tmp/c64-https-dnsmasq.log" + if os.path.isfile(dnsmasq_log): + print(f"\n--- tail of {dnsmasq_log} ---") + with open(dnsmasq_log, "rb") as f: + data = f.read()[-4000:] + print(data.decode("utf-8", errors="replace")) + + # Host-side DNS check + try: + r = subprocess.run( + ["dig", "+short", "@10.0.65.1", "www.foo.bar"], + capture_output=True, text=True, timeout=5, + ) + print(f"\n dig @10.0.65.1 www.foo.bar -> {r.stdout.strip()}") + except Exception as e: + print(f" dig check failed: {e}") + + # Host-side HTTPS check (self-signed, so disable verification). + try: + import ssl as _ssl + import urllib.request + ctx = _ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = _ssl.CERT_NONE + resp = urllib.request.urlopen( + "https://10.0.65.1:443/", timeout=3, context=ctx + ) + print(f" HTTPS from host: {resp.status} {resp.read()[:100]}") + except Exception as e: + print(f" HTTPS from host failed: {e}") + + # ip65 error code from C64 memory + if transport is not None: + try: + transport.resume() + err_addr = _label_addr("ip65_error") or 0x4CEA + err_data = transport.read_memory(err_addr, 1) + print(f" ip65_error @ ${err_addr:04X} = 0x{err_data[0]:02X}") + except Exception as e: + print(f" ip65_error read failed: {e}") + + state_names = { + 0x00: "IDLE", 0x01: "CLIENT_HELLO", 0x02: "SERVER_HELLO", + 0x03: "ENCRYPTED_EXT", 0x04: "CERTIFICATE", 0x05: "CERT_VERIFY", + 0x06: "FINISHED", 0x07: "CONNECTED", 0xFF: "ERROR", + } + + # TLS state machine progress (set before each step; $FF on error) + try: + transport.resume() + ts_addr = _label_addr("tls_state") + if ts_addr is not None: + tls_state = transport.read_memory(ts_addr, 1)[0] + name = state_names.get(tls_state, "UNKNOWN") + print(f" tls_state @ ${ts_addr:04X} = ${tls_state:02X} ({name})") + else: + print(" tls_state: label missing") + except Exception as e: + print(f" tls_state read failed: {e}") + + # Last attempted TLS state (preserved before error handler overwrote tls_state) + try: + transport.resume() + tls_addr = _label_addr("tls_last_state") + if tls_addr is not None: + last = transport.read_memory(tls_addr, 1)[0] + last_name = state_names.get(last, "UNKNOWN") + print(f" tls_last_state @ ${tls_addr:04X} = ${last:02X} ({last_name})") + else: + print(" tls_last_state: label missing") + except Exception as e: + print(f" tls_last_state read failed: {e}") + + # Most recent TLS record buffer head + try: + transport.resume() + buf_addr = _label_addr("tls_rec_buf") + if buf_addr is not None: + rec = transport.read_memory(buf_addr, 256) + print(f" tls_rec_buf @ ${buf_addr:04X} = ({len(rec)} bytes)") + for i in range(0, len(rec), 16): + line = rec[i:i+16] + hex_part = " ".join(f"{b:02X}" for b in line) + ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in line) + print(f" +${i:02X} {hex_part:<47} {ascii_part}") + else: + print(" tls_rec_buf: label missing") + except Exception as e: + print(f" tls_rec_buf read failed: {e}") + + # Raw ip65 TCP receive ring — what ip65 actually delivered + try: + transport.resume() + ring_addr = _label_addr("tcp_recv_buf") + if ring_addr is not None: + ring = transport.read_memory(ring_addr, 256) + print(f" tcp_recv_buf @ ${ring_addr:04X} = ({len(ring)} bytes)") + for i in range(0, len(ring), 16): + line = ring[i:i+16] + hex_part = " ".join(f"{b:02X}" for b in line) + ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in line) + print(f" +${i:02X} {hex_part:<47} {ascii_part}") + else: + print(" tcp_recv_buf: label missing") + except Exception as e: + print(f" tcp_recv_buf read failed: {e}") + + # Parser input: tls_hs_buf (stable copy made during record reception) + try: + transport.resume() + hs_addr = _label_addr("tls_hs_buf") + if hs_addr is not None: + hs = transport.read_memory(hs_addr, 128) + print(f" tls_hs_buf @ ${hs_addr:04X} = ({len(hs)} bytes)") + for i in range(0, len(hs), 16): + line = hs[i:i+16] + hex_part = " ".join(f"{b:02X}" for b in line) + ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in line) + print(f" +${i:02X} {hex_part:<47} {ascii_part}") + else: + print(" tls_hs_buf: label missing") + except Exception as e: + print(f" tls_hs_buf read failed: {e}") + + # tls_rec_header raw 5-byte buffer (state-machine target) + try: + transport.resume() + hdr_addr = _label_addr("tls_rec_header") + if hdr_addr is not None: + hdr = transport.read_memory(hdr_addr, 5) + print(f" tls_rec_header @ ${hdr_addr:04X} = {' '.join(f'{b:02X}' for b in hdr)}") + else: + print(" tls_rec_header: label missing") + except Exception as e: + print(f" tls_rec_header read failed: {e}") + + # tls_recv_state and tls_recv_count (16-bit) — dynamic addrs + try: + transport.resume() + rs_addr = _label_addr("tls_recv_state") + rc_addr = _label_addr("tls_recv_count") + if rs_addr is not None: + rs_v = transport.read_memory(rs_addr, 1)[0] + print(f" tls_recv_state @ ${rs_addr:04X} = ${rs_v:02X}") + if rc_addr is not None: + rc_b = transport.read_memory(rc_addr, 2) + print(f" tls_recv_count @ ${rc_addr:04X} = ${rc_b[1]:02X}{rc_b[0]:02X}") + except Exception as e: + print(f" tls_recv_state read failed: {e}") + + # Single-byte diagnostic labels (dynamic; skip silently if missing) + for lbl_name in ("tls_hs_len", "tls_rec_len", "tls_rec_type"): + addr = _label_addr(lbl_name) + if addr is None: + continue + try: + transport.resume() + # 16-bit for *_len, 8-bit for type + n = 1 if lbl_name == "tls_rec_type" else 2 + b = transport.read_memory(addr, n) + if n == 1: + print(f" {lbl_name} @ ${addr:04X} = ${b[0]:02X}") + else: + print(f" {lbl_name} @ ${addr:04X} = ${b[1]:02X}{b[0]:02X}") + except Exception: + pass + + # tls_recv_progress — granular progress within tls_recv_server_hello + # $01=entered $02=record-recv ok $03=ct-handshake ok $04=copied to hs_buf $05=parse ok + try: + transport.resume() + prog_addr = _label_addr("tls_recv_progress") + if prog_addr is not None: + pv = transport.read_memory(prog_addr, 1)[0] + print(f" tls_recv_progress @ ${prog_addr:04X} = ${pv:02X}") + except Exception as e: + print(f" tls_recv_progress read failed: {e}") + + # tls_recv_sub_progress — granular progress within tls_record_recv_and_decrypt + sub_state_names = { + 0x00: "never-entered", + 0x01: "entered tls_record_recv_and_decrypt", + 0x02: "reading record header (state 0)", + 0x03: "header bytes received, parsing", + 0x04: "record type/version validated", + 0x05: "record length parsed, entering state 1", + 0x06: "reading record body (state 1)", + 0x07: "record body complete", + 0x08: "about to decrypt", + 0x09: "decrypt succeeded", + 0x0A: "returning success", + } + try: + transport.resume() + sub_addr = _label_addr("tls_recv_sub_progress") + if sub_addr is not None: + sv = transport.read_memory(sub_addr, 1)[0] + name = sub_state_names.get(sv, "UNKNOWN") + print(f" tls_recv_sub_progress @ ${sub_addr:04X} = ${sv:02X} ({name})") + except Exception as e: + print(f" tls_recv_sub_progress read failed: {e}") + + # tls_recv_poll_count — how many times @sh_wait looped + try: + transport.resume() + pc_addr = _label_addr("tls_recv_poll_count") + if pc_addr is not None: + pcb = transport.read_memory(pc_addr, 2) + pc = pcb[0] | (pcb[1] << 8) + print(f" tls_recv_poll_count @ ${pc_addr:04X} = {pc} (${pcb[1]:02X}{pcb[0]:02X})") + except Exception as e: + print(f" tls_recv_poll_count read failed: {e}") + + # TCP receive ring buffer head/tail — tells us if ip65 wrote data + # that TLS never drained. + try: + transport.resume() + head_addr = _label_addr("tcp_recv_head") + tail_addr = _label_addr("tcp_recv_tail") + if head_addr is not None and tail_addr is not None: + head = transport.read_memory(head_addr, 1)[0] + tail = transport.read_memory(tail_addr, 1)[0] + avail = (tail - head) & 0xFF + print(f" tcp_recv_head @ ${head_addr:04X} = ${head:02X}") + print(f" tcp_recv_tail @ ${tail_addr:04X} = ${tail:02X}") + print(f" tcp ring available = {avail} bytes") + if avail > 0: + # dump first 32 bytes of ring starting at head + buf_addr = _label_addr("tcp_recv_buf") + if buf_addr is not None: + ring = transport.read_memory(buf_addr, 256) + n = min(avail, 48) + line_hex = " ".join(f"{ring[(head + i) & 0xFF]:02X}" for i in range(n)) + print(f" ring[head..head+{n}] = {line_hex}") + except Exception as e: + print(f" tcp ring read failed: {e}") + + +def main() -> int: + from https_e2e import ( + BridgeEnv, + check_prerequisites, + launch_vice_on_bridge, + shutdown_vice, + press_key, + wait_for_screen_text, + get_screen_text, + start_https_listener, + stop_https_listener, + ) + + missing = check_prerequisites() + if missing: + return _skip("missing prerequisites: " + "; ".join(missing)) + + if not _ensure_built(): + return _skip("c64-https.prg could not be built") + + handle = None + listener = None + try: + with BridgeEnv() as env: + try: + # --- Start HTTPS listener on bridge IP --- + print(f"\n=== Starting HTTPS listener on {env.bridge_ip}:443 ===") + listener = start_https_listener( + host=env.bridge_ip, + port=443, + response_body=RESPONSE_BODY, + ) + print(f" listener ready on {listener.host}:{listener.port}") + print(f" cert: {listener.cert_path}") + + # --- Launch VICE --- + print(f"\n=== Launching VICE on {env.tap0} with {PRG_PATH} ===") + handle = launch_vice_on_bridge( + prg_path=PRG_PATH, + tap=env.tap0, + ready_timeout=90.0, + ) + transport = handle.transport + + # --- Wait for boot menu --- + print(f"\n=== Waiting for boot menu ({MENU_NEEDLE!r}) ===") + try: + wait_for_screen_text(transport, MENU_NEEDLE, timeout=MENU_TIMEOUT) + except TimeoutError as e: + print(f"FAIL: boot menu did not appear\n{e}") + return 1 + print(" boot menu OK") + + # --- DHCP init --- + print("\n=== Pressing 'I' for DHCP init ===") + press_key(transport, "I") + + print(f"\n=== Waiting up to {DHCP_TIMEOUT:.0f}s for {DHCP_OK_NEEDLE!r} ===") + try: + wait_for_screen_text(transport, DHCP_OK_NEEDLE, timeout=DHCP_TIMEOUT) + except TimeoutError as e: + print(f"FAIL: DHCP did not complete\n{e}") + _dump_diagnostics(transport) + return 1 + print(" DHCP OK") + + # --- HTTPS GET --- + print("\n=== Pressing 'G' for HTTPS GET ===") + press_key(transport, "G") + + print(f"\n=== Waiting up to {HTTPS_TIMEOUT:.0f}s for HTTPS completion ===") + print(" (TLS handshake is slow: X25519 keygen ~3.6 min + handshake)") + # After pressing G, the C64 prints a success sequence + # culminating in "CONNECTION CLOSED", or one of the + # FAIL_NEEDLES on failure. Poll screen text and break + # on either. + deadline = time.monotonic() + HTTPS_TIMEOUT + final = "" + https_started = False + result = None # "pass" | "fail" + fail_reason = "" + last_progress = "(none)" + last_log_progress = "(none)" + next_heartbeat = time.monotonic() + 30.0 + + while time.monotonic() < deadline: + try: + transport.resume() + except Exception: + pass + time.sleep(3.0) + try: + final = get_screen_text(transport) + except Exception: + continue + + upper = final.upper() + + # Check if the HTTPS GET banner appeared. + idx_get = upper.find("HTTPS GET") + if idx_get < 0: + continue + if not https_started: + print(" HTTPS GET initiated") + https_started = True + + after_get = upper[idx_get:] + last_progress = _last_progress_seen(after_get) + + # Heartbeat log so the test shows forward motion. + if time.monotonic() >= next_heartbeat: + remaining = int(deadline - time.monotonic()) + print(f" [heartbeat] last seen: {last_progress} ({remaining}s left)") + next_heartbeat = time.monotonic() + 30.0 + elif last_progress != last_log_progress: + print(f" progress: {last_progress}") + last_log_progress = last_progress + + # Short-circuit on any failure message. + failed = False + for needle in FAIL_NEEDLES: + if needle in after_get: + fail_reason = needle + failed = True + break + if failed: + result = "fail" + break + + # Primary success marker. + if SUCCESS_NEEDLE in after_get: + result = "pass" + break + + if result == "fail" or result != "pass": + if result == "fail": + reason = f"HTTPS GET reported {fail_reason}" + else: + reason = f"HTTPS GET did not complete within {HTTPS_TIMEOUT:.0f}s" + print(f"FAIL: {reason}") + try: + final = get_screen_text(transport) + except Exception: + pass + last_progress = _last_progress_seen(final.upper()) + print(f" last progress marker seen: {last_progress}") + print(f"\n--- final screen ---\n{final}") + _dump_diagnostics(transport) + return 1 + + print("\n=== PASS: HTTPS CONNECTION CLOSED seen on screen ===") + snippet = "\n".join(final.splitlines()[:25]) + print(f"--- final screen (first 25 lines) ---\n{snippet}") + print(f" last progress marker seen: " + f"{_last_progress_seen(final.upper())}") + + # Check for response body on screen (not a hard failure + # -- print_resp_body only writes up to 200 bytes and it + # may scroll). + body_upper = RESPONSE_BODY.upper() + if body_upper in final.upper(): + print(f" response body verified: {RESPONSE_BODY!r}") + else: + print(f" (response body not found on screen, may have scrolled)") + + return 0 + finally: + if listener is not None: + try: + stop_https_listener(listener) + except Exception as e: + print(f" stop_https_listener: {e}") + listener = None + if handle is not None: + try: + shutdown_vice(handle) + except Exception as e: + print(f" shutdown_vice: {e}") + handle = None + except Exception as e: + print(f"FAIL: unexpected error: {e}") + import traceback + traceback.print_exc() + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/https_e2e/__init__.py b/tools/https_e2e/__init__.py index 20e3943..c8e3fbd 100644 --- a/tools/https_e2e/__init__.py +++ b/tools/https_e2e/__init__.py @@ -16,6 +16,7 @@ from .vice_on_bridge import launch_vice_on_bridge, shutdown_vice from .c64_menu import press_key, wait_for_screen_text, get_screen_text from .http_listener import start_http_listener, stop_http_listener +from .https_listener import start_https_listener, stop_https_listener __all__ = [ "BridgeEnv", @@ -27,4 +28,6 @@ "get_screen_text", "start_http_listener", "stop_http_listener", + "start_https_listener", + "stop_https_listener", ] diff --git a/tools/https_e2e/https_listener.py b/tools/https_e2e/https_listener.py new file mode 100644 index 0000000..f623f5e --- /dev/null +++ b/tools/https_e2e/https_listener.py @@ -0,0 +1,162 @@ +"""HTTPS (TLS 1.3) listener for e2e testing. + +Runs a background HTTPS server on a specified host:port. Every GET request +returns a fixed 200 OK with a short body. The server runs in a daemon +thread so the test can drive VICE in the main thread. + +The TLS layer uses a self-signed P-256 ECDSA certificate generated at +import time (cached on disk in the certs/ directory next to this file). +TLS 1.3 is required; older versions are rejected. + +Binding to port 443 requires root. The test already runs under sudo +(BridgeEnv needs it), so no special handling is needed here. + +Public API: + start_https_listener(host, port, response_body) -> HttpsListenerHandle + stop_https_listener(handle) +""" + +from __future__ import annotations + +import os +import ssl +import threading +from dataclasses import dataclass +from http.server import BaseHTTPRequestHandler, HTTPServer + +# --------------------------------------------------------------------------- +# Certificate generation +# --------------------------------------------------------------------------- + +_CERTS_DIR = os.path.join(os.path.dirname(__file__), "certs") +_CERT_PATH = os.path.join(_CERTS_DIR, "server.pem") +_KEY_PATH = os.path.join(_CERTS_DIR, "server.key") + +DEFAULT_RESPONSE_BODY = "HELLO FROM HTTPS TEST SERVER" + + +def _ensure_certs() -> tuple[str, str]: + """Return (cert_path, key_path), generating them if they don't exist.""" + if os.path.isfile(_CERT_PATH) and os.path.isfile(_KEY_PATH): + return _CERT_PATH, _KEY_PATH + + from cryptography import x509 + from cryptography.hazmat.primitives import hashes, serialization + from cryptography.hazmat.primitives.asymmetric import ec + from cryptography.x509.oid import NameOID + import datetime + + key = ec.generate_private_key(ec.SECP256R1()) + + subject = issuer = x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, "www.foo.bar"), + ]) + + cert = ( + x509.CertificateBuilder() + .subject_name(subject) + .issuer_name(issuer) + .public_key(key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime.datetime.utcnow()) + .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=3650)) + .add_extension( + x509.SubjectAlternativeName([ + x509.DNSName("foo.bar"), + x509.DNSName("www.foo.bar"), + ]), + critical=False, + ) + .sign(key, hashes.SHA256()) + ) + + os.makedirs(_CERTS_DIR, exist_ok=True) + + with open(_KEY_PATH, "wb") as f: + f.write(key.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.TraditionalOpenSSL, + serialization.NoEncryption(), + )) + + with open(_CERT_PATH, "wb") as f: + f.write(cert.public_bytes(serialization.Encoding.PEM)) + + return _CERT_PATH, _KEY_PATH + + +# --------------------------------------------------------------------------- +# HTTPS handler +# --------------------------------------------------------------------------- + +class _Handler(BaseHTTPRequestHandler): + """Serves a canned 200 OK response for any GET.""" + + # Class-level attribute set before server starts. + response_body: str = DEFAULT_RESPONSE_BODY + + def do_GET(self) -> None: # noqa: N802 + body = self.response_body.encode("ascii") + self.send_response(200) + self.send_header("Content-Type", "text/plain") + self.send_header("Content-Length", str(len(body))) + self.send_header("Connection", "close") + self.end_headers() + self.wfile.write(body) + + # Silence per-request log lines. + def log_message(self, format: str, *args: object) -> None: # noqa: A002 + pass + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + +@dataclass +class HttpsListenerHandle: + """Returned by start_https_listener; pass to stop_https_listener.""" + server: HTTPServer + thread: threading.Thread + host: str + port: int + cert_path: str + key_path: str + + +def start_https_listener( + host: str = "10.0.65.1", + port: int = 443, + response_body: str = DEFAULT_RESPONSE_BODY, +) -> HttpsListenerHandle: + """Start a TLS 1.3 HTTPS server in a daemon thread. Returns a handle.""" + cert_path, key_path = _ensure_certs() + + _Handler.response_body = response_body + + server = HTTPServer((host, port), _Handler) + + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ctx.minimum_version = ssl.TLSVersion.TLSv1_3 + ctx.maximum_version = ssl.TLSVersion.TLSv1_3 + ctx.load_cert_chain(cert_path, key_path) + server.socket = ctx.wrap_socket(server.socket, server_side=True) + + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + return HttpsListenerHandle( + server=server, thread=thread, host=host, port=port, + cert_path=cert_path, key_path=key_path, + ) + + +def stop_https_listener(handle: HttpsListenerHandle) -> None: + """Shut the server down cleanly.""" + try: + handle.server.shutdown() + except Exception: # noqa: BLE001 + pass + try: + handle.server.server_close() + except Exception: # noqa: BLE001 + pass From ac57d1f19b9d3890e585d6f759e43d66467d189f Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Tue, 14 Apr 2026 10:16:22 -0500 Subject: [PATCH 08/22] Grow tcp_recv_buf to 4KB + widen aead_data_len to 16-bit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two coordinated architectural changes to unblock TLS 1.3 handshake past the ServerHello state: **tcp_recv_buf at \$C000, 4096 bytes, 16-bit indices** The previous 256-byte ring with 8-bit head/tail silently overwrote unread data whenever ip65 delivered a TCP segment larger than the empty space — the TLS 1.3 post-ServerHello flight (CCS + Encrypted- Extensions + Certificate + CertVerify + Finished, ~600-800 bytes) always overflowed. Move the buffer to the 4KB free-RAM region at \$C000-\$CFFF (always RAM on a C64, never ROM-shadowed) via an equate in constants.asm — zero impact on data.asm BSS footprint. Convert head/tail to 16-bit words. Add a tcp_recv_overflow sticky flag set by the producer when it would lap the consumer. Update all consumer and producer paths in net.asm to use 16-bit indexing with mask-on-access (\$0FFF = 4095). http.asm feed loop also updated. Add producer overflow check in net_tcp_recv_cb so ip65's callback cannot silently corrupt the ring. **aead_data_len widened to 16-bit** Was a single byte, which capped AEAD encrypt/decrypt at 255 bytes. TLS 1.3 Certificate records carry ~350+ bytes of ciphertext, so decrypt was always truncated and the Poly1305 tag always failed. Change aead_data_len to !word in data.asm. Update the aead_encrypt / aead_decrypt / aead_compute_tag paths in crypto/aead.asm to 16-bit counter semantics (compare-with-ora for zero, decrement with borrow, length block stored in both bytes of the 64-bit field). Update chacha20_encrypt block loop to a 16-bit counter over cc20_remain:cc20_remain_hi. Update tls_record.asm where tls_enc_aead_len is copied into aead_data_len — both bytes are stored now. poly1305_update in poly1305.asm is unchanged because AEAD exclusively drives Poly1305 via aead_process_padded now; the standalone poly1305_update path is dead code and doesn't need widening for this fix. **Verification** After this change, Phase 3 test reaches tls_state=\$03 (ENCRYPTED_EXT), confirms the full post-ServerHello server flight is visible and intact in the ring at \$C000 (ServerHello, CCS, EncryptedExtensions record 50B, Certificate record 369B), tcp_recv_head/tail 16-bit values working. No regression on DHCP, DNS, TCP connect, or ServerHello parse. Co-Authored-By: Claude Opus 4.6 (1M context) --- build/c64-https.prg | Bin 45891 -> 45896 bytes build/labels.txt | 927 +++++++++++++++++++------------------ src/constants.asm | 11 +- src/crypto/aead.asm | 31 +- src/crypto/chacha20.asm | 20 +- src/data.asm | 13 +- src/http.asm | 34 +- src/net.asm | 110 ++++- src/tls_record.asm | 10 +- tests/test_phase3_https.py | 4 +- 10 files changed, 654 insertions(+), 506 deletions(-) diff --git a/build/c64-https.prg b/build/c64-https.prg index cb876bf66f7051aa8b5cef5739a6df9369ad89d6..27c02cfedb50b2a7137f4d27128bd3801601ea5c 100644 GIT binary patch delta 16564 zcmc(Gdwf$>zHidBw6wHMDa9=Xx(fv@>{?6YpuQ4+S&V0$F!V|sTdhQeTaB@r5LOn+cd^R zvNSifB1vwnNfMhA#4X8IIjDgnrpd`->%_xc=HYYYwd`PvMj{z`XMYD3E*_5ACpJ~(dniFYQ7qs_1- z_p2EoCN(Em-G;W7P*NbZaZ=1>vA<5%kn%6xDPpF`Xajl?7-l$^CbLt^A54V;`cNvF z^EjQ8O6ERJXQyT~0Gk?RY#?KIZi_3HVrtbyYOFnKs#Rn1tb&yqYL5b!py5Je1Rx3w zWNHc!1s{=VDfGsTIh}@D)zoRM1ui$07;EzdYn~MZa*mP7`IO{yDXPix4?a4=KaZx+ z^C_W3neiY`=TqDUdM+hdIC9NlY!ciK!N<^E!+$<*Pn+KE<*z1*JeS?iNXY3LPhsf)v2a zptPKgp>9PAy+2Gg~VIb3rXyR(+ zBuBSg>$ob5;!CUKN1_H|Yc)KNs}=7u`yNGG04gy1ekN|n^v#e>5*v4#eUs!^9U3dg z>5wS*)1krQ<5|8m@v&jP0pg-GU%VU-0%{dkOZwF!rE#@Lf27&zeLm7!NALHM&+2Ln zNAB~5643T1>RK?>ZX;@)I5)#8Vv0PO;meY!S{J-c7333adeyP%)mq1<*JpK@UaYJ< zJgYeLcJX%^&}Q<6`Zt?)adqdY$;&LodSOW(ERM_Y4U|oYeV5}ZNRI2`eYbNJMiO5O zJ>QtgSu^QhOA>24`?C&8td%rJDh70ZV^+WJJZn}@!(Ih&?4f6;e9=dBvQI% zCiNmYvQgn+yl;djTTPYkL}KgEVM*>fOM9C9ls>$l4>`vM9rdEd@xDHM!(HdNw&}X( znq#=hWT$NCSK)VhYxP$O?^UDXNM^2$IC)CnjRCernE6blpS zMmjq1a2<+zq>dZ~^?G`Nxt`ug2kls0`SCg|lY<|g^JV!k3Hs5Ohy1AS zVL#gR2pb;tLy{cxLy{c#i$#kCNj_n}p3Wk+-$-W*elzh`cDH1>dYr^DFkuGSGjo<5?^aJk6i-QI=FApwv7?~3thETs|B&jML z9gSLR6wN`n00;F#XbxJz!Wto_B0+p%pzm(Wg>>;R>EeZS-w1%{T$WKuPbr%e$LbIh zIbLTK4TI2u;@yMLh4Nu{-5ezGAzX^coYZ`Aak6i)d;&ypuD4`6NNp7|ak12P2qnv_v+0vG$u83mXlbO4sB%5LtM=GW2II`t^1M?b4m zKd~fd7_fvwvjLhlz4}rY9_Cr2+7!M;a>`Q5st~K?7QVray9fEC925d;z&}`}Ockke z0wWAb_oc}FnWaR0FdJ+^Fw_`iBU{N}KXw<1+eP~AYH4YaO$^8W;|3|ImI3W;qIy9# zXuSM0hJU8>PtLDvlA@TR*zb&jHFiyb2FFHG6Qkg0yvDC-9~Q-v90h}4V$pDvCZMU% z%xL(ZLsEFM8O=hJ+I>)GGQ0WG=3N0SkwtH>J_`|b(5D>&ugtQunR$qgw8DxP(}Q?4b7%q zt~+bB@&RtC55?l9^D4k#x;mzXeVbH~e;ZVReLGD2GluoVQh_WJEbre2g`Su=lWMZe z7Z^)7XSvEhuk+7qEuI+o<%sb)=cVRITN|o(`7;^hV?S;s9-5U)dvRW zx2f*?M=Wt?1tu%5uWQ-zZ6H?!@|{4saWSOd3AXDP&~Bl?0F38)=#hpi0=v?PEtRYOUb~`Y08)Vjk%Xe%9mEU-5biKZ+fwA{lSZCmSi{r z47*u7vl};SABmyC=ug17RDT;vbZ23wbY}M<^LMn0$-Kp647A!CN`lDZ#+D=1UiBiz zq@_`u{SrI~HoD@xaE2u+&dX&lLt>jZG@yC5my2xkVu3LN1a=y`=jE~uZZAt5ya1@A83NJP1*#VyNZC=a&9D*zSAH8xdsqxAp+2JK_uXt^UwP@L@2|p2e#Cbl~ z(>??t=aDzPvXA_MF>d$DPm?M(e2WdMz2dwf?ktH#`7ZNVt|D)HE5E{w zWc4S>qhKY=Q7}G6AbnjxiYkTQts`qo%H4)VVpjupyX6jzTfNOS(m-H zX1taKi;(#$OXpJ~d?w~=Rm4{V5(2MyN;D%$b$w({L}X*gJJLt2SRV!{IJ!booZTlLZV0y6^xvR3(vNInw&tB#z{ z<_{`6Uv!n4@?*5$21J5rinjvaAdL!nTCZe%}3IGWT20v`fwg38zhoGjSQN`wuv}Ivh?h$VGDcdJC*J^ z`VqvJQ`u?7N93!i`8a^+#IcE~6K4-5b|_-1ZBbRxG`)h|xpIXTI<+%>np)dAXqxKQ z&YT2XLUHtjTXcQ|#W&lEGy^w1ugxgmJ)=RH;Yy~R%8Z8HGuqo?)ehQ^#T}2hX#&Oe z`s_5YZn>E?N$mjP&6^4Rpd*qy8W+*=eiW;u+l%2!-j1T^gmg}b7U_|o$_$d+fRIxf zaA$PjdMH)tj)c)-`NuRXP4fi}*fJYKCd=2;Ew1U>_Tsh~!NiccA>G#|W%>zNU&vM%gc;YK=kOtzUP)!UbtM_#R?V8?Z#3)f6qmN=ZZHP7k-i%EW z*TMS-YBY5`FkkK*K~A-&;v7W73XMgcItN2@{A&l?QaKEl0^y}Ul!nUF`99!_;^})h4{GboH(NWyO zhbs%LxCeb?k86wb__L!HY;oxR{hnp!k#J^kY@21xuWxTNSNuEJx9o8pW;yI!vqw$t ze07hS(7ARGO1^H7)gvT_{Gp$TpT0;#ezl)R2+pjCk!Lz?ugBfd;AajyAAK%U0;$75 zLVnJy2CQPmLbn-re^YTL@|_r5Xr+()vB*5`r@v(1zx30m*!NR@`Yii?)=z)o&%H{1 z;ipfs?6q# zw&gF_8W!7VzAcnWU%=t_d-O$o?zCju`0!mm^w7n&L}FuXW62_0r_F{y7uf`;vfFI1 z2G#aO%qC0~7+|<_F=K)0d|NOE%fSCx$Tr21Khh-G%A0M1z1fCY4~ak7Xp=4PY_rW5 zJNHHbj!^z&3k?bw-pjjUc$XB|0*0Me3?8z?W(^qLjo>^w4r<*ZQn!qy4wsuDJBI|O z`a_Qa%;eylotXiPKQsxa=u2&$A%ZQlUJ?S!{B|7rFY~uE2nYYm{1t=R+p>MBc|a5} zb-s{g@S^A)X<@G@r>5v{~+2JkNB@jKd zS-seI41b1V(SFsR=oltp`{Bs+3KGW4bP5ztHBF+pwnjaUN-a3OLU;cvJlaab%gl z#_Tr2EuN+J&9IFW#D+2u9Zw3IBo}+qVg`Pyf9-QDU*JBEp)}R+B7V+i726kMk-p!L z#?AAS$$m1$FW#On&U=9#@jC|7qkhM2^qAiv(Bpng7;(}zB=K@3CjlT z)~v`sEcUU78H=(W-s7^cgGqpY-Q#+WfugjK-PO4Kt|U%L3IzPFI~i!Dhy9^J4gu;y zej4=S;(Z7y6W`2-%gM+6AtB0j7Ed>e=c*{ZnctIq=pb!=Fp_pZ3$mjp^{jeg)?zD` z3pD{%wV%dxpmP`#0|9rKv(K^m3#S~yu9_Ux# znyv&A3tY35KuUpYwh|av;F_xh1{b&dJv()lYta{-z3?cYzmIWwOcyJ5NpTFIixm4h#gRm9 zihZr(NUX_henzR9uWZIlVyP<#%atnjjS6{Ap^{?Xppa)3x=yjLSIDmwx;9s_mnq~K zMN1Jb97mUmKV6Rd%m92%)Fa?Sr(@yRiLYon#rIb*6WdY(ujoFX6aT_d^HQRaAu;tO z=c$G15i#{1@SrQ^;%#_btl2ht5^(1qzu96~9pIFDc{~3VmF$uTaS2 z3VlqmFIUK83Vl?u+ZFOCWRezV-c6rS?1c(>0y0l4_SFh`TA@!V_7a6WrO;m~_Lmj% zD}~Ne?8OS1=Nz>-Pg=UdvUG(=R*0{zu)N9u?NbCYLgLg21LM&|fREE}`p zCN4@^G+;xW>h-E;5b%a@2R}( z*+oKBFXD97qu{X<3yMhm+R#@Wn-cMjIGfM}@JOkV=I#j%!xaZ#iGhwr*XG7{%Y{M# z=z?t0(r6j9QfA-mJk`LAox$Y+#(FCXA9HYyc&1&#K`d1MrZ=Av2K(50er3egkF}w) z>5t2%<`OQOveCtjqn)I`7WN6YkB_dgdsrankgME1BDmRovVmrP{%Gu%u8u}}VPz<{ zeB>&ed0bfO%WA&3($U>~X{F=p>vyiI$yB=ChWSWVM`Mkl#WG-3O~1|RHKy)=;lu~( z>Q_z#@bDfheRxk$tQc7Sn^mt{SBVE#!rH@o+=d!M`SMi_5AO+#-P6g&R-9$bagJX) zYE1bm*@zwTcq<+YxJBiz!f}=3am#th7bkwbs=}PdvE#2O&b3RkC1o!sUbNO^DdYd5 z?5&7b<2Rcxu)Q>nRC@7;IUN%{cagV>NL>-xQ$#*4BFBozmqnz#h+Hc2+)b*Bh+IVc zMPz>wIbKA*Dk2?4?$I@BGObuJ}DxHipXbqMdV}=`Hv#C0%~X!zBp!PIpW=#EL8? z-z89lJQ_^c#^}LU#Bqw1j3}WaO326(baGBf%E*#&IVE&t3B02@CGbweFdyw2c8Sd{ zMNu^Rt++T7pM?)#PKUeVF1;(>b!5mM$8E?-xI5BUqdTG&nBe%uR0$Z2NUL;KX9Hqao%5% z02c6FhT^e`jn(0JZYFEFpj1;!YwF-ov~srTISj3?XIcqWjlHSz?8Y%##QBweXSQ=sVs*aJ<0rVqZ)p2i(^ z1>AjIfx~g)TWg~5nn-JpfmUun>WK(4iP18xaQFi_{KIAjMT->bF-D2pQe{`f4N30m z6DZu67_GBQcOwllO@#8Ih(kT5v%PVMiM%OU zld!y_RGx`qMo~WStQ{D>G72vqdiB96s$VX3OgT8&Og)+s=|^gbs7?#}{7S?NmUh-K zVNz@{%Rsn|HJYM}xmU@p43vvvzfadxDk)=0%u`g{lc!d8|ybOPAcZ zO8j7rcxVl~AskUTdb9Zo?jmZOIWIpit&}$7_!qtQ-ehG2u}ir2*(8joyf;P339Tl} zC2E%h`*KM;a$doPd1seP;Uni2QfP4a$oVBnKXNXVERz6hht3Sk51k7o&t$Olwvp3UMkdx z%?C@za`YwdnbPK!(%tQ)p#*xmw7-#@E~N)cRd!DNX=&@2ieYLT`=QCXJX|V1E$tdp z^(mBH1WSj394vjTy;SvarJZL=>6y}C(oA&+7p=*+`9b0woXJHpqvjv6-P8bd>1Vgxrb(s2ho$bko= z#I@@zZePDdGcHGj%brrdhtW>OH{;|jqdJaM#6-mj!Qp@zRun!R6(im&$ zgdm)Oz(@#ar0j6=xFl+LYCS50^EFI{X5$9HjX(eGpG?vY#~8@vTA@n=#95ofj#AHv zIi8VoZLau4`3rw?^&lN)jW?L%XXm0tdI(mxOW@jdl|fQpnSjFBrkv-NCA}t z)F`lMq=8cDO*9r16Wc9ujSh(I)5T8pXgmdtjKtz)8jT=r(xi+aZHgj=jX_~qlpoB+ zeNvCcu#f+mF(drg6cu!o_d9O(p83JNFOzO||CCM(Ys}VN99YWTXJXlXG=j84mpau) zA(FpOC++Ja1=1<0j~Fwir!gBOmPTfbMRrQ}1CfPY3>%A*GWL<0)!Td41UsNG+#izO zAZ5BAWx9_GBF3ydG`;0Uqj~pj>0yRRaM%bEYY8JrtR=E_ zjcAFmRCW|e&p=p;k=Rs?QV9E6cJ}EsV~>wzy8EYeQrH;WkJiZin57p<%PCLy4oI;k zTcDGExyi~ix?rPrNi5rftJnH1b3~Tu=4?k5BV6{DZBeG)y?#q5MLfAV3jf&pE$SCs zptEcXE!z?lLnce!7H}-kyHUSXXGv@&T%biIhDy<-*&C|LSd-pV#T^mX!nFrW?-U`{ zY&}CPAeOxiklLbEaaNz?y$h10(O`GOmdHGLcZ)VptR7?Tb%&8|Z985S z1#Awmqi~&xF#3Bj>TcbN@rwu}mu2&KA5a^)5qTDCD26-LgY!G4!rC-3XN&v!K(@nm zu^G=|^u=ILqNW#R(@jOCUySwhwV? ziL4mi7m0`ET553d_G^YMape)N0}yTEHPC)forecmN_TKP`tDqL3*N)wKa~K8H*#iw z*n--P2LByY%4r5uKio4}9xcZwo&Dj_oaWGN6&Zl?&gL}N<5uJ<&1r^|Wm!3ZytA`E ztOt=i_#u7p!%#AQetn>+rTy50A6kA}&g`hxu#pS8kscdLUmMa*M%3>yazQupf6LkV zEQ{`Rw~E(@I5<9)9IM35@(uFJ8|A-!W8<~WO7~xG+)(>%HeX_o z{f$=n5BN#LRy=>dP|oxIUL^!P-9)YQTqUiQz0i4{?z3to`K(g%Ts_-bNlsJ>^s`DB z1d5)hgh__tE|8s&XNX#^qDJixwc?X+SPT_9V5$%ozR~MnvMHAWm2#!T$oS3PFphy45JQIF+$zr`leXf}e%n^d+ZDK|0%K*E z@xahJq>*BW$1}rT<>9up3QDYmyj5d4K`S5Kw$*W+VXHo`Y&mcpFJXDQZP&HR?BoR3 z=aub@axscRcQQk@%nRLd_X^tTONHwymK7j%yn4!E5Rzy!N|o z>!$1Q;@+)&z0k$~DQVlgPya5SycNoD%w&l*gvUu#Na{Pfog}z6GMX49uqlK{ay@H zdstI(q`F0rFlp$)Y9#2>YWx87AQvP@t1*NqIacktdh}Q|e}(Gdw>;fPk5P3!7UoDWJ_3!6K${}aoCtI*0!`PUvDylxsbRm~)Sw>K38DC4T*v~vv$S$*^42D_ z6PBI*O)VCSv!(J537GK_%v)NSEK{rUtlI@!H>RG!c9s32SBs;+dWHw#YlGLOP=BzW zK7G~lYaT^eC<$vr8aZCwVsSA{7}7zA7}Q!8Z>(B_>~rTH7WncD=t6J^2&Vv+%@(L!4N0jad zWD$=f+3)n&4^sj=s`&%TY&@Vekev!J_;qwG9$5~@6V0E4g&p^Ee%et>@N&w3){;#M zZarR8@RHoeifqOcO{j{zUgB(GS2F|Q`Q6_nKJR?SdH;+@YF({+*`;dMI$WR9K5#8% zZ=tZ?B}FJ7tifnYt*eXk9bzN!?P7emnaOl^I0flH|Ns8w{4?eoJDl0(#(2-p{*AjD Hw#WWI6rMveHYF{q=WF-etIByp-~PZBVwyXW3>PjhRkzmwG9Es|-Y#uPO7zt-L}3~17m zbNKz(`&-}p`_{MCUTf{ytrrcyzi6nqYhcSAV&fXIxpLyM2?CXqNeq=!iGj*DNGxl{ zQ8|XhQ+Y8NKxKgpq+-uJS7OZMxnbs{+c3Y&m}9`~5rTh94kfs+WOiJ*PyJFB0F~#s zUW=J6{lQp~BsbM0i7g4@t`w`>rh&1!aw56MFHdP@Crcd{Rjr})K5iDE-4 zY{>&^lEtK!1gl55&>Bh#CO736zKR+3gsq`HnJ%-rg#&asuI{e(Wc5>s8qUiHTzPTR zTo1>kxSVm*?>InW&jTNKo(w!s63@#9l5pNC2(-|p#@!>mZIlI35EI1T87KJT#LY&N zReakh4$v?hjuk<)=IzV_A-_IfG>PIMovb0{u^yv1LS(e$J_Lpt&85j{WBC~)6wtFq zlJ{e(8p*;R)6>R`24GX8j16Jzt~cU~q?p=}!DKaCsZe_w@SysjaJWmT<*0AIAsdYp z*GdV zp_atfA{T~QsnFsndEex_@61QR$9#>C&=&{v-^lH~0JFOjZ`H zAc(Jy?(f2PXHM@F=bh-$wt@aJ-aFM)c>)qyhl)z@)LC8~EHBfWIlRd^N`R6Wi&bOs z=`6cFbq$|)2Dw4HHyYiZ&T>2zJgS@e(ZOkI^K zTVjTApw5JD!XVl>6di>@v}q`+ueA;x+EzzSfx2;ML0UcCG!(Rt>dM>eFjKub*pn2z zQty5za4PL9xs`knKuvxaK+m=YP@-+Dc`5)&@=-v1ZZ*o*zHz8iLZa-OhO+s#WoMwW zyY*z6dQH>M%9C3_f$Cw4|27!i1m7;DX}(=zzUiuZHDSoKBWKVg$NUq&q<)T-uWl1m zvl(t)iect2384WkNKD31R51ElqiC+dI9$^^LUYXu7FGy`iUjfh4fap5ygF1|K2&^l zsDBJVRBBWf*%N*|GCSgNHDAw{wI!0c6tvJ|N5o@VVZ-~usx7J+{RAXDZKi1+-Z!-b!PVb7%z|f7$ zps^Wk@#-IFoRp!qqg&%^X)6nlw$-)8LY+IEo(e-4r69N0v7kSK`8t?KfTM0xQqhSzeLN@YlO<~D928p zsZ&pbeEMvi`o3jHx_~JYnlV*pXnMm_o8B~A`zKY;DN8Bq1w1=hZs8l;kcaxmq95AQ zLtn6tl42`TDxG)=RJ`idIIfl(#upe`T7>Rr zY8lXRK~%5Eg2v0Q27VpNube-ush6ip;;K1fQZHIlFKY@k(YPpTViX*W*Z4ITMn&Q$c)mTLL25GPe&l zUE!5X26S}hnAoC^3+lZQ1#`?k^Q9kE%;dP`K&bp6^^tS zP`H1|L?`0Uvs!2oj<_e(YTXC8wLTPw)m{(4wxQ~TR(6k7MSd4l6T3TO`PIN`;`2<> zk!hKf35{Mue5@L4`8boYbaR#-e!YpS>vZZg^fgqiX1KOE^|{ur&zYpRTb=3cX}edQ z2E4kEQ%9-yv%5N%w?y?`Bo}RWneJy7x9Pp94nKSYCMRVwk8z`)XNCsSj!bQNhE)?R zC8&I@hEjHIPV-2g6nL_NGZlA7Et{qVQe-0QOk_Qte{`M6b`uS{-c%p}eSbz*s2pR73-satSE3$kb-8NP)s$R`{Ao-c#V}L~>G*g^A>Z zLf=z3?xezS^aO6VB1ds7bcrtJ-|rgTtt=4$UpKisT(gIamfrA%vZ)IXWcmvqdoHyD z!B%0PHDVQZQVb14Ap*uD`FbePlZCy=XN{-Q{y~4qM1I9Z=ssU42_h?-THE*e)T?Nd z_nSoFRoMBU$!+k#B+G}v$7R1lVea*Xl3O0|RjqslGJAcPN@5@mG+WGa*@j>HSb88o zhRY-Q5BTV>edIwOtp?iw@8eN+*MhOzl!P3is-Imx@&LIeUj@Q8PS~#-QuUiHAPP|MF3Cww4}=uZp$aR zH|UH{XX%WV2xBaBHiJ2v#hex3E285m(TpV2X+|QlKaR*6ZX=tl%O#MLQVeDt}5xD?e9?{yZg)b?J22$ef6mk9}~99OCtG5xJ^~{smPD@ zYW_G&n_p0;t+nozP0)kVjhcyC7^Bsb*;{|w2FOq!Y{~j*oGU-G%2koJttrMdVT2^m zh8QYlr(n7F^;~(aXMy-+3LQWjaF#P)LsEfazKNlH+B8r;ZC=S_n^&RgO6KkylZexr zSj)A!;&&X+_%Y8Dqo1VtNvfYD`*Gv~#Fe=cxjL6^0C7$;t#`+a`5C+9RHeHirq(SS z?kJC`ZB;wP&vGl+I`qn12wCdqc3qjP{=Vz#T=low(Txd9JDlKP!Oc%{_*QxEe71s_ zU+$XUuy?*Yk#_B!-{6|xaUo8Ln1?-%1z&C6VM% zLqtc#t*i#FW6LcB>B3M>xxEP8JD(&rAmpS5taJYLCQnH?j#i7BYo$hin-EHDiwVVA z+UHq{=V_aT>+{+YLun0Z{tH?D%kIH`98y@W=F;JQ^QCEI5Uq#G1Pb7SB!n_dG=UbZ z1cDBt1p=_AEO*VLBN%Tz4$CxpVjud0^XN!MH`$@4R}ialBkR{xzvVjf#9!2qit{!# zu`NZN*#&IzybCdCC8`tjmSo_>;aU@fG>loUgVzYuXzF-i{_H%HoNQ0Vah}{@Pr-p+ zKHp{}`?3E?@zWH4o53HCJeaZZN6%=L?SHO+S^F2ERN49AS>r5rfT$YfL=R;fvgxpP zOO-x9pS&=i6)Ea3vFrKyIHY)?X~WF2J28r~nqr>%iQ&P#W(+wD$b^9VDGtU4a{}(y zIhYij5O8m1p!IM@aOMk%iKc=wXPn$zAUNZJnW14^_x~h)UNqjUb0%=kLE;LlxRGfH z<9jvC(lvyh>8l}ZD@wze=l`cZSeSUCk^X7$&9m& ze~8U1t<)08P9>HAUCi!_19Ta?FALD8*!`&hT@=XfA&UZZ3A--|(BbE-Zi{S>{n+;MW44zU*&3GG=;O9fDqVzwB#d=3*<0Ox{& z_{J*u#P((8KtVhy2-Ku`Vvsr>d`B5fAhNhn2At-xvg*Iu8*Li!=D?PxSsK9oC2zWS z1~{8!6+c{!88|0^dYlm;GXrE+K)k+MT>b)W3pj_-Qvqit{V3oxi8(99)O^N{n^>E# zPWrEk$%0k>hShscpq~lxL+M5IP;0Y$A?qQ4e{6O?%|P*`m26wIKj2Q_l%(K00rvz3 zT4`$_ll2q@vh(dLcp11;9f->rE>p5MM@$(aMB$_ccLsji-gMnMHAV{N zV>29lFW)^!3BI52p05Pk^4$+8!PEKfhm_zS^4*Up!GF(pKc)mf&UY_W*aG7tr0G8! z$)}B0Izs`3=BJIu|7@Jv_v#T4TPY+KsMkG0=L`ieov;(*n2M0LS)L&&ZDxNO{j`y7 zf}!+11-o4<+m1&`_a~z|a3=7(#^4;q{Xt`JzT!UB7<@o+pJ@y}q`3dBG5Cn${y&Yu z#}xPfZ453|+<$K5ZsW~YuoQCKrK@t9bBK3{WWHobOsp4QD-^p5y+h2G2C`O3p}42e zBLouN8+pXn7$sHblR*h?RNgQ$rPsj7h-ncq?+}0(UWo#;)Os>by>bzT+7@8ApF>#8 z^(PeO`ZG#!zNEOHRhaKDD9ra&3iJIXh57!n!hBy7PPuakWLGI~Bx+_~x1+fx$_p`Q z=e}_3!B6t5jDlu83i~@H8p{Hd!VyB-6A|V2+QpBVHlBiFEd?fOQ|wz5X9``Z*f%TA zWcs3F-=sK`=nIN{qvA}g8PW2DQuT~dhC#wow<;{RMX_&H$kPhltk_Ew@{~e1DRxOA zPbzd{wqh?<$Pkk)v`G(5Uu5{F!FY; zJb$Z_SHt{KN+j{OVQfSTYGU3?r~}*=elzc7g=K)b`8g~1CzN^5FdFX9a!T--FvNJy zW4@lP*q>I&Y=xQ?`%?-rD|D7(e^Mc{6gpF}KcSGBiiyrs>_1h=yzvUnRqQJilB>|U ziv1@FnXAw_iv1ad%mLT^iv4+o+^@I_R*gtB(H|*xn?io1xXM?JSZ1ON6?=g~7AiDP zvF9r!PoWPh_SFh`SfLLo_Eidb2r3pScDq6rDb%9a3l(Bf=%b4LX9{^#p^qr`mlW~{ zn3gE^*A%iuaot!oVyuZSSL{U!S+3A!irt}*WeR;*w-jzsq3ZHX8ZV8 zEaP7h^Ij2Wy<(Zg0PQLQ86k0Mgn{vBBEU!U^b?8F$@DWRS8Dzo>9fX#Gx>ZcVo#|- zd@c-vu);mIi5g{sG4jI`~MO3fKCB&oP4G{~9Nc>^aUmY73aZNa0 z&;$gst%;6r4voSi3b(}I#U{72sl)QtYbKxz?j-LwSQ3O$2I_2UrUQ7m*-9U7ZWAkpl;5#-_lULP)-|y9 zaI;6K5z4dIHay%Myt}!JwXHa)h; z8e;O_rtv(+v;*o_f96u9pps^dBs7t zILHnM`IUnlbP&biowABN;UFs=lza{M0J@v9h%5Sf}M2%26h(v6d4>r0|${I$7>C*>cE(MPy+SDJvr7 zMKV^`XB@X53+1*5 z2e27x5!~1b9iCwd!am!>bOwT#`4;AKpyCd1j>$H|RA8vTu)v>e4tBD|d}pA;AG-K$ zTE3qW?~FDPi=xRz7jwK)4u}84A^xeTCaa}0P&o}*5Z_tLR;<%K!tvyzq3JC`4R`uC zaN0zd*=*CD)9G?-6P<{lGhmwz_q;REoQ^uIXtzvW7eTYVp@t54r-v5R^;OjwkPtoT z3{W0DLG+V6hH%6cSxnvEOcWUy4<>AE^juWLbBdLGvW|YTj{JEYYWw^;DR#mD7+80Fz2nXDIy$C)7?a%Lh$;bJ+n9>t&Y)QWaWDDtM#<-m@^$m2ssj7+~~!DlKcJoedf!B$Lf{z ziorMdwpkf=gYTLx>(}29-b7#w@oN^^GTYSUHlE&*tdMRg0a>t(42?USQ@2yErhTR@ zodJYWk%s4kKS?Vr2?$nLdQJRrJ!&GSEequ#tPh75n$3KzQ_{J4*!JcmV)q@k*ufy| zzWZs`6fxtV_Yv|??{u_Q1ebBk8kqc zLzZolCp)LfN1gYG_pJpu8Q>JzqN6UX1E`@2dr^hoj+*S83I#uq!`vqv0K+;pl23xB zw_y)7A82oXn>~#^>r|w2FWR7XMNrud^sNBHymC+)}5INS@^V_P7a#PEe z1jVeAeVR@B_$KExx@;4-is^^E)|=6=?1gn_UfZO0SOwaCH2XSfKT7|4G+P?9fc*8S zmDdmJ&&SvDEHZH6TAK8BwnUQNmRab`TA`ke`_@YGZXJe%;55rw&e=HeoO5_~u>I(` zIU$1_Q~x9Lb~Au)@|@FoQL-W_)co~M7uB`-z+3dB?vBEX&dm?aQ3G;`bIu2|)2R1$ zN+j0d->D^{Iyvn0hKLs|?W|zJq!SL7fp8gXG@mZ^)3tKD4sKf;DR!w4I5yxgy4oW# zL{No#!dO^rC`|@8D+y>|B$f$1VoqX;V8^2C1eZlu;L|-=i04v!R~HNL70nmy6z|&>P&}*dZ9NieHxey8Y}h5dJyK< zBkYs4EHE#^T$V^YRvs&p?9?UiTPv>FBsyKstb19SB9-A_7sYjGwlaoHknpe*CA8-p znxl*jy-uo*%8 zGKgkZVEvewiLO7cpFHf*;(X`0eCODF=Uw^EF;?->V(-MzB-cCZM@iM3bUTA==hi&mX0b-nlcL}^R0 zD3oB?MjkFDd8OX*dEUG8F+j*C~If9Z8=%84P^V)Yy<>Qfd zzVq&U=lHINOYsgMuM}#=K+Rp^LbNSED20xw_S{mssWdw?M*UHQ@8x3pZfSN*XgGe$ zzIDUU+2&%)lUp&rO!Q8CgzX$!4wSxBb)b~(EiB?zS`A)EhqXfbPnz^`CXGy64)&2g z4(a2Xbm7*pQHYO)ljFpR#TD6+z#lC}l-yZF$LW!%PfM2TC1Ilw(5T_`K=8OGI1v&W zEu0@NiEcVjisa+m%?kK9gPV42d?!{~;hZ4<+?j2KZVeE7oZ_Jp@0dLAU3oaW*K#)u z^}~yb2e1vu(1RE6e9VX zb%mR6_2Yn~)52oRn4bJ|I*A!$84epm8pAH$M(TJ!sh+;xvm#ibd!GxEq!&o8E|#ks z8yWG#%0tsjZaj*2^EO^{U$L6rR@H;@1BgN#>16X+44LcvXVthF#`J2GcxWqhYe?3L zUK%HD?j;@2Nt^p6M3-V6GcqbP78yE*N%5}MNnu-BcQX5YFpUWJARAfO@NSP9T)b?< zI#^dkIhCuczl~EP`}upkHqYDN{uvaHJNKc%%d+dakr87lrxNEq%Bm;#j!nBeUb?UeY-|2`n5I z>3ooOX>)3iP70^)S&$@+2D|fjMMlZ9yR=ba^~Pl1bjDC}3>xDm|}Ygr3LV*GZEC_&kglJH{#tLgD_3%ngx%s9$5UBHe7 zb{w8E5k`L>M%}I3F{%+pF8g!T2g?@g{9(m%4=X2zJJ5^sJLke$u6TZ@=egjyZSGJD zUcSmA7+d-ZFV;~}vTC$!sKq^U8+)Z&ybEu1p=mQ~0qoi4KEMh4%OnuFW2|s{6In67 zKT-}yj7PU$Gi*6YB3vhsqGP-dwBI_L@p?gAuLkKj!K}p2RaLTv2D=40OIJKmCU^zl?)%N;{n@uRI<0<_MMf^ z9;^6Px%f`GC3O$U*;77a&$b(7O83X#e4`G)r$g+eevD+5m*Q6nd-1;gt#Y2_=PDuL z?Iyb_y**@iWgoQJ+kJUgB_SYP-d#z`D@~MC!XPlTyb>lEin~Cb2MJR z-D7!oj}H8HkC?Rw-@oyjc8&I+k6m5oi@MI0T;~9k1b0@pc`9oLiWByziLk%~dr5^L zC4dw%$_{|>49tKS61-%uynskwRN@?d@m|Z56?lLGWhKB&1BSkcJkpiOBGbq|q~XR` zHENy3>oShxH3VI}*Ljm+tM*j3{_9PA`{eDm-PC&KFekY8RCX{5ew?IJq>DZDd1mMZ zW(aABwxy}N4JtG+aq_>pu4F!PaV=ejXvE)w_RQ zEq=VOf~lfEuQu6Bs+|VBTa&7tv0ek&R_%=?&gw%3rgpog;$Ny;O%h&E(5=--P-!)O zu(_2BlCo+vAxd^sdwWjrsOBFq#qalapDwG0h4}rvZ&(%dRAG>`4U9k&BhdB;lox@{ zM4)*(G*O#xG&StkmKxNnIw3T$Ek0xc-t~Uv+!S&Q<%CJ*x5rv716?V5@6HEiS_HFS z>nF>#eP-)EldT&=Pq6i{pV(?~3{pSmLHIAg>)6mBuwT5`W69XhqSzWr!rU;J?5u9J zJjF0!h=Z;Jdq-gs)GJVI;T7n;*~bWqVzEYGKa@Pu#=iS{rkcj#8x{?U$5$;HG~kGK zA9#f9s^*{7vad;ZRXcC;0LA}BDwr(ZIC}1rMZ95TzouKoewh+nQO#dC-ia5E0y(Gv zgC8s(!YjzpctZ&ec2>{%>BU-tZ$dt=B|lfN#Mq|b^RyF+oQ4;a;ER0KQGSd)w*BO8v#G ZrkFPdrMc9ZH<}W?hX*w^HvBT~{{u-hRW$$r diff --git a/build/labels.txt b/build/labels.txt index 4b3a7d5..e5ed7f9 100644 --- a/build/labels.txt +++ b/build/labels.txt @@ -1,5 +1,5 @@ al C:ffbd .setnam -al C:0100 .TCP_RECV_BUF_SIZE +al C:1000 .TCP_RECV_BUF_SIZE al C:2027 .ip65_vt_cfg_gateway al C:1303 .TLS_CHACHA20_POLY1305_SHA256 al C:2023 .ip65_vt_cfg_ip @@ -67,6 +67,7 @@ al C:0015 .cc20_qr_idx al C:d40f .sid_v3_freq_hi al C:d40e .sid_v3_freq_lo al C:ffe4 .getin +al C:0fff .TCP_RECV_MASK al C:d412 .sid_v3_ctrl al C:0005 .TLS_STATE_CERT_VERIFY al C:d41b .sid_osc3 @@ -122,6 +123,7 @@ al C:2009 .ip65_dns_resolve al C:001e .tls_rec_ptr al C:00fd .zp_temp al C:0038 .x25_prev_bit +al C:c000 .tcp_recv_buf al C:df06 .reu_reu_bank al C:001d .poly_tmp al C:0022 .fp_src1 @@ -156,533 +158,538 @@ al C:201b .ip65_set_tcp_cb al C:2003 .ip65_process al C:202f .ip65_vt_tcp_in_len -al C:a828 .tls_hs_write_iv -al C:889a .ec_point_add -al C:8b3c .ec_sc_byte -al C:8f1f .der_skip_tlv -al C:732b .drbg_fill_bytes -al C:1f15 .lbl_derived -al C:6fca .sha256_shr3 -al C:6faf .sha256_rotr22 -al C:74bf .fe_mul -al C:68c1 .sha256_h1_init -al C:9220 .cert_sig_s -al C:6fbe .sha256_rotr25 -al C:7c85 .x25519_ladder_step -al C:918f .cert_pubkey -al C:91f0 .cert_sig_r +al C:a82b .tls_hs_write_iv +al C:88df .ec_point_add +al C:8b81 .ec_sc_byte +al C:8f64 .der_skip_tlv +al C:7370 .drbg_fill_bytes +al C:6ff4 .sha256_rotr22 +al C:1f99 .lbl_derived +al C:700f .sha256_shr3 +al C:7504 .fe_mul +al C:6906 .sha256_h1_init +al C:9265 .cert_sig_s +al C:7003 .sha256_rotr25 +al C:7cca .x25519_ladder_step +al C:91d4 .cert_pubkey +al C:9235 .cert_sig_r al C:0a91 .menu_msg -al C:b5dc .aead_scratch -al C:7349 .fe_zero -al C:ac9e .hkdf_context_len -al C:7353 .fe_one -al C:0ecd .net_send_ptr +al C:b5e0 .aead_scratch +al C:738e .fe_zero +al C:aca1 .hkdf_context_len +al C:7398 .fe_one +al C:0f45 .net_send_ptr al C:0ab3 .init_msg -al C:a860 .tls_app_write_key -al C:a100 .mul38_hi_tab -al C:b8ef .ecdsa_sig_s -al C:8b3e .ec_affine_x -al C:7f2a .fp_s_hi -al C:b8bf .ecdsa_sig_r -al C:69dd .sha256_init -al C:8b5e .ec_affine_y -al C:0e7a .cb_remaining -al C:68bd .sha256_h0_init -al C:7f2d .fp_wide -al C:8adf .ec_scalar_mul -al C:1f59 .tls_c_hs_secret -al C:14aa .tls_record_send_plaintext -al C:9ba6 .tls_ecdh_compute_shared -al C:680f .aead_compute_tag -al C:8000 .fp_mod_reduce -al C:80a4 .fp_mod_mul -al C:1505 .tls_build_client_hello -al C:68c9 .sha256_h3_init -al C:1e70 .tls_compute_finished -al C:b182 .sha256_block +al C:a863 .tls_app_write_key +al C:a200 .mul38_hi_tab +al C:b8f4 .ecdsa_sig_s +al C:8b83 .ec_affine_x +al C:6a22 .sha256_init +al C:7f6f .fp_s_hi +al C:b8c4 .ecdsa_sig_r +al C:8ba3 .ec_affine_y +al C:0ef2 .cb_remaining +al C:6902 .sha256_h0_init +al C:7f72 .fp_wide +al C:8b24 .ec_scalar_mul +al C:1fdd .tls_c_hs_secret +al C:152e .tls_record_send_plaintext +al C:9beb .tls_ecdh_compute_shared +al C:6830 .aead_compute_tag +al C:8045 .fp_mod_reduce +al C:80e9 .fp_mod_mul +al C:1589 .tls_build_client_hello +al C:690e .sha256_h3_init +al C:1ef4 .tls_compute_finished +al C:b185 .sha256_block al C:6434 .chacha20_encrypt -al C:1f79 .tls_s_hs_secret +al C:1ffd .tls_s_hs_secret al C:0bec .send_ok_msg -al C:ab00 .tls_hs_buf +al C:ab03 .tls_hs_buf al C:61ff .copy32 al C:61ce .rotl32_12 -al C:b950 .ecdsa_pubkey_y -al C:b920 .ecdsa_pubkey_x -al C:7e16 .fp_copy -al C:1a79 .hkdf_expand_label -al C:a7c8 .tls_transcript -al C:6550 .sq_ad -al C:9250 .cert_sig_len +al C:b955 .ecdsa_pubkey_y +al C:b925 .ecdsa_pubkey_x +al C:7e5b .fp_copy +al C:1afd .hkdf_expand_label +al C:a7cb .tls_transcript +al C:6565 .sq_ad +al C:9295 .cert_sig_len al C:0aed .dhcp_msg -al C:68c5 .sha256_h2_init -al C:8dd1 .ecdsa_verify_384 -al C:ac92 .hkdf_info_len +al C:690a .sha256_h2_init +al C:8e16 .ecdsa_verify_384 +al C:ac95 .hkdf_info_len al C:0843 .main_loop al C:0c19 .reu_mul_init al C:0d3e .net_tcp_connect -al C:b135 .input_length -al C:ad05 .http_path_len -al C:a8ce .tls_rec_len -al C:654d .sq_sh -al C:918d .cert_tbs_len -al C:1376 .tls_record_read -al C:67f8 .aead_setup_chacha -al C:8b7e .ec_jacobian_to_affine -al C:b86d .mul_src2_buf -al C:0ff1 .tls_recv_server_hello -al C:8247 .fp_inv_iter -al C:b980 .ecdsa_verify_tmp -al C:685e .aead_process_padded -al C:b508 .cc20_key -al C:68d1 .sha256_h5_init -al C:9b81 .cert_data_ptr -al C:a702 .tls_state -al C:b3e6 .drbg_seed -al C:83dc .ec_gx -al C:0fae .tls_close -al C:a768 .tls_ecdhe_pubkey -al C:83fc .ec_gy -al C:19b9 .entropy_init +al C:b138 .input_length +al C:ad08 .http_path_len +al C:a8d1 .tls_rec_len +al C:b5f0 .cc20_remain_hi +al C:6562 .sq_sh +al C:91d2 .cert_tbs_len +al C:13fa .tls_record_read +al C:6819 .aead_setup_chacha +al C:8bc3 .ec_jacobian_to_affine +al C:b872 .mul_src2_buf +al C:1069 .tls_recv_server_hello +al C:b985 .ecdsa_verify_tmp +al C:828c .fp_inv_iter +al C:6896 .aead_process_padded +al C:b50b .cc20_key +al C:6916 .sha256_h5_init +al C:9bc6 .cert_data_ptr +al C:a705 .tls_state +al C:b3e9 .drbg_seed +al C:8421 .ec_gx +al C:1026 .tls_close +al C:a76b .tls_ecdhe_pubkey +al C:8441 .ec_gy +al C:1a3d .entropy_init +al C:9297 .cert_buf al C:6000 .add32 -al C:9252 .cert_buf -al C:a706 .tls_recv_poll_count -al C:9a9e .tls_handle_cert_verify -al C:68cd .sha256_h4_init -al C:6dbd .sha256_ch -al C:3e7d .http_conn_hdr -al C:1fd9 .tls_finished_key -al C:9251 .cert_curve_id -al C:ae0c .http_resp_buf -al C:9c00 .mul_dma_lo -al C:8c1a .ecdsa_verify +al C:a709 .tls_recv_poll_count +al C:9ae3 .tls_handle_cert_verify +al C:6912 .sha256_h4_init +al C:6e02 .sha256_ch +al C:3e9c .http_conn_hdr +al C:205d .tls_finished_key +al C:9296 .cert_curve_id +al C:ae0f .http_resp_buf +al C:9d00 .mul_dma_lo +al C:8c5f .ecdsa_verify al C:0bfe .failed_msg -al C:b448 .cc20_state -al C:68d9 .sha256_h7_init -al C:9b84 .cert_parse_pos -al C:64b4 .sqtab_init -al C:9d00 .mul_dma_hi -al C:b5ba .aead_nonce -al C:1835 .tls_transcript_block -al C:6556 .mul_8x8 -al C:ac98 .hkdf_ikm_len -al C:7b71 .fe_inv_sqr_cnt -al C:9165 .oid_ec_pubkey -al C:ac99 .hkdf_label_ptr -al C:b5cc .aead_tag -al C:b86c .mul_cached_a -al C:68d5 .sha256_h6_init -al C:ae0a .http_req_len -al C:b2e2 .sha256_len -al C:65a6 .poly1305_multiply +al C:b44b .cc20_state +al C:691e .sha256_h7_init +al C:9bc9 .cert_parse_pos +al C:64c9 .sqtab_init +al C:9e00 .mul_dma_hi +al C:b5bd .aead_nonce +al C:18b9 .tls_transcript_block +al C:656b .mul_8x8 +al C:ac9b .hkdf_ikm_len +al C:91aa .oid_ec_pubkey +al C:7bb6 .fe_inv_sqr_cnt +al C:ac9c .hkdf_label_ptr +al C:b5d0 .aead_tag +al C:b871 .mul_cached_a +al C:691a .sha256_h6_init +al C:ae0d .http_req_len +al C:b2e5 .sha256_len +al C:65bb .poly1305_multiply al C:0c06 .done_msg -al C:ac9f .hkdf_out_len +al C:aca2 .hkdf_out_len al C:614c .rotl32_8 -al C:b7cc .x25_b -al C:82bc .fp_inv_x2 -al C:b6cc .x25_scalar -al C:0e24 .net_tcp_recv_cb -al C:b7ac .x25_a -al C:829c .fp_inv_x1 -al C:b80c .x25_cb -al C:9b88 .cert_end_lo -al C:ba50 .ev_u1_384 +al C:b7d1 .x25_b +al C:8301 .fp_inv_x2 +al C:b6d1 .x25_scalar +al C:0e53 .net_tcp_recv_cb +al C:b7b1 .x25_a +al C:82e1 .fp_inv_x1 +al C:b811 .x25_cb +al C:9bcd .cert_end_lo +al C:ba55 .ev_u1_384 al C:08b9 .print_string al C:0d57 .net_set_tcp_dest -al C:b82c .x25_e -al C:9b89 .cert_end_hi +al C:b831 .x25_e +al C:9bce .cert_end_hi al C:601f .add32_to_dst -al C:b447 .drbg_buf_idx +al C:b44a .drbg_buf_idx al C:0d13 .net_dhcp al C:0ccd .reu_fetch_mul_row al C:0ce1 .http_host_zimmers -al C:a854 .tls_hs_read_iv +al C:a857 .tls_hs_read_iv al C:6127 .rotl32_1 -al C:7f28 .fp_a_byte -al C:b3c5 .hmac_data_len -al C:a000 .mul38_lo_tab -al C:9b5c .cv_label -al C:72b0 .extra_sid_lo -al C:64b4 .fp_init_sqtab -al C:1ecb .tls_verify_finished -al C:b6ac .fe_p -al C:8f0f .der_skip -al C:a748 .tls_ecdhe_privkey -al C:1338 .tls_record_write -al C:1878 .tls_transcript_save +al C:7f6d .fp_a_byte +al C:b3c8 .hmac_data_len +al C:a100 .mul38_lo_tab +al C:9ba1 .cv_label +al C:0eba .cb_not_full +al C:72f5 .extra_sid_lo +al C:64c9 .fp_init_sqtab +al C:1f4f .tls_verify_finished +al C:b6b1 .fe_p +al C:8f54 .der_skip +al C:a74b .tls_ecdhe_privkey +al C:13bc .tls_record_write +al C:18fc .tls_transcript_save al C:616f .rotl32_4 -al C:b9f0 .ev_point_save +al C:b9f5 .ev_point_save al C:61f9 .rotl32_7 -al C:72b1 .extra_sid_hi -al C:b426 .drbg_seed_len -al C:670a .poly1305_final -al C:a400 .sqtab_hi -al C:7f29 .fp_b_byte -al C:1f28 .lbl_s_hs_traffic +al C:72f6 .extra_sid_hi +al C:b429 .drbg_seed_len +al C:671f .poly1305_final +al C:a500 .sqtab_hi +al C:7f6e .fp_b_byte +al C:1fac .lbl_s_hs_traffic al C:3b27 .http_get -al C:862c .ec_point_double -al C:1f51 .lbl_finished -al C:7e7d .fp_mul -al C:0f57 .tls_send -al C:1898 .tls_transcript_init -al C:1f1c .lbl_c_hs_traffic -al C:9b7e .cert_list_len_lo -al C:a834 .tls_hs_read_key -al C:0e5d .cb_copy_byte -al C:1ef5 .empty_hash -al C:a200 .sqtab_lo -al C:a704 .tls_recv_progress -al C:76dd .mul38_in -al C:0fb4 .tls_send_client_hello -al C:b6ec .x25_u +al C:8671 .ec_point_double +al C:1fd5 .lbl_finished +al C:7ec2 .fp_mul +al C:0fcf .tls_send +al C:191c .tls_transcript_init +al C:1fa0 .lbl_c_hs_traffic +al C:9bc3 .cert_list_len_lo +al C:a837 .tls_hs_read_key +al C:0ecb .cb_copy_byte +al C:1f79 .empty_hash +al C:a300 .sqtab_lo +al C:a707 .tls_recv_progress +al C:7722 .mul38_in +al C:102c .tls_send_client_hello +al C:b6f1 .x25_u al C:0a42 .banner_msg -al C:9b7d .cert_list_len_hi -al C:3e92 .http_bg_idx -al C:b031 .tls_app_ptr -al C:17e9 .tls_parse_encrypted_extensions +al C:9bc2 .cert_list_len_hi +al C:3eb1 .http_bg_idx +al C:b034 .tls_app_ptr +al C:186d .tls_parse_encrypted_extensions al C:0acc .net_fail_msg al C:0b74 .dns_fail_msg -al C:b5cb .aead_data_len +al C:b5ce .aead_data_len al C:603e .xor32 al C:0b1d .no_net_msg al C:0e05 .net_recv_ready al C:0b01 .dhcp_fail_msg -al C:b538 .poly_h -al C:6617 .poly1305_reduce -al C:6f26 .sha256_rotr1 -al C:a8cd .tls_rec_type -al C:6c71 .sha256_load_word -al C:6f5b .sha256_rotr2 -al C:1ae3 .tls_derive_secret +al C:b53b .poly_h +al C:662c .poly1305_reduce +al C:6f6b .sha256_rotr1 +al C:a8d0 .tls_rec_type +al C:6cb6 .sha256_load_word +al C:6fa0 .sha256_rotr2 +al C:1b67 .tls_derive_secret al C:0ae1 .net_ok_msg al C:0a2b .print_resp_body al C:0b88 .dns_ok_msg -al C:1834 .tls_hostname_len -al C:7196 .hmac_drbg_update +al C:18b8 .tls_hostname_len +al C:71db .hmac_drbg_update al C:605c .xor32_in_place -al C:6f61 .sha256_rotr6 -al C:ac93 .hkdf_salt_ptr -al C:647b .poly1305_clamp -al C:7b53 .fe_inv_dst +al C:6fa6 .sha256_rotr6 +al C:ac96 .hkdf_salt_ptr +al C:6490 .poly1305_clamp +al C:7b98 .fe_inv_dst al C:0ba4 .tcp_ok_msg -al C:7000 .hmac_sha256 -al C:6f6a .sha256_rotr7 -al C:b176 .sha_temp3 -al C:b5c6 .aead_aad_ptr -al C:7dfc .x25519_base -al C:6f48 .sha256_rotr8 +al C:7045 .hmac_sha256 +al C:6faf .sha256_rotr7 +al C:b179 .sha_temp3 +al C:b5c9 .aead_aad_ptr +al C:7e41 .x25519_base +al C:6f8d .sha256_rotr8 al C:0cff .net_init al C:0cf1 .http_host_foo -al C:7f2c .fp_p_hi -al C:8b3d .ec_sc_mask +al C:7f71 .fp_p_hi +al C:8b82 .ec_sc_mask al C:a700 .tcp_recv_head -al C:7303 .drbg_random_byte -al C:91ef .cert_pubkey_len -al C:8f41 .x509_parse_cert +al C:7348 .drbg_random_byte +al C:9234 .cert_pubkey_len +al C:8f86 .x509_parse_cert al C:0bc9 .tls_ok_msg -al C:0e76 .cb_done -al C:6f37 .sha256_rotl1 -al C:18b2 .tls_transcript_update -al C:a705 .tls_recv_sub_progress -al C:7f2b .fp_p_lo +al C:0eef .cb_done +al C:6f7c .sha256_rotl1 +al C:1936 .tls_transcript_update +al C:a708 .tls_recv_sub_progress +al C:7f70 .fp_p_lo al C:0b0e .dhcp_ok_msg -al C:a808 .tls_hs_write_key -al C:67c7 .aead_derive_otk -al C:3e77 .http_host_hdr -al C:ba80 .ev_u2_384 -al C:0eb7 .net_save_zp -al C:7b72 .x25519_clamp -al C:7e60 .fp_is_zero -al C:6a1d .sha256_update -al C:9b80 .cert_data_len_lo -al C:68a7 .aead_verify_tag -al C:b559 .poly_s -al C:72b2 .drbg_init_entropy -al C:b549 .poly_r -al C:6c80 .sha256_load_word_to_temp2 -al C:6dd9 .sha256_maj +al C:a80b .tls_hs_write_key +al C:67e8 .aead_derive_otk +al C:3e96 .http_host_hdr +al C:ba85 .ev_u2_384 +al C:0f2f .net_save_zp +al C:7bb7 .x25519_clamp +al C:7ea5 .fp_is_zero +al C:0ef0 .cb_next_lo +al C:6a62 .sha256_update +al C:9bc5 .cert_data_len_lo +al C:68ec .aead_verify_tag +al C:b55c .poly_s +al C:72f7 .drbg_init_entropy +al C:b54c .poly_r +al C:6cc5 .sha256_load_word_to_temp2 +al C:6e1e .sha256_maj al C:0b3d .http_get_msg -al C:ad00 .http_host_ptr -al C:9b7f .cert_data_len_hi -al C:0ec2 .net_restore_zp -al C:bb40 .ev_der_int_len -al C:b488 .cc20_work -al C:65a5 .mul_s_pg -al C:bb41 .ev_der_copy_cnt +al C:ad03 .http_host_ptr +al C:0ef1 .cb_next_hi +al C:9bc4 .cert_data_len_hi +al C:0f3a .net_restore_zp +al C:bb45 .ev_der_int_len +al C:b48b .cc20_work +al C:65ba .mul_s_pg +al C:bb46 .ev_der_copy_cnt al C:0b5a .https_get_msg -al C:7265 .hmac_drbg_instantiate -al C:ace0 .tls_master_secret -al C:6c8f .sha256_add_temp2_to_temp1 -al C:68dd .sha256_k -al C:735d .fe_add -al C:9854 .tls_handle_certificate -al C:6f70 .sha256_rotr11 -al C:b427 .drbg_output -al C:6554 .poly_prod_lo -al C:1aff .tls_derive_handshake_keys -al C:6acb .sha256_process_block -al C:6f7c .sha256_rotr13 -al C:17f4 .tls_hostname -al C:9b8a .cert_bs_len -al C:b011 .http_line_buf -al C:80ab .fp_mod_inv -al C:6555 .poly_prod_hi +al C:72aa .hmac_drbg_instantiate +al C:ace3 .tls_master_secret +al C:6cd4 .sha256_add_temp2_to_temp1 +al C:6922 .sha256_k +al C:73a2 .fe_add +al C:9899 .tls_handle_certificate +al C:6fb5 .sha256_rotr11 +al C:b42a .drbg_output +al C:6569 .poly_prod_lo +al C:1b83 .tls_derive_handshake_keys +al C:6b10 .sha256_process_block +al C:6fc1 .sha256_rotr13 +al C:1878 .tls_hostname +al C:b014 .http_line_buf +al C:80f0 .fp_mod_inv +al C:9bcf .cert_bs_len +al C:656a .poly_prod_hi al C:0d67 .net_tcp_send -al C:66b4 .poly1305_update -al C:1126 .tls_select_keys -al C:3e90 .http_crlf -al C:6f8b .sha256_rotr17 -al C:6f94 .sha256_rotr18 -al C:6fa0 .sha256_rotr19 -al C:7384 .fe_sub -al C:129c .tls_record_decrypt -al C:918b .cert_tbs_ptr -al C:ad03 .http_path_ptr -al C:a8b8 .tls_write_seq +al C:66c9 .poly1305_update +al C:119e .tls_select_keys +al C:3eaf .http_crlf +al C:6fd0 .sha256_rotr17 +al C:6fd9 .sha256_rotr18 +al C:6fe5 .sha256_rotr19 +al C:73c9 .fe_sub +al C:0ece .cb_store +al C:131a .tls_record_decrypt +al C:91d0 .cert_tbs_ptr +al C:ad06 .http_path_ptr +al C:a8bb .tls_write_seq al C:0d1f .net_poll -al C:a7e8 .tls_transcript_h0 -al C:7f6d .fp_mod_add -al C:6dfd .sha256_add_to_hash -al C:a7ec .tls_transcript_h1 -al C:b534 .cc20_counter -al C:a7f0 .tls_transcript_h2 -al C:a600 .tcp_recv_buf -al C:675f .aead_encrypt -al C:a7f4 .tls_transcript_h3 -al C:ac9c .hkdf_context_ptr -al C:b569 .poly_product -al C:b00f .http_hdr_match -al C:a7f8 .tls_transcript_h4 -al C:ac00 .tls_hs_len -al C:1f15 .empty_context -al C:793f .fe_inv -al C:a7fc .tls_transcript_h5 -al C:b84c .x25_basepoint -al C:7e6e .fp_rshift1 -al C:a800 .tls_transcript_h6 -al C:0e7c .net_init_cb_addrs -al C:a804 .tls_transcript_h7 +al C:a7eb .tls_transcript_h0 +al C:7fb2 .fp_mod_add +al C:6e42 .sha256_add_to_hash +al C:a7ef .tls_transcript_h1 +al C:b537 .cc20_counter +al C:a7f3 .tls_transcript_h2 +al C:6774 .aead_encrypt +al C:a7f7 .tls_transcript_h3 +al C:ac9f .hkdf_context_ptr +al C:b56c .poly_product +al C:b012 .http_hdr_match +al C:a7fb .tls_transcript_h4 +al C:ac03 .tls_hs_len +al C:1f99 .empty_context +al C:7984 .fe_inv +al C:a7ff .tls_transcript_h5 +al C:b851 .x25_basepoint +al C:7eb3 .fp_rshift1 +al C:a803 .tls_transcript_h6 +al C:0ef4 .net_init_cb_addrs +al C:a807 .tls_transcript_h7 al C:0879 .do_net_init al C:0ccb .reu_init_a -al C:b64c .fe_tmp2 -al C:76e0 .fe_sqr -al C:b66c .fe_tmp3 -al C:b91f .ecdsa_sig_len -al C:b4c8 .cc20_keystream -al C:7fcd .fp_mod_sub +al C:b651 .fe_tmp2 +al C:7725 .fe_sqr +al C:b671 .fe_tmp3 +al C:b924 .ecdsa_sig_len +al C:b4cb .cc20_keystream +al C:8012 .fp_mod_sub al C:0b90 .tcp_fail_msg -al C:b62c .fe_tmp1 +al C:b631 .fe_tmp1 al C:0ccc .reu_init_b -al C:0ecf .net_send_len -al C:16ce .tls_parse_server_hello -al C:b1c2 .sha256_w -al C:8081 .fp_rem -al C:a8d0 .tls_rec_buf -al C:13a9 .tls_recv_record -al C:ac02 .hkdf_prk -al C:b68c .fe_tmp4 -al C:1f4c .lbl_key -al C:75db .fe_reduce_wide -al C:b74c .x25_z2 -al C:b76c .x25_x3 -al C:ac42 .hkdf_info_buf -al C:b72c .x25_x2 -al C:b78c .x25_z3 +al C:0f47 .net_send_len +al C:1752 .tls_parse_server_hello +al C:b1c5 .sha256_w +al C:80c6 .fp_rem +al C:a8d3 .tls_rec_buf +al C:142d .tls_recv_record +al C:ac05 .hkdf_prk +al C:b691 .fe_tmp4 +al C:1fd0 .lbl_key +al C:7620 .fe_reduce_wide +al C:b751 .x25_z2 +al C:b771 .x25_x3 +al C:ac45 .hkdf_info_buf +al C:b731 .x25_x2 +al C:b791 .x25_z3 al C:08ca .do_http_get -al C:1503 .tls_recv_count +al C:1587 .tls_recv_count al C:0d29 .net_dns_resolve al C:609f .rotr32_8 -al C:1913 .tls_transcript_hash -al C:9f00 .sqtab2_hi -al C:b2e4 .hmac_key +al C:1997 .tls_transcript_hash +al C:a000 .sqtab2_hi +al C:a704 .tcp_recv_overflow +al C:b2e7 .hmac_key al C:0bfa .ok_msg -al C:1076 .tls_recv_encrypted -al C:b324 .hmac_opad_block -al C:b5ec .fe_wide -al C:b172 .sha_h -al C:9e00 .sqtab2_lo -al C:9b8b .cv_sig_len -al C:a88c .tls_app_read_key -al C:85fc .ec_set_modp -al C:0ed1 .tls_connect -al C:b00e .http_parse_state -al C:7281 .hmac_drbg_generate -al C:72af .extra_sid_count -al C:b166 .sha_e -al C:ac9b .hkdf_label_len +al C:10ee .tls_recv_encrypted +al C:b327 .hmac_opad_block +al C:b5f1 .fe_wide +al C:b175 .sha_h +al C:9f00 .sqtab2_lo +al C:9bd0 .cv_sig_len +al C:a88f .tls_app_read_key +al C:8641 .ec_set_modp +al C:0f49 .tls_connect +al C:b011 .http_parse_state +al C:72c6 .hmac_drbg_generate +al C:72f4 .extra_sid_count +al C:b169 .sha_e +al C:ac9e .hkdf_label_len al C:61d4 .rotr32_1 -al C:b162 .sha_d -al C:0e35 .cb_load_ptr_lo -al C:ac96 .hkdf_ikm_ptr -al C:b16e .sha_g -al C:b16a .sha_f -al C:835c .ec_p -al C:b156 .sha_a +al C:b165 .sha_d +al C:0e67 .cb_load_ptr_lo +al C:ac99 .hkdf_ikm_ptr +al C:b171 .sha_g +al C:b16d .sha_f +al C:83a1 .ec_p +al C:b159 .sha_a al C:60c5 .rotr32_4 -al C:65a3 .mul_a -al C:b70c .x25_result -al C:14d0 .tls_record_recv_and_decrypt -al C:a8ac .tls_app_read_iv -al C:0e3b .cb_load_ptr_hi -al C:65a4 .mul_b -al C:7838 .fe_mul_a24 -al C:b15e .sha_c -al C:b88e .ecdsa_hash -al C:9852 .cert_buf_len -al C:8eaf .der_read_tag -al C:1875 .tls_transcript_block_len -al C:7e20 .fp_zero -al C:b15a .sha_b +al C:65b8 .mul_a +al C:b711 .x25_result +al C:1554 .tls_record_recv_and_decrypt +al C:a8af .tls_app_read_iv +al C:0e6d .cb_load_ptr_hi +al C:65b9 .mul_b +al C:b161 .sha_c +al C:787d .fe_mul_a24 +al C:b893 .ecdsa_hash +al C:9897 .cert_buf_len +al C:8ef4 .der_read_tag +al C:18f9 .tls_transcript_block_len +al C:7e65 .fp_zero +al C:b15d .sha_b al C:6124 .rotr32_7 -al C:6fe3 .sha256_shr10 -al C:1876 .tls_transcript_total_lo -al C:733f .fe_copy -al C:9189 .der_len -al C:6a93 .sha256_final -al C:1af9 .hkdf_tls13_prefix +al C:7028 .sha256_shr10 +al C:18fa .tls_transcript_total_lo +al C:7384 .fe_copy +al C:91ce .der_len +al C:6ad8 .sha256_final +al C:1b7d .hkdf_tls13_prefix al C:6255 .chacha20_init -al C:73ba .fe_reduce_final -al C:916c .oid_prime256v1 -al C:b00c .http_resp_len -al C:3e68 .http_get_verb -al C:767e .mul_by_38 -al C:831c .fp_r2 -al C:1877 .tls_transcript_total_hi -al C:7b85 .x25519_scalarmult -al C:833c .fp_r3 -al C:837c .ec_n -al C:82dc .fp_r0 -al C:3dfc .http_get_plain -al C:82fc .fp_r1 -al C:a708 .tls_client_random -al C:8605 .ec_set_modn -al C:11d2 .tls_seq_increment -al C:8dd3 .ecdsa_parse_der_sig -al C:83bc .ec_b +al C:73ff .fe_reduce_final +al C:91b1 .oid_prime256v1 +al C:b00f .http_resp_len +al C:3e87 .http_get_verb +al C:76c3 .mul_by_38 +al C:8361 .fp_r2 +al C:18fb .tls_transcript_total_hi +al C:8381 .fp_r3 +al C:7bca .x25519_scalarmult +al C:83c1 .ec_n +al C:8321 .fp_r0 +al C:3e1b .http_get_plain +al C:8341 .fp_r1 +al C:a70b .tls_client_random +al C:864a .ec_set_modn +al C:124a .tls_seq_increment +al C:8e18 .ecdsa_parse_der_sig +al C:8401 .ec_b al C:0cfd .http_path_root -al C:839c .ec_a -al C:a8c0 .tls_read_seq +al C:83e1 .ec_a +al C:a8c3 .tls_read_seq al C:6215 .zero32 -al C:ad0a .http_req_buf -al C:ad06 .http_port -al C:b035 .input_buffer -al C:3e93 .http_bg_src -al C:7e2a .fp_cmp -al C:b3c6 .hmac_result -al C:b7ec .x25_da -al C:b9b0 .ev_u1 -al C:3c03 .http_build_get -al C:9b83 .cert_data_offset -al C:0e11 .net_recv_byte -al C:b8be .ecdsa_hash_len +al C:ad0d .http_req_buf +al C:ad09 .http_port +al C:b038 .input_buffer +al C:3eb2 .http_bg_src +al C:7e6f .fp_cmp +al C:b3c9 .hmac_result +al C:b7f1 .x25_da +al C:b9b5 .ev_u1 +al C:3c22 .http_build_get +al C:9bc8 .cert_data_offset +al C:0e19 .net_recv_byte +al C:b8c3 .ecdsa_hash_len al C:0bdb .send_fail_msg -al C:b58a .poly1305_tag -al C:b9d0 .ev_u2 -al C:b5c9 .aead_data_ptr +al C:b58d .poly1305_tag +al C:b9d5 .ev_u2 +al C:b5cc .aead_data_ptr al C:63c7 .chacha20_block -al C:b528 .cc20_nonce -al C:1fb9 .tls_verify_data -al C:b364 .hmac_data_buf -al C:6ca9 .sha256_sig0 -al C:98d9 .x509_extract_pubkey -al C:7b55 .fe_inv_sqrn_tmp2 -al C:b033 .tls_app_len -al C:6cee .sha256_sig1 +al C:b52b .cc20_nonce +al C:203d .tls_verify_data +al C:b367 .hmac_data_buf +al C:6cee .sha256_sig0 +al C:991e .x509_extract_pubkey +al C:7b9a .fe_inv_sqrn_tmp2 +al C:b036 .tls_app_len +al C:6d33 .sha256_sig1 al C:0d8b .net_tcp_close -al C:860e .ec_mulp -al C:6d78 .sha256_big_sig1 -al C:678e .aead_decrypt -al C:6d33 .sha256_big_sig0 -al C:9bce .zp_save_buf -al C:11ea .tls_record_encrypt -al C:a728 .tls_server_random -al C:ac22 .hkdf_okm -al C:7e38 .fp_add -al C:b010 .http_line_idx -al C:119e .tls_build_nonce -al C:a788 .tls_server_pubkey -al C:76df .mul38_hi -al C:1a3a .hkdf_expand -al C:1f34 .lbl_c_ap_traffic -al C:1f40 .lbl_s_ap_traffic -al C:76de .mul38_lo -al C:0e24 .cb_load_len_lo -al C:8c2a .ecdsa_verify_256 -al C:10ee .tls_send_finished -al C:137a .tls_enc_aead_len +al C:8653 .ec_mulp +al C:6dbd .sha256_big_sig1 +al C:67a9 .aead_decrypt +al C:6d78 .sha256_big_sig0 +al C:9c13 .zp_save_buf +al C:1262 .tls_record_encrypt +al C:a72b .tls_server_random +al C:ac25 .hkdf_okm +al C:7e7d .fp_add +al C:b013 .http_line_idx +al C:1216 .tls_build_nonce +al C:a78b .tls_server_pubkey +al C:7724 .mul38_hi +al C:1abe .hkdf_expand +al C:1fb8 .lbl_c_ap_traffic +al C:1fc4 .lbl_s_ap_traffic +al C:7723 .mul38_lo +al C:0e53 .cb_load_len_lo +al C:8c6f .ecdsa_verify_256 +al C:1166 .tls_send_finished +al C:13fe .tls_enc_aead_len al C:0bb3 .tls_fail_msg -al C:1cca .tls_derive_traffic_keys -al C:8f28 .der_match_oid -al C:9b8c .tls_ecdh_generate_keypair -al C:a8c8 .tls_rec_header -al C:1502 .tls_recv_state -al C:bab0 .ev_point_save_384 -al C:7e4b .fp_sub -al C:0e2a .cb_load_len_hi -al C:3e6c .http_version -al C:1f4f .lbl_iv +al C:1d4e .tls_derive_traffic_keys +al C:8f6d .der_match_oid +al C:9bd1 .tls_ecdh_generate_keypair +al C:a8cb .tls_rec_header +al C:1586 .tls_recv_state +al C:bab5 .ev_point_save_384 +al C:7e90 .fp_sub +al C:0e59 .cb_load_len_hi +al C:3e8b .http_version +al C:1fd3 .lbl_iv al C:08b8 .net_initialized -al C:aaf4 .tls_nonce -al C:b59a .aead_key +al C:aaf7 .tls_nonce +al C:b59d .aead_key al C:6235 .cc20_qr_table -al C:1f99 .tls_derived_tmp -al C:85dc .ec_t6 -al C:aca0 .tls_early_secret -al C:85bc .ec_t5 +al C:201d .tls_derived_tmp +al C:8621 .ec_t6 +al C:aca3 .tls_early_secret +al C:8601 .ec_t5 al C:607a .rotr32_16 -al C:3ce4 .http_recv_response -al C:80a3 .fp_bm -al C:859c .ec_t4 -al C:841c .ec_p1 -al C:857c .ec_t3 -al C:a701 .tcp_recv_tail -al C:855c .ec_t2 +al C:3d03 .http_recv_response +al C:80e8 .fp_bm +al C:85e1 .ec_t4 +al C:8461 .ec_p1 +al C:85c1 .ec_t3 +al C:a702 .tcp_recv_tail +al C:85a1 .ec_t2 al C:60c2 .rotr32_12 -al C:84dc .ec_p3 -al C:853c .ec_t1 -al C:73d0 .fe_cswap -al C:0f85 .tls_recv -al C:8249 .fp_chk_one -al C:847c .ec_p2 +al C:8521 .ec_p3 +al C:8581 .ec_t1 +al C:7415 .fe_cswap +al C:0ffd .tls_recv +al C:828e .fp_chk_one +al C:84c1 .ec_p2 al C:0d95 .net_print_ip -al C:9179 .oid_sha256_ecdsa -al C:827c .fp_inv_v +al C:91be .oid_sha256_ecdsa +al C:82c1 .fp_inv_v al C:091e .do_https_get -al C:6695 .poly1305_block -al C:a7a8 .tls_shared_secret -al C:825c .fp_inv_u -al C:9174 .oid_secp384r1 -al C:ad08 .http_status -al C:b17a .sha_t1 -al C:80a2 .fp_bc -al C:654a .sq_acc +al C:66aa .poly1305_block +al C:a7ab .tls_shared_secret +al C:91b9 .oid_secp384r1 +al C:82a1 .fp_inv_u +al C:ad0b .http_status +al C:b17d .sha_t1 +al C:80e7 .fp_bc +al C:655f .sq_acc al C:6225 .cc20_constants -al C:b17e .sha_t2 -al C:b88d .ecdsa_curve_id -al C:ad02 .http_host_len -al C:acc0 .tls_handshake_secret -al C:646a .poly1305_init -al C:b13a .sha256_h1 -al C:9b87 .cert_ext_len_lo -al C:b136 .sha256_h0 -al C:a880 .tls_app_write_iv -al C:b142 .sha256_h3 -al C:b304 .hmac_val +al C:b181 .sha_t2 +al C:b892 .ecdsa_curve_id +al C:ad05 .http_host_len +al C:acc3 .tls_handshake_secret +al C:647f .poly1305_init +al C:b13d .sha256_h1 +al C:9bcc .cert_ext_len_lo +al C:b139 .sha256_h0 +al C:a883 .tls_app_write_iv +al C:b145 .sha256_h3 +al C:b307 .hmac_val al C:6282 .chacha20_quarter_round -al C:b13e .sha256_h2 -al C:137c .tls_send_record -al C:6552 .sq_i -al C:b14a .sha256_h5 -al C:ac95 .hkdf_salt_len -al C:9b86 .cert_ext_len_hi -al C:b146 .sha256_h4 -al C:b152 .sha256_h7 -al C:b14e .sha256_h6 -al C:a703 .tls_last_state -al C:b2c2 .sha256_hash -al C:0e55 .cb_loop -al C:14c5 .tls_record_send_encrypted -al C:b5c8 .aead_aad_len -al C:9181 .oid_sha384_ecdsa -al C:73a6 .fe_cmp_p -al C:19d1 .hkdf_extract -al C:8eba .der_read_length +al C:b141 .sha256_h2 +al C:1400 .tls_send_record +al C:6567 .sq_i +al C:b14d .sha256_h5 +al C:ac98 .hkdf_salt_len +al C:b149 .sha256_h4 +al C:9bcb .cert_ext_len_hi +al C:b155 .sha256_h7 +al C:b151 .sha256_h6 +al C:a706 .tls_last_state +al C:b2c5 .sha256_hash +al C:0e84 .cb_loop +al C:1549 .tls_record_send_encrypted +al C:b5cb .aead_aad_len +al C:91c6 .oid_sha384_ecdsa +al C:73eb .fe_cmp_p +al C:1a55 .hkdf_extract +al C:8eff .der_read_length diff --git a/src/constants.asm b/src/constants.asm index 23669a2..3a0b8c3 100644 --- a/src/constants.asm +++ b/src/constants.asm @@ -56,7 +56,8 @@ sha256_round = $12 ; 1 byte cc20_round = $14 ; 1 byte cc20_qr_idx = $15 ; 1 byte cc20_data_ptr = $16 ; 2 bytes ($16-$17) -cc20_remain = $18 ; 1 byte (also poly1305_update counter) +cc20_remain = $18 ; low byte of 16-bit ChaCha20/AEAD length + ; (high byte = cc20_remain_hi in data.asm) cc20_buf_pos = $19 ; 1 byte ; --- mult66 indirect-indexed multiply pointers (time-shared with ChaCha20) --- @@ -239,5 +240,11 @@ TLS_ALERT_FATAL = 2 ; Buffer sizes ; ============================================================================= TLS_RECORD_MAX = 512 ; negotiated via max_fragment_length -TCP_RECV_BUF_SIZE = 256 ; ring buffer for ip65 callback data (8-bit wrap) +TCP_RECV_BUF_SIZE = 4096 ; ring buffer for ip65 callback data (masked wrap) +TCP_RECV_MASK = $0fff ; 12-bit mask for 16-bit head/tail wrap HTTP_BUF_SIZE = 256 ; HTTP request/response line buffer + +; TCP receive ring buffer lives at $C000-$CFFF (4KB always-RAM region between +; BASIC ROM shadow and I/O). Declared here as an equate rather than in +; data.asm so the PRG/BSS stays small — the RAM exists regardless. +tcp_recv_buf = $c000 diff --git a/src/crypto/aead.asm b/src/crypto/aead.asm index 4f6b82e..d6aa51a 100644 --- a/src/crypto/aead.asm +++ b/src/crypto/aead.asm @@ -10,7 +10,7 @@ ; aead_aad_ptr (2 bytes) -- pointer to AAD ; aead_aad_len (1 byte) -- AAD length (0-255) ; aead_data_ptr (2 bytes) -- pointer to plaintext/ciphertext -; aead_data_len (1 byte) -- data length (0-255) +; aead_data_len (2 bytes) -- data length (16-bit; supports records >255) ; ; Output: ; Ciphertext written in-place at aead_data_ptr @@ -48,6 +48,8 @@ aead_encrypt: sta cc20_data_ptr+1 lda aead_data_len sta cc20_remain + lda aead_data_len+1 + sta cc20_remain_hi jsr chacha20_encrypt ; --- 3. Compute Poly1305 tag --- @@ -92,6 +94,8 @@ aead_decrypt: sta cc20_data_ptr+1 lda aead_data_len sta cc20_remain + lda aead_data_len+1 + sta cc20_remain_hi jsr chacha20_encrypt ; XOR = decrypt lda #0 ; success @@ -181,6 +185,8 @@ aead_compute_tag: lda aead_aad_len beq @skip_aad sta cc20_remain + lda #0 ; AAD length is 8-bit, high byte zero + sta cc20_remain_hi lda aead_aad_ptr sta zp_ptr lda aead_aad_ptr+1 @@ -189,9 +195,14 @@ aead_compute_tag: @skip_aad: ; --- Process ciphertext --- + ; aead_data_len is 16-bit; skip only if both bytes zero. lda aead_data_len + ora aead_data_len+1 beq @skip_ct + lda aead_data_len sta cc20_remain + lda aead_data_len+1 + sta cc20_remain_hi lda aead_data_ptr sta zp_ptr lda aead_data_ptr+1 @@ -211,7 +222,9 @@ aead_compute_tag: lda aead_aad_len sta aead_scratch ; low byte of AAD length (rest is 0) lda aead_data_len - sta aead_scratch+8 ; low byte of CT length (rest is 0) + sta aead_scratch+8 ; low byte of CT length + lda aead_data_len+1 + sta aead_scratch+9 ; high byte of CT length (rest is 0) ; Process as one 16-byte block with hibit=1 lda #0) +; Input: zp_ptr = data pointer +; cc20_remain:cc20_remain_hi = 16-bit length (>0) ; All blocks processed with hibit=1. Last partial block is zero-padded to 16. ; ; Clobbers: A, X, Y ; ============================================================================= aead_process_padded: @next_block: + ; If high byte of remaining is nonzero, there's certainly >= 16 left. + lda cc20_remain_hi + bne @full_block lda cc20_remain beq @done cmp #16 - bcc @partial ; < 16 bytes left - + bcc @partial ; < 16 bytes left (and high byte is 0) +@full_block: ; Full 16-byte block with hibit=1 lda #1 jsr poly1305_block @@ -253,10 +270,14 @@ aead_process_padded: adc #0 sta zp_ptr+1 + ; 16-bit subtract: (cc20_remain_hi:cc20_remain) -= 16 lda cc20_remain sec sbc #16 sta cc20_remain + lda cc20_remain_hi + sbc #0 + sta cc20_remain_hi jmp @next_block @partial: diff --git a/src/crypto/chacha20.asm b/src/crypto/chacha20.asm index 70fdda8..88e67d6 100644 --- a/src/crypto/chacha20.asm +++ b/src/crypto/chacha20.asm @@ -272,7 +272,7 @@ chacha20_block: ; ; Inputs: ; cc20_data_ptr ($16-$17) = pointer to plaintext/ciphertext (in-place XOR) -; cc20_remain ($18) = number of bytes to process (0-255) +; cc20_remain ($18) : cc20_remain_hi = 16-bit byte count to process ; State must already be initialized via chacha20_init ; ; The function generates keystream blocks and XORs them with the data. @@ -281,16 +281,21 @@ chacha20_block: ; ============================================================================= chacha20_encrypt: lda cc20_remain + ora cc20_remain_hi beq @enc_done ; nothing to do @next_block: ; Generate a keystream block jsr chacha20_block - ; Determine how many bytes to XOR from this block + ; Determine how many bytes to XOR from this block. + ; If high byte is nonzero, >= 256 remain, so it's a full 64-byte block. + lda cc20_remain_hi + bne @full lda cc20_remain cmp #64 bcc @partial ; < 64 bytes remaining +@full: lda #64 ; full block @partial: sta cc20_buf_pos ; bytes to XOR this iteration @@ -315,12 +320,19 @@ chacha20_encrypt: adc #0 sta cc20_data_ptr+1 - ; Subtract processed bytes from remaining + ; 16-bit subtract: remain -= buf_pos lda cc20_remain sec sbc cc20_buf_pos sta cc20_remain - bne @next_block ; more bytes to process + lda cc20_remain_hi + sbc #0 + sta cc20_remain_hi + + ; Loop while 16-bit remain != 0 + lda cc20_remain + ora cc20_remain_hi + bne @next_block @enc_done: rts diff --git a/src/data.asm b/src/data.asm index f622009..260fd53 100644 --- a/src/data.asm +++ b/src/data.asm @@ -55,9 +55,12 @@ sqtab_hi: !fill 512, 0 ; ============================================================================= ; Network layer buffers ; ============================================================================= -tcp_recv_buf: !fill 256, 0 ; TCP receive ring buffer (256 bytes, wraps) -tcp_recv_head: !byte 0 ; read position -tcp_recv_tail: !byte 0 ; write position (updated by ip65 callback) +; NOTE: tcp_recv_buf itself is an equate in constants.asm pointing at $C000 +; (4KB always-RAM region between BASIC ROM shadow and I/O). The buffer does +; not occupy any .prg bytes here. +tcp_recv_head: !word 0 ; read position (16-bit, masked with TCP_RECV_MASK) +tcp_recv_tail: !word 0 ; write position (updated by ip65 callback, 16-bit) +tcp_recv_overflow: !byte 0 ; set to 1 by callback if ring fills up ; ============================================================================= ; TLS state @@ -241,9 +244,11 @@ aead_nonce: !fill 12, 0 aead_aad_ptr: !word 0 aead_aad_len: !byte 0 aead_data_ptr: !word 0 -aead_data_len: !byte 0 +aead_data_len: !word 0 ; data length (16-bit; TLS records can be up to ~4KB) aead_tag: !fill 16, 0 aead_scratch: !fill 16, 0 ; Poly1305 padding/length block +cc20_remain_hi: !byte 0 ; high byte of 16-bit ChaCha20/Poly1305 length counter + ; (low byte lives in ZP at cc20_remain = $18) ; ============================================================================= ; fe25519 field arithmetic (from c64-wireguard) diff --git a/src/http.asm b/src/http.asm index 40e7f23..4f2c55a 100644 --- a/src/http.asm +++ b/src/http.asm @@ -74,7 +74,9 @@ http_get: lda http_req_len+1 sta tls_app_len+1 jsr tls_send - bcs @close_error + bcc + + jmp @close_error ++ ; --- 8. Receive response via TLS --- ; Initialise parser state @@ -101,18 +103,36 @@ http_get: lda tls_app_ptr+1 sta zp_ptr+1 - ; Feed decrypted bytes into the TCP ring buffer + ; Feed decrypted bytes into the TCP ring buffer. + ; Ring is 1024 bytes with 16-bit masked head/tail. We compute the + ; destination absolute address per-byte via SMC on @feed_store. ldy #0 @feed_loop: cpy tls_app_len ; low byte only (TLS records < 256) beq @feed_done lda (zp_ptr),y - ldx tcp_recv_tail - sta tcp_recv_buf,x - inx - stx tcp_recv_tail + pha + ; dest = tcp_recv_buf + tail + clc + lda tcp_recv_tail+0 + adc #tcp_recv_buf + sta @feed_store+2 + pla +@feed_store: + sta $ffff ; SMC: patched above + ; tail = (tail + 1) & TCP_RECV_MASK + inc tcp_recv_tail+0 + bne @feed_mask + inc tcp_recv_tail+1 +@feed_mask: + lda tcp_recv_tail+1 + and #>TCP_RECV_MASK + sta tcp_recv_tail+1 iny - bne @feed_loop ; always branches + bne @feed_loop ; always branches (tls_app_len < 256) @feed_done: ; Parse from ring buffer jsr http_recv_response diff --git a/src/net.asm b/src/net.asm index 54c8a69..a7bb842 100644 --- a/src/net.asm +++ b/src/net.asm @@ -216,28 +216,60 @@ net_print_ip: ; ============================================================================= ; net_recv_ready - check if data is available in receive ring buffer ; Output: C=0 if data available, C=1 if empty +; +; The ring is empty iff head == tail (16-bit compare). ; ============================================================================= net_recv_ready: - lda tcp_recv_head - cmp tcp_recv_tail - beq @empty - clc + lda tcp_recv_head+0 + cmp tcp_recv_tail+0 + bne @has + lda tcp_recv_head+1 + cmp tcp_recv_tail+1 + bne @has + sec ; empty rts -@empty: - sec +@has: + clc rts ; ============================================================================= ; net_recv_byte - read one byte from receive ring buffer ; Output: A = byte, C=0 success, C=1 buffer empty +; +; Ring addressing: effective = tcp_recv_buf + (head & TCP_RECV_MASK). +; Uses self-modifying code on @nrb_ld's absolute operand — no ZP scratch +; needed (important: $FB-$FE and $02-$1B are both time-shared with ip65 +; and crypto). ; ============================================================================= net_recv_byte: - lda tcp_recv_head - cmp tcp_recv_tail + ; empty? (16-bit compare; head/tail are both kept in range [0,TCP_RECV_MASK]) + lda tcp_recv_head+0 + cmp tcp_recv_tail+0 + bne @not_empty + lda tcp_recv_head+1 + cmp tcp_recv_tail+1 beq @empty - tax - lda tcp_recv_buf,x - inc tcp_recv_head ; wraps at 256 +@not_empty: + ; effective address = tcp_recv_buf + head (head is already masked) + clc + lda tcp_recv_head+0 + adc #tcp_recv_buf + sta @nrb_ld+2 +@nrb_ld: + lda $ffff ; SMC: patched above + pha + ; head = (head + 1) & TCP_RECV_MASK + inc tcp_recv_head+0 + bne @nrb_mask + inc tcp_recv_head+1 +@nrb_mask: + lda tcp_recv_head+1 + and #>TCP_RECV_MASK ; = $0f (12-bit mask high byte) + sta tcp_recv_head+1 + pla clc rts @empty: @@ -264,7 +296,9 @@ cb_load_len_hi: sta cb_remaining+1 ; if length == 0, nothing to copy ora cb_remaining - beq cb_done + bne + + jmp cb_done ++ ; --- Read inbound data pointer (16-bit), patch copy source --- cb_load_ptr_lo: @@ -284,21 +318,59 @@ cb_load_ptr_hi: lda #0 sta cb_remaining+1 + - ; Copy loop: X = source index, Y = ring buffer tail + ; Copy loop: X = source index; ring store uses SMC on cb_store ldx #0 - ldy tcp_recv_tail cb_loop: ; Check 16-bit remaining count lda cb_remaining ora cb_remaining+1 - beq cb_done + bne + + jmp cb_done ++ + + ; --- Overflow check: if ((tail+1) & $3FF) == head, ring is full --- + lda tcp_recv_tail+0 + clc + adc #1 + sta cb_next_lo + lda tcp_recv_tail+1 + adc #0 + and #>TCP_RECV_MASK ; = $0f (12-bit mask high byte) + sta cb_next_hi + lda cb_next_lo + cmp tcp_recv_head+0 + bne cb_not_full + lda cb_next_hi + cmp tcp_recv_head+1 + bne cb_not_full + ; ring full — record overflow and stop copying + lda #1 + sta tcp_recv_overflow + jmp cb_done + +cb_not_full: + ; Patch destination absolute address for this store: + ; dest = tcp_recv_buf + tail + clc + lda tcp_recv_tail+0 + adc #tcp_recv_buf + sta cb_store+2 cb_copy_byte: lda $ffff,x ; SMC: patched to ip65 inbound data base address - sta tcp_recv_buf,y - iny ; tail wraps at 256 (8-bit) +cb_store: + sta $ffff ; SMC: patched to tcp_recv_buf + tail inx + ; tail = next (already computed above) + lda cb_next_lo + sta tcp_recv_tail+0 + lda cb_next_hi + sta tcp_recv_tail+1 + ; decrement 16-bit remaining lda cb_remaining sec @@ -309,9 +381,11 @@ cb_copy_byte: jmp cb_loop cb_done: - sty tcp_recv_tail ; store updated tail rts +cb_next_lo: !byte 0 ; scratch: (tail+1) & mask, low +cb_next_hi: !byte 0 ; scratch: (tail+1) & mask, high + cb_remaining: !word 0 ; bytes remaining to copy (callback-local) ; ============================================================================= diff --git a/src/tls_record.asm b/src/tls_record.asm index 8110cec..2222dc4 100644 --- a/src/tls_record.asm +++ b/src/tls_record.asm @@ -266,11 +266,11 @@ tls_record_encrypt: lda #>tls_rec_buf sta aead_data_ptr+1 - ; aead_data_len = AEAD plaintext length - ; Note: aead_data_len is 1 byte, so max 255. For records >255 bytes - ; this would need extension. For now, store low byte. + ; aead_data_len = AEAD plaintext length (16-bit) lda tls_enc_aead_len sta aead_data_len + lda tls_enc_aead_len+1 + sta aead_data_len+1 ; --- 6. Encrypt --- jsr aead_encrypt @@ -387,9 +387,11 @@ tls_record_decrypt: lda #>tls_rec_buf sta aead_data_ptr+1 - ; aead_data_len = ciphertext_len (low byte) + ; aead_data_len = ciphertext_len (16-bit) lda tls_enc_aead_len sta aead_data_len + lda tls_enc_aead_len+1 + sta aead_data_len+1 ; --- 5. Decrypt and verify --- jsr aead_decrypt diff --git a/tests/test_phase3_https.py b/tests/test_phase3_https.py index 3133a15..c2748e3 100644 --- a/tests/test_phase3_https.py +++ b/tests/test_phase3_https.py @@ -63,8 +63,8 @@ DHCP_TIMEOUT = 90.0 # TLS handshake dominates: X25519 keygen ~3.6 min PLUS X25519 shared secret # ~3.6 min PLUS HKDF (many HMAC-SHA256) ~2 min PLUS ECDSA P-256 verify ~2 min. -# Budget 15 minutes total. -HTTPS_TIMEOUT = 900.0 +# Budget 30 minutes total to cover full handshake + app data round-trip. +HTTPS_TIMEOUT = 1800.0 def _skip(reason: str) -> int: From eab75709630f987d815a187153f938aa6b157348 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Tue, 14 Apr 2026 16:56:06 -0500 Subject: [PATCH 09/22] Phase 3 screen markers + net_poll counters + diagnostic instrumentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds on-screen progress markers at each TLS handshake state transition in tls_connect plus entry/exit counters on net_poll to definitively localize the Phase 3 HTTPS stall. **Markers** (src/tls13.asm + src/boot.asm): CH/SH/HK1/KEYS/ENC1/RX/GOT2/DEC/PROC/EE/CERT/CV/FIN/CFIN printed on screen as each step of tls_connect completes. The branch-target trampoline pattern (`bcc @okN / jmp @error / @okN:`) is used because the inserted print calls push @error out of bcs range. **Counters** (src/data.asm + src/net.asm): net_poll_entry_count and net_poll_return_count 16-bit counters increment at entry and exit of net_poll. Gap between them = number of active ip65_process calls currently on the stack. **Test diagnostics** (tests/test_phase3_https.py): - Dynamic label lookup via _label_addr() against build/labels.txt so diagnostic reads always hit the current build regardless of BSS layout shifts - File-logged post-mortem at /tmp/c64-https-phase3-diag.log with explicit flush(), survives hard-kill - Per-heartbeat sampling of tcp_recv_head, tcp_recv_tail, net_poll_entry/return_count, printed inline - CPU register read (PC, SP, A, X, Y) via transport.read_registers() - Top-of-stack return-address chain with label resolution - Full dumps of tls_rec_buf, tls_hs_buf, tcp_recv_buf in hex+ASCII **Conclusion from this instrumentation**: Phase 3 handshake progresses successfully through CH, SH, HK1, KEYS, ENC1, RX markers — i.e., all of X25519 keygen, X25519 shared secret, HKDF handshake-key derivation, and transition into tls_recv_encrypted. It then stalls permanently while net_poll_entry - net_poll_return = 1 (exactly one ip65_process call on the stack, never returning) and tcp_recv_tail frozen at 0x0198 (408 bytes) across 1800 seconds / 55+ heartbeats. The stall is inside ip65 — likely the CS8900a driver or IP/TCP receive path — triggered by the second TCP segment carrying Certificate record bytes arriving after the multi-minute X25519 silence. None of the c64-https TLS code has a bug at this point; the TLS layer never gets a CPU quantum because net_poll doesn't return. This commit preserves the instrumentation for future sessions to investigate the ip65 stall with PC sampling and ip65 code inspection. Co-Authored-By: Claude Opus 4.6 (1M context) --- build/c64-https.prg | Bin 45896 -> 45900 bytes build/labels.txt | 567 +++++++++++++++++++------------------ src/boot.asm | 17 ++ src/data.asm | 4 + src/net.asm | 8 + src/tls13.asm | 95 ++++++- tests/test_phase3_https.py | 240 ++++++++++++---- 7 files changed, 592 insertions(+), 339 deletions(-) diff --git a/build/c64-https.prg b/build/c64-https.prg index 27c02cfedb50b2a7137f4d27128bd3801601ea5c..6b16d195b7b12919debe7b2fdebc9c89d2622fec 100644 GIT binary patch delta 11543 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 + jsr print_string ; --- receive ServerHello --- lda #TLS_STATE_SERVER_HELLO sta tls_state jsr tls_recv_server_hello - bcs @error + bcc @ok2 + jmp @error +@ok2: + lda #sh_recv_msg + jsr print_string + + lda #hk1_msg + jsr print_string ; derive handshake keys from ECDHE shared secret jsr tls_derive_handshake_keys - bcs @error + bcc @ok3 + jmp @error +@ok3: + lda #keys_ok_msg + jsr print_string ; --- receive EncryptedExtensions (encrypted) --- lda #TLS_STATE_ENCRYPTED_EXT sta tls_state jsr tls_recv_encrypted - bcs @error + bcc @ok4 + jmp @error +@ok4: + lda #ee_recv_msg + jsr print_string ; --- receive Certificate (encrypted) --- lda #TLS_STATE_CERTIFICATE sta tls_state jsr tls_recv_encrypted - bcs @error + bcc @ok5 + jmp @error +@ok5: + lda #cert_recv_msg + jsr print_string ; --- receive CertificateVerify (encrypted) --- lda #TLS_STATE_CERT_VERIFY sta tls_state jsr tls_recv_encrypted - bcs @error + bcc @ok6 + jmp @error +@ok6: + lda #cv_recv_msg + jsr print_string ; --- receive server Finished (encrypted) --- lda #TLS_STATE_FINISHED sta tls_state jsr tls_recv_encrypted - bcs @error + bcc @ok7 + jmp @error +@ok7: + lda #fin_recv_msg + jsr print_string ; verify server Finished jsr tls_verify_finished - bcs @error + bcc @ok9 + jmp @error +@ok9: ; derive application traffic keys jsr tls_derive_traffic_keys - bcs @error + bcc @ok10 + jmp @error +@ok10: ; --- send client Finished (encrypted) --- jsr tls_send_finished - bcs @error + bcc @ok8 + jmp @error +@ok8: + lda #cfin_sent_msg + jsr print_string ; connected! lda #TLS_STATE_CONNECTED @@ -325,13 +373,26 @@ tls_recv_server_hello: ; Output: C=0 success, C=1 timeout/error ; ============================================================================= tls_recv_encrypted: + lda #enc1_msg + jsr print_string lda #0 sta @enc_timeout sta @enc_timeout+1 + lda #rx_msg + jsr print_string @enc_wait: jsr net_poll jsr tls_record_recv_and_decrypt - bcc @enc_got_record + bcs + + ; success -- print GOT2 marker so we can distinguish progress + lda #got2_msg + jsr print_string + clc + jmp @enc_got_record ++ inc @enc_timeout bne @enc_wait inc @enc_timeout+1 @@ -340,6 +401,11 @@ tls_recv_encrypted: sec rts @enc_got_record: + pha + lda #got_msg + jsr print_string + pla ; verify inner content type is handshake lda tls_rec_type cmp #TLS_CT_HANDSHAKE @@ -369,6 +435,13 @@ tls_recv_encrypted: sta zp_count jsr tls_transcript_update + lda #dec_msg + jsr print_string + lda #proc_msg + jsr print_string + ; dispatch based on handshake type (first byte of tls_hs_buf) lda tls_hs_buf cmp #TLS_HS_ENCRYPTED_EXT diff --git a/tests/test_phase3_https.py b/tests/test_phase3_https.py index c2748e3..7140d6e 100644 --- a/tests/test_phase3_https.py +++ b/tests/test_phase3_https.py @@ -48,10 +48,25 @@ "TLS SEND FAILED", ) # Progress needles we use to report how far we got on failure. +# Ordered roughly by expected appearance; _last_progress_seen picks the +# one with the latest rfind index on screen. PROGRESS_NEEDLES = ( "HTTPS GET", "DNS OK", "TCP CONNECTED", + "CH", + "SH", + "KEYS", + "ENC1", + "RX", + "GOT", + "DEC", + "PROC", + "EE", + "CERT", + "CV", + "FIN", + "CFIN", "TLS HANDSHAKE OK", "REQUEST SENT", "CONNECTION CLOSED", @@ -121,12 +136,30 @@ def _label_addr(name: str): def _dump_diagnostics(transport=None) -> None: """Print dnsmasq log and host-side connectivity checks for post-mortem.""" + diag_log_path = "/tmp/c64-https-phase3-diag.log" + try: + diag_log = open(diag_log_path, "a", buffering=1) # line-buffered + _ts = time.strftime("%Y-%m-%d %H:%M:%S") + diag_log.write(f"\n=== diagnostic dump at {_ts} ===\n") + diag_log.flush() + except Exception: + diag_log = None + + def _emit(line: str) -> None: + print(line, flush=True) + if diag_log is not None: + try: + diag_log.write(line + "\n") + diag_log.flush() + except Exception: + pass + dnsmasq_log = "/tmp/c64-https-dnsmasq.log" if os.path.isfile(dnsmasq_log): - print(f"\n--- tail of {dnsmasq_log} ---") + _emit(f"\n--- tail of {dnsmasq_log} ---") with open(dnsmasq_log, "rb") as f: data = f.read()[-4000:] - print(data.decode("utf-8", errors="replace")) + _emit(data.decode("utf-8", errors="replace")) # Host-side DNS check try: @@ -134,9 +167,9 @@ def _dump_diagnostics(transport=None) -> None: ["dig", "+short", "@10.0.65.1", "www.foo.bar"], capture_output=True, text=True, timeout=5, ) - print(f"\n dig @10.0.65.1 www.foo.bar -> {r.stdout.strip()}") + _emit(f"\n dig @10.0.65.1 www.foo.bar -> {r.stdout.strip()}") except Exception as e: - print(f" dig check failed: {e}") + _emit(f" dig check failed: {e}") # Host-side HTTPS check (self-signed, so disable verification). try: @@ -148,19 +181,69 @@ def _dump_diagnostics(transport=None) -> None: resp = urllib.request.urlopen( "https://10.0.65.1:443/", timeout=3, context=ctx ) - print(f" HTTPS from host: {resp.status} {resp.read()[:100]}") + _emit(f" HTTPS from host: {resp.status} {resp.read()[:100]}") except Exception as e: - print(f" HTTPS from host failed: {e}") + _emit(f" HTTPS from host failed: {e}") # ip65 error code from C64 memory if transport is not None: + # Force-load labels so the PC/stack lookups below have data. + _label_addr("tls_state") + # CPU registers -- PC tells us where the 6502 is currently stuck. + try: + transport.resume() + regs = transport.read_registers() + pc = regs.get("PC", 0) + sp = regs.get("SP", 0) + a = regs.get("A", 0) + x = regs.get("X", 0) + y = regs.get("Y", 0) + _emit(f" CPU PC=${pc:04X} SP=${sp:02X} A=${a:02X} X=${x:02X} Y=${y:02X}") + # Find nearest label <= PC + nearest_name = None + nearest_addr = -1 + for name, addr in _LABELS_CACHE.items() if _LABELS_CACHE else []: + if addr <= pc and addr > nearest_addr: + nearest_addr = addr + nearest_name = name + if nearest_name is not None: + _emit(f" nearest label <= PC: {nearest_name} @ ${nearest_addr:04X} (PC+${pc-nearest_addr:X})") + except Exception as e: + _emit(f" read_registers failed: {e}") + + # Top of stack: return address chain from JSRs. + # 6502 SP indexes into $0100-$01FF; stack grows downward. + # Bytes ABOVE current SP (i.e. $0100+SP+1 .. $01FF) are live. + try: + transport.resume() + stack = transport.read_memory(0x01F0, 16) + _emit(f" stack $01F0-$01FF = {' '.join(f'{b:02X}' for b in stack)}") + # Parse as little-endian return-address pairs (each JSR pushes hi, lo + # where the saved addr = actual_return - 1). + _emit(" possible return-address pairs (addr+1 = instruction after JSR):") + for i in range(0, 16, 2): + lo = stack[i] + hi = stack[i + 1] + ret = ((hi << 8) | lo) + 1 + # Find nearest label <= ret + near_n = None + near_a = -1 + for name, addr in _LABELS_CACHE.items() if _LABELS_CACHE else []: + if addr <= ret and addr > near_a: + near_a = addr + near_n = name + tag = f"{near_n}+${ret-near_a:X}" if near_n else "?" + _emit(f" $01{0xF0+i:02X}: lo=${lo:02X} hi=${hi:02X} -> ${ret:04X} ({tag})") + except Exception as e: + _emit(f" stack read failed: {e}") + try: transport.resume() err_addr = _label_addr("ip65_error") or 0x4CEA err_data = transport.read_memory(err_addr, 1) - print(f" ip65_error @ ${err_addr:04X} = 0x{err_data[0]:02X}") + _emit(f" ip65_error @ ${err_addr:04X} = 0x{err_data[0]:02X}") except Exception as e: - print(f" ip65_error read failed: {e}") + _emit(f" ip65_error read failed: {e}") state_names = { 0x00: "IDLE", 0x01: "CLIENT_HELLO", 0x02: "SERVER_HELLO", @@ -175,11 +258,11 @@ def _dump_diagnostics(transport=None) -> None: if ts_addr is not None: tls_state = transport.read_memory(ts_addr, 1)[0] name = state_names.get(tls_state, "UNKNOWN") - print(f" tls_state @ ${ts_addr:04X} = ${tls_state:02X} ({name})") + _emit(f" tls_state @ ${ts_addr:04X} = ${tls_state:02X} ({name})") else: - print(" tls_state: label missing") + _emit(" tls_state: label missing") except Exception as e: - print(f" tls_state read failed: {e}") + _emit(f" tls_state read failed: {e}") # Last attempted TLS state (preserved before error handler overwrote tls_state) try: @@ -188,11 +271,11 @@ def _dump_diagnostics(transport=None) -> None: if tls_addr is not None: last = transport.read_memory(tls_addr, 1)[0] last_name = state_names.get(last, "UNKNOWN") - print(f" tls_last_state @ ${tls_addr:04X} = ${last:02X} ({last_name})") + _emit(f" tls_last_state @ ${tls_addr:04X} = ${last:02X} ({last_name})") else: - print(" tls_last_state: label missing") + _emit(" tls_last_state: label missing") except Exception as e: - print(f" tls_last_state read failed: {e}") + _emit(f" tls_last_state read failed: {e}") # Most recent TLS record buffer head try: @@ -200,16 +283,16 @@ def _dump_diagnostics(transport=None) -> None: buf_addr = _label_addr("tls_rec_buf") if buf_addr is not None: rec = transport.read_memory(buf_addr, 256) - print(f" tls_rec_buf @ ${buf_addr:04X} = ({len(rec)} bytes)") + _emit(f" tls_rec_buf @ ${buf_addr:04X} = ({len(rec)} bytes)") for i in range(0, len(rec), 16): line = rec[i:i+16] hex_part = " ".join(f"{b:02X}" for b in line) ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in line) - print(f" +${i:02X} {hex_part:<47} {ascii_part}") + _emit(f" +${i:02X} {hex_part:<47} {ascii_part}") else: - print(" tls_rec_buf: label missing") + _emit(" tls_rec_buf: label missing") except Exception as e: - print(f" tls_rec_buf read failed: {e}") + _emit(f" tls_rec_buf read failed: {e}") # Raw ip65 TCP receive ring — what ip65 actually delivered try: @@ -217,16 +300,16 @@ def _dump_diagnostics(transport=None) -> None: ring_addr = _label_addr("tcp_recv_buf") if ring_addr is not None: ring = transport.read_memory(ring_addr, 256) - print(f" tcp_recv_buf @ ${ring_addr:04X} = ({len(ring)} bytes)") + _emit(f" tcp_recv_buf @ ${ring_addr:04X} = ({len(ring)} bytes)") for i in range(0, len(ring), 16): line = ring[i:i+16] hex_part = " ".join(f"{b:02X}" for b in line) ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in line) - print(f" +${i:02X} {hex_part:<47} {ascii_part}") + _emit(f" +${i:02X} {hex_part:<47} {ascii_part}") else: - print(" tcp_recv_buf: label missing") + _emit(" tcp_recv_buf: label missing") except Exception as e: - print(f" tcp_recv_buf read failed: {e}") + _emit(f" tcp_recv_buf read failed: {e}") # Parser input: tls_hs_buf (stable copy made during record reception) try: @@ -234,16 +317,16 @@ def _dump_diagnostics(transport=None) -> None: hs_addr = _label_addr("tls_hs_buf") if hs_addr is not None: hs = transport.read_memory(hs_addr, 128) - print(f" tls_hs_buf @ ${hs_addr:04X} = ({len(hs)} bytes)") + _emit(f" tls_hs_buf @ ${hs_addr:04X} = ({len(hs)} bytes)") for i in range(0, len(hs), 16): line = hs[i:i+16] hex_part = " ".join(f"{b:02X}" for b in line) ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in line) - print(f" +${i:02X} {hex_part:<47} {ascii_part}") + _emit(f" +${i:02X} {hex_part:<47} {ascii_part}") else: - print(" tls_hs_buf: label missing") + _emit(" tls_hs_buf: label missing") except Exception as e: - print(f" tls_hs_buf read failed: {e}") + _emit(f" tls_hs_buf read failed: {e}") # tls_rec_header raw 5-byte buffer (state-machine target) try: @@ -251,11 +334,11 @@ def _dump_diagnostics(transport=None) -> None: hdr_addr = _label_addr("tls_rec_header") if hdr_addr is not None: hdr = transport.read_memory(hdr_addr, 5) - print(f" tls_rec_header @ ${hdr_addr:04X} = {' '.join(f'{b:02X}' for b in hdr)}") + _emit(f" tls_rec_header @ ${hdr_addr:04X} = {' '.join(f'{b:02X}' for b in hdr)}") else: - print(" tls_rec_header: label missing") + _emit(" tls_rec_header: label missing") except Exception as e: - print(f" tls_rec_header read failed: {e}") + _emit(f" tls_rec_header read failed: {e}") # tls_recv_state and tls_recv_count (16-bit) — dynamic addrs try: @@ -264,12 +347,12 @@ def _dump_diagnostics(transport=None) -> None: rc_addr = _label_addr("tls_recv_count") if rs_addr is not None: rs_v = transport.read_memory(rs_addr, 1)[0] - print(f" tls_recv_state @ ${rs_addr:04X} = ${rs_v:02X}") + _emit(f" tls_recv_state @ ${rs_addr:04X} = ${rs_v:02X}") if rc_addr is not None: rc_b = transport.read_memory(rc_addr, 2) - print(f" tls_recv_count @ ${rc_addr:04X} = ${rc_b[1]:02X}{rc_b[0]:02X}") + _emit(f" tls_recv_count @ ${rc_addr:04X} = ${rc_b[1]:02X}{rc_b[0]:02X}") except Exception as e: - print(f" tls_recv_state read failed: {e}") + _emit(f" tls_recv_state read failed: {e}") # Single-byte diagnostic labels (dynamic; skip silently if missing) for lbl_name in ("tls_hs_len", "tls_rec_len", "tls_rec_type"): @@ -282,9 +365,9 @@ def _dump_diagnostics(transport=None) -> None: n = 1 if lbl_name == "tls_rec_type" else 2 b = transport.read_memory(addr, n) if n == 1: - print(f" {lbl_name} @ ${addr:04X} = ${b[0]:02X}") + _emit(f" {lbl_name} @ ${addr:04X} = ${b[0]:02X}") else: - print(f" {lbl_name} @ ${addr:04X} = ${b[1]:02X}{b[0]:02X}") + _emit(f" {lbl_name} @ ${addr:04X} = ${b[1]:02X}{b[0]:02X}") except Exception: pass @@ -295,9 +378,9 @@ def _dump_diagnostics(transport=None) -> None: prog_addr = _label_addr("tls_recv_progress") if prog_addr is not None: pv = transport.read_memory(prog_addr, 1)[0] - print(f" tls_recv_progress @ ${prog_addr:04X} = ${pv:02X}") + _emit(f" tls_recv_progress @ ${prog_addr:04X} = ${pv:02X}") except Exception as e: - print(f" tls_recv_progress read failed: {e}") + _emit(f" tls_recv_progress read failed: {e}") # tls_recv_sub_progress — granular progress within tls_record_recv_and_decrypt sub_state_names = { @@ -319,9 +402,9 @@ def _dump_diagnostics(transport=None) -> None: if sub_addr is not None: sv = transport.read_memory(sub_addr, 1)[0] name = sub_state_names.get(sv, "UNKNOWN") - print(f" tls_recv_sub_progress @ ${sub_addr:04X} = ${sv:02X} ({name})") + _emit(f" tls_recv_sub_progress @ ${sub_addr:04X} = ${sv:02X} ({name})") except Exception as e: - print(f" tls_recv_sub_progress read failed: {e}") + _emit(f" tls_recv_sub_progress read failed: {e}") # tls_recv_poll_count — how many times @sh_wait looped try: @@ -330,33 +413,45 @@ def _dump_diagnostics(transport=None) -> None: if pc_addr is not None: pcb = transport.read_memory(pc_addr, 2) pc = pcb[0] | (pcb[1] << 8) - print(f" tls_recv_poll_count @ ${pc_addr:04X} = {pc} (${pcb[1]:02X}{pcb[0]:02X})") + _emit(f" tls_recv_poll_count @ ${pc_addr:04X} = {pc} (${pcb[1]:02X}{pcb[0]:02X})") except Exception as e: - print(f" tls_recv_poll_count read failed: {e}") + _emit(f" tls_recv_poll_count read failed: {e}") # TCP receive ring buffer head/tail — tells us if ip65 wrote data - # that TLS never drained. + # that TLS never drained. Both are 16-bit little-endian words. try: transport.resume() head_addr = _label_addr("tcp_recv_head") tail_addr = _label_addr("tcp_recv_tail") + ovf_addr = _label_addr("tcp_recv_overflow") if head_addr is not None and tail_addr is not None: - head = transport.read_memory(head_addr, 1)[0] - tail = transport.read_memory(tail_addr, 1)[0] - avail = (tail - head) & 0xFF - print(f" tcp_recv_head @ ${head_addr:04X} = ${head:02X}") - print(f" tcp_recv_tail @ ${tail_addr:04X} = ${tail:02X}") - print(f" tcp ring available = {avail} bytes") + hb = transport.read_memory(head_addr, 2) + tb = transport.read_memory(tail_addr, 2) + head = hb[0] | (hb[1] << 8) + tail = tb[0] | (tb[1] << 8) + avail = (tail - head) & 0xFFFF + _emit(f" tcp_recv_head @ ${head_addr:04X} = ${head:04X}") + _emit(f" tcp_recv_tail @ ${tail_addr:04X} = ${tail:04X}") + _emit(f" tcp ring available = {avail} bytes") + if ovf_addr is not None: + ov = transport.read_memory(ovf_addr, 1)[0] + _emit(f" tcp_recv_overflow @ ${ovf_addr:04X} = ${ov:02X}") if avail > 0: - # dump first 32 bytes of ring starting at head + # dump first 48 bytes of ring starting at head (mod 4096) buf_addr = _label_addr("tcp_recv_buf") if buf_addr is not None: - ring = transport.read_memory(buf_addr, 256) + ring = transport.read_memory(buf_addr, 4096) n = min(avail, 48) - line_hex = " ".join(f"{ring[(head + i) & 0xFF]:02X}" for i in range(n)) - print(f" ring[head..head+{n}] = {line_hex}") + line_hex = " ".join(f"{ring[(head + i) & 0xFFF]:02X}" for i in range(n)) + _emit(f" ring[head..head+{n}] = {line_hex}") except Exception as e: - print(f" tcp ring read failed: {e}") + _emit(f" tcp ring read failed: {e}") + + if diag_log is not None: + try: + diag_log.close() + except Exception: + pass def main() -> int: @@ -471,7 +566,46 @@ def main() -> int: # Heartbeat log so the test shows forward motion. if time.monotonic() >= next_heartbeat: remaining = int(deadline - time.monotonic()) - print(f" [heartbeat] last seen: {last_progress} ({remaining}s left)") + # Sample ip65/TCP ring and net_poll counters so we + # can tell "slow progress" from "dead stuck". + hb_head = hb_tail = hb_pin = hb_pout = None + try: + transport.resume() + head_addr = _label_addr("tcp_recv_head") + tail_addr = _label_addr("tcp_recv_tail") + pin_addr = _label_addr("net_poll_entry_count") + pout_addr = _label_addr("net_poll_return_count") + if head_addr is not None: + b = transport.read_memory(head_addr, 2) + hb_head = b[0] | (b[1] << 8) + if tail_addr is not None: + b = transport.read_memory(tail_addr, 2) + hb_tail = b[0] | (b[1] << 8) + if pin_addr is not None: + b = transport.read_memory(pin_addr, 2) + hb_pin = b[0] | (b[1] << 8) + if pout_addr is not None: + b = transport.read_memory(pout_addr, 2) + hb_pout = b[0] | (b[1] << 8) + except Exception as _hb_exc: + print(f" (heartbeat sample failed: {_hb_exc})") + hb_extra = ( + f" head=${hb_head:04X}" if hb_head is not None else "" + ) + ( + f" tail=${hb_tail:04X}" if hb_tail is not None else "" + ) + ( + f" poll_in={hb_pin}" if hb_pin is not None else "" + ) + ( + f" poll_out={hb_pout}" if hb_pout is not None else "" + ) + print(f" [heartbeat] last seen: {last_progress} ({remaining}s left){hb_extra}") + # Also dump the 10 lines from idx_get onward so we + # can see fine-grained markers like ENC1/RX/GOT. + tail_lines = final[idx_get:].splitlines()[:12] + for tl in tail_lines: + tl_stripped = tl.rstrip() + if tl_stripped: + print(f" | {tl_stripped}") next_heartbeat = time.monotonic() + 30.0 elif last_progress != last_log_progress: print(f" progress: {last_progress}") From 3c503c57d6a49c0949e9d46ae7a615adf9908ec4 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Wed, 15 Apr 2026 09:16:55 -0500 Subject: [PATCH 10/22] Phase 2: ca65 scaffolding + entropy pilot (re-run after PR #13 merge) Re-runs Phase 2 bootstrap on the post-PR-13 base (master 6cf0104), after hard-resetting refactor/ca65-conversion to include the memory-layout and TLS receive-path fixes from commits 1c75ed9, ac57d1f, eab7570. - Add cfg/c64-https-ip65.cfg (ld65 config with NET/CRYPTO/SHADOW/TCP regions) - Add cfg/c64-https-uci.cfg placeholder for U64E UCI backend - Add src/macros.inc, src/crypto_abi.inc, src/net_abi.inc facades - Add src/extern/{c64-x25519,c64-ChaCha20-Poly1305,c64-nist-curves}/README.md - Add src/net/{ip65,uci}/README.md - Add Makefile.ca65 (runs alongside ACME Makefile during refactor) - Convert src/constants.asm -> src/constants.inc (pilot, pure equates, with post-fix TCP_RECV_BUF_SIZE = 4096) - Convert src/entropy.asm -> src/entropy.s (pilot, assembles to entropy.o) ACME originals preserved; Phase 3 agents handle bulk conversion. Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 1 + Makefile.ca65 | 28 +++ cfg/c64-https-ip65.cfg | 52 +++++ cfg/c64-https-uci.cfg | 11 + src/constants.inc | 254 +++++++++++++++++++++ src/crypto_abi.inc | 26 +++ src/entropy.s | 34 +++ src/extern/c64-ChaCha20-Poly1305/README.md | 7 + src/extern/c64-nist-curves/README.md | 7 + src/extern/c64-x25519/README.md | 9 + src/macros.inc | 61 +++++ src/net/ip65/README.md | 8 + src/net/uci/README.md | 9 + src/net_abi.inc | 22 ++ 14 files changed, 529 insertions(+) create mode 100644 Makefile.ca65 create mode 100644 cfg/c64-https-ip65.cfg create mode 100644 cfg/c64-https-uci.cfg create mode 100644 src/constants.inc create mode 100644 src/crypto_abi.inc create mode 100644 src/entropy.s create mode 100644 src/extern/c64-ChaCha20-Poly1305/README.md create mode 100644 src/extern/c64-nist-curves/README.md create mode 100644 src/extern/c64-x25519/README.md create mode 100644 src/macros.inc create mode 100644 src/net/ip65/README.md create mode 100644 src/net/uci/README.md create mode 100644 src/net_abi.inc diff --git a/.gitignore b/.gitignore index e6e44a4..7039da5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ __pycache__/ *.pyc +build/ ip65-build/*.o ip65-build/*.bin ip65-build/*.map diff --git a/Makefile.ca65 b/Makefile.ca65 new file mode 100644 index 0000000..4128ae5 --- /dev/null +++ b/Makefile.ca65 @@ -0,0 +1,28 @@ +# Makefile.ca65 — ca65/ld65 build for c64-https +# +# Runs alongside the existing ACME Makefile during the refactor. +# Deleted in Phase 7 when the ACME build is retired. + +CA65 ?= ca65 +LD65 ?= ld65 +BACKEND ?= ip65 +CFG := cfg/c64-https-$(BACKEND).cfg + +CA65FLAGS := -I src -I src/inc --debug-info +LD65FLAGS := -C $(CFG) -Ln build/labels.txt -m build/c64-https.map + +# Source inventory grows as Phase 3 converts more files. +# Phase 2 only exercises the entropy pilot. +PILOT_SRCS := src/entropy.s +PILOT_OBJS := $(patsubst src/%.s,build/%.o,$(PILOT_SRCS)) + +.PHONY: pilot clean + +pilot: $(PILOT_OBJS) + +build/%.o: src/%.s + @mkdir -p $(dir $@) + $(CA65) $(CA65FLAGS) -o $@ $< + +clean: + rm -rf build diff --git a/cfg/c64-https-ip65.cfg b/cfg/c64-https-ip65.cfg new file mode 100644 index 0000000..6d5af21 --- /dev/null +++ b/cfg/c64-https-ip65.cfg @@ -0,0 +1,52 @@ +# 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) + +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; + NET_CODE: start = $2000, size = $2000, file = %O, define = yes; + NET_BSS: start = $4000, size = $2000, type = rw, define = yes; + CRYPTO: start = $6000, size = $4000, file = %O, define = yes; + + SHADOW_BSS: start = $A000, size = $2000, type = rw, define = yes; + TCP_BUF: start = $C000, size = $1000, type = rw, define = yes; +} + +SEGMENTS { + ZP_SHARED: load = ZP_IP65, type = zp, define = yes; + ZEROPAGE: load = ZP_CRYPTO, type = zp, define = yes; + ZP_WIDE: load = ZP_WIDE, type = zp, define = yes; + + LOADADDR: load = LOADADDR, type = ro; + EXEHDR: load = LOADER, type = ro; + STARTUP: load = LOADER, type = ro, optional = yes; + CODE: load = LOADER, type = ro, define = yes; + RODATA: load = LOADER, type = ro; + INIT: load = LOADER, type = ro, optional = yes; + + NET_CODE: load = NET_CODE, type = ro, define = yes; + NET_BSS: load = NET_BSS, type = bss, define = yes; + + CRYPTO_CODE: load = CRYPTO, type = ro, define = yes; + CRYPTO_RODATA: load = CRYPTO, type = ro; + + BSS: load = SHADOW_BSS, type = bss, define = yes; + CRYPTO_BSS: load = SHADOW_BSS, type = bss, define = yes; + + TCP_RECV_BUF: load = TCP_BUF, type = bss, define = yes; +} diff --git a/cfg/c64-https-uci.cfg b/cfg/c64-https-uci.cfg new file mode 100644 index 0000000..41d7274 --- /dev/null +++ b/cfg/c64-https-uci.cfg @@ -0,0 +1,11 @@ +# c64-https ld65 config — UCI (Ultimate Command Interface) backend +# +# PLACEHOLDER / TODO — not yet functional. +# +# Intended target: Commodore Ultimate 64 / U64E, using UCI ethernet +# instead of ip65 + RR-Net. Most MEMORY/SEGMENTS should mirror the +# ip65 cfg; the NET_* regions will differ once the UCI backend lands +# under src/net/uci/. +# +# This file exists to reserve the filename and document intent. +# Do NOT build with BACKEND=uci yet — it will fail. diff --git a/src/constants.inc b/src/constants.inc new file mode 100644 index 0000000..91bceb8 --- /dev/null +++ b/src/constants.inc @@ -0,0 +1,254 @@ +; ============================================================================= +; constants.inc - System equates, zero page, hardware addresses +; +; Converted from constants.asm (ACME) to ca65 include file. Pure equates, +; no code or data — included by every .s module that needs the symbols. +; ACME `=` assignments are syntactically identical in ca65. +; ============================================================================= + +; ============================================================================= +; C64 system addresses +; ============================================================================= +chrout = $ffd2 ; KERNAL character output +chrin = $ffcf ; KERNAL character input +getin = $ffe4 ; KERNAL get key +setlfs = $ffba ; KERNAL set file params +setnam = $ffbd ; KERNAL set filename +open = $ffc0 ; KERNAL open file +close = $ffc3 ; KERNAL close file +chkin = $ffc6 ; KERNAL set input channel +chkout = $ffc9 ; KERNAL set output channel +clrchn = $ffcc ; KERNAL clear channels +readst = $ffb7 ; KERNAL read status +load = $ffd5 ; KERNAL load + +screen_ram = $0400 ; screen memory +color_ram = $d800 ; color memory +border_color = $d020 +bg_color = $d021 + +; CIA / SID for entropy +sid_osc3 = $d41b ; SID oscillator 3 output +cia1_ta_lo = $dc04 ; CIA1 timer A low +cia1_ta_hi = $dc05 ; CIA1 timer A high +cia1_cra = $dc0e ; CIA1 control register A + +; ============================================================================= +; Zero page assignments — time-shared with ip65 ($02-$1B) +; +; ip65 uses $02-$1B (cc65 standard ZP) during ip65_process / tcp_send / etc. +; Crypto modules use overlapping ranges. Before calling ip65, save $02-$1B +; to zp_save_buf. After ip65 returns, restore. This costs ~60 cycles per +; ip65 call — negligible vs. network latency. +; ============================================================================= + +; --- Shared tmp (used by both crypto and general code) --- +zp_tmp1 = $02 ; general temp +zp_tmp2 = $03 ; general temp + +; --- word32 pointers (ChaCha20 / Poly1305 via wireguard) --- +w32_src1 = $04 ; 2 bytes ($04-$05) +w32_src2 = $06 ; 2 bytes ($06-$07) +w32_dst = $08 ; 2 bytes ($08-$09) + +; --- SHA-256 accumulators --- +sha_temp1 = $0a ; 4 bytes ($0A-$0D) +sha_temp2 = $0e ; 4 bytes ($0E-$11) +sha256_round = $12 ; 1 byte + +; --- ChaCha20 state / mult66 pointers (time-shared: fe25519 and ChaCha20 never run simultaneously) --- +cc20_round = $14 ; 1 byte +cc20_qr_idx = $15 ; 1 byte +cc20_data_ptr = $16 ; 2 bytes ($16-$17) +cc20_remain = $18 ; low byte of 16-bit ChaCha20/AEAD length + ; (high byte = cc20_remain_hi in data.asm) +cc20_buf_pos = $19 ; 1 byte + +; --- mult66 indirect-indexed multiply pointers (time-shared with ChaCha20) --- +lmul0 = $14 ; 2 bytes ($14-$15) — sqtab lookup pointer +lmul1 = $16 ; 2 bytes ($16-$17) — sqtab_hi lookup pointer + +; --- Poly1305 state --- +poly_i = $1a ; 1 byte +poly_j = $1b ; 1 byte +poly_carry = $1c ; 1 byte +poly_tmp = $1d ; 1 byte + +; --- TLS record layer --- +tls_rec_ptr = $1e ; 2 bytes ($1E-$1F) — pointer to record data +tls_rec_idx = $20 ; 1 byte — index during record read/write +tls_direction = $21 ; 1 byte — 0=write, 1=read (key/IV/seq select) + +; --- ECDSA P-256/P-384 bignum arithmetic (from c64-aes256-ecdsa) --- +; These overlap with x25519 at $39-$3A but never run simultaneously. +fp_src1 = $22 ; 2 bytes ($22-$23) — operand 1 pointer +fp_src2 = $24 ; 2 bytes ($24-$25) — operand 2 pointer +fp_dst = $26 ; 2 bytes ($26-$27) — destination pointer +fp_misc = $28 ; 2 bytes ($28-$29) — modulus pointer +fp_carry = $2a ; 1 byte +fp_loop = $2b ; 1 byte +fp_mul_i = $39 ; 1 byte (shares with x25_byte_idx — OK, never simultaneous) +fp_mul_j = $3a ; 1 byte (shares with x25_bit_mask — OK) +ec_scalar_ptr = $3b ; 2 bytes ($3B-$3C) — scalar for point multiply + +; --- fe25519 field arithmetic (relocated from wireguard $1E-$29) --- +fe_src1 = $2c ; 2 bytes ($2C-$2D) — operand 1 pointer +fe_src2 = $2e ; 2 bytes ($2E-$2F) — operand 2 pointer +fe_dst = $30 ; 2 bytes ($30-$31) — destination pointer +fe_carry = $32 ; 1 byte +fe_loop = $33 ; 1 byte +fe_mul_i = $34 ; 1 byte +fe_mul_j = $35 ; 1 byte +; $36-$37 reserved (fe25519 uses fe_tmp1..4 as 32-byte data labels) + +; --- x25519 state (relocated from wireguard $2A-$2D) --- +x25_prev_bit = $38 ; 1 byte — previous k_t for swap +x25_byte_idx = $39 ; 1 byte — byte index in scalar +x25_bit_mask = $3a ; 1 byte — current bit mask + +; --- General pointers (shared, save/restore around ip65) --- +zp_ptr = $fb ; 2 bytes ($FB-$FC) +zp_temp = $fd ; 1 byte +zp_count = $fe ; 1 byte + +; --- Quarter-square multiply table (shared by Poly1305 and ECDSA) --- +; sqtab_lo/sqtab_hi now defined as labels in data.asm — moved out of $7800 to free code space + +; --- REU (Ram Expansion Unit) registers --- +reu_status = $df00 ; status register +reu_command = $df01 ; command register +reu_c64_lo = $df02 ; C64 base address low +reu_c64_hi = $df03 ; C64 base address high +reu_reu_lo = $df04 ; REU base address low +reu_reu_hi = $df05 ; REU base address high +reu_reu_bank = $df06 ; REU bank +reu_len_lo = $df07 ; transfer length low +reu_len_hi = $df08 ; transfer length high +reu_addr_ctrl = $df0a ; address control + +; --- SID voice 3 setup for noise (entropy collection) --- +sid_base = $d400 +sid_v3_freq_lo = $d40e +sid_v3_freq_hi = $d40f +sid_v3_ctrl = $d412 +sid_v3_ad = $d413 +sid_v3_sr = $d414 + +; --- ip65 ZP overlap zone --- +; ip65 uses $02-$1B during its execution (cc65 standard: c_sp, sreg, +; regsave, ptr1-ptr4, tmp1-tmp4, regbank). These overlap our crypto +; ZP at $02-$1B. The net.asm wrapper handles save/restore. +ip65_zp_start = $02 +ip65_zp_end = $1b ; inclusive +ip65_zp_size = ip65_zp_end - ip65_zp_start + 1 ; 26 bytes + +; ============================================================================= +; ip65 jump table at $2000 (fixed offsets from ip65-build/ip65_stub.s) +; ============================================================================= +ip65_base = $2000 +ip65_init = ip65_base + 0 ; A=0 default; C=0 ok +ip65_process = ip65_base + 3 ; poll; C=0 packet, C=1 idle +ip65_dhcp_init = ip65_base + 6 ; DHCP; C=0 ok +ip65_dns_resolve = ip65_base + 9 ; resolve; C=0 ok +ip65_tcp_connect = ip65_base + 12 ; AX=port; C=0 ok +ip65_tcp_send = ip65_base + 15 ; AX=data ptr; C=0 ok +ip65_tcp_close = ip65_base + 18 ; close connection +ip65_tcp_keepalive = ip65_base + 21 ; send keepalive +ip65_dns_set_host = ip65_base + 24 ; AX=hostname ptr +ip65_set_tcp_cb = ip65_base + 27 ; AX=callback addr +ip65_set_tcp_dest = ip65_base + 30 ; AX=4-byte IP ptr + +; ip65 variable table at ip65_base+33 (2-byte address pointers) +; Read the pointer, then dereference to access the variable. +; For convenience, we define the indirect addresses directly: +ip65_vt = ip65_base + 33 +ip65_vt_cfg_mac = ip65_vt + 0 ; -> 6 bytes MAC +ip65_vt_cfg_ip = ip65_vt + 2 ; -> 4 bytes our IP +ip65_vt_cfg_netmask = ip65_vt + 4 ; -> 4 bytes netmask +ip65_vt_cfg_gateway = ip65_vt + 6 ; -> 4 bytes gateway +ip65_vt_cfg_dns = ip65_vt + 8 ; -> 4 bytes DNS server +ip65_vt_dns_ip = ip65_vt + 10 ; -> 4 bytes resolved IP +ip65_vt_tcp_in_ptr = ip65_vt + 12 ; -> 2 bytes inbound data ptr +ip65_vt_tcp_in_len = ip65_vt + 14 ; -> 2 bytes inbound data length +ip65_vt_tcp_snd_len = ip65_vt + 16 ; -> 2 bytes send data length +ip65_vt_ip65_error = ip65_vt + 18 ; -> 1 byte error code +ip65_vt_tcp_dest_ip = ip65_vt + 20 ; -> 4 bytes dest IP + +; Direct addresses (from ip65-c64.map, for when we need to poke directly) +ip65_cfg_ip = $3a8a ; 4 bytes: our IP address +ip65_cfg_mac = $3a84 ; 6 bytes: our MAC address +ip65_tcp_snd_len = $4f48 ; 2 bytes: tcp_send_data_len +ip65_dns_ip_addr = $4073 ; 4 bytes: resolved DNS IP +ip65_error = $4cea ; 1 byte: last error code + +; ============================================================================= +; TLS 1.3 constants +; ============================================================================= +TLS_VERSION_12 = $0303 ; legacy version in ClientHello +TLS_VERSION_13 = $0304 ; actual TLS 1.3 + +; content types +TLS_CT_CHANGE_CIPHER = 20 +TLS_CT_ALERT = 21 +TLS_CT_HANDSHAKE = 22 +TLS_CT_APPLICATION = 23 + +; handshake types +TLS_HS_CLIENT_HELLO = 1 +TLS_HS_SERVER_HELLO = 2 +TLS_HS_ENCRYPTED_EXT = 8 +TLS_HS_CERTIFICATE = 11 +TLS_HS_CERT_VERIFY = 15 +TLS_HS_FINISHED = 20 + +; cipher suite +TLS_CHACHA20_POLY1305_SHA256 = $1303 + +; named groups +TLS_GROUP_SECP256R1 = $0017 +TLS_GROUP_X25519 = $001d + +; signature algorithm +TLS_SIG_ECDSA_SECP256R1_SHA256 = $0403 + +; extensions +TLS_EXT_SERVER_NAME = $0000 +TLS_EXT_MAX_FRAG_LEN = $0001 +TLS_EXT_SUPPORTED_GROUPS = $000a +TLS_EXT_SIG_ALGORITHMS = $000d +TLS_EXT_SUPPORTED_VERSIONS = $002b +TLS_EXT_KEY_SHARE = $0033 + +; max_fragment_length values (RFC 6066) +TLS_MAX_FRAG_512 = 1 +TLS_MAX_FRAG_1024 = 2 +TLS_MAX_FRAG_2048 = 3 +TLS_MAX_FRAG_4096 = 4 + +; TLS state machine states +TLS_STATE_IDLE = 0 +TLS_STATE_CLIENT_HELLO = 1 +TLS_STATE_SERVER_HELLO = 2 +TLS_STATE_ENCRYPTED_EXT = 3 +TLS_STATE_CERTIFICATE = 4 +TLS_STATE_CERT_VERIFY = 5 +TLS_STATE_FINISHED = 6 +TLS_STATE_CONNECTED = 7 +TLS_STATE_ERROR = $ff + +; alert levels +TLS_ALERT_WARNING = 1 +TLS_ALERT_FATAL = 2 + +; ============================================================================= +; Buffer sizes +; ============================================================================= +TLS_RECORD_MAX = 512 ; negotiated via max_fragment_length +TCP_RECV_BUF_SIZE = 4096 ; ring buffer for ip65 callback data (masked wrap) +TCP_RECV_MASK = $0fff ; 12-bit mask for 16-bit head/tail wrap +HTTP_BUF_SIZE = 256 ; HTTP request/response line buffer + +; TCP receive ring buffer lives at $C000-$CFFF (4KB always-RAM region between +; BASIC ROM shadow and I/O). Declared here as an equate rather than in +; data.asm so the PRG/BSS stays small — the RAM exists regardless. +tcp_recv_buf = $c000 diff --git a/src/crypto_abi.inc b/src/crypto_abi.inc new file mode 100644 index 0000000..adfdfce --- /dev/null +++ b/src/crypto_abi.inc @@ -0,0 +1,26 @@ +; src/crypto_abi.inc — public crypto API consumed by TLS/HTTP layers. +; +; Drop-in contract: any implementation (in-tree today, vendored sibling +; library tomorrow) must export these exact symbols with these exact +; calling conventions. Swapping implementation = changing the link line, +; no call-site changes. +; +; Sibling library correspondence (from Phase 0 discovery): +; x25519/fe25519 symbols → c64-x25519 +; chacha20/poly1305/aead symbols → c64-ChaCha20-Poly1305 +; ec_point_* symbols → c64-nist-curves (P-256; P-384 deferred) +; sha256 symbols → in-tree only, no sibling equivalent + +.import x25519_scalarmult +.import fe25519_mul, fe25519_sqr, fe25519_inv + +.import chacha20_encrypt +.import poly1305_init, poly1305_update, poly1305_final +.import aead_encrypt, aead_decrypt + +.import sha256_init, sha256_update, sha256_final + +.import ec_point_double, ec_point_add, ec_jacobian_to_affine + +; P-384 symbols NOT imported — stubbed per project_p384_stubbed. +; Future: ec_point_double_384, ec_point_add_384, ec_jacobian_to_affine_384 diff --git a/src/entropy.s b/src/entropy.s new file mode 100644 index 0000000..b0c33cd --- /dev/null +++ b/src/entropy.s @@ -0,0 +1,34 @@ +; ============================================================================= +; entropy.s - SID voice 3 + CIA1 timer initialization for hardware entropy +; +; Must be called before drbg_init_entropy. Sets SID voice 3 to noise +; waveform at maximum frequency, starts CIA1 Timer A in continuous mode. +; +; Converted from entropy.asm (ACME) to ca65. Pure code, no ACME directives +; other than the implicit segment — the whole file is a single routine. +; ============================================================================= + +.include "constants.inc" + +.export entropy_init + +.segment "CODE" + +; ============================================================================= +; entropy_init - Initialize hardware entropy sources +; Clobbers: A +; ============================================================================= +entropy_init: + ; SID voice 3: maximum frequency for fastest oscillation + lda #$ff + sta sid_v3_freq_lo ; $D40E + sta sid_v3_freq_hi ; $D40F + ; Noise waveform (bit 7 = 1, all others 0) + lda #$80 + sta sid_v3_ctrl ; $D412 + ; Start CIA1 Timer A in continuous mode + lda cia1_cra + ora #$01 ; set start bit + and #$f7 ; clear one-shot bit (continuous) + sta cia1_cra + rts diff --git a/src/extern/c64-ChaCha20-Poly1305/README.md b/src/extern/c64-ChaCha20-Poly1305/README.md new file mode 100644 index 0000000..e795b7d --- /dev/null +++ b/src/extern/c64-ChaCha20-Poly1305/README.md @@ -0,0 +1,7 @@ +# c64-ChaCha20-Poly1305 (vendored) + +Placeholder for vendored sources from the sibling `c64-ChaCha20-Poly1305` +project. Vendoring happens in a follow-up PR after the base refactor. + +Baseline: Profile B (no REU). Profile A with Shoup tables is a future +opt-in. diff --git a/src/extern/c64-nist-curves/README.md b/src/extern/c64-nist-curves/README.md new file mode 100644 index 0000000..c0ae0a7 --- /dev/null +++ b/src/extern/c64-nist-curves/README.md @@ -0,0 +1,7 @@ +# c64-nist-curves (vendored) + +Placeholder for vendored sources from the sibling `c64-nist-curves` +project. Vendoring happens in a follow-up PR after the base refactor. + +P-256 support lands with vendoring. P-384 stays stubbed for now; +unstubbing is tracked separately. diff --git a/src/extern/c64-x25519/README.md b/src/extern/c64-x25519/README.md new file mode 100644 index 0000000..e1bff12 --- /dev/null +++ b/src/extern/c64-x25519/README.md @@ -0,0 +1,9 @@ +# c64-x25519 (vendored) + +Placeholder for vendored sources from the sibling `c64-x25519` project. + +Vendoring happens in a follow-up PR after the base ACME→ca65 refactor is +merged. See `src/crypto_abi.inc` for the public symbols this library is +expected to provide. ABI alignment was verified in Phase 0 discovery. + +Baseline profile: no-REU. Profile A (REU mul tables) is a future opt-in. diff --git a/src/macros.inc b/src/macros.inc new file mode 100644 index 0000000..9316cf8 --- /dev/null +++ b/src/macros.inc @@ -0,0 +1,61 @@ +; src/macros.inc — shared assembly macros for c64-https +; +; Scope rule: a pattern lives here only if it appears 3+ times AND +; hides nothing load-bearing. When in doubt, inline. + +.macro ldax addr + lda addr + ldx addr+1 +.endmacro + +.macro stax addr + sta addr + stx addr+1 +.endmacro + +.macro setptr zp, addr + lda #<(addr) + sta zp + lda #>(addr) + sta zp+1 +.endmacro + +.macro add16 dst, src + clc + lda dst + adc src + sta dst + lda dst+1 + adc src+1 + sta dst+1 +.endmacro + +.macro sub16 dst, src + sec + lda dst + sbc src + sta dst + lda dst+1 + sbc src+1 + sta dst+1 +.endmacro + +.macro inc16 addr + inc addr + bne :+ + inc addr+1 +: +.endmacro + +.macro dec16 addr + lda addr + bne :+ + dec addr+1 +: dec addr +.endmacro + +.macro screen_marker msg + lda #msg + jsr print_null_terminated +.endmacro diff --git a/src/net/ip65/README.md b/src/net/ip65/README.md new file mode 100644 index 0000000..2769f0a --- /dev/null +++ b/src/net/ip65/README.md @@ -0,0 +1,8 @@ +# src/net/ip65 — ip65 / RR-Net backend + +The current networking backend for c64-https. Implements the `net_*` +ABI declared in `src/net_abi.inc` on top of the ip65 TCP/IP stack with +the RR-Net ethernet driver. + +Port of `src/net.asm` from ACME lands here in Phase 3 Batch D. Until +then this directory holds only this README. diff --git a/src/net/uci/README.md b/src/net/uci/README.md new file mode 100644 index 0000000..9abbed9 --- /dev/null +++ b/src/net/uci/README.md @@ -0,0 +1,9 @@ +# src/net/uci — UCI / U64E backend (future) + +Placeholder for the Ultimate Command Interface networking backend, +targeting the Commodore Ultimate 64 / U64E. Will implement the same +`net_*` ABI as the ip65 backend, letting c64-https run natively on +U64E without RR-Net hardware. + +Not implemented yet. Select via `BACKEND=uci` in the Makefile (also +not functional yet). diff --git a/src/net_abi.inc b/src/net_abi.inc new file mode 100644 index 0000000..bb62991 --- /dev/null +++ b/src/net_abi.inc @@ -0,0 +1,22 @@ +; src/net_abi.inc — public networking API consumed by TLS/HTTP layers. +; +; Drop-in contract: any backend (ip65/RR-Net today, UCI/U64E next) must +; export these exact symbols. Swapping backend = link-time choice via +; different ld65 cfg + different net//*.o files. No changes +; to TLS or HTTP sources. + +.import net_init +.import net_dhcp_acquire +.import net_poll + +.import net_tcp_connect +.import net_tcp_send +.import net_tcp_close +.import net_tcp_set_recv_cb + +.import net_dns_resolve + +.import net_local_ip +.import net_resolved_ip +.import net_last_error +.import net_tcp_state From dc600d986cffddc3240744d1c9c75f49a0fecdf1 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Wed, 15 Apr 2026 09:30:59 -0500 Subject: [PATCH 11/22] Phase 3 Batch A: convert crypto/*.asm to ca65 (post-PR-13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Converts 13 crypto leaf files from ACME to ca65 object format on the post-PR-13 merged base (master 6cf0104). Each file uses explicit .export/.import discipline, .segment placement per cfg/c64-https-ip65.cfg, and .include "constants.inc" for zero-page equates. Preserves all fixes from commits 1c75ed9, ac57d1f, eab7570 that are now on master after the PR #13 merge: - aead.s uses 16-bit aead_data_len counter (ora aead_data_len+1 pattern) - chacha20.s imports cc20_remain_hi for 16-bit counter - all crypto files inherit the * = \$6000 anchor placement via cfg Batch A contents: - crypto/word32.s — 32-bit word primitives - crypto/sha256.s — SHA-256 hash (K[64] in CRYPTO_RODATA) - crypto/chacha20.s — ChaCha20 (cc20_set_* macros ported as ca65 .macro) - crypto/poly1305.s — Poly1305 MAC (scratch to CRYPTO_BSS) - crypto/fe25519.s — Field arithmetic mod 2^255-19 (uses fe_* prefix) - crypto/x25519.s — Curve25519 Montgomery ladder - crypto/hmac_drbg.s — HMAC-DRBG deterministic RNG - crypto/aead.s — ChaCha20-Poly1305 AEAD envelope (16-bit counters) - crypto/ecdsa_fp.s — P-256 field prime arithmetic (owns fp_wide) - crypto/ecdsa_mod.s — P-256 scalar modular arithmetic (owns fp_r0..r3) - crypto/ecdsa_curve.s — P-256 curve parameters + helpers - crypto/ecdsa_points.s— Jacobian point arithmetic - crypto/ecdsa_verify.s— ECDSA signature verification (P-256; P-384 stubbed to sec/rts, imports preserved for future restore) All 13 files assembled via: ca65 -I src -o build/crypto/.o src/crypto/.s Cross-file dependencies expressed as .import and resolve at link time once Batch B (TLS primitives) and Batch D (data.asm/net.asm) complete. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/crypto/{aead.asm => aead.s} | 42 ++++- src/crypto/{chacha20.asm => chacha20.s} | 159 +++++++++++------- src/crypto/ecdsa_curve.asm | 97 ----------- src/crypto/ecdsa_curve.s | 138 +++++++++++++++ src/crypto/{ecdsa_fp.asm => ecdsa_fp.s} | 50 ++++-- src/crypto/{ecdsa_mod.asm => ecdsa_mod.s} | 78 ++++++--- .../{ecdsa_points.asm => ecdsa_points.s} | 53 +++++- .../{ecdsa_verify.asm => ecdsa_verify.s} | 118 ++++++++++--- src/crypto/{fe25519.asm => fe25519.s} | 135 ++++++++++----- src/crypto/{hmac_drbg.asm => hmac_drbg.s} | 48 +++++- src/crypto/{poly1305.asm => poly1305.s} | 80 ++++++--- src/crypto/{sha256.asm => sha256.s} | 95 +++++++---- src/crypto/{word32.asm => word32.s} | 36 +++- src/crypto/{x25519.asm => x25519.s} | 55 +++++- 14 files changed, 828 insertions(+), 356 deletions(-) rename src/crypto/{aead.asm => aead.s} (91%) rename src/crypto/{chacha20.asm => chacha20.s} (72%) delete mode 100644 src/crypto/ecdsa_curve.asm create mode 100644 src/crypto/ecdsa_curve.s rename src/crypto/{ecdsa_fp.asm => ecdsa_fp.s} (85%) rename src/crypto/{ecdsa_mod.asm => ecdsa_mod.s} (91%) rename src/crypto/{ecdsa_points.asm => ecdsa_points.s} (93%) rename src/crypto/{ecdsa_verify.asm => ecdsa_verify.s} (87%) rename src/crypto/{fe25519.asm => fe25519.s} (93%) rename src/crypto/{hmac_drbg.asm => hmac_drbg.s} (92%) rename src/crypto/{poly1305.asm => poly1305.s} (92%) rename src/crypto/{sha256.asm => sha256.s} (87%) rename src/crypto/{word32.asm => word32.s} (96%) rename src/crypto/{x25519.asm => x25519.s} (90%) diff --git a/src/crypto/aead.asm b/src/crypto/aead.s similarity index 91% rename from src/crypto/aead.asm rename to src/crypto/aead.s index d6aa51a..9bcda93 100644 --- a/src/crypto/aead.asm +++ b/src/crypto/aead.s @@ -1,5 +1,7 @@ -; ============================================================================= -; aead.asm - ChaCha20-Poly1305 AEAD (RFC 7539 S2.8) +; aead.s — ChaCha20-Poly1305 AEAD envelope +; Converted from ACME to ca65 in Phase 3 Batch A. +; +; ChaCha20-Poly1305 AEAD (RFC 7539 S2.8) ; ; Encrypt: derive OTK, encrypt plaintext, compute tag ; Decrypt: derive OTK, verify tag, decrypt ciphertext @@ -16,7 +18,41 @@ ; Ciphertext written in-place at aead_data_ptr ; aead_tag (16 bytes) -- authentication tag ; A register: 0 = success (decrypt), nonzero = auth failure -; ============================================================================= + +.include "constants.inc" + +; --- External ChaCha20 routines (chacha20.s) --- +.import chacha20_init +.import chacha20_block +.import chacha20_encrypt + +; --- External Poly1305 routines (poly1305.s) --- +.import poly1305_init +.import poly1305_block +.import poly1305_final + +; --- External data (data.asm BSS) --- +.import cc20_key, cc20_nonce, cc20_counter +.import cc20_keystream +.import cc20_remain_hi +.import poly_r, poly_s +.import poly1305_tag +.import aead_key, aead_nonce +.import aead_aad_ptr, aead_aad_len +.import aead_data_ptr, aead_data_len +.import aead_tag +.import aead_scratch + +; --- Exports --- +.export aead_encrypt +.export aead_decrypt +.export aead_derive_otk +.export aead_setup_chacha +.export aead_compute_tag +.export aead_process_padded +.export aead_verify_tag + +.segment "CRYPTO_CODE" ; ============================================================================= ; aead_encrypt - ChaCha20-Poly1305 authenticated encryption diff --git a/src/crypto/chacha20.asm b/src/crypto/chacha20.s similarity index 72% rename from src/crypto/chacha20.asm rename to src/crypto/chacha20.s index 88e67d6..0a54f1c 100644 --- a/src/crypto/chacha20.asm +++ b/src/crypto/chacha20.s @@ -1,5 +1,5 @@ -; ============================================================================= -; chacha20.asm - ChaCha20 stream cipher (RFC 7539/8439) +; chacha20.s — ChaCha20 stream cipher (RFC 7539/8439) +; Converted from ACME to ca65 in Phase 3 Batch A. ; ; State layout: 16 x 32-bit words = 64 bytes (little-endian) ; words[0-3] = "expand 32-byte k" constants @@ -10,27 +10,95 @@ ; Uses ZP pointers w32_src1/w32_dst for word32 operations. ; ============================================================================= +.include "constants.inc" + +; --- External data (data.asm) --- +.import cc20_state +.import cc20_work +.import cc20_keystream +.import cc20_key +.import cc20_nonce +.import cc20_counter +.import cc20_remain_hi + +; --- External word32 routines (word32.asm) --- +.import add32_to_dst +.import xor32_in_place +.import rotr32_16 +.import rotl32_12 +.import rotl32_8 +.import rotl32_7 + +; --- Exports --- +.export cc20_constants +.export cc20_qr_table +.export chacha20_init +.export chacha20_quarter_round +.export chacha20_block +.export chacha20_encrypt + +; ============================================================================= +; Local macros — set w32_dst / w32_src1 to cc20_work + word_index*4 +; tbl_off is the offset within cc20_qr_table entry (0..3) +; Uses cc20_qr_idx as base row index. +; ============================================================================= +.macro cc20_set_dst tbl_off + ldx cc20_qr_idx + lda cc20_qr_table+tbl_off,x + asl + asl ; *4 for byte offset + clc + adc #cc20_work + adc #0 + sta w32_dst+1 +.endmacro + +.macro cc20_set_src1 tbl_off + ldx cc20_qr_idx + lda cc20_qr_table+tbl_off,x + asl + asl + clc + adc #cc20_work + adc #0 + sta w32_src1+1 +.endmacro + +; ============================================================================= +; Read-only data +; ============================================================================= +.segment "CRYPTO_RODATA" + ; --- ChaCha20 constants ("expand 32-byte k" as LE uint32 words) --- cc20_constants: - !byte $65, $78, $70, $61 ; 0x61707865 "expa" (LE) - !byte $6e, $64, $20, $33 ; 0x3320646e "nd 3" (LE) - !byte $32, $2d, $62, $79 ; 0x79622d32 "2-by" (LE) - !byte $74, $65, $20, $6b ; 0x6b206574 "te k" (LE) + .byte $65, $78, $70, $61 ; 0x61707865 "expa" (LE) + .byte $6e, $64, $20, $33 ; 0x3320646e "nd 3" (LE) + .byte $32, $2d, $62, $79 ; 0x79622d32 "2-by" (LE) + .byte $74, $65, $20, $6b ; 0x6b206574 "te k" (LE) ; --- Quarter-round index table --- ; 8 quarter-rounds per double-round: 4 columns + 4 diagonals ; Each entry: 4 indices (a, b, c, d) into state words cc20_qr_table: ; Column rounds - !byte 0, 4, 8, 12 ; QR(0, 4, 8, 12) - !byte 1, 5, 9, 13 ; QR(1, 5, 9, 13) - !byte 2, 6, 10, 14 ; QR(2, 6, 10, 14) - !byte 3, 7, 11, 15 ; QR(3, 7, 11, 15) + .byte 0, 4, 8, 12 ; QR(0, 4, 8, 12) + .byte 1, 5, 9, 13 ; QR(1, 5, 9, 13) + .byte 2, 6, 10, 14 ; QR(2, 6, 10, 14) + .byte 3, 7, 11, 15 ; QR(3, 7, 11, 15) ; Diagonal rounds - !byte 0, 5, 10, 15 ; QR(0, 5, 10, 15) - !byte 1, 6, 11, 12 ; QR(1, 6, 11, 12) - !byte 2, 7, 8, 13 ; QR(2, 7, 8, 13) - !byte 3, 4, 9, 14 ; QR(3, 4, 9, 14) + .byte 0, 5, 10, 15 ; QR(0, 5, 10, 15) + .byte 1, 6, 11, 12 ; QR(1, 6, 11, 12) + .byte 2, 7, 8, 13 ; QR(2, 7, 8, 13) + .byte 3, 4, 9, 14 ; QR(3, 4, 9, 14) + +; ============================================================================= +; Code +; ============================================================================= +.segment "CRYPTO_CODE" ; ============================================================================= ; chacha20_init - Initialize ChaCha20 state @@ -88,46 +156,15 @@ chacha20_init: ; ; Clobbers: A, X, Y ; ============================================================================= - -; Macro-like helper: set w32_dst to cc20_work + word_index*4 -; Input: X = table offset for desired index position -; Output: w32_dst points to cc20_work[table[X]*4] -!macro cc20_set_dst .tbl_off { - ldx cc20_qr_idx - lda cc20_qr_table+.tbl_off,x - asl - asl ; *4 for byte offset - clc - adc #cc20_work - adc #0 - sta w32_dst+1 -} - -; Set w32_src1 to cc20_work + word_index*4 -!macro cc20_set_src1 .tbl_off { - ldx cc20_qr_idx - lda cc20_qr_table+.tbl_off,x - asl - asl - clc - adc #cc20_work - adc #0 - sta w32_src1+1 -} - chacha20_quarter_round: ; --- a += b --- - +cc20_set_src1 1 ; src1 = &work[b] - +cc20_set_dst 0 ; dst = &work[a] + cc20_set_src1 1 ; src1 = &work[b] + cc20_set_dst 0 ; dst = &work[a] jsr add32_to_dst ; --- d ^= a --- - +cc20_set_src1 0 ; src1 = &work[a] - +cc20_set_dst 3 ; dst = &work[d] + cc20_set_src1 0 ; src1 = &work[a] + cc20_set_dst 3 ; dst = &work[d] jsr xor32_in_place ; --- d <<<= 16 --- @@ -135,39 +172,39 @@ chacha20_quarter_round: jsr rotr32_16 ; rotr16 = rotl16 (same for 32-bit) ; --- c += d --- - +cc20_set_src1 3 ; src1 = &work[d] - +cc20_set_dst 2 ; dst = &work[c] + cc20_set_src1 3 ; src1 = &work[d] + cc20_set_dst 2 ; dst = &work[c] jsr add32_to_dst ; --- b ^= c --- - +cc20_set_src1 2 ; src1 = &work[c] - +cc20_set_dst 1 ; dst = &work[b] + cc20_set_src1 2 ; src1 = &work[c] + cc20_set_dst 1 ; dst = &work[b] jsr xor32_in_place ; --- b <<<= 12 --- jsr rotl32_12 ; --- a += b --- - +cc20_set_src1 1 ; src1 = &work[b] - +cc20_set_dst 0 ; dst = &work[a] + cc20_set_src1 1 ; src1 = &work[b] + cc20_set_dst 0 ; dst = &work[a] jsr add32_to_dst ; --- d ^= a --- - +cc20_set_src1 0 ; src1 = &work[a] - +cc20_set_dst 3 ; dst = &work[d] + cc20_set_src1 0 ; src1 = &work[a] + cc20_set_dst 3 ; dst = &work[d] jsr xor32_in_place ; --- d <<<= 8 --- jsr rotl32_8 ; --- c += d --- - +cc20_set_src1 3 ; src1 = &work[d] - +cc20_set_dst 2 ; dst = &work[c] + cc20_set_src1 3 ; src1 = &work[d] + cc20_set_dst 2 ; dst = &work[c] jsr add32_to_dst ; --- b ^= c --- - +cc20_set_src1 2 ; src1 = &work[c] - +cc20_set_dst 1 ; dst = &work[b] + cc20_set_src1 2 ; src1 = &work[c] + cc20_set_dst 1 ; dst = &work[b] jsr xor32_in_place ; --- b <<<= 7 --- diff --git a/src/crypto/ecdsa_curve.asm b/src/crypto/ecdsa_curve.asm deleted file mode 100644 index 9ef0a5e..0000000 --- a/src/crypto/ecdsa_curve.asm +++ /dev/null @@ -1,97 +0,0 @@ -; ============================================================================= -; ecdsa_curve.asm - P-256 curve parameters, point storage, helpers -; -; Imported from c64-aes256-ecdsa for TLS 1.3 certificate verification. -; Test vectors stripped — not needed for verification-only use. -; ============================================================================= - -; ============================================================================= -; P-256 Curve Parameters -; ============================================================================= -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. - -; --- Point storage --- -ec_p1: !fill 96, 0 ; working point (Jacobian) -ec_p2: !fill 96, 0 ; second point (affine X,Y only used) -ec_p3: !fill 96, 0 ; result point (Jacobian) - -; --- Temporaries for point math (mod p) --- -ec_t1: !fill 32, 0 -ec_t2: !fill 32, 0 -ec_t3: !fill 32, 0 -ec_t4: !fill 32, 0 -ec_t5: !fill 32, 0 -ec_t6: !fill 32, 0 - -; --- Helper: set fp_misc = ec_p --- -ec_set_modp: - lda #ec_p - sta fp_misc+1 - rts - -; --- Helper: set fp_misc = ec_n --- -ec_set_modn: - 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+1 - jsr fp_copy - pla - sta fp_src1+1 - pla - sta fp_src1 - rts diff --git a/src/crypto/ecdsa_curve.s b/src/crypto/ecdsa_curve.s new file mode 100644 index 0000000..3761ae2 --- /dev/null +++ b/src/crypto/ecdsa_curve.s @@ -0,0 +1,138 @@ +; 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_fp.asm b/src/crypto/ecdsa_fp.s similarity index 85% rename from src/crypto/ecdsa_fp.asm rename to src/crypto/ecdsa_fp.s index fe3d681..db0544f 100644 --- a/src/crypto/ecdsa_fp.asm +++ b/src/crypto/ecdsa_fp.s @@ -1,12 +1,34 @@ -; ============================================================================= -; ecdsa_fp.asm - Big-number primitives for ECDSA P-256 +; 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.asm. +; 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 @@ -15,6 +37,8 @@ ; ============================================================================= fp_init_sqtab = sqtab_init + .segment "CRYPTO_CODE" + ; ============================================================================= ; fp_copy: copy 32 bytes from (fp_src1) to (fp_dst) ; ============================================================================= @@ -155,10 +179,10 @@ fp_mul: lda fp_a_byte sec sbc fp_b_byte - bcs + + bcs :+ eor #$ff adc #1 -+ tay ; Y = |a-b| (always page 0) +: tay ; Y = |a-b| (always page 0) lda fp_s_hi beq @s0 @@ -215,9 +239,11 @@ fp_mul: @mul_done: rts -fp_a_byte: !byte 0 -fp_b_byte: !byte 0 -fp_s_hi: !byte 0 -fp_p_lo: !byte 0 -fp_p_hi: !byte 0 -fp_wide: !fill 64, 0 + .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_mod.asm b/src/crypto/ecdsa_mod.s similarity index 91% rename from src/crypto/ecdsa_mod.asm rename to src/crypto/ecdsa_mod.s index 664f958..0b66409 100644 --- a/src/crypto/ecdsa_mod.asm +++ b/src/crypto/ecdsa_mod.s @@ -1,11 +1,40 @@ +; ecdsa_mod.s - P-256 scalar modular arithmetic (mod n) +; Converted from ACME to ca65 in Phase 3 Batch A. ; ============================================================================= -; ecdsa_mod.asm - Modular arithmetic for ECDSA P-256 +; 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) ; ============================================================================= @@ -190,10 +219,6 @@ fp_mod_reduce: bne @cpy rts -fp_rem: !fill 33, 0 -fp_bc: !byte 0 -fp_bm: !byte 0 - ; ============================================================================= ; fp_mod_mul: fp_r0 = ((fp_src1) * (fp_src2)) mod (fp_misc) ; ============================================================================= @@ -257,9 +282,9 @@ fp_mod_inv: @mainlp: inc fp_inv_iter - bne + + bne :+ inc fp_inv_iter+1 -+ +: ; Check u == 1 lda #fp_inv_u sta fp_src1+1 jsr fp_chk_one - bne + + bne :+ jmp @u_one -+ +: ; Check v == 1 lda #fp_inv_v sta fp_src1+1 jsr fp_chk_one - bne + + bne :+ jmp @v_one -+ +: ; While u is even @halfu: lda fp_inv_u+31 @@ -449,8 +474,6 @@ fp_mod_inv: bpl @cv rts -fp_inv_iter: !word 0 - ; Check if (fp_src1) == 1: Z flag set if yes fp_chk_one: ldy #0 @@ -465,15 +488,24 @@ fp_chk_one: @no: lda #$ff ; clear Z rts -fp_inv_u: !fill 32, 0 -fp_inv_v: !fill 32, 0 -fp_inv_x1: !fill 32, 0 -fp_inv_x2: !fill 32, 0 - ; ============================================================================= -; Working registers +; BSS / scratch ; ============================================================================= -fp_r0: !fill 32, 0 ; primary result register -fp_r1: !fill 32, 0 -fp_r2: !fill 32, 0 -fp_r3: !fill 32, 0 +.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_points.asm b/src/crypto/ecdsa_points.s similarity index 93% rename from src/crypto/ecdsa_points.asm rename to src/crypto/ecdsa_points.s index 87a50d5..4c33b79 100644 --- a/src/crypto/ecdsa_points.asm +++ b/src/crypto/ecdsa_points.s @@ -1,10 +1,53 @@ -; ============================================================================= -; ecdsa_points.asm - Point operations for ECDSA P-256 +; 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) @@ -780,17 +823,11 @@ ec_scalar_mul: bpl @cfin rts -ec_sc_byte: !byte 0 -ec_sc_mask: !byte 0 - ; ============================================================================= ; 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_affine_x: !fill 32, 0 -ec_affine_y: !fill 32, 0 - ec_jacobian_to_affine: jsr ec_set_modp diff --git a/src/crypto/ecdsa_verify.asm b/src/crypto/ecdsa_verify.s similarity index 87% rename from src/crypto/ecdsa_verify.asm rename to src/crypto/ecdsa_verify.s index cdfbcd1..586a656 100644 --- a/src/crypto/ecdsa_verify.asm +++ b/src/crypto/ecdsa_verify.s @@ -1,5 +1,5 @@ -; ============================================================================= -; ecdsa_verify.asm - ECDSA signature verification for P-256 and P-384 +; ecdsa_verify.s - P-256 (and P-384 dispatch stub) ECDSA signature verification +; Converted from ACME to ca65 in Phase 3 Batch A. ; ; Verifies ECDSA signatures as required for TLS 1.3 CertificateVerify ; (P-256/SHA-256) and certificate chain verification (P-384). @@ -11,26 +11,92 @@ ; ecdsa_pubkey_x/y (32 or 48 bytes each) = public key Q ; Output: C=0 signature valid, C=1 invalid ; -; Algorithm: -; 1. Check 0 < r < n and 0 < s < n -; 2. w = s^(-1) mod n -; 3. u1 = z * w mod n -; 4. u2 = r * w mod n -; 5. R = u1*G + u2*Q (two scalar multiplies + point addition) -; 6. Convert R to affine coordinates -; 7. Check R.x mod n == r -; -; External dependencies: -; P-256: fp_copy, fp_zero, fp_cmp, fp_is_zero, fp_mod_mul, fp_mod_inv, -; fp_mod_reduce, ec_set_modn, ec_set_modp, -; ec_scalar_mul, ec_point_add, ec_jacobian_to_affine -; ec_p1, ec_p2, ec_p3, ec_t1..ec_t6, -; ec_gx, ec_gy, ec_n, fp_r0, fp_wide -; P-384: _384 suffixed versions of all the above -; -; ZP: fp_src1, fp_src2, fp_dst, fp_misc, fp_carry, ec_scalar_ptr +; 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. ; ============================================================================= +.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) --- +.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 + +.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 + +.segment "CRYPTO_CODE" + ; ============================================================================= ; Curve dispatch ; ============================================================================= @@ -392,9 +458,9 @@ ecdsa_parse_der_sig: ; Expect SEQUENCE tag (0x30) lda (zp_ptr),y cmp #$30 - beq + + beq :+ jmp @der_error -+ +: iny ; Skip SEQUENCE length byte (we trust the outer length) @@ -404,9 +470,9 @@ ecdsa_parse_der_sig: ; Expect INTEGER tag (0x02) lda (zp_ptr),y cmp #$02 - beq + + beq :+ jmp @der_error -+ +: iny ; Read r length @@ -541,7 +607,3 @@ ecdsa_parse_der_sig: @der_error: sec rts - -; ============================================================================= -; Data buffers are in data.asm (moved there to avoid $7800-$7BFF sqtab region) -; ============================================================================= diff --git a/src/crypto/fe25519.asm b/src/crypto/fe25519.s similarity index 93% rename from src/crypto/fe25519.asm rename to src/crypto/fe25519.s index 9d1c016..eec50ad 100644 --- a/src/crypto/fe25519.asm +++ b/src/crypto/fe25519.s @@ -1,5 +1,5 @@ -; ============================================================================= -; fe25519.asm - Field arithmetic mod p = 2^255 - 19 +; fe25519.s - Field arithmetic mod 2^255 - 19 +; Converted from ACME to ca65 in Phase 3 Batch A. ; ; Optimized version imported from c64-x25519 project. ; Key optimizations over baseline: @@ -12,17 +12,63 @@ ; Uses ZP pointers fe_src1, fe_src2, fe_dst for operands. ; Reuses mul_8x8 and sqtab from poly1305.asm for multiplication. ; -; Key design: -; - Little-endian throughout (matches 6502 carry propagation and X25519 wire) -; - DEX/DEY for all carry-dependent loops (CPX/CPY clobber carry) -; - Reduction mod p: 2^256 ≡ 38 mod p, so multiply overflow by 38 and add -; -; ZP equates (fe_src1, fe_src2, fe_dst, lmul0, lmul1) defined in constants.asm. +; ZP equates (fe_src1, fe_src2, fe_dst, lmul0, lmul1) defined in constants.inc. ; Data labels (fe_wide, fe_tmp1..4, fe_p, mul_cached_a, mul_src2_buf, ; mul_dma_lo, mul_dma_hi, sqtab2_lo, sqtab2_hi, mul38_lo_tab, -; mul38_hi_tab) defined in data.asm. +; mul38_hi_tab) defined in data.asm — imported, unresolved until Batch D. ; ============================================================================= +.include "constants.inc" + +; --- Exports (column-0 labels) --- +.export fe_copy +.export fe_zero +.export fe_one +.export fe_add +.export fe_sub +.export fe_cmp_p +.export fe_reduce_final +.export fe_cswap +.export fe_mul +.export fe_reduce_wide +.export mul_by_38 +.export mul38_in +.export mul38_lo +.export mul38_hi +.export fe_sqr +.export fe_mul_a24 +.export fe_inv +.export fe_inv_dst +.export fe_inv_sqrn_tmp2 +.export fe_inv_sqr_cnt + +; --- Imports (data.asm BSS + poly1305 routines + boot REU helper) --- +.import fe_p +.import fe_wide +.import fe_tmp1 +.import fe_tmp2 +.import fe_tmp3 +.import mul_src2_buf +.import mul_cached_a +.import mul_dma_lo +.import mul_dma_hi +.import mul38_lo_tab +.import mul38_hi_tab +.import sqtab_lo +.import sqtab_hi +.import sqtab2_lo +.import sqtab2_hi +.import x25_a +.import x25_b +.import x25_da +.import x25_cb +.import poly_prod_lo +.import poly_prod_hi +.import mul_8x8 +.import reu_fetch_mul_row + +.segment "CRYPTO_CODE" + ; ============================================================================= ; fe_copy - Copy 32 bytes: (fe_dst) = (fe_src1) ; Clobbers: A, Y @@ -77,7 +123,7 @@ fe_add: iny dex ; DEX doesn't affect carry bne @add_loop - bcs @must_reduce ; carry out → result >= 2^256 > p + bcs @must_reduce ; carry out -> result >= 2^256 > p ; Check if result >= p jsr fe_cmp_p @@ -115,7 +161,7 @@ fe_sub: iny dex bne @sub_loop - bcs @done ; no borrow → done + bcs @done ; no borrow -> done ; Borrow: add p clc @@ -147,7 +193,7 @@ fe_cmp_p: bne @greater dey bpl @cmp_loop - sec ; equal → >= p + sec ; equal -> >= p rts @less: clc @@ -664,10 +710,6 @@ mul_by_38: sta poly_prod_hi ; prod = A*6 + A*32 = A*38 rts -mul38_in: !byte 0 -mul38_lo: !byte 0 -mul38_hi: !byte 0 - ; ============================================================================= ; fe_sqr - (fe_dst) = (fe_src1)^2 mod p ; @@ -942,7 +984,7 @@ fe_mul_a24: lda (fe_src1),y beq @skip_zero_a24 - ; src1[i] * $41 → add at offset i + ; src1[i] * $41 -> add at offset i ldx #$41 jsr mul_8x8 ldx fe_mul_i @@ -953,12 +995,12 @@ fe_mul_a24: lda fe_wide+1,x adc poly_prod_hi sta fe_wide+1,x - bcc + + bcc :+ inc fe_wide+2,x - bne + + bne :+ inc fe_wide+3,x -+ - ; src1[i] * $DB → add at offset i+1 +: + ; src1[i] * $DB -> add at offset i+1 ldy fe_mul_i lda (fe_src1),y ldx #$db @@ -971,30 +1013,30 @@ fe_mul_a24: lda fe_wide+2,x adc poly_prod_hi sta fe_wide+2,x - bcc + + bcc :+ inc fe_wide+3,x - bne + + bne :+ inc fe_wide+4,x -+ - ; src1[i] * $01 → add at offset i+2 +: + ; src1[i] * $01 -> add at offset i+2 ldy fe_mul_i lda (fe_src1),y ldx fe_mul_i clc adc fe_wide+2,x sta fe_wide+2,x - bcc + + bcc :+ inc fe_wide+3,x - bne + + bne :+ inc fe_wide+4,x -+ +: @skip_zero_a24: ldx fe_mul_i inx cpx #32 bcc @outer - ; Reduce: fe_wide[32..34] * 38 → add to fe_wide[0..31] + ; Reduce: fe_wide[32..34] * 38 -> add to fe_wide[0..31] lda fe_wide+32 beq @r_b33 jsr mul_by_38 @@ -1099,7 +1141,7 @@ fe_inv: sta fe_dst+1 jsr fe_copy ; fe_tmp1 = z - ; --- z2 = z^2 → fe_tmp2 --- + ; --- z2 = z^2 -> fe_tmp2 --- lda #fe_tmp1 @@ -1110,7 +1152,7 @@ fe_inv: sta fe_dst+1 jsr fe_sqr ; fe_tmp2 = z^2 - ; --- z4 = z2^2 → fe_tmp3 --- + ; --- z4 = z2^2 -> fe_tmp3 --- lda #fe_tmp2 @@ -1121,7 +1163,7 @@ fe_inv: sta fe_dst+1 jsr fe_sqr ; fe_tmp3 = z^4 - ; --- z8 = z4^2 → fe_tmp3 --- + ; --- z8 = z4^2 -> fe_tmp3 --- lda #fe_tmp3 @@ -1132,7 +1174,7 @@ fe_inv: sta fe_dst+1 jsr fe_sqr ; fe_tmp3 = z^8 - ; --- z9 = z8 * z → fe_tmp3 --- + ; --- z9 = z8 * z -> fe_tmp3 --- lda #fe_tmp3 @@ -1147,7 +1189,7 @@ fe_inv: sta fe_dst+1 jsr fe_mul ; fe_tmp3 = z^9 - ; --- z11 = z9 * z2 → x25_a (saved for final step) --- + ; --- z11 = z9 * z2 -> x25_a (saved for final step) --- lda #fe_tmp3 @@ -1162,7 +1204,7 @@ fe_inv: sta fe_dst+1 jsr fe_mul ; x25_a = z^11 - ; --- z22 = z11^2 → fe_tmp2 --- + ; --- z22 = z11^2 -> fe_tmp2 --- lda #x25_a @@ -1173,7 +1215,7 @@ fe_inv: sta fe_dst+1 jsr fe_sqr ; fe_tmp2 = z^22 - ; --- z_5_0 = z22 * z9 = z^31 → fe_tmp2 --- + ; --- z_5_0 = z22 * z9 = z^31 -> fe_tmp2 --- lda #fe_tmp2 @@ -1202,7 +1244,7 @@ fe_inv: lda #5 jsr fe_inv_sqrn_tmp2 ; fe_tmp2 = z_5_0^(2^5) - ; --- z_10_0 = fe_tmp2 * fe_tmp3 → x25_b (saved) --- + ; --- z_10_0 = fe_tmp2 * fe_tmp3 -> x25_b (saved) --- lda #fe_tmp2 @@ -1273,7 +1315,7 @@ fe_inv: sta fe_dst+1 jsr fe_mul ; fe_tmp2 = z^(2^40-1) - ; --- z_50_0: square 10x, multiply with z_10_0 → x25_da (saved) --- + ; --- z_50_0: square 10x, multiply with z_10_0 -> x25_da (saved) --- lda #10 jsr fe_inv_sqrn_tmp2 ; fe_tmp2 = z_40_0^(2^10) @@ -1291,7 +1333,7 @@ fe_inv: sta fe_dst+1 jsr fe_mul ; x25_da = z^(2^50-1) - ; --- z_100_0: copy z_50_0 to tmp2, square 50x, multiply → x25_cb (saved) --- + ; --- z_100_0: copy z_50_0 to tmp2, square 50x, multiply -> x25_cb (saved) --- lda #x25_da @@ -1385,9 +1427,6 @@ fe_inv: rts -; Saved destination pointer for fe_inv -fe_inv_dst: !word 0 - ; ============================================================================= ; fe_inv_sqrn_tmp2 - Square fe_tmp2 in place N times ; @@ -1410,4 +1449,14 @@ fe_inv_sqrn_tmp2: bne @loop rts -fe_inv_sqr_cnt: !byte 0 +; ============================================================================= +; Local writable scratch variables (were inline !byte 0 in ACME source). +; Kept here rather than moved to data.asm — these are file-private state. +; ============================================================================= +.segment "CRYPTO_BSS" + +mul38_in: .res 1 +mul38_lo: .res 1 +mul38_hi: .res 1 +fe_inv_dst: .res 2 +fe_inv_sqr_cnt: .res 1 diff --git a/src/crypto/hmac_drbg.asm b/src/crypto/hmac_drbg.s similarity index 92% rename from src/crypto/hmac_drbg.asm rename to src/crypto/hmac_drbg.s index ce48b53..61cf0ba 100644 --- a/src/crypto/hmac_drbg.asm +++ b/src/crypto/hmac_drbg.s @@ -1,5 +1,7 @@ +; hmac_drbg.s — HMAC-DRBG deterministic RNG +; Converted from ACME to ca65 in Phase 3 Batch A. ; ============================================================================= -; hmac_drbg.asm - HMAC-SHA256 and HMAC-DRBG (RFC 6979 + entropy-seeded) +; HMAC-SHA256 and HMAC-DRBG (RFC 6979 + entropy-seeded) ; ============================================================================= ; Adapted from c64-aes256-ecdsa for c64-https (TLS 1.3) ; @@ -12,13 +14,47 @@ ; drbg_fill_bytes - Fill buffer: zp_ptr=dest, A=count ; ; Uses SHA-256 primitives: sha256_init, sha256_process_block, sha256_final -; ZP equates (zp_ptr, zp_count) are in constants.asm -; Hardware addresses (sid_osc3, cia1_ta_lo) are in constants.asm +; ZP equates (zp_ptr, zp_count) are in constants.inc +; Hardware addresses (sid_osc3, cia1_ta_lo) are in constants.inc ; Data labels (hmac_key, hmac_val, hmac_opad_block, hmac_data_buf, ; hmac_data_len, hmac_result, drbg_seed, drbg_seed_len, ; drbg_output, drbg_buf_idx, sha256_block, sha256_hash) in data.asm ; ============================================================================= +.include "constants.inc" + +.export hmac_sha256 +.export hmac_drbg_update +.export hmac_drbg_instantiate +.export hmac_drbg_generate +.export extra_sid_count +.export extra_sid_lo +.export extra_sid_hi +.export drbg_init_entropy +.export drbg_random_byte +.export drbg_fill_bytes + +; SHA-256 primitives +.import sha256_init +.import sha256_process_block +.import sha256_final + +; Data (BSS) symbols from data.asm +.import hmac_key +.import hmac_val +.import hmac_opad_block +.import hmac_data_buf +.import hmac_data_len +.import hmac_result +.import drbg_seed +.import drbg_seed_len +.import drbg_output +.import drbg_buf_idx +.import sha256_block +.import sha256_hash + +.segment "CRYPTO_CODE" + ; ============================================================================= ; hmac_sha256 - compute HMAC-SHA256 ; Input: hmac_key (32 bytes), hmac_data_buf (hmac_data_len bytes, max 97) @@ -498,11 +534,11 @@ hmac_drbg_generate: ; Set to 0 so drbg_init_entropy skips the extra-SID XOR loop. ; ============================================================================= extra_sid_count: - !byte 0 + .byte 0 extra_sid_lo: - !byte 0 + .byte 0 extra_sid_hi: - !byte 0 + .byte 0 ; ============================================================================= ; drbg_init_entropy - collect 32 bytes from SID+CIA hardware, instantiate DRBG diff --git a/src/crypto/poly1305.asm b/src/crypto/poly1305.s similarity index 92% rename from src/crypto/poly1305.asm rename to src/crypto/poly1305.s index be1b48f..7a4c99d 100644 --- a/src/crypto/poly1305.asm +++ b/src/crypto/poly1305.s @@ -1,5 +1,5 @@ -; ============================================================================= -; poly1305.asm - Poly1305 MAC (RFC 7539) +; poly1305.s — Poly1305 MAC +; Converted from ACME to ca65 in Phase 3 Batch A. ; ; 130-bit modular arithmetic using quarter-square lookup table for fast ; 8x8->16-bit byte multiplication. @@ -7,12 +7,49 @@ ; Accumulator h: 17 bytes (136 bits, room for carries in 130-bit range) ; Key r: 16 bytes (clamped per RFC 7539) ; Key s: 16 bytes (added to final result) -; -; Quarter-square table: sqtab_lo/hi at $7800-$7BFF (1024 bytes) -; Identity: a*b = floor((a+b)^2/4) - floor((a-b)^2/4) + +.include "constants.inc" + +; --- External data (data.asm) --- +.import sqtab_lo, sqtab_hi +.import poly_h, poly_r, poly_s, poly_product, poly1305_tag +.import aead_scratch + +; --- Exports --- +.export poly1305_init +.export poly1305_clamp +.export sqtab_init +.export poly_prod_lo +.export poly_prod_hi +.export mul_8x8 +.export poly1305_multiply +.export poly1305_reduce +.export poly1305_block +.export poly1305_update +.export poly1305_final + ; ============================================================================= +; Scratch / BSS +; ============================================================================= +.segment "CRYPTO_BSS" + +poly_prod_lo: .res 1 +poly_prod_hi: .res 1 -; sqtab_lo/sqtab_hi now defined as labels in data.asm — moved out of $7800 to free code space +mul_a: .res 1 +mul_b: .res 1 +mul_s_pg: .res 1 + +; Temporaries for sqtab_init +sq_acc: .res 3 ; 24-bit accumulator for i^2 +sq_sh: .res 3 ; 24-bit shifted result (i^2 / 4) +sq_ad: .res 2 ; 16-bit addition term (2i+1) +sq_i: .res 2 ; 16-bit index counter (0..511) + +; ============================================================================= +; Code +; ============================================================================= +.segment "CRYPTO_CODE" ; ============================================================================= ; poly1305_init - Initialize Poly1305 state @@ -77,7 +114,7 @@ poly1305_clamp: rts ; ============================================================================= -; sqtab_init - Build quarter-square lookup table at $7800-$7BFF +; sqtab_init - Build quarter-square lookup table ; ; Computes floor(i^2/4) for i = 0..511 using recurrence i^2 = (i-1)^2 + 2i - 1 ; Ported from c64-aes256-ecdsa fp_init_sqtab. @@ -132,9 +169,9 @@ sqtab_init: rol sta sq_ad+1 inc sq_ad - bne + + bne :+ inc sq_ad+1 -+ +: clc lda sq_acc adc sq_ad @@ -147,20 +184,14 @@ sqtab_init: sta sq_acc+2 inc sq_i - bne + + bne :+ inc sq_i+1 -+ lda sq_i+1 +: lda sq_i+1 cmp #2 ; check if i reached 512 (0x200) beq @done jmp @loop @done: rts -; Temporaries for sqtab_init -sq_acc: !fill 3, 0 ; 24-bit accumulator for i^2 -sq_sh: !fill 3, 0 ; 24-bit shifted result (i^2 / 4) -sq_ad: !fill 2, 0 ; 16-bit addition term (2i+1) -sq_i: !fill 2, 0 ; 16-bit index counter (0..511) - ; ============================================================================= ; mul_8x8 - 8-bit x 8-bit -> 16-bit multiply using quarter-square table ; @@ -170,9 +201,6 @@ sq_i: !fill 2, 0 ; 16-bit index counter (0..511) ; Uses identity: a*b = sqtab[a+b] - sqtab[|a-b|] ; Clobbers: A, X, Y ; ============================================================================= -poly_prod_lo: !byte 0 -poly_prod_hi: !byte 0 - mul_8x8: sta mul_a ; save A stx mul_b ; save X @@ -189,10 +217,10 @@ mul_8x8: lda mul_a sec sbc mul_b - bcs + + bcs :+ eor #$ff adc #1 ; negate (carry was clear, so ADC adds 1) -+ tay ; Y = |a-b| (always page 0, <=255) +: tay ; Y = |a-b| (always page 0, <=255) ; sqtab[sum] - sqtab[|diff|] lda mul_s_pg @@ -217,10 +245,6 @@ mul_8x8: sta poly_prod_hi rts -mul_a: !byte 0 -mul_b: !byte 0 -mul_s_pg: !byte 0 - ; ============================================================================= ; poly1305_multiply - Multiply h (17 bytes) by r (16 bytes), reduce mod 2^130-5 ; @@ -522,9 +546,9 @@ poly1305_update: sta aead_scratch,y ; Point zp_ptr to scratch buffer - lda #aead_scratch + lda #>(aead_scratch) sta zp_ptr+1 ; Process with high bit = 0 (the 0x01 in the buffer handles it) diff --git a/src/crypto/sha256.asm b/src/crypto/sha256.s similarity index 87% rename from src/crypto/sha256.asm rename to src/crypto/sha256.s index 627c8c0..b0662f3 100644 --- a/src/crypto/sha256.asm +++ b/src/crypto/sha256.s @@ -1,54 +1,85 @@ -; ============================================================================= -; sha256.asm - SHA-256 hash: init, update, final, process_block, H/K constants +; sha256.s - SHA-256 hash / init, update, final, process_block +; Converted from ACME to ca65 in Phase 3 Batch A. ; ============================================================================= ; Adapted from c64-aes256-ecdsa for c64-https (TLS 1.3) ; -; ZP equates (sha_temp1, sha_temp2, sha256_round) are in constants.asm +; ZP equates (sha_temp1, sha_temp2, sha256_round) are in constants.inc ; Data labels (sha256_h0-h7, sha_a-sha_h, sha_temp3, sha_t1, sha_t2, ; sha256_block, sha256_w, sha256_hash, sha256_len, ; input_buffer, input_length) are in data.asm ; ============================================================================= +.include "constants.inc" + +; ---- Imports from data.asm BSS (resolved in Batch D) ---- +.import sha256_h0, sha256_h1, sha256_h2, sha256_h3 +.import sha256_h4, sha256_h5, sha256_h6, sha256_h7 +.import sha_a, sha_b, sha_c, sha_d, sha_e, sha_f, sha_g, sha_h +.import sha_temp3, sha_t1, sha_t2 +.import sha256_block, sha256_w, sha256_hash, sha256_len +.import input_buffer, input_length + +; ---- Exports ---- +.export sha256_h0_init, sha256_h1_init, sha256_h2_init, sha256_h3_init +.export sha256_h4_init, sha256_h5_init, sha256_h6_init, sha256_h7_init +.export sha256_k +.export sha256_init, sha256_update, sha256_final, sha256_process_block +.export sha256_load_word, sha256_load_word_to_temp2 +.export sha256_add_temp2_to_temp1 +.export sha256_sig0, sha256_sig1, sha256_big_sig0, sha256_big_sig1 +.export sha256_ch, sha256_maj, sha256_add_to_hash +.export sha256_rotr1, sha256_rotl1, sha256_rotr8 +.export sha256_rotr2, sha256_rotr6, sha256_rotr7, sha256_rotr11 +.export sha256_rotr13, sha256_rotr17, sha256_rotr18, sha256_rotr19 +.export sha256_rotr22, sha256_rotr25 +.export sha256_shr3, sha256_shr10 + ; ============================================================================= -; SHA-256 Implementation +; SHA-256 constants (read-only data) ; ============================================================================= +.segment "CRYPTO_RODATA" ; SHA-256 initial hash values (first 32 bits of fractional parts of square roots of first 8 primes) sha256_h0_init: - !byte $6a, $09, $e6, $67 + .byte $6a, $09, $e6, $67 sha256_h1_init: - !byte $bb, $67, $ae, $85 + .byte $bb, $67, $ae, $85 sha256_h2_init: - !byte $3c, $6e, $f3, $72 + .byte $3c, $6e, $f3, $72 sha256_h3_init: - !byte $a5, $4f, $f5, $3a + .byte $a5, $4f, $f5, $3a sha256_h4_init: - !byte $51, $0e, $52, $7f + .byte $51, $0e, $52, $7f sha256_h5_init: - !byte $9b, $05, $68, $8c + .byte $9b, $05, $68, $8c sha256_h6_init: - !byte $1f, $83, $d9, $ab + .byte $1f, $83, $d9, $ab sha256_h7_init: - !byte $5b, $e0, $cd, $19 + .byte $5b, $e0, $cd, $19 ; SHA-256 round constants (first 32 bits of fractional parts of cube roots of first 64 primes) sha256_k: - !byte $42, $8a, $2f, $98, $71, $37, $44, $91, $b5, $c0, $fb, $cf, $e9, $b5, $db, $a5 - !byte $39, $56, $c2, $5b, $59, $f1, $11, $f1, $92, $3f, $82, $a4, $ab, $1c, $5e, $d5 - !byte $d8, $07, $aa, $98, $12, $83, $5b, $01, $24, $31, $85, $be, $55, $0c, $7d, $c3 - !byte $72, $be, $5d, $74, $80, $de, $b1, $fe, $9b, $dc, $06, $a7, $c1, $9b, $f1, $74 - !byte $e4, $9b, $69, $c1, $ef, $be, $47, $86, $0f, $c1, $9d, $c6, $24, $0c, $a1, $cc - !byte $2d, $e9, $2c, $6f, $4a, $74, $84, $aa, $5c, $b0, $a9, $dc, $76, $f9, $88, $da - !byte $98, $3e, $51, $52, $a8, $31, $c6, $6d, $b0, $03, $27, $c8, $bf, $59, $7f, $c7 - !byte $c6, $e0, $0b, $f3, $d5, $a7, $91, $47, $06, $ca, $63, $51, $14, $29, $29, $67 - !byte $27, $b7, $0a, $85, $2e, $1b, $21, $38, $4d, $2c, $6d, $fc, $53, $38, $0d, $13 - !byte $65, $0a, $73, $54, $76, $6a, $0a, $bb, $81, $c2, $c9, $2e, $92, $72, $2c, $85 - !byte $a2, $bf, $e8, $a1, $a8, $1a, $66, $4b, $c2, $4b, $8b, $70, $c7, $6c, $51, $a3 - !byte $d1, $92, $e8, $19, $d6, $99, $06, $24, $f4, $0e, $35, $85, $10, $6a, $a0, $70 - !byte $19, $a4, $c1, $16, $1e, $37, $6c, $08, $27, $48, $77, $4c, $34, $b0, $bc, $b5 - !byte $39, $1c, $0c, $b3, $4e, $d8, $aa, $4a, $5b, $9c, $ca, $4f, $68, $2e, $6f, $f3 - !byte $74, $8f, $82, $ee, $78, $a5, $63, $6f, $84, $c8, $78, $14, $8c, $c7, $02, $08 - !byte $90, $be, $ff, $fa, $a4, $50, $6c, $eb, $be, $f9, $a3, $f7, $c6, $71, $78, $f2 + .byte $42, $8a, $2f, $98, $71, $37, $44, $91, $b5, $c0, $fb, $cf, $e9, $b5, $db, $a5 + .byte $39, $56, $c2, $5b, $59, $f1, $11, $f1, $92, $3f, $82, $a4, $ab, $1c, $5e, $d5 + .byte $d8, $07, $aa, $98, $12, $83, $5b, $01, $24, $31, $85, $be, $55, $0c, $7d, $c3 + .byte $72, $be, $5d, $74, $80, $de, $b1, $fe, $9b, $dc, $06, $a7, $c1, $9b, $f1, $74 + .byte $e4, $9b, $69, $c1, $ef, $be, $47, $86, $0f, $c1, $9d, $c6, $24, $0c, $a1, $cc + .byte $2d, $e9, $2c, $6f, $4a, $74, $84, $aa, $5c, $b0, $a9, $dc, $76, $f9, $88, $da + .byte $98, $3e, $51, $52, $a8, $31, $c6, $6d, $b0, $03, $27, $c8, $bf, $59, $7f, $c7 + .byte $c6, $e0, $0b, $f3, $d5, $a7, $91, $47, $06, $ca, $63, $51, $14, $29, $29, $67 + .byte $27, $b7, $0a, $85, $2e, $1b, $21, $38, $4d, $2c, $6d, $fc, $53, $38, $0d, $13 + .byte $65, $0a, $73, $54, $76, $6a, $0a, $bb, $81, $c2, $c9, $2e, $92, $72, $2c, $85 + .byte $a2, $bf, $e8, $a1, $a8, $1a, $66, $4b, $c2, $4b, $8b, $70, $c7, $6c, $51, $a3 + .byte $d1, $92, $e8, $19, $d6, $99, $06, $24, $f4, $0e, $35, $85, $10, $6a, $a0, $70 + .byte $19, $a4, $c1, $16, $1e, $37, $6c, $08, $27, $48, $77, $4c, $34, $b0, $bc, $b5 + .byte $39, $1c, $0c, $b3, $4e, $d8, $aa, $4a, $5b, $9c, $ca, $4f, $68, $2e, $6f, $f3 + .byte $74, $8f, $82, $ee, $78, $a5, $63, $6f, $84, $c8, $78, $14, $8c, $c7, $02, $08 + .byte $90, $be, $ff, $fa, $a4, $50, $6c, $eb, $be, $f9, $a3, $f7, $c6, $71, $78, $f2 + +; ============================================================================= +; SHA-256 Implementation (code) +; ============================================================================= +.segment "CRYPTO_CODE" ; ============================================================================= ; sha256_init - initialize hash state @@ -885,11 +916,11 @@ sha256_rotr1: ror sha_temp1+1 ror sha_temp1+2 ror sha_temp1+3 - bcc + + bcc :+ lda sha_temp1 ora #$80 sta sha_temp1 -+ rts +: rts ; rotate sha_temp1 left by 1 bit sha256_rotl1: @@ -897,11 +928,11 @@ sha256_rotl1: rol sha_temp1+2 rol sha_temp1+1 rol sha_temp1 - bcc + + bcc :+ lda sha_temp1+3 ora #$01 sta sha_temp1+3 -+ rts +: rts ; rotate sha_temp1 right by 8: [B0 B1 B2 B3] -> [B3 B0 B1 B2] sha256_rotr8: diff --git a/src/crypto/word32.asm b/src/crypto/word32.s similarity index 96% rename from src/crypto/word32.asm rename to src/crypto/word32.s index 04cad66..3225948 100644 --- a/src/crypto/word32.asm +++ b/src/crypto/word32.s @@ -1,5 +1,7 @@ -; ============================================================================= -; word32.asm - 32-bit word operations (little-endian) +; word32.s - 32-bit word primitives +; Converted from ACME to ca65 in Phase 3 Batch A. +; +; 32-bit word operations (little-endian) ; ; All operations use zero-page pointers: ; w32_src1 / w32_src2 = source operands @@ -8,6 +10,28 @@ ; Little-endian words: byte[0] = LSB, byte[3] = MSB ; ============================================================================= +.include "constants.inc" + +.export add32 +.export add32_to_dst +.export xor32 +.export xor32_in_place +.export rotr32_16 +.export rotr32_8 +.export rotr32_12 +.export rotr32_4 +.export rotr32_7 +.export rotl32_1 +.export rotl32_8 +.export rotl32_4 +.export rotl32_12 +.export rotr32_1 +.export rotl32_7 +.export copy32 +.export zero32 + +.segment "CRYPTO_CODE" + ; ============================================================================= ; add32 - 32-bit addition: (w32_dst) = (w32_src1) + (w32_src2) ; Preserves: X @@ -290,12 +314,12 @@ rotl32_1: rol sta (w32_dst),y ; carry = old MSB, wraps to bit 0 of byte 0 - bcc + + bcc :+ ldy #0 lda (w32_dst),y ora #$01 sta (w32_dst),y -+ +: rts ; ============================================================================= @@ -450,12 +474,12 @@ rotr32_1: ror sta (w32_dst),y ; carry = old LSB, wraps to bit 7 of byte 3 - bcc + + bcc :+ ldy #3 lda (w32_dst),y ora #$80 sta (w32_dst),y -+ +: rts ; ============================================================================= diff --git a/src/crypto/x25519.asm b/src/crypto/x25519.s similarity index 90% rename from src/crypto/x25519.asm rename to src/crypto/x25519.s index 41a2a46..f879d6d 100644 --- a/src/crypto/x25519.asm +++ b/src/crypto/x25519.s @@ -1,12 +1,9 @@ -; ============================================================================= -; x25519.asm - X25519 Diffie-Hellman (RFC 7748) +; x25519.s — Curve25519 scalar multiplication +; Converted from ACME to ca65 in Phase 3 Batch A. ; +; X25519 Diffie-Hellman (RFC 7748) ; Montgomery ladder scalar multiplication on Curve25519. -; Uses fe25519.asm field arithmetic. -; -; Optimized version imported from c64-x25519 project: -; - Streamlined bit extraction (single read of scalar byte, no double-read) -; - RFC 7748 u-coordinate high-bit masking in scalarmult +; Uses fe25519.s field arithmetic. ; ; API: ; x25519_clamp - Clamp 32-byte scalar per RFC 7748 @@ -16,9 +13,49 @@ ; Input: x25_scalar (32 bytes), x25_u (32 bytes) ; Output: x25_result (32 bytes) ; -; ZP equates (x25_prev_bit, x25_byte_idx, x25_bit_mask) in constants.asm. +; ZP equates (x25_prev_bit, x25_byte_idx, x25_bit_mask, +; fe_src1, fe_src2, fe_dst, fe_carry) in constants.inc. ; Data labels (x25_scalar, x25_u, x25_result, etc.) in data.asm. -; ============================================================================= + + .include "constants.inc" + + .export x25519_clamp + .export x25519_scalarmult + .export x25519_ladder_step + .export x25519_base + + ; Field arithmetic (fe25519.s) + .import fe_one + .import fe_zero + .import fe_copy + .import fe_add + .import fe_sub + .import fe_sqr + .import fe_mul + .import fe_mul_a24 + .import fe_inv + .import fe_cswap + + ; Field temporaries and X25519 working storage (data.asm BSS) + .import fe_tmp1 + .import fe_tmp2 + .import fe_tmp3 + .import fe_tmp4 + .import x25_scalar + .import x25_u + .import x25_result + .import x25_x2 + .import x25_z2 + .import x25_x3 + .import x25_z3 + .import x25_a + .import x25_b + .import x25_da + .import x25_cb + .import x25_e + .import x25_basepoint + + .segment "CRYPTO_CODE" ; ============================================================================= ; x25519_clamp - Clamp scalar per RFC 7748 §5 From 2424bbb245b98bf3cf3b8db5d9134c66d9d932ad Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Wed, 15 Apr 2026 09:36:55 -0500 Subject: [PATCH 12/22] Phase 3 Batch B: convert TLS primitives to ca65 (post-PR-13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Converts 6 TLS primitive files from ACME to ca65 object format on the post-PR-13 merged base: - src/hkdf.s — HKDF (RFC 5869) wrapping HMAC-SHA256 - src/tls_transcript.s — TLS 1.3 handshake transcript hash - src/tls_ecdh.s — TLS 1.3 X25519 key exchange wrapper - src/tls_record_io.s — TLS record TCP I/O - src/tls_record.s — TLS 1.3 record layer framing + AEAD - src/tls_keyschedule.s — TLS 1.3 HKDF key derivation tree All 6 files assembled clean on first try via ca65 -I src. All fixes from commits 1c75ed9, ac57d1f, eab7570 are natively present in the post-PR-13 source and carried through the conversion verbatim: - tls_record_io.s: TLS_STATE_ENCRYPTED_EXT (\$03) state check + CCS (ChangeCipherSpec) filter per RFC 8446 \xA75. - tls_record.s: 16-bit aead_data_len store sequence in both tls_record_encrypt and tls_record_decrypt. - tls_keyschedule.s: clc before rts in tls_derive_handshake_keys to clear stale carry from hmac_sha256. Cross-file imports (crypto .o files, data.asm BSS) resolve at link time once Batches C and D complete. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/{hkdf.asm => hkdf.s} | 127 +++++--- src/{tls_ecdh.asm => tls_ecdh.s} | 23 +- ...{tls_keyschedule.asm => tls_keyschedule.s} | 259 ++++++++------- src/{tls_record.asm => tls_record.s} | 62 +++- src/{tls_record_io.asm => tls_record_io.s} | 74 +++-- src/tls_transcript.asm | 278 ---------------- src/tls_transcript.s | 298 ++++++++++++++++++ 7 files changed, 656 insertions(+), 465 deletions(-) rename src/{hkdf.asm => hkdf.s} (75%) rename src/{tls_ecdh.asm => tls_ecdh.s} (86%) rename src/{tls_keyschedule.asm => tls_keyschedule.s} (84%) rename src/{tls_record.asm => tls_record.s} (92%) rename src/{tls_record_io.asm => tls_record_io.s} (87%) delete mode 100644 src/tls_transcript.asm create mode 100644 src/tls_transcript.s diff --git a/src/hkdf.asm b/src/hkdf.s similarity index 75% rename from src/hkdf.asm rename to src/hkdf.s index 3c023c5..1fd7875 100644 --- a/src/hkdf.asm +++ b/src/hkdf.s @@ -1,5 +1,5 @@ -; ============================================================================= -; hkdf.asm - HKDF-SHA256 (RFC 5869) for TLS 1.3 key derivation +; hkdf.s - HKDF-SHA256 (RFC 5869) for TLS 1.3 key derivation +; Converted from ACME to ca65 in Phase 3 Batch B. ; ; TLS 1.3 key schedule uses three HKDF operations: ; HKDF-Extract(salt, IKM) = HMAC-SHA256(salt, IKM) @@ -12,8 +12,41 @@ ; For TLS 1.3 with SHA-256, L <= 32 always, so we only need T(1). ; This simplifies HKDF-Expand to a single HMAC call. ; -; Dependencies: hmac_sha256 from hmac_drbg.asm (HMAC-SHA256 primitive) -; ============================================================================= +; Dependencies: hmac_sha256 from hmac_drbg.s (HMAC-SHA256 primitive) + + .include "constants.inc" + + .export hkdf_extract + .export hkdf_expand + .export hkdf_expand_label + .export tls_derive_secret + + ; HMAC primitive (from hmac_drbg.s) + .import hmac_sha256 + .import hmac_key + .import hmac_data_buf + .import hmac_data_len + .import hmac_result + + ; HKDF I/O buffers (data.asm BSS — unresolved until Batch D) + .import hkdf_salt_ptr + .import hkdf_salt_len + .import hkdf_ikm_ptr + .import hkdf_ikm_len + .import hkdf_prk + .import hkdf_info_buf + .import hkdf_info_len + .import hkdf_out_len + .import hkdf_okm + .import hkdf_label_ptr + .import hkdf_label_len + .import hkdf_context_ptr + .import hkdf_context_len + + ; TLS transcript hash output buffer + .import tls_transcript + + .segment "CODE" ; ============================================================================= ; hkdf_extract - HKDF-Extract(salt, IKM) -> PRK @@ -27,7 +60,7 @@ hkdf_extract: ; Step 1: Set up HMAC key from salt lda hkdf_salt_len - beq .extract_zero_salt + beq @extract_zero_salt ; Non-empty salt: copy salt_len bytes via indirect addressing lda hkdf_salt_ptr @@ -35,36 +68,36 @@ hkdf_extract: lda hkdf_salt_ptr+1 sta zp_ptr+1 ldy #0 -.extract_copy_salt: +@extract_copy_salt: cpy hkdf_salt_len - beq .extract_zero_rest + beq @extract_zero_rest lda (zp_ptr),y sta hmac_key,y iny - bne .extract_copy_salt ; always branches (salt_len < 256) + bne @extract_copy_salt ; always branches (salt_len < 256) ; Zero-fill remainder of hmac_key (32 - salt_len bytes) -.extract_zero_rest: +@extract_zero_rest: cpy #32 - beq .extract_key_done + beq @extract_key_done lda #0 -.extract_zero_loop: +@extract_zero_loop: sta hmac_key,y iny cpy #32 - bne .extract_zero_loop - beq .extract_key_done ; always branches + bne @extract_zero_loop + beq @extract_key_done ; always branches ; Empty salt: zero-fill all 32 bytes of hmac_key -.extract_zero_salt: +@extract_zero_salt: ldx #31 lda #0 -.extract_zero_all: +@extract_zero_all: sta hmac_key,x dex - bpl .extract_zero_all + bpl @extract_zero_all -.extract_key_done: +@extract_key_done: ; Step 2: Copy IKM to hmac_data_buf lda hkdf_ikm_ptr sta zp_ptr @@ -72,14 +105,14 @@ hkdf_extract: sta zp_ptr+1 ldy #0 lda hkdf_ikm_len - beq .extract_ikm_done -.extract_copy_ikm: + beq @extract_ikm_done +@extract_copy_ikm: lda (zp_ptr),y sta hmac_data_buf,y iny cpy hkdf_ikm_len - bne .extract_copy_ikm -.extract_ikm_done: + bne @extract_copy_ikm +@extract_ikm_done: ; Step 3: Set data length and call HMAC lda hkdf_ikm_len @@ -88,11 +121,11 @@ hkdf_extract: ; Step 4: Copy hmac_result to hkdf_prk ldx #31 -.extract_copy_result: +@extract_copy_result: lda hmac_result,x sta hkdf_prk,x dex - bpl .extract_copy_result + bpl @extract_copy_result rts ; ============================================================================= @@ -108,23 +141,23 @@ hkdf_expand: ; Step 1: Copy hkdf_prk to hmac_key (32 bytes) ldx #31 -.expand_copy_prk: +@expand_copy_prk: lda hkdf_prk,x sta hmac_key,x dex - bpl .expand_copy_prk + bpl @expand_copy_prk ; Step 2: Copy hkdf_info_len bytes from hkdf_info_buf to hmac_data_buf ldy #0 lda hkdf_info_len - beq .expand_info_done -.expand_copy_info: + beq @expand_info_done +@expand_copy_info: lda hkdf_info_buf,y sta hmac_data_buf,y iny cpy hkdf_info_len - bne .expand_copy_info -.expand_info_done: + bne @expand_copy_info +@expand_info_done: ; Step 3: Append 0x01 byte at end of info lda #$01 @@ -141,14 +174,14 @@ hkdf_expand: ; Step 6: Copy first hkdf_out_len bytes of hmac_result to hkdf_okm ldx hkdf_out_len - beq .expand_done + beq @expand_done dex -.expand_copy_okm: +@expand_copy_okm: lda hmac_result,x sta hkdf_okm,x dex - bpl .expand_copy_okm -.expand_done: + bpl @expand_copy_okm +@expand_done: rts ; ============================================================================= @@ -188,12 +221,12 @@ hkdf_expand_label: ; [3..8] = "tls13 " prefix (6 bytes) ldy #0 -.elabel_copy_prefix: +@elabel_copy_prefix: lda hkdf_tls13_prefix,y sta hkdf_info_buf+3,y iny cpy #6 - bne .elabel_copy_prefix + bne @elabel_copy_prefix ; X = 9 (next write position, absolute index into hkdf_info_buf) ldx #9 @@ -204,16 +237,16 @@ hkdf_expand_label: lda hkdf_label_ptr+1 sta zp_ptr+1 lda hkdf_label_len - beq .elabel_label_done + beq @elabel_label_done ldy #0 ; source index -.elabel_copy_label: +@elabel_copy_label: lda (zp_ptr),y sta hkdf_info_buf,x iny inx cpy hkdf_label_len - bne .elabel_copy_label -.elabel_label_done: + bne @elabel_copy_label +@elabel_label_done: ; Store context_len at current position lda hkdf_context_len @@ -226,16 +259,16 @@ hkdf_expand_label: lda hkdf_context_ptr+1 sta zp_ptr+1 lda hkdf_context_len - beq .elabel_ctx_done + beq @elabel_ctx_done ldy #0 ; source index -.elabel_copy_ctx: +@elabel_copy_ctx: lda (zp_ptr),y sta hkdf_info_buf,x iny inx cpy hkdf_context_len - bne .elabel_copy_ctx -.elabel_ctx_done: + bne @elabel_copy_ctx +@elabel_ctx_done: ; hkdf_info_len = X (total bytes written) stx hkdf_info_len @@ -251,9 +284,9 @@ hkdf_expand_label: ; ============================================================================= tls_derive_secret: ; Set context = transcript hash (32 bytes) - lda #tls_transcript + lda #>(tls_transcript) sta hkdf_context_ptr+1 lda #32 sta hkdf_context_len @@ -264,5 +297,7 @@ tls_derive_secret: ; ============================================================================= ; Constant: "tls13 " prefix for labels ; ============================================================================= + .segment "RODATA" + hkdf_tls13_prefix: - !text "tls13 " ; 6 bytes, no null terminator + .byte "tls13 " ; 6 bytes, no null terminator diff --git a/src/tls_ecdh.asm b/src/tls_ecdh.s similarity index 86% rename from src/tls_ecdh.asm rename to src/tls_ecdh.s index 1b32d6a..b7a6e98 100644 --- a/src/tls_ecdh.asm +++ b/src/tls_ecdh.s @@ -1,5 +1,5 @@ -; ============================================================================= -; tls_ecdh.asm - ECDH key exchange wrapper for TLS 1.3 +; tls_ecdh.s — TLS 1.3 ECDH (X25519) wrapper +; Converted from ACME to ca65 in Phase 3 Batch B. ; ; Uses x25519 (RFC 7748) for ephemeral key exchange. ; @@ -19,7 +19,24 @@ ; x25_result (32 bytes) = output ; x25519_base = scalar * basepoint(9) (clamps + scalarmult) ; x25519_scalarmult = scalar * u (raw, caller must clamp) -; ============================================================================= + +.include "constants.inc" + +.export tls_ecdh_generate_keypair +.export tls_ecdh_compute_shared + +.import x25519_base +.import x25519_scalarmult +.import x25519_clamp +.import x25_scalar +.import x25_u +.import x25_result +.import tls_ecdhe_privkey +.import tls_ecdhe_pubkey +.import tls_server_pubkey +.import tls_shared_secret + +.segment "CODE" ; ============================================================================= ; tls_ecdh_generate_keypair diff --git a/src/tls_keyschedule.asm b/src/tls_keyschedule.s similarity index 84% rename from src/tls_keyschedule.asm rename to src/tls_keyschedule.s index 405d9bf..2fee26a 100644 --- a/src/tls_keyschedule.asm +++ b/src/tls_keyschedule.s @@ -1,5 +1,5 @@ -; ============================================================================= -; tls_keyschedule.asm - TLS 1.3 key schedule and Finished MAC +; tls_keyschedule.s — TLS 1.3 HKDF key derivation +; Converted from ACME to ca65 in Phase 3 Batch B. ; ; Implements RFC 8446 §7.1 key schedule: ; - tls_derive_handshake_keys: ECDHE → handshake traffic keys @@ -7,10 +7,60 @@ ; - tls_compute_finished: compute Finished verify_data ; - tls_verify_finished: verify server's Finished message ; -; Dependencies: hkdf.asm (hkdf_extract, hkdf_expand_label, tls_derive_secret) -; hmac_sha256 from hmac_drbg.asm -; data.asm (all buffer labels) -; ============================================================================= +; Dependencies: hkdf.s (hkdf_extract, hkdf_expand_label, tls_derive_secret) +; hmac_sha256 from crypto/hmac_drbg.s +; data.asm (all BSS buffer labels) + +.include "constants.inc" + +.export tls_derive_handshake_keys +.export tls_derive_traffic_keys +.export tls_compute_finished +.export tls_verify_finished + +; HKDF primitives (hkdf.s) +.import hkdf_extract +.import hkdf_expand_label +.import tls_derive_secret + +; HMAC primitive (crypto/hmac_drbg.s) +.import hmac_sha256 + +; HKDF BSS state (data.asm) +.import hkdf_prk +.import hkdf_okm +.import hkdf_salt_ptr +.import hkdf_salt_len +.import hkdf_ikm_ptr +.import hkdf_ikm_len +.import hkdf_label_ptr +.import hkdf_label_len +.import hkdf_context_ptr +.import hkdf_context_len +.import hkdf_out_len + +; TLS state / buffers (data.asm) +.import tls_shared_secret +.import tls_transcript +.import tls_early_secret +.import tls_handshake_secret +.import tls_master_secret +.import tls_hs_write_key +.import tls_hs_write_iv +.import tls_hs_read_key +.import tls_hs_read_iv +.import tls_app_write_key +.import tls_app_write_iv +.import tls_app_read_key +.import tls_app_read_iv +.import tls_hs_buf +.import input_buffer + +; HMAC BSS state (data.asm) +.import hmac_key +.import hmac_data_buf +.import hmac_data_len +.import hmac_result ; ============================================================================= ; tls_derive_handshake_keys @@ -33,16 +83,18 @@ ; 8. server_hs_key = HKDF-Expand-Label(s_hs_traffic, "key", "", 32) ; 9. server_hs_iv = HKDF-Expand-Label(s_hs_traffic, "iv", "", 12) ; ============================================================================= +.segment "CODE" + tls_derive_handshake_keys: ; --- Step 1: early_secret = HKDF-Extract(salt=zeros, IKM=zeros) --- ; Write 32 zero bytes to input_buffer (salt) and input_buffer+32 (IKM) ldx #31 lda #0 -.dhk_z1: +@dhk_z1: sta input_buffer,x sta input_buffer+32,x dex - bpl .dhk_z1 + bpl @dhk_z1 ; Set salt ptr/len lda #= 24: invalid @store_continue: ; increment tls_recv_count (16-bit) inc tls_recv_count - bne + + bne :+ inc tls_recv_count+1 -+ +: ; have we received all 5 header bytes? lda tls_recv_count cmp #5 @@ -125,13 +149,13 @@ tls_recv_record: ; Validate version = 0x0303 (header[1..2]) lda tls_rec_header+1 cmp #$03 - beq + + beq :+ jmp @error -+ lda tls_rec_header+2 +: lda tls_rec_header+2 cmp #$03 - beq + + beq :+ jmp @error -+ +: lda #$04 sta tls_recv_sub_progress @@ -145,9 +169,9 @@ tls_recv_record: lda tls_rec_len+1 cmp #>TLS_REC_BUF_MAX bcc @len_ok ; high byte < 2: definitely ok - beq + ; high byte == 2: check low byte + beq :+ ; high byte == 2: check low byte jmp @error ; high byte > 2: too big -+ lda tls_rec_len +: lda tls_rec_len cmp #= $25: too big @@ -195,9 +219,9 @@ tls_recv_record: ; Increment tls_recv_count (16-bit) inc tls_recv_count - bne + + bne :+ inc tls_recv_count+1 -+ +: ; Check if tls_recv_count == tls_rec_len lda tls_recv_count cmp tls_rec_len @@ -208,7 +232,7 @@ tls_recv_record: jmp @complete -@recv_byte_tmp: !byte 0 +@recv_byte_tmp: .byte 0 ; --- Record complete --- @complete: @@ -273,7 +297,7 @@ tls_record_send_plaintext: ; tls_rec_type = inner content type ; Output: C=0 success, C=1 error ; -; Calls tls_record_encrypt (from tls_record.asm) to build the header, +; Calls tls_record_encrypt (from tls_record.s) to build the header, ; encrypt in-place, and update tls_rec_len, then sends via TCP. ; ============================================================================= tls_record_send_encrypted: @@ -347,5 +371,5 @@ tls_record_recv_and_decrypt: ; ============================================================================= ; Module data — state machine for tls_recv_record ; ============================================================================= -tls_recv_state: !byte 0 ; 0 = reading header, 1 = reading payload -tls_recv_count: !word 0 ; bytes received so far in current phase +tls_recv_state: .byte 0 ; 0 = reading header, 1 = reading payload +tls_recv_count: .word 0 ; bytes received so far in current phase diff --git a/src/tls_transcript.asm b/src/tls_transcript.asm deleted file mode 100644 index 77394e9..0000000 --- a/src/tls_transcript.asm +++ /dev/null @@ -1,278 +0,0 @@ -; ============================================================================= -; tls_transcript.asm - Streaming SHA-256 transcript hash for TLS 1.3 -; ============================================================================= -; Maintains a running SHA-256 state across arbitrary-length handshake messages. -; Unlike sha256_update (single <=63 byte input), this handles multi-block -; incremental hashing with non-destructive finalization (clone-and-pad). -; -; ZP usage: -; zp_ptr ($FB-$FC) - source data pointer (tls_transcript_update) -; zp_count ($FE) - remaining bytes in current call -; tls_rec_idx ($20) - block position index during copy -; -; External dependencies (sha256.asm / data.asm): -; sha256_init, sha256_process_block, sha256_final -; sha256_h0..h7, sha256_block, sha256_hash -; tls_transcript, tls_transcript_h0..h7 -; ============================================================================= - -; ============================================================================= -; Local data buffers -; ============================================================================= -tls_transcript_block: !fill 64, 0 ; partial block buffer -tls_transcript_block_len: !byte 0 ; bytes in current partial block (0-63) -tls_transcript_total_lo: !byte 0 ; total bytes hashed (low byte) -tls_transcript_total_hi: !byte 0 ; total bytes hashed (high byte) - -; Temporary save area for tls_transcript_hash (32 bytes) -; Used to preserve running state during non-destructive finalization -tls_transcript_save: !fill 32, 0 - -; ============================================================================= -; tls_transcript_init - Initialize transcript hash state -; ============================================================================= -; Calls sha256_init to load IV, then saves that initial state into the -; tls_transcript_h0..h7 shadow registers. Resets block buffer and counters. -; Clobbers: A, X -; ============================================================================= -tls_transcript_init: - ; Initialize SHA-256 with standard IV - jsr sha256_init - - ; Save initial hash state to transcript shadow registers - ldx #31 -- lda sha256_h0,x - sta tls_transcript_h0,x - dex - bpl - - - ; Reset partial block length and total byte counters - lda #0 - sta tls_transcript_block_len - sta tls_transcript_total_lo - sta tls_transcript_total_hi - rts - -; ============================================================================= -; tls_transcript_update - Feed data into the running transcript hash -; ============================================================================= -; Input: zp_ptr ($FB-$FC) = pointer to data -; zp_count ($FE) = length (1-255, call multiple times for >255) -; Clobbers: A, X, Y -; -; Algorithm: -; 1. Copy bytes from source into tls_transcript_block at current offset -; 2. When block reaches 64 bytes, process it through SHA-256 -; 3. Continue until all input consumed -; 4. Update total byte counter -; ============================================================================= -tls_transcript_update: - ; Update total byte counter (16-bit addition) - clc - lda tls_transcript_total_lo - adc zp_count - sta tls_transcript_total_lo - lda tls_transcript_total_hi - adc #0 - sta tls_transcript_total_hi - -@update_loop: - ; Check if any bytes remain - lda zp_count - beq @update_done - - ; Load current block position - ldx tls_transcript_block_len - - ; Copy bytes into partial block until block full or input exhausted -@copy_byte: - ldy #0 - lda (zp_ptr),y - - sta tls_transcript_block,x - inx - - ; Advance source pointer - inc zp_ptr - bne + - inc zp_ptr+1 -+ - ; Decrement remaining count - dec zp_count - - ; Check if block is full (64 bytes) - cpx #64 - beq @block_full - - ; Check if more bytes remain - lda zp_count - bne @copy_byte - - ; Input exhausted, save block position and return - stx tls_transcript_block_len - rts - -@block_full: - ; Block is full — process it through SHA-256 - ; Reset block length (will be 0 after processing) - lda #0 - sta tls_transcript_block_len - - ; Step 1: Restore running state to SHA-256 working registers - ldx #31 -- lda tls_transcript_h0,x - sta sha256_h0,x - dex - bpl - - - ; Step 2: Copy transcript block to sha256_block - ldx #63 -- lda tls_transcript_block,x - sta sha256_block,x - dex - bpl - - - ; Step 3: Process the block - jsr sha256_process_block - - ; Step 4: Save updated state back to transcript shadow registers - ldx #31 -- lda sha256_h0,x - sta tls_transcript_h0,x - dex - bpl - - - ; Continue with remaining bytes (if any) - jmp @update_loop - -@update_done: - rts - -; ============================================================================= -; tls_transcript_hash - Get current hash WITHOUT destroying running state -; ============================================================================= -; Output: tls_transcript (32 bytes) = current SHA-256 hash of all data fed so far -; Clobbers: A, X, Y -; -; This performs SHA-256 padding and finalization on a CLONE of the running -; state, so the transcript can continue to accept more data afterward. -; ============================================================================= -tls_transcript_hash: - ; Step 1: Save running state (will be restored at the end) - ldx #31 -- lda tls_transcript_h0,x - sta tls_transcript_save,x - dex - bpl - - - ; Step 2: Restore running state to SHA-256 working registers - ldx #31 -- lda tls_transcript_h0,x - sta sha256_h0,x - dex - bpl - - - ; Step 3: Copy partial block to sha256_block, zero-fill the rest - ; First, clear the entire block - lda #0 - ldx #63 -- sta sha256_block,x - dex - bpl - - - ; Copy the partial data - ldx tls_transcript_block_len - beq @add_padding ; no partial data to copy - dex -- lda tls_transcript_block,x - sta sha256_block,x - dex - bpl - - -@add_padding: - ; Step 4a: Append 0x80 byte after data - ldx tls_transcript_block_len - lda #$80 - sta sha256_block,x - - ; Step 4b: Check if padding fits in this block - ; Need room for 0x80 + 8 bytes of length = need block_len <= 55 - lda tls_transcript_block_len - cmp #56 - bcc @pad_fits - - ; Block_len >= 56: not enough room for length field - ; Process this block (with 0x80 and zeros), then use a fresh block for length - jsr sha256_process_block - - ; Clear the new block - lda #0 - ldx #63 -- sta sha256_block,x - dex - bpl - - -@pad_fits: - ; Step 4c: Write total bit count at block[56..63] (big-endian 64-bit) - ; Total bits = tls_transcript_total * 8 - ; Since total is 16-bit, bit count is at most 19 bits - ; bit_count = (total_hi : total_lo) << 3 - ; - ; 64-bit big-endian layout in block[56..63]: - ; block[56..60] = 0 (high 40 bits always zero for 19-bit value) - ; block[61] = high byte of bit count >> 16 (bits 16-18) - ; block[62] = mid byte of bit count (bits 8-15) - ; block[63] = low byte of bit count (bits 0-7) - - ; Compute bit count = total * 8 (shift left 3) - lda tls_transcript_total_lo - asl ; *2 - sta sha256_block+63 - lda tls_transcript_total_hi - rol - sta sha256_block+62 - lda #0 - rol - sta sha256_block+61 - - lda sha256_block+63 - asl ; *4 - sta sha256_block+63 - lda sha256_block+62 - rol - sta sha256_block+62 - lda sha256_block+61 - rol - sta sha256_block+61 - - lda sha256_block+63 - asl ; *8 - sta sha256_block+63 - lda sha256_block+62 - rol - sta sha256_block+62 - lda sha256_block+61 - rol - sta sha256_block+61 - - ; Step 5: Process final padded block - jsr sha256_process_block - - ; Step 6: Copy hash state to output - jsr sha256_final - - ; Copy sha256_hash to tls_transcript - ldx #31 -- lda sha256_hash,x - sta tls_transcript,x - dex - bpl - - - ; Step 8: Restore running state from save area - ldx #31 -- lda tls_transcript_save,x - sta tls_transcript_h0,x - dex - bpl - - - rts diff --git a/src/tls_transcript.s b/src/tls_transcript.s new file mode 100644 index 0000000..e6e6d13 --- /dev/null +++ b/src/tls_transcript.s @@ -0,0 +1,298 @@ +; tls_transcript.s — TLS 1.3 handshake transcript hash +; Converted from ACME to ca65 in Phase 3 Batch B. +; ============================================================================= +; Streaming SHA-256 transcript hash for TLS 1.3 +; ============================================================================= +; Maintains a running SHA-256 state across arbitrary-length handshake messages. +; Unlike sha256_update (single <=63 byte input), this handles multi-block +; incremental hashing with non-destructive finalization (clone-and-pad). +; +; ZP usage: +; zp_ptr ($FB-$FC) - source data pointer (tls_transcript_update) +; zp_count ($FE) - remaining bytes in current call +; +; External dependencies (sha256.s / data.asm): +; sha256_init, sha256_process_block, sha256_final +; sha256_h0, sha256_block, sha256_hash +; tls_transcript, tls_transcript_h0 +; ============================================================================= + +.include "constants.inc" + +.import sha256_init +.import sha256_process_block +.import sha256_final +.import sha256_h0 +.import sha256_block +.import sha256_hash +.import tls_transcript +.import tls_transcript_h0 + +.export tls_transcript_init +.export tls_transcript_update +.export tls_transcript_hash + +; ============================================================================= +; Local data buffers (BSS) +; ============================================================================= +.segment "BSS" + +tls_transcript_block: .res 64, 0 ; partial block buffer +tls_transcript_block_len: .res 1 ; bytes in current partial block (0-63) +tls_transcript_total_lo: .res 1 ; total bytes hashed (low byte) +tls_transcript_total_hi: .res 1 ; total bytes hashed (high byte) + +; Temporary save area for tls_transcript_hash (32 bytes) +; Used to preserve running state during non-destructive finalization +tls_transcript_save: .res 32, 0 + +.segment "CODE" + +; ============================================================================= +; tls_transcript_init - Initialize transcript hash state +; ============================================================================= +; Calls sha256_init to load IV, then saves that initial state into the +; tls_transcript_h0..h7 shadow registers. Resets block buffer and counters. +; Clobbers: A, X +; ============================================================================= +tls_transcript_init: + ; Initialize SHA-256 with standard IV + jsr sha256_init + + ; Save initial hash state to transcript shadow registers + ldx #31 +: lda sha256_h0,x + sta tls_transcript_h0,x + dex + bpl :- + + ; Reset partial block length and total byte counters + lda #0 + sta tls_transcript_block_len + sta tls_transcript_total_lo + sta tls_transcript_total_hi + rts + +; ============================================================================= +; tls_transcript_update - Feed data into the running transcript hash +; ============================================================================= +; Input: zp_ptr ($FB-$FC) = pointer to data +; zp_count ($FE) = length (1-255, call multiple times for >255) +; Clobbers: A, X, Y +; +; Algorithm: +; 1. Copy bytes from source into tls_transcript_block at current offset +; 2. When block reaches 64 bytes, process it through SHA-256 +; 3. Continue until all input consumed +; 4. Update total byte counter +; ============================================================================= +tls_transcript_update: + ; Update total byte counter (16-bit addition) + clc + lda tls_transcript_total_lo + adc zp_count + sta tls_transcript_total_lo + lda tls_transcript_total_hi + adc #0 + sta tls_transcript_total_hi + +@update_loop: + ; Check if any bytes remain + lda zp_count + beq @update_done + + ; Load current block position + ldx tls_transcript_block_len + + ; Copy bytes into partial block until block full or input exhausted +@copy_byte: + ldy #0 + lda (zp_ptr),y + + sta tls_transcript_block,x + inx + + ; Advance source pointer + inc zp_ptr + bne :+ + inc zp_ptr+1 +: + ; Decrement remaining count + dec zp_count + + ; Check if block is full (64 bytes) + cpx #64 + beq @block_full + + ; Check if more bytes remain + lda zp_count + bne @copy_byte + + ; Input exhausted, save block position and return + stx tls_transcript_block_len + rts + +@block_full: + ; Block is full — process it through SHA-256 + ; Reset block length (will be 0 after processing) + lda #0 + sta tls_transcript_block_len + + ; Step 1: Restore running state to SHA-256 working registers + ldx #31 +: lda tls_transcript_h0,x + sta sha256_h0,x + dex + bpl :- + + ; Step 2: Copy transcript block to sha256_block + ldx #63 +: lda tls_transcript_block,x + sta sha256_block,x + dex + bpl :- + + ; Step 3: Process the block + jsr sha256_process_block + + ; Step 4: Save updated state back to transcript shadow registers + ldx #31 +: lda sha256_h0,x + sta tls_transcript_h0,x + dex + bpl :- + + ; Continue with remaining bytes (if any) + jmp @update_loop + +@update_done: + rts + +; ============================================================================= +; tls_transcript_hash - Get current hash WITHOUT destroying running state +; ============================================================================= +; Output: tls_transcript (32 bytes) = current SHA-256 hash of all data fed so far +; Clobbers: A, X, Y +; +; This performs SHA-256 padding and finalization on a CLONE of the running +; state, so the transcript can continue to accept more data afterward. +; ============================================================================= +tls_transcript_hash: + ; Step 1: Save running state (will be restored at the end) + ldx #31 +: lda tls_transcript_h0,x + sta tls_transcript_save,x + dex + bpl :- + + ; Step 2: Restore running state to SHA-256 working registers + ldx #31 +: lda tls_transcript_h0,x + sta sha256_h0,x + dex + bpl :- + + ; Step 3: Copy partial block to sha256_block, zero-fill the rest + ; First, clear the entire block + lda #0 + ldx #63 +: sta sha256_block,x + dex + bpl :- + + ; Copy the partial data + ldx tls_transcript_block_len + beq @add_padding ; no partial data to copy + dex +: lda tls_transcript_block,x + sta sha256_block,x + dex + bpl :- + +@add_padding: + ; Step 4a: Append 0x80 byte after data + ldx tls_transcript_block_len + lda #$80 + sta sha256_block,x + + ; Step 4b: Check if padding fits in this block + ; Need room for 0x80 + 8 bytes of length = need block_len <= 55 + lda tls_transcript_block_len + cmp #56 + bcc @pad_fits + + ; Block_len >= 56: not enough room for length field + ; Process this block (with 0x80 and zeros), then use a fresh block for length + jsr sha256_process_block + + ; Clear the new block + lda #0 + ldx #63 +: sta sha256_block,x + dex + bpl :- + +@pad_fits: + ; Step 4c: Write total bit count at block[56..63] (big-endian 64-bit) + ; Total bits = tls_transcript_total * 8 + ; Since total is 16-bit, bit count is at most 19 bits + ; bit_count = (total_hi : total_lo) << 3 + ; + ; 64-bit big-endian layout in block[56..63]: + ; block[56..60] = 0 (high 40 bits always zero for 19-bit value) + ; block[61] = high byte of bit count >> 16 (bits 16-18) + ; block[62] = mid byte of bit count (bits 8-15) + ; block[63] = low byte of bit count (bits 0-7) + + ; Compute bit count = total * 8 (shift left 3) + lda tls_transcript_total_lo + asl ; *2 + sta sha256_block+63 + lda tls_transcript_total_hi + rol + sta sha256_block+62 + lda #0 + rol + sta sha256_block+61 + + lda sha256_block+63 + asl ; *4 + sta sha256_block+63 + lda sha256_block+62 + rol + sta sha256_block+62 + lda sha256_block+61 + rol + sta sha256_block+61 + + lda sha256_block+63 + asl ; *8 + sta sha256_block+63 + lda sha256_block+62 + rol + sta sha256_block+62 + lda sha256_block+61 + rol + sta sha256_block+61 + + ; Step 5: Process final padded block + jsr sha256_process_block + + ; Step 6: Copy hash state to output + jsr sha256_final + + ; Copy sha256_hash to tls_transcript + ldx #31 +: lda sha256_hash,x + sta tls_transcript,x + dex + bpl :- + + ; Step 8: Restore running state from save area + ldx #31 +: lda tls_transcript_save,x + sta tls_transcript_h0,x + dex + bpl :- + + rts From 8fa9f6f39a21886ca9d5febdfe4c7a2bde328275 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:04:18 -0500 Subject: [PATCH 13/22] Phase 3 Batch C: convert TLS state machine and HTTP to ca65 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Converts 5 TLS state-machine and HTTP files from ACME to ca65: - src/tls13.s — TLS 1.3 state machine and record assembly - src/tls_handshake.s — ClientHello, ServerHello, finished, sig verify - src/tls_cert.s — X.509 certificate chain validation - src/der_decode.s — X.509 ASN.1 DER decoder - src/http.s — HTTP/1.1 client over TLS (www.foo.bar) All 5 files assemble clean via ca65 -I src. Cross-file imports to data.asm BSS, net.asm, and src/net/ip65/*.s remain unresolved until Batch D converts the remaining glue files. Three of the five agents hit API 500s returning their summary reports but had already completed the source conversions. A recovery agent verified all five .s files assemble cleanly before this commit. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/{der_decode.asm => der_decode.s} | 318 +++++++++++---------- src/{http.asm => http.s} | 105 +++++-- src/{tls13.asm => tls13.s} | 127 ++++++-- src/{tls_cert.asm => tls_cert.s} | 221 ++++++-------- src/{tls_handshake.asm => tls_handshake.s} | 181 +++++++----- 5 files changed, 558 insertions(+), 394 deletions(-) rename src/{der_decode.asm => der_decode.s} (74%) rename src/{http.asm => http.s} (84%) rename src/{tls13.asm => tls13.s} (83%) rename src/{tls_cert.asm => tls_cert.s} (74%) rename src/{tls_handshake.asm => tls_handshake.s} (83%) diff --git a/src/der_decode.asm b/src/der_decode.s similarity index 74% rename from src/der_decode.asm rename to src/der_decode.s index b893804..00deda4 100644 --- a/src/der_decode.asm +++ b/src/der_decode.s @@ -1,5 +1,5 @@ -; ============================================================================= -; der_decode.asm - Minimal DER/ASN.1 decoder for X.509 certificate parsing +; der_decode.s — X.509 ASN.1 DER decoder +; Converted from ACME to ca65 in Phase 3 Batch C. ; ; A "skip-and-seek" parser that extracts only the fields needed for TLS 1.3 ; certificate verification: TBS bytes (for hashing), public key, and signature. @@ -10,6 +10,39 @@ ; zp_ptr ($FB-$FC) - parse cursor into certificate buffer ; zp_temp ($FD) - temporary ; zp_count ($FE) - temporary + +.include "constants.inc" + +; --- Public exports: code --- +.export der_read_tag +.export der_read_length +.export der_skip +.export der_skip_tlv +.export der_match_oid +.export x509_parse_cert + +; --- Public exports: OID tables (RODATA) --- +.export oid_ec_pubkey +.export oid_prime256v1 +.export oid_secp384r1 +.export oid_sha256_ecdsa +.export oid_sha384_ecdsa + +; --- Public exports: BSS data --- +.export der_len +.export cert_tbs_ptr +.export cert_tbs_len +.export cert_pubkey +.export cert_pubkey_len +.export cert_sig_r +.export cert_sig_s +.export cert_sig_len +.export cert_curve_id +.export cert_buf +.export cert_buf_len + +; ============================================================================= +.segment "CODE" ; ============================================================================= ; ============================================================================= @@ -22,9 +55,9 @@ der_read_tag: lda (zp_ptr),y ; advance zp_ptr by 1 inc zp_ptr - bne + + bne :+ inc zp_ptr+1 -+ rts +: rts ; ============================================================================= ; der_read_length - Read a DER length at (zp_ptr) and advance pointer @@ -35,7 +68,7 @@ der_read_tag: der_read_length: ldy #0 lda (zp_ptr),y - bmi .long_form ; bit 7 set = long form + bmi @long_form ; bit 7 set = long form ; --- Short form: length < $80, single byte --- sta der_len @@ -43,15 +76,15 @@ der_read_length: sta der_len+1 ; advance zp_ptr by 1 inc zp_ptr - bne + + bne :+ inc zp_ptr+1 -+ rts +: rts -.long_form: +@long_form: cmp #$81 - beq .one_byte_len + beq @one_byte_len cmp #$82 - beq .two_byte_len + beq @two_byte_len ; Unsupported length encoding (>= $83 or indefinite $80) ; Set der_len = 0 as error indicator @@ -60,7 +93,7 @@ der_read_length: sta der_len+1 rts -.one_byte_len: +@one_byte_len: ; $81 xx: one length byte follows iny ; Y=1 lda (zp_ptr),y @@ -72,11 +105,11 @@ der_read_length: lda zp_ptr adc #2 sta zp_ptr - bcc + + bcc :+ inc zp_ptr+1 -+ rts +: rts -.two_byte_len: +@two_byte_len: ; $82 xx xx: two length bytes follow (big-endian) iny ; Y=1 lda (zp_ptr),y ; high byte @@ -89,9 +122,9 @@ der_read_length: lda zp_ptr adc #3 sta zp_ptr - bcc + + bcc :+ inc zp_ptr+1 -+ rts +: rts ; ============================================================================= ; der_skip - Advance zp_ptr by der_len bytes (skip over a TLV value) @@ -125,25 +158,24 @@ der_skip_tlv: ; Clobbers: A, Y ; ============================================================================= der_match_oid: - ; Store expected OID pointer in .oid_ptr (self-modifying) - sta .oid_ptr+1 - stx .oid_ptr+2 + ; Store expected OID pointer in @oid_ptr (self-modifying) + sta @oid_ptr+1 + stx @oid_ptr+2 ; Save OID length sty zp_temp dey ; start comparing from last byte -.oid_cmp_loop: +@oid_cmp_loop: lda (zp_ptr),y -.oid_ptr: +@oid_ptr: cmp $ffff,y ; self-modified: address of OID table - bne .oid_mismatch + bne @oid_mismatch dey - bpl .oid_cmp_loop - ; All bytes matched — Z flag is set (from BPL falling through with Y=$FF, - ; but we need Z=1). Force it: + bpl @oid_cmp_loop + ; All bytes matched — force Z=1 lda #0 rts -.oid_mismatch: +@oid_mismatch: lda #1 ; clear Z flag rts @@ -167,9 +199,9 @@ x509_parse_cert: ; --- Step 1: Read outer SEQUENCE tag+length --- jsr der_read_tag cmp #$30 ; SEQUENCE - beq + - jmp .parse_error -+ jsr der_read_length + beq :+ + jmp @parse_error +: jsr der_read_length ; --- Step 2: Save pointer to start of TBS SEQUENCE --- lda zp_ptr @@ -178,17 +210,14 @@ x509_parse_cert: sta cert_tbs_ptr+1 ; --- Step 3: Read TBS SEQUENCE tag+length --- - ; We need to compute cert_tbs_len = total bytes of TBS including tag+len - ; Save current position before reading tag+length jsr der_read_tag cmp #$30 ; SEQUENCE - beq + - jmp .parse_error -+ + beq :+ + jmp @parse_error +: jsr der_read_length ; cert_tbs_len = (zp_ptr - cert_tbs_ptr) + der_len - ; (zp_ptr - cert_tbs_ptr) gives the tag+length header size sec lda zp_ptr sbc cert_tbs_ptr @@ -209,33 +238,32 @@ x509_parse_cert: clc lda zp_ptr adc der_len - sta .tbs_end + sta @tbs_end lda zp_ptr+1 adc der_len+1 - sta .tbs_end+1 + sta @tbs_end+1 ; --- Step 4: Parse inside TBS --- ; 4a: Skip [0] EXPLICIT version (tag $A0) jsr der_read_tag cmp #$a0 ; context-specific, constructed, tag 0 - bne .no_version ; v1 certs may omit version + bne @no_version ; v1 certs may omit version jsr der_read_length jsr der_skip - jmp .parse_serial + jmp @parse_serial -.no_version: +@no_version: ; Tag wasn't $A0, so it's the serialNumber INTEGER. - ; We already consumed the tag byte; read length and skip value. jsr der_read_length jsr der_skip - jmp .skip_sig_alg + jmp @skip_sig_alg -.parse_serial: +@parse_serial: ; 4b: Skip INTEGER serialNumber jsr der_skip_tlv -.skip_sig_alg: +@skip_sig_alg: ; 4c: Skip SEQUENCE signatureAlgorithm jsr der_skip_tlv @@ -251,38 +279,38 @@ x509_parse_cert: ; --- 4g: Parse SEQUENCE subjectPublicKeyInfo --- jsr der_read_tag cmp #$30 ; SEQUENCE - beq + - jmp .parse_error -+ jsr der_read_length + beq :+ + jmp @parse_error +: jsr der_read_length ; Parse SEQUENCE algorithm identifier jsr der_read_tag cmp #$30 ; SEQUENCE - beq + - jmp .parse_error -+ jsr der_read_length + beq :+ + jmp @parse_error +: jsr der_read_length ; Save end of algorithmIdentifier clc lda zp_ptr adc der_len - sta .algid_end + sta @algid_end lda zp_ptr+1 adc der_len+1 - sta .algid_end+1 + sta @algid_end+1 ; Read OID tag inside algorithmIdentifier jsr der_read_tag cmp #$06 ; OID - beq + - jmp .parse_error -+ jsr der_read_length + beq :+ + jmp @parse_error +: jsr der_read_length ; Match ecPublicKey OID (1.2.840.10045.2.1) lda #oid_ec_pubkey ldy #7 ; length of oid_ec_pubkey jsr der_match_oid - bne .parse_error_jmp + bne @parse_error_jmp ; Skip past the ecPublicKey OID value jsr der_skip @@ -290,38 +318,38 @@ x509_parse_cert: ; Now read the curve OID jsr der_read_tag cmp #$06 ; OID - beq + -.parse_error_jmp: - jmp .parse_error -+ jsr der_read_length + beq :+ +@parse_error_jmp: + jmp @parse_error +: jsr der_read_length ; Try P-256 first lda #oid_prime256v1 ldy #8 ; length of oid_prime256v1 jsr der_match_oid - beq .curve_p256 + beq @curve_p256 ; Try P-384 lda #oid_secp384r1 ldy #5 ; length of oid_secp384r1 jsr der_match_oid - beq .curve_p384 + beq @curve_p384 ; Unknown curve - jmp .parse_error + jmp @parse_error -.curve_p256: +@curve_p256: lda #0 sta cert_curve_id lda #64 sta cert_pubkey_len lda #32 sta cert_sig_len - jmp .curve_done + jmp @curve_done -.curve_p384: +@curve_p384: lda #1 sta cert_curve_id lda #96 @@ -329,85 +357,84 @@ x509_parse_cert: lda #48 sta cert_sig_len -.curve_done: +@curve_done: ; Skip to end of algorithmIdentifier - lda .algid_end + lda @algid_end sta zp_ptr - lda .algid_end+1 + lda @algid_end+1 sta zp_ptr+1 ; --- Parse BIT STRING containing the public key --- jsr der_read_tag cmp #$03 ; BIT STRING - beq + - jmp .parse_error -+ jsr der_read_length + beq :+ + jmp @parse_error +: jsr der_read_length ; Skip unused bits byte (always $00) ldy #0 lda (zp_ptr),y ; (should be $00, but don't error-check — just skip) inc zp_ptr - bne + + bne :+ inc zp_ptr+1 -+ +: ; Skip uncompressed point marker ($04) ldy #0 lda (zp_ptr),y cmp #$04 - beq + - jmp .parse_error -+ inc zp_ptr - bne + + beq :+ + jmp @parse_error +: inc zp_ptr + bne :+ inc zp_ptr+1 -+ +: ; --- Copy Qx to cert_pubkey --- ; Length is cert_sig_len (32 for P-256, 48 for P-384) = half of pubkey lda cert_sig_len ; 32 or 48 sta zp_count ldy #0 -.copy_qx: +@copy_qx: lda (zp_ptr),y sta cert_pubkey,y iny cpy zp_count - bne .copy_qx + bne @copy_qx ; Advance zp_ptr by coordinate size clc lda zp_ptr adc zp_count sta zp_ptr - bcc + + bcc :+ inc zp_ptr+1 -+ +: ; --- Copy Qy to cert_pubkey + coord_size --- ; Destination offset = cert_sig_len (32 or 48) - ; Use X as destination index, Y as source index ldx zp_count ; dest starts at offset 32 or 48 ldy #0 -.copy_qy: +@copy_qy: lda (zp_ptr),y sta cert_pubkey,x inx iny cpy zp_count - bne .copy_qy + bne @copy_qy ; Advance zp_ptr past Qy clc lda zp_ptr adc zp_count sta zp_ptr - bcc + + bcc :+ inc zp_ptr+1 -+ +: ; --- Skip any remaining TBS fields (extensions, etc.) --- ; Jump to saved end-of-TBS - lda .tbs_end + lda @tbs_end sta zp_ptr - lda .tbs_end+1 + lda @tbs_end+1 sta zp_ptr+1 ; --- Step 5: Skip SEQUENCE signatureAlgorithm (after TBS) --- @@ -416,132 +443,135 @@ x509_parse_cert: ; --- Step 6: Parse BIT STRING signatureValue --- jsr der_read_tag cmp #$03 ; BIT STRING - beq + - jmp .parse_error -+ jsr der_read_length + beq :+ + jmp @parse_error +: jsr der_read_length ; Skip unused bits byte ($00) inc zp_ptr - bne + + bne :+ inc zp_ptr+1 -+ +: ; Read inner SEQUENCE (contains r, s as INTEGERs) jsr der_read_tag cmp #$30 ; SEQUENCE - beq + - jmp .parse_error -+ jsr der_read_length + beq :+ + jmp @parse_error +: jsr der_read_length ; --- Parse INTEGER r --- jsr der_read_tag cmp #$02 ; INTEGER - beq + - jmp .parse_error -+ jsr der_read_length + beq :+ + jmp @parse_error +: jsr der_read_length ; Handle leading zero pad byte ; If der_len > cert_sig_len, there's a leading $00 lda der_len sec sbc cert_sig_len - beq .copy_r ; exact length, no padding + beq @copy_r ; exact length, no padding ; Leading pad byte(s) — skip (der_len - cert_sig_len) bytes sta zp_temp ; number of pad bytes to skip -.skip_r_pad: +@skip_r_pad: inc zp_ptr - bne + + bne :+ inc zp_ptr+1 -+ dec zp_temp - bne .skip_r_pad +: dec zp_temp + bne @skip_r_pad -.copy_r: +@copy_r: ldy #0 ldx cert_sig_len ; 32 or 48 stx zp_count -.copy_r_loop: +@copy_r_loop: lda (zp_ptr),y sta cert_sig_r,y iny cpy zp_count - bne .copy_r_loop + bne @copy_r_loop ; Advance past r value clc lda zp_ptr adc zp_count sta zp_ptr - bcc + + bcc :+ inc zp_ptr+1 -+ +: ; --- Parse INTEGER s --- jsr der_read_tag cmp #$02 ; INTEGER - beq + - jmp .parse_error -+ jsr der_read_length + beq :+ + jmp @parse_error +: jsr der_read_length ; Handle leading zero pad byte lda der_len sec sbc cert_sig_len - beq .copy_s + beq @copy_s sta zp_temp -.skip_s_pad: +@skip_s_pad: inc zp_ptr - bne + + bne :+ inc zp_ptr+1 -+ dec zp_temp - bne .skip_s_pad +: dec zp_temp + bne @skip_s_pad -.copy_s: +@copy_s: ldy #0 ldx cert_sig_len stx zp_count -.copy_s_loop: +@copy_s_loop: lda (zp_ptr),y sta cert_sig_s,y iny cpy zp_count - bne .copy_s_loop + bne @copy_s_loop ; --- Success --- clc rts -.parse_error: +@parse_error: sec rts -; --- Local temporaries (not ZP, just inline storage) --- -.tbs_end: !word 0 -.algid_end: !word 0 +; --- Local temporaries (not ZP, just inline storage within x509_parse_cert) --- +@tbs_end: .word 0 +@algid_end: .word 0 ; ============================================================================= -; Known OIDs (DER-encoded value bytes, without tag and length) +.segment "RODATA" ; ============================================================================= + +; Known OIDs (DER-encoded value bytes, without tag and length) oid_ec_pubkey: ; 1.2.840.10045.2.1 (ecPublicKey) - !byte $2a,$86,$48,$ce,$3d,$02,$01 + .byte $2a,$86,$48,$ce,$3d,$02,$01 oid_prime256v1: ; 1.2.840.10045.3.1.7 (P-256) - !byte $2a,$86,$48,$ce,$3d,$03,$01,$07 + .byte $2a,$86,$48,$ce,$3d,$03,$01,$07 oid_secp384r1: ; 1.3.132.0.34 (P-384) - !byte $2b,$81,$04,$00,$22 + .byte $2b,$81,$04,$00,$22 oid_sha256_ecdsa: ; 1.2.840.10045.4.3.2 (ecdsa-with-SHA256) - !byte $2a,$86,$48,$ce,$3d,$04,$03,$02 + .byte $2a,$86,$48,$ce,$3d,$04,$03,$02 oid_sha384_ecdsa: ; 1.2.840.10045.4.3.3 (ecdsa-with-SHA384) - !byte $2a,$86,$48,$ce,$3d,$04,$03,$03 + .byte $2a,$86,$48,$ce,$3d,$04,$03,$03 ; ============================================================================= -; Data labels +.segment "BSS" ; ============================================================================= -der_len: !word 0 ; last parsed length (16-bit LE) -cert_tbs_ptr: !word 0 ; pointer to TBS bytes in cert_buf -cert_tbs_len: !word 0 ; length of TBS (tag + length + value) -cert_pubkey: !fill 96, 0 ; public key Qx||Qy (max 48+48 for P-384) -cert_pubkey_len: !byte 0 ; 64 (P-256) or 96 (P-384) -cert_sig_r: !fill 48, 0 ; signature r component (max 48 for P-384) -cert_sig_s: !fill 48, 0 ; signature s component (max 48 for P-384) -cert_sig_len: !byte 0 ; 32 (P-256) or 48 (P-384) -cert_curve_id: !byte 0 ; 0=P-256, 1=P-384 -cert_buf: !fill 1536, 0 ; certificate DER buffer -cert_buf_len: !word 0 ; certificate length + +der_len: .res 2 ; last parsed length (16-bit LE) +cert_tbs_ptr: .res 2 ; pointer to TBS bytes in cert_buf +cert_tbs_len: .res 2 ; length of TBS (tag + length + value) +cert_pubkey: .res 96 ; public key Qx||Qy (max 48+48 for P-384) +cert_pubkey_len: .res 1 ; 64 (P-256) or 96 (P-384) +cert_sig_r: .res 48 ; signature r component (max 48 for P-384) +cert_sig_s: .res 48 ; signature s component (max 48 for P-384) +cert_sig_len: .res 1 ; 32 (P-256) or 48 (P-384) +cert_curve_id: .res 1 ; 0=P-256, 1=P-384 +cert_buf: .res 1536 ; certificate DER buffer +cert_buf_len: .res 2 ; certificate length diff --git a/src/http.asm b/src/http.s similarity index 84% rename from src/http.asm rename to src/http.s index 4f2c55a..a411de4 100644 --- a/src/http.asm +++ b/src/http.s @@ -1,12 +1,67 @@ -; ============================================================================= -; http.asm - HTTP/1.1 client over TLS +; http.s — HTTP/1.1 client over TLS +; Converted from ACME to ca65 in Phase 3 Batch C. ; ; Builds HTTP requests, parses responses. Operates over the TLS layer ; (tls_send / tls_recv), so all data is encrypted transparently. ; ; For the MVP, supports only GET requests with basic response parsing ; (status line + headers + body). -; ============================================================================= + + .include "constants.inc" + + ; ---- exports ---- + .export http_get + .export http_build_get + .export http_recv_response + .export http_get_plain + .export http_get_verb + .export http_version + .export http_host_hdr + .export http_conn_hdr + .export http_crlf + .export http_bg_idx + .export http_bg_src + + ; ---- imports: data.asm BSS (HTTP I/O + parser state) ---- + .import http_host_ptr + .import http_host_len + .import http_path_ptr + .import http_path_len + .import http_port + .import http_status + .import http_req_buf + .import http_req_len + .import http_resp_buf + .import http_resp_len + .import http_parse_state + .import http_hdr_match + .import http_line_idx + .import http_line_buf + + ; ---- imports: data.asm BSS (TLS app data + TCP ring tail) ---- + .import tls_app_ptr + .import tls_app_len + .import tcp_recv_tail + + ; ---- imports: TLS handshake layer (SNI buffer + connect/close) ---- + .import tls_hostname + .import tls_hostname_len + .import tls_connect + .import tls_close + + ; ---- imports: TLS record layer (app-data send/recv) ---- + .import tls_send + .import tls_recv + + ; ---- imports: net.asm wrappers around ip65 ---- + .import net_dns_resolve + .import net_set_tcp_dest + .import net_tcp_connect + .import net_tcp_close + .import net_tcp_send + .import net_send_len + .import net_poll + .import net_recv_byte ; ============================================================================= ; http_get - perform an HTTPS GET request @@ -15,6 +70,8 @@ ; http_port = port (default 443) ; Output: C=0 success (response in http_resp_buf), C=1 failure ; ============================================================================= + .segment "CODE" + http_get: ; --- 1. DNS resolve hostname --- lda http_host_ptr @@ -74,9 +131,9 @@ http_get: lda http_req_len+1 sta tls_app_len+1 jsr tls_send - bcc + + bcc :+ jmp @close_error -+ +: ; --- 8. Receive response via TLS --- ; Initialise parser state @@ -129,7 +186,7 @@ http_get: inc tcp_recv_tail+1 @feed_mask: lda tcp_recv_tail+1 - and #>TCP_RECV_MASK + and #>(TCP_RECV_MASK) sta tcp_recv_tail+1 iny bne @feed_loop ; always branches (tls_app_len < 256) @@ -156,7 +213,7 @@ http_get: clc rts -@recv_timeout: !word 0 +@recv_timeout: .word 0 @tls_error: jsr net_tcp_close @@ -201,7 +258,7 @@ http_build_get: sta zp_ptr+1 lda http_path_len sta zp_count - jsr @copy_indirect + jsr bg_copy_indirect ; --- " HTTP/1.1\r\n" (11 bytes) --- ldx #0 @@ -236,7 +293,7 @@ http_build_get: sta zp_ptr+1 lda http_host_len sta zp_count - jsr @copy_indirect + jsr bg_copy_indirect ; --- \r\n after Host value (2 bytes) --- ldx #0 @@ -285,11 +342,13 @@ http_build_get: rts ; ----------------------------------------------------------------------------- -; @copy_indirect - copy zp_count bytes from (zp_ptr) into http_req_buf +; bg_copy_indirect - copy zp_count bytes from (zp_ptr) into http_req_buf ; at offset http_bg_idx. Advances http_bg_idx. ; Clobbers: A, X, Y +; (Was a cheap local @copy_indirect under ACME; promoted to a module-local +; label so it is reachable from http_build_get without scope games.) ; ----------------------------------------------------------------------------- -@copy_indirect: +bg_copy_indirect: ldy #0 @ci_loop: cpy zp_count @@ -554,7 +613,7 @@ http_get_plain: clc rts -@poll_timeout: !word 0 +@poll_timeout: .word 0 @plain_close_err: jsr net_tcp_close @@ -565,19 +624,25 @@ http_get_plain: ; ============================================================================= ; HTTP request/response string constants ; ============================================================================= + .segment "RODATA" + http_get_verb: - !text "GET " + .byte "GET " http_version: - !text " HTTP/1.1", $0d, $0a + .byte " HTTP/1.1", $0d, $0a http_host_hdr: - !text "Host: " + .byte "Host: " http_conn_hdr: - !text "Connection: close", $0d, $0a + .byte "Connection: close", $0d, $0a http_crlf: - !byte $0d, $0a + .byte $0d, $0a ; ============================================================================= -; Module-local data (build_get temporaries only; parser state is in data.asm) +; Module-local scratch (build_get temporaries only; parser state is in +; data.asm). These were ACME `!byte 0` slots; under ca65 they live in the +; zero-initialised BSS segment. ; ============================================================================= -http_bg_idx: !byte 0 ; build_get write cursor -http_bg_src: !byte 0 ; build_get source index temp + .segment "BSS" + +http_bg_idx: .res 1 ; build_get write cursor +http_bg_src: .res 1 ; build_get source index temp diff --git a/src/tls13.asm b/src/tls13.s similarity index 83% rename from src/tls13.asm rename to src/tls13.s index f865813..838ce53 100644 --- a/src/tls13.asm +++ b/src/tls13.s @@ -1,5 +1,5 @@ -; ============================================================================= -; tls13.asm - TLS 1.3 state machine +; tls13.s — TLS 1.3 state machine and record assembly +; Converted from ACME to ca65 in Phase 3 Batch C. ; ; Orchestrates the TLS 1.3 handshake and application data flow: ; @@ -24,7 +24,86 @@ ; After ServerHello, all messages are encrypted with handshake keys ; derived from ECDHE shared secret via HKDF. ; After both Finished, traffic keys replace handshake keys. -; ============================================================================= + +.include "constants.inc" + +; --- Public exports --- +.export tls_connect +.export tls_send +.export tls_recv +.export tls_close +.export tls_send_client_hello +.export tls_recv_server_hello +.export tls_recv_encrypted +.export tls_send_finished + +; --- TLS BSS / data (data.asm) --- +.import tls_state +.import tls_last_state +.import tls_client_random +.import tls_ecdhe_privkey +.import tls_hs_buf +.import tls_hs_len +.import tls_rec_buf +.import tls_rec_len +.import tls_rec_type +.import tls_app_ptr +.import tls_app_len +.import tls_recv_progress +.import tls_recv_poll_count + +; --- Crypto / DRBG / ECDH helpers --- +.import drbg_fill_bytes +.import tls_ecdh_generate_keypair +.import tls_ecdh_compute_shared + +; --- TLS record layer (tls_record.s / tls_record_io.s) --- +.import tls_record_send_plaintext +.import tls_record_send_encrypted +.import tls_record_recv_and_decrypt + +; --- ClientHello / ServerHello builders & parsers (tls_handshake) --- +.import tls_build_client_hello +.import tls_parse_server_hello + +; --- Transcript hash (tls_transcript.s) --- +.import tls_transcript_init +.import tls_transcript_update + +; --- Key schedule (tls_keyschedule.s) --- +.import tls_derive_handshake_keys +.import tls_derive_traffic_keys +.import tls_compute_finished +.import tls_verify_finished + +; --- Encrypted handshake sub-handlers (tls_cert.s) --- +.import tls_handle_certificate +.import tls_handle_cert_verify + +; --- Networking (net.s) --- +.import net_poll + +; --- Console output (main/util) --- +.import print_string + +; --- Status strings (data.asm / rodata) --- +.import ch_sent_msg +.import sh_recv_msg +.import hk1_msg +.import keys_ok_msg +.import ee_recv_msg +.import cert_recv_msg +.import cv_recv_msg +.import fin_recv_msg +.import cfin_sent_msg +.import enc1_msg +.import rx_msg +.import got2_msg +.import got_msg +.import dec_msg +.import proc_msg + +.segment "CODE" ; ============================================================================= ; tls_connect - perform full TLS 1.3 handshake @@ -292,21 +371,21 @@ tls_recv_server_hello: lda #$01 sta tls_recv_progress lda #0 - sta @sh_timeout - sta @sh_timeout+1 + sta sh_timeout + sta sh_timeout+1 sta tls_recv_poll_count sta tls_recv_poll_count+1 @sh_wait: inc tls_recv_poll_count - bne + + bne :+ inc tls_recv_poll_count+1 -+ +: jsr net_poll jsr tls_record_recv_and_decrypt bcc @sh_got_record - inc @sh_timeout + inc sh_timeout bne @sh_wait - inc @sh_timeout+1 + inc sh_timeout+1 bne @sh_wait ; timeout sec @@ -362,11 +441,10 @@ tls_recv_server_hello: @sh_error: sec rts -@sh_timeout: !word 0 -; tls_derive_handshake_keys — in tls_keyschedule.asm -; tls_derive_traffic_keys — in tls_keyschedule.asm -; tls_verify_finished — in tls_keyschedule.asm +; tls_derive_handshake_keys - in tls_keyschedule.s +; tls_derive_traffic_keys - in tls_keyschedule.s +; tls_verify_finished - in tls_keyschedule.s ; ============================================================================= ; tls_recv_encrypted - receive encrypted handshake msg, decrypt, dispatch @@ -377,25 +455,25 @@ tls_recv_encrypted: ldy #>enc1_msg jsr print_string lda #0 - sta @enc_timeout - sta @enc_timeout+1 + sta enc_timeout + sta enc_timeout+1 lda #rx_msg jsr print_string @enc_wait: jsr net_poll jsr tls_record_recv_and_decrypt - bcs + + bcs :+ ; success -- print GOT2 marker so we can distinguish progress lda #got2_msg jsr print_string clc jmp @enc_got_record -+ - inc @enc_timeout +: + inc enc_timeout bne @enc_wait - inc @enc_timeout+1 + inc enc_timeout+1 bne @enc_wait ; timeout sec @@ -473,7 +551,6 @@ tls_recv_encrypted: @enc_error: sec rts -@enc_timeout: !word 0 ; ============================================================================= ; tls_send_finished - compute client Finished, encrypt, send @@ -513,3 +590,13 @@ tls_send_finished: ; encrypt and send jsr tls_record_send_encrypted rts + +; ============================================================================= +; File-local BSS — 16-bit timeout counters used by recv routines. +; Originally `@sh_timeout` / `@enc_timeout` cheap locals embedded in code with +; `!word 0`. Promoted to module-scope BSS so ca65 can place them cleanly; they +; are not exported. +; ============================================================================= +.segment "BSS" +sh_timeout: .res 2 +enc_timeout: .res 2 diff --git a/src/tls_cert.asm b/src/tls_cert.s similarity index 74% rename from src/tls_cert.asm rename to src/tls_cert.s index 8558783..7504988 100644 --- a/src/tls_cert.asm +++ b/src/tls_cert.s @@ -1,24 +1,49 @@ -; ============================================================================= -; tls_cert.asm - TLS 1.3 Certificate and CertificateVerify handling +; tls_cert.s — TLS 1.3 certificate chain validation +; Converted from ACME to ca65 in Phase 3 Batch C. ; ; Processes the server's Certificate message (extracts leaf cert and ; public key) and CertificateVerify message (verifies the server's ; signature over the transcript hash). ; ; External dependencies: -; sha256.asm: sha256_init, sha256_process_block, sha256_hash, -; sha256_h0..h7, sha256_block -; ecdsa_verify.asm: ecdsa_verify, ecdsa_parse_der_sig, -; ecdsa_curve_id, ecdsa_hash, ecdsa_hash_len, -; ecdsa_sig_r, ecdsa_sig_s, ecdsa_sig_len, -; ecdsa_pubkey_x, ecdsa_pubkey_y -; tls_transcript.asm: tls_transcript (32-byte current hash) +; sha256.s: sha256_init, sha256_process_block, sha256_final, +; sha256_hash, sha256_block +; ecdsa_verify.s: ecdsa_verify, ecdsa_parse_der_sig, ecdsa_curve_id, +; ecdsa_hash, ecdsa_hash_len, ecdsa_sig_r, ecdsa_sig_s, +; ecdsa_sig_len, ecdsa_pubkey_x, ecdsa_pubkey_y +; tls_transcript.s: tls_transcript (32-byte current hash) ; data.asm: tls_hs_buf, tls_hs_len -; constants.asm: TLS_HS_CERTIFICATE, TLS_HS_CERT_VERIFY, -; TLS_SIG_ECDSA_SECP256R1_SHA256, zp_ptr, zp_count, zp_tmp1 +; constants.inc: TLS_HS_CERTIFICATE, TLS_HS_CERT_VERIFY, +; zp_ptr, zp_count, zp_tmp1, zp_tmp2 ; ; ZP usage: zp_ptr ($FB-$FC), zp_count ($FE), zp_tmp1 ($02), zp_tmp2 ($03) -; ============================================================================= + + .include "constants.inc" + + .export tls_handle_certificate + .export x509_extract_pubkey + .export tls_handle_cert_verify + + .import tls_hs_buf + .import tls_hs_len + .import tls_transcript + .import sha256_init + .import sha256_process_block + .import sha256_final + .import sha256_block + .import sha256_hash + .import ecdsa_verify + .import ecdsa_parse_der_sig + .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 + + .segment "CODE" ; ============================================================================= ; tls_handle_certificate - Process TLS 1.3 Certificate message @@ -27,20 +52,6 @@ ; tls_hs_len = message length ; Output: C=0 success (leaf cert pubkey extracted to ecdsa_pubkey_x/y) ; C=1 error (bad format, unsupported key type) -; -; TLS 1.3 Certificate message format: -; [0] HandshakeType = 11 -; [1-3] Length (24-bit big-endian) -; [4] certificate_request_context length (1 byte, 0 for server) -; [5-7] certificate_list length (24-bit) -; For each CertificateEntry: -; [+0..+2] cert_data length (24-bit) -; [+3..] cert_data (DER-encoded X.509 certificate) -; [+n..+n+1] extensions length (2 bytes) -; [+n+2..] extensions data (we skip these) -; -; We extract only the FIRST (leaf) certificate. The leaf cert's public -; key (ECDSA P-256) is parsed out via x509_extract_pubkey. ; ============================================================================= tls_handle_certificate: ldy #0 @@ -65,7 +76,6 @@ tls_handle_certificate: iny ; Y=5 ; --- certificate_list length [5-7] (24-bit, skip high byte) --- - ; We just need to know where the first cert starts. ; High byte must be 0 (certs < 64K) lda tls_hs_buf,y bne @cert_error ; cert list > 65535 bytes @@ -103,14 +113,6 @@ tls_handle_certificate: sty cert_data_offset ; --- Parse X.509 certificate to extract ECDSA public key --- - ; The DER cert contains: - ; SEQUENCE { tbsCertificate, signatureAlgorithm, signatureValue } - ; tbsCertificate SEQUENCE contains subjectPublicKeyInfo - ; subjectPublicKeyInfo: SEQUENCE { algorithm, BIT STRING { point } } - ; For ECDSA P-256: uncompressed point = 04 || X (32 bytes) || Y (32 bytes) - ; - ; We scan for the OID 1.2.840.10045.2.1 (ecPublicKey) followed by - ; the curve OID, then extract the uncompressed point. jsr x509_extract_pubkey bcc @cert_key_ok jmp @cert_error @@ -130,7 +132,6 @@ tls_handle_certificate: ; For MVP, we use zp_ptr as a 16-bit index into tls_hs_buf. ; Skip extensions after the leaf cert. - ; extensions_length at current position (2 bytes) lda zp_tmp1 sta cert_parse_pos lda zp_tmp2 @@ -146,9 +147,7 @@ tls_handle_certificate: sta cert_ext_len_lo iny - ; Skip extension bytes (we don't process cert extensions) ; Done — we only need the leaf cert's public key. - clc rts @@ -163,12 +162,6 @@ tls_handle_certificate: ; cert_data_len_hi/lo = certificate length ; Output: ecdsa_pubkey_x/y filled (32 or 48 bytes depending on curve) ; C=0 success, C=1 not found / unsupported -; -; Strategy: Scan for the ecPublicKey OID (1.2.840.10045.2.1) encoded as -; 06 07 2A 86 48 CE 3D 02 01 -; followed by curve OID (P-256: 06 08 2A 86 48 CE 3D 03 01 07 -; P-384: 06 05 2B 81 04 00 22) -; then find the BIT STRING containing the uncompressed point (04 || X || Y). ; ============================================================================= x509_extract_pubkey: ; Set up pointer to scan through cert data @@ -192,85 +185,84 @@ x509_extract_pubkey: lda zp_ptr+1 cmp cert_end_hi bcc @scan_continue - beq + + beq :+ jmp @scan_not_found -+ +: lda zp_ptr cmp cert_end_lo - bcc + + bcc :+ jmp @scan_not_found -+ +: @scan_continue: ldy #0 lda (zp_ptr),y cmp #$06 ; ASN.1 OID tag - beq + + beq :+ jmp @scan_next -+ +: ; Check if this is ecPublicKey OID iny lda (zp_ptr),y cmp #$07 ; OID length = 7 - beq + + beq :+ jmp @scan_next -+ +: ; Compare remaining OID bytes: 2A 86 48 CE 3D 02 01 iny lda (zp_ptr),y cmp #$2a - beq + + beq :+ jmp @scan_next -+ +: iny lda (zp_ptr),y cmp #$86 - beq + + beq :+ jmp @scan_next -+ +: iny lda (zp_ptr),y cmp #$48 - beq + + beq :+ jmp @scan_next -+ +: iny lda (zp_ptr),y cmp #$ce - beq + + beq :+ jmp @scan_next -+ +: iny lda (zp_ptr),y cmp #$3d - beq + + beq :+ jmp @scan_next -+ +: iny lda (zp_ptr),y cmp #$02 - beq + + beq :+ jmp @scan_next -+ +: iny lda (zp_ptr),y cmp #$01 - beq + + beq :+ jmp @scan_next -+ +: ; Found ecPublicKey OID! Now check curve OID that follows. - ; Advance past the OID (9 bytes from start) iny ; Y = 9, pointing to next byte ; Check for curve OID tag lda (zp_ptr),y cmp #$06 ; OID tag - beq + + beq :+ jmp @scan_next -+ +: iny lda (zp_ptr),y ; OID length @@ -359,9 +351,9 @@ x509_extract_pubkey: @scan_next: ; Advance pointer by 1 and continue scanning inc zp_ptr - bne + + bne :+ inc zp_ptr+1 -+ jmp @scan_loop +: jmp @scan_loop @scan_not_found: sec @@ -370,9 +362,6 @@ x509_extract_pubkey: @find_bitstring: ; After the algorithm identifier, we need the BIT STRING ; containing the uncompressed EC point. - ; The BIT STRING tag is 0x03, followed by length, then 0x00 - ; (unused bits), then 0x04 (uncompressed point marker), - ; then X || Y. ; Advance zp_ptr by Y to current position tya @@ -497,29 +486,6 @@ x509_extract_pubkey: ; tls_transcript (32 bytes) = current transcript hash ; Server's public key already in ecdsa_pubkey_x/y ; Output: C=0 signature valid, C=1 invalid -; -; CertificateVerify format: -; [0] HandshakeType = 15 -; [1-3] Length (24-bit) -; [4-5] SignatureScheme algorithm (2 bytes) -; 0x0403 = ecdsa_secp256r1_sha256 -; 0x0503 = ecdsa_secp384r1_sha384 (not supported) -; [6-7] signature length (2 bytes) -; [8..] signature (DER-encoded SEQUENCE { INTEGER r, INTEGER s }) -; -; We advertise only 0x0403 in ClientHello, so the server MUST respond -; with ecdsa_secp256r1_sha256 for CertificateVerify. This means: -; - Hash the signed content with SHA-256 (32-byte hash) -; - Verify with P-256 ECDSA -; -; The signed content is: -; 64 x 0x20 || "TLS 1.3, server CertificateVerify" || 0x00 || transcript_hash -; = 64 + 33 + 1 + 32 = 130 bytes -; -; This is a 3-block SHA-256 computation: -; Block 1: bytes 0-63 (64 spaces) -; Block 2: bytes 64-127 (label + separator + first 30 bytes of hash) -; Block 3: bytes 128-129 + padding (last 2 hash bytes + 0x80 + zeros + length) ; ============================================================================= tls_handle_cert_verify: ; --- Verify handshake type = 15 (CertificateVerify) --- @@ -533,20 +499,20 @@ tls_handle_cert_verify: ; Must be 0x0403 (ecdsa_secp256r1_sha256) lda tls_hs_buf+4 cmp #$04 - beq + + beq :+ jmp @cv_error -+ +: lda tls_hs_buf+5 cmp #$03 - beq + + beq :+ jmp @cv_error -+ +: ; --- Read signature length [6-7] (big-endian) --- lda tls_hs_buf+6 ; high byte (expect 0) - beq + + beq :+ jmp @cv_error ; signature > 255 bytes -+ +: lda tls_hs_buf+7 ; low byte sta cv_sig_len @@ -576,11 +542,6 @@ tls_handle_cert_verify: ; [64-96] "TLS 1.3, server CertificateVerify" (33 bytes) ; [97] 0x00 (separator) ; [98-129] transcript_hash (32 bytes) - ; - ; SHA-256 processes this as: - ; Block 1 (bytes 0-63): all spaces - ; Block 2 (bytes 64-127): label + sep + hash[0..29] - ; Block 3 (bytes 128-129 + padding): hash[30..31] + pad ; --------------------------------------------------------------- ; Initialize SHA-256 @@ -597,7 +558,6 @@ tls_handle_cert_verify: jsr sha256_process_block ; --- Block 2: label (33 bytes) + separator (1 byte) + hash[0..29] --- - ; Copy label "TLS 1.3, server CertificateVerify" (33 bytes) ldx #0 @copy_label: lda cv_label,x @@ -683,36 +643,33 @@ tls_handle_cert_verify: ; Signed content constant data ; ============================================================================= + .segment "RODATA" + ; The CertificateVerify context string ; (The 64 spaces are generated dynamically in Block 1 above) cv_label: - !text "TLS 1.3, server CertificateVerify" + .byte "TLS 1.3, server CertificateVerify" ; 33 bytes (no null terminator needed — length is fixed) + ; ============================================================================= -; Inline data +; Inline data — certificate parsing state ; ============================================================================= -; Certificate parsing state -cert_list_len_hi: !byte 0 -cert_list_len_lo: !byte 0 -cert_data_len_hi: !byte 0 -cert_data_len_lo: !byte 0 -cert_data_ptr: !word 0 ; pointer to DER cert data in tls_hs_buf -cert_data_offset: !byte 0 ; Y offset where cert_data starts -cert_parse_pos: !word 0 ; 16-bit parse position -cert_ext_len_hi: !byte 0 ; extensions length high -cert_ext_len_lo: !byte 0 ; extensions length low -cert_end_lo: !byte 0 ; end address of cert data (low) -cert_end_hi: !byte 0 ; end address of cert data (high) -cert_bs_len: !byte 0 ; BIT STRING content length + .segment "BSS" + +cert_list_len_hi: .res 1 +cert_list_len_lo: .res 1 +cert_data_len_hi: .res 1 +cert_data_len_lo: .res 1 +cert_data_ptr: .res 2 ; pointer to DER cert data in tls_hs_buf +cert_data_offset: .res 1 ; Y offset where cert_data starts +cert_parse_pos: .res 2 ; 16-bit parse position +cert_ext_len_hi: .res 1 ; extensions length high +cert_ext_len_lo: .res 1 ; extensions length low +cert_end_lo: .res 1 ; end address of cert data (low) +cert_end_hi: .res 1 ; end address of cert data (high) +cert_bs_len: .res 1 ; BIT STRING content length ; CertificateVerify parsing state -cv_sig_len: !byte 0 ; DER signature length - -; ============================================================================= -; Certificate buffer for large certs (if needed beyond tls_hs_buf) -; For MVP, we parse directly from tls_hs_buf. If certs exceed the -; 256-byte handshake buffer, this would need to be a larger staging area -; fed by multiple TLS records (future work). -; ============================================================================= +cv_sig_len: .res 1 ; DER signature length diff --git a/src/tls_handshake.asm b/src/tls_handshake.s similarity index 83% rename from src/tls_handshake.asm rename to src/tls_handshake.s index 4a99f74..88fb0f8 100644 --- a/src/tls_handshake.asm +++ b/src/tls_handshake.s @@ -1,11 +1,28 @@ -; ============================================================================= -; tls_handshake.asm - TLS 1.3 handshake message construction and parsing +; tls_handshake.s — TLS 1.3 handshake messages +; Converted from ACME to ca65 in Phase 3 Batch C. ; ; Builds ClientHello, parses ServerHello and EncryptedExtensions. -; ============================================================================= -; x25519 named group (not in constants.asm) -TLS_GROUP_X25519 = $001d +.include "constants.inc" + +; --- Externals (data.asm BSS / scratch) --- +.import tls_hs_buf +.import tls_hs_len +.import tls_client_random +.import tls_ecdhe_pubkey +.import tls_server_random +.import tls_server_pubkey + +; --- Exports --- +.export tls_build_client_hello +.export tls_parse_server_hello +.export tls_parse_encrypted_extensions +.export tls_hostname +.export tls_hostname_len + +; x25519 named group is defined in constants.inc; no local equate needed. + +.segment "CODE" ; ============================================================================= ; tls_build_client_hello - construct ClientHello message @@ -43,13 +60,13 @@ tls_build_client_hello: ; --- [6-37] client_random (32 bytes) --- ldx #0 -.copy_random: +@copy_random: lda tls_client_random,x sta tls_hs_buf,y iny inx cpx #32 - bne .copy_random + bne @copy_random ; Y=38 ; --- [38] session_id_length = 0x00 --- @@ -207,19 +224,19 @@ tls_build_client_hello: ; Copy 32 bytes of x25519 public key ldx #0 -.copy_pubkey: +@copy_pubkey: lda tls_ecdhe_pubkey,x sta tls_hs_buf,y iny inx cpx #32 - bne .copy_pubkey + bne @copy_pubkey ; 10 + 32 = 42 bytes written ; --- Extension 5: server_name / SNI (0x0000) --- ; Only include if tls_hostname_len > 0 lda tls_hostname_len - beq .skip_sni + beq @skip_sni ; Type 00 00 lda #$00 @@ -263,14 +280,14 @@ tls_build_client_hello: ; Copy hostname bytes ldx #0 -.copy_hostname: +@copy_hostname: lda tls_hostname,x sta tls_hs_buf,y iny inx cpx tls_hostname_len - bne .copy_hostname -.skip_sni: + bne @copy_hostname +@skip_sni: ; --- Extension 6: max_fragment_length (0x0001) --- ; 00 01 00 01 01 @@ -335,9 +352,9 @@ tls_parse_server_hello: ; --- [0] Verify handshake type = 0x02 --- lda tls_hs_buf cmp #TLS_HS_SERVER_HELLO - beq .sh_type_ok - jmp .sh_error -.sh_type_ok: + beq @sh_type_ok + jmp @sh_error +@sh_type_ok: iny ; Y=1 ; --- [1-3] Length (24-bit) — skip past --- @@ -351,40 +368,40 @@ tls_parse_server_hello: ; --- [6-37] server_random — copy 32 bytes --- ldx #0 -.copy_server_random: +@copy_server_random: lda tls_hs_buf,y sta tls_server_random,x iny inx cpx #32 - bne .copy_server_random + bne @copy_server_random ; Y=38 ; --- [38] session_id_echo_length — skip that many bytes --- lda tls_hs_buf,y iny ; past length byte tax - beq .sh_no_session_id -.sh_skip_session_id: + beq @sh_no_session_id +@sh_skip_session_id: iny dex - bne .sh_skip_session_id -.sh_no_session_id: + bne @sh_skip_session_id +@sh_no_session_id: ; --- cipher_suite (2 bytes) — verify = 0x1303 --- lda tls_hs_buf,y cmp #$13 - bne .sh_error_jmp + bne @sh_error_jmp iny lda tls_hs_buf,y cmp #$03 - bne .sh_error_jmp + bne @sh_error_jmp iny ; --- compression_method (1 byte) — verify = 0x00 --- lda tls_hs_buf,y cmp #$00 - bne .sh_error_jmp + bne @sh_error_jmp iny ; --- extensions_length (2 bytes, big-endian) --- @@ -397,25 +414,25 @@ tls_parse_server_hello: ; Reset flags for required extensions lda #0 - sta .sh_found_ver ; supported_versions found? - sta .sh_found_ks ; key_share found? - jmp .sh_ext_loop + sta sh_found_ver ; supported_versions found? + sta sh_found_ks ; key_share found? + jmp @sh_ext_loop -.sh_error_jmp: - jmp .sh_error +@sh_error_jmp: + jmp @sh_error ; ================================================================= ; Extension parsing loop ; zp_tmp1 = remaining extension bytes (low) ; zp_tmp2 = remaining extension bytes (high) ; ================================================================= -.sh_ext_loop: +@sh_ext_loop: ; Check if we've consumed all extension bytes lda zp_tmp1 ora zp_tmp2 - bne .sh_ext_continue - jmp .sh_done -.sh_ext_continue: + bne @sh_ext_continue + jmp @sh_done +@sh_ext_continue: ; Read extension type (2 bytes, big-endian) lda tls_hs_buf,y ; type high byte @@ -438,115 +455,111 @@ tls_parse_server_hello: sec sbc #4 sta zp_tmp1 - bcs + + bcs :+ dec zp_tmp2 -+ +: ; Subtract ext data length from remaining lda zp_tmp1 sec sbc zp_temp sta zp_tmp1 - bcs + + bcs :+ dec zp_tmp2 -+ +: lda zp_tmp1 sec sbc zp_count sta zp_tmp1 - bcs + + bcs :+ dec zp_tmp2 -+ +: ; --- Check: supported_versions (type 0x002B)? --- lda zp_ptr ; type_hi - bne .sh_not_sup_ver ; high byte != 0 + bne @sh_not_sup_ver ; high byte != 0 lda zp_ptr+1 ; type_lo cmp #$2b - bne .sh_not_sup_ver + bne @sh_not_sup_ver ; supported_versions: expect 2 bytes = 03 04 lda tls_hs_buf,y cmp #$03 - bne .sh_error + bne @sh_error iny lda tls_hs_buf,y cmp #$04 - bne .sh_error + bne @sh_error iny - inc .sh_found_ver ; mark supported_versions found - jmp .sh_ext_loop + inc sh_found_ver ; mark supported_versions found + jmp @sh_ext_loop -.sh_not_sup_ver: +@sh_not_sup_ver: ; --- Check: key_share (type 0x0033)? --- lda zp_ptr ; type_hi - bne .sh_skip_ext ; high byte != 0 + bne @sh_skip_ext ; high byte != 0 lda zp_ptr+1 ; type_lo cmp #$33 - bne .sh_skip_ext + bne @sh_skip_ext ; key_share: group(2) + key_len(2) + key_data ; Verify group = 0x001D (x25519) lda tls_hs_buf,y - bne .sh_error ; high byte must be 0 + bne @sh_error ; high byte must be 0 iny lda tls_hs_buf,y cmp #$1d - bne .sh_error + bne @sh_error iny ; Verify key_len = 0x0020 lda tls_hs_buf,y - bne .sh_error ; high byte must be 0 + bne @sh_error ; high byte must be 0 iny lda tls_hs_buf,y cmp #$20 - bne .sh_error + bne @sh_error iny ; Copy 32 bytes to tls_server_pubkey ldx #0 -.copy_server_key: +@copy_server_key: lda tls_hs_buf,y sta tls_server_pubkey,x iny inx cpx #32 - bne .copy_server_key - inc .sh_found_ks ; mark key_share found - jmp .sh_ext_loop + bne @copy_server_key + inc sh_found_ks ; mark key_share found + jmp @sh_ext_loop ; --- Unknown extension: skip ext data --- -.sh_skip_ext: +@sh_skip_ext: ; zp_count = ext_len_hi, zp_temp = ext_len_lo ; For ServerHello extensions, length should be small (<256) lda zp_count - bne .sh_error ; can't handle >255 byte ext here + bne @sh_error ; can't handle >255 byte ext here ldx zp_temp - bne .sh_skip_bytes ; has data to skip - jmp .sh_ext_loop ; zero-length: nothing to skip -.sh_skip_bytes: + bne @sh_skip_bytes ; has data to skip + jmp @sh_ext_loop ; zero-length: nothing to skip +@sh_skip_bytes: iny dex - bne .sh_skip_bytes - jmp .sh_ext_loop + bne @sh_skip_bytes + jmp @sh_ext_loop -.sh_done: +@sh_done: ; Verify required extensions were found - lda .sh_found_ver - beq .sh_error ; supported_versions is mandatory - lda .sh_found_ks - beq .sh_error ; key_share is mandatory + lda sh_found_ver + beq @sh_error ; supported_versions is mandatory + lda sh_found_ks + beq @sh_error ; key_share is mandatory clc rts -.sh_error: +@sh_error: sec rts -; Extension tracking flags (inline data) -.sh_found_ver: !byte 0 -.sh_found_ks: !byte 0 - ; ============================================================================= ; tls_parse_encrypted_extensions - parse EncryptedExtensions @@ -558,16 +571,28 @@ tls_parse_encrypted_extensions: ; Verify handshake type byte lda tls_hs_buf cmp #TLS_HS_ENCRYPTED_EXT - bne .ee_error + bne @ee_error clc rts -.ee_error: +@ee_error: sec rts +; ============================================================================= +; Extension tracking flags (module-local BSS; moved out of CODE so they don't +; break relative-branch reachability, and so CODE stays pure instructions). +; ============================================================================= +.segment "BSS" + +sh_found_ver: .res 1 +sh_found_ks: .res 1 + + ; ============================================================================= ; Inline data: hostname for SNI extension ; ============================================================================= -tls_hostname: !fill 64, 0 -tls_hostname_len: !byte 0 +.segment "BSS" + +tls_hostname: .res 64 +tls_hostname_len: .res 1 From e6f8c233ca9538a538895c3161b57cd9b12e65e9 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:33:19 -0500 Subject: [PATCH 14/22] Phase 3 Batch D: convert glue files and relocate net backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Converts the last 4 ACME files to ca65 and introduces the ip65 backend infrastructure under src/net/ip65/: - src/data.s — program-wide BSS + initialized tables - src/boot.s — startup, BASIC stub, screen output, REU multiply support, TLS state markers - src/main.s — orchestrator shell (was 89 lines of !source/!binary directives; now ~zero code since each .s is its own TU) - src/net/ip65/net.s — ip65/RR-Net networking backend (relocated from src/net.asm) - src/net/ip65/ip65_blob.s — .incbin wrapper placing the pre-built ip65-c64.bin at \$2000 via NET_CODE segment - src/net/ip65/ip65_symbols.inc — ip65 jump-table + variable-table equates sourced from ip65-build/ip65-c64.map All 7 files assemble clean via ca65 -I src -I src/net/ip65. Load-bearing fixes preserved from PR #13: - net.s: cb_remaining clamp-to-255 in net_tcp_recv_cb (1c75ed9) - net.s: ZP \$02-\$1B save/restore around every ip65 call site - data.s: 4KB tcp_recv_buf, 16-bit aead_data_len (ac57d1f) - boot.s: all 15 TLS state-transition screen markers (eab7570) ip65_symbols.inc is guarded with .ifndef ip65_base so it co-exists with the legacy ip65 equates still in constants.inc. Phase 7 will consolidate them. Phase 3 structurally complete. Cross-file imports (data.s BSS producer, crypto .o files, TLS .o files) all resolve via ca65 object discipline; final ld65 link is Phase 4. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/{boot.asm => boot.s} | 405 ++++++++++++++++-------- src/data.asm | 325 -------------------- src/data.s | 528 ++++++++++++++++++++++++++++++++ src/main.asm | 89 ------ src/main.s | 18 ++ src/net/ip65/ip65_blob.s | 22 ++ src/net/ip65/ip65_symbols.inc | 59 ++++ src/{net.asm => net/ip65/net.s} | 78 +++-- 8 files changed, 953 insertions(+), 571 deletions(-) rename src/{boot.asm => boot.s} (68%) delete mode 100644 src/data.asm create mode 100644 src/data.s delete mode 100644 src/main.asm create mode 100644 src/main.s create mode 100644 src/net/ip65/ip65_blob.s create mode 100644 src/net/ip65/ip65_symbols.inc rename src/{net.asm => net/ip65/net.s} (88%) diff --git a/src/boot.asm b/src/boot.s similarity index 68% rename from src/boot.asm rename to src/boot.s index 1016039..c110304 100644 --- a/src/boot.asm +++ b/src/boot.s @@ -1,20 +1,145 @@ +; boot.s — Startup, BASIC stub, screen output, phase 3 orchestration +; Converted from ACME to ca65 in Phase 3 Batch D. + + .include "constants.inc" + + ; ---- exports: entry + print helpers ---- + .export start + .export main_loop + .export print_string + .export print_null_terminated + .export print_resp_body + + ; ---- exports: REU multiply table routines ---- + .export reu_mul_init + .export reu_fetch_mul_row + + ; ---- exports: menu handlers ---- + .export do_net_init + .export do_http_get + .export do_https_get + + ; ---- exports: banner / menu / status strings ---- + .export banner_msg + .export menu_msg + .export init_msg + .export net_fail_msg + .export net_ok_msg + .export dhcp_msg + .export dhcp_fail_msg + .export dhcp_ok_msg + .export no_net_msg + .export http_get_msg + .export https_get_msg + .export dns_fail_msg + .export dns_ok_msg + .export tcp_fail_msg + .export tcp_ok_msg + .export tls_fail_msg + .export tls_ok_msg + .export send_fail_msg + .export send_ok_msg + .export ok_msg + .export failed_msg + .export done_msg + + ; ---- exports: 15 TLS state transition markers (used by tls13.s) ---- + .export ch_sent_msg + .export sh_recv_msg + .export hk1_msg + .export keys_ok_msg + .export ee_recv_msg + .export cert_recv_msg + .export cv_recv_msg + .export fin_recv_msg + .export cfin_sent_msg + .export enc1_msg + .export rx_msg + .export got_msg + .export got2_msg + .export dec_msg + .export proc_msg + + ; ---- exports: hostnames / path data ---- + .export http_host_zimmers + .export http_host_zimmers_len + .export http_host_foo + .export http_host_foo_len + .export http_path_root + + ; ---- exports: local BSS ---- + .export net_initialized + + ; ---- imports: entropy / DRBG / sqtab ---- + .import entropy_init + .import drbg_init_entropy + .import sqtab_init + + ; ---- imports: network (ip65 wrapper) ---- + .import net_init + .import net_dhcp + .import net_poll + .import net_print_ip + .import net_dns_resolve + .import net_set_tcp_dest + .import net_tcp_connect + .import net_tcp_close + + ; ---- imports: TLS state machine ---- + .import tls_connect + .import tls_send + .import tls_recv + .import tls_close + + ; ---- imports: HTTP ---- + .import http_get_plain + .import http_build_get + + ; ---- imports: HTTP I/O state (data.asm) ---- + .import http_host_ptr + .import http_host_len + .import http_path_ptr + .import http_path_len + .import http_port + .import http_req_buf + .import http_req_len + .import http_resp_buf + .import http_resp_len + + ; ---- imports: TLS app-data pointers (data.asm) ---- + .import tls_app_ptr + .import tls_app_len + .import tls_hostname + .import tls_hostname_len + + ; ---- imports: multiply / REU staging (data.asm) ---- + .import mul_8x8 + .import mul_dma_lo + .import mul_dma_hi + .import mul_cached_a + .import poly_prod_lo + .import poly_prod_hi + ; ============================================================================= -; boot.asm - BASIC stub and startup +; BASIC stub: 10 SYS 2064 +; Loaded at $0801 via EXEHDR segment (first bytes of LOADER region). ; ============================================================================= + .segment "EXEHDR" + .word bas_end ; pointer to next BASIC line + .word 10 ; line number + .byte $9e ; SYS token + .byte "2064" ; decimal address of `start` + .byte 0 ; end of BASIC line +bas_end: + .word 0 ; end of BASIC program -* = $0801 - -; BASIC stub: 10 SYS 2064 - !word @end ; pointer to next BASIC line - !word 10 ; line number - !byte $9e ; SYS token - !text "2064" ; decimal address of @start - !byte 0 ; end of BASIC line -@end: - !word 0 ; end of BASIC program +; ============================================================================= +; Code +; ============================================================================= + .segment "CODE" ; --- entry point (address $0810) --- -@start: +start: ; disable BASIC ROM to free $A000-$BFFF lda $01 and #%11111110 ; clear bit 0 (BASIC ROM off) @@ -147,12 +272,14 @@ do_net_init: sta net_initialized rts -net_initialized: !byte 0 - ; ============================================================================= ; print_string - print null-terminated string at A(lo)/Y(hi) +; +; Also aliased as `print_null_terminated` for the screen_marker macro in +; macros.inc. ; ============================================================================= print_string: +print_null_terminated: sta zp_ptr sty zp_ptr+1 ldy #0 @@ -410,118 +537,6 @@ print_resp_body: jsr chrout rts -; ============================================================================= -; strings -; ============================================================================= -banner_msg: - !text "C64-HTTPS CLIENT V0.1" - !byte $0d, $0d - !text "TLS 1.3 / CHACHA20-POLY1305" - !byte $0d - !text "RR-NET (CS8900A) ETHERNET" - !byte $0d, $0d, 0 - -menu_msg: - !text "I=INIT H=HTTP G=HTTPS Q=QUIT" - !byte $0d, $0d, 0 - -init_msg: - !text "INITIALIZING NETWORK..." - !byte $0d, 0 - -net_fail_msg: - !text "NETWORK INIT FAILED" - !byte $0d, 0 - -net_ok_msg: - !text "NETWORK OK" - !byte $0d, 0 - -dhcp_msg: - !text "REQUESTING DHCP..." - !byte $0d, 0 - -dhcp_fail_msg: - !text "DHCP FAILED" - !byte $0d, 0 - -dhcp_ok_msg: - !text "DHCP OK - IP: " - !byte 0 - -no_net_msg: - !text "ERROR: NETWORK NOT INITIALIZED" - !byte $0d, 0 - -http_get_msg: - !text "HTTP GET WWW.ZIMMERS.NET..." - !byte $0d, 0 - -https_get_msg: - !text "HTTPS GET WWW.FOO.BAR..." - !byte $0d, 0 - -dns_fail_msg: - !text "DNS RESOLVE FAILED" - !byte $0d, 0 - -dns_ok_msg: - !text "DNS OK" - !byte $0d, 0 - -tcp_fail_msg: - !text "TCP CONNECT FAILED" - !byte $0d, 0 - -tcp_ok_msg: - !text "TCP CONNECTED" - !byte $0d, 0 - -tls_fail_msg: - !text "TLS HANDSHAKE FAILED" - !byte $0d, 0 - -tls_ok_msg: - !text "TLS HANDSHAKE OK" - !byte $0d, 0 - -; TLS state transition markers (debug) -ch_sent_msg: !text "CH", $0d, 0 -sh_recv_msg: !text "SH", $0d, 0 -hk1_msg: !text "HK1", $0d, 0 -keys_ok_msg: !text "KEYS", $0d, 0 -ee_recv_msg: !text "EE", $0d, 0 -cert_recv_msg: !text "CERT", $0d, 0 -cv_recv_msg: !text "CV", $0d, 0 -fin_recv_msg: !text "FIN", $0d, 0 -cfin_sent_msg: !text "CFIN", $0d, 0 -enc1_msg: !text "ENC1", $0d, 0 -rx_msg: !text "RX", $0d, 0 -got_msg: !text "GOT", $0d, 0 -got2_msg: !text "GOT2", $0d, 0 -dec_msg: !text "DEC", $0d, 0 -proc_msg: !text "PROC", $0d, 0 - -send_fail_msg: - !text "TLS SEND FAILED" - !byte $0d, 0 - -send_ok_msg: - !text "REQUEST SENT" - !byte $0d, 0 - -ok_msg: - !text "OK" - !byte $0d, 0 - -failed_msg: - !text "FAILED" - !byte $0d, 0 - -done_msg: - !text "CONNECTION CLOSED" - !byte $0d, 0 - ; ============================================================================= ; REU multiply table initialization (from c64-x25519 optimizations) ; ============================================================================= @@ -624,9 +639,6 @@ reu_mul_init: sta reu_len_hi ; length high = 2 (512 bytes) rts -reu_init_a: !byte 0 -reu_init_b: !byte 0 - ; ============================================================================= ; reu_fetch_mul_row - DMA a multiplication table row from REU to C64 ; @@ -645,19 +657,142 @@ reu_fetch_mul_row: sta reu_command rts +; ============================================================================= +; Strings (read-only) +; ============================================================================= + .segment "RODATA" + +banner_msg: + .byte "C64-HTTPS CLIENT V0.1" + .byte $0d, $0d + .byte "TLS 1.3 / CHACHA20-POLY1305" + .byte $0d + .byte "RR-NET (CS8900A) ETHERNET" + .byte $0d, $0d, 0 + +menu_msg: + .byte "I=INIT H=HTTP G=HTTPS Q=QUIT" + .byte $0d, $0d, 0 + +init_msg: + .byte "INITIALIZING NETWORK..." + .byte $0d, 0 + +net_fail_msg: + .byte "NETWORK INIT FAILED" + .byte $0d, 0 + +net_ok_msg: + .byte "NETWORK OK" + .byte $0d, 0 + +dhcp_msg: + .byte "REQUESTING DHCP..." + .byte $0d, 0 + +dhcp_fail_msg: + .byte "DHCP FAILED" + .byte $0d, 0 + +dhcp_ok_msg: + .byte "DHCP OK - IP: " + .byte 0 + +no_net_msg: + .byte "ERROR: NETWORK NOT INITIALIZED" + .byte $0d, 0 + +http_get_msg: + .byte "HTTP GET WWW.ZIMMERS.NET..." + .byte $0d, 0 + +https_get_msg: + .byte "HTTPS GET WWW.FOO.BAR..." + .byte $0d, 0 + +dns_fail_msg: + .byte "DNS RESOLVE FAILED" + .byte $0d, 0 + +dns_ok_msg: + .byte "DNS OK" + .byte $0d, 0 + +tcp_fail_msg: + .byte "TCP CONNECT FAILED" + .byte $0d, 0 + +tcp_ok_msg: + .byte "TCP CONNECTED" + .byte $0d, 0 + +tls_fail_msg: + .byte "TLS HANDSHAKE FAILED" + .byte $0d, 0 + +tls_ok_msg: + .byte "TLS HANDSHAKE OK" + .byte $0d, 0 + +; TLS state transition markers (debug) — imported by tls13.s +ch_sent_msg: .byte "CH", $0d, 0 +sh_recv_msg: .byte "SH", $0d, 0 +hk1_msg: .byte "HK1", $0d, 0 +keys_ok_msg: .byte "KEYS", $0d, 0 +ee_recv_msg: .byte "EE", $0d, 0 +cert_recv_msg: .byte "CERT", $0d, 0 +cv_recv_msg: .byte "CV", $0d, 0 +fin_recv_msg: .byte "FIN", $0d, 0 +cfin_sent_msg: .byte "CFIN", $0d, 0 +enc1_msg: .byte "ENC1", $0d, 0 +rx_msg: .byte "RX", $0d, 0 +got_msg: .byte "GOT", $0d, 0 +got2_msg: .byte "GOT2", $0d, 0 +dec_msg: .byte "DEC", $0d, 0 +proc_msg: .byte "PROC", $0d, 0 + +send_fail_msg: + .byte "TLS SEND FAILED" + .byte $0d, 0 + +send_ok_msg: + .byte "REQUEST SENT" + .byte $0d, 0 + +ok_msg: + .byte "OK" + .byte $0d, 0 + +failed_msg: + .byte "FAILED" + .byte $0d, 0 + +done_msg: + .byte "CONNECTION CLOSED" + .byte $0d, 0 + ; ============================================================================= ; hostname and path data ; ============================================================================= http_host_zimmers: - !text "www.zimmers.net" - !byte 0 + .byte "www.zimmers.net" + .byte 0 http_host_zimmers_len = 15 http_host_foo: - !text "www.foo.bar" - !byte 0 + .byte "www.foo.bar" + .byte 0 http_host_foo_len = 11 http_path_root: - !text "/" - !byte 0 + .byte "/" + .byte 0 + +; ============================================================================= +; Local BSS +; ============================================================================= + .segment "BSS" + +net_initialized: .res 1 +reu_init_a: .res 1 +reu_init_b: .res 1 diff --git a/src/data.asm b/src/data.asm deleted file mode 100644 index 524719d..0000000 --- a/src/data.asm +++ /dev/null @@ -1,325 +0,0 @@ -; ============================================================================= -; data.asm - Mutable data buffers -; ============================================================================= - -; ============================================================================= -; Zero page save buffer (for ip65 time-sharing) -; ============================================================================= -zp_save_buf: !fill 26, 0 ; saves $02-$1B during ip65 calls - -; ============================================================================= -; fe25519/x25519 optimization tables — MUST live below $A000 to avoid -; BASIC ROM shadow. REU DMA and CPU reads need direct RAM access. -; Placed here (early in data section) to guarantee addresses < $A000. -; ============================================================================= - -; --- REU DMA target buffers (page-aligned for LDA abs,Y without penalty) --- - !align 255, 0 ; align to next page boundary -mul_dma_lo: - !fill 256, 0 ; DMA target: lo bytes of a*b for current a -mul_dma_hi: - !fill 256, 0 ; DMA target: hi bytes of a*b for current a - -; --- mult66 second quarter-square table --- -sqtab2_lo: - !byte 0 - !for i, 1, 255 { - !byte <(((256-i)*(256-i))/4 - 1) - } - -sqtab2_hi: - !byte 0 - !for i, 1, 255 { - !byte >(((256-i)*(256-i))/4 - 1) - } - -; --- mul_by_38 lookup tables --- -mul38_lo_tab: - !byte 0 - !for i, 1, 255 { - !byte <(i * 38) - } - -mul38_hi_tab: - !byte 0 - !for i, 1, 255 { - !byte >(i * 38) - } - -; --- Quarter-square tables (runtime-generated by sqtab_init in poly1305.asm). -; 512 bytes each (indexed 0..511). Moved from $7800/$7A00 to free up that code region. - !align 255, 0 -sqtab_lo: !fill 512, 0 -sqtab_hi: !fill 512, 0 - -; ============================================================================= -; Network layer buffers -; ============================================================================= -; NOTE: tcp_recv_buf itself is an equate in constants.asm pointing at $C000 -; (4KB always-RAM region between BASIC ROM shadow and I/O). The buffer does -; not occupy any .prg bytes here. -tcp_recv_head: !word 0 ; read position (16-bit, masked with TCP_RECV_MASK) -tcp_recv_tail: !word 0 ; write position (updated by ip65 callback, 16-bit) -tcp_recv_overflow: !byte 0 ; set to 1 by callback if ring fills up - -; Diagnostic counters — incremented by net_poll at entry and return -net_poll_entry_count: !word 0 -net_poll_return_count: !word 0 - -; ============================================================================= -; TLS state -; ============================================================================= -tls_state: !byte 0 ; current TLS state machine state -tls_last_state: !byte 0 ; preserves tls_state before error handler overwrites it -tls_recv_progress: !byte 0 ; granular progress within tls_recv_server_hello -tls_recv_sub_progress: !byte 0 ; granular progress within tls_record_recv_and_decrypt -tls_recv_poll_count: !word 0 ; number of sh_wait poll loop iterations (16-bit) -tls_client_random: !fill 32, 0 ; client random (32 bytes) -tls_server_random: !fill 32, 0 ; server random (32 bytes) - -; ECDHE key exchange -tls_ecdhe_privkey: !fill 32, 0 ; our ephemeral private key -tls_ecdhe_pubkey: !fill 32, 0 ; our ephemeral public key (x25519, 32 bytes) -tls_server_pubkey: !fill 32, 0 ; server's ephemeral public key (x25519, 32 bytes) -tls_shared_secret: !fill 32, 0 ; ECDHE shared secret (x-coordinate) - -; Transcript hash (running SHA-256 state) -tls_transcript: !fill 32, 0 ; current transcript hash output -tls_transcript_h0: !fill 4, 0 ; saved SHA-256 state for cloning -tls_transcript_h1: !fill 4, 0 -tls_transcript_h2: !fill 4, 0 -tls_transcript_h3: !fill 4, 0 -tls_transcript_h4: !fill 4, 0 -tls_transcript_h5: !fill 4, 0 -tls_transcript_h6: !fill 4, 0 -tls_transcript_h7: !fill 4, 0 - -; Handshake keys (derived from ECDHE via HKDF) -tls_hs_write_key: !fill 32, 0 ; client handshake write key -tls_hs_write_iv: !fill 12, 0 ; client handshake write IV -tls_hs_read_key: !fill 32, 0 ; server handshake read key -tls_hs_read_iv: !fill 12, 0 ; server handshake read IV - -; Application traffic keys (derived after Finished) -tls_app_write_key: !fill 32, 0 ; client application write key -tls_app_write_iv: !fill 12, 0 ; client application write IV -tls_app_read_key: !fill 32, 0 ; server application read key -tls_app_read_iv: !fill 12, 0 ; server application read IV - -; Sequence numbers (64-bit, big-endian) -tls_write_seq: !fill 8, 0 ; write sequence number -tls_read_seq: !fill 8, 0 ; read sequence number - -; Record layer buffers -tls_rec_header: !fill 5, 0 ; 5-byte record header -tls_rec_type: !byte 0 ; content type of current record -tls_rec_len: !word 0 ; length of current record payload -tls_rec_buf: !fill 548, 0 ; record payload (512 + 1 inner type + 16 tag + padding) - -; AEAD nonce construction -tls_nonce: !fill 12, 0 ; constructed nonce for AEAD - -; Handshake message buffer -tls_hs_buf: !fill 256, 0 ; handshake message assembly/parsing -tls_hs_len: !word 0 ; handshake message length - -; ============================================================================= -; HKDF buffers -; ============================================================================= -hkdf_prk: !fill 32, 0 ; pseudorandom key -hkdf_okm: !fill 32, 0 ; output keying material -hkdf_info_buf: !fill 80, 0 ; HkdfLabel construction buffer -hkdf_info_len: !byte 0 -hkdf_salt_ptr: !word 0 -hkdf_salt_len: !byte 0 -hkdf_ikm_ptr: !word 0 -hkdf_ikm_len: !byte 0 -hkdf_label_ptr: !word 0 -hkdf_label_len: !byte 0 -hkdf_context_ptr: !word 0 -hkdf_context_len: !byte 0 -hkdf_out_len: !byte 0 - -; TLS key schedule intermediate values -tls_early_secret: !fill 32, 0 ; HKDF-Extract(0, 0) for PSK=0 -tls_handshake_secret: !fill 32, 0 ; HKDF-Extract(derived, shared_secret) -tls_master_secret: !fill 32, 0 ; HKDF-Extract(derived, 0) - -; ============================================================================= -; HTTP buffers -; ============================================================================= -http_host_ptr: !word 0 -http_host_len: !byte 0 -http_path_ptr: !word 0 -http_path_len: !byte 0 -http_port: !word 443 ; default HTTPS port -http_status: !word 0 ; HTTP status code (e.g., 200) -http_req_buf: !fill 256, 0 ; HTTP request buffer -http_req_len: !word 0 -http_resp_buf: !fill 512, 0 ; HTTP response body buffer -http_resp_len: !word 0 - -; HTTP parser state -http_parse_state: !byte 0 ; 0=status line, 1=headers, 2=body -http_hdr_match: !byte 0 ; consecutive \r\n\r\n match count -http_line_idx: !byte 0 ; index into status line buffer -http_line_buf: !fill 32, 0 ; status line accumulator - -; ============================================================================= -; Application data pointers (for tls_send) -; ============================================================================= -tls_app_ptr: !word 0 -tls_app_len: !word 0 - -; ============================================================================= -; General I/O buffers (used by SHA-256 update) -; ============================================================================= -input_buffer: !fill 256, 0 ; general input buffer -input_length: !byte 0 ; length of data in input_buffer - -; ============================================================================= -; SHA-256 working variables (from c64-aes256-ecdsa) -; ============================================================================= -sha256_h0: !fill 4, 0 -sha256_h1: !fill 4, 0 -sha256_h2: !fill 4, 0 -sha256_h3: !fill 4, 0 -sha256_h4: !fill 4, 0 -sha256_h5: !fill 4, 0 -sha256_h6: !fill 4, 0 -sha256_h7: !fill 4, 0 - -sha_a: !fill 4, 0 -sha_b: !fill 4, 0 -sha_c: !fill 4, 0 -sha_d: !fill 4, 0 -sha_e: !fill 4, 0 -sha_f: !fill 4, 0 -sha_g: !fill 4, 0 -sha_h: !fill 4, 0 - -sha_temp3: !fill 4, 0 -sha_t1: !fill 4, 0 -sha_t2: !fill 4, 0 - -sha256_block: !fill 64, 0 -sha256_w: !fill 256, 0 ; message schedule (64 words * 4 bytes) -sha256_hash: !fill 32, 0 ; final hash output -sha256_len: !fill 2, 0 ; message length in bits - -; ============================================================================= -; HMAC-DRBG state (from c64-aes256-ecdsa) -; ============================================================================= -hmac_key: !fill 32, 0 ; HMAC key / DRBG K state -hmac_val: !fill 32, 0 ; DRBG V state -hmac_opad_block: !fill 64, 0 ; Scratch: K XOR opad -hmac_data_buf: !fill 97, 0 ; V(32) + 0x00/0x01(1) + seed(64) -hmac_data_len: !byte 0 ; Length of data in hmac_data_buf -hmac_result: !fill 32, 0 ; HMAC output -drbg_seed: !fill 64, 0 ; Seed material (privkey||hash) -drbg_seed_len: !byte 0 ; Length of seed -drbg_output: !fill 32, 0 ; Generate output -drbg_buf_idx: !byte 32 ; Buffer index (32 = empty, forces first generate) - -; ============================================================================= -; ChaCha20 state (from c64-wireguard) -; ============================================================================= -cc20_state: !fill 64, 0 ; initial state (16 x 32-bit words) -cc20_work: !fill 64, 0 ; working state during block computation -cc20_keystream: !fill 64, 0 ; generated keystream for XOR -cc20_key: !fill 32, 0 ; 256-bit key -cc20_nonce: !fill 12, 0 ; 96-bit nonce -cc20_counter: !fill 4, 0 ; 32-bit block counter - -; ============================================================================= -; Poly1305 state (from c64-wireguard) -; ============================================================================= -poly_h: !fill 17, 0 ; 130-bit accumulator -poly_r: !fill 16, 0 ; clamped key part r -poly_s: !fill 16, 0 ; key part s (added at end) -poly_product: !fill 33, 0 ; multiplication scratch (17x16) -poly1305_tag: !fill 16, 0 ; output tag - -; ============================================================================= -; AEAD state (from c64-wireguard) -; ============================================================================= -aead_key: !fill 32, 0 -aead_nonce: !fill 12, 0 -aead_aad_ptr: !word 0 -aead_aad_len: !byte 0 -aead_data_ptr: !word 0 -aead_data_len: !word 0 ; data length (16-bit; TLS records can be up to ~4KB) -aead_tag: !fill 16, 0 -aead_scratch: !fill 16, 0 ; Poly1305 padding/length block -cc20_remain_hi: !byte 0 ; high byte of 16-bit ChaCha20/Poly1305 length counter - ; (low byte lives in ZP at cc20_remain = $18) - -; ============================================================================= -; fe25519 field arithmetic (from c64-wireguard) -; ============================================================================= -fe_wide: !fill 64, 0 ; 512-bit product from multiply -fe_tmp1: !fill 32, 0 ; temporary field element 1 -fe_tmp2: !fill 32, 0 ; temporary field element 2 -fe_tmp3: !fill 32, 0 ; temporary field element 3 -fe_tmp4: !fill 32, 0 ; temporary field element 4 - -; p = 2^255 - 19 in little-endian -fe_p: - !byte $ed - !fill 30, $ff - !byte $7f - -; ============================================================================= -; X25519 state (from c64-wireguard) -; ============================================================================= -x25_scalar: !fill 32, 0 ; clamped scalar -x25_u: !fill 32, 0 ; input u-coordinate -x25_result: !fill 32, 0 ; output u-coordinate -x25_x2: !fill 32, 0 ; Montgomery ladder state -x25_z2: !fill 32, 0 -x25_x3: !fill 32, 0 -x25_z3: !fill 32, 0 -x25_a: !fill 32, 0 ; ladder temporaries -x25_b: !fill 32, 0 -x25_da: !fill 32, 0 -x25_cb: !fill 32, 0 -x25_e: !fill 32, 0 -x25_basepoint: - !byte 9 - !fill 31, 0 - -; --- fe_mul optimization buffers (from c64-x25519 optimizations) --- -mul_cached_a: - !byte 0 ; cached src1[i] for inlined multiply -mul_src2_buf: - !fill 32, 0 ; absolute copy of src2 for fast indexed access - -; ============================================================================= -; ECDSA signature verification (moved from ecdsa_verify.asm to avoid -; $7800-$7BFF sqtab memory collision) -; ============================================================================= - -; --- Verification parameters --- -ecdsa_curve_id: !byte 0 ; 0=P-256, 1=P-384 -ecdsa_hash: !fill 48, 0 ; message hash (32 for P-256, 48 for P-384) -ecdsa_hash_len: !byte 32 ; hash length -ecdsa_sig_r: !fill 48, 0 ; signature r component -ecdsa_sig_s: !fill 48, 0 ; signature s component -ecdsa_sig_len: !byte 32 ; component length (32 or 48) -ecdsa_pubkey_x: !fill 48, 0 ; public key Q.x -ecdsa_pubkey_y: !fill 48, 0 ; public key Q.y -ecdsa_verify_tmp: !fill 48, 0 ; temporary for w - -; --- P-256 working buffers --- -ev_u1: !fill 32, 0 ; u1 = z * w mod n -ev_u2: !fill 32, 0 ; u2 = r * w mod n -ev_point_save: !fill 96, 0 ; saved Jacobian point (u1*G) - -; --- P-384 working buffers --- -ev_u1_384: !fill 48, 0 ; u1 = z * w mod n (P-384) -ev_u2_384: !fill 48, 0 ; u2 = r * w mod n (P-384) -ev_point_save_384: !fill 144, 0 ; saved Jacobian point (u1*G, P-384) - -; --- DER parsing temporaries --- -ev_der_int_len: !byte 0 ; current INTEGER length -ev_der_copy_cnt: !byte 0 ; copy counter diff --git a/src/data.s b/src/data.s new file mode 100644 index 0000000..984851f --- /dev/null +++ b/src/data.s @@ -0,0 +1,528 @@ +; data.s — Program-wide BSS and initialized data +; Converted from ACME to ca65 in Phase 3 Batch D. + +.include "constants.inc" + +; ============================================================================= +; Initialized read-only tables (fe25519/x25519 optimization tables) +; ============================================================================= +; NOTE: original ACME layout placed these (and the sqtab_lo/hi BSS below) +; BEFORE $A000 to avoid the BASIC ROM shadow. Under ca65/ld65 they are split +; across RODATA (initialized) and BSS (zero-filled); RODATA loads into LOADER +; ($0801+) which is fine, but the zero-filled mul_dma/sqtab buffers below +; currently land in SHADOW_BSS ($A000+), which breaks the "< $A000" invariant. +; This must be addressed at Phase 4 link time (likely a new segment or moving +; these into CRYPTO_BSS with a crypto-local BSS memory area below $A000). + +.segment "RODATA" + +.export sqtab2_lo +.export sqtab2_hi +.export mul38_lo_tab +.export mul38_hi_tab + +; --- mult66 second quarter-square table --- +sqtab2_lo: + .byte 0 + .repeat 255, I + .byte <(((256-(I+1))*(256-(I+1)))/4 - 1) + .endrepeat + +sqtab2_hi: + .byte 0 + .repeat 255, I + .byte >(((256-(I+1))*(256-(I+1)))/4 - 1) + .endrepeat + +; --- mul_by_38 lookup tables --- +mul38_lo_tab: + .byte 0 + .repeat 255, I + .byte <((I+1) * 38) + .endrepeat + +mul38_hi_tab: + .byte 0 + .repeat 255, I + .byte >((I+1) * 38) + .endrepeat + +; --- fe25519 prime p = 2^255 - 19, little-endian --- +.export fe_p +fe_p: + .byte $ed + .res 30, $ff + .byte $7f + +; --- X25519 base point (u=9) --- +.export x25_basepoint +x25_basepoint: + .byte 9 + .res 31, 0 + +; ============================================================================= +; Initialized mutable data (needs DATA segment — small defaults) +; ============================================================================= +; Most of these are exported as labels with a nonzero default value. +; ca65's DATA segment isn't declared in the cfg, so we fold them into RODATA +; for now; the code writes to them via absolute stores, which works because +; RODATA resides in RAM ($0801+) on the C64 (the "ro" type in ld65 only +; affects file placement, not runtime writability). If this causes issues +; at Phase 4, move to a proper DATA segment. + +.export http_port +http_port: .word 443 ; default HTTPS port + +.export drbg_buf_idx +drbg_buf_idx: .byte 32 ; Buffer index (32 = empty, forces first generate) + +.export ecdsa_hash_len +ecdsa_hash_len: .byte 32 ; hash length default (P-256) + +.export ecdsa_sig_len +ecdsa_sig_len: .byte 32 ; component length default (32 or 48) + +; ============================================================================= +; BSS — zero-initialized mutable state +; ============================================================================= + +.segment "BSS" + +; ----------------------------------------------------------------------------- +; Zero page save buffer (for ip65 time-sharing) +; ----------------------------------------------------------------------------- +.export zp_save_buf +zp_save_buf: .res 26 ; saves $02-$1B during ip65 calls + +; ----------------------------------------------------------------------------- +; fe25519/x25519 optimization tables — MUST live below $A000 to avoid +; BASIC ROM shadow. (Original ACME layout guarantee; see NOTE at top.) +; ----------------------------------------------------------------------------- + +.align 256 +.export mul_dma_lo +.export mul_dma_hi +mul_dma_lo: .res 256 ; DMA target: lo bytes of a*b for current a +mul_dma_hi: .res 256 ; DMA target: hi bytes of a*b for current a + +; --- Quarter-square tables (runtime-generated by sqtab_init in poly1305.asm) --- +.align 256 +.export sqtab_lo +.export sqtab_hi +sqtab_lo: .res 512 +sqtab_hi: .res 512 + +; ----------------------------------------------------------------------------- +; Network layer buffers +; ----------------------------------------------------------------------------- +; NOTE: tcp_recv_buf itself is an equate in constants.inc pointing at $C000. +.export tcp_recv_head +.export tcp_recv_tail +.export tcp_recv_overflow +tcp_recv_head: .res 2 ; read position (16-bit, masked with TCP_RECV_MASK) +tcp_recv_tail: .res 2 ; write position (updated by ip65 callback) +tcp_recv_overflow: .res 1 ; set to 1 by callback if ring fills up + +; Diagnostic counters — incremented by net_poll at entry and return +.export net_poll_entry_count +.export net_poll_return_count +net_poll_entry_count: .res 2 +net_poll_return_count: .res 2 + +; ----------------------------------------------------------------------------- +; TLS state +; ----------------------------------------------------------------------------- +.export tls_state +.export tls_last_state +.export tls_recv_progress +.export tls_recv_sub_progress +.export tls_recv_poll_count +.export tls_client_random +.export tls_server_random +tls_state: .res 1 +tls_last_state: .res 1 +tls_recv_progress: .res 1 +tls_recv_sub_progress: .res 1 +tls_recv_poll_count: .res 2 +tls_client_random: .res 32 +tls_server_random: .res 32 + +; ECDHE key exchange +.export tls_ecdhe_privkey +.export tls_ecdhe_pubkey +.export tls_server_pubkey +.export tls_shared_secret +tls_ecdhe_privkey: .res 32 +tls_ecdhe_pubkey: .res 32 +tls_server_pubkey: .res 32 +tls_shared_secret: .res 32 + +; Transcript hash (running SHA-256 state) +.export tls_transcript +.export tls_transcript_h0 +.export tls_transcript_h1 +.export tls_transcript_h2 +.export tls_transcript_h3 +.export tls_transcript_h4 +.export tls_transcript_h5 +.export tls_transcript_h6 +.export tls_transcript_h7 +tls_transcript: .res 32 +tls_transcript_h0: .res 4 +tls_transcript_h1: .res 4 +tls_transcript_h2: .res 4 +tls_transcript_h3: .res 4 +tls_transcript_h4: .res 4 +tls_transcript_h5: .res 4 +tls_transcript_h6: .res 4 +tls_transcript_h7: .res 4 + +; Handshake keys (derived from ECDHE via HKDF) +.export tls_hs_write_key +.export tls_hs_write_iv +.export tls_hs_read_key +.export tls_hs_read_iv +tls_hs_write_key: .res 32 +tls_hs_write_iv: .res 12 +tls_hs_read_key: .res 32 +tls_hs_read_iv: .res 12 + +; Application traffic keys (derived after Finished) +.export tls_app_write_key +.export tls_app_write_iv +.export tls_app_read_key +.export tls_app_read_iv +tls_app_write_key: .res 32 +tls_app_write_iv: .res 12 +tls_app_read_key: .res 32 +tls_app_read_iv: .res 12 + +; Sequence numbers (64-bit, big-endian) +.export tls_write_seq +.export tls_read_seq +tls_write_seq: .res 8 +tls_read_seq: .res 8 + +; Record layer buffers +.export tls_rec_header +.export tls_rec_type +.export tls_rec_len +.export tls_rec_buf +tls_rec_header: .res 5 +tls_rec_type: .res 1 +tls_rec_len: .res 2 +tls_rec_buf: .res 548 + +; AEAD nonce construction +.export tls_nonce +tls_nonce: .res 12 + +; Handshake message buffer +.export tls_hs_buf +.export tls_hs_len +tls_hs_buf: .res 256 +tls_hs_len: .res 2 + +; ----------------------------------------------------------------------------- +; HKDF buffers +; ----------------------------------------------------------------------------- +.export hkdf_prk +.export hkdf_okm +.export hkdf_info_buf +.export hkdf_info_len +.export hkdf_salt_ptr +.export hkdf_salt_len +.export hkdf_ikm_ptr +.export hkdf_ikm_len +.export hkdf_label_ptr +.export hkdf_label_len +.export hkdf_context_ptr +.export hkdf_context_len +.export hkdf_out_len +hkdf_prk: .res 32 +hkdf_okm: .res 32 +hkdf_info_buf: .res 80 +hkdf_info_len: .res 1 +hkdf_salt_ptr: .res 2 +hkdf_salt_len: .res 1 +hkdf_ikm_ptr: .res 2 +hkdf_ikm_len: .res 1 +hkdf_label_ptr: .res 2 +hkdf_label_len: .res 1 +hkdf_context_ptr: .res 2 +hkdf_context_len: .res 1 +hkdf_out_len: .res 1 + +; TLS key schedule intermediate values +.export tls_early_secret +.export tls_handshake_secret +.export tls_master_secret +tls_early_secret: .res 32 +tls_handshake_secret: .res 32 +tls_master_secret: .res 32 + +; ----------------------------------------------------------------------------- +; HTTP buffers +; ----------------------------------------------------------------------------- +.export http_host_ptr +.export http_host_len +.export http_path_ptr +.export http_path_len +.export http_status +.export http_req_buf +.export http_req_len +.export http_resp_buf +.export http_resp_len +http_host_ptr: .res 2 +http_host_len: .res 1 +http_path_ptr: .res 2 +http_path_len: .res 1 +http_status: .res 2 +http_req_buf: .res 256 +http_req_len: .res 2 +http_resp_buf: .res 512 +http_resp_len: .res 2 + +; HTTP parser state +.export http_parse_state +.export http_hdr_match +.export http_line_idx +.export http_line_buf +http_parse_state: .res 1 +http_hdr_match: .res 1 +http_line_idx: .res 1 +http_line_buf: .res 32 + +; ----------------------------------------------------------------------------- +; Application data pointers (for tls_send) +; ----------------------------------------------------------------------------- +.export tls_app_ptr +.export tls_app_len +tls_app_ptr: .res 2 +tls_app_len: .res 2 + +; ----------------------------------------------------------------------------- +; General I/O buffers (used by SHA-256 update) +; ----------------------------------------------------------------------------- +.export input_buffer +.export input_length +input_buffer: .res 256 +input_length: .res 1 + +; ----------------------------------------------------------------------------- +; SHA-256 working variables +; ----------------------------------------------------------------------------- +.export sha256_h0 +.export sha256_h1 +.export sha256_h2 +.export sha256_h3 +.export sha256_h4 +.export sha256_h5 +.export sha256_h6 +.export sha256_h7 +sha256_h0: .res 4 +sha256_h1: .res 4 +sha256_h2: .res 4 +sha256_h3: .res 4 +sha256_h4: .res 4 +sha256_h5: .res 4 +sha256_h6: .res 4 +sha256_h7: .res 4 + +.export sha_a +.export sha_b +.export sha_c +.export sha_d +.export sha_e +.export sha_f +.export sha_g +.export sha_h +sha_a: .res 4 +sha_b: .res 4 +sha_c: .res 4 +sha_d: .res 4 +sha_e: .res 4 +sha_f: .res 4 +sha_g: .res 4 +sha_h: .res 4 + +.export sha_temp3 +.export sha_t1 +.export sha_t2 +sha_temp3: .res 4 +sha_t1: .res 4 +sha_t2: .res 4 + +.export sha256_block +.export sha256_w +.export sha256_hash +.export sha256_len +sha256_block: .res 64 +sha256_w: .res 256 ; message schedule (64 words * 4 bytes) +sha256_hash: .res 32 ; final hash output +sha256_len: .res 2 ; message length in bits + +; ----------------------------------------------------------------------------- +; HMAC-DRBG state +; ----------------------------------------------------------------------------- +.export hmac_key +.export hmac_val +.export hmac_opad_block +.export hmac_data_buf +.export hmac_data_len +.export hmac_result +.export drbg_seed +.export drbg_seed_len +.export drbg_output +hmac_key: .res 32 ; HMAC key / DRBG K state +hmac_val: .res 32 ; DRBG V state +hmac_opad_block: .res 64 ; Scratch: K XOR opad +hmac_data_buf: .res 97 ; V(32) + 0x00/0x01(1) + seed(64) +hmac_data_len: .res 1 ; Length of data in hmac_data_buf +hmac_result: .res 32 ; HMAC output +drbg_seed: .res 64 ; Seed material (privkey||hash) +drbg_seed_len: .res 1 ; Length of seed +drbg_output: .res 32 ; Generate output + +; ----------------------------------------------------------------------------- +; ChaCha20 state +; ----------------------------------------------------------------------------- +.export cc20_state +.export cc20_work +.export cc20_keystream +.export cc20_key +.export cc20_nonce +.export cc20_counter +cc20_state: .res 64 ; initial state (16 x 32-bit words) +cc20_work: .res 64 ; working state during block computation +cc20_keystream: .res 64 ; generated keystream for XOR +cc20_key: .res 32 ; 256-bit key +cc20_nonce: .res 12 ; 96-bit nonce +cc20_counter: .res 4 ; 32-bit block counter + +; ----------------------------------------------------------------------------- +; Poly1305 state +; ----------------------------------------------------------------------------- +.export poly_h +.export poly_r +.export poly_s +.export poly_product +.export poly1305_tag +poly_h: .res 17 ; 130-bit accumulator +poly_r: .res 16 ; clamped key part r +poly_s: .res 16 ; key part s (added at end) +poly_product: .res 33 ; multiplication scratch (17x16) +poly1305_tag: .res 16 ; output tag + +; ----------------------------------------------------------------------------- +; AEAD state +; ----------------------------------------------------------------------------- +.export aead_key +.export aead_nonce +.export aead_aad_ptr +.export aead_aad_len +.export aead_data_ptr +.export aead_data_len +.export aead_tag +.export aead_scratch +.export cc20_remain_hi +aead_key: .res 32 +aead_nonce: .res 12 +aead_aad_ptr: .res 2 +aead_aad_len: .res 1 +aead_data_ptr: .res 2 +aead_data_len: .res 2 ; data length (16-bit; TLS records up to ~4KB) +aead_tag: .res 16 +aead_scratch: .res 16 ; Poly1305 padding/length block +cc20_remain_hi: .res 1 ; high byte of 16-bit ChaCha20/Poly1305 length counter + ; (low byte lives in ZP at cc20_remain = $18) + +; ----------------------------------------------------------------------------- +; fe25519 field arithmetic temporaries +; ----------------------------------------------------------------------------- +.export fe_wide +.export fe_tmp1 +.export fe_tmp2 +.export fe_tmp3 +.export fe_tmp4 +fe_wide: .res 64 ; 512-bit product from multiply +fe_tmp1: .res 32 +fe_tmp2: .res 32 +fe_tmp3: .res 32 +fe_tmp4: .res 32 + +; ----------------------------------------------------------------------------- +; X25519 state +; ----------------------------------------------------------------------------- +.export x25_scalar +.export x25_u +.export x25_result +.export x25_x2 +.export x25_z2 +.export x25_x3 +.export x25_z3 +.export x25_a +.export x25_b +.export x25_da +.export x25_cb +.export x25_e +x25_scalar: .res 32 ; clamped scalar +x25_u: .res 32 ; input u-coordinate +x25_result: .res 32 ; output u-coordinate +x25_x2: .res 32 ; Montgomery ladder state +x25_z2: .res 32 +x25_x3: .res 32 +x25_z3: .res 32 +x25_a: .res 32 ; ladder temporaries +x25_b: .res 32 +x25_da: .res 32 +x25_cb: .res 32 +x25_e: .res 32 + +; --- fe_mul optimization buffers --- +.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 + +; ----------------------------------------------------------------------------- +; ECDSA signature verification +; ----------------------------------------------------------------------------- + +; --- Verification parameters --- +.export ecdsa_curve_id +.export ecdsa_hash +.export ecdsa_sig_r +.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) + +; --- DER parsing temporaries --- +.export ev_der_int_len +.export ev_der_copy_cnt +ev_der_int_len: .res 1 ; current INTEGER length +ev_der_copy_cnt: .res 1 ; copy counter diff --git a/src/main.asm b/src/main.asm deleted file mode 100644 index 83dd49e..0000000 --- a/src/main.asm +++ /dev/null @@ -1,89 +0,0 @@ -; ============================================================================= -; main.asm - c64-https: HTTPS client for the Commodore 64 -; -; TLS 1.3 (TLS_CHACHA20_POLY1305_SHA256) over TCP/IP -; RR-Net (CS8900a) ethernet via ip65 -; -; Build: acme -f cbm -o ../build/c64-https.prg --vicelabels ../build/labels.txt main.asm -; ============================================================================= - -!to "../build/c64-https.prg", cbm - -; --- system constants and zero page --- -!source "constants.asm" - -; --- boot stub and main loop --- -!source "boot.asm" - -; --- network wrapper (ip65 ZP time-sharing) --- -!source "net.asm" - -; --- TLS 1.3 engine --- -!source "tls13.asm" -!source "tls_record.asm" -!source "tls_record_io.asm" -!source "tls_handshake.asm" -!source "tls_transcript.asm" - -; --- entropy initialization --- -!source "entropy.asm" - -; --- HKDF key derivation + key schedule --- -!source "hkdf.asm" -!source "tls_keyschedule.asm" - -; ============================================================================= -; ip65 binary blob — built with ca65/ld65, placed at $2000 -; Jump table at $2000, code $2000-$3B26, BSS at $4000+ -; ============================================================================= -* = $2000 -!binary "../ip65-build/ip65-c64.bin" - -; --- HTTP/1.1 client (placed after ip65 to avoid code overlap at $2000) --- -!source "http.asm" - -; ============================================================================= -; Crypto modules (from c64-wireguard and c64-aes256-ecdsa) -; ============================================================================= - -; Relocate all crypto (word32 through ecdsa) out of ip65 BSS window ($4000-$5FFF). -; ip65's DHCP output_buffer at $4CED clobbers any code linked in that range. -; This puts word32/chacha/poly/aead + sha256/hmac/drbg + fe25519/x25519 + -; ecdsa P-256 + ecdsa_verify/der/tls_cert/tls_ecdh all contiguously at $6000+. -* = $6000 -!source "crypto/word32.asm" -!source "crypto/chacha20.asm" -!source "crypto/poly1305.asm" -!source "crypto/aead.asm" - -!source "crypto/sha256.asm" -!source "crypto/hmac_drbg.asm" -!source "crypto/fe25519.asm" -!source "crypto/x25519.asm" - -; --- ECDSA P-256 (for certificate verification) --- -!source "crypto/ecdsa_fp.asm" -!source "crypto/ecdsa_mod.asm" -!source "crypto/ecdsa_curve.asm" -!source "crypto/ecdsa_points.asm" - -; --- ECDSA P-384 (for CA certificate verification) --- -; !source "crypto/ecdsa_fp_384.asm" ; STUBBED — see project_p384_stubbed.md -; !source "crypto/ecdsa_mod_384.asm" ; STUBBED — see project_p384_stubbed.md -; !source "crypto/ecdsa_curve_384.asm" ; STUBBED — see project_p384_stubbed.md -; !source "crypto/ecdsa_points_384.asm" ; STUBBED — see project_p384_stubbed.md - -; --- ECDSA signature verification (P-256 + P-384) --- -!source "crypto/ecdsa_verify.asm" - -; --- DER/ASN.1 decoder for X.509 certificates --- -!source "der_decode.asm" - -; --- TLS certificate + CertificateVerify handling --- -!source "tls_cert.asm" - -; --- TLS ECDH wrapper (x25519-based key exchange) --- -!source "tls_ecdh.asm" - -; --- mutable data buffers (must come after all code) --- -!source "data.asm" diff --git a/src/main.s b/src/main.s new file mode 100644 index 0000000..6ac3247 --- /dev/null +++ b/src/main.s @@ -0,0 +1,18 @@ +; ============================================================================= +; main.s - Program entry +; Converted from ACME to ca65 in Phase 3 Batch D. +; +; The original main.asm was a top-level ACME orchestrator that !source'd every +; other .asm file and placed the ip65 binary blob at $2000 and crypto code at +; $6000. Under ca65, each .s file is an independent translation unit, and +; segment placement is driven by the ld65 config (MEMORY/SEGMENTS). So there is +; no orchestration to do here: all !source lines drop away, the !binary ip65 +; blob moves to src/net/ip65/ip65_blob.s, and the * = $2000 / * = $6000 anchors +; are enforced by segment placement in the ld65 cfg ("CRYPTO_CODE" etc). +; +; This file therefore contains no code of its own. It exists only so the +; per-file build list stays consistent; it assembles to an empty CODE +; contribution. +; ============================================================================= + +.segment "CODE" diff --git a/src/net/ip65/ip65_blob.s b/src/net/ip65/ip65_blob.s new file mode 100644 index 0000000..8b9beb8 --- /dev/null +++ b/src/net/ip65/ip65_blob.s @@ -0,0 +1,22 @@ +; src/net/ip65/ip65_blob.s — ca65 wrapper around the pre-built ip65 binary. +; +; The ip65 library is built by the legacy ACME Makefile pipeline into +; ip65-build/ip65-c64.bin +; which is a ~7KB blob pre-linked at $2000 (jump table + library code). +; This wrapper incbin's that blob into the NET_CODE segment so ld65 places +; it at $2000 inside the final c64-https.prg image. +; +; Do NOT modify ip65-build/ or the ip65 submodule — they remain the source +; of truth for the ip65 binary. This file just glues the pre-built blob +; into the ca65 link. +; +; Segment NET_CODE is defined by cfg/c64-https-ip65.cfg as +; start = $2000, size = $2000, file = %O, type = ro +; so ld65 places the blob at $2000 and the loader fragments written in +; Phase 3 Batch D Round 1 are unaffected. + +.segment "NET_CODE" + +; ca65 resolves .incbin paths relative to the including source file, so +; from src/net/ip65/ip65_blob.s the blob is three levels up from repo root. +.incbin "../../../ip65-build/ip65-c64.bin" diff --git a/src/net/ip65/ip65_symbols.inc b/src/net/ip65/ip65_symbols.inc new file mode 100644 index 0000000..97c5301 --- /dev/null +++ b/src/net/ip65/ip65_symbols.inc @@ -0,0 +1,59 @@ +; src/net/ip65/ip65_symbols.inc — ca65 equates for ip65 symbols used by net.s +; +; The ip65 blob (ip65-build/ip65-c64.bin) is pre-linked at $2000 and exposes +; a fixed jump table plus a variable-address table. Symbols here are derived +; from ip65-build/ip65-c64.map and ip65-build/ip65_stub.s. +; +; Only symbols actually referenced by src/net/ip65/net.s are defined here — +; do not dump the full map. Add more symbols as the backend grows. +; +; During Phase 3 these equates are ALSO present in src/constants.inc (legacy +; ACME-era header). To avoid duplicate-symbol errors when net.s includes +; both constants.inc and this file, every equate is guarded with `.ifndef`. +; In Phase 7 constants.inc will stop defining ip65_* symbols and this file +; becomes the single source of truth. + +.ifndef ip65_base + +; --- ip65 ZP overlap zone (26 bytes: $02-$1B) --- +ip65_zp_start = $02 +ip65_zp_end = $1b +ip65_zp_size = ip65_zp_end - ip65_zp_start + 1 + +; --- ip65 jump table at $2000 (fixed offsets from ip65-build/ip65_stub.s) --- +ip65_base = $2000 +ip65_init = ip65_base + 0 +ip65_process = ip65_base + 3 +ip65_dhcp_init = ip65_base + 6 +ip65_dns_resolve = ip65_base + 9 +ip65_tcp_connect = ip65_base + 12 +ip65_tcp_send = ip65_base + 15 +ip65_tcp_close = ip65_base + 18 +ip65_tcp_keepalive = ip65_base + 21 +ip65_dns_set_host = ip65_base + 24 +ip65_set_tcp_cb = ip65_base + 27 +ip65_set_tcp_dest = ip65_base + 30 + +; --- ip65 variable-pointer table at ip65_base+33 --- +; Each entry holds a 2-byte address pointing at the real ip65 variable. +ip65_vt = ip65_base + 33 +ip65_vt_cfg_mac = ip65_vt + 0 +ip65_vt_cfg_ip = ip65_vt + 2 +ip65_vt_cfg_netmask = ip65_vt + 4 +ip65_vt_cfg_gateway = ip65_vt + 6 +ip65_vt_cfg_dns = ip65_vt + 8 +ip65_vt_dns_ip = ip65_vt + 10 +ip65_vt_tcp_in_ptr = ip65_vt + 12 +ip65_vt_tcp_in_len = ip65_vt + 14 +ip65_vt_tcp_snd_len = ip65_vt + 16 +ip65_vt_ip65_error = ip65_vt + 18 +ip65_vt_tcp_dest_ip = ip65_vt + 20 + +; --- Direct variable addresses (from ip65-build/ip65-c64.map) --- +ip65_cfg_ip = $3a8a ; 4 bytes: our IP address +ip65_cfg_mac = $3a84 ; 6 bytes: our MAC address +ip65_tcp_snd_len = $4f48 ; 2 bytes: tcp_send_data_len +ip65_dns_ip_addr = $4073 ; 4 bytes: resolved DNS IP +ip65_error = $4cea ; 1 byte: last error code + +.endif ; ip65_base diff --git a/src/net.asm b/src/net/ip65/net.s similarity index 88% rename from src/net.asm rename to src/net/ip65/net.s index 53a08c0..1ff4c02 100644 --- a/src/net.asm +++ b/src/net/ip65/net.s @@ -1,5 +1,5 @@ -; ============================================================================= -; net.asm - ip65 network wrapper with zero page time-sharing +; src/net/ip65/net.s — ip65/RR-Net networking backend +; Converted from ACME to ca65 in Phase 3 Batch D. ; ; All ip65 calls go through this wrapper. Before each call: ; 1. Save crypto ZP ($02-$1B) to zp_save_buf @@ -9,7 +9,40 @@ ; The ip65 TCP callback fires DURING ip65_process, while ip65's ZP is active. ; 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. + +.include "constants.inc" +.include "ip65_symbols.inc" + +; --- Public ABI (what the rest of the firmware imports) --- +; Names match the legacy ACME entry points; net_abi.inc-style renames +; (net_dhcp_acquire, net_tcp_set_recv_cb, etc.) are deferred to Phase 7. +.export net_init +.export net_dhcp +.export net_poll +.export net_dns_resolve +.export net_tcp_connect +.export net_set_tcp_dest +.export net_tcp_send +.export net_tcp_close +.export net_print_ip +.export net_recv_ready +.export net_recv_byte +.export net_send_len +.export net_tcp_recv_cb + +; --- BSS imports from data.s --- +.import zp_save_buf +.import tcp_recv_head +.import tcp_recv_tail +.import tcp_recv_overflow +.import net_poll_entry_count +.import net_poll_return_count + +.segment "CODE" ; ============================================================================= ; net_init - initialize ip65 + ethernet (RR-Net CS8900a) @@ -219,7 +252,7 @@ net_print_ip: ora #$30 jsr chrout rts -@pb_val: !byte 0 +@pb_val: .byte 0 ; ============================================================================= ; net_recv_ready - check if data is available in receive ring buffer @@ -293,6 +326,9 @@ net_recv_byte: ; net_init_cb_addrs resolves the variable table pointers and patches the ; 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. ; ============================================================================= net_tcp_recv_cb: ; --- Read inbound data length (16-bit) --- @@ -304,10 +340,9 @@ cb_load_len_hi: sta cb_remaining+1 ; if length == 0, nothing to copy ora cb_remaining - bne + + bne :+ jmp cb_done -+ - +: ; --- Read inbound data pointer (16-bit), patch copy source --- cb_load_ptr_lo: lda $ffff ; SMC: patched to addr of tcp_inbound_data_ptr @@ -318,24 +353,23 @@ cb_load_ptr_hi: ; 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. + ; and overwrite previously-copied ring bytes. (d973531) lda cb_remaining+1 - beq + + beq :+ lda #255 sta cb_remaining lda #0 sta cb_remaining+1 -+ +: ; Copy loop: X = source index; ring store uses SMC on cb_store ldx #0 cb_loop: ; Check 16-bit remaining count lda cb_remaining ora cb_remaining+1 - bne + + bne :+ jmp cb_done -+ - +: ; --- Overflow check: if ((tail+1) & $3FF) == head, ring is full --- lda tcp_recv_tail+0 clc @@ -391,10 +425,10 @@ cb_store: cb_done: rts -cb_next_lo: !byte 0 ; scratch: (tail+1) & mask, low -cb_next_hi: !byte 0 ; scratch: (tail+1) & mask, high +cb_next_lo: .byte 0 ; scratch: (tail+1) & mask, low +cb_next_hi: .byte 0 ; scratch: (tail+1) & mask, high -cb_remaining: !word 0 ; bytes remaining to copy (callback-local) +cb_remaining: .word 0 ; bytes remaining to copy (callback-local) ; ============================================================================= ; net_init_cb_addrs - resolve ip65 variable table pointers for TCP callback @@ -445,22 +479,22 @@ net_init_cb_addrs: ; ============================================================================= net_save_zp: ldx #ip65_zp_size - 1 -- lda ip65_zp_start,x +: lda ip65_zp_start,x sta zp_save_buf,x dex - bpl - + bpl :- rts net_restore_zp: ldx #ip65_zp_size - 1 -- lda zp_save_buf,x +: lda zp_save_buf,x sta ip65_zp_start,x dex - bpl - + bpl :- rts ; ============================================================================= ; net module data ; ============================================================================= -net_send_ptr: !word 0 ; pointer for tcp_send wrapper -net_send_len: !word 0 ; length for tcp_send wrapper +net_send_ptr: .word 0 ; pointer for tcp_send wrapper +net_send_len: .word 0 ; length for tcp_send wrapper From 1a2f7c13be97bba400a08ef32d44f502f7e1c98e Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:00:34 -0500 Subject: [PATCH 15/22] Phase 4: ld65 layout and cfg fixes for clean link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves all ld65 link errors by restructuring the memory layout and adding two new segments. Produces a clean 27812-byte .prg. cfg/c64-https-ip65.cfg: - Drop 'define = yes' from SEGMENTS entries (was duplicating __NET_CODE_SIZE__ etc. against the MEMORY-side defines, causing ld65 abort) - Route RODATA segment to CRYPTO region (was LOADER; reclaims ~1.9 KB of LOADER headroom) - Add TLS_CODE segment, mapped to CRYPTO region, so large TLS object files can opt out of the tight LOADER region - Add TABLES_BSS segment with align=\$100, mapped to CRYPTO, for x25519 multiplication tables that must live below \$A000 - Mark ZP_SHARED, ZP_WIDE, LOADADDR, NET_BSS, TCP_RECV_BUF as optional (silences warnings about empty segments) src/data.s: - Move mul_dma_lo, mul_dma_hi, sqtab_lo, sqtab_hi into TABLES_BSS segment so they land below \$A000 (x25519 optimization requires it per project_x25519_optimization memory) - mul38_lo_tab / mul38_hi_tab stay in RODATA (now routed to CRYPTO via cfg, still below \$A000) src/tls_keyschedule.s, src/tls_cert.s: - Change top-level .segment from "CODE" to "TLS_CODE" so these large TLS state-machine and certificate files load into CRYPTO region instead of the tight LOADER region Final region utilization: LOADER 99% (49 B free) NET_CODE 84% (1241 B free) CRYPTO 100% (0 B free — packed with RODATA + CRYPTO_CODE + CRYPTO_RODATA + TLS_CODE + TABLES_BSS) SHADOW_BSS 99% (20 B free) Tables verified below \$A000 via build/labels.txt: mul_dma_lo = \$9A00 mul_dma_hi = \$9B00 sqtab_lo = \$9C00 sqtab_hi = \$9E00 tls_handshake.s was NOT moved to TLS_CODE because doing so would overflow CRYPTO by 669 bytes. Memory budget is now tight — future additions (P-384 restore, sibling crypto vendor) will need more headroom before they can land. Co-Authored-By: Claude Opus 4.6 (1M context) --- cfg/c64-https-ip65.cfg | 26 ++++++++++++++------------ src/data.s | 7 ++++++- src/tls_cert.s | 2 +- src/tls_keyschedule.s | 2 +- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/cfg/c64-https-ip65.cfg b/cfg/c64-https-ip65.cfg index 6d5af21..550af74 100644 --- a/cfg/c64-https-ip65.cfg +++ b/cfg/c64-https-ip65.cfg @@ -28,25 +28,27 @@ MEMORY { } SEGMENTS { - ZP_SHARED: load = ZP_IP65, type = zp, define = yes; - ZEROPAGE: load = ZP_CRYPTO, type = zp, define = yes; - ZP_WIDE: load = ZP_WIDE, type = zp, define = 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; + LOADADDR: load = LOADADDR, type = ro, optional = yes; EXEHDR: load = LOADER, type = ro; STARTUP: load = LOADER, type = ro, optional = yes; - CODE: load = LOADER, type = ro, define = yes; - RODATA: load = LOADER, type = ro; + CODE: load = LOADER, type = ro; + RODATA: load = CRYPTO, type = ro; INIT: load = LOADER, type = ro, optional = yes; - NET_CODE: load = NET_CODE, type = ro, define = yes; - NET_BSS: load = NET_BSS, type = bss, define = yes; + NET_CODE: load = NET_CODE, type = ro; + NET_BSS: load = NET_BSS, type = bss, optional = yes; - CRYPTO_CODE: load = CRYPTO, type = ro, define = 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, define = yes; - CRYPTO_BSS: load = SHADOW_BSS, type = bss, define = yes; + 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, define = yes; + TCP_RECV_BUF: load = TCP_BUF, type = bss, optional = yes; } diff --git a/src/data.s b/src/data.s index 984851f..39d4d51 100644 --- a/src/data.s +++ b/src/data.s @@ -96,9 +96,12 @@ zp_save_buf: .res 26 ; saves $02-$1B during ip65 calls ; ----------------------------------------------------------------------------- ; fe25519/x25519 optimization tables — MUST live below $A000 to avoid -; BASIC ROM shadow. (Original ACME layout guarantee; see NOTE at top.) +; BASIC ROM shadow. Placed in TABLES_BSS which ld65 maps to the top of the +; CRYPTO region ($6000-$9FFF), keeping them below $A000. ; ----------------------------------------------------------------------------- +.segment "TABLES_BSS" + .align 256 .export mul_dma_lo .export mul_dma_hi @@ -112,6 +115,8 @@ mul_dma_hi: .res 256 ; DMA target: hi bytes of a*b for current a sqtab_lo: .res 512 sqtab_hi: .res 512 +.segment "BSS" + ; ----------------------------------------------------------------------------- ; Network layer buffers ; ----------------------------------------------------------------------------- diff --git a/src/tls_cert.s b/src/tls_cert.s index 7504988..a0b317c 100644 --- a/src/tls_cert.s +++ b/src/tls_cert.s @@ -43,7 +43,7 @@ .import ecdsa_pubkey_x .import ecdsa_pubkey_y - .segment "CODE" + .segment "TLS_CODE" ; ============================================================================= ; tls_handle_certificate - Process TLS 1.3 Certificate message diff --git a/src/tls_keyschedule.s b/src/tls_keyschedule.s index 2fee26a..2a93c45 100644 --- a/src/tls_keyschedule.s +++ b/src/tls_keyschedule.s @@ -83,7 +83,7 @@ ; 8. server_hs_key = HKDF-Expand-Label(s_hs_traffic, "key", "", 32) ; 9. server_hs_iv = HKDF-Expand-Label(s_hs_traffic, "iv", "", 12) ; ============================================================================= -.segment "CODE" +.segment "TLS_CODE" tls_derive_handshake_keys: ; --- Step 1: early_secret = HKDF-Extract(salt=zeros, IKM=zeros) --- From 3755a188d1373dfcf1d6a030146261534bb609b4 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:24:27 -0500 Subject: [PATCH 16/22] Phase 5: PRG structure fixes (LOADADDR, fill, label format, exports) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves three structural problems in the ca65 build output that would prevent the PRG from loading in VICE or being consumable by the test harness. 1. Missing PRG load-address header. Add src/loadaddr.s: a 2-byte .word \$0801 in the LOADADDR segment. Remove 'optional = yes' from the LOADADDR segment in cfg so ld65 requires it to be populated. 2. No inter-segment padding. Add 'fill = yes, fillval = \$00' to the LOADER, NET_CODE, and CRYPTO MEMORY regions in cfg/c64-https-ip65.cfg. Without this, ld65 packed segments immediately after each other in the file, so NET_CODE landed at file offset \$17CD instead of \$1801 and the ip65 blob loaded 50 bytes too early at runtime. 3. Label file format mismatch. ca65 ld65 -Ln emits 'al XXXXXX .name' (no 'C:' prefix). The test harness Labels parser requires 'al C:XXXX .name'. Add a post-link sed pass in Makefile.ca65 'link' target: sed -i 's/^al 00\\([0-9a-fA-F]\\{4\\}\\) /al C:\\1 /' build/labels.txt Fixes Labels.from_file() parsing (was 0 entries, now 535). 4. Missing exports for source-level equates. Three symbols the harness looks up were defined as bare '=' equates in .inc files, so they never appeared in the linker symbol table: tcp_recv_buf (constants.inc), ip65_init and ip65_process (ip65_symbols.inc). Add src/exports.s — a single-TU stub that .include's the relevant .inc files once and issues explicit .export directives. Avoids duplicate-symbol errors from putting .export directly in .inc files that are included from many TUs. Results: - build/c64-https.prg: 30721 bytes (up from 27812), valid PRG header \$01 \$08 - ip65 blob at file offset \$1801 byte-identical to ACME baseline - build/labels.txt: 535 VICE-format entries, including tcp_recv_buf (\$C000), ip65_init (\$2000), ip65_process (\$2003) - Clean ld65 link, no warnings Delta vs ACME baseline (45900 bytes): ca65 is ~15 KB smaller because ACME emitted the SHADOW_BSS region into the file as pre-zeroed data; ca65 correctly treats it as bss. Whether this matters depends on whether C64 startup zeros its own BSS. Phase 6 VICE smoke test will verify runtime behavior. Co-Authored-By: Claude Opus 4.6 (1M context) --- Makefile.ca65 | 26 ++++++++++++++++++++++++-- cfg/c64-https-ip65.cfg | 8 ++++---- src/exports.s | 18 ++++++++++++++++++ src/loadaddr.s | 4 ++++ 4 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 src/exports.s create mode 100644 src/loadaddr.s diff --git a/Makefile.ca65 b/Makefile.ca65 index 4128ae5..cdc8308 100644 --- a/Makefile.ca65 +++ b/Makefile.ca65 @@ -8,7 +8,7 @@ LD65 ?= ld65 BACKEND ?= ip65 CFG := cfg/c64-https-$(BACKEND).cfg -CA65FLAGS := -I src -I src/inc --debug-info +CA65FLAGS := -I src -I src/inc -I src/net/ip65 --debug-info LD65FLAGS := -C $(CFG) -Ln build/labels.txt -m build/c64-https.map # Source inventory grows as Phase 3 converts more files. @@ -16,10 +16,32 @@ LD65FLAGS := -C $(CFG) -Ln build/labels.txt -m build/c64-https.map PILOT_SRCS := src/entropy.s PILOT_OBJS := $(patsubst src/%.s,build/%.o,$(PILOT_SRCS)) -.PHONY: pilot clean +# Full-link source inventory for Phase 5. +TOP_SRCS := $(wildcard src/*.s) +CRYPTO_SRCS := $(wildcard src/crypto/*.s) +IP65_SRCS := src/net/ip65/ip65_blob.s src/net/ip65/net.s + +TOP_OBJS := $(patsubst src/%.s,build/%.o,$(TOP_SRCS)) +CRYPTO_OBJS := $(patsubst src/%.s,build/%.o,$(CRYPTO_SRCS)) +IP65_OBJS := $(patsubst src/%.s,build/%.o,$(IP65_SRCS)) + +ALL_OBJS := $(TOP_OBJS) $(CRYPTO_OBJS) $(IP65_OBJS) + +.PHONY: pilot all link clean pilot: $(PILOT_OBJS) +all: build/c64-https.prg + +build/c64-https.prg: $(ALL_OBJS) + @mkdir -p build + $(LD65) $(LD65FLAGS) -o $@ $(ALL_OBJS) + # 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 /' build/labels.txt + +link: build/c64-https.prg + build/%.o: src/%.s @mkdir -p $(dir $@) $(CA65) $(CA65FLAGS) -o $@ $< diff --git a/cfg/c64-https-ip65.cfg b/cfg/c64-https-ip65.cfg index 550af74..7eaf609 100644 --- a/cfg/c64-https-ip65.cfg +++ b/cfg/c64-https-ip65.cfg @@ -18,10 +18,10 @@ MEMORY { 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; - NET_CODE: start = $2000, size = $2000, file = %O, define = yes; + 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, type = rw, define = yes; - CRYPTO: start = $6000, size = $4000, file = %O, define = yes; + 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; @@ -32,7 +32,7 @@ SEGMENTS { ZEROPAGE: load = ZP_CRYPTO, type = zp, optional = yes; ZP_WIDE: load = ZP_WIDE, type = zp, optional = yes; - LOADADDR: load = LOADADDR, 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; diff --git a/src/exports.s b/src/exports.s new file mode 100644 index 0000000..42c32c7 --- /dev/null +++ b/src/exports.s @@ -0,0 +1,18 @@ +; src/exports.s — Single-compilation-unit re-exports of `=` equates. +; +; Many symbols in constants.inc and ip65_symbols.inc are defined as +; numeric equates (`foo = $c000`). Those don't appear in the ld65 map +; unless something `.export`s them. constants.inc/ip65_symbols.inc are +; `.include`d in many translation units, so putting `.export` there +; would cause duplicate-symbol errors. This file is assembled exactly +; once and is the single place that promotes those equates to +; linker-visible symbols so the c64-test-harness Labels reader can +; find them in build/labels.txt. +; +; Add symbols here as the harness needs them. + +.include "constants.inc" + +.export tcp_recv_buf +.export ip65_init +.export ip65_process diff --git a/src/loadaddr.s b/src/loadaddr.s new file mode 100644 index 0000000..c06c641 --- /dev/null +++ b/src/loadaddr.s @@ -0,0 +1,4 @@ +; src/loadaddr.s — 2-byte PRG load address header (CBM convention). +; ld65 places this at the start of the output file. +.segment "LOADADDR" +.word $0801 From f36b10628d5000d40086aabd405f3d4c9947c011 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Wed, 15 Apr 2026 16:34:37 -0500 Subject: [PATCH 17/22] Phase 6: fix boot regression (SYS target, layout gap, BSS zero) Three compounding defects in the ca65 port of the boot path caused c64-https.prg to return to BASIC READY. without displaying the main menu. All three are fixed here. 1. BASIC stub SYS target was off by 3. The stub at $0801 said 'SYS 2064' ($0810), but start: is at $080D (2061) in both the ACME and ca65 builds. $0810 is mid-instruction, so SYS 2064 executed garbage on 6502 NMOS and skipped the bank-switch (LDA $01 / AND #$FE / STA $01) at $080D-$0812 that maps out BASIC ROM. The ACME build happened to survive this because none of its BSS sat under ROM, but the ca65 build placed crypto BSS in SHADOW_BSS ($A000-$BFFF) which is under BASIC ROM when bank-switch is skipped. Fix: change the stub text from '2064' to '2061' in src/boot.s. 2. NET_BSS memory gap flattened the file into the wrong addresses. cfg had: LOADER $0801-$1FFF file=%O NET_CODE $2000-$3FFF file=%O NET_BSS $4000-$5FFF (no file=) CRYPTO $6000-$9FFF file=%O ld65 packed the file-backed regions contiguously, skipping the 8 KB NET_BSS hole. But a PRG has a single load address and the KERNAL LOADs contiguously, so CRYPTO bytes intended for $6000-$9FFF were loaded physically into $4000-$7FFF. Functions like drbg_init_entropy, linked at $8243, ended up at $6243 and calls to them jumped into uninitialized RAM. Fix: add file=%O, fill=yes, fillval=$00 to the NET_BSS MEMORY region so ld65 emits 8 KB of zeros in the PRG for the gap. PRG size grows from 30721 to 38913 bytes. 3. SHADOW_BSS not zeroed on boot. The C64 KERNAL does not zero BSS on PRG load. net_initialized at $A000 held powered-on RAM garbage ($55 in test runs), so the main_loop first-run guard misdispatched. Other crypto BSS (drbg_seed, hmac_key, sha256_block) also started non-zero. Fix: add a 20-byte zero loop at the top of start: that clears $A000-$BFFF before any init runs. Uses self-modifying sta $A000,y page-walking, preserving registers X/Y minimally. Verification: - ca65 build boots cleanly in VICE, banner and main menu appear. - tools/test_entropy.py passes 7/7 (SID noise, CIA1 timer, non-zero drbg_seed, DRBG output entropy, reseed changes output). Known TODO: - The 'SYS 2064' typo exists verbatim in master's src/boot.asm too. ACME survives it by luck of BSS placement. Should be fixed on master before the ACME tree is retired, to avoid confusing any future reader. - The NET_BSS file-fill adds 8 KB of zeros to the PRG image. A cleaner Phase 7 fix would restructure the MEMORY map so all file-backed regions are physically contiguous (move all BSS to the end above CRYPTO). - TCP_RECV_BUF at $C000-$CFFF is not currently zeroed; if the HTTP GET path assumes an empty ring, add it to the start: zero loop or make it part of SHADOW_BSS zeroing. Co-Authored-By: Claude Opus 4.6 (1M context) --- cfg/c64-https-ip65.cfg | 2 +- src/boot.s | 23 +++++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/cfg/c64-https-ip65.cfg b/cfg/c64-https-ip65.cfg index 7eaf609..ae49430 100644 --- a/cfg/c64-https-ip65.cfg +++ b/cfg/c64-https-ip65.cfg @@ -20,7 +20,7 @@ MEMORY { 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, type = rw, define = yes; + 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; diff --git a/src/boot.s b/src/boot.s index c110304..7716df9 100644 --- a/src/boot.s +++ b/src/boot.s @@ -128,7 +128,7 @@ .word bas_end ; pointer to next BASIC line .word 10 ; line number .byte $9e ; SYS token - .byte "2064" ; decimal address of `start` + .byte "2061" ; decimal address of `start` ($080D) .byte 0 ; end of BASIC line bas_end: .word 0 ; end of BASIC program @@ -138,7 +138,7 @@ bas_end: ; ============================================================================= .segment "CODE" -; --- entry point (address $0810) --- +; --- entry point (address $080D; SYS 2061) --- start: ; disable BASIC ROM to free $A000-$BFFF lda $01 @@ -147,6 +147,25 @@ start: sei ; disable interrupts during init + ; Zero SHADOW_BSS ($A000-$BFFF, 8 KiB). PRG LOAD does not zero BSS; + ; ca65 BSS segments in file-less regions start with whatever RAM + ; happened to contain. Without this, `net_initialized` and similar + ; boot guards read garbage and send us straight into ip65 code before + ; ip65 has been initialised, crashing us back to BASIC READY. + ldy #$00 + ldx #$20 ; 32 pages = $2000 bytes + lda #$A0 + sta @zbss_store+2 ; reset high byte (idempotent across resets) + lda #$00 +@zbss_page: +@zbss_store: + sta $A000,y ; self-modified high byte walks $A0..$BF + iny + bne @zbss_store + inc @zbss_store+2 + dex + bne @zbss_page + ; clear screen lda #$93 jsr chrout From 04cb3972ffb53ef7cb4df2eb48a10b9537b2a41e Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Wed, 15 Apr 2026 17:04:58 -0500 Subject: [PATCH 18/22] Retire ACME Makefile; promote Makefile.ca65 to canonical build The ACME-era top-level Makefile is gone. Makefile.ca65 has been renamed to Makefile and extended with the targets from the old file (run, ip65 libs, ip65 blob) so the default `make` invocation drives the ca65/ld65 toolchain. ACME is no longer required to build this project. New canonical targets: make build/c64-https.prg + build/labels.txt make clean remove build/ make run autostart PRG in VICE make ip65-libs rebuild ip65 object libraries from the submodule make ip65-blob rebuild ip65-build/ip65-c64.bin Co-Authored-By: Claude Opus 4.6 (1M context) --- Makefile | 86 ++++++++++++++++++++++++++++++++++++--------------- Makefile.ca65 | 50 ------------------------------ 2 files changed, 61 insertions(+), 75 deletions(-) delete mode 100644 Makefile.ca65 diff --git a/Makefile b/Makefile index 49a5e82..da045eb 100644 --- a/Makefile +++ b/Makefile @@ -1,44 +1,80 @@ -ACME = acme -CA65 = ca65 -LD65 = ld65 -VICE = x64sc +# Makefile — ca65/ld65 build for c64-https +# +# Replaces the original ACME-based build. ACME is no longer required. +# +# Targets: +# make — default, produces build/c64-https.prg + build/labels.txt +# make clean — remove build artifacts +# make run — launch the PRG in VICE x64sc +# make ip65-libs — rebuild ip65 object libraries from the submodule +# make ip65-blob — rebuild ip65-build/ip65-c64.bin (requires ip65-libs first) +# +# Variables: +# BACKEND=ip65|uci — select networking backend config (default: ip65) +# CA65, LD65 — ca65 / ld65 binaries (default: cc65 toolchain in PATH) +# VICE — VICE binary for `make run` (default: x64sc) -SRC_DIR = src -BUILD_DIR = build -IP65_BUILD = ip65-build -IP65_DIR = ip65 +CA65 ?= ca65 +LD65 ?= ld65 +VICE ?= x64sc +BACKEND ?= ip65 +CFG := cfg/c64-https-$(BACKEND).cfg -PRG = $(BUILD_DIR)/c64-https.prg -LABELS = $(BUILD_DIR)/labels.txt -IP65_BIN = $(IP65_BUILD)/ip65-c64.bin +IP65_DIR := ip65 +IP65_BUILD := ip65-build +IP65_BIN := $(IP65_BUILD)/ip65-c64.bin -# ACME sources -ASM_SRCS = $(wildcard $(SRC_DIR)/*.asm) +CA65FLAGS := -I src -I src/inc -I src/net/ip65 --debug-info +LD65FLAGS := -C $(CFG) -Ln build/labels.txt -m build/c64-https.map -.PHONY: all clean run ip65-libs +# Source inventory. +TOP_SRCS := $(wildcard src/*.s) +CRYPTO_SRCS := $(wildcard src/crypto/*.s) +IP65_SRCS := src/net/ip65/ip65_blob.s src/net/ip65/net.s + +TOP_OBJS := $(patsubst src/%.s,build/%.o,$(TOP_SRCS)) +CRYPTO_OBJS := $(patsubst src/%.s,build/%.o,$(CRYPTO_SRCS)) +IP65_OBJS := $(patsubst src/%.s,build/%.o,$(IP65_SRCS)) + +ALL_OBJS := $(TOP_OBJS) $(CRYPTO_OBJS) $(IP65_OBJS) + +PRG := build/c64-https.prg +LABELS := build/labels.txt + +.PHONY: all link run clean ip65-libs ip65-blob all: $(PRG) -$(PRG): $(ASM_SRCS) $(IP65_BIN) | $(BUILD_DIR) - cd $(SRC_DIR) && $(ACME) -f cbm -o ../$(PRG) --vicelabels ../$(LABELS) main.asm +$(PRG): $(ALL_OBJS) $(IP65_BIN) + @mkdir -p build + $(LD65) $(LD65FLAGS) -o $@ $(ALL_OBJS) + # 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) + +link: $(PRG) -$(BUILD_DIR): - mkdir -p $(BUILD_DIR) +build/%.o: src/%.s + @mkdir -p $(dir $@) + $(CA65) $(CA65FLAGS) -o $@ $< -# Build ip65 libraries (only if not already built) +# Build ip65 object libraries from the submodule. Only needed if the ip65 +# submodule changes; the prebuilt blob is committed to ip65-build/. ip65-libs: cd $(IP65_DIR) && $(MAKE) -C ip65 && $(MAKE) -C drivers -# Build ip65 binary blob -$(IP65_BIN): $(IP65_BUILD)/ip65_stub.s $(IP65_BUILD)/ip65.cfg ip65-libs +# Build the ip65 binary blob (ip65-build/ip65-c64.bin). The resulting file is +# committed to the repo so a normal `make` does not need to rebuild it. +ip65-blob: $(IP65_BIN) + +$(IP65_BIN): $(IP65_BUILD)/ip65_stub.s $(IP65_BUILD)/ip65.cfg cd $(IP65_BUILD) && $(CA65) -I ../$(IP65_DIR) ip65_stub.s -o ip65_stub.o cd $(IP65_BUILD) && $(LD65) -C ip65.cfg -o ip65-c64.bin -m ip65-c64.map \ - ip65_stub.o ../$(IP65_DIR)/ip65/ip65_tcp.lib \ - ../$(IP65_DIR)/drivers/ip65_c64.lib c64.lib + ip65_stub.o ../$(IP65_DIR)/ip65/ip65_tcp.lib \ + ../$(IP65_DIR)/drivers/ip65_c64.lib c64.lib run: $(PRG) $(VICE) -autostart $(PRG) clean: - rm -f $(BUILD_DIR)/c64-https.prg $(BUILD_DIR)/labels.txt - rm -f $(IP65_BUILD)/ip65_stub.o $(IP65_BUILD)/ip65-c64.bin $(IP65_BUILD)/ip65-c64.map + rm -rf build diff --git a/Makefile.ca65 b/Makefile.ca65 deleted file mode 100644 index cdc8308..0000000 --- a/Makefile.ca65 +++ /dev/null @@ -1,50 +0,0 @@ -# Makefile.ca65 — ca65/ld65 build for c64-https -# -# Runs alongside the existing ACME Makefile during the refactor. -# Deleted in Phase 7 when the ACME build is retired. - -CA65 ?= ca65 -LD65 ?= ld65 -BACKEND ?= ip65 -CFG := cfg/c64-https-$(BACKEND).cfg - -CA65FLAGS := -I src -I src/inc -I src/net/ip65 --debug-info -LD65FLAGS := -C $(CFG) -Ln build/labels.txt -m build/c64-https.map - -# Source inventory grows as Phase 3 converts more files. -# Phase 2 only exercises the entropy pilot. -PILOT_SRCS := src/entropy.s -PILOT_OBJS := $(patsubst src/%.s,build/%.o,$(PILOT_SRCS)) - -# Full-link source inventory for Phase 5. -TOP_SRCS := $(wildcard src/*.s) -CRYPTO_SRCS := $(wildcard src/crypto/*.s) -IP65_SRCS := src/net/ip65/ip65_blob.s src/net/ip65/net.s - -TOP_OBJS := $(patsubst src/%.s,build/%.o,$(TOP_SRCS)) -CRYPTO_OBJS := $(patsubst src/%.s,build/%.o,$(CRYPTO_SRCS)) -IP65_OBJS := $(patsubst src/%.s,build/%.o,$(IP65_SRCS)) - -ALL_OBJS := $(TOP_OBJS) $(CRYPTO_OBJS) $(IP65_OBJS) - -.PHONY: pilot all link clean - -pilot: $(PILOT_OBJS) - -all: build/c64-https.prg - -build/c64-https.prg: $(ALL_OBJS) - @mkdir -p build - $(LD65) $(LD65FLAGS) -o $@ $(ALL_OBJS) - # 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 /' build/labels.txt - -link: build/c64-https.prg - -build/%.o: src/%.s - @mkdir -p $(dir $@) - $(CA65) $(CA65FLAGS) -o $@ $< - -clean: - rm -rf build From 654a366896497f6c5719e918f421ae51186eed90 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Wed, 15 Apr 2026 17:06:25 -0500 Subject: [PATCH 19/22] Honor C64_SKIP_BUILD=1 in test scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test scripts previously ran `make clean && make` unconditionally at startup. Phase 6 worked around that with an external make shim while the ca65 build was still being stabilised. Now that the canonical build is ca65 and safe to reuse, callers that have already built can set C64_SKIP_BUILD=1 to skip the make invocation entirely. This is opt-in — with C64_SKIP_BUILD unset, every test still performs its own clean + rebuild, so the normal path is unchanged. No shared helper was patched because each test script inlines its own build block; the 7 sweep-list scripts are edited directly: test_entropy, test_hkdf, test_chained_hmac, test_keyschedule_steps, test_tls_handshake, test_http, test_x509. Co-Authored-By: Claude Opus 4.6 (1M context) --- tools/test_chained_hmac.py | 21 ++++++++++++--------- tools/test_entropy.py | 19 +++++++++++-------- tools/test_hkdf.py | 19 +++++++++++-------- tools/test_http.py | 19 +++++++++++-------- tools/test_keyschedule_steps.py | 19 +++++++++++-------- tools/test_tls_handshake.py | 21 ++++++++++++--------- tools/test_x509.py | 21 ++++++++++++--------- 7 files changed, 80 insertions(+), 59 deletions(-) diff --git a/tools/test_chained_hmac.py b/tools/test_chained_hmac.py index 05394ac..41d98ab 100644 --- a/tools/test_chained_hmac.py +++ b/tools/test_chained_hmac.py @@ -57,15 +57,18 @@ def build_trampoline(hmac_addr, n): def main(): os.chdir(PROJECT_ROOT) - # Build - print("=== 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) - print(" Build OK") + # Build (skippable via C64_SKIP_BUILD=1 when a caller has already built) + if os.environ.get("C64_SKIP_BUILD"): + print("=== Building (skipped: C64_SKIP_BUILD set) ===") + else: + print("=== 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) + print(" Build OK") if not os.path.exists(PRG_PATH): print(f"FATAL: {PRG_PATH} not found") diff --git a/tools/test_entropy.py b/tools/test_entropy.py index ce063d4..942f06a 100644 --- a/tools/test_entropy.py +++ b/tools/test_entropy.py @@ -349,14 +349,17 @@ def main(): if idx + 1 < len(sys.argv): vice_seed = sys.argv[idx + 1] - # Build - print("=== Building ===") - subprocess.run(["make", "clean"], capture_output=True) - result = subprocess.run(["make"], capture_output=True, text=True) - if result.returncode != 0: - print(f" Build failed:\n{result.stderr}") - sys.exit(1) - print(" Build OK") + # Build (skippable via C64_SKIP_BUILD=1 when a caller has already built) + if os.environ.get("C64_SKIP_BUILD"): + print("=== Building (skipped: C64_SKIP_BUILD set) ===") + else: + print("=== Building ===") + subprocess.run(["make", "clean"], capture_output=True) + result = subprocess.run(["make"], capture_output=True, text=True) + if result.returncode != 0: + print(f" Build failed:\n{result.stderr}") + sys.exit(1) + print(" Build OK") if not os.path.exists(PRG_PATH): print(f"FATAL: {PRG_PATH} not found") diff --git a/tools/test_hkdf.py b/tools/test_hkdf.py index d363d8d..6dc8eb1 100644 --- a/tools/test_hkdf.py +++ b/tools/test_hkdf.py @@ -437,14 +437,17 @@ def main(): random.seed(seed) print(f"Random seed: {seed} (reproduce with --seed {seed})") - # Build - print("\n=== Building ===") - subprocess.run(["make", "clean"], capture_output=True) - result = subprocess.run(["make"], capture_output=True, text=True) - if result.returncode != 0: - print(f"Build failed:\n{result.stderr}") - sys.exit(1) - print(" Build OK") + # Build (skippable via C64_SKIP_BUILD=1 when a caller has already built) + if os.environ.get("C64_SKIP_BUILD"): + print("\n=== Building (skipped: C64_SKIP_BUILD set) ===") + else: + print("\n=== Building ===") + subprocess.run(["make", "clean"], capture_output=True) + result = subprocess.run(["make"], capture_output=True, text=True) + if result.returncode != 0: + print(f"Build failed:\n{result.stderr}") + sys.exit(1) + print(" Build OK") if not os.path.exists(PRG_PATH): print(f"FATAL: {PRG_PATH} not found") diff --git a/tools/test_http.py b/tools/test_http.py index f81206b..e9f9207 100755 --- a/tools/test_http.py +++ b/tools/test_http.py @@ -426,14 +426,17 @@ def main(): random.seed(seed) print(f"Random seed: {seed} (reproduce with --seed {seed})") - # Build - print("\n=== Building ===") - result = subprocess.run(["make", "clean"], capture_output=True) - result = subprocess.run(["make"], capture_output=True, text=True) - if result.returncode != 0: - print(f" Build failed:\n{result.stderr}") - sys.exit(1) - print(" Build OK") + # Build (skippable via C64_SKIP_BUILD=1 when a caller has already built) + if os.environ.get("C64_SKIP_BUILD"): + print("\n=== Building (skipped: C64_SKIP_BUILD set) ===") + else: + print("\n=== Building ===") + result = subprocess.run(["make", "clean"], capture_output=True) + result = subprocess.run(["make"], capture_output=True, text=True) + if result.returncode != 0: + print(f" Build failed:\n{result.stderr}") + sys.exit(1) + print(" Build OK") labels = Labels.from_file(LABELS_PATH) print(f" Labels loaded, {len(labels)} symbols") diff --git a/tools/test_keyschedule_steps.py b/tools/test_keyschedule_steps.py index 905d88b..8521a18 100644 --- a/tools/test_keyschedule_steps.py +++ b/tools/test_keyschedule_steps.py @@ -347,14 +347,17 @@ def main(): if "--verbose" in sys.argv: VERBOSE = True - # Build - print("\n=== Building ===") - subprocess.run(["make", "clean"], capture_output=True) - result = subprocess.run(["make"], capture_output=True, text=True) - if result.returncode != 0: - print(f"Build failed:\n{result.stderr}") - sys.exit(1) - print(" Build OK") + # Build (skippable via C64_SKIP_BUILD=1 when a caller has already built) + if os.environ.get("C64_SKIP_BUILD"): + print("\n=== Building (skipped: C64_SKIP_BUILD set) ===") + else: + print("\n=== Building ===") + subprocess.run(["make", "clean"], capture_output=True) + result = subprocess.run(["make"], capture_output=True, text=True) + if result.returncode != 0: + print(f"Build failed:\n{result.stderr}") + sys.exit(1) + print(" Build OK") if not os.path.exists(PRG_PATH): print(f"FATAL: {PRG_PATH} not found") diff --git a/tools/test_tls_handshake.py b/tools/test_tls_handshake.py index 5c1b3ba..83864e0 100644 --- a/tools/test_tls_handshake.py +++ b/tools/test_tls_handshake.py @@ -1200,15 +1200,18 @@ def main(): random.seed(seed) 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) - print(f" Build OK: {PRG_PATH}") + # Build (skippable via C64_SKIP_BUILD=1 when a caller has already built) + if os.environ.get("C64_SKIP_BUILD"): + print("\n=== Building (skipped: C64_SKIP_BUILD set) ===") + else: + 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) + print(f" Build OK: {PRG_PATH}") if not os.path.exists(PRG_PATH): print(f"FATAL: {PRG_PATH} not found") diff --git a/tools/test_x509.py b/tools/test_x509.py index 41a5518..521ce6a 100644 --- a/tools/test_x509.py +++ b/tools/test_x509.py @@ -722,15 +722,18 @@ def main(): random.seed(seed) 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) - print(f" Build OK: {PRG_PATH}") + # Build (skippable via C64_SKIP_BUILD=1 when a caller has already built) + if os.environ.get("C64_SKIP_BUILD"): + print("\n=== Building (skipped: C64_SKIP_BUILD set) ===") + else: + 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) + print(f" Build OK: {PRG_PATH}") if not os.path.exists(PRG_PATH): print(f"FATAL: {PRG_PATH} not found") From c5b6ed62cd1cfde6fe70c8327f6a25ea141c5bd5 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Wed, 15 Apr 2026 17:19:21 -0500 Subject: [PATCH 20/22] Consolidate ip65 symbols into ip65_symbols.inc src/net/ip65/ip65_symbols.inc is now the single source of truth for all ip65_* equates (ZP overlap zone, jump-table offsets, variable table, direct map addresses). The legacy ACME-era copy in src/constants.inc has been removed and the `.ifndef` guard dropped from ip65_symbols.inc. Files that previously got ip65_* symbols transitively through constants.inc now `.include "ip65_symbols.inc"` directly: - src/boot.s (boot phase DNS wait) - src/http.s (DNS response read-out) - src/exports.s (promotes ip65_init / ip65_process for labels.txt) Verified by clean rebuild + tools/test_entropy.py (7/7 pass, both with and without C64_SKIP_BUILD=1). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/boot.s | 1 + src/constants.inc | 49 +++-------------------------------- src/exports.s | 1 + src/http.s | 1 + src/net/ip65/ip65_symbols.inc | 12 +++------ 5 files changed, 9 insertions(+), 55 deletions(-) diff --git a/src/boot.s b/src/boot.s index 7716df9..9264fd7 100644 --- a/src/boot.s +++ b/src/boot.s @@ -2,6 +2,7 @@ ; Converted from ACME to ca65 in Phase 3 Batch D. .include "constants.inc" + .include "ip65_symbols.inc" ; ---- exports: entry + print helpers ---- .export start diff --git a/src/constants.inc b/src/constants.inc index 91bceb8..5a7bd96 100644 --- a/src/constants.inc +++ b/src/constants.inc @@ -134,52 +134,9 @@ sid_v3_ctrl = $d412 sid_v3_ad = $d413 sid_v3_sr = $d414 -; --- ip65 ZP overlap zone --- -; ip65 uses $02-$1B during its execution (cc65 standard: c_sp, sreg, -; regsave, ptr1-ptr4, tmp1-tmp4, regbank). These overlap our crypto -; ZP at $02-$1B. The net.asm wrapper handles save/restore. -ip65_zp_start = $02 -ip65_zp_end = $1b ; inclusive -ip65_zp_size = ip65_zp_end - ip65_zp_start + 1 ; 26 bytes - -; ============================================================================= -; ip65 jump table at $2000 (fixed offsets from ip65-build/ip65_stub.s) -; ============================================================================= -ip65_base = $2000 -ip65_init = ip65_base + 0 ; A=0 default; C=0 ok -ip65_process = ip65_base + 3 ; poll; C=0 packet, C=1 idle -ip65_dhcp_init = ip65_base + 6 ; DHCP; C=0 ok -ip65_dns_resolve = ip65_base + 9 ; resolve; C=0 ok -ip65_tcp_connect = ip65_base + 12 ; AX=port; C=0 ok -ip65_tcp_send = ip65_base + 15 ; AX=data ptr; C=0 ok -ip65_tcp_close = ip65_base + 18 ; close connection -ip65_tcp_keepalive = ip65_base + 21 ; send keepalive -ip65_dns_set_host = ip65_base + 24 ; AX=hostname ptr -ip65_set_tcp_cb = ip65_base + 27 ; AX=callback addr -ip65_set_tcp_dest = ip65_base + 30 ; AX=4-byte IP ptr - -; ip65 variable table at ip65_base+33 (2-byte address pointers) -; Read the pointer, then dereference to access the variable. -; For convenience, we define the indirect addresses directly: -ip65_vt = ip65_base + 33 -ip65_vt_cfg_mac = ip65_vt + 0 ; -> 6 bytes MAC -ip65_vt_cfg_ip = ip65_vt + 2 ; -> 4 bytes our IP -ip65_vt_cfg_netmask = ip65_vt + 4 ; -> 4 bytes netmask -ip65_vt_cfg_gateway = ip65_vt + 6 ; -> 4 bytes gateway -ip65_vt_cfg_dns = ip65_vt + 8 ; -> 4 bytes DNS server -ip65_vt_dns_ip = ip65_vt + 10 ; -> 4 bytes resolved IP -ip65_vt_tcp_in_ptr = ip65_vt + 12 ; -> 2 bytes inbound data ptr -ip65_vt_tcp_in_len = ip65_vt + 14 ; -> 2 bytes inbound data length -ip65_vt_tcp_snd_len = ip65_vt + 16 ; -> 2 bytes send data length -ip65_vt_ip65_error = ip65_vt + 18 ; -> 1 byte error code -ip65_vt_tcp_dest_ip = ip65_vt + 20 ; -> 4 bytes dest IP - -; Direct addresses (from ip65-c64.map, for when we need to poke directly) -ip65_cfg_ip = $3a8a ; 4 bytes: our IP address -ip65_cfg_mac = $3a84 ; 6 bytes: our MAC address -ip65_tcp_snd_len = $4f48 ; 2 bytes: tcp_send_data_len -ip65_dns_ip_addr = $4073 ; 4 bytes: resolved DNS IP -ip65_error = $4cea ; 1 byte: last error code +; --- ip65 ZP overlap zone + jump table --- +; Moved to src/net/ip65/ip65_symbols.inc (single source of truth). Files +; that need ip65_* symbols must `.include "ip65_symbols.inc"` directly. ; ============================================================================= ; TLS 1.3 constants diff --git a/src/exports.s b/src/exports.s index 42c32c7..bea231e 100644 --- a/src/exports.s +++ b/src/exports.s @@ -12,6 +12,7 @@ ; Add symbols here as the harness needs them. .include "constants.inc" +.include "ip65_symbols.inc" .export tcp_recv_buf .export ip65_init diff --git a/src/http.s b/src/http.s index a411de4..f4c26fc 100644 --- a/src/http.s +++ b/src/http.s @@ -8,6 +8,7 @@ ; (status line + headers + body). .include "constants.inc" + .include "ip65_symbols.inc" ; ---- exports ---- .export http_get diff --git a/src/net/ip65/ip65_symbols.inc b/src/net/ip65/ip65_symbols.inc index 97c5301..a5a5d32 100644 --- a/src/net/ip65/ip65_symbols.inc +++ b/src/net/ip65/ip65_symbols.inc @@ -7,13 +7,9 @@ ; Only symbols actually referenced by src/net/ip65/net.s are defined here — ; do not dump the full map. Add more symbols as the backend grows. ; -; During Phase 3 these equates are ALSO present in src/constants.inc (legacy -; ACME-era header). To avoid duplicate-symbol errors when net.s includes -; both constants.inc and this file, every equate is guarded with `.ifndef`. -; In Phase 7 constants.inc will stop defining ip65_* symbols and this file -; becomes the single source of truth. - -.ifndef ip65_base +; Phase 7: this file is the single source of truth for ip65_* equates. +; src/constants.inc no longer defines them, so the earlier `.ifndef` +; guard has been removed. ; --- ip65 ZP overlap zone (26 bytes: $02-$1B) --- ip65_zp_start = $02 @@ -55,5 +51,3 @@ ip65_cfg_mac = $3a84 ; 6 bytes: our MAC address ip65_tcp_snd_len = $4f48 ; 2 bytes: tcp_send_data_len ip65_dns_ip_addr = $4073 ; 4 bytes: resolved DNS IP ip65_error = $4cea ; 1 byte: last error code - -.endif ; ip65_base From 8a92bb5343516cd78e2389aac606d67431039573 Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Wed, 15 Apr 2026 17:20:39 -0500 Subject: [PATCH 21/22] Add CLAUDE.md with architecture and build notes Covers: - Build section: make targets, BACKEND variable, C64_SKIP_BUILD - Crypto ABI section: public symbols, MEMORY requirements for a drop-in sibling library, mapping to c64-x25519 / c64-ChaCha20-Poly1305 / c64-nist-curves - Networking backend ABI section: net_abi.inc, ip65 vs uci backends, BACKEND=ip65|uci selection - Memory layout section: cfg region map, tight CRYPTO/SHADOW_BSS regions, intentional loadaddr / exports stubs - Smoke test section: the 7 passing scripts and the known ip65 upstream blocker for end-to-end HTTPS No code changes. Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..36b8275 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,166 @@ +# c64-https — architecture notes + +TLS 1.3 / HTTPS client for the Commodore 64, assembled with ca65/ld65 and +delivered as a single PRG. Networking is provided by the ip65/RR-Net stack +(prebuilt blob at $2000). All crypto is hand-written 6502 tuned to fit +under the BASIC ROM shadow at $A000. + +This file is the load-bearing "how does this hang together" reference. +Keep it terse. + +## Build + +Dependencies: + - `ca65`, `ld65` from cc65 (ACME is no longer required) + - GNU make + - VICE (`x64sc`) only for `make run` / the test harness + +Targets: + - `make` — default, produces `build/c64-https.prg` + and `build/labels.txt` (VICE label format) + - `make clean` — remove build artifacts + - `make run` — autostart the PRG in VICE + - `make ip65-libs` — rebuild ip65 object libraries from the submodule + (only needed if the ip65 submodule changes) + - `make ip65-blob` — rebuild `ip65-build/ip65-c64.bin` from those + libraries (the committed blob is normally reused) + +Variables: + - `BACKEND=ip65|uci` — select networking backend cfg + (`cfg/c64-https-$(BACKEND).cfg`; default ip65) + - `CA65`, `LD65` — toolchain overrides + - `VICE` — override the `make run` emulator + +Test harness expectations: + - Most `tools/test_*.py` scripts run `make clean && make` themselves + before launching VICE. Set `C64_SKIP_BUILD=1` in the environment to + reuse the already-built PRG (7 scripts currently honor the var — + see the "Honor C64_SKIP_BUILD" commit for the list). + - Use the `c64-test-harness` Python package to launch VICE; never run + `x64sc` directly from tests. + +## Crypto ABI + +Public crypto API is fronted by `src/crypto_abi.inc`. TLS/HTTP sources +consume crypto only through the symbols listed there. The intent is +that any implementation (in-tree today, vendored sibling library +tomorrow) can fulfil the contract by providing the same `.export`s; +swapping implementations is a link-line change, not a call-site change. + +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_scalarmult — X25519 scalar × point, 32-byte buffers + fe25519_mul, fe25519_sqr, fe25519_inv + + ChaCha20-Poly1305 (c64-ChaCha20-Poly1305 sibling) + chacha20_encrypt + poly1305_init, poly1305_update, poly1305_final + aead_encrypt, aead_decrypt + + SHA-256 (in-tree; no sibling) + sha256_init, sha256_update, sha256_final + + ECDSA P-256 point ops (c64-nist-curves sibling) + ec_point_double, ec_point_add, ec_jacobian_to_affine + +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. + +MEMORY requirements for a drop-in sibling library: + - Code + rodata must load into the `CRYPTO` region at **$6000-$9FFF** + (below the BASIC ROM shadow at $A000, so it survives ROM banking). + - `TABLES_BSS` (`x25519` squaring tables etc.) must stay **below $A000**; + the cfg pins it inside the CRYPTO region with `align = $100`. + - 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. + +## Networking backend ABI + +Public net API is fronted by `src/net_abi.inc`. TLS/HTTP sources consume +networking only through those symbols. Switching backend = picking a +different `cfg/c64-https-$(BACKEND).cfg` and linking different +`src/net//*.o` files. + +Current backends: + - `src/net/ip65/` — ip65/RR-Net (cs8900a driver). The ip65 blob is + prebuilt to `ip65-build/ip65-c64.bin` and loaded at $2000 via + `src/net/ip65/ip65_blob.s` (`.incbin`). `src/net/ip65/net.s` + is the ABI adapter. `src/net/ip65/ip65_symbols.inc` is the single + source of truth for the `ip65_*` jump-table / variable-table + equates (Phase 7 consolidated these out of `constants.inc`). + - `src/net/uci/` — placeholder for a future U64E UCI backend + (Ultimate 64 Elite). `cfg/c64-https-uci.cfg` exists but is empty + stubs; selecting `BACKEND=uci` does not yet produce a working PRG. + +Public symbols (see `src/net_abi.inc`): + net_init, net_poll, net_dhcp_acquire + net_tcp_connect, net_tcp_send, net_tcp_close, net_tcp_set_recv_cb + net_dns_resolve + net_local_ip, net_resolved_ip, net_last_error, net_tcp_state + +## Memory layout + +Defined in `cfg/c64-https-ip65.cfg`. Physically contiguous file-backed +regions run from $0801 through $9FFF, with SHADOW_BSS at $A000 and the +TCP ring at $C000. + + $0801-$1FFF LOADER BASIC stub + boot + TLS + HTTP + net wrapper + $2000-$3FFF NET_CODE ip65 code (as .incbin blob) + $4000-$5FFF NET_BSS ip65 BSS (zero-filled in the PRG) + $6000-$9FFF CRYPTO all crypto code, rodata, and TABLES_BSS + $A000-$BFFF SHADOW_BSS mutable state behind BASIC ROM shadow + (CPU port $01 = $36 selects RAM) + $C000-$CFFF TCP_BUF `tcp_recv_buf`, 4KB ring for ip65 callback + +Tight regions (after Phase 6 fit-up): + - **CRYPTO** is **100%** full. Any new crypto byte requires relocation + or reclamation somewhere. + - **SHADOW_BSS** is **99.8%** full — roughly 20 bytes of slack. + +There is a known TODO to restructure the MEMORY map so that all +file-backed regions are physically contiguous in a single ROM-like +run (the LOADER/NET gap is currently zero-filled into the PRG just +to keep offsets right). That cleanup is explicitly **out of scope** +for the ca65-conversion branch — see the Phase 6 commit for the +rationale and follow-up plan. + +### LOADADDR / exports stubs + +Two small `src/*.s` files exist as thin wrappers to work around +ld65 and ca65 edge cases; they are intentional and should stay: + + - `src/loadaddr.s` — a single `.word $0801` in the `LOADADDR` + segment. ld65 needs *some* symbol in that segment for the 2-byte + PRG load-address header to land at `$07FF`. + - `src/exports.s` — promotes the numeric equates `tcp_recv_buf`, + `ip65_init`, `ip65_process` to linker-visible `.export`s so they + appear in `build/labels.txt` for the Python test harness. The + `.export` has to live in exactly one translation unit; doing it + inside the `.inc` header would duplicate on every include. + +## Smoke tests + +The `tools/test_*.py` scripts cover individual crypto primitives and +the TLS state machine. For a quick sanity check after a build: + + - `tools/test_entropy.py` — fastest (DRBG seed + fill, 7 tests) + - `tools/test_hkdf.py` — HKDF extract/expand + - `tools/test_chained_hmac.py` — HMAC chain + - `tools/test_keyschedule_steps.py` — TLS 1.3 key schedule + - `tools/test_tls_handshake.py` — full handshake state machine + - `tools/test_http.py` — HTTP request/response build + parse + - `tools/test_x509.py` — X.509 parser + +All 7 pass as of the ca65-conversion branch (97/97 assertions). + +End-to-end HTTPS against a real server (`www.foo.bar` via the local +bridge rig — never a real internet domain) is still blocked on an +upstream ip65 bug unchanged by this refactor; see +`project_phase3_handoff` in memory. From 907398305340130d80ad42ba4d1d709e9318b97c Mon Sep 17 00:00:00 2001 From: JC-000 <3798556+JC-000@users.noreply.github.com> Date: Wed, 15 Apr 2026 17:21:02 -0500 Subject: [PATCH 22/22] Untrack build/c64-https.prg and build/labels.txt These were historically committed under the ACME build. The ca65 build produces different output and build/ is already in .gitignore. Untrack them so `make clean && make` leaves a clean working tree. Co-Authored-By: Claude Opus 4.6 (1M context) --- build/c64-https.prg | Bin 45900 -> 0 bytes build/labels.txt | 712 -------------------------------------------- 2 files changed, 712 deletions(-) delete mode 100644 build/c64-https.prg delete mode 100644 build/labels.txt diff --git a/build/c64-https.prg b/build/c64-https.prg deleted file mode 100644 index 6b16d195b7b12919debe7b2fdebc9c89d2622fec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45900 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 jwl47hk_Te{m-KX~VLGz^Gr9k91^+W