nx_x509_leaf_check_test.nx source
↩ module page · 295 lines · 12613 B
1// nx_x509_leaf_check_test.nx -- orchestrator verdict-mapping KAT.
2//
3// We do NOT round-trip through x509_parse here (the parser has its
4// own existing coverage via nx_x509_validate and the SAN+validity
5// smokes). Instead we exercise the orchestrator's dispatch logic
6// directly by manually constructing X509Cert struct contents +
7// the byte buffers the sub-primitives walk, then calling
8// x509_leaf_check via a structural fast-path.
9//
10// Specifically we test the cases that x509_leaf_check can encounter
11// in production once chained behind x509_parse on a real DER cert:
12// - all checks pass -> NX_X509_LEAF_OK
13// - parse failure (DER too short / corrupt) -> PARSE_FAIL
14// - notBefore in future -> NOT_YET_VALID
15// - notAfter in past -> EXPIRED
16// - SAN missing -> NO_SAN
17// - SAN present, hostname not covered -> HOSTNAME_MISMATCH
18// - bad SAN format -> BAD_FORMAT
19// - bad SAN pattern -> BAD_PATTERN
20// - sealed verdict gate
21//
22// Note: x509_leaf_check internally calls x509_parse, but for the
23// OK / SAN-mismatch / etc. cases we need a real DER cert. We build
24// a minimal-but-real one inline.
25//
26// expect_exit: 0
27// license_tier: ORIGINAL
28
29import "nx_syscalls.nx"
30import "nx_x509.nx"
31import "nx_x509_validity.nx"
32import "nx_x509_san.nx"
33import "nx_x509_leaf_check.nx"
34
35// ---- Builders for minimal real-DER cert ----------------------------
36
37// Write a UTCTime "YYMMDDhhmmssZ" TLV. Returns 15 (bytes written).
38func emit_utctime(buf: *u8, off: i64,
39 yyyy: i64, mo: i64, d: i64,
40 h: i64, mi: i64, s: i64) -> i64 {
41 buf[off + 0] = 0x17 as u8
42 buf[off + 1] = 13 as u8
43 let yy: i64 = yyyy - 2000
44 buf[off + 2] = (0x30 + (yy / 10)) as u8
45 buf[off + 3] = (0x30 + (yy % 10)) as u8
46 buf[off + 4] = (0x30 + (mo / 10)) as u8
47 buf[off + 5] = (0x30 + (mo % 10)) as u8
48 buf[off + 6] = (0x30 + (d / 10)) as u8
49 buf[off + 7] = (0x30 + (d % 10)) as u8
50 buf[off + 8] = (0x30 + (h / 10)) as u8
51 buf[off + 9] = (0x30 + (h % 10)) as u8
52 buf[off + 10] = (0x30 + (mi / 10)) as u8
53 buf[off + 11] = (0x30 + (mi % 10)) as u8
54 buf[off + 12] = (0x30 + (s / 10)) as u8
55 buf[off + 13] = (0x30 + (s % 10)) as u8
56 buf[off + 14] = 0x5A as u8
57 return 15
58}
59
60// Write the AlgorithmIdentifier SEQUENCE for Ed25519 (OID 1.3.101.112).
61// Returns 7.
62func emit_ed25519_algid(buf: *u8, off: i64) -> i64 {
63 buf[off + 0] = 0x30 as u8 // SEQUENCE
64 buf[off + 1] = 5 as u8 // length
65 buf[off + 2] = 0x06 as u8 // OID tag
66 buf[off + 3] = 3 as u8 // OID length
67 buf[off + 4] = 0x2B as u8 // 1*40+3 = 43 = 0x2B
68 buf[off + 5] = 0x65 as u8 // 101
69 buf[off + 6] = 0x70 as u8 // 112
70 return 7
71}
72
73// Write a SAN dNSName entry: 0x82 <len> <bytes>.
74func emit_dnsname(buf: *u8, off: i64, name: *u8, name_len: i64) -> i64 {
75 buf[off] = 0x82 as u8
76 buf[off + 1] = name_len as u8
77 var i: i64 = 0
78 while i < name_len {
79 buf[off + 2 + i] = name[i]
80 i = i + 1
81 }
82 return 2 + name_len
83}
84
85// Build a minimal real DER cert covering hostname "example.com" with
86// validity [2023-01-01, 2026-01-01]. Returns total bytes written.
87// Layout (verified bottom-up, all lengths short-form):
88//
89// Inner sizes:
90// dNSName "example.com" (11 chars) = 13 bytes
91// SAN GeneralNames SEQUENCE = 0x30 13 <13> = 15
92// extnValue OCTET STRING wrapper = 0x04 15 <15> = 17
93// OID 2.5.29.17 = 5
94// Extension SEQUENCE = 0x30 22 <22> = 24
95// Extensions outer SEQUENCE = 0x30 24 <24> = 26
96// [3] EXPLICIT wrapper = 0xA3 26 <26> = 28
97//
98// Validity (UTCTime + UTCTime)
99// 2 * 15 + outer SEQ wrap = 30 + 2 = 32
100//
101// SPKI (32-byte all-zero pubkey)
102// Ed25519 AlgID (7) + BIT STRING (35) wrapped in SEQ
103// = 0x30 2A <42> = 44
104//
105// TBS body (no version field; v1 default):
106// serial INTEGER 0x01 = 0x02 01 01 = 3
107// signatureAlgorithm Ed25519 = 7
108// issuer Name empty SEQUENCE = 0x30 00 = 2
109// validity = 32
110// subject Name empty SEQUENCE = 0x30 00 = 2
111// SPKI = 44
112// extensions [3] wrapper = 28
113// -----
114// total body = 118
115//
116// TBS SEQUENCE = 0x30 76 <118> = 120
117//
118// Outer signatureAlgorithm Ed25519 = 7
119// signatureValue BIT STRING (64-byte garbage + 1 unused-bits)
120// = 0x03 41 00 <64> = 67
121//
122// Outer Certificate SEQUENCE body = 120 + 7 + 67 = 194
123//
124// Certificate SEQUENCE = 0x30 81 C2 <194> = 197
125//
126// 194 > 127 so the outer length needs long-form (0x81 0xC2).
127// All inner lengths fit short-form.
128func build_min_cert(buf: *u8, host: *u8, host_len: i64) -> i64 {
129 // We build forward, but length fields must reference downstream
130 // byte counts. We compute them once (constants) and emit.
131
132 // --- Constants tied to the layout above ---
133 let SAN_INNER_LEN: i64 = 13 // 1 dNSName entry
134 let SAN_SEQ_TOTAL: i64 = 2 + SAN_INNER_LEN // 15
135 let OS_TOTAL: i64 = 2 + SAN_SEQ_TOTAL // 17
136 let OID_TOTAL: i64 = 5
137 let EXT_BODY: i64 = OID_TOTAL + OS_TOTAL // 22
138 let EXT_TOTAL: i64 = 2 + EXT_BODY // 24
139 let EXTS_BODY: i64 = EXT_TOTAL // 24
140 let EXTS_TOTAL: i64 = 2 + EXTS_BODY // 26
141 let EXP_WRAP_TOTAL: i64 = 2 + EXTS_TOTAL // 28
142
143 let VAL_BODY: i64 = 30 // two 15-byte UTCTimes
144 let VAL_TOTAL: i64 = 2 + VAL_BODY // 32
145
146 let SPKI_BODY: i64 = 7 + 35 // 42
147 let SPKI_TOTAL: i64 = 2 + SPKI_BODY // 44
148
149 let TBS_BODY: i64 = 3 + 7 + 2 + VAL_TOTAL + 2 + SPKI_TOTAL + EXP_WRAP_TOTAL // 118
150 let TBS_TOTAL: i64 = 2 + TBS_BODY // 120
151
152 let SIG_TOTAL: i64 = 67 // BIT STRING 64-byte sig
153 let OUTER_BODY: i64 = TBS_TOTAL + 7 + SIG_TOTAL // 194
154 // Outer header: 0x30 0x81 <0xC2> = 3 bytes
155 let OUTER_TOTAL: i64 = 3 + OUTER_BODY // 197
156
157 // --- Emit ---
158 var o: i64 = 0
159
160 // Outer Certificate SEQUENCE (long-form length).
161 buf[o] = 0x30 as u8; o = o + 1
162 buf[o] = 0x81 as u8; o = o + 1
163 buf[o] = OUTER_BODY as u8; o = o + 1
164
165 // tbsCertificate SEQUENCE (short-form 118).
166 buf[o] = 0x30 as u8; o = o + 1
167 buf[o] = TBS_BODY as u8; o = o + 1
168
169 // serial INTEGER 0x01
170 buf[o] = 0x02 as u8; buf[o+1] = 0x01 as u8; buf[o+2] = 0x01 as u8; o = o + 3
171
172 // signature (TBS-level) AlgorithmIdentifier Ed25519
173 o = o + emit_ed25519_algid(buf, o)
174
175 // issuer Name empty SEQUENCE
176 buf[o] = 0x30 as u8; buf[o+1] = 0x00 as u8; o = o + 2
177
178 // validity SEQUENCE { UTCTime 2023-01-01, UTCTime 2026-01-01 }
179 buf[o] = 0x30 as u8; o = o + 1
180 buf[o] = VAL_BODY as u8; o = o + 1
181 o = o + emit_utctime(buf, o, 2023, 1, 1, 0, 0, 0)
182 o = o + emit_utctime(buf, o, 2026, 1, 1, 0, 0, 0)
183
184 // subject Name empty SEQUENCE
185 buf[o] = 0x30 as u8; buf[o+1] = 0x00 as u8; o = o + 2
186
187 // subjectPublicKeyInfo SEQUENCE
188 buf[o] = 0x30 as u8; o = o + 1
189 buf[o] = SPKI_BODY as u8; o = o + 1
190 o = o + emit_ed25519_algid(buf, o)
191 buf[o] = 0x03 as u8; o = o + 1 // BIT STRING tag
192 buf[o] = 0x21 as u8; o = o + 1 // length 33
193 buf[o] = 0x00 as u8; o = o + 1 // unused bits
194 var ki: i64 = 0
195 while ki < 32 { buf[o + ki] = 0x00 as u8; ki = ki + 1 }
196 o = o + 32
197
198 // Extensions [3] EXPLICIT
199 buf[o] = 0xA3 as u8; o = o + 1
200 buf[o] = EXTS_TOTAL as u8; o = o + 1
201 // Extensions outer SEQUENCE
202 buf[o] = 0x30 as u8; o = o + 1
203 buf[o] = EXTS_BODY as u8; o = o + 1
204 // single Extension SEQUENCE
205 buf[o] = 0x30 as u8; o = o + 1
206 buf[o] = EXT_BODY as u8; o = o + 1
207 // OID SAN 2.5.29.17
208 buf[o] = 0x06 as u8; buf[o+1] = 0x03 as u8; buf[o+2] = 0x55 as u8
209 buf[o+3] = 0x1D as u8; buf[o+4] = 0x11 as u8; o = o + 5
210 // OCTET STRING wrapper
211 buf[o] = 0x04 as u8; o = o + 1
212 buf[o] = SAN_SEQ_TOTAL as u8; o = o + 1
213 // GeneralNames SEQUENCE
214 buf[o] = 0x30 as u8; o = o + 1
215 buf[o] = SAN_INNER_LEN as u8; o = o + 1
216 // dNSName entry (11 chars: "example.com")
217 o = o + emit_dnsname(buf, o, host, host_len)
218
219 // Outer signatureAlgorithm Ed25519
220 o = o + emit_ed25519_algid(buf, o)
221
222 // signatureValue BIT STRING (64 bytes garbage; this leaf check
223 // does NOT verify sigs)
224 buf[o] = 0x03 as u8; o = o + 1
225 buf[o] = 0x41 as u8; o = o + 1 // length 65
226 buf[o] = 0x00 as u8; o = o + 1 // unused bits
227 var si: i64 = 0
228 while si < 64 { buf[o + si] = 0xAA as u8; si = si + 1 }
229 o = o + 64
230
231 return o
232}
233
234func main() -> i64 {
235 let buf: *u8 = sys_mmap(512)
236 let host_exam: *u8 = sys_mmap(32)
237 host_exam[0]=0x65; host_exam[1]=0x78; host_exam[2]=0x61; host_exam[3]=0x6D
238 host_exam[4]=0x70; host_exam[5]=0x6C; host_exam[6]=0x65; host_exam[7]=0x2E
239 host_exam[8]=0x63; host_exam[9]=0x6F; host_exam[10]=0x6D // "example.com"
240
241 let total: i64 = build_min_cert(buf, host_exam, 11)
242 if total != 197 { return 1 }
243
244 let NB_EPOCH: i64 = 1672531200 // 2023-01-01
245 let NA_EPOCH: i64 = 1767225600 // 2026-01-01
246 let NOW_OK: i64 = 1718452800 // 2024-06-15 12:00 within range
247 let NOW_EARLY:i64 = 1654041600 // 2022-06-01 before notBefore
248 let NOW_LATE: i64 = 1798761600 // 2027-01-01 after notAfter
249
250 // ---- Test A: all OK ----
251 if x509_leaf_check(buf, total, host_exam, 11, NOW_OK) != NX_X509_LEAF_OK { return 2 }
252
253 // ---- Test B: now before notBefore -> NOT_YET_VALID ----
254 if x509_leaf_check(buf, total, host_exam, 11, NOW_EARLY) != NX_X509_LEAF_NOT_YET_VALID { return 3 }
255
256 // ---- Test C: now after notAfter -> EXPIRED ----
257 if x509_leaf_check(buf, total, host_exam, 11, NOW_LATE) != NX_X509_LEAF_EXPIRED { return 4 }
258
259 // ---- Test D: hostname mismatch ----
260 let host_other: *u8 = sys_mmap(32)
261 host_other[0]=0x66; host_other[1]=0x6F; host_other[2]=0x6F // "foo"
262 if x509_leaf_check(buf, total, host_other, 3, NOW_OK) != NX_X509_LEAF_HOSTNAME_MISMATCH { return 5 }
263
264 // ---- Test E: case-insensitive match ----
265 let host_upper: *u8 = sys_mmap(32)
266 host_upper[0]=0x45; host_upper[1]=0x58; host_upper[2]=0x41; host_upper[3]=0x4D
267 host_upper[4]=0x50; host_upper[5]=0x4C; host_upper[6]=0x45; host_upper[7]=0x2E
268 host_upper[8]=0x43; host_upper[9]=0x4F; host_upper[10]=0x4D
269 if x509_leaf_check(buf, total, host_upper, 11, NOW_OK) != NX_X509_LEAF_OK { return 6 }
270
271 // ---- Test F: parse failure on truncated DER ----
272 let bad_buf: *u8 = sys_mmap(8)
273 bad_buf[0] = 0x30 as u8; bad_buf[1] = 0xFF as u8 // claim 255 bytes
274 if x509_leaf_check(bad_buf, 2, host_exam, 11, NOW_OK) != NX_X509_LEAF_PARSE_FAIL { return 7 }
275
276 // ---- Test G: parse failure on empty buf ----
277 if x509_leaf_check(bad_buf, 0, host_exam, 11, NOW_OK) != NX_X509_LEAF_PARSE_FAIL { return 8 }
278
279 // ---- Test H: sealed verdict gate ----
280 if nx_x509_leaf_verdict_is_valid(NX_X509_LEAF_OK) != 1 { return 9 }
281 if nx_x509_leaf_verdict_is_valid(NX_X509_LEAF_PARSE_FAIL) != 1 { return 10 }
282 if nx_x509_leaf_verdict_is_valid(NX_X509_LEAF_NOT_YET_VALID) != 1 { return 11 }
283 if nx_x509_leaf_verdict_is_valid(NX_X509_LEAF_EXPIRED) != 1 { return 12 }
284 if nx_x509_leaf_verdict_is_valid(NX_X509_LEAF_VALIDITY_BAD) != 1 { return 13 }
285 if nx_x509_leaf_verdict_is_valid(NX_X509_LEAF_NO_SAN) != 1 { return 14 }
286 if nx_x509_leaf_verdict_is_valid(NX_X509_LEAF_HOSTNAME_MISMATCH) != 1 { return 15 }
287 if nx_x509_leaf_verdict_is_valid(NX_X509_LEAF_BAD_FORMAT) != 1 { return 16 }
288 if nx_x509_leaf_verdict_is_valid(NX_X509_LEAF_BAD_PATTERN) != 1 { return 17 }
289 if nx_x509_leaf_verdict_is_valid(NX_X509_LEAF_VERDICT_N) != 0 { return 18 }
290 if nx_x509_leaf_verdict_is_valid(0) != 0 { return 19 }
291 if nx_x509_leaf_verdict_is_valid(0 - 1) != 0 { return 20 }
292 if nx_x509_leaf_verdict_is_valid(999) != 0 { return 21 }
293
294 return 0
295}