use subtle::ConstantTimeEq;
use zeroize::{Zeroize, Zeroizing};
use super::der;
use super::types::Label;
use crate::util::{b64_decode_into, b64_encode_append, b64_encoded_len};
use crate::{Error, Result};
const BEGIN: &[u8] = b"-----BEGIN ";
const END: &[u8] = b"-----END ";
const DASHES: &[u8] = b"-----";
const LINE_WIDTH: usize = 64;
const LINE_OCTETS: usize = LINE_WIDTH / 4 * 3;
const PROC_TYPE: &[u8] = b"Proc-Type:";
const LONGEST_LABEL: usize = "ENCRYPTED PRIVATE KEY".len();
const SURROUNDING_WHITESPACE: usize = 8 * 1024;
const MAX_BODY_CHARS: usize = der::MAX_INPUT_LEN.div_ceil(3) * 4;
const MAX_BODY_LINES: usize = MAX_BODY_CHARS.div_ceil(LINE_WIDTH);
pub(crate) const MAX_INPUT_LEN: usize = MAX_BODY_CHARS
+ 2 * (MAX_BODY_LINES + 2)
+ BEGIN.len()
+ LONGEST_LABEL
+ DASHES.len()
+ END.len()
+ LONGEST_LABEL
+ DASHES.len()
+ SURROUNDING_WHITESPACE;
pub(crate) fn decode(text: &str) -> Result<(Label, Zeroizing<Vec<u8>>)> {
let mut body = Zeroizing::new(Vec::new());
let mut der = Zeroizing::new(Vec::new());
let label = decode_into(text.as_bytes(), &mut body, &mut der)?;
Ok((label, der))
}
fn decode_into(text: &[u8], body: &mut Vec<u8>, der: &mut Vec<u8>) -> Result<Label> {
if text.len() > MAX_INPUT_LEN {
return Err(Error::InvalidKey("PEM input too large"));
}
let result = parse(text, body).and_then(|label| {
b64_decode_into(body, der)
.map_err(|_| Error::InvalidKey("PEM body is not canonical padded base64"))?;
Ok(label)
});
body.as_mut_slice().zeroize();
if result.is_err() {
der.as_mut_slice().zeroize();
}
result
}
fn parse(text: &[u8], body: &mut Vec<u8>) -> Result<Label> {
let start = text
.iter()
.position(|&c| !is_whitespace(c))
.unwrap_or(text.len());
let rest = text[start..].strip_prefix(BEGIN).ok_or(Error::InvalidKey(
"PEM text does not start with a BEGIN line",
))?;
let (line, mut rest) =
split_line(rest).ok_or(Error::InvalidKey("PEM BEGIN line has no line ending"))?;
let label_text = line.strip_suffix(DASHES).ok_or(Error::InvalidKey(
"PEM BEGIN line does not end with five dashes",
))?;
let label = parse_label(label_text)?;
if is_unsupported(label) {
return Err(Error::UnsupportedAlgorithm);
}
body.zeroize();
body.reserve_exact(rest.len());
let mut last_width = None;
while !starts_with_ct(rest, END) {
let (line, next) =
split_line(rest).ok_or(Error::InvalidKey("PEM block has no END line"))?;
if last_width.is_none() && starts_with_ct(line, PROC_TYPE) {
return Err(Error::UnsupportedAlgorithm);
}
if last_width.is_some_and(|width| width != LINE_WIDTH)
|| line.is_empty()
|| line.len() > LINE_WIDTH
{
return Err(Error::InvalidKey("PEM base64 lines must be 64 characters"));
}
body.extend_from_slice(line);
last_width = Some(line.len());
rest = next;
}
if last_width.is_none() {
return Err(Error::InvalidKey("PEM block has an empty body"));
}
let after = rest[END.len()..]
.strip_prefix(label_text)
.and_then(|r| r.strip_prefix(DASHES))
.ok_or(Error::InvalidKey(
"PEM END line does not match the BEGIN line",
))?;
if !after.iter().all(|&c| is_whitespace(c)) {
return Err(Error::InvalidKey("PEM text after the END line"));
}
Ok(label)
}
fn is_unsupported(label: Label) -> bool {
matches!(label, Label::Other | Label::EncryptedPrivateKey)
}
fn is_whitespace(c: u8) -> bool {
matches!(c, b' ' | b'\t' | b'\r' | b'\n')
}
fn split_line(text: &[u8]) -> Option<(&[u8], &[u8])> {
let end = text.iter().position(|&c| c == b'\r' || c == b'\n')?;
let eol = if text[end..].starts_with(b"\r\n") {
2
} else {
1
};
Some((&text[..end], &text[end + eol..]))
}
fn starts_with_ct(text: &[u8], prefix: &[u8]) -> bool {
text.len() >= prefix.len() && bool::from(text[..prefix.len()].ct_eq(prefix))
}
fn parse_label(text: &[u8]) -> Result<Label> {
let is_labelchar = |c: u8| matches!(c, 0x21..=0x2c | 0x2e..=0x7e) && !c.is_ascii_lowercase();
let mut after_separator = true;
for &c in text {
if c == b'-' || c == b' ' {
if after_separator {
return Err(Error::InvalidKey("PEM label is malformed"));
}
after_separator = true;
} else if is_labelchar(c) {
after_separator = false;
} else {
return Err(Error::InvalidKey("PEM label is malformed"));
}
}
if after_separator && !text.is_empty() {
return Err(Error::InvalidKey("PEM label is malformed"));
}
let text =
core::str::from_utf8(text).map_err(|_| Error::InvalidKey("PEM label is malformed"))?;
Ok(Label::from_text(text))
}
pub(crate) fn encode(label: Label, der: &[u8]) -> Result<Zeroizing<String>> {
if is_unsupported(label) {
return Err(Error::UnsupportedAlgorithm);
}
let text = label.as_str().ok_or(Error::UnsupportedAlgorithm)?;
if der.is_empty() {
return Err(Error::InvalidKey("PEM body cannot be empty"));
}
let lines = der.len().div_ceil(LINE_OCTETS);
let len = BEGIN.len()
+ text.len()
+ DASHES.len()
+ 1
+ b64_encoded_len(der.len())
+ lines
+ END.len()
+ text.len()
+ DASHES.len()
+ 1;
let mut out = Zeroizing::new(String::with_capacity(len));
for part in ["-----BEGIN ", text, "-----\n"] {
out.push_str(part);
}
for chunk in der.chunks(LINE_OCTETS) {
b64_encode_append(chunk, &mut out);
out.push('\n');
}
for part in ["-----END ", text, "-----\n"] {
out.push_str(part);
}
debug_assert_eq!(out.len(), len);
Ok(out)
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::{Path, PathBuf};
use super::*;
use crate::json;
fn data_dir(dir: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/data/keys")
.join(dir)
}
fn read_text(dir: &str, name: &str) -> String {
fs::read_to_string(data_dir(dir).join(name)).unwrap()
}
fn read_der(dir: &str, name: &str) -> Vec<u8> {
fs::read(data_dir(dir).join(name)).unwrap()
}
fn pem_files(dir: &str) -> Vec<String> {
let mut names: Vec<String> = fs::read_dir(data_dir(dir))
.unwrap()
.map(|entry| entry.unwrap().file_name().into_string().unwrap())
.filter(|name| name.ends_with(".pem"))
.collect();
names.sort();
names
}
fn outcome(text: &str) -> core::result::Result<(Label, Vec<u8>), &'static str> {
decode(text)
.map(|(label, der)| (label, der.to_vec()))
.map_err(|e| e.code())
}
fn ed25519_der() -> Vec<u8> {
read_der("rfc", "rfc8037-a1-ed25519-pkcs8.der")
}
#[test]
fn negative_pem_vectors_fail_with_the_manifest_code() {
let manifest =
json::parse(&fs::read(data_dir("negative").join("manifest.json")).unwrap()).unwrap();
let mut checked = Vec::new();
for row in manifest.as_array().unwrap() {
let id = row.get("id").and_then(json::Value::as_str).unwrap();
let file = row.get("file").and_then(json::Value::as_str).unwrap();
let expect = row.get("expect").and_then(json::Value::as_str).unwrap();
if !id.starts_with('P') {
continue;
}
let got = outcome(&read_text("negative", file));
match id {
"P14" => assert_eq!(got, Ok((Label::PublicKey, ed25519_der())), "{id}"),
"P16" => {
let mut der = ed25519_der();
der.push(0);
assert_eq!(got, Ok((Label::PrivateKey, der)), "{id}");
}
"P17" => assert_eq!(got.map(|(label, _)| label), Ok(Label::PrivateKey), "{id}"),
"P18" => assert_eq!(got.map(|(label, _)| label), Ok(Label::EcPrivateKey), "{id}"),
_ => assert_eq!(got.map(|_| ()), Err(expect), "{id} {file}"),
}
checked.push(id.to_owned());
}
let expected: Vec<String> = (1..=18).map(|n| format!("P{n:02}")).collect();
assert_eq!(checked, expected);
}
#[test]
fn acceptance_vectors_decode_to_the_base_key() {
let der = ed25519_der();
for file in [
"A01-accept-crlf.pem",
"A02-accept-no-final-newline.pem",
"A03-accept-surrounding-whitespace.pem",
] {
assert_eq!(
outcome(&read_text("negative", file)),
Ok((Label::PrivateKey, der.clone())),
"{file}"
);
}
let cr_only = read_text("negative", "A01-accept-crlf.pem").replace("\r\n", "\r");
assert!(!cr_only.contains('\n'));
assert_eq!(outcome(&cr_only), Ok((Label::PrivateKey, der)));
}
fn expected_label(name: &str) -> Label {
if name.contains("-pkcs8") {
Label::PrivateKey
} else if name.contains("-spki") {
Label::PublicKey
} else if name.contains("-sec1") {
Label::EcPrivateKey
} else if name.contains("-pkcs1-pub") {
Label::RsaPublic
} else if name.contains("-private-pkcs1") {
Label::RsaPrivate
} else {
panic!("no label known for {name}")
}
}
#[test]
fn every_positive_corpus_file_decodes() {
let mut with_der = 0;
let mut total = 0;
for dir in ["openssl", "rfc"] {
for name in pem_files(dir)
.into_iter()
.filter(|n| !n.starts_with("reject-"))
{
let text = read_text(dir, &name);
let (label, der) = decode(&text).unwrap_or_else(|e| panic!("{dir}/{name}: {e:?}"));
assert_eq!(label, expected_label(&name), "{dir}/{name}");
let sibling = data_dir(dir).join(name.replace(".pem", ".der"));
if sibling.exists() {
assert_eq!(*der, fs::read(sibling).unwrap(), "{dir}/{name}");
with_der += 1;
}
total += 1;
}
}
assert!(total >= 47, "{total} corpus files");
assert!(with_der >= 28, "{with_der} DER siblings");
}
#[test]
fn reject_corpus_files_are_classified() {
let mut seen = 0;
for dir in ["openssl", "rfc"] {
for name in pem_files(dir)
.into_iter()
.filter(|n| n.starts_with("reject-"))
{
let got = outcome(&read_text(dir, &name)).map(|(label, _)| label);
let want = if name.contains("-cert") || name.contains("-encrypted") {
Err("UnsupportedAlgorithm")
} else if name.contains("six-dash") {
Err("InvalidKey")
} else {
Ok(expected_label(&name))
};
assert_eq!(got, want, "{dir}/{name}");
seen += 1;
}
}
assert!(seen >= 30, "{seen} reject files");
}
#[test]
fn encrypted_blocks_are_rejected_before_the_body() {
let mut texts = vec![read_text("openssl", "reject-ec-p256-pkcs8-encrypted.pem")];
let rsa = read_text("openssl", "reject-rsa2048-private-pkcs1.pem");
assert_eq!(outcome(&rsa).map(|(label, _)| label), Ok(Label::RsaPrivate));
{
let label = "ENCRYPTED PRIVATE KEY";
texts.push(block(label, ED25519_BODY));
texts.push(block(label, "not base64 at all *** QR=="));
texts.push(block(label, &"A".repeat(65)));
texts.push(format!("-----BEGIN {label}-----\n{ED25519_BODY}\n"));
texts.push(format!(
"-----BEGIN {label}-----\n{ED25519_BODY}\n-----END PUBLIC KEY-----\njunk"
));
}
for text in &texts {
let mut body = Vec::new();
let mut der = Vec::new();
assert_eq!(
decode_into(text.as_bytes(), &mut body, &mut der),
Err(Error::UnsupportedAlgorithm),
"{text:?}"
);
assert_eq!((body.capacity(), der.capacity()), (0, 0), "{text:?}");
assert_eq!(outcome(text).map(|_| ()), Err("UnsupportedAlgorithm"));
}
let mut body = Vec::new();
let mut der = Vec::new();
let ok = block("PRIVATE KEY", ED25519_BODY);
assert_eq!(
decode_into(ok.as_bytes(), &mut body, &mut der),
Ok(Label::PrivateKey)
);
assert!(body.capacity() > 0 && der.capacity() > 0);
}
#[test]
fn encoder_matches_the_openssl_fixtures() {
let mut checked = 0;
for dir in ["openssl", "rfc"] {
for name in pem_files(dir)
.into_iter()
.filter(|n| !n.starts_with("reject-"))
{
let sibling = data_dir(dir).join(name.replace(".pem", ".der"));
if !sibling.exists() {
continue;
}
let der = fs::read(sibling).unwrap();
let pem = encode(expected_label(&name), &der).unwrap();
assert_eq!(*pem, read_text(dir, &name), "{dir}/{name}");
checked += 1;
}
}
assert!(checked >= 28, "{checked}");
}
#[test]
fn encoder_wraps_at_64_and_sizes_exactly() {
for n in 1..=200usize {
let der: Vec<u8> = (0..n).map(|i| i as u8).collect();
let pem = encode(Label::PublicKey, &der).unwrap();
assert_eq!(pem.capacity(), pem.len(), "{n} octets: no reallocation");
let lines: Vec<&str> = pem.split_terminator('\n').collect();
assert_eq!(lines[0], "-----BEGIN PUBLIC KEY-----");
assert_eq!(*lines.last().unwrap(), "-----END PUBLIC KEY-----");
let body = &lines[1..lines.len() - 1];
assert_eq!(body.len(), n.div_ceil(48), "{n} octets");
for line in &body[..body.len() - 1] {
assert_eq!(line.len(), 64);
}
assert!((4..=64).contains(&body.last().unwrap().len()));
assert!(pem.ends_with("-----\n"));
assert_eq!(outcome(&pem), Ok((Label::PublicKey, der)));
}
}
#[test]
fn encoder_rejects_unsupported_labels_and_empty_input() {
let rsa_private = read_der("openssl", "reject-rsa2048-private-pkcs1.der");
for label in [Label::Other, Label::EncryptedPrivateKey] {
assert_eq!(
encode(label, &rsa_private).map(|_| ()),
Err(Error::UnsupportedAlgorithm),
"{label:?}"
);
}
assert!(matches!(
encode(Label::PrivateKey, &[]),
Err(Error::InvalidKey(_))
));
}
#[test]
fn encoder_returns_zeroizing_text() {
let pem: Result<Zeroizing<String>> = encode(Label::PrivateKey, &ed25519_der());
assert_eq!(
*pem.unwrap(),
read_text("rfc", "rfc8037-a1-ed25519-pkcs8.pem")
);
}
const ED25519_BODY: &str = "MC4CAQAwBQYDK2VwBCIEIJ1hsZ3v/VpguoRK9JLsLMREScVpezJpGXA7rAMcrn9g";
fn block(label: &str, body: &str) -> String {
format!("-----BEGIN {label}-----\n{body}\n-----END {label}-----\n")
}
#[test]
fn label_syntax() {
for bad in [
"private key",
"Private Key",
"PRIVATE KEY",
"PRIVATE--KEY",
"PRIVATE -KEY",
" PRIVATE KEY",
"PRIVATE KEY ",
"-PRIVATE KEY",
"PRIVATE KEY-",
"PRIVATE\tKEY",
"PRIVATE\u{e9}KEY",
] {
assert_eq!(
outcome(&block(bad, ED25519_BODY)).map(|_| ()),
Err("InvalidKey"),
"{bad:?}"
);
}
for unknown in [
"",
"X",
"DSA PRIVATE KEY",
"X509 CRL",
"A-B C",
"!\"#$%&'()*+,./09:;<=>?@[\\]^_`{|}~",
] {
assert_eq!(
outcome(&block(unknown, ED25519_BODY)).map(|_| ()),
Err("UnsupportedAlgorithm"),
"{unknown:?}"
);
}
}
#[test]
fn unknown_label_is_classified_before_the_body() {
for text in [
"-----BEGIN CERTIFICATE-----\nnot base64 at all\n",
"-----BEGIN CERTIFICATE-----\n",
"-----BEGIN CERTIFICATE-----\nAAAA\n-----END PUBLIC KEY-----\ntrailing\n",
] {
assert_eq!(
outcome(text).map(|_| ()),
Err("UnsupportedAlgorithm"),
"{text:?}"
);
}
assert_eq!(
outcome("x\n-----BEGIN CERTIFICATE-----\n").map(|_| ()),
Err("InvalidKey")
);
}
#[test]
fn headers() {
let enc = format!(
"-----BEGIN EC PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,00\n\n{ED25519_BODY}\n-----END EC PRIVATE KEY-----\n"
);
assert_eq!(outcome(&enc).map(|_| ()), Err("UnsupportedAlgorithm"));
let later = format!(
"-----BEGIN PRIVATE KEY-----\n{ED25519_BODY}\nProc-Type: 4,ENCRYPTED\n-----END PRIVATE KEY-----\n"
);
assert_eq!(outcome(&later).map(|_| ()), Err("InvalidKey"));
let other = format!(
"-----BEGIN PRIVATE KEY-----\nComment: x\n{ED25519_BODY}\n-----END PRIVATE KEY-----\n"
);
assert_eq!(outcome(&other).map(|_| ()), Err("InvalidKey"));
}
#[test]
fn line_structure() {
let der = ed25519_der();
let ok = |text: &str| {
assert_eq!(
outcome(text),
Ok((Label::PrivateKey, der.clone())),
"{text:?}"
)
};
let bad = |text: &str| assert_eq!(outcome(text).map(|_| ()), Err("InvalidKey"), "{text:?}");
let b = ED25519_BODY;
ok(&format!(
" \t\r\n-----BEGIN PRIVATE KEY-----\n{b}\n-----END PRIVATE KEY----- \t\r\n\n"
));
ok(&format!(
"-----BEGIN PRIVATE KEY-----\r{b}\n-----END PRIVATE KEY-----\r\n"
));
bad("");
bad(" \n ");
bad("-----BEGIN PRIVATE KEY-----");
bad("-----BEGIN PRIVATE KEY-----\n");
bad(&format!("-----BEGIN PRIVATE KEY-----\n{b}\n"));
bad(&format!("-----BEGIN PRIVATE KEY-----\n{b}"));
bad(&format!(
"-----BEGIN PRIVATE KEY-----\n{b}-----END PRIVATE KEY-----\n"
));
bad(&format!(
"-----BEGIN PRIVATE KEY-----\n\n{b}\n-----END PRIVATE KEY-----\n"
));
bad(&format!(
"-----BEGIN PRIVATE KEY-----\n{b}\n\n-----END PRIVATE KEY-----\n"
));
bad(&format!(
"-----BEGIN PRIVATE KEY----- \n{b}\n-----END PRIVATE KEY-----\n"
));
bad(&format!(
"-----BEGIN PRIVATE KEY-----\n{b} \n-----END PRIVATE KEY-----\n"
));
bad(&format!(
"-----BEGIN PRIVATE KEY-----\n {b}\n-----END PRIVATE KEY-----\n"
));
bad(&format!(
"-----BEGIN PRIVATE KEY-----\n{b}\n -----END PRIVATE KEY-----\n"
));
bad(&format!(
"-----BEGIN PRIVATE KEY----\n{b}\n-----END PRIVATE KEY-----\n"
));
bad(&format!(
"------BEGIN PRIVATE KEY-----\n{b}\n-----END PRIVATE KEY-----\n"
));
bad(&format!(
"-----BEGIN PRIVATE KEY-----\n{b}\n-----END PRIVATE KEY----\n"
));
bad(&format!(
"-----BEGIN PRIVATE KEY-----\n{b}\n-----END PRIVATE KEY------\n"
));
bad(&format!(
"-----BEGIN PRIVATE KEY-----\n{b}\n-----END PRIVATE KEYS-----\n"
));
bad(&format!(
"-----BEGIN PRIVATE KEY-----\n{b}\n-----END -----\n"
));
bad(&format!(
"-----BEGIN PRIVATE KEY-----\n{b}\n-----end PRIVATE KEY-----\n"
));
bad(&format!(
"-----begin PRIVATE KEY-----\n{b}\n-----END PRIVATE KEY-----\n"
));
bad(&format!(
"-----BEGINPRIVATE KEY-----\n{b}\n-----END PRIVATE KEY-----\n"
));
bad(&format!(
"-----BEGIN PRIVATE KEY-----\n{b}\n-----END PRIVATE KEY-----\n\0"
));
bad(&format!(
"\u{feff}-----BEGIN PRIVATE KEY-----\n{b}\n-----END PRIVATE KEY-----\n"
));
bad(&format!(
"\u{a0}-----BEGIN PRIVATE KEY-----\n{b}\n-----END PRIVATE KEY-----\n"
));
}
#[test]
fn line_widths() {
for n in [1usize, 47, 48, 49, 95, 96, 97, 200] {
let der: Vec<u8> = (0..n).map(|i| (i * 7) as u8).collect();
let mut b64 = String::new();
crate::util::b64_encode_append(&der, &mut b64);
let wrap = |w: usize| -> String {
b64.as_bytes()
.chunks(w)
.map(|c| core::str::from_utf8(c).unwrap())
.collect::<Vec<_>>()
.join("\n")
};
assert_eq!(
outcome(&block("PUBLIC KEY", &wrap(64))),
Ok((Label::PublicKey, der.clone())),
"{n}"
);
for w in [4usize, 60, 63, 65, 68, 76] {
let got = outcome(&block("PUBLIC KEY", &wrap(w))).map(|_| ());
if b64.len() <= w.min(64) {
assert_eq!(got, Ok(()), "{n} octets, width {w}");
} else {
assert_eq!(got, Err("InvalidKey"), "{n} octets, width {w}");
}
}
}
}
#[test]
fn buffers_are_wiped_on_every_error_path() {
let b = ED25519_BODY;
let cases = [
format!("-----BEGIN PRIVATE KEY-----\n{b}\n-----END PUBLIC KEY-----\n"),
format!("-----BEGIN PRIVATE KEY-----\n{b}\n-----END PRIVATE KEY-----\nx\n"),
format!(
"-----BEGIN PRIVATE KEY-----\n{}\n{}\n-----END PRIVATE KEY-----\n",
&b[..60],
&b[60..]
),
format!("-----BEGIN PRIVATE KEY-----\n{b}\n"),
format!(
"-----BEGIN PRIVATE KEY-----\n{}*\n-----END PRIVATE KEY-----\n",
&b[..63]
),
read_text("negative", "P15-noncanonical-pad-bits.pem"),
read_text("negative", "P05-missing-padding.pem"),
];
for text in &cases {
let mut body = Vec::new();
let mut der = Vec::new();
assert!(
decode_into(text.as_bytes(), &mut body, &mut der).is_err(),
"{text:?}"
);
assert!(!body.is_empty(), "{text:?}: the body was collected");
assert!(body.iter().all(|&x| x == 0), "{text:?}: body {body:?}");
assert!(der.iter().all(|&x| x == 0), "{text:?}: der {der:?}");
}
let mut body = Vec::new();
let mut der = Vec::new();
let p15 = read_text("negative", "P15-noncanonical-pad-bits.pem");
assert!(decode_into(p15.as_bytes(), &mut body, &mut der).is_err());
assert_eq!(der.len(), 91);
assert!(der.iter().all(|&x| x == 0));
let mut body = Vec::new();
let mut der = Vec::new();
let ok = block("PRIVATE KEY", b);
assert_eq!(
decode_into(ok.as_bytes(), &mut body, &mut der),
Ok(Label::PrivateKey)
);
assert!(!body.is_empty() && body.iter().all(|&x| x == 0));
assert_eq!(der, ed25519_der());
}
fn planned_body_capacity(text: &[u8]) -> usize {
let start = text
.iter()
.position(|&c| !matches!(c, b' ' | b'\t' | b'\r' | b'\n'))
.unwrap_or(text.len());
let begin = &text[start..];
let Some(eol) = begin.iter().position(|&c| c == b'\r' || c == b'\n') else {
return 0;
};
let eol_len = if begin[eol..].starts_with(b"\r\n") {
2
} else {
1
};
begin.len() - eol - eol_len
}
fn check_planned_capacity(text: &[u8]) -> Result<Label> {
let mut body = Vec::new();
let mut der = Vec::new();
let result = decode_into(text, &mut body, &mut der);
let planned = planned_body_capacity(text);
let what = String::from_utf8_lossy(&text[..text.len().min(60)]).into_owned();
assert!(
body.capacity() == 0 || body.capacity() == planned,
"{what:?}: body capacity {} != planned {planned}",
body.capacity()
);
if result.is_ok() {
assert_eq!(body.capacity(), planned, "{what:?}");
assert!(body.len() <= planned, "{what:?}");
}
assert_eq!(der.capacity(), der.len(), "{what:?}: DER buffer regrew");
result
}
#[test]
fn buffers_keep_their_planned_capacity_across_the_corpus() {
let mut accepted = 0;
let mut checked = 0;
for dir in ["openssl", "rfc", "negative"] {
for name in pem_files(dir) {
let text = fs::read(data_dir(dir).join(&name)).unwrap();
accepted += usize::from(check_planned_capacity(&text).is_ok());
checked += 1;
}
}
assert!(checked >= 90, "{checked} fixtures");
assert!(accepted >= 60, "only {accepted} of {checked} accepted");
}
#[test]
fn errors_never_contain_input_text() {
const MARKER: &str = "QUJDTUFSS0VSWFla";
let b = ED25519_BODY;
let texts = [
format!("{MARKER}\n{}", block("PRIVATE KEY", b)),
format!("{}{MARKER}\n", block("PRIVATE KEY", b)),
block(&format!("PRIVATE KEY {MARKER}"), b),
block(&MARKER.to_ascii_lowercase(), b),
block("PRIVATE KEY", &format!("{MARKER}{MARKER}*")),
block("PRIVATE KEY", &format!("{MARKER}: header")),
format!("-----BEGIN PRIVATE KEY-----\n{b}\n-----END {MARKER}-----\n"),
format!(
"-----BEGIN PRIVATE KEY-----\nProc-Type: {MARKER}\n\n{b}\n-----END PRIVATE KEY-----\n"
),
format!("-----BEGIN {MARKER}-----\n{b}\n-----END {MARKER}-----\n"),
];
let lower = MARKER.to_ascii_lowercase();
for text in &texts {
let err = decode(text).map(|_| ()).unwrap_err();
for shown in [format!("{err:?}"), format!("{err}")] {
assert!(
!shown.contains(MARKER) && !shown.contains(&lower),
"{shown}"
);
}
}
}
const TOO_LARGE: Error = Error::InvalidKey("PEM input too large");
fn crlf_block(label: &str, der_len: usize) -> (String, Vec<u8>) {
let der: Vec<u8> = (0..der_len).map(|i| (i * 31 + 7) as u8).collect();
let mut b64 = String::new();
b64_encode_append(&der, &mut b64);
let mut text = format!("-----BEGIN {label}-----\r\n");
for line in b64.as_bytes().chunks(LINE_WIDTH) {
text.push_str(core::str::from_utf8(line).unwrap());
text.push_str("\r\n");
}
text.push_str(&format!("-----END {label}-----\r\n"));
(text, der)
}
fn padded(mut text: String, len: usize) -> String {
assert!(text.len() <= len);
let fill = len - text.len();
text.extend(core::iter::repeat_n(' ', fill));
text
}
#[test]
fn cap_is_the_largest_crlf_block_plus_whitespace() {
assert_eq!(MAX_INPUT_LEN, 98_384);
let (largest, _) = crlf_block("ENCRYPTED PRIVATE KEY", der::MAX_INPUT_LEN);
assert_eq!(largest.len() + SURROUNDING_WHITESPACE, MAX_INPUT_LEN);
for label in [
Label::PrivateKey,
Label::PublicKey,
Label::EcPrivateKey,
Label::RsaPublic,
Label::RsaPrivate,
Label::EncryptedPrivateKey,
] {
assert!(label.as_str().unwrap().len() <= LONGEST_LABEL, "{label:?}");
}
}
#[test]
fn input_at_the_cap_is_read_and_one_more_octet_is_not() {
for label in ["EC PRIVATE KEY", "RSA PUBLIC KEY", "PRIVATE KEY"] {
let (text, der) = crlf_block(label, der::MAX_INPUT_LEN);
let at_cap = padded(text, MAX_INPUT_LEN);
let (got_label, got) = decode(&at_cap).unwrap();
assert_eq!(got_label.as_str(), Some(label));
assert_eq!(*got, der, "{label}");
let over = padded(at_cap, MAX_INPUT_LEN + 1);
assert_eq!(decode(&over).map(|_| ()), Err(TOO_LARGE), "{label}");
}
let (text, _) = crlf_block("ENCRYPTED PRIVATE KEY", der::MAX_INPUT_LEN);
let at_cap = padded(text, MAX_INPUT_LEN);
assert_eq!(
decode(&at_cap).map(|_| ()),
Err(Error::UnsupportedAlgorithm)
);
let over = padded(at_cap, MAX_INPUT_LEN + 1);
assert_eq!(decode(&over).map(|_| ()), Err(TOO_LARGE));
}
#[test]
fn a_der_over_its_cap_gets_through_the_pem_layer() {
let der: Vec<u8> = (0..=der::MAX_INPUT_LEN).map(|i| i as u8).collect();
let text = encode(Label::PublicKey, &der).unwrap();
assert!(text.len() < MAX_INPUT_LEN);
assert_eq!(outcome(&text), Ok((Label::PublicKey, der)));
}
#[test]
fn a_65_mib_input_is_rejected_without_allocating() {
const LEN: usize = 65 * 1024 * 1024;
let lines = |label: &str| {
let mut text = format!("-----BEGIN {label}-----\n");
let line = format!("{ED25519_BODY}\n");
while text.len() + line.len() <= LEN {
text.push_str(&line);
}
padded(text, LEN)
};
for text in [
lines("PRIVATE KEY"),
lines("RSA PRIVATE KEY"),
" ".repeat(LEN),
] {
assert_eq!(text.len(), LEN);
let mut body = Vec::new();
let mut der = Vec::new();
assert_eq!(
decode_into(text.as_bytes(), &mut body, &mut der),
Err(TOO_LARGE)
);
assert_eq!((body.capacity(), der.capacity()), (0, 0));
assert_eq!(decode(&text).map(|_| ()), Err(TOO_LARGE));
}
}
#[test]
fn oversize_input_gives_the_size_error_whatever_its_label() {
let b = ED25519_BODY;
let over = |text: String| padded(text, MAX_INPUT_LEN + 1);
let texts = [
over(block("PRIVATE KEY", b)),
over(block("PUBLIC KEY", b)),
over(block("EC PRIVATE KEY", b)),
over(block("RSA PUBLIC KEY", b)),
over(block("RSA PRIVATE KEY", b)),
over(block("ENCRYPTED PRIVATE KEY", b)),
over(block("CERTIFICATE", b)),
over(block("private key", b)),
over(format!(
"-----BEGIN EC PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\n\n{b}\n-----END EC PRIVATE KEY-----\n"
)),
over(format!("-----BEGIN PRIVATE KEY-----\n{b}\n")),
over("not PEM".to_owned()),
over(String::new()),
"x".repeat(MAX_INPUT_LEN + 1),
];
for text in &texts {
assert_eq!(text.len(), MAX_INPUT_LEN + 1);
let mut body = Vec::new();
let mut der = Vec::new();
assert_eq!(
decode_into(text.as_bytes(), &mut body, &mut der),
Err(TOO_LARGE),
"{:?}",
&text[..text.len().min(40)]
);
assert_eq!((body.capacity(), der.capacity()), (0, 0));
}
}
mod properties {
use super::super::*;
use proptest::prelude::*;
fn known_label() -> impl Strategy<Value = Label> {
prop::sample::select(vec![
Label::PrivateKey,
Label::PublicKey,
Label::EcPrivateKey,
Label::RsaPublic,
])
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(1000))]
#[test]
fn encoder_output_decodes_with_pem_rfc7468(
label in known_label(),
der in proptest::collection::vec(any::<u8>(), 1..400),
) {
let text = label.as_str().unwrap();
let ours = encode(label, &der).unwrap();
let (oracle_label, oracle_der) = pem_rfc7468::decode_vec(ours.as_bytes()).unwrap();
prop_assert_eq!(oracle_label, text);
prop_assert_eq!(&oracle_der, &der);
let oracle_text =
pem_rfc7468::encode_string(text, pem_rfc7468::LineEnding::LF, &der).unwrap();
prop_assert_eq!(ours.as_str(), oracle_text.as_str());
let (back_label, back) = decode(&ours).unwrap();
prop_assert_eq!(back_label, label);
prop_assert_eq!(&*back, &der);
}
#[test]
fn accepted_input_is_accepted_by_pem_rfc7468(
label in known_label(),
der in proptest::collection::vec(any::<u8>(), 1..150),
at in any::<prop::sample::Index>(),
with in prop::sample::select(
"AQgw+/=-_ \t\r\n.:Pz".chars().collect::<Vec<char>>()
),
insert in any::<bool>(),
) {
let pem = encode(label, &der).unwrap();
let i = at.index(pem.len());
let mutated = if insert {
format!("{}{with}{}", &pem[..i], &pem[i..])
} else {
format!("{}{with}{}", &pem[..i], &pem[i + 1..])
};
if let Ok((got_label, got)) = decode(&mutated) {
let trimmed = format!("{}\n", mutated.trim_matches([' ', '\t', '\r', '\n']));
let (oracle_label, oracle) = pem_rfc7468::decode_vec(trimmed.as_bytes())
.map_err(|e| TestCaseError::fail(format!("{e:?} for {mutated:?}")))?;
prop_assert_eq!(Some(oracle_label), got_label.as_str());
prop_assert_eq!(&oracle, &*got);
}
}
#[test]
fn buffers_keep_their_planned_capacity(
label in known_label(),
der in proptest::collection::vec(any::<u8>(), 1..1500),
eol in prop::sample::select(vec!["\n", "\r\n", "\r"]),
around in prop::sample::select(vec!["", " ", "\n", "\r\n\t "]),
mutation in proptest::option::of((
any::<prop::sample::Index>(),
prop::sample::select("AQgw+/=-_ \t\r\n.:".chars().collect::<Vec<char>>()),
any::<bool>(),
)),
) {
let pem = encode(label, &der).unwrap().replace('\n', eol);
let text = format!("{around}{pem}{around}");
let Some((at, with, insert)) = mutation else {
let got = super::check_planned_capacity(text.as_bytes());
prop_assert_eq!(got, Ok(label));
return Ok(());
};
let i = at.index(text.len());
let mutated = if insert {
format!("{}{with}{}", &text[..i], &text[i..])
} else {
format!("{}{with}{}", &text[..i], &text[i + 1..])
};
let _ = super::check_planned_capacity(mutated.as_bytes());
}
}
}
}