Skip to main content

simoom_tls/
pem.rs

1//! PEM (RFC 7468) and base64 encoding/decoding.
2
3const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
4
5pub fn base64_encode(data: &[u8]) -> String {
6    let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
7    for chunk in data.chunks(3) {
8        let b = [chunk[0], *chunk.get(1).unwrap_or(&0), *chunk.get(2).unwrap_or(&0)];
9        let n = (b[0] as u32) << 16 | (b[1] as u32) << 8 | b[2] as u32;
10        out.push(B64[(n >> 18) as usize & 63] as char);
11        out.push(B64[(n >> 12) as usize & 63] as char);
12        out.push(if chunk.len() > 1 { B64[(n >> 6) as usize & 63] as char } else { '=' });
13        out.push(if chunk.len() > 2 { B64[n as usize & 63] as char } else { '=' });
14    }
15    out
16}
17
18/// Decodes standard base64, ignoring whitespace. Returns None on invalid input.
19pub fn base64_decode(s: &str) -> Option<Vec<u8>> {
20    let mut out = Vec::with_capacity(s.len() * 3 / 4);
21    let mut acc = 0u32;
22    let mut bits = 0;
23    let mut pad = 0;
24    for c in s.bytes() {
25        let v = match c {
26            b'A'..=b'Z' => c - b'A',
27            b'a'..=b'z' => c - b'a' + 26,
28            b'0'..=b'9' => c - b'0' + 52,
29            b'+' => 62,
30            b'/' => 63,
31            b'=' => {
32                pad += 1;
33                continue;
34            }
35            b' ' | b'\t' | b'\r' | b'\n' => continue,
36            _ => return None,
37        };
38        if pad > 0 {
39            return None;
40        }
41        acc = (acc << 6) | v as u32;
42        bits += 6;
43        if bits >= 8 {
44            bits -= 8;
45            out.push((acc >> bits) as u8);
46        }
47    }
48    if pad > 2 {
49        return None;
50    }
51    Some(out)
52}
53
54/// One PEM block.
55#[derive(Clone, Debug, PartialEq, Eq)]
56pub struct PemBlock {
57    pub label: String,
58    pub der: Vec<u8>,
59}
60
61/// Parses every PEM block in `input` (text outside blocks is ignored; blocks
62/// with headers such as encrypted keys are rejected).
63pub fn parse_pem(input: &[u8]) -> Result<Vec<PemBlock>, String> {
64    let text = String::from_utf8_lossy(input);
65    let mut blocks = Vec::new();
66    let mut current: Option<(String, String)> = None;
67    for line in text.lines() {
68        let line = line.trim();
69        if let Some(rest) = line.strip_prefix("-----BEGIN ") {
70            if let Some(label) = rest.strip_suffix("-----") {
71                current = Some((label.to_string(), String::new()));
72                continue;
73            }
74        }
75        if let Some(rest) = line.strip_prefix("-----END ") {
76            if let Some(label) = rest.strip_suffix("-----") {
77                match current.take() {
78                    Some((l, body)) if l == label => {
79                        let der = base64_decode(&body).ok_or_else(|| format!("invalid base64 in PEM block {l}"))?;
80                        blocks.push(PemBlock { label: l, der });
81                    }
82                    _ => return Err(format!("mismatched PEM END {label}")),
83                }
84                continue;
85            }
86        }
87        if let Some((label, body)) = current.as_mut() {
88            if line.contains(':') {
89                return Err(format!("PEM block {label} has headers (encrypted keys are not supported)"));
90            }
91            body.push_str(line);
92        }
93    }
94    if let Some((l, _)) = current {
95        return Err(format!("unterminated PEM block {l}"));
96    }
97    Ok(blocks)
98}
99
100pub fn encode_pem(label: &str, der: &[u8]) -> String {
101    let b = base64_encode(der);
102    let mut out = format!("-----BEGIN {label}-----\n");
103    for chunk in b.as_bytes().chunks(64) {
104        out.push_str(std::str::from_utf8(chunk).unwrap());
105        out.push('\n');
106    }
107    out.push_str(&format!("-----END {label}-----\n"));
108    out
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn base64_rfc4648() {
117        let cases = [("", ""), ("f", "Zg=="), ("fo", "Zm8="), ("foo", "Zm9v"), ("foob", "Zm9vYg=="), ("fooba", "Zm9vYmE="), ("foobar", "Zm9vYmFy")];
118        for (plain, enc) in cases {
119            assert_eq!(base64_encode(plain.as_bytes()), enc);
120            assert_eq!(base64_decode(enc).unwrap(), plain.as_bytes());
121        }
122        assert!(base64_decode("Zm9v!").is_none());
123    }
124
125    #[test]
126    fn pem_roundtrip() {
127        let data: Vec<u8> = (0..200u8).collect();
128        let p = encode_pem("CERTIFICATE", &data) + "junk\n" + &encode_pem("PRIVATE KEY", &[1, 2, 3]);
129        let blocks = parse_pem(p.as_bytes()).unwrap();
130        assert_eq!(blocks.len(), 2);
131        assert_eq!(blocks[0].der, data);
132        assert_eq!(blocks[1].label, "PRIVATE KEY");
133    }
134}