pub const IDP_CA_MAX_PEM_BYTES: usize = 64 * 1024;
pub const IDP_CA_MAX_CERTIFICATES: usize = 10;
pub fn parse_idp_ca(pem: &str, now: i64) -> Result<Vec<Vec<u8>>, String> {
if pem.len() > IDP_CA_MAX_PEM_BYTES {
return Err(format!(
"the IdP CA is {} bytes; at most 64 KiB is taken (paste the CA, or its chain, as PEM)",
pem.len()
));
}
if !pem.contains("-----BEGIN ") {
return Err("the IdP CA holds no certificate: paste it as PEM (-----BEGIN CERTIFICATE-----)".to_owned());
}
let mut ders = Vec::new();
for block in x509_parser::pem::Pem::iter_from_buffer(pem.as_bytes()) {
let block = block.map_err(|err| format!("the IdP CA is not PEM: {err}"))?;
if block.label.contains("PRIVATE KEY") {
return Err(
"a private key was pasted with the IdP CA; paste only the CA's certificate — the key was not kept"
.to_owned(),
);
}
if block.label != "CERTIFICATE" {
return Err(format!(
"the IdP CA holds a {:?} block; only CERTIFICATE blocks are taken",
block.label
));
}
ders.push(block.contents);
if ders.len() > IDP_CA_MAX_CERTIFICATES {
return Err(format!(
"the IdP CA holds more than {IDP_CA_MAX_CERTIFICATES} certificates; at most 10 are taken"
));
}
}
if ders.is_empty() {
return Err("the IdP CA holds no certificate: paste it as PEM (-----BEGIN CERTIFICATE-----)".to_owned());
}
for der in &ders {
let (_rest, cert) = x509_parser::parse_x509_certificate(der)
.map_err(|err| format!("a certificate in the IdP CA is not an X.509 certificate: {err}"))?;
let subject = cert.subject().to_string();
let is_ca = cert.basic_constraints().ok().flatten().is_some_and(|bc| bc.value.ca);
if !is_ca {
return Err(format!(
"the certificate {subject:?} is not a CA (its basicConstraints say cA false or nothing): paste the CA that \
issued the IdP's certificate, not the IdP's own"
));
}
let validity = cert.validity();
let (from, until) = (validity.not_before.timestamp() * 1000, validity.not_after.timestamp() * 1000);
if until < now {
return Err(format!("the CA {subject:?} expired ({})", validity.not_after));
}
if from > now + 120_000 {
return Err(format!("the CA {subject:?} is not valid yet ({})", validity.not_before));
}
}
Ok(ders)
}
pub fn idp_ca_pem(ders: &[Vec<u8>]) -> String {
use base64ct::{Base64, Encoding as _};
let mut out = String::new();
for der in ders {
out.push_str("-----BEGIN CERTIFICATE-----\n");
let text = Base64::encode_string(der);
for line in text.as_bytes().chunks(64) {
out.push_str(&String::from_utf8_lossy(line));
out.push('\n');
}
out.push_str("-----END CERTIFICATE-----\n");
}
out
}
pub fn idp_ca_blocks(pem: &str) -> Vec<Vec<u8>> {
x509_parser::pem::Pem::iter_from_buffer(pem.as_bytes())
.filter_map(Result::ok)
.filter(|b| b.label == "CERTIFICATE")
.map(|b| b.contents)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn minted(ca: bool, not_after_year: i32) -> (String, Vec<u8>) {
let key = rcgen::KeyPair::generate().expect("a key");
let mut params = rcgen::CertificateParams::new(vec!["idp.acme.example".to_owned()]).expect("params");
params.not_before = rcgen::date_time_ymd(2020, 1, 1);
params.not_after = rcgen::date_time_ymd(not_after_year, 1, 1);
if ca {
params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
params.key_usages = vec![rcgen::KeyUsagePurpose::KeyCertSign, rcgen::KeyUsagePurpose::CrlSign];
}
let cert = params.self_signed(&key).expect("self-signed");
(cert.pem(), cert.der().to_vec())
}
const NOW: i64 = 1_791_250_000_000;
#[test]
fn an_idp_ca_is_read_as_the_ca_certificates_it_holds() {
let (pem, der) = minted(true, 2036);
assert_eq!(parse_idp_ca(&pem, NOW).expect("a CA"), vec![der.clone()]);
let (pem2, der2) = minted(true, 2036);
let chain = format!("{pem}\n{pem2}");
assert_eq!(parse_idp_ca(&chain, NOW).expect("a chain"), vec![der.clone(), der2]);
assert_eq!(
parse_idp_ca(&idp_ca_pem(std::slice::from_ref(&der)), NOW).expect("round trip"),
vec![der]
);
}
#[test]
fn what_is_not_a_ca_is_refused_by_name() {
let refused = |pem: &str| parse_idp_ca(pem, NOW).expect_err("refused");
assert!(refused("").contains("no certificate"), "{}", refused(""));
assert!(refused("hello").contains("no certificate"));
let (leaf, _) = minted(false, 2036);
assert!(refused(&leaf).contains("not a CA"), "{}", refused(&leaf));
let (old, _) = minted(true, 2025);
assert!(refused(&old).contains("expired"), "{}", refused(&old));
let key = rcgen::KeyPair::generate().expect("a key").serialize_pem();
let (ca, _) = minted(true, 2036);
let with_key = format!("{ca}\n{key}");
assert!(refused(&with_key).contains("private key"), "{}", refused(&with_key));
assert!(!refused(&with_key).contains("PRIVATE KEY-----\n"), "the key is never repeated back");
let big = format!("{ca}\n{}", " ".repeat(IDP_CA_MAX_PEM_BYTES));
assert!(refused(&big).contains("64 KiB"), "{}", refused(&big));
let many = std::iter::repeat_n(ca.as_str(), IDP_CA_MAX_CERTIFICATES + 1).collect::<Vec<_>>().join("\n");
assert!(refused(&many).contains("at most 10"), "{}", refused(&many));
}
}