nx_x509_san_test.nx source
↩ module page · 259 lines · 12049 B
1// nx_x509_san_test.nx -- KAT for the SAN extension walk + hostname
2// match primitive. Exercises every verdict path:
3// - exact dNSName match
4// - leftmost-label wildcard match
5// - wildcard rejection for non-leftmost labels
6// - bad pattern (non-leftmost or mid-label '*')
7// - cert without v3 extensions -> NO_EXTENSIONS
8// - cert with extensions but no SAN OID -> NO_SAN
9// - malformed extension SEQUENCE -> BAD_FORMAT
10// - sealed verdict gate
11//
12// expect_exit: 0
13// license_tier: ORIGINAL
14
15import "nx_syscalls.nx"
16import "nx_x509.nx"
17import "nx_x509_san.nx"
18
19// ---- Builders ----------------------------------------------------
20
21// Write `n` bytes from src to buf[off..off+n]. Returns n.
22func write_bytes(buf: *u8, off: i64, src: *u8, n: i64) -> i64 {
23 var i: i64 = 0
24 while i < n {
25 buf[off + i] = src[i]
26 i = i + 1
27 }
28 return n
29}
30
31// Write a single dNSName entry: 0x82 <len> <bytes>. Returns bytes
32// written = 2 + len.
33func emit_dnsname(buf: *u8, off: i64, name: *u8, name_len: i64) -> i64 {
34 buf[off] = 0x82 as u8
35 buf[off + 1] = name_len as u8
36 var i: i64 = 0
37 while i < name_len {
38 buf[off + 2 + i] = name[i]
39 i = i + 1
40 }
41 return 2 + name_len
42}
43
44func main() -> i64 {
45 let buf: *u8 = sys_mmap(1024)
46 let cert_raw: *u8 = sys_mmap(256)
47 let cert: *X509Cert = cert_raw as *X509Cert
48 cert.tbs_off = 0; cert.tbs_len = 0
49 cert.serial_off = 0; cert.serial_len = 0
50 cert.sig_alg_off = 0; cert.sig_alg_len = 0
51 cert.spki_off = 0; cert.spki_len = 0
52 cert.sig_off = 0; cert.sig_len = 0
53 cert.pubkey_off = 0; cert.pubkey_len = 0
54 cert.pubkey_alg_off = 0; cert.pubkey_alg_len = 0
55 cert.validity_off = 0; cert.validity_len = 0
56 cert.extensions_off = 0 - 1
57 cert.extensions_len = 0
58
59 // ASCII hostname pools (kept as small inline arrays).
60 let host_exam: *u8 = sys_mmap(32)
61 host_exam[0]=0x65; host_exam[1]=0x78; host_exam[2]=0x61; host_exam[3]=0x6D
62 host_exam[4]=0x70; host_exam[5]=0x6C; host_exam[6]=0x65; host_exam[7]=0x2E
63 host_exam[8]=0x63; host_exam[9]=0x6F; host_exam[10]=0x6D // "example.com" (11)
64
65 let host_foo_test: *u8 = sys_mmap(32)
66 host_foo_test[0]=0x66; host_foo_test[1]=0x6F; host_foo_test[2]=0x6F
67 host_foo_test[3]=0x2E; host_foo_test[4]=0x74; host_foo_test[5]=0x65
68 host_foo_test[6]=0x73; host_foo_test[7]=0x74; host_foo_test[8]=0x2E
69 host_foo_test[9]=0x6F; host_foo_test[10]=0x72; host_foo_test[11]=0x67 // "foo.test.org" (12)
70
71 let host_bad: *u8 = sys_mmap(32)
72 host_bad[0]=0x68; host_bad[1]=0x6F; host_bad[2]=0x73; host_bad[3]=0x74
73 host_bad[4]=0x2E; host_bad[5]=0x62; host_bad[6]=0x61; host_bad[7]=0x64
74 host_bad[8]=0x2E; host_bad[9]=0x63; host_bad[10]=0x6F; host_bad[11]=0x6D // "host.bad.com" (12)
75
76 let host_evil: *u8 = sys_mmap(32)
77 host_evil[0]=0x65; host_evil[1]=0x76; host_evil[2]=0x69; host_evil[3]=0x6C
78 host_evil[4]=0x2E; host_evil[5]=0x66; host_evil[6]=0x6F; host_evil[7]=0x6F
79 host_evil[8]=0x2E; host_evil[9]=0x74; host_evil[10]=0x65; host_evil[11]=0x73
80 host_evil[12]=0x74; host_evil[13]=0x2E; host_evil[14]=0x6F; host_evil[15]=0x72
81 host_evil[16]=0x67 // "evil.foo.test.org" (17)
82
83 // SAN dNSName patterns.
84 let pat_exam: *u8 = sys_mmap(32)
85 pat_exam[0]=0x65; pat_exam[1]=0x78; pat_exam[2]=0x61; pat_exam[3]=0x6D
86 pat_exam[4]=0x70; pat_exam[5]=0x6C; pat_exam[6]=0x65; pat_exam[7]=0x2E
87 pat_exam[8]=0x63; pat_exam[9]=0x6F; pat_exam[10]=0x6D // "example.com" (11)
88
89 let pat_wild_test: *u8 = sys_mmap(32)
90 pat_wild_test[0]=0x2A; pat_wild_test[1]=0x2E; pat_wild_test[2]=0x74
91 pat_wild_test[3]=0x65; pat_wild_test[4]=0x73; pat_wild_test[5]=0x74
92 pat_wild_test[6]=0x2E; pat_wild_test[7]=0x6F; pat_wild_test[8]=0x72
93 pat_wild_test[9]=0x67 // "*.test.org" (10)
94
95 // ---- Phase A: standalone wildcard_match unit tests ----
96 // (No cert involved; pure function-under-test.)
97 if hostname_wildcard_match(pat_exam, 11, host_exam, 11) != NX_X509_SAN_OK { return 1 }
98 if hostname_wildcard_match(pat_exam, 11, host_foo_test, 12) != NX_X509_SAN_NO_MATCH { return 2 }
99 if hostname_wildcard_match(pat_wild_test, 10, host_foo_test, 12) != NX_X509_SAN_OK { return 3 }
100 if hostname_wildcard_match(pat_wild_test, 10, host_evil, 17) != NX_X509_SAN_NO_MATCH { return 4 }
101 if hostname_wildcard_match(pat_wild_test, 10, host_exam, 11) != NX_X509_SAN_NO_MATCH { return 5 }
102
103 // Reject mid-label wildcard "foo.*.com"
104 let pat_mid: *u8 = sys_mmap(32)
105 pat_mid[0]=0x66; pat_mid[1]=0x6F; pat_mid[2]=0x6F; pat_mid[3]=0x2E
106 pat_mid[4]=0x2A; pat_mid[5]=0x2E; pat_mid[6]=0x63; pat_mid[7]=0x6F
107 pat_mid[8]=0x6D // "foo.*.com" (9)
108 if hostname_wildcard_match(pat_mid, 9, host_foo_test, 12) != NX_X509_SAN_BAD_PATTERN { return 6 }
109
110 // Reject "*foo.com" (partial leftmost label) -- '*' must be
111 // followed by '.' to claim the whole leftmost label.
112 let pat_partial: *u8 = sys_mmap(32)
113 pat_partial[0]=0x2A; pat_partial[1]=0x66; pat_partial[2]=0x6F
114 pat_partial[3]=0x6F; pat_partial[4]=0x2E; pat_partial[5]=0x63
115 pat_partial[6]=0x6F; pat_partial[7]=0x6D // "*foo.com" (8)
116 if hostname_wildcard_match(pat_partial, 8, host_exam, 11) != NX_X509_SAN_BAD_PATTERN { return 7 }
117
118 // Reject two wildcards "*.*.com"
119 let pat_two: *u8 = sys_mmap(32)
120 pat_two[0]=0x2A; pat_two[1]=0x2E; pat_two[2]=0x2A; pat_two[3]=0x2E
121 pat_two[4]=0x63; pat_two[5]=0x6F; pat_two[6]=0x6D // "*.*.com" (7)
122 if hostname_wildcard_match(pat_two, 7, host_foo_test, 12) != NX_X509_SAN_BAD_PATTERN { return 8 }
123
124 // Case-insensitive exact match.
125 let host_upper: *u8 = sys_mmap(32)
126 host_upper[0]=0x45; host_upper[1]=0x58; host_upper[2]=0x41; host_upper[3]=0x4D
127 host_upper[4]=0x50; host_upper[5]=0x4C; host_upper[6]=0x45; host_upper[7]=0x2E
128 host_upper[8]=0x43; host_upper[9]=0x4F; host_upper[10]=0x4D // "EXAMPLE.COM"
129 if hostname_wildcard_match(pat_exam, 11, host_upper, 11) != NX_X509_SAN_OK { return 9 }
130
131 // ---- Phase B: build extensions block with SAN ----
132 //
133 // Layout (assembled bottom-up so length fields are known):
134 //
135 // SAN GeneralNames body:
136 // dNSName "example.com" = 0x82 11 <bytes> (13)
137 // dNSName "*.test.org" = 0x82 10 <bytes> (12)
138 // SAN GeneralNames SEQUENCE wrapper: 0x30 <len=25> (27)
139 // extnValue OCTET STRING wrapper: 0x04 <len=27> (29)
140 // extnID OID 2.5.29.17: 0x06 03 55 1D 11 (5)
141 // Extension SEQUENCE wrapper: 0x30 <len=34> (36)
142 // Outer Extensions SEQUENCE wrapper: 0x30 <len=36> (38)
143
144 var o: i64 = 0
145 // Outer SEQUENCE header (Extensions list)
146 buf[o]=0x30; buf[o+1]=36; o = o + 2
147 // Extension SEQUENCE header
148 buf[o]=0x30; buf[o+1]=34; o = o + 2
149 // extnID OID (SAN)
150 buf[o]=0x06; buf[o+1]=0x03; buf[o+2]=0x55; buf[o+3]=0x1D; buf[o+4]=0x11
151 o = o + 5
152 // extnValue OCTET STRING header
153 buf[o]=0x04; buf[o+1]=27; o = o + 2
154 // GeneralNames SEQUENCE header
155 buf[o]=0x30; buf[o+1]=25; o = o + 2
156 // dNSName #1 "example.com"
157 o = o + emit_dnsname(buf, o, pat_exam, 11)
158 // dNSName #2 "*.test.org"
159 o = o + emit_dnsname(buf, o, pat_wild_test, 10)
160 let total: i64 = o // should equal 38
161
162 if total != 38 { return 20 }
163
164 cert.extensions_off = 0
165 cert.extensions_len = total
166
167 // ---- Phase B-1: locate SAN ----
168 let san_off_p: *i64 = sys_mmap(16) as *i64
169 let san_len_p: *i64 = sys_mmap(16) as *i64
170 let loc: i64 = x509_san_locate(buf, cert, san_off_p, san_len_p)
171 if loc != NX_X509_SAN_OK { return 21 }
172 if *san_len_p != 25 { return 22 }
173
174 // ---- Phase B-2: exact match wins ----
175 if x509_san_match_hostname(buf, cert, host_exam, 11) != NX_X509_SAN_OK { return 23 }
176
177 // ---- Phase B-3: wildcard match wins ----
178 if x509_san_match_hostname(buf, cert, host_foo_test, 12) != NX_X509_SAN_OK { return 24 }
179
180 // ---- Phase B-4: no match returns NO_MATCH ----
181 if x509_san_match_hostname(buf, cert, host_bad, 12) != NX_X509_SAN_NO_MATCH { return 25 }
182
183 // ---- Phase B-5: wildcard does NOT match multi-label leftmost ----
184 if x509_san_match_hostname(buf, cert, host_evil, 17) != NX_X509_SAN_NO_MATCH { return 26 }
185
186 // ---- Phase B-6: case-insensitive match ----
187 if x509_san_match_hostname(buf, cert, host_upper, 11) != NX_X509_SAN_OK { return 27 }
188
189 // ---- Phase C: cert with no extensions ----
190 cert.extensions_off = 0 - 1
191 cert.extensions_len = 0
192 if x509_san_match_hostname(buf, cert, host_exam, 11) != NX_X509_SAN_NO_EXTENSIONS { return 30 }
193
194 // ---- Phase D: cert with extensions but no SAN OID ----
195 // Build a single Extension with a DIFFERENT OID (subjectKeyIdentifier
196 // 2.5.29.14 = 55 1D 0E) and an OCTET STRING extnValue.
197 let buf2: *u8 = sys_mmap(256)
198 var p: i64 = 0
199 // Outer SEQUENCE
200 buf2[p]=0x30; buf2[p+1]=15; p = p + 2
201 // Extension SEQUENCE
202 buf2[p]=0x30; buf2[p+1]=13; p = p + 2
203 // OID 2.5.29.14
204 buf2[p]=0x06; buf2[p+1]=0x03; buf2[p+2]=0x55; buf2[p+3]=0x1D; buf2[p+4]=0x0E
205 p = p + 5
206 // extnValue OCTET STRING wrapping 6 arbitrary bytes
207 buf2[p]=0x04; buf2[p+1]=6; p = p + 2
208 buf2[p]=0xAA; buf2[p+1]=0xBB; buf2[p+2]=0xCC; buf2[p+3]=0xDD
209 buf2[p+4]=0xEE; buf2[p+5]=0xFF; p = p + 6
210 cert.extensions_off = 0
211 cert.extensions_len = p
212 if x509_san_match_hostname(buf2, cert, host_exam, 11) != NX_X509_SAN_NO_SAN { return 35 }
213
214 // ---- Phase E: bad-format extensions (truncated outer SEQUENCE) ----
215 let buf3: *u8 = sys_mmap(64)
216 buf3[0]=0x30; buf3[1]=0xFF // claim 255-byte outer, only have 2
217 cert.extensions_off = 0
218 cert.extensions_len = 2
219 let bad: i64 = x509_san_match_hostname(buf3, cert, host_exam, 11)
220 if bad != NX_X509_SAN_BAD_FORMAT { return 40 }
221
222 // ---- Phase F: SAN entry with bad pattern (no other entry matches) ----
223 // Build SAN with single dNSName "*foo" (illegal partial wildcard).
224 // Bottom-up sizes:
225 // dNSName "*foo" = 0x82 04 <4 bytes> (6)
226 // GeneralNames SEQUENCE = 0x30 06 <6 bytes> (8)
227 // OCTET STRING wrap = 0x04 08 <8 bytes> (10)
228 // OID 2.5.29.17 = 0x06 03 55 1D 11 (5)
229 // Extension SEQUENCE = 0x30 0F <15 bytes> (17)
230 // Outer Extensions SEQ = 0x30 11 <17 bytes> (19)
231 let buf4: *u8 = sys_mmap(128)
232 var q: i64 = 0
233 buf4[q]=0x30; buf4[q+1]=17; q = q + 2 // outer SEQUENCE
234 buf4[q]=0x30; buf4[q+1]=15; q = q + 2 // Extension SEQUENCE
235 buf4[q]=0x06; buf4[q+1]=0x03; buf4[q+2]=0x55 // SAN OID
236 buf4[q+3]=0x1D; buf4[q+4]=0x11; q = q + 5
237 buf4[q]=0x04; buf4[q+1]=8; q = q + 2 // OCTET STRING (8 bytes follow)
238 buf4[q]=0x30; buf4[q+1]=6; q = q + 2 // GeneralNames SEQUENCE
239 q = q + emit_dnsname(buf4, q, pat_partial, 4) // dNSName "*foo" (4 chars, illegal)
240 if q != 19 { return 50 }
241 cert.extensions_off = 0
242 cert.extensions_len = q
243 let badpat: i64 = x509_san_match_hostname(buf4, cert, host_exam, 11)
244 if badpat != NX_X509_SAN_BAD_PATTERN { return 51 }
245
246 // ---- Phase G: sealed verdict gate ----
247 if nx_x509_san_verdict_is_valid(NX_X509_SAN_OK) != 1 { return 60 }
248 if nx_x509_san_verdict_is_valid(NX_X509_SAN_NO_MATCH) != 1 { return 61 }
249 if nx_x509_san_verdict_is_valid(NX_X509_SAN_NO_EXTENSIONS) != 1 { return 62 }
250 if nx_x509_san_verdict_is_valid(NX_X509_SAN_NO_SAN) != 1 { return 63 }
251 if nx_x509_san_verdict_is_valid(NX_X509_SAN_BAD_FORMAT) != 1 { return 64 }
252 if nx_x509_san_verdict_is_valid(NX_X509_SAN_BAD_PATTERN) != 1 { return 65 }
253 if nx_x509_san_verdict_is_valid(NX_X509_SAN_VERDICT_N) != 0 { return 66 }
254 if nx_x509_san_verdict_is_valid(0) != 0 { return 67 }
255 if nx_x509_san_verdict_is_valid(0 - 1) != 0 { return 68 }
256 if nx_x509_san_verdict_is_valid(999) != 0 { return 69 }
257
258 return 0
259}