nx_x509.nx source
↩ module page · 279 lines · 11374 B
1// x509.nx -- minimal X.509 v3 certificate parser (RFC 5280).
2//
3// Reader-only subset that extracts the fields needed for TLS 1.3
4// certificate chain verification:
5// - tbsCertificate bytes + length (to hash for signature verify)
6// - serialNumber (for revocation / pinning)
7// - subjectPublicKeyInfo (the public key we trust)
8// - signatureAlgorithm OID (tells us which verify to use)
9// - signatureValue BIT STRING (the actual signature bytes)
10//
11// ASN.1 structure (abbreviated from RFC 5280 §4.1):
12// Certificate ::= SEQUENCE {
13// tbsCertificate TBSCertificate,
14// signatureAlgorithm AlgorithmIdentifier,
15// signatureValue BIT STRING
16// }
17// TBSCertificate ::= SEQUENCE {
18// version [0] EXPLICIT Version DEFAULT v1,
19// serialNumber CertificateSerialNumber,
20// signature AlgorithmIdentifier,
21// issuer Name,
22// validity Validity,
23// subject Name,
24// subjectPublicKeyInfo SubjectPublicKeyInfo,
25// ... optional extensions
26// }
27//
28// Invariants:
29// X1 Returns pointers into the caller's DER buffer; nothing
30// copied. Caller owns the buffer's lifetime.
31// X2 All bounds-checks delegated to asn1.nx (A1 invariant).
32// Malformed certs fail with a negative return code, never
33// silent truncation.
34// X3 Version field is optional (DEFAULT v1); we detect the [0]
35// EXPLICIT wrapper and skip over it if present.
36// X4 Extensions (v3 only) are parsed enough to find
37// Subject Alternative Name -- the rest is skipped. Extension
38// OID matching is caller-driven via x509_next_extension.
39//
40// license_tier: INDEPENDENT_REDERIVE
41// genealogy_id: international-research-sources/ietf/rfc_8446
42//
43
44// nx_safety_envelope:
45// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
46// sil_target: SIL1
47// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
48// verdict: NOT_YET_EVALUATED
49
50import "nx_syscalls.nx"
51import "nx_asn1.nx"
52
53// Parsed-cert view. Holds offset+length pointers into the caller's
54// DER buffer for each interesting field. Returns from x509_parse.
55struct X509Cert {
56 tbs_off: i64, tbs_len: i64,
57 serial_off: i64, serial_len: i64,
58 sig_alg_off: i64, sig_alg_len: i64,
59 spki_off: i64, spki_len: i64,
60 sig_off: i64, sig_len: i64,
61 // SubjectPublicKeyInfo decomposed further for verify:
62 pubkey_off: i64, pubkey_len: i64,
63 pubkey_alg_off: i64, pubkey_alg_len: i64,
64 // Validity SEQUENCE body (two ASN.1 time TLVs back-to-back).
65 // Caller passes (buf, cert) to x509_validity_get for parse.
66 validity_off: i64, validity_len: i64,
67 // Extensions [3] EXPLICIT wrapper -- extensions_off = -1 means
68 // the cert has no v3 extensions block.
69 extensions_off: i64, extensions_len: i64,
70 // Issuer + Subject Name SEQUENCEs (full DER TLV: tag + length +
71 // body). Captured for chain-walker DN matching per RFC 5280
72 // §6.1 -- cert[i].issuer must match cert[i+1].subject via DER-
73 // equal byte comparison.
74 issuer_off: i64, issuer_len: i64,
75 subject_off: i64, subject_len: i64,
76}
77
78// Internal helper: read a TLV at cursor; write the value offset +
79// length to *out_val_off / *out_val_len; advance cursor past value.
80// Returns 0 on success, negative on error.
81func x509_read_tlv(buf: *u8, c: *Asn1Cursor,
82 expected_tag: i64,
83 out_val_off: *i64, out_val_len: *i64) -> i64 {
84 let len_p: *i64 = sys_mmap(16) as *i64
85 let rc: i64 = asn1_expect_tag(buf, c, expected_tag, len_p)
86 if rc < 0 { return rc }
87 *out_val_off = c.pos
88 *out_val_len = *len_p
89 c.pos = c.pos + *len_p
90 return 0
91}
92
93// Parse an AlgorithmIdentifier at the cursor. Writes the whole
94// SEQUENCE's offset+length to *out_off / *out_len so the caller
95// can identify the algorithm OID by byte comparison.
96// AlgorithmIdentifier ::= SEQUENCE { algorithm OID, parameters ANY }
97func x509_read_alg_id(buf: *u8, c: *Asn1Cursor,
98 out_off: *i64, out_len: *i64) -> i64 {
99 // Capture position BEFORE reading the tag so the output covers
100 // the whole TLV (tag+length+value), which is what cert-chain
101 // verifiers hash.
102 let start: i64 = c.pos
103 let len_p: *i64 = sys_mmap(16) as *i64
104 let rc: i64 = asn1_expect_tag(buf, c, ASN1_SEQUENCE, len_p)
105 if rc < 0 { return rc }
106 c.pos = c.pos + *len_p
107 *out_off = start
108 *out_len = c.pos - start
109 return 0
110}
111
112// Parse a cert. `buf` points at the DER-encoded certificate
113// (32 bytes minimum of structure); `buf_len` caps the read.
114// Fills `cert` with offsets+lengths into buf for each field.
115// Returns 0 on success, negative on malformed input.
116func x509_parse(buf: *u8, buf_len: i64, cert: *X509Cert) -> i64 {
117 let c_raw: *u8 = sys_mmap(32)
118 let c: *Asn1Cursor = c_raw as *Asn1Cursor
119 asn1_cursor_init(c, buf_len)
120
121 // Outermost SEQUENCE.
122 let cert_len_p: *i64 = sys_mmap(16) as *i64
123 let rc1: i64 = asn1_expect_tag(buf, c, ASN1_SEQUENCE, cert_len_p)
124 if rc1 < 0 { return rc1 }
125 // Treat cursor's new end as cert_body_end so we can trust
126 // subsequent reads to stay inside the outer SEQUENCE.
127 let cert_end: i64 = c.pos + *cert_len_p
128 if cert_end > buf_len { return ASN1_ERR_OOB }
129
130 // --- tbsCertificate (SEQUENCE) ---
131 // Capture start so we can record tbs_off + tbs_len for signature
132 // verify (the hash input is the raw DER of tbsCertificate
133 // INCLUDING its outer tag and length).
134 let tbs_start: i64 = c.pos
135 let tbs_len_p: *i64 = sys_mmap(16) as *i64
136 let rc2: i64 = asn1_expect_tag(buf, c, ASN1_SEQUENCE, tbs_len_p)
137 if rc2 < 0 { return rc2 }
138 let tbs_body_end: i64 = c.pos + *tbs_len_p
139 cert.tbs_off = tbs_start
140 cert.tbs_len = tbs_body_end - tbs_start
141
142 // Version [0] EXPLICIT (optional).
143 if c.pos < tbs_body_end {
144 if buf[c.pos] == 0xA0 {
145 // Skip the [0] EXPLICIT wrapper.
146 c.pos = c.pos + 1
147 let vl: i64 = asn1_read_length(buf, c)
148 if vl < 0 { return vl }
149 c.pos = c.pos + vl
150 }
151 }
152
153 // serialNumber INTEGER. Scratch slots hold the result; copy
154 // into cert after because NishiLang can't take &struct.field.
155 let s_off_p: *i64 = sys_mmap(16) as *i64
156 let s_len_p: *i64 = sys_mmap(16) as *i64
157 let rc3: i64 = x509_read_tlv(buf, c, ASN1_INTEGER, s_off_p, s_len_p)
158 if rc3 < 0 { return rc3 }
159 cert.serial_off = *s_off_p
160 cert.serial_len = *s_len_p
161
162 // signature AlgorithmIdentifier (TBS-level; usually identical to
163 // the outer one). We skip past it but don't store -- outer one
164 // is what verifiers consult.
165 let alg_off_tbs: *i64 = sys_mmap(16) as *i64
166 let alg_len_tbs: *i64 = sys_mmap(16) as *i64
167 let rc4: i64 = x509_read_alg_id(buf, c, alg_off_tbs, alg_len_tbs)
168 if rc4 < 0 { return rc4 }
169
170 // issuer Name (SEQUENCE of RDNs) -- capture full TLV (tag+len+body)
171 // for chain-walker DN matching.
172 let issuer_start: i64 = c.pos
173 let skip_len: *i64 = sys_mmap(16) as *i64
174 let rc5: i64 = asn1_expect_tag(buf, c, ASN1_SEQUENCE, skip_len)
175 if rc5 < 0 { return rc5 }
176 c.pos = c.pos + *skip_len
177 cert.issuer_off = issuer_start
178 cert.issuer_len = c.pos - issuer_start
179
180 // validity Validity (SEQUENCE of two times) -- capture body
181 // offset/length for x509_validity_get to parse later.
182 let val_len_p: *i64 = sys_mmap(16) as *i64
183 let rc6: i64 = asn1_expect_tag(buf, c, ASN1_SEQUENCE, val_len_p)
184 if rc6 < 0 { return rc6 }
185 cert.validity_off = c.pos
186 cert.validity_len = *val_len_p
187 c.pos = c.pos + *val_len_p
188
189 // subject Name -- capture full TLV (tag+len+body) for chain-
190 // walker DN matching. cert[i].issuer == cert[i+1].subject per
191 // RFC 5280 §6.1.
192 let subject_start: i64 = c.pos
193 let rc7: i64 = asn1_expect_tag(buf, c, ASN1_SEQUENCE, skip_len)
194 if rc7 < 0 { return rc7 }
195 c.pos = c.pos + *skip_len
196 cert.subject_off = subject_start
197 cert.subject_len = c.pos - subject_start
198
199 // subjectPublicKeyInfo SEQUENCE { algorithm AlgorithmIdentifier,
200 // subjectPublicKey BIT STRING }
201 let spki_start: i64 = c.pos
202 let spki_len_p: *i64 = sys_mmap(16) as *i64
203 let rc8: i64 = asn1_expect_tag(buf, c, ASN1_SEQUENCE, spki_len_p)
204 if rc8 < 0 { return rc8 }
205 let spki_body_end: i64 = c.pos + *spki_len_p
206 cert.spki_off = spki_start
207 cert.spki_len = spki_body_end - spki_start
208
209 // Inner: algorithm AlgorithmIdentifier.
210 let pa_off_p: *i64 = sys_mmap(16) as *i64
211 let pa_len_p: *i64 = sys_mmap(16) as *i64
212 let rc9: i64 = x509_read_alg_id(buf, c, pa_off_p, pa_len_p)
213 if rc9 < 0 { return rc9 }
214 cert.pubkey_alg_off = *pa_off_p
215 cert.pubkey_alg_len = *pa_len_p
216
217 // Inner: subjectPublicKey BIT STRING. The first byte of a
218 // BIT STRING is "unused bits in last byte", always 0 for keys.
219 let pub_len_p: *i64 = sys_mmap(16) as *i64
220 let rc10: i64 = asn1_expect_tag(buf, c, ASN1_BIT_STRING, pub_len_p)
221 if rc10 < 0 { return rc10 }
222 // Skip the "unused bits" byte; the key bytes follow.
223 cert.pubkey_off = c.pos + 1
224 cert.pubkey_len = *pub_len_p - 1
225 c.pos = spki_body_end
226
227 // Optional v3 extensions: [3] EXPLICIT SEQUENCE OF Extension.
228 // Detect the 0xA3 wrapper and capture its body offset/length.
229 // Absent -> extensions_off = -1.
230 cert.extensions_off = 0 - 1
231 cert.extensions_len = 0
232 if c.pos < tbs_body_end {
233 if buf[c.pos] == 0xA3 {
234 c.pos = c.pos + 1
235 let ext_wrapper_len: i64 = asn1_read_length(buf, c)
236 if ext_wrapper_len < 0 { return ext_wrapper_len }
237 cert.extensions_off = c.pos
238 cert.extensions_len = ext_wrapper_len
239 }
240 }
241
242 // Jump past any optional extensions to the outer SEQUENCE's
243 // remaining siblings (signatureAlgorithm + signatureValue).
244 c.pos = tbs_body_end
245
246 // --- signatureAlgorithm (outer) ---
247 let sa_off_p: *i64 = sys_mmap(16) as *i64
248 let sa_len_p: *i64 = sys_mmap(16) as *i64
249 let rc11: i64 = x509_read_alg_id(buf, c, sa_off_p, sa_len_p)
250 if rc11 < 0 { return rc11 }
251 cert.sig_alg_off = *sa_off_p
252 cert.sig_alg_len = *sa_len_p
253
254 // --- signatureValue BIT STRING ---
255 let sig_len_p: *i64 = sys_mmap(16) as *i64
256 let rc12: i64 = asn1_expect_tag(buf, c, ASN1_BIT_STRING, sig_len_p)
257 if rc12 < 0 { return rc12 }
258 cert.sig_off = c.pos + 1 // skip unused-bits byte
259 cert.sig_len = *sig_len_p - 1
260 c.pos = c.pos + *sig_len_p
261
262 return 0
263}
264
265// Compile-only smoke: parse a hand-crafted minimal cert stub.
266// Real cert parsing validated against known CA certs pending
267// execution harness.
268func main() -> i64 {
269 let buf: *u8 = sys_mmap(32)
270 // Minimal SEQUENCE { SEQUENCE{...tbs stub...} alg sig }
271 // Too small to be a real cert, so we expect failure -- just
272 // verify the parser doesn't crash.
273 buf[0] = 0x30; buf[1] = 0x00
274 let cert_raw: *u8 = sys_mmap(256)
275 let cert: *X509Cert = cert_raw as *X509Cert
276 let rc: i64 = x509_parse(buf, 2, cert)
277 // Expect a negative rc (malformed input); main returns its code.
278 return 0 - rc
279}