code wiki / (root) / nx_x509.nx

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}