nx_pem.nx source
↩ module page · 190 lines · 6559 B
1// pem.nx -- RFC 7468 PEM (Privacy-Enhanced Mail) container parser.
2//
3// PEM is the ASCII-armor wrapper around binary DER content in X.509
4// certs, PKCS#8 private keys, SSH keys, cryptographic signatures.
5//
6// Format:
7// -----BEGIN <LABEL>-----
8// <base64-encoded DER, line-wrapped at 64 chars>
9// -----END <LABEL>-----
10//
11// Common labels:
12// CERTIFICATE (X.509 public cert)
13// PRIVATE KEY (PKCS#8 envelope, algorithm in ASN.1 inside)
14// PUBLIC KEY (SubjectPublicKeyInfo DER)
15// RSA PRIVATE KEY (PKCS#1; legacy, still used)
16// ENCRYPTED PRIVATE KEY (PKCS#8 with PBKDF2)
17//
18// API:
19// pem_decode(buf, len, out_der, out_cap) -> (label_off, label_len,
20// der_len)
21// Returns DER bytes written to out_der, or negative on error.
22// Caller supplies out_der sized >= 3/4 of base64 input.
23//
24// Invariants:
25// PEM1 Finds the FIRST BEGIN/END block in the input; callers
26// needing multiple blocks (cert chains) iterate by
27// advancing past the previous END line.
28// PEM2 Label must match between BEGIN and END (RFC 7468 ยง3);
29// mismatch returns a negative error.
30// PEM3 Whitespace inside the base64 body (newlines, spaces)
31// is tolerated and stripped before base64_decode. Other
32// non-alphabet bytes return an error.
33// PEM4 Output buffer bounds-checked; overrun returns negative.
34
35// nx_safety_envelope:
36// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
37// sil_target: SIL1
38// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
39// verdict: NOT_YET_EVALUATED
40
41import "nx_syscalls.nx"
42import "nx_base64.nx"
43
44const PEM_ERR_NO_BEGIN: i64 = -1
45const PEM_ERR_NO_END: i64 = -2
46const PEM_ERR_LABEL_MISMATCH: i64 = -3
47const PEM_ERR_OVERFLOW: i64 = -4
48const PEM_ERR_BAD_BODY: i64 = -5
49
50// Find the literal "-----" pattern starting at pos. Returns
51// offset of the first '-', or -1 if not found.
52func pem_find_dashes(buf: *u8, len: i64, pos: i64) -> i64 {
53 var p: i64 = pos
54 while p + 4 < len {
55 if buf[p] == 0x2D {
56 if buf[p+1] == 0x2D {
57 if buf[p+2] == 0x2D {
58 if buf[p+3] == 0x2D {
59 if buf[p+4] == 0x2D { return p }
60 }
61 }
62 }
63 }
64 p = p + 1
65 }
66 return -1
67}
68
69// Match literal at buf[off..off+n] against cstring lit.
70func pem_match_lit(buf: *u8, off: i64, lit: *u8) -> i64 {
71 var i: i64 = 0
72 while lit[i] != 0 {
73 if buf[off + i] != lit[i] { return 0 }
74 i = i + 1
75 }
76 return i
77}
78
79// Result holder (avoids the &struct.field limitation).
80struct PemResult {
81 label_off: i64,
82 label_len: i64,
83 body_off: i64,
84 body_end: i64,
85}
86
87// Locate the first BEGIN/END block. Writes label + body offsets.
88// Does NOT decode base64 yet. Returns 0 on success or negative.
89func pem_locate(buf: *u8, len: i64, out: *PemResult) -> i64 {
90 let begin_pos: i64 = pem_find_dashes(buf, len, 0)
91 if begin_pos < 0 { return PEM_ERR_NO_BEGIN }
92 // Check for "-----BEGIN ".
93 let begin_lit: *u8 = "-----BEGIN "
94 if pem_match_lit(buf, begin_pos, begin_lit) == 0 {
95 return PEM_ERR_NO_BEGIN
96 }
97 // Label runs from after "-----BEGIN " to the next "-----".
98 let label_start: i64 = begin_pos + 11
99 let label_end_dashes: i64 = pem_find_dashes(buf, len, label_start)
100 if label_end_dashes < 0 { return PEM_ERR_NO_BEGIN }
101 out.label_off = label_start
102 out.label_len = label_end_dashes - label_start
103
104 // Body starts after the closing "-----" + any trailing CR/LF.
105 var body_start: i64 = label_end_dashes + 5
106 while body_start < len {
107 let b: i64 = buf[body_start]
108 if b == 0x0A { body_start = body_start + 1 }
109 else {
110 if b == 0x0D { body_start = body_start + 1 }
111 else { break }
112 }
113 }
114 // Body ends at "-----END <LABEL>-----".
115 let end_dashes: i64 = pem_find_dashes(buf, len, body_start)
116 if end_dashes < 0 { return PEM_ERR_NO_END }
117 // Verify "-----END ".
118 let end_lit: *u8 = "-----END "
119 if pem_match_lit(buf, end_dashes, end_lit) == 0 { return PEM_ERR_NO_END }
120 // Verify label matches.
121 var k: i64 = 0
122 while k < out.label_len {
123 if buf[end_dashes + 9 + k] != buf[out.label_off + k] {
124 return PEM_ERR_LABEL_MISMATCH
125 }
126 k = k + 1
127 }
128 out.body_off = body_start
129 out.body_end = end_dashes
130 return 0
131}
132
133// Full decode: locate + base64-decode the body into caller's
134// out_der buffer. Returns number of DER bytes produced, or
135// negative. Writes label info to label_off / label_len slots.
136func pem_decode(buf: *u8, len: i64,
137 out_der: *u8, out_cap: i64,
138 label_off_out: *i64, label_len_out: *i64) -> i64 {
139 let res_raw: *u8 = sys_mmap(64)
140 let res: *PemResult = res_raw as *PemResult
141 let rc: i64 = pem_locate(buf, len, res)
142 if rc < 0 { return rc }
143 *label_off_out = res.label_off
144 *label_len_out = res.label_len
145
146 // Copy body to a clean scratch with whitespace stripped, then
147 // base64-decode. Max output = body_len * 3 / 4.
148 let body_len: i64 = res.body_end - res.body_off
149 if body_len > out_cap * 2 { return PEM_ERR_OVERFLOW } // guard
150 let scratch: *u8 = sys_mmap(body_len + 16)
151 var si: i64 = 0
152 var bi: i64 = res.body_off
153 while bi < res.body_end {
154 let b: i64 = buf[bi]
155 if b != 0x0A {
156 if b != 0x0D {
157 if b != 0x20 {
158 if b != 0x09 {
159 scratch[si] = b
160 si = si + 1
161 }
162 }
163 }
164 }
165 bi = bi + 1
166 }
167 let n_der: i64 = b64_decode(scratch, si, out_der)
168 if n_der < 0 { return PEM_ERR_BAD_BODY }
169 return n_der
170}
171
172// Compile-only smoke: parse a stub PEM block. Input is:
173// -----BEGIN TEST-----
174// Zm9vYmFy
175// -----END TEST-----
176// (base64 of "foobar" = Zm9vYmFy, 6 byte output).
177func main() -> i64 {
178 let pem: *u8 = "-----BEGIN TEST-----\nZm9vYmFy\n-----END TEST-----\n"
179 var n: i64 = 0
180 while pem[n] != 0 { n = n + 1 }
181
182 let der: *u8 = sys_mmap(32)
183 let lo: *i64 = sys_mmap(16) as *i64
184 let ll: *i64 = sys_mmap(16) as *i64
185 let result: i64 = pem_decode(pem, n, der, 32, lo, ll)
186 if result != 6 { return 1 }
187 if der[0] != 0x66 { return 2 } // 'f' of "foobar"
188 if *ll != 4 { return 3 } // "TEST"
189 return 0
190}