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}