pem.nx source
↩ module page · 184 lines · 6486 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
35import "syscalls.nx"
36import "base64.nx"
37
38const PEM_ERR_NO_BEGIN: i64 = -1
39const PEM_ERR_NO_END: i64 = -2
40const PEM_ERR_LABEL_MISMATCH: i64 = -3
41const PEM_ERR_OVERFLOW: i64 = -4
42const PEM_ERR_BAD_BODY: i64 = -5
43
44// Find the literal "-----" pattern starting at pos. Returns
45// offset of the first '-', or -1 if not found.
46func pem_find_dashes(buf: *u8, len: i64, pos: i64) -> i64 {
47 var p: i64 = pos
48 while p + 4 < len {
49 if buf[p] == 0x2D {
50 if buf[p+1] == 0x2D {
51 if buf[p+2] == 0x2D {
52 if buf[p+3] == 0x2D {
53 if buf[p+4] == 0x2D { return p }
54 }
55 }
56 }
57 }
58 p = p + 1
59 }
60 return -1
61}
62
63// Match literal at buf[off..off+n] against cstring lit.
64func pem_match_lit(buf: *u8, off: i64, lit: *u8) -> i64 {
65 var i: i64 = 0
66 while lit[i] != 0 {
67 if buf[off + i] != lit[i] { return 0 }
68 i = i + 1
69 }
70 return i
71}
72
73// Result holder (avoids the &struct.field limitation).
74struct PemResult {
75 label_off: i64,
76 label_len: i64,
77 body_off: i64,
78 body_end: i64,
79}
80
81// Locate the first BEGIN/END block. Writes label + body offsets.
82// Does NOT decode base64 yet. Returns 0 on success or negative.
83func pem_locate(buf: *u8, len: i64, out: *PemResult) -> i64 {
84 let begin_pos: i64 = pem_find_dashes(buf, len, 0)
85 if begin_pos < 0 { return PEM_ERR_NO_BEGIN }
86 // Check for "-----BEGIN ".
87 let begin_lit: *u8 = "-----BEGIN "
88 if pem_match_lit(buf, begin_pos, begin_lit) == 0 {
89 return PEM_ERR_NO_BEGIN
90 }
91 // Label runs from after "-----BEGIN " to the next "-----".
92 let label_start: i64 = begin_pos + 11
93 let label_end_dashes: i64 = pem_find_dashes(buf, len, label_start)
94 if label_end_dashes < 0 { return PEM_ERR_NO_BEGIN }
95 out.label_off = label_start
96 out.label_len = label_end_dashes - label_start
97
98 // Body starts after the closing "-----" + any trailing CR/LF.
99 var body_start: i64 = label_end_dashes + 5
100 while body_start < len {
101 let b: i64 = buf[body_start]
102 if b == 0x0A { body_start = body_start + 1 }
103 else {
104 if b == 0x0D { body_start = body_start + 1 }
105 else { break }
106 }
107 }
108 // Body ends at "-----END <LABEL>-----".
109 let end_dashes: i64 = pem_find_dashes(buf, len, body_start)
110 if end_dashes < 0 { return PEM_ERR_NO_END }
111 // Verify "-----END ".
112 let end_lit: *u8 = "-----END "
113 if pem_match_lit(buf, end_dashes, end_lit) == 0 { return PEM_ERR_NO_END }
114 // Verify label matches.
115 var k: i64 = 0
116 while k < out.label_len {
117 if buf[end_dashes + 9 + k] != buf[out.label_off + k] {
118 return PEM_ERR_LABEL_MISMATCH
119 }
120 k = k + 1
121 }
122 out.body_off = body_start
123 out.body_end = end_dashes
124 return 0
125}
126
127// Full decode: locate + base64-decode the body into caller's
128// out_der buffer. Returns number of DER bytes produced, or
129// negative. Writes label info to label_off / label_len slots.
130func pem_decode(buf: *u8, len: i64,
131 out_der: *u8, out_cap: i64,
132 label_off_out: *i64, label_len_out: *i64) -> i64 {
133 let res_raw: *u8 = sys_mmap(64)
134 let res: *PemResult = res_raw as *PemResult
135 let rc: i64 = pem_locate(buf, len, res)
136 if rc < 0 { return rc }
137 *label_off_out = res.label_off
138 *label_len_out = res.label_len
139
140 // Copy body to a clean scratch with whitespace stripped, then
141 // base64-decode. Max output = body_len * 3 / 4.
142 let body_len: i64 = res.body_end - res.body_off
143 if body_len > out_cap * 2 { return PEM_ERR_OVERFLOW } // guard
144 let scratch: *u8 = sys_mmap(body_len + 16)
145 var si: i64 = 0
146 var bi: i64 = res.body_off
147 while bi < res.body_end {
148 let b: i64 = buf[bi]
149 if b != 0x0A {
150 if b != 0x0D {
151 if b != 0x20 {
152 if b != 0x09 {
153 scratch[si] = b
154 si = si + 1
155 }
156 }
157 }
158 }
159 bi = bi + 1
160 }
161 let n_der: i64 = b64_decode(scratch, si, out_der)
162 if n_der < 0 { return PEM_ERR_BAD_BODY }
163 return n_der
164}
165
166// Compile-only smoke: parse a stub PEM block. Input is:
167// -----BEGIN TEST-----
168// Zm9vYmFy
169// -----END TEST-----
170// (base64 of "foobar" = Zm9vYmFy, 6 byte output).
171func main() -> i64 {
172 let pem: *u8 = "-----BEGIN TEST-----\nZm9vYmFy\n-----END TEST-----\n"
173 var n: i64 = 0
174 while pem[n] != 0 { n = n + 1 }
175
176 let der: *u8 = sys_mmap(32)
177 let lo: *i64 = sys_mmap(16) as *i64
178 let ll: *i64 = sys_mmap(16) as *i64
179 let result: i64 = pem_decode(pem, n, der, 32, lo, ll)
180 if result != 6 { return 1 }
181 if der[0] != 0x66 { return 2 } // 'f' of "foobar"
182 if *ll != 4 { return 3 } // "TEST"
183 return 0
184}