code wiki / (root) / pem.nx

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}