use alloc::string::String;
use alloc::vec::Vec;
use crate::base64;
use crate::error::{Error, Result};
use crate::types::KeyType;
const PUBLIC_KEY_LABEL: &str = "PUBLIC KEY";
const PRIVATE_KEY_LABEL: &str = "PRIVATE KEY";
const PEM_LINE_LENGTH: usize = 64;
pub(crate) fn label_for_key_type(key_type: KeyType) -> &'static str {
match key_type {
KeyType::Public => PUBLIC_KEY_LABEL,
KeyType::Private => PRIVATE_KEY_LABEL,
}
}
pub(crate) fn encode_pem(der: &[u8], label: &str) -> String {
let mut out = String::with_capacity(pem_encoded_size(der.len(), label.len()));
encode_pem_to(der, label, &mut out);
out
}
pub(crate) fn encode_pem_to(der: &[u8], label: &str, out: &mut String) {
out.push_str("-----BEGIN ");
out.push_str(label);
out.push_str("-----\n");
const RAW_LINE_BYTES: usize = 48;
let mut offset = 0;
while offset < der.len() {
if offset > 0 {
out.push('\n');
}
let end = core::cmp::min(offset + RAW_LINE_BYTES, der.len());
base64::encode_base64_to(&der[offset..end], out);
offset = end;
}
out.push_str("\n-----END ");
out.push_str(label);
out.push_str("-----");
}
fn pem_encoded_size(der_len: usize, label_len: usize) -> usize {
let header_len = 11 + label_len + 6; let b64_len = base64::encoded_len(der_len, true);
let num_lines = b64_len.div_ceil(PEM_LINE_LENGTH);
let body_len = b64_len + num_lines.saturating_sub(1); let footer_len = 10 + label_len + 5; header_len + body_len + footer_len
}
pub(crate) fn decode_pem(pem: &str) -> Result<(&str, Vec<u8>)> {
let trimmed = pem.trim();
if trimmed.is_empty() {
return Err(Error::InvalidPem("PEM input is empty"));
}
let header_prefix = "-----BEGIN ";
if !trimmed.starts_with(header_prefix) {
return Err(Error::InvalidPem("missing BEGIN header"));
}
let label_start = header_prefix.len();
let label_end = trimmed[label_start..]
.find("-----")
.ok_or(Error::InvalidPem("malformed BEGIN header"))?;
let label = &trimmed[label_start..label_start + label_end];
if label.is_empty() {
return Err(Error::InvalidPem("empty PEM label"));
}
let header_end = label_start + label_end + 5;
let remainder = &trimmed[header_end..];
let end_marker = "-----END ";
let footer_pos = find_footer(remainder, end_marker, label)
.ok_or(Error::InvalidPem("missing or mismatched END footer"))?;
let body = &remainder[..footer_pos];
let footer_len = end_marker.len() + label.len() + 5; let after_footer = &remainder[footer_pos + footer_len..];
if after_footer.bytes().any(|b| !b.is_ascii_whitespace()) {
return Err(Error::InvalidPem("trailing data after END footer"));
}
let der = base64::decode_base64(body)?;
Ok((label, der))
}
fn find_footer(s: &str, end_marker: &str, label: &str) -> Option<usize> {
let s_bytes = s.as_bytes();
let marker_bytes = end_marker.as_bytes();
let label_bytes = label.as_bytes();
let total_footer_len = marker_bytes.len() + label_bytes.len() + 5;
if s_bytes.len() < total_footer_len {
return None;
}
let search_end = s_bytes.len() - total_footer_len + 1;
let mut pos = 0;
while pos < search_end {
if s_bytes[pos..].starts_with(marker_bytes)
&& s_bytes[pos + marker_bytes.len()..].starts_with(label_bytes)
&& s_bytes[pos + marker_bytes.len() + label_bytes.len()..].starts_with(b"-----")
{
return Some(pos);
}
pos += 1;
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use pq_oid::{Algorithm, MlDsa, MlKem, SlhDsa};
#[test]
fn test_encode_pem_structure() {
let der = [0x30, 0x03, 0x02, 0x01, 0x00]; let pem = encode_pem(&der, "PUBLIC KEY");
assert!(pem.starts_with("-----BEGIN PUBLIC KEY-----\n"));
assert!(pem.ends_with("\n-----END PUBLIC KEY-----"));
}
#[test]
fn test_encode_pem_line_length() {
let der = vec![0x42u8; 100];
let pem = encode_pem(&der, "PUBLIC KEY");
let lines: Vec<&str> = pem.lines().collect();
for line in &lines[1..lines.len() - 1] {
assert!(line.len() <= 64, "line too long: {} chars", line.len());
}
}
#[test]
fn test_roundtrip_pem() {
let der = vec![0xABu8; 200];
let pem = encode_pem(&der, "PRIVATE KEY");
let (label, decoded) = decode_pem(&pem).unwrap();
assert_eq!(label, "PRIVATE KEY");
assert_eq!(decoded, der);
}
#[test]
fn test_decode_pem_empty() {
assert!(decode_pem("").is_err());
assert!(decode_pem(" ").is_err());
}
#[test]
fn test_decode_pem_no_header() {
assert!(decode_pem("some random text").is_err());
}
#[test]
fn test_decode_pem_junk_before_header() {
let pem = "junk-----BEGIN PUBLIC KEY-----\nAAA=\n-----END PUBLIC KEY-----";
assert!(decode_pem(pem).is_err());
}
#[test]
fn test_decode_pem_trailing_data() {
let pem = "-----BEGIN PUBLIC KEY-----\nAAA=\n-----END PUBLIC KEY-----\nextra garbage";
assert!(decode_pem(pem).is_err());
}
#[test]
fn test_decode_pem_trailing_whitespace_ok() {
let der = vec![0x42u8; 10];
let pem = encode_pem(&der, "PUBLIC KEY");
let padded = format!("{}\n \n", pem);
let (label, decoded) = decode_pem(&padded).unwrap();
assert_eq!(label, "PUBLIC KEY");
assert_eq!(decoded, der);
}
#[test]
fn test_decode_pem_mismatched_footer() {
let pem = "-----BEGIN PUBLIC KEY-----\nAAA=\n-----END PRIVATE KEY-----";
assert!(decode_pem(pem).is_err());
}
#[test]
fn test_decode_pem_whitespace_in_body() {
let der = vec![0x42u8; 10];
let pem = encode_pem(&der, "PUBLIC KEY");
let padded = format!(" \n{}\n ", pem);
let (label, decoded) = decode_pem(&padded).unwrap();
assert_eq!(label, "PUBLIC KEY");
assert_eq!(decoded, der);
}
#[test]
fn test_label_for_key_type() {
assert_eq!(label_for_key_type(KeyType::Public), "PUBLIC KEY");
assert_eq!(label_for_key_type(KeyType::Private), "PRIVATE KEY");
}
#[test]
fn test_real_fixture_pem_roundtrip() {
let fixtures: &[(&str, Algorithm, KeyType)] = &[
(
include_str!("../../test-data/test-keys/ml_kem_512_pub.pem"),
Algorithm::MlKem(MlKem::Kem512),
KeyType::Public,
),
(
include_str!("../../test-data/test-keys/ml_kem_512_priv.pem"),
Algorithm::MlKem(MlKem::Kem512),
KeyType::Private,
),
(
include_str!("../../test-data/test-keys/ml_kem_768_pub.pem"),
Algorithm::MlKem(MlKem::Kem768),
KeyType::Public,
),
(
include_str!("../../test-data/test-keys/ml_kem_768_priv.pem"),
Algorithm::MlKem(MlKem::Kem768),
KeyType::Private,
),
(
include_str!("../../test-data/test-keys/ml_kem_1024_pub.pem"),
Algorithm::MlKem(MlKem::Kem1024),
KeyType::Public,
),
(
include_str!("../../test-data/test-keys/ml_kem_1024_priv.pem"),
Algorithm::MlKem(MlKem::Kem1024),
KeyType::Private,
),
(
include_str!("../../test-data/test-keys/ml_dsa_44_pub.pem"),
Algorithm::MlDsa(MlDsa::Dsa44),
KeyType::Public,
),
(
include_str!("../../test-data/test-keys/ml_dsa_44_priv.pem"),
Algorithm::MlDsa(MlDsa::Dsa44),
KeyType::Private,
),
(
include_str!("../../test-data/test-keys/ml_dsa_65_pub.pem"),
Algorithm::MlDsa(MlDsa::Dsa65),
KeyType::Public,
),
(
include_str!("../../test-data/test-keys/ml_dsa_65_priv.pem"),
Algorithm::MlDsa(MlDsa::Dsa65),
KeyType::Private,
),
(
include_str!("../../test-data/test-keys/slh_dsa_sha2_128s_pub.pem"),
Algorithm::SlhDsa(SlhDsa::Sha2_128s),
KeyType::Public,
),
(
include_str!("../../test-data/test-keys/slh_dsa_sha2_128s_priv.pem"),
Algorithm::SlhDsa(SlhDsa::Sha2_128s),
KeyType::Private,
),
];
for (pem_str, expected_alg, expected_type) in fixtures {
let (label, der) = decode_pem(pem_str).unwrap();
let expected_label = label_for_key_type(*expected_type);
assert_eq!(label, expected_label, "label mismatch for {}", expected_alg);
let (alg, key_type, _) = crate::der::decode_der(&der).unwrap();
assert_eq!(
alg, *expected_alg,
"algorithm mismatch for {}",
expected_alg
);
assert_eq!(
key_type, *expected_type,
"type mismatch for {}",
expected_alg
);
let re_encoded = encode_pem(&der, label);
let (label2, der2) = decode_pem(&re_encoded).unwrap();
assert_eq!(label2, label);
assert_eq!(der2, der, "re-encode roundtrip failed for {}", expected_alg);
}
}
}