code wiki / (root) / nx_x509_leaf_check_test.nx

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}