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 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 zcmeHw3tSUdzJEdhLkJKQH7d&ZLhym8pcr3?)&h29eD<|&H&$c88f_J|t#4~wik=8| z+m&LvY&60k!)C0svTa)1-CD3}r~A0Ky=$qvZ7pt<#@1)G^8cQh1R~nKxBtES|NlSt z&wOC!oZtKW&hI?t%$bQ=8?Du;ULKu3HbbRSdDN-59O|d|=b1mW@>45xOuhC&roI<* zXeD!av4UiJ>G@I_qvY9XTH%P+uVIp6n9p1N-!|q-k<8OtfE#m=IxeTOk?I#kVY8(V zJ?bd6Lv7_>k)<%L`VuXFLQY%xzsgtsf`aN61C+WV8fIAeSozAl5W}m<5v^6_xUubK zg-X2PkA$%e(iJPrkhFZ?0Qn^O%Kw$91YS?n0v|`h1pa%Xq1kW&LLvly7Ks#i73m@{ zHzz^WtpM+LimG9{4l*BBWL|8O8GRR-uTz<7$aJfG7xjLv>r&6=GgG8*c@$?!4{#FgmyzsN*vUmbs5AxAAa;=I>{QC`OlVYJr)7uJsPo>BRu z#2aP$+J@uOw>$M-_$0>54AU{SanQv_R_JLJxK(Ew{Lwyrt?cq<=^`Hk*ITEJ%@~n0 zZ{DoA%rrK4dfq(d{`9m_F){Plxy-1vG0aG2T8;&Oqti#s;@F2qjY-dpnKNfZ-t>7) z%Cxyz2v0$!3tTh;*%iu zUXDwfYMG;`%$v*1nLd|e@1NeT88B+%JXkZ0%gdWSZC)EL1V*(|&*oV2?wOloxvyQo z?NC`1=;lt(yQfV~p!gIxFGfX4g5?fq%;oY>iQHVO1u^ix9;Xv};eCDW2|aVa4QFAV z&86Ba%*WZ)azZbC28ysx`MpH*DN^B|f>uW&kSNDAumDH?;8rgWe3UZSQcFt|b) z0@Rc@3hnZ)Y%TBW{Tj9(wJ1~{QWOPdsJ7_hL@?VDnV6wQeEi}Gg`B(fpk>0Pj#S0RVjm7TgfxA0?zAVW1fliTWInT z8St}MVO^|W$5R4S%{rJ;qplR5iH!lcm#?+8Om(248TqVpYLIxpIJGX8ogceXT}i8J z1VhHg3TX@(7mKv9KuspnY=MeQiWNpOWN@sI&X8o9rpRG53*_)vtGicSnCn#ealTDtWEr24Cs8$urEbReVlJhS^!pt3kmWj`Pk$+#q!^&O421?d_e+ zYiM?j!JEN{2U0`%h(LcYuFZrOVLUm${*RMraD^)dMNa#sURx$4;Sn1#?C3usBlU0p9B_l7<9=(e8 z=%=b^kN!awdURS7a^bAZ9wQUc9l}&K{-}D@L0Q^2f~tlpQrAEc--D@6@OI@fO7aIC zH-U28Vh$N)5o!`%2|0xZvf4l*D@m8imdHv;)6%63B1 z@w`F$w&j9BYJuxyWwZaGo(|;_R0kWi{=~*;zpgRPAJdrNH%JC%-X3o>TJy^)DZf5g!bf|IQL(LjPpj;C`Ra;drj z=gac>ihRCWZS>qI2zT5l2|IhUqKkNAcE7S~)kTIYR%H62wUqWN(HDvR3RFmFiVT;j zUu!==&DET$DwB9t-BnIh8Bu{~ThDYu6{2me>xQUos6tgYR*~btKGUthP%W(M2Hg8q z&J$JWE?Fwj$nf0|jeLW9>~PpH*&cQsU@V~=f2@M>6BLZcCIS3--X zJ6d#jcR|y=F+%!K2=6YPK$S*R3JtVyLSt2<2GU#!!tr1VZ5QYhRkXC-+DPY3tBsJ?Tz{fYN!p%v$@a8tOH$50KeWB~xpJU)7LlH9|Sa zBFQ~9m>OK~T{gX@;b^3E*&QX6QIlSRNucn@$`w4drlI*nITQ#l*SMzEGz)tvjf>K} zd>hTnpmFZ5VNJa;L)lxyR}xnZ&ybgE$ggUcr+Zrqm#rE#hLOvk>2=NbqDXuudA1D!TFx(MM&`iEX0w5Y2qnTM_gS6@=u`8l?vKl)KYC9iV$CDL}`z(P^lu z-KnWb)KnGRfwIXCQFLgl6di56TpMrLU3m2Y-e_9TlmPG3{~LNUI`aMqz4Iuq+-%!C z^G{VyASTcz5D<5bfHQJ6Wwg2GuGMrVSWV`;)BQ8mWc??qX>&(c|A_F|*^&20R8tM* zweqvuyrI2IK;0IM9|r>Y<0Pe8Ifb zBf=bEbmn%^6Xp-g!mZ$E-rrB%b2BmFr5l$T%2w158d7Wdd~x~n?EV+Z`aCt~$o$2& zl?7{Tix)BZtC+%-3zsY@ShOl6y@*-(L_54a{kZMvf;CGDmKUtbw=MpEeuPyFWM5BV zKTKhV3}myC*`$%|fpqrYN3#A5Hj7~=GVC;Fo2l63H0?IMXj)~S#m(VzxWBW@I4kq| zU@MuyIq&1F4&C->^vyc5jBCKzE}xgn=M@~LWTO5+_D;ectt@bk>9i)1qiQ0VJ z-__{R*%i@tDw_R_fz?ofk(zVGm}ygQB{s8L<8zvFTN_*bniiYc^;2`--6q#UpquF( zkJ&0jnMdf%BcjbC^ybD^N#~AqX(+SNT5B`=qRf4NYW`%QS&BGr_$ECX!oVE`{8gr@ zzc;!fLe!OM%tctI62i=lYJaq_#9aHBnfW%wJD({`VK${Orw5=(3$*lc%%Z=1;# zmyH3sS2iRcG{I)9?5`&eU^x-=ZsNtcXPkIq=2bLg;w5Pe)~!YyW!MT!;}3v7$TB>Lej zD|C*sd{S(Z*yD!Fe1Ervd3gZr>ThvfzZX_11|B!GgxVfPZP)mQSYV}MaYtK?vID}T zUj-y8&Q+WgRj)CZ>Zp@=O~CdjU#{6T*X)~OcIBFVnP%4%vu~K$m1*|%H@k+JeLc;t z{$^i{+0_%i{$oWH+#DmjnU7?4q`*O4s6(~cxCM-YH;FDAx2nmTLWL#M zSW#E1Z;-B^jXoQ59IIOSX1`7boya%i;gVZ}YA80*Kv9zdGr!S2h+NKdUdiL{ajTfg zsU>)zr6Nns%=kgVQgeZtEHfi-r#0ey+{}E4+8tjK$!{lrXF+(YGj5Z7v3TSM^!T-^?M~`WY@W-)p20|`9&mWAlta4kbmEQts%qs)so8Aj7 z3~OOgvafMZM*OT>V=OLEyDp;U==AwAORliwDod`h?l{+VK)+K0ENUhQ+(%J7V;Y995}br84aIaM5g!Ob#~?4&SoSu@q@aT>KIvQ@~mprF)uC#PE^HI29F z7T0pxW^k`Lcq$s1hBXgv%#>o;F@t?WrpnYvHmapQ$P%Z?mok;?GWk-ck`j||=u}c_ zqG*kR-Yuh_m(jnF(J#p8NCo|(jQ*vJ-Xo*SWb{}C{gR9>m(hD=w2PvPjRj#FlBasZ ztJ(5FjJ7|mu?T+z)>jZ1YE-(Fx+Rl*G84I`nSg&flOIja%yfr4|2p%yZ@Jm^H|i;V zEICVIUjp`$$<;hF;J9zd`I+dL)VKN0Yrx7&0qvo{P6P3(-0w2R6mLA#gjs;mSX`^9 z>(J-A)jPv1JD)c1e8#*}WkJ)|Hjea%H=3k~7L`Q`Z%mW=7Uu|0n`@sjyVlcCecGH~ z?R>^8s4V^{c2F{aEl-;pn$r#J7lVE4%>GCO$a-@#Zl$&%>gaNmh2B~6E8IhrhTzU@ zS!otlngfkNO>R=L>&(oFWMLg`5w~c*Rc;aAcW4p9O*3LWqcPl%=6bR<&^W#~`#(B$ z%PciOM9Vreq%?LhXxPQc{1z#U|1~wkt2S!gnN;hx)D)|*)?E9Pxw!y0g=XhdW^#db zqD80?gNz5USyr$(3FaN%Q`QyXctboT{zJ$RKE3QG>vA1RxTuKAJV9jzCyvT zv#uKul_SP#AuK z3&0W55SXN^u2507==82-sHcYXNd`X>2#r>=6=c=yi6LNCNI^(enE@q*X5TmqS#5@B zvc`;1MXkv!WS_~o-{e*m6ODz*Pl3sqshb+p`~wpp@n_;eM=WDw#7HWzB(cn_Bjnuqe6TWOYX%(y!l(E&@q9hO{Dm)k-31 z7afspMbBuJuO)5h!Tu&NMj^An3iUi)emSn$FgNZ=|j7L6UbMr^E`_ubXga^ zVv8@Fr~NXC=d40J=kH#9+!w{2Ot(l{UkrD0q{WXR{X&-2r{+kd$rr(qDid?SWVDj5 zoU6Kl*$5@~*Q8zQ+33&?6M7kYB(V=%Kp?`M{VVs;}976w|RF2W!SjZD6m+d_TCCtw`^W*8cLB$m^G zXi4M{t$n#H8W%=RNVS~AJgB!Y)N*o%N$M&Lvz**%l61mw%SmFA89+4Lg1O_RmVT5q ziLy?olMfZ0e3Mu;{x>0}8!LMg_V8S?}e+fp#E{gX9wt>S5 zjtHLyz0|$Ic5@LAWOk$oy6hPxU56HQap;7o?1F3uX5@QHbyq%%dhrricdV{MndO-8tzCp%<<1UYyXIUVC%i79oL~ zQPR66fh!XAMuo3L+tQnsx)*mUfir4>JF4>cN+|+`LeM6-NXtGvJmoGzdvm$1)qv-= zQkSQoDULuZ_74$KIDcGi3U}O>%$*z|x5dk7ivu`PZSoD`$Uzgsqb&+Xj^5qDKhYNS zD86_)Nky!qI|bnSz0K#lSfopE@?Z-B)R$_J-46HBHZT(LqeTNK(X^l~bg@vEX)SOV z&~ioHwJtF4#?&9?#h(fD_jjv4N#^{$o6o0k(&d&E&Xvpw4Cfla3H>?OAhgXanKtS; zB}0dDEz4R=8#efRR{4fddfz}w?;A|%n~S8spGpB+g{D50E9;qEgeij-#=@k;Y}krc zOIsn6-I7K?|4ii{mBv+fx6^y2>9d|4jC_yA9inR9)lJUQCrxAv8 z_D6UzQ%68wn#`KUVTemoB+$`|9#qI@@)Q=d%SX^=h6_YxT^Pyb6kkZ^s`F8)V=N~} za8d-!OXI8sPmImP-kidG8#6XDeKa=dFslj|E_z<{eRwd7P8uKsbs9_eze;%eL=K~zmZDfa;xccNM&i@rZ&&|pGCZ-C^ZyAKhJkIqXCyz->Ip^cDgnZ6f zz+o8wLFvG;RP#L?na)ARGR`%YqmoRJbWzCXY700rnsbifgr!{V;~dH0oS7U`G)h>> z6{yLR9Lnx|g8D1uUO^beHHJyaK%IYr!@a_job*x4N)9ZIbS^YwBox9TdMAL+5X9)a z=n4l-YW!VftkKFp*j_-m1!Y90x!n37Y977#F%b58r`Yb9E~gOj~#N}Y=lgjvY=gB2yw;=dbG zd?`J&qJja}W;H5oDpxyA@yN))n0pUbJ3TOZkL7L~y+?C&^d7^}o+|?*XSR@uArylN z=71)yEGe5e3}-nhoLBiZzR4Wz1dbcNz;?$W*#gUv6nX=UaX!L*n51SqhtiH=ST-4c z+lYS;hjCjT?x)M$|7~Oaux#gWI(EUXwy|C|*I1Bpx3NA_8SC2ulpl;ym(Em{wfhcn ziw?gwO-2ZhbG1u3W&=aJbo9=(=?b`jtIZFB5!d0vO&0%f#y;AcEcCTBW?($-XKCz; z$$>vZarkqH6|C*d;mBN$c`uF5xPc!BmJ$v7+YD+^m^7Gru6~rowEz<-!xer(dvifs zVZujZVmykYG7V4&>93Co3XnN7Ew05J<+oCPe;dEGt#lHHDGJ8sJ{Cu`Fy1m_2Au0w zIrNN!!9vv>4hvOtIjM^W3t|v%^ymwEky#vXDNvEw)b5~2VGdV|g1{&kqgIR=m2sOf zK{MKm(QXE2!XeI3JQPw`rEd^sa{h#1h|=39+FuyhZ!-9JoTvlo1J8F*B??I)v{=trq zMQ^Qcic%AG6R)afwvFZOGsBx^Ml{hdt$1(`vvsTx-c%dWbaa7t)W>}=KP>(@(Ika8 zG)SKb`lgvNO}X<=#x_Yqjdp#L(T;5O$+#w|4{)_HO%>r%Wc@1Bn}wz_uc^39G!+#Y zzUC7kw8FeIyb0Hu@TOEONAclEqxr@#JH{$n>mM||`H%_OhK8e$lw9DuQi4CC>UNAR zMmrgVtPs(Z8skr?jcO9KP0HC3yFJcf*dEVRj1`WU-ZcpIrZ3{XT4wiHln!(6>aifj zvX~G>&(b$o8Z%t*#2$#leFoV0MvNc&CTC2O$8aE?#5OtOnjoq|%V!1jsU<3`h;HkG zFi}smO_&FgsHUJWqt%L=@~|eSrpZdeo176%*l|Xg!<(Ya09a>~M4mO^fo{B&>d0-a zYS@NZ?W?9QbPm1nn(3U%5rgH9opDV%^+g0_)O=aOtcd;}rL`j02WB?izL6w}1zH2e%hTF|emX zOI2XsI&ekOw^>$9@bpcBrU@>AROW9~y1C1JUTZt$1KdFLLi3&SIu%U38ec3XO9U?d=?3x;Eb3+j1!!_Ch*w!nnkXdoL5a0bKT_p&cw%3*ykqa z7bZTQCc8{&mU7j z8-pP1p&Cz$xW>^~MagrI$1vB|J)R7j;2b=`%2bVK8pgX-+rzg<2t^!owmr*FWUQvf z(cZz0Dc;1!e%@}_GNr=;hT3{g7iF-XONe5u=lVsZThFCLnXTtWN6n(roI5yOn(|Mc z<7#(s&f+$J(f%9HYhafN`q*O{fU)kgEqq%<4s&RHg~k(6 z8dl^9$Emn9EWfBU3~hsAnTz9@?j{ea*h<^HC){E1L^$Z)!mtL1P7HHIH{cXbPZ8`( zKcF!9?nkqm#pvBZel&Bqtz(BDR{s95EE? zNI0u2E|MA?`r9=pIKn;M9T6V=ff#GbNw$@~0f5ce*!MpcFP>~RsAj8qPCi_Jun>Ci*GFHZHjtgRbKbGw~jx8L= zemPF~ImfiN;-;{*)wPW-oANF02y&~{38{Rv+oB{vLb_O-T8kBn*7qtfA<9yV8)jMa z6hS4_ss;MqhK9X9fmhzu5F&+IygP!oH7F1y*omMP&5z|4)>rM z_QWK1<0NJKNdRYHvF_nLL$W*?U2&WNXH*qr*U5P8a2ebx;73k;7Gd0 z7-?(^pc^NhQ6s05QUbX!8P{9M0dAmU6cr(d2c#&{;nZ_qVPuH?=GWK#mI$3H#l(hvLeGi7Auf&WTg|~xrIV)BbnwV)TmM^z0DlAyB z+{7$ewqlhnMi-+Cw*Ag)rw2Mc(CL9r4|IB<(*vCz==4CR2Rc2_>48oUbb6rE1Dzh| z^gyQvIz7P6yX@|;g=x{E9X`(g?vhbn~wV##eY#Yj`$ft0{Qg$J`Vxf`BURtUxy5kIEfr{Ep3kA|Y zErfM)1>JVSjxcI$L-5aPd+LdW%NH|aMvr*(=|UUxxGG$$S4Tv}ghlFNHC>|PR1vy3 zb!4+wJgC4sD(5K{=`MWz@Nzl9SQClseaXnzSD1?i>54 zPG>AAa)k4T3$V`-kjwx?D?_4~BuW+pwv%ObAX{K%o}rXMcsu2|4wSOMAh4Y*Xh~2n z1fE*>-)tdE1^#zhczvN2AGFFj>?)!T<#mn(Y~bw9Y<<+z-H1;_c5h@#zB!g4{T*-f zDt-=>PR`{7y^gnHldSpG&1VcHA3J=!;-e;nZwvbw(hXiWAu+= z-dp4`HkwPqc(bDy_MnPA9%?vT+5_*QUN(Aq*m^m7czU8PdwPsnU-WVqr4Ovky2Wk- zUJK5`S0lf}_r$FNTbCL~*g9brz0Rr==F#iCI^jWjeXvfLS(kc~%&Ze;)9dUyVLrXi zucI3lXV}PnHp1G-92=Qy6Ij~}!r6rTY)eU=O_+hpOj{mjTb^fIKGRk^$0p3R`QwC{ z_-gS!ArEH{8)=iXZaKSOm}AqCS(I!jnQd#CWkW${+l-K8pJjtGNVd?0?04)pQm*7-B4 z^YM{pPj92GPqookP-n-tnhWZhX$s$KE~qO>Xg<^58y=!u%6gZQM8`$)ww_|1uKZkLirF}alYcZ zpPs0TNOUCPbqY_Sb!pVu$6kM|)-iGUw<|r|Wz+1~Ih)EO@^>aZ`|-h_oH^NRYVpWD zPma5%wC?DQ56;#7#WVi?cYgBF#rTW6r)=GSF!7N;f7GRVPuFLEq8>EL@#cK}hTpAx z^QVQIo5Wk?rz0!hF27j#d3nLxU%#2XJ??GSu|fJ@pBQm&_=*{Y+o~6M`O|Bz7yZYc z$+PEFjXGxcYKAnt_0XpGj-84A_Rp22*^$Q=%}z*7eQd~UI!9Wc{#i4J+i%Xzis@$4 zt(v!HsqXbH?=+_EUOC+1eyjP{RlS$o_s)Ggo_KHB?0p~ZZtnGOWs!p}#b!DTODmq} zwg2rN{l+cR4#`=|W_aJI8=t6uCGVr^89&*3oXbyJ@onL*tu0S^7OmLU@KnOi_rkO# zZ?=B7f7Y_E-n_o=>ai!E`UbJ+%f}ZFeQ)QxOrr1IeAi^r*O>2`D*8_5yY3ZzALhGe zh`vAPyYfWe$N8=~BHi-+K7#3QhsbA#tU{)U6tbTkiv8Q6wA;^a724NUaG0fQZq@lr z5x)(9FX_pU62J0E#7wa<3twaS>=6CZA!NQS;+uw6`pLx*=8kAsjqg6bbI4~BUB?dj zCX24)hkR2-*QrCkdqvkD4*6z?u75k^%M)GyamY7EbTu84t;X*Y__l&iw&*f%aCG%_ z%{mvR(^WH1E@N7j(Oegr+q{g~wal%m)486J%X@`l;sSJdBT?mhMtmWb%De(XiZryb zdAcHro=K;oiW-iFOP9_~mOmm=%O4e~qt_(4P@!4flWaYk3^2=gXit-TXJ7Op_w0sbDf1B(vcIIO$ zOeWZ~cSoW}pSI}j?B|*Lo zqJ4`<9ukH1qJ6VS9u$RjqJ5J{9uS3RMEgdO+%GEr!Pfht$C>Z#n5+9>sV-oDAR8=> z)#!<_#p6{Hz0Kgzd!lV|j%bh07VFTVc2Qw#6ur#S6*T)otZz-AT;DLikU{ffLx6tf zcn~N%_VVpQcHAkXB1qpZ+0T3y1l~=^Pwo`5K1d%bQ^*rR`cNTn27zrt z5HypPB2aM6ZJo4Sq~)hE^d)T#_luJrrZ`+5kuko9gDFb$DTci9qWvL}j2DG0(f*)F zvP5B=Xn#N?<3wSsXun@1V@0DdNwoh|B$Gs8qG(?rl8K@)LA3uwBojozB-$Sqi3vm& z(Y{b57Ev&ZcB@FtqA*3Y|4bxPL}9XMe?%mcK{Q>o+e9*56z&o2i$!veC`=RWi$pR_ z6sC&yM@2FfM7g59KqR@MkR#gjMUo>5*`ob1kz|X)y`p`INbUvEOwn!^$xKmTMf)<5 zu%d9EXn$NJ_ld#`(Y{n9Ge9(3v_C15*`hE@v_Bz|S)#y+_7x)GL?KVKFBeH3h~|s- z)gqZM3iCvJp-AS5!d%h5N+fedVUB2DDUvy&^xA>Ld5O4=oh4}CL=fDdUYy00-WKG~tD9V~PWIZTW34IGtxm{QYwA(hbQPcy?$ zyUxBM{AQ&;)!DQXFT(w1r8lYGyV7x^UR>$8x%2GGiaz2Ex9X>G)!A1nR1NHVD=Q+4 zr0=P?xA65)h_kK4HT$qt$Ucla>F~ph32hz z`AxG53nMpzUznD@wpJvG!%NeP4A-q#GfASU8&)QF6@Il!Rhmv$jogel{K>S^bliAc z@kTHQR+SjCWN7;Jnpb7;Wvc=helbX9Q25fHZli#j^p9&^E$Je4DKcE5?{4Asy}QbC zjP?vC^9#wMLb9TeY$znp6_Q^Tl6{3lEcA>Z_ZO1Ih2)7svayg97m_`NWPc$!ROm?~ z4;7Lnh2-%g`}>K{I;+nslK(f`k~g=!J{2xil>h$Yi$KUz83xWVhk^0-2;(z9=9&v z!9{6h_9J+Y7=2vE{tR)rq+#D$O_m*DPU`vdMq37%wwmOwCPk}>b2X3KJ95uzGGjG) zZZ+Apn!i8^*wtjtYErtI{8Ay9xth#fO?Iy)dsahe=7G#N>__|)>s5Pk&t9)`;fDP{ zrqOp^zSBNm=er=^XkVx+>E+2V+A{H(!0Iz*uRaSD1=PxBum9|Sj}T90BZC>|@YQEC zJQ;HG-wT<)tcL5)*U=!n#{6~_BGNJ1t$IWMs(*C7szNsT*D%>A%(K}>J4Orhv6SJE z?YE7Fb)HOQN27g|oM4Bqp~aHHbp1jB*Lg_@fpns4^?kz$CWOyJz+>Y-2en(F@C>SWG)*?`pDEawXLe|RhQM8sg*D8@wvX;E4 z087`BU#`UuP2v6M9;oI5bs#PfZNFQ)C+otpBh_L;N&NTn!j9PEdwE@lUAy*s)ypUr z1^kK@Y@RJ`zD@RR;$35iSJ@K%gAZg>6$c_|k8#8KI@kHS5(DnIywPnzMPn)u4)?5; z!yyTgp;{X(oPRvEv`S2nwL`ThLx<&|V9++x6qbkXDmWNipRBzjxZL+3q&Fx`I%&>?8BMs=ai*CN}{Qdgn{3F!PX9Yk$t zsdI&b4`k55aS90PLCi+{2Ed&GsCS3mI$tNr5q3q02@=Y>5TJsri$JK}tqCEF4H5!B z&j!5kH8qNSO$o!>32$#>o^&#Ioo5u8w~im~NatU5jAEv&LUK5gBX~9dI{azg382P^N;afuPr*4yYQSUi%?+3U$yDPBn&5<*#)1xgsga#xY(+pRc%rxl|^MV=2>p$E$m#WQ-a90~CZ z3|}0|q6f>7+YP^~$Pg=U^Hq(aQkc8WkuJLiB5?n@IF)JS{j4r)2?OpcFd;Nie7! zS$vXXJl5e&2gdtOygJzA598I<)3df`A<>%S7%j&m4G`*d)RSj4Bdy8@oDFz%X9%2e ztiRVJ)$yAhreos`g6DrrNSkDR(B!AvjG#WJ!4gEhRY=Ppj9>*<6JY!4D*i+uxouTj zoG$~2wlp-W6Ky4E11$@9ix!Huq2ZvW;Q#>?Mg0&cuZ*ICpoR^|1Qi<)P!llpe82*< zo#6r0iI6e`LhXD&wjL-NQq)LVkB}WuOYQAQwKG5kwL297K`L6|0Xu0Mr!s<|V5tJl ze>hMy$cG1tko8m>^>D=p)QBJz+g2K@TvEz;KEOF;6|-_3<5)+hwF63%+orWh-M~tX zIN-xUQUv|wkjt3bg5=3Tl<2BFj19#!7NCpmpj@?MD7bi4`~b>xy-^jI5D!>dI)E<3 zTPrX)myOs|gvT0);Qiyp0VHJ;22y4d@}~VJaghHhlDbJq*<`e*Zc-kl45itu3#pre zk5Yzi^7jZnN*T5(@F->YCid%%!G|dnDnCpazR5G*Xfste8+{`-*@ta%jo5UYriRm0 zNzCzd9dekejo9S<_LwOhzp%CO*mzuaY@}0(fBs;yHhP9iBRm6gJOgt)gK|8Bb3Dn8 z0iGd_fu0n{AWy1eFf(*xzGHB{V^F?hV7_BO$*_M&eBW)vR^~p)_y1#-tlL>C7*xTQ z&9n+s#Jw5!ru#Md%T^&>Wo*tglQ6@I-rN=<&fS=h2!qK{{`ZvHT( zq^}YY-=OvZxR_G56~Zoq#t?bveR3YK>9CumD#Y7 zQEkS2mSk@wIa@u+Ii4Xo`~pWZztu5>>9G;M8vx%8_`QK7vk?zcUf;^U(U#A5 z49RySw`6a{1H7EA5HkQ`1~R$Gvm>`cMo50bR$<-NRJAm{jcoZw;dMa#y`f=%EOVe^ zNL%G)ZDb$Vf*yIOXXp%imsh`c>(a8lTM_J+S!Rn+uE+|ifULhLvfjA6tbMo1dIPfF zP-HFJ5;O?*4ubtOZc0+yd~)ALY91{Y3NGW@9CC6Kdu?;DlweS>LC=8^w#bZo1E`?q zWJXl--mR!Rw0d+B^=|~z^f%PR0g|raXp(eQ9MGty52gMKRr^1hgnqpU1#J785iKl* zg&v`>H_m=1RQ7y%+qd<{-?_PH@N7!=5BP6Bd@S>$YSZ&yzVelC{+d18->~(K^q!dO zy)xm{#K}YcV7>40o*#bm*WpvMuf1{hi$(j*->lVa*fQd)F%_47J%0K}d)|4G`)ump zZuQ<&?)>tr8SCR-?lJ7tm=Ki30j{VDvj4*ZhD`apuY z>0tJ6R)!orAy<0B9vCs`SY(y^oNeeUtsAB2il1TNIK2%62R^y(2%_W;Wt(F#KM{SS z1HE(Xc0Lhuzy;7CfChs^L7m)m2a0w>l*_(J7U+=sbcJhgyFl8tQ55Z=sK7VcxoGDV ziVEt2z=Dm6CD3p zAg+h9y9FZbSkM8rzTL#V0o3{q?g((vx=id)Fw_d=3RW|%umu5u?R808QJ-M#(G1q} zwuiK;JA4yj)5L)EyV#W2!Q406OR^xqwIEQE5P|ChsJ$IfF!zlB>Wu*EbXyg*#n#2v!K9gU1%!+u)~ zio`%5TL1Zxf%cq=(P}-K&qfWi$Pz&S;0(0n2d?xTMNH`+*{(e(1gAf>m zA%sppy@UC$+bN8}OelqhT?Ii*#P%Q(g$@ogSa3+@>tSn;jg-KthyrvFpo1|gwGn!6 zBMew|H$th6Q09FWEtUF;Moo{62%rJ5oQ@)GAg^NrlucwlEq4FRcY2%4UynUeJOje| z+j(aNKq0(RD6hZXmAH-WwA#1>8?7K|Jc|&IbH!xV%3RN&JAg`Sk`=Tr%i~2ubyz ziDa0Ktk#LMV1E5%QoX-tNq3~OE+o}gqtZMDa-3@rjx3XtU(=L-=SmKf2w2059s(hprg!5A9~TUDUaXeWO^KVt`sf1xHJH4 z+=8FX+UmzmEf4_9CIv|7KEW{M2h(M6aVfR;xl#((1mH;9pDU%C3GKzDj+<8IH%{ia zPBv~A$=K!0+_mldBJsw@t*ufdercWVyu*FY9Bd@qjZJaCamr5rSt%GiH^`1s&rPzk z^fqvo=f;H{rGx-=VP`3EmKp_83VnbG&QfTkAlU-45Hu9>%y$ve!b&+h4k3>(v=ET?X2C~Z@~!v9Y*{ZZf3Vq1Vk(yB!+I!IW;LvEnhV1 zF2lBjs|LuKa7|d?s`=*!lTHbp9_aKyrw2Mc(CLBys~*75!K)57Mq{M?_dN|b>Qx7& z!C`La>INb}^(CIz;+ya7Kc9(T+!4E5L^~aOq@&@)n3JInHNm+9x)f_HW zpibMx<5nUt(PQf6S(4{g2v(i}&QcKg0S?3wt)+*{h46#;c~ChOaZsL60v35tfpj@Y zc9ip46yPALdq=tBmh2klkEDchU_U1Ii80uJiR~)#&QITha(Opqf9%Fok$oceKjN3g z_hWx%KWyK`>j`w%kW4O|tt4AT;mb<$tccZsZ6bcS^DU7#VD~2I=*Gngk;mj)A$~Cr?%rx7|!UFu3_`*Wl{rH9PB~N!~r4W@<={oT$ zy>Yy`0Xh9^uKY^m=-WDNn3e00E2?yzm+2fd2f8Ymu7kjk?B9Rj2qEcA{{QHKO{y&# zhjvHY^G4S|KK->R@6Gyn(S@fpyZTgR{%-b}6|K7%aoV5rZ|oX)Waej28!D&#`x8<7 zCVjLj?qJRr&-Oq1v({gk|N89UhR5SxdGPyvx&B{H`LbmEXC&*hT@#vLn0B#(YyFKi z{&%ZW{<3S@HDOUN>E~0fdJ6`AvS)7G`$f|=Z*QK``qpNP=G~(E;{LejrvuM<*G}mo zl`R@^?RUF=l5+WXrPh(HA5=U!t$Xw9yB1}|d{$rb*p#0B#(f(e9256dgXh_W=A_n7 zkN)~uTh6e!Z$I+wd+vz`rVKG$|Lo+EeY@7>&!0AmiEI7(%!j|Lt1931>{Iqd56#M+ zGA=!JU|(amxM*#JMy+a9O_?%z@}x-l#*7|4YE*i9 z`pA)KX(L9A7(RU1uwg@orlzK*qzoA{BsqET;K73i4IDUdz<>e$`}b#;am3>=7z}arh{X|uLr;%r9C)8w zr;DP8RzAAON2Gj2C`Y(*Xo5#r+o29U{*#byRVA}4_q_SJF8%)X?uM2GlXZK|AFlPg zXKBf+r^8b63ST;URzLc|jr))LdrW+^sP<3aG50RpeWWRT=&Ut+-)WA`czE-H6WOTEoYb{_iZ)}R?ry!gi7v}yC6sW^7N zYt|#%s--K5Q}dmN|KtC48oU zbb6rE1OEd(@cM0gH2(({u(L>=9_aKyrw2Mc(CLAHg$I~_h0so!ogN7Fz=hBkXYNdI g3Z?B#|DX3j)c|J5G&Up!KaXaE2J literal 41278 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 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 8/9] 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 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 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 9/9] 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}")