code wiki / (root) / nx_nss_certdata_parse.nx

nx_nss_certdata_parse.nx source

↩ module page · 343 lines · 13682 B

1// nx_nss_certdata_parse.nx -- bits-up parser for Mozilla NSS's 2// `certdata.txt` CA bundle format. 3// 4// Phase 0b §I.4 piece 11 of the chain-walker arc. The PARSER that 5// turns Mozilla's certdata.txt text format into our internal bundle 6// wire format -- closes the gap between "real CA bundle exists" and 7// "TrustStore is populated". Per 8// [[feedback-bits-up-exceed-never-match]]: re-derives the parser 9// from the certdata.txt spec rather than adopting any third-party 10// extractor binary. 11// 12// certdata.txt format (PKCS#11-style flat text, RFC-free): 13// 14// # 15// # Certificate "GTS Root R1" 16// # 17// CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE 18// CKA_TOKEN CK_BBOOL CK_TRUE 19// CKA_LABEL UTF8 "GTS Root R1" 20// CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509 21// CKA_SUBJECT MULTILINE_OCTAL 22// \060\107\061\013... <-- 3-digit octal escapes 23// \003\125\123\061... <-- continues across lines 24// END 25// CKA_ID UTF8 "0" 26// CKA_VALUE MULTILINE_OCTAL 27// \060\202\005\025... <-- full DER cert 28// ... 29// END 30// 31// Trust records (CKO_NSS_TRUST) typically follow each cert and 32// carry CKA_TRUST_SERVER_AUTH = CKT_NSS_TRUSTED_DELEGATOR. This 33// first-cut parser TRUSTS EVERY listed cert (conservative; matches 34// what most CA bundle extractors do at the "load all certs" layer). 35// Filtering by trust-record attributes is queued for piece 11b. 36// 37// Algorithm: line-by-line state machine. 38// State IDLE: 39// Looking for `CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE` 40// -> transition to IN_CERT 41// State IN_CERT: 42// Looking for `CKA_VALUE MULTILINE_OCTAL` 43// -> transition to IN_VALUE 44// State IN_VALUE: 45// Each line is a series of \NNN octal escapes; decode + append 46// to current DER buffer. 47// On `END` line: emit (3-byte len + DER bytes) to output, 48// bump cert count, transition back to IDLE. 49// 50// Skip lines: comments starting with `#`, blank lines. 51// 52// Wire format emitted (matches nx_x509_trust_store_load consumer): 53// [2 bytes BE] cert_count 54// per cert: [3 bytes BE] cert_len; [cert_len] DER bytes 55// 56// Public API: 57// nx_nss_certdata_parse(in_buf, in_len, out_buf, out_cap, 58// out_n_certs) -> verdict 59// nx_nss_certdata_verdict_is_valid(v) -> 0|1 60// 61// Sealed verdict: 62// NX_NSS_CD_OK parse succeeded, output written 63// NX_NSS_CD_TRUNCATED input ended mid-record 64// NX_NSS_CD_BAD_ESCAPE octal escape malformed (\NNN with 65// non-octal digit or short) 66// NX_NSS_CD_OUTPUT_FULL output buffer too small for the bundle 67// NX_NSS_CD_NO_CERTS no CKO_CERTIFICATE objects found 68// NX_NSS_CD_TOO_MANY more than MAX_CERTS certs in input 69// 70// Per Cardinals 9 (single-responsibility -- parse text -> emit 71// wire format; trust-record cross-ref is separate), 12 (defensive 72// at boundaries -- bounded reads + output-cap check + per-cert 73// size limit), 23 (preamble names what is in/out of scope). 74// 75// license_tier: INDEPENDENT_REDERIVE 76// genealogy_id: international-research-sources/mozilla/nss + oasis/pkcs11 77// lineage_id: nishi_nss_certdata_parse_q10 78 79// nx_safety_envelope: 80// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 81// sil_target: SIL1 82// evidence: [bulk_applied_2026-05-19, nss-certdata-bits-up-parser] 83// verdict: NOT_YET_EVALUATED 84 85import "nx_syscalls.nx" 86 87const NX_NSS_CD_OK: i64 = 1 88const NX_NSS_CD_TRUNCATED: i64 = 2 89const NX_NSS_CD_BAD_ESCAPE: i64 = 3 90const NX_NSS_CD_OUTPUT_FULL: i64 = 4 91const NX_NSS_CD_NO_CERTS: i64 = 5 92const NX_NSS_CD_TOO_MANY: i64 = 6 93const NX_NSS_CD_BAD_DER_SIZE: i64 = 7 94const NX_NSS_CD_VERDICT_N: i64 = 8 95 96const NX_NSS_CD_MAX_CERTS: i64 = 256 // cap on certs per bundle 97const NX_NSS_CD_MAX_DER_SIZE: i64 = 16384 // 16KB per cert (real roots are 1-3KB) 98 99// State machine states (private, encoded as small ints). 100const _ST_IDLE: i64 = 0 101const _ST_IN_CERT: i64 = 1 102const _ST_IN_VALUE: i64 = 2 103 104func nx_nss_certdata_verdict_is_valid(v: i64) -> i64 { 105 if v < NX_NSS_CD_OK { return 0 } 106 if v >= NX_NSS_CD_VERDICT_N { return 0 } 107 return 1 108} 109 110// ---- Tiny helpers (all forward-defined per nx no-forward-ref rule) ---- 111 112// Returns 1 if byte is ASCII space/tab/CR. 113func _is_space(b: i64) -> i64 { 114 if b == 0x20 { return 1 } 115 if b == 0x09 { return 1 } 116 if b == 0x0D { return 1 } 117 return 0 118} 119 120// Returns 1 if byte is ASCII octal digit '0'..'7'. 121func _is_octal(b: i64) -> i64 { 122 if b < 0x30 { return 0 } 123 if b > 0x37 { return 0 } 124 return 1 125} 126 127// Compare a fixed-length prefix. Returns 1 if buf[off..off+n) 128// byte-equals pat[0..n). Both args are byte buffers. 129func _starts_with(buf: *u8, off: i64, in_len: i64, 130 pat: *u8, n: i64) -> i64 { 131 if off + n > in_len { return 0 } 132 var i: i64 = 0 133 while i < n { 134 if (buf[off + i] & 0xff) != (pat[i] & 0xff) { return 0 } 135 i = i + 1 136 } 137 return 1 138} 139 140// Find the next newline starting from off; returns the offset of 141// the '\n' byte, or in_len if EOF. 142func _find_newline(buf: *u8, off: i64, in_len: i64) -> i64 { 143 var i: i64 = off 144 while i < in_len { 145 if (buf[i] & 0xff) == 0x0A { return i } 146 i = i + 1 147 } 148 return in_len 149} 150 151// Helper: write u8-array literal for "CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE". 152// We can't use string literals as *u8 directly in nx; emit bytes. 153func _pat_class_cert(buf: *u8) -> i64 { 154 // "CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE" (40 bytes) 155 buf[0]=0x43; buf[1]=0x4B; buf[2]=0x41; buf[3]=0x5F // "CKA_" 156 buf[4]=0x43; buf[5]=0x4C; buf[6]=0x41; buf[7]=0x53; buf[8]=0x53 // "CLASS" 157 buf[9]=0x20 // " " 158 buf[10]=0x43; buf[11]=0x4B; buf[12]=0x5F // "CK_" 159 buf[13]=0x4F; buf[14]=0x42; buf[15]=0x4A; buf[16]=0x45; buf[17]=0x43; buf[18]=0x54 // "OBJECT" 160 buf[19]=0x5F; buf[20]=0x43; buf[21]=0x4C; buf[22]=0x41; buf[23]=0x53; buf[24]=0x53 // "_CLASS" 161 buf[25]=0x20 // " " 162 buf[26]=0x43; buf[27]=0x4B; buf[28]=0x4F; buf[29]=0x5F // "CKO_" 163 buf[30]=0x43; buf[31]=0x45; buf[32]=0x52; buf[33]=0x54; buf[34]=0x49 // "CERTI" 164 buf[35]=0x46; buf[36]=0x49; buf[37]=0x43; buf[38]=0x41; buf[39]=0x54; buf[40]=0x45 // "FICATE" 165 return 41 166} 167 168// "CKA_VALUE MULTILINE_OCTAL" (25 bytes) 169func _pat_value_octal(buf: *u8) -> i64 { 170 buf[0]=0x43; buf[1]=0x4B; buf[2]=0x41; buf[3]=0x5F // "CKA_" 171 buf[4]=0x56; buf[5]=0x41; buf[6]=0x4C; buf[7]=0x55; buf[8]=0x45 // "VALUE" 172 buf[9]=0x20 // " " 173 buf[10]=0x4D; buf[11]=0x55; buf[12]=0x4C; buf[13]=0x54; buf[14]=0x49 // "MULTI" 174 buf[15]=0x4C; buf[16]=0x49; buf[17]=0x4E; buf[18]=0x45 // "LINE" 175 buf[19]=0x5F; buf[20]=0x4F; buf[21]=0x43; buf[22]=0x54; buf[23]=0x41; buf[24]=0x4C // "_OCTAL" 176 return 25 177} 178 179// "END" (3 bytes); the END line MUST be exactly "END" (possibly 180// followed by whitespace + newline). 181func _pat_end(buf: *u8) -> i64 { 182 buf[0]=0x45; buf[1]=0x4E; buf[2]=0x44 183 return 3 184} 185 186// Decode a single octal escape `\NNN` starting at off. On success 187// writes the byte value to *out_byte and returns 4 (bytes consumed). 188// On malformed input returns negative. 189func _decode_octal_escape(buf: *u8, off: i64, in_len: i64, 190 out_byte: *i64) -> i64 { 191 if off + 4 > in_len { return 0 - NX_NSS_CD_BAD_ESCAPE } 192 if (buf[off] & 0xff) != 0x5C { return 0 - NX_NSS_CD_BAD_ESCAPE } // '\\' 193 let a: i64 = buf[off + 1] & 0xff 194 let b: i64 = buf[off + 2] & 0xff 195 let c: i64 = buf[off + 3] & 0xff 196 if _is_octal(a) != 1 { return 0 - NX_NSS_CD_BAD_ESCAPE } 197 if _is_octal(b) != 1 { return 0 - NX_NSS_CD_BAD_ESCAPE } 198 if _is_octal(c) != 1 { return 0 - NX_NSS_CD_BAD_ESCAPE } 199 *out_byte = ((a - 0x30) * 64) + ((b - 0x30) * 8) + (c - 0x30) 200 return 4 201} 202 203// Parse one MULTILINE_OCTAL line into bytes appended to out_buf 204// starting at *out_pos. Updates *out_pos. Returns 0 on success, 205// negative verdict on malformed escape OR output-overflow. 206func _parse_octal_line( 207 in_buf: *u8, line_off: i64, line_len: i64, 208 out_buf: *u8, out_cap: i64, out_pos: *i64 209) -> i64 { 210 let in_end: i64 = line_off + line_len 211 var p: i64 = line_off 212 let byte_p: *i64 = sys_mmap(16) as *i64 213 while p < in_end { 214 // Skip leading whitespace 215 let bp: i64 = in_buf[p] & 0xff 216 if _is_space(bp) == 1 { p = p + 1 } else { 217 // Expect '\' 218 let rc: i64 = _decode_octal_escape(in_buf, p, in_end, byte_p) 219 if rc < 0 { return rc } 220 if *out_pos >= out_cap { return 0 - NX_NSS_CD_OUTPUT_FULL } 221 out_buf[*out_pos] = (*byte_p & 0xff) as u8 222 *out_pos = *out_pos + 1 223 p = p + rc 224 } 225 } 226 return 0 227} 228 229// Top-level parse. Walks in_buf via the line-by-line state 230// machine described in the preamble. Emits bundle wire format 231// to out_buf. Sets *out_n_certs to the count of certs extracted. 232func nx_nss_certdata_parse( 233 in_buf: *u8, in_len: i64, 234 out_buf: *u8, out_cap: i64, 235 out_n_certs: *i64 236) -> i64 { 237 // Reserve 2 bytes at out_buf[0..2] for the cert_count BE 238 // header. Cert entries written starting at out_buf[2]. 239 if out_cap < 2 { return NX_NSS_CD_OUTPUT_FULL } 240 var out_pos: i64 = 2 241 242 // Allocate pattern scratch + per-cert DER scratch. 243 let pat_cert: *u8 = sys_mmap(64) 244 let pat_value: *u8 = sys_mmap(64) 245 let pat_end: *u8 = sys_mmap(8) 246 let n_cert_pat: i64 = _pat_class_cert(pat_cert) 247 let n_value_pat: i64 = _pat_value_octal(pat_value) 248 let n_end_pat: i64 = _pat_end(pat_end) 249 let der_scratch: *u8 = sys_mmap(NX_NSS_CD_MAX_DER_SIZE) 250 let der_pos_p: *i64 = sys_mmap(16) as *i64 251 252 var state: i64 = _ST_IDLE 253 var n_certs: i64 = 0 254 var line_off: i64 = 0 255 256 while line_off < in_len { 257 let nl: i64 = _find_newline(in_buf, line_off, in_len) 258 // Compute "trimmed" line length: drop trailing CR. 259 var line_len: i64 = nl - line_off 260 if line_len > 0 { 261 if (in_buf[line_off + line_len - 1] & 0xff) == 0x0D { 262 line_len = line_len - 1 263 } 264 } 265 266 // Skip blanks + comments + skip in IDLE/IN_CERT states. 267 // (In IN_VALUE state, '#' is legal inside octal escapes 268 // only AFTER '\', which can't be a '#' anyway, so '#' at 269 // line start during IN_VALUE means malformed -- but real 270 // certdata.txt never puts comments inside MULTILINE_OCTAL.) 271 var skip_line: i64 = 0 272 if line_len == 0 { skip_line = 1 } 273 if line_len > 0 { 274 if state != _ST_IN_VALUE { 275 if (in_buf[line_off] & 0xff) == 0x23 { skip_line = 1 } // '#' 276 } 277 } 278 279 if skip_line == 0 { 280 if state == _ST_IDLE { 281 if _starts_with(in_buf, line_off, in_len, 282 pat_cert, n_cert_pat) == 1 { 283 state = _ST_IN_CERT 284 *der_pos_p = 0 285 } 286 } else { if state == _ST_IN_CERT { 287 if _starts_with(in_buf, line_off, in_len, 288 pat_value, n_value_pat) == 1 { 289 state = _ST_IN_VALUE 290 *der_pos_p = 0 291 } 292 } else { if state == _ST_IN_VALUE { 293 // Check for END (must be EXACTLY "END" on its own line) 294 if line_len == n_end_pat { 295 if _starts_with(in_buf, line_off, in_len, 296 pat_end, n_end_pat) == 1 { 297 // Emit (3-byte len + DER bytes) to output. 298 let der_len: i64 = *der_pos_p 299 if der_len < 1 { return NX_NSS_CD_BAD_DER_SIZE } 300 if der_len > NX_NSS_CD_MAX_DER_SIZE { return NX_NSS_CD_BAD_DER_SIZE } 301 if out_pos + 3 + der_len > out_cap { return NX_NSS_CD_OUTPUT_FULL } 302 out_buf[out_pos] = ((der_len >> 16) & 0xff) as u8 303 out_buf[out_pos + 1] = ((der_len >> 8) & 0xff) as u8 304 out_buf[out_pos + 2] = (der_len & 0xff) as u8 305 var k: i64 = 0 306 while k < der_len { 307 out_buf[out_pos + 3 + k] = der_scratch[k] 308 k = k + 1 309 } 310 out_pos = out_pos + 3 + der_len 311 n_certs = n_certs + 1 312 if n_certs >= NX_NSS_CD_MAX_CERTS { return NX_NSS_CD_TOO_MANY } 313 state = _ST_IDLE 314 } 315 } 316 // Not END (or END check failed): parse this line as octal escapes 317 if state == _ST_IN_VALUE { 318 let rc: i64 = _parse_octal_line( 319 in_buf, line_off, line_len, 320 der_scratch, NX_NSS_CD_MAX_DER_SIZE, der_pos_p 321 ) 322 if rc < 0 { return 0 - rc } 323 } 324 } } } 325 } 326 327 // Advance to next line 328 if nl < in_len { line_off = nl + 1 } else { line_off = in_len } 329 } 330 331 if n_certs == 0 { return NX_NSS_CD_NO_CERTS } 332 333 // Write cert_count BE header at out_buf[0..2]. 334 out_buf[0] = ((n_certs >> 8) & 0xff) as u8 335 out_buf[1] = (n_certs & 0xff) as u8 336 *out_n_certs = n_certs 337 return NX_NSS_CD_OK 338} 339 340// Compile-only smoke. Real KAT in nx_nss_certdata_parse_test.nx. 341func main() -> i64 { 342 return 0 343}