use bytes::Bytes;
use pem::{self as pem_crate, LineEnding};
use thiserror::Error;
use valuable::Valuable;
use x509_parser::{
error::{PEMError, X509Error},
nom::Parser,
pem::Pem,
prelude::X509CertificateParser,
};
use crate::{
certificate::{
Fingerprint, SerialNumber, Validity,
id::{CertId, InvalidCertId, InvalidIssuerCertId, IssuerCertId},
},
keys::PublicKey,
};
#[derive(Debug, Error)]
pub enum InvalidPem {
#[error("no PEM block found")]
NoPEM,
#[error("invalid PEM block: {0}")]
DeserialisePEM(#[from] PEMError),
#[error("pem deserialised to: {0}")]
ParseX509(#[from] InvalidDer),
#[error("expected 1 PEM block but more provided")]
TooManyBlocks,
}
#[derive(Debug, Error)]
#[error("invalid der when parsing x509 cert: {0}")]
pub enum InvalidDer {
Parse(#[from] x509_parser::nom::Err<X509Error>),
#[error("excess der bytes")]
ExcessDER,
#[error("invalid timestamp in certificate validity: {0}")]
InvalidTimestamp(#[from] jiff::Error),
#[error("cert ID missing or invalid length in certificate: {0}")]
CertId(#[from] InvalidCertId),
#[error("issuer cert ID missing or invalid length in certificate: {0}")]
IssuerCertId(#[from] InvalidIssuerCertId),
}
#[derive(Debug, Clone, Valuable)]
pub struct Certificate {
#[valuable(skip)]
der: Bytes,
#[valuable(skip)]
public_key_der: Bytes,
serial_number: SerialNumber,
fingerprint: Fingerprint,
validity: Validity,
#[valuable(skip)] cert_id: CertId,
#[valuable(skip)] issuer_cert_id: IssuerCertId,
}
impl Certificate {
pub fn from_pem(pem: &[u8]) -> Result<Self, InvalidPem> {
let mut pem_iter = Pem::iter_from_buffer(pem);
let pem = pem_iter.next().ok_or(InvalidPem::NoPEM)??;
if pem_iter.next().is_some() {
return Err(InvalidPem::TooManyBlocks);
}
Self::from_der(pem.contents).map_err(InvalidPem::from)
}
pub fn from_der(der: impl Into<Bytes>) -> Result<Self, InvalidDer> {
let der = der.into();
let (rem, cert) = X509CertificateParser::new()
.with_deep_parse_extensions(true)
.parse(&der)
.map_err(InvalidDer::Parse)?;
if !rem.is_empty() {
return Err(InvalidDer::ExcessDER);
}
let fingerprint = Fingerprint::from(&cert);
let serial_number = SerialNumber::from(&cert);
let validity = Validity::try_from(&cert)?;
let cert_id = CertId::try_from(&cert)?;
let issuer_cert_id = IssuerCertId::try_from(&cert)?;
let public_key_der = Bytes::from(cert.public_key().subject_public_key.data.to_vec());
Ok(Self {
der,
serial_number,
fingerprint,
public_key_der,
validity,
cert_id,
issuer_cert_id,
})
}
pub fn as_der(&self) -> Bytes {
self.der.clone() }
pub fn generate_pem(&self) -> String {
let pem = pem_crate::Pem::new("CERTIFICATE", self.der.as_ref());
pem_crate::encode_config(
&pem,
pem_crate::EncodeConfig::new().set_line_ending(LineEnding::LF),
)
}
pub fn serial_number(&self) -> &SerialNumber {
&self.serial_number
}
pub fn fingerprint(&self) -> &Fingerprint {
&self.fingerprint
}
pub fn validity(&self) -> &Validity {
&self.validity
}
pub fn public_key<'a>(&'a self) -> PublicKey<'a> {
PublicKey::new(self.public_key_der.as_ref())
}
pub fn cert_id(&self) -> &CertId {
&self.cert_id
}
pub fn issuer_cert_id(&self) -> &IssuerCertId {
&self.issuer_cert_id
}
}
impl From<rcgen::Certificate> for Certificate {
fn from(value: rcgen::Certificate) -> Self {
Certificate::from_pem(value.pem().as_bytes()).expect("valid cert round-trip")
}
}
#[cfg(test)]
pub(super) mod tests {
use std::fmt::Display;
use rc_x509_test_helpers::assert_valuable_repr;
use super::*;
use proptest::prelude::*;
use valuable::Valuable;
const CERT_PEM_DATA: &str = "\
MIICWjCCAgCgAwIBAgIRAOJ7lLc8PQi630WNVnql4WQwCgYIKoZIzj0EAwIwVjEh
MB8GA1UECgwYTGEgRsOhYnJpY2EgZGUgUGzDoXRhbm9zMTEwLwYDVQQDDChMYSBG
w6FicmljYSBkZSBQbMOhdGFub3MgSW50ZXJtZWRpYXRlIENBMB4XDTI1MDgxMzE0
NTg0MFoXDTM1MDgxMTE0NTk0MFowGzEZMBcGA1UEAxMQaXRzYWxsYnJva2VuLmNv
bTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEHLJcMR9Px/OfC9kXFCOqxlPe4Z
sQa9wW3V8mMwxzwdDCvH7PWfW+uKof7LPw9XZ6F1fmTTw1YxG1NZ56GPpUGjgekw
geYwDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
AjAdBgNVHQ4EFgQU3I22J1J4WEz9okPbyyvgV2huK44wHwYDVR0jBBgwFoAUIGyO
z+Qhp//tI8g9Nw93gYRxDhUwGwYDVR0RBBQwEoIQaXRzYWxsYnJva2VuLmNvbTBY
BgwrBgEEAYKkZMYoQAEESDBGAgEBBBRkb21AaXRzYWxsYnJva2VuLmNvbQQrbGhN
WDU2VVFVQjVlMnNvR1hzN2RRcE5wXy1jb19BUzd0dkpoQmstaHFJazAKBggqhkjO
PQQDAgNIADBFAiANQrCtWI0ejFhyydcpsrqQ5vSlL26PIWBjurEsF7i9JwIhAMTX
YxZ1HPGBZ43mYEaEdMR47YlQlNwwK+43yTDBRgd7\
";
fn assert_certs_equal(a: &Certificate, b: &Certificate) {
assert_eq!(a.der, b.der);
assert_eq!(a.public_key_der, b.public_key_der);
assert_eq!(a.fingerprint, b.fingerprint);
}
pub(crate) fn cert_fixture() -> Certificate {
let pem =
format!("-----BEGIN CERTIFICATE-----\n{CERT_PEM_DATA}\n-----END CERTIFICATE-----");
Certificate::from_pem(pem.as_bytes()).expect("valid PEM")
}
#[test]
fn test_fixture() {
let pem =
format!("-----BEGIN CERTIFICATE-----\n{CERT_PEM_DATA}\n-----END CERTIFICATE-----\n");
let cert = Certificate::from_pem(pem.as_bytes()).expect("valid PEM");
assert_eq!(
cert.serial_number().as_hex_str(),
"00:e2:7b:94:b7:3c:3d:08:ba:df:45:8d:56:7a:a5:e1:64"
);
assert_eq!(
*cert.public_key().key_id(),
[
79, 76, 105, 90, 163, 235, 170, 81, 228, 220, 126, 244, 31, 241, 56, 133, 220, 5,
215, 45, 202, 124, 72, 64, 131, 33, 152, 138, 94, 248, 14, 204
]
);
assert_eq!(cert.generate_pem(), pem);
assert_eq!(
cert.cert_id().as_hex_str(),
"dc:8d:b6:27:52:78:58:4c:fd:a2:43:db:cb:2b:e0:57:68:6e:2b:8e"
);
assert_eq!(
cert.issuer_cert_id().as_hex_str(),
"20:6c:8e:cf:e4:21:a7:ff:ed:23:c8:3d:37:0f:77:81:84:71:0e:15"
);
}
#[test]
fn test_valuable_repr() {
let cert = cert_fixture();
#[derive(Valuable)]
struct Wrapper {
cert: Certificate,
}
let cert = Wrapper { cert };
assert_valuable_repr(
&cert,
"\
- cert:
Certificate {}:
- serial_number:
00:e2:7b:94:b7:3c:3d:08:ba:df:45:8d:56:7a:a5:e1:64
- fingerprint:
49:ef:bb:e5:7f:3d:ff:9c:6d:b5:6a:15:b7:24:ba:8b:78:76:9c:16:a6:58:75:f9:b7:76:ae:ee:21:53:e5:e5
- validity:
2025-08-13T14:58:40Z..2035-08-11T14:59:40Z
",
);
}
#[test]
fn test_validity_fixture() {
let cert = cert_fixture();
assert_eq!(
cert.validity().not_before_as_timestamp().to_string(),
"2025-08-13T14:58:40Z"
);
assert_eq!(
cert.validity().not_after_as_timestamp().to_string(),
"2035-08-11T14:59:40Z"
);
}
#[test]
fn test_fingerprint_fixture() {
let cert = cert_fixture();
assert_eq!(
cert.fingerprint().as_hex_str(),
"49:ef:bb:e5:7f:3d:ff:9c:6d:b5:6a:15:b7:24:ba:8b:78:76:9c:16:a6:58:75:f9:b7:76:ae:ee:21:53:e5:e5"
);
}
#[test]
fn test_round_trip_pem() {
let cert = cert_fixture();
let got = Certificate::from_pem(cert.generate_pem().as_bytes()).expect("valid cert");
assert_certs_equal(&got, &cert);
}
#[test]
fn test_der_zero_copy_construction() {
let buf = Bytes::from(cert_fixture().as_der().to_vec());
assert!(buf.is_unique());
let cert = Certificate::from_der(buf.clone()).expect("valid DER");
assert!(!buf.is_unique());
assert!(!cert.as_der().is_unique());
}
#[derive(Debug, Clone)]
enum PemPart {
Start,
End,
RandomData(String),
ValidCertData,
}
impl Display for PemPart {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PemPart::Start => f.write_str("-----BEGIN CERTIFICATE-----\n"),
PemPart::End => f.write_str("-----END CERTIFICATE-----\n"),
PemPart::RandomData(s) => f.write_str(s),
PemPart::ValidCertData => f.write_str(CERT_PEM_DATA),
}
}
}
fn arbitrary_pem_part() -> impl Strategy<Value = PemPart> {
prop_oneof![
Just(PemPart::Start),
Just(PemPart::End),
any::<String>().prop_map(PemPart::RandomData),
Just(PemPart::ValidCertData),
]
}
proptest! {
#[test]
fn prop_from_pem(
parts in prop::collection::vec(arbitrary_pem_part(), 0..5),
) {
prop_from_pem_test(parts);
}
#[test]
fn prop_invalid_pem_bytes(
binary in prop::collection::vec(any::<u8>(), 0..200),
) {
let _ = Certificate::from_pem(&binary).expect_err("non-pem input");
}
}
fn prop_from_pem_test(parts: Vec<PemPart>) {
let pem: String = parts.iter().map(ToString::to_string).collect();
let cert = match Certificate::from_pem(pem.as_bytes()) {
Ok(cert) => {
cert
}
Err(_) => {
return;
}
};
assert_certs_equal(
&cert,
&Certificate::from_pem(cert.generate_pem().as_bytes()).unwrap(),
);
assert_certs_equal(&cert, &Certificate::from_der(cert.as_der()).unwrap());
assert!(matches!(
parts.as_slice(),
[PemPart::Start, PemPart::ValidCertData, PemPart::End]
));
}
}