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 25bc431..4eb9506 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 @@ -112,7 +116,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 @@ -121,13 +125,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. +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 suites in parallel (5 VICE instances, ~2.5 min wall time) -python3 tools/run_all_tests.py --workers 5 +# 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 # Individual suites python3 tools/test_net.py # 60 tests: ip65 integration, ZP save/restore, ring buffer, TCP recv callback @@ -139,9 +145,47 @@ 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_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) + +# 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/build/c64-https.prg b/build/c64-https.prg index 3678495..1b33b38 100644 Binary files a/build/c64-https.prg and b/build/c64-https.prg differ diff --git a/build/labels.txt b/build/labels.txt index 65a4af0..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:1da4 .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:0ddc .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:0d89 .cb_remaining -al C:467a .sha256_h0_init -al C:5a0f .fp_wide -al C:65c1 .ec_scalar_mul -al C:1de8 .tls_c_hs_secret -al C:1355 .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:1395 .tls_build_client_hello -al C:4686 .sha256_h3_init -al C:1cff .tls_compute_finished -al C:98f5 .sha256_block -al C:41f1 .chacha20_encrypt -al C:1e08 .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:1909 .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:0c62 .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:1254 .tls_record_read -al C:45b5 .aead_setup_chacha -al C:6660 .ec_jacobian_to_affine -al C:7070 .ec_t5_384 -al C:0efa .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:0eb7 .tls_close -al C:8edb .tls_ecdhe_pubkey -al C:5ede .ec_gy -al C:1849 .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:1e68 .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:16c5 .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:0d42 .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:0c7b .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:1d5a .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:1708 .tls_transcript_save -al C:3f2c .rotl32_4 -al C:a142 .ev_point_save -al C:1216 .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:1db7 .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:1de0 .lbl_finished -al C:595f .fp_mul -al C:0e60 .tls_send -al C:1728 .tls_transcript_init -al C:1dab .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:0d6c .cb_copy_byte -al C:1d84 .empty_hash -al C:0ebd .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:1679 .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:0d23 .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:1973 .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:16c4 .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:0d85 .cb_done -al C:4cf4 .sha256_rotl1 -al C:1742 .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:0dc6 .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:0dd1 .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:198f .tls_derive_handshake_keys -al C:4311 .poly_prod_lo -al C:4d39 .sha256_rotr13 -al C:4888 .sha256_process_block -al C:1684 .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:0c85 .net_tcp_send -al C:4471 .poly1305_update -al C:1004 .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:117a .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:1da4 .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:0d8b .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:0dde .net_send_len -al C:155e .tls_parse_server_hello -al C:9935 .sha256_w -al C:5b63 .fp_rem -al C:9043 .tls_rec_buf -al C:1287 .tls_recv_record -al C:9375 .hkdf_prk -al C:9dff .fe_tmp4 -al C:1ddb .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:1393 .tls_recv_count -al C:0c53 .net_dns_resolve -al C:3e5c .rotr32_8 -al C:17a3 .tls_transcript_hash -al C:9a57 .hmac_key -al C:0bea .ok_msg -al C:0f54 .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:0de0 .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:0d53 .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:137b .tls_record_recv_and_decrypt -al C:901f .tls_app_read_iv -al C:0d59 .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:1705 .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:1706 .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:1989 .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:1707 .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:10b0 .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:0d2f .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:1e48 .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:0ca9 .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:10c8 .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:107c .tls_build_nonce -al C:8efb .tls_server_pubkey -al C:18ca .hkdf_expand -al C:70e2 .ec_mulp_384 -al C:1dc3 .lbl_c_ap_traffic -al C:1dcf .lbl_s_ap_traffic -al C:0d42 .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:0fcc .tls_send_finished -al C:1258 .tls_enc_aead_len -al C:0ba3 .tls_fail_msg -al C:1b59 .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:1392 .tls_recv_state -al C:a202 .ev_point_save_384 -al C:592d .fp_sub -al C:0d48 .cb_load_len_hi -al C:3d95 .http_version -al C:1dde .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:1e28 .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:0e8e .tls_recv -al C:5d2b .fp_chk_one -al C:5f5e .ec_p2 -al C:0cb3 .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:125a .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:0d64 .cb_loop -al C:1370 .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:1861 .hkdf_extract +al C:523a .fe_cmp_p +al C:1947 .hkdf_extract al C:804a .der_read_length 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/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/src/net.asm b/src/net.asm index e961dba..d87d5fd 100644 --- a/src/net.asm +++ b/src/net.asm @@ -57,7 +57,13 @@ net_poll: ; Output: C=0 success (IP in ip65_dns_ip_addr), C=1 failure ; ============================================================================= net_dns_resolve: + pha ; save A (hostname lo) across ZP save + txa + pha ; save X (hostname hi) across ZP save jsr net_save_zp + pla + tax ; restore X + pla ; restore A jsr ip65_dns_set_host ; AX = hostname pointer jsr ip65_dns_resolve php @@ -94,7 +100,13 @@ net_tcp_connect: ; Input: A/X = pointer to 4-byte IP address ; ============================================================================= net_set_tcp_dest: + pha ; save A (IP ptr lo) across ZP save + txa + pha ; save X (IP ptr hi) across ZP save jsr net_save_zp + pla + tax ; restore X + pla ; restore A jsr ip65_set_tcp_dest ; AX = pointer to 4-byte IP jsr net_restore_zp rts 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/_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/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/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 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/run_all_tests.py b/tools/run_all_tests.py index a881549..cf5403f 100644 --- a/tools/run_all_tests.py +++ b/tools/run_all_tests.py @@ -2,19 +2,21 @@ """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__)), "..")) 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") @@ -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,36 @@ 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) + + 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}") + traceback.print_exc() failed += 1 duration = time.time() - start @@ -95,72 +101,68 @@ 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", + "x25519"] + if not skip_slow: + suites.insert(0, "x509") + + 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 " + f"{num_instances} concurrent VICE instances ===") + + 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: + 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])) - config = ViceConfig(prg_path=PRG_PATH, warp=True, ntsc=True, sound=False) + return run_test_suite(suite_name, inst.transport, labels, seed) + finally: + mgr.release(inst) - print(f"\n=== Starting {workers} VICE instances (staggered 100ms) ===") + results = [] 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 = 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) - if grid is None: - print(f" Worker {i}: FATAL - menu did not appear") - sys.exit(1) - # 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 + 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() @@ -169,18 +171,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) @@ -191,7 +181,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}") 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_dns.py b/tools/test_dns.py new file mode 100644 index 0000000..5817a9b --- /dev/null +++ b/tools/test_dns.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 +"""test_dns.py -- DNS resolution tests for c64-https. + +Tests the net_dns_resolve routine over real networking via the TAP interface. + +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_dns.py +""" + +import os +import subprocess +import sys + +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 + +# Scratch RAM locations +HOSTNAME_ADDR = 0xC000 +TRAMPOLINE_ADDR = 0xC100 +CARRY_RESULT_ADDR = 0xC0F0 + + +# --------------------------------------------------------------------------- +# DNS resolve helper +# --------------------------------------------------------------------------- + +def build_dns_trampoline(hostname_lo, hostname_hi, dns_resolve_addr): + """Build a 6502 trampoline that calls net_dns_resolve and stores + the carry result (0=success, 1=failure) at CARRY_RESULT_ADDR. + + Layout at TRAMPOLINE_ADDR ($C100): + LDA #hostname_lo + LDX #hostname_hi + JSR net_dns_resolve + LDA #$00 ; assume success (carry clear) + BCC +2 ; skip next instruction if carry clear + LDA #$01 ; failure (carry set) + STA $C0F0 ; store result + RTS + """ + dns_lo = dns_resolve_addr & 0xFF + dns_hi = (dns_resolve_addr >> 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 skip_if_no_network(): + sys.exit(0) + + # Late imports -- only needed if prerequisites are met + from c64_test_harness import ( + Labels, ViceConfig, ViceInstanceManager, + read_bytes, write_bytes, jsr, wait_for_text, + ) + + passed = 0 + failed = 0 + mgr = None + inst = None + + 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 ===") + + 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}") + + # ---- 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_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_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_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_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() 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() 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_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() 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)