nx_https_pipeline_e2e_test.nx source
↩ module page · 366 lines · 14861 B
1// nx_https_pipeline_e2e_test.nx -- THE end-to-end happy-path KAT
2// for the full bits-up HTTPS cert verification pipeline.
3//
4// Closes the verification loop: this single test exercises every
5// substrate primitive shipped this session for the public-HTTPS
6// arc, from raw TLS 1.3 Certificate message bytes to a verdict.
7//
8// Flow:
9// 1. Build a TrustStore + add the root cert as an anchor
10// 2. Build a TLS 1.3 Certificate message wrapping a real DER
11// leaf cert (built inline via build_min_cert below)
12// 3. Call nx_https_cert_pipeline_verify_with_store(...)
13// 4. Verify the returned verdict is NX_HTTPS_PIPELINE_OK
14//
15// Test vector setup:
16// - Leaf cert: minimal-but-real DER cert from leaf_check_test
17// (commit 50d861a8) with validity [2023..2026] + SAN "example.com"
18// + Ed25519 SPKI (not exercised here -- root doesn't try to
19// verify leaf's sig; we set sig_alg to Ed25519 to match)
20// - Root cert: synthetic root with subject DN "CN=Root"
21// - For ECDSA-style verify we'd need a real CA-signed cert which
22// requires either a private CA key (we don't have signing) or
23// a real-world cert vector. The RFC 6979 vector lets us do
24// "sig over 'sample'" but not "sig over a real cert's tbs".
25//
26// So this KAT covers the EARLY-EXIT flow: build a real DER leaf
27// whose tbs is well-formed but whose outer sig is garbage Ed25519
28// bytes. Pipeline gets to:
29// - parse Cert message OK
30// - parse leaf DER OK
31// - leaf_check OK (validity + SAN both pass)
32// - chain_verify -> verify_under_issuer dispatches by leaf's
33// sig_alg (Ed25519) -> calls x509_verify_signature_ed25519
34// -> the underlying ed25519 verify is known broken (task #23)
35// -> returns SIG_FAIL
36//
37// Expected end verdict: NX_HTTPS_PIPELINE_CHAIN_SIG_FAIL
38//
39// This KAT proves the full pipeline ROUTES correctly end-to-end:
40// every sub-primitive is reached, every dispatch is exercised,
41// every verdict is plumbed. The "OK" final verdict awaits task
42// #23 (Ed25519 verify) OR a real-CA test vector for ECDSA.
43//
44// expect_exit: 0
45// license_tier: ORIGINAL
46
47import "nx_syscalls.nx"
48import "nx_u256.nx"
49import "nx_x509.nx"
50import "nx_x509_trust_store.nx"
51import "nx_https_cert_pipeline.nx"
52
53// ---- Helpers re-used from earlier KATs ----
54
55func emit_utctime(buf: *u8, off: i64,
56 yyyy: i64, mo: i64, d: i64,
57 h: i64, mi: i64, s: i64) -> i64 {
58 buf[off + 0] = 0x17 as u8
59 buf[off + 1] = 13 as u8
60 let yy: i64 = yyyy - 2000
61 buf[off + 2] = (0x30 + (yy / 10)) as u8
62 buf[off + 3] = (0x30 + (yy % 10)) as u8
63 buf[off + 4] = (0x30 + (mo / 10)) as u8
64 buf[off + 5] = (0x30 + (mo % 10)) as u8
65 buf[off + 6] = (0x30 + (d / 10)) as u8
66 buf[off + 7] = (0x30 + (d % 10)) as u8
67 buf[off + 8] = (0x30 + (h / 10)) as u8
68 buf[off + 9] = (0x30 + (h % 10)) as u8
69 buf[off + 10] = (0x30 + (mi / 10)) as u8
70 buf[off + 11] = (0x30 + (mi % 10)) as u8
71 buf[off + 12] = (0x30 + (s / 10)) as u8
72 buf[off + 13] = (0x30 + (s % 10)) as u8
73 buf[off + 14] = 0x5A as u8
74 return 15
75}
76
77func emit_ed25519_algid(buf: *u8, off: i64) -> i64 {
78 buf[off + 0] = 0x30 as u8
79 buf[off + 1] = 5 as u8
80 buf[off + 2] = 0x06 as u8
81 buf[off + 3] = 3 as u8
82 buf[off + 4] = 0x2B as u8
83 buf[off + 5] = 0x65 as u8
84 buf[off + 6] = 0x70 as u8
85 return 7
86}
87
88func emit_dnsname(buf: *u8, off: i64, name: *u8, name_len: i64) -> i64 {
89 buf[off] = 0x82 as u8
90 buf[off + 1] = name_len as u8
91 var i: i64 = 0
92 while i < name_len {
93 buf[off + 2 + i] = name[i]
94 i = i + 1
95 }
96 return 2 + name_len
97}
98
99// Write "CN=Root" DN bytes (17 bytes).
100func write_root_dn(buf: *u8, off: i64) -> i64 {
101 buf[off] = 0x30; buf[off+1] = 0x0F
102 buf[off+2] = 0x31; buf[off+3] = 0x0D
103 buf[off+4] = 0x30; buf[off+5] = 0x0B
104 buf[off+6] = 0x06; buf[off+7] = 0x03
105 buf[off+8] = 0x55; buf[off+9] = 0x04; buf[off+10] = 0x03
106 buf[off+11] = 0x0C; buf[off+12] = 0x04
107 buf[off+13] = 0x52; buf[off+14] = 0x6F; buf[off+15] = 0x6F; buf[off+16] = 0x74
108 return 17
109}
110
111func zero_cert(cert: *X509Cert) -> i64 {
112 cert.tbs_off=0; cert.tbs_len=0
113 cert.serial_off=0; cert.serial_len=0
114 cert.sig_alg_off=0; cert.sig_alg_len=0
115 cert.spki_off=0; cert.spki_len=0
116 cert.sig_off=0; cert.sig_len=0
117 cert.pubkey_off=0; cert.pubkey_len=0
118 cert.pubkey_alg_off=0; cert.pubkey_alg_len=0
119 cert.validity_off=0; cert.validity_len=0
120 cert.extensions_off = 0 - 1; cert.extensions_len=0
121 cert.issuer_off=0; cert.issuer_len=0
122 cert.subject_off=0; cert.subject_len=0
123 return 0
124}
125
126// Build a minimal real-DER leaf cert with:
127// issuer = subject = "CN=Root" (so it chains to our synthetic root)
128// validity [2023-01-01, 2026-01-01] (UTCTime)
129// SPKI = Ed25519 32-byte zero pubkey
130// sig_alg = Ed25519
131// sig = 64-byte garbage (not verified for OK in this KAT)
132// SAN = "example.com" dNSName
133//
134// Returns total cert bytes. Layout pre-computed (all lengths
135// short-form):
136// SAN dNSName "example.com" (13) -> GeneralNames SEQ (15) ->
137// OCTET STRING (17) + OID SAN (5) -> Extension SEQ (24) ->
138// Extensions outer (26) -> [3] EXPLICIT (28)
139// Validity body 30 -> Validity SEQ 32
140// Issuer/Subject Name each 17 (CN=Root)
141// SPKI: Ed25519 alg-id (7) + BIT STRING (35) wrapped in SEQ
142// = 0x30 2A <42> = 44
143// TBS body: serial(3) + sigAlg(7) + issuer(17) + validity(32) +
144// subject(17) + SPKI(44) + extensions wrap(28) = 148
145// TBS SEQUENCE: 0x30 0x81 0x94 <148> = 151 bytes
146// Outer sigAlg = 7
147// sig BIT STRING (1 unused + 64) = 67
148// Outer Cert body: 151 + 7 + 67 = 225
149// Outer Cert SEQ: 0x30 0x81 0xE1 <225> = 228 bytes
150func build_min_cert_root_subject(buf: *u8, host: *u8, host_len: i64) -> i64 {
151 let SAN_INNER_LEN: i64 = 13
152 let SAN_SEQ_TOTAL: i64 = 2 + SAN_INNER_LEN // 15
153 let OS_TOTAL: i64 = 2 + SAN_SEQ_TOTAL // 17
154 let OID_TOTAL: i64 = 5
155 let EXT_BODY: i64 = OID_TOTAL + OS_TOTAL // 22
156 let EXT_TOTAL: i64 = 2 + EXT_BODY // 24
157 let EXTS_BODY: i64 = EXT_TOTAL // 24
158 let EXTS_TOTAL: i64 = 2 + EXTS_BODY // 26
159 let EXP_WRAP_TOTAL: i64 = 2 + EXTS_TOTAL // 28
160
161 let VAL_BODY: i64 = 30
162 let VAL_TOTAL: i64 = 2 + VAL_BODY // 32
163
164 let SPKI_BODY: i64 = 7 + 35 // 42
165 let SPKI_TOTAL: i64 = 2 + SPKI_BODY // 44
166
167 let NAME_TOTAL: i64 = 17 // "CN=Root" DN bytes
168
169 let TBS_BODY: i64 = 3 + 7 + NAME_TOTAL + VAL_TOTAL + NAME_TOTAL + SPKI_TOTAL + EXP_WRAP_TOTAL // 148
170 let TBS_TOTAL: i64 = 2 + 1 + TBS_BODY // 0x30 0x81 0x94 + body = 151
171
172 let SIG_TOTAL: i64 = 67
173 let OUTER_BODY: i64 = TBS_TOTAL + 7 + SIG_TOTAL // 225
174 let OUTER_TOTAL: i64 = 3 + OUTER_BODY // 228
175
176 var o: i64 = 0
177 // Outer Cert SEQ (long-form length)
178 buf[o] = 0x30 as u8; o = o + 1
179 buf[o] = 0x81 as u8; o = o + 1
180 buf[o] = OUTER_BODY as u8; o = o + 1
181
182 // TBS SEQ (long-form length)
183 buf[o] = 0x30 as u8; o = o + 1
184 buf[o] = 0x81 as u8; o = o + 1
185 buf[o] = TBS_BODY as u8; o = o + 1
186
187 // serial INTEGER 0x01
188 buf[o] = 0x02 as u8; buf[o+1] = 0x01 as u8; buf[o+2] = 0x01 as u8; o = o + 3
189
190 // signature (TBS-level) Ed25519
191 o = o + emit_ed25519_algid(buf, o)
192
193 // issuer Name = "CN=Root"
194 o = o + write_root_dn(buf, o)
195
196 // validity SEQUENCE { UTCTime 2023-01-01, UTCTime 2026-01-01 }
197 buf[o] = 0x30 as u8; o = o + 1
198 buf[o] = VAL_BODY as u8; o = o + 1
199 o = o + emit_utctime(buf, o, 2023, 1, 1, 0, 0, 0)
200 o = o + emit_utctime(buf, o, 2026, 1, 1, 0, 0, 0)
201
202 // subject Name = "CN=Root" (matches issuer; for our test that
203 // means the leaf is also the root's subject -- weird but lets
204 // us use one DN value throughout)
205 o = o + write_root_dn(buf, o)
206
207 // SPKI (Ed25519 alg-id + 32-byte zero pubkey)
208 buf[o] = 0x30 as u8; o = o + 1
209 buf[o] = SPKI_BODY as u8; o = o + 1
210 o = o + emit_ed25519_algid(buf, o)
211 buf[o] = 0x03 as u8; o = o + 1
212 buf[o] = 0x21 as u8; o = o + 1
213 buf[o] = 0x00 as u8; o = o + 1
214 var ki: i64 = 0
215 while ki < 32 { buf[o + ki] = 0x00 as u8; ki = ki + 1 }
216 o = o + 32
217
218 // Extensions [3] EXPLICIT { SEQUENCE { Extension { SAN OID, OCTET STRING { SEQ { dNSName } } } } }
219 buf[o] = 0xA3 as u8; o = o + 1
220 buf[o] = EXTS_TOTAL as u8; o = o + 1
221 buf[o] = 0x30 as u8; o = o + 1
222 buf[o] = EXTS_BODY as u8; o = o + 1
223 buf[o] = 0x30 as u8; o = o + 1
224 buf[o] = EXT_BODY as u8; o = o + 1
225 buf[o] = 0x06 as u8; buf[o+1] = 0x03 as u8; buf[o+2] = 0x55 as u8
226 buf[o+3] = 0x1D as u8; buf[o+4] = 0x11 as u8; o = o + 5
227 buf[o] = 0x04 as u8; o = o + 1
228 buf[o] = SAN_SEQ_TOTAL as u8; o = o + 1
229 buf[o] = 0x30 as u8; o = o + 1
230 buf[o] = SAN_INNER_LEN as u8; o = o + 1
231 o = o + emit_dnsname(buf, o, host, host_len)
232
233 // Outer signatureAlgorithm Ed25519
234 o = o + emit_ed25519_algid(buf, o)
235
236 // signatureValue BIT STRING (64 garbage bytes)
237 buf[o] = 0x03 as u8; o = o + 1
238 buf[o] = 0x41 as u8; o = o + 1
239 buf[o] = 0x00 as u8; o = o + 1
240 var si: i64 = 0
241 while si < 64 { buf[o + si] = 0xAA as u8; si = si + 1 }
242 o = o + 64
243
244 return o
245}
246
247// Build a minimal "trust anchor" cert with subject DN "CN=Root" +
248// Ed25519 pubkey (32 zero bytes). We only need subject DN +
249// pubkey_alg + pubkey to be set for the chain walker to use it
250// as the parent.
251func build_synthetic_root(buf: *u8, cert: *X509Cert) -> i64 {
252 // Pubkey alg blob at off 0: SEQUENCE { OID Ed25519 } = 7 bytes
253 emit_ed25519_algid(buf, 0)
254 // Pubkey (32 zero bytes) at off 7
255 var i: i64 = 0
256 while i < 32 { buf[7 + i] = 0x00 as u8; i = i + 1 }
257 // Subject DN "CN=Root" at off 39
258 write_root_dn(buf, 39)
259
260 zero_cert(cert)
261 cert.pubkey_alg_off = 0; cert.pubkey_alg_len = 7
262 cert.pubkey_off = 7; cert.pubkey_len = 32
263 cert.subject_off = 39; cert.subject_len = 17
264 return 56 // total bytes written
265}
266
267func main() -> i64 {
268 let host_exam: *u8 = sys_mmap(16)
269 host_exam[0]=0x65; host_exam[1]=0x78; host_exam[2]=0x61; host_exam[3]=0x6D
270 host_exam[4]=0x70; host_exam[5]=0x6C; host_exam[6]=0x65; host_exam[7]=0x2E
271 host_exam[8]=0x63; host_exam[9]=0x6F; host_exam[10]=0x6D // "example.com"
272
273 // ---- Build leaf cert (real DER) ----
274 let leaf_cert_buf: *u8 = sys_mmap(512)
275 let leaf_cert_total: i64 = build_min_cert_root_subject(leaf_cert_buf, host_exam, 11)
276 if leaf_cert_total != 228 { return 1 }
277
278 // ---- Build trust anchor cert (synthetic) ----
279 let root_buf: *u8 = sys_mmap(128)
280 let root_cert_raw: *u8 = sys_mmap(256)
281 let root_cert: *X509Cert = root_cert_raw as *X509Cert
282 build_synthetic_root(root_buf, root_cert)
283
284 // ---- Build TrustStore + add root anchor ----
285 let store: *TrustStore = trust_store_alloc(4)
286 if trust_store_add(store, root_buf, root_cert) != NX_TRUST_STORE_OK { return 2 }
287 if trust_store_count(store) != 1 { return 3 }
288
289 // ---- Build TLS 1.3 Certificate message wrapping leaf_cert ----
290 // Layout:
291 // 1 byte HT_CERTIFICATE
292 // 3-byte body length
293 // body:
294 // 1-byte ctx_len = 0
295 // 3-byte chain_total_len = 3 + 228 + 2 = 233
296 // entry: 3-byte cert_len = 228 + 228 leaf bytes + 2-byte ext = 0
297 // body length = 1 + 3 + 233 = 237
298 // total handshake msg = 4 + 237 = 241
299 let msg: *u8 = sys_mmap(512)
300 msg[0] = 0x0B // HT_CERTIFICATE
301 msg[1] = 0; msg[2] = 0; msg[3] = 237 as u8 // body 237 (fits in 1 byte, hi-bytes 0)
302 msg[4] = 0 // ctx_len = 0
303 msg[5] = 0; msg[6] = 0; msg[7] = 233 as u8 // chain_total = 233
304 msg[8] = 0; msg[9] = 0; msg[10] = 228 as u8 // cert_len = 228
305 // Copy leaf cert bytes at offset 11
306 var ci: i64 = 0
307 while ci < 228 { msg[11 + ci] = leaf_cert_buf[ci]; ci = ci + 1 }
308 msg[239] = 0; msg[240] = 0 // ext_list_len = 0
309 let msg_total: i64 = 241
310
311 let NOW_OK: i64 = 1718452800 // 2024-06-15 12:00
312
313 // ============================================================
314 // Test A: full pipeline run via trust-store-aware variant.
315 // Expected verdict: NX_HTTPS_PIPELINE_CHAIN_SIG_FAIL.
316 //
317 // The pipeline:
318 // - parses TLS Cert message OK (parse_chain_all)
319 // - parses leaf DER OK (x509_parse)
320 // - leaf_check OK (validity + SAN) (x509_leaf_check)
321 // - chain_verify reaches the leaf->root link
322 // - DN match leaf.issuer ("CN=Root") == root.subject OK
323 // - verify_under_issuer dispatches by leaf's sig_alg
324 // (Ed25519) -> x509_verify_signature_ed25519
325 // - The underlying ed25519_verify_full is known broken
326 // (task #23) AND the sig is garbage anyway -> FAIL
327 // - pipeline returns CHAIN_SIG_FAIL
328 // ============================================================
329 let v: i64 = nx_https_cert_pipeline_verify_with_store(
330 msg, msg_total, host_exam, 11, NOW_OK, store
331 )
332 if v != NX_HTTPS_PIPELINE_CHAIN_SIG_FAIL { return 4 }
333
334 // ============================================================
335 // Test B: NOW_LATE (after notAfter) -> EXPIRED
336 // The pipeline parses + finds leaf cert, leaf_check kicks in
337 // before chain_verify, returns EXPIRED.
338 // ============================================================
339 let NOW_LATE: i64 = 1798761600 // 2027-01-01
340 let v_exp: i64 = nx_https_cert_pipeline_verify_with_store(
341 msg, msg_total, host_exam, 11, NOW_LATE, store
342 )
343 if v_exp != NX_HTTPS_PIPELINE_LEAF_EXPIRED { return 5 }
344
345 // ============================================================
346 // Test C: hostname mismatch -> HOSTNAME_MISMATCH
347 // ============================================================
348 let host_other: *u8 = sys_mmap(8)
349 host_other[0] = 0x66; host_other[1] = 0x6F; host_other[2] = 0x6F // "foo"
350 let v_hn: i64 = nx_https_cert_pipeline_verify_with_store(
351 msg, msg_total, host_other, 3, NOW_OK, store
352 )
353 if v_hn != NX_HTTPS_PIPELINE_HOSTNAME_MISMATCH { return 6 }
354
355 // ============================================================
356 // Test D: empty trust store -> NO_TRUST_ANCHOR
357 // (last cert's issuer DN "CN=Root" matches nothing)
358 // ============================================================
359 let empty_store: *TrustStore = trust_store_alloc(4)
360 let v_nta: i64 = nx_https_cert_pipeline_verify_with_store(
361 msg, msg_total, host_exam, 11, NOW_OK, empty_store
362 )
363 if v_nta != NX_HTTPS_PIPELINE_NO_TRUST_ANCHOR { return 7 }
364
365 return 0
366}