nx_tls13_ch_adversarial_test.nx source
↩ module page · 145 lines · 5784 B
1// nx_tls13_ch_adversarial_test.nx -- Phase A2 of S-class hardening.
2//
3// Adversarial fuzzing of tls13_client_hello_parse, the parser exposed
4// to every public-internet client when the substrate serves TLS.
5// Top-priority hardening target for the nishifamily.com goal: a
6// single malformed ClientHello byte stream from the wild internet
7// must NOT crash, corrupt memory, or false-OK the server.
8//
9// What this smoke proves: parser returns structured verdict on every
10// malformed input class. No buffer overruns, no infinite loops, no
11// false-OK on a manifestly-broken ClientHello.
12//
13// Per the S-class honest audit
14// (docs/NISHI_3D_PRINT_SCLASS_HONEST_AUDIT_2026_05_20.md):
15// Phase A1 ✓ STL parser (commit prior)
16// Phase A2 -- this smoke
17//
18// expect_exit: 0
19// license_tier: ORIGINAL
20
21import "nx_syscalls.nx"
22import "nx_tls13.nx"
23import "nx_tls13_hello.nx"
24
25func ch_parse_call(buf: *u8, n: i64) -> i64 {
26 let pv_box: *i64 = sys_mmap(8) as *i64
27 let ro_box: *i64 = sys_mmap(8) as *i64
28 let sl_box: *i64 = sys_mmap(8) as *i64
29 let cso_box: *i64 = sys_mmap(8) as *i64
30 let csl_box: *i64 = sys_mmap(8) as *i64
31 let eo_box: *i64 = sys_mmap(8) as *i64
32 let el_box: *i64 = sys_mmap(8) as *i64
33 return tls13_client_hello_parse(buf, n,
34 pv_box, ro_box, sl_box, cso_box, csl_box, eo_box, el_box)
35}
36
37// Verdict is "valid range" if it falls in the sealed HELLO_VERDICT set.
38func verdict_in_range(v: i64) -> i64 {
39 if v < 0 { return 0 }
40 if v >= NX_TLS13_HELLO_VERDICT_N { return 0 }
41 return 1
42}
43
44func main() -> i64 {
45 // ===== (a) Empty buffer (0 bytes) -> TRUNCATED =====
46 let empty: *u8 = sys_mmap(4)
47 let r_empty: i64 = ch_parse_call(empty, 0)
48 if r_empty != NX_TLS13_HELLO_VERDICT_TRUNCATED { return 10 }
49
50 // ===== (b) 2-byte buffer (header truncated) -> TRUNCATED =====
51 let tiny: *u8 = sys_mmap(4)
52 let r_tiny: i64 = ch_parse_call(tiny, 2)
53 if r_tiny != NX_TLS13_HELLO_VERDICT_TRUNCATED { return 20 }
54
55 // ===== (c) msg_type != CLIENT_HELLO -> BAD_HEADER =====
56 let bad_type: *u8 = sys_mmap(64)
57 bad_type[0] = 0x02 // ServerHello, not ClientHello
58 let r_bad_type: i64 = ch_parse_call(bad_type, 64)
59 if r_bad_type != NX_TLS13_HELLO_VERDICT_BAD_HEADER { return 30 }
60
61 // ===== (d) body_len claims more bytes than buffer -> TRUNCATED =====
62 let lying_len: *u8 = sys_mmap(32)
63 lying_len[0] = HT_CLIENT_HELLO
64 // body_len = 0xFFFFFF (24-bit), buffer is only 32 bytes
65 lying_len[1] = 0xff
66 lying_len[2] = 0xff
67 lying_len[3] = 0xff
68 let r_lying: i64 = ch_parse_call(lying_len, 32)
69 if r_lying != NX_TLS13_HELLO_VERDICT_TRUNCATED { return 40 }
70
71 // ===== (e) legacy_version truncated (claim 100 bytes, only have 5) -----
72 let trunc_ver: *u8 = sys_mmap(16)
73 trunc_ver[0] = HT_CLIENT_HELLO
74 // body_len = 4 (so 8 total bytes needed), buffer = 5
75 trunc_ver[3] = 4
76 let r_trunc_ver: i64 = ch_parse_call(trunc_ver, 5)
77 if verdict_in_range(r_trunc_ver) != 1 { return 50 }
78 if r_trunc_ver == NX_TLS13_HELLO_VERDICT_OK { return 51 }
79
80 // ===== (f) session_id_len > 32 -> BAD_HEADER =====
81 //
82 // Layout: hdr(4) + version(2) + random(32) + sid_len(1).
83 // Put sid_len = 50 (illegal per RFC 8446).
84 let big_sid: *u8 = sys_mmap(128)
85 big_sid[0] = HT_CLIENT_HELLO
86 big_sid[3] = 100 // body_len = 100 (truthful within buf)
87 big_sid[4] = 0x03 // version high byte
88 big_sid[5] = 0x03 // version low byte = 0x0303
89 // random[32] -- leave zeros at offset 6..37
90 big_sid[38] = 50 // sid_len = 50 (invalid, > 32)
91 let r_big_sid: i64 = ch_parse_call(big_sid, 128)
92 if r_big_sid != NX_TLS13_HELLO_VERDICT_BAD_HEADER { return 60 }
93
94 // ===== (g) cipher_suites_len = 0 (invalid: must have >= 2) -----
95 // Layout: hdr(4) + version(2) + random(32) + sid_len(1) +
96 // cipher_suites_len(2)
97 let zero_cs: *u8 = sys_mmap(128)
98 zero_cs[0] = HT_CLIENT_HELLO
99 zero_cs[3] = 100
100 zero_cs[4] = 0x03
101 zero_cs[5] = 0x03
102 // random 6..37 = 0
103 zero_cs[38] = 0 // sid_len = 0
104 // cipher_suites_len at offset 39..40 = 0
105 zero_cs[39] = 0
106 zero_cs[40] = 0
107 let r_zero_cs: i64 = ch_parse_call(zero_cs, 128)
108 if r_zero_cs != NX_TLS13_HELLO_VERDICT_BAD_HEADER { return 70 }
109
110 // ===== (h) cipher_suites_len walks past buffer -> TRUNCATED =====
111 let cs_overflow: *u8 = sys_mmap(64)
112 cs_overflow[0] = HT_CLIENT_HELLO
113 cs_overflow[3] = 60 // body_len truthful within buf
114 cs_overflow[4] = 0x03
115 cs_overflow[5] = 0x03
116 cs_overflow[38] = 0 // sid_len = 0
117 // cipher_suites_len = 1000 bytes (way past buffer)
118 cs_overflow[39] = 0x03
119 cs_overflow[40] = 0xE8
120 let r_cs_over: i64 = ch_parse_call(cs_overflow, 64)
121 if r_cs_over != NX_TLS13_HELLO_VERDICT_TRUNCATED { return 80 }
122
123 // ===== (i) Extensions list_len walks past buffer -> TRUNCATED =====
124 //
125 // Layout: hdr(4) + version(2) + random(32) + sid_len(1) +
126 // cipher_suites_len(2) + cipher_suites(2) +
127 // compress_len(1) + compress(1) + ext_list_len(2)
128 let ext_over: *u8 = sys_mmap(80)
129 ext_over[0] = HT_CLIENT_HELLO
130 ext_over[3] = 76 // body_len
131 ext_over[4] = 0x03; ext_over[5] = 0x03
132 // random zeros
133 ext_over[38] = 0 // sid_len = 0
134 ext_over[39] = 0; ext_over[40] = 2 // cipher_suites_len = 2
135 ext_over[41] = 0x13; ext_over[42] = 0x03 // 1 suite
136 ext_over[43] = 1 // compress_len = 1
137 ext_over[44] = 0 // compress = 0
138 // ext_list_len = 1000 (way past)
139 ext_over[45] = 0x03
140 ext_over[46] = 0xE8
141 let r_ext_over: i64 = ch_parse_call(ext_over, 80)
142 if r_ext_over != NX_TLS13_HELLO_VERDICT_TRUNCATED { return 90 }
143
144 return 0
145}