use crate::{
utils::{
codec::{CodecError, Decode},
extract_oid_from_rdn, hex_with_colons,
},
v1,
};
use chrono::{DateTime, Utc};
use const_oid::db::rfc4519::{CN, COMMON_NAME, O, ORGANIZATION, ORGANIZATION_NAME};
use p256::pkcs8::ObjectIdentifier;
use sha2::{Digest, Sha256};
use std::{
fmt::{self, Display},
io::Cursor,
};
use thiserror::Error;
use x509_cert::{
Certificate as Cert,
der::{Decode as CertDecode, DecodePem, Encode as CertEncode, EncodePem, asn1::OctetString},
ext::pkix::{AuthorityKeyIdentifier, SubjectKeyIdentifier},
};
pub(crate) const SCT_V1: ObjectIdentifier = const_oid::db::rfc6962::CT_PRECERT_SCTS;
pub(crate) const CT_POISON: ObjectIdentifier = const_oid::db::rfc6962::CT_PRECERT_POISON;
pub(crate) const SUBJECT_KEY_ID: ObjectIdentifier =
const_oid::db::rfc5280::ID_CE_SUBJECT_KEY_IDENTIFIER;
pub(crate) const AUTH_KEY_ID: ObjectIdentifier =
const_oid::db::rfc5280::ID_CE_AUTHORITY_KEY_IDENTIFIER;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Certificate(pub(crate) Cert);
impl Certificate {
pub fn from_pem(input: &str) -> Result<Self, CertificateError> {
Ok(Self(
Cert::from_pem(input.as_bytes()).map_err(CodecError::DerError)?,
))
}
pub fn as_pem(&self) -> String {
self.0.to_pem(p256::pkcs8::LineEnding::LF).unwrap()
}
pub fn from_der(input: &[u8]) -> Result<Self, CertificateError> {
Ok(Self(Cert::from_der(input).map_err(CodecError::DerError)?))
}
pub fn extract_scts_v1(&self) -> Result<Vec<v1::SignedCertificateTimestamp>, CertificateError> {
let Some(extensions) = &self.0.tbs_certificate.extensions else {
return Ok(vec![]);
};
let sct_lists = extensions
.iter()
.filter(|extension| extension.extn_id == SCT_V1)
.map(|sct| &sct.extn_value)
.map(|sct| {
let sct = OctetString::from_der(sct.as_bytes()).unwrap();
let mut reader = Cursor::new(sct.as_bytes());
v1::SctList::decode(&mut reader)
})
.collect::<Result<Vec<_>, _>>()?;
let scts = sct_lists
.into_iter()
.flat_map(|list| list.into_inner())
.collect();
Ok(scts)
}
pub fn is_precert(&self) -> Result<bool, CertificateError> {
let Some(extensions) = &self.0.tbs_certificate.extensions else {
return Ok(false);
};
let scts = extensions
.iter()
.filter(|extension| extension.extn_id == SCT_V1)
.count();
let poisons = extensions
.iter()
.filter(|extension| extension.extn_id == CT_POISON && extension.critical)
.filter(|extension| extension.extn_value.as_bytes() == [0x05, 0x00])
.count();
match (poisons, scts) {
(1, 0) => Ok(true),
(0, _) => Ok(false),
_ => Err(CertificateError::InvalidPreCert),
}
}
pub fn fingerprint_sha256(&self) -> Fingerprint {
let mut cert_bytes = vec![];
self.0.encode_to_vec(&mut cert_bytes).unwrap();
let hash: [u8; 32] = Sha256::digest(&cert_bytes).into();
Fingerprint(hash)
}
pub fn get_issuer_name(&self) -> String {
let issuer = &self.0.tbs_certificate.issuer;
extract_oid_from_rdn(issuer, O)
.or_else(|| extract_oid_from_rdn(issuer, ORGANIZATION))
.or_else(|| extract_oid_from_rdn(issuer, ORGANIZATION_NAME))
.unwrap_or_else(|| issuer.to_string())
}
pub fn get_subject_name(&self) -> String {
let subject = &self.0.tbs_certificate.subject;
extract_oid_from_rdn(subject, CN)
.or_else(|| extract_oid_from_rdn(subject, COMMON_NAME))
.unwrap_or_else(|| subject.to_string())
}
pub fn get_validity(&self) -> (DateTime<Utc>, DateTime<Utc>) {
(
DateTime::from(self.0.tbs_certificate.validity.not_before.to_system_time()),
DateTime::from(self.0.tbs_certificate.validity.not_after.to_system_time()),
)
}
pub fn get_subject_key_info(&self) -> Option<Vec<u8>> {
let Some(extensions) = &self.0.tbs_certificate.extensions else {
return None;
};
extensions
.iter()
.find(|extension| extension.extn_id == SUBJECT_KEY_ID)
.and_then(|extension| {
SubjectKeyIdentifier::from_der(extension.extn_value.as_bytes()).ok()
})
.map(|key_id| key_id.0.as_bytes().to_vec())
}
pub fn get_authority_key_info(&self) -> Option<Vec<u8>> {
let Some(extensions) = &self.0.tbs_certificate.extensions else {
return None;
};
extensions
.iter()
.find(|extension| extension.extn_id == AUTH_KEY_ID)
.and_then(|extension| {
AuthorityKeyIdentifier::from_der(extension.extn_value.as_bytes()).ok()
})
.and_then(|key_id| key_id.key_identifier)
.map(|key_id| key_id.as_bytes().to_vec())
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Fingerprint(pub [u8; 32]);
impl Display for Fingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", hex_with_colons(&self.0))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum CertificateError {
#[error("A precert can't have SCTs or more than one poison value")]
InvalidPreCert,
#[error("The certificate chain is malformed")]
InvalidChain,
#[error("Failed to decode a value: {0}")]
CodecError(#[from] CodecError),
#[error("Failed to verify certificate: {0}")]
VerificationError(x509_verify::Error),
}
impl From<x509_verify::Error> for CertificateError {
fn from(value: x509_verify::Error) -> Self {
Self::VerificationError(value)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
CertificateChain,
tests::{CERT_CHAIN_GOOGLE_COM, get_log_argon2025h2},
utils::codec::Encode,
};
const CERT_GOOGLE_COM: &str = include_str!("../../testdata/google-cert.pem");
const PRE_CERT_GOOGLE_COM: &str = include_str!("../../testdata/google-precert.pem");
const GOOGLE_COM_FINGERPRINT: &str = "4B:4F:46:F8:E1:78:B4:08:F9:A7:AF:2B:CE:31:0A:6A:9F:BD:59:37:BD:F8:5B:C5:9B:45:D6:3C:81:61:73:67";
const CERT_GEOMYS_ORG: &str = include_str!("../../testdata/geomys-org.pem");
#[test]
fn sct_list_codec_roundtrip() {
let cert = CertificateChain::from_pem_chain(CERT_CHAIN_GOOGLE_COM).unwrap();
cert.verify_chain().unwrap();
let scts = cert.cert().extract_scts_v1().unwrap();
let mut writer = Cursor::new(vec![]);
v1::SctList::new(scts.clone()).encode(&mut writer).unwrap();
let scts2 = v1::SctList::decode(Cursor::new(writer.into_inner()))
.unwrap()
.into_inner();
assert_eq!(scts, scts2)
}
#[test]
fn validate_scts() {
let cert = CertificateChain::from_pem_chain(CERT_CHAIN_GOOGLE_COM).unwrap();
cert.verify_chain().unwrap();
let scts = cert.cert().extract_scts_v1().unwrap();
let log = get_log_argon2025h2();
assert_eq!(log.log_id(), &scts[0].log_id());
log.validate_sct_v1(&cert, &scts[0], true).unwrap();
}
#[test]
fn precert_transformation() {
let cert1 = CertificateChain::from_pem_chain(CERT_CHAIN_GOOGLE_COM).unwrap();
cert1.verify_chain().unwrap();
let cert2 = Certificate::from_pem(CERT_GOOGLE_COM).unwrap();
assert_eq!(cert1.cert(), &cert2);
assert!(!cert1.cert().is_precert().unwrap());
let precert = Certificate::from_pem(PRE_CERT_GOOGLE_COM).unwrap();
assert!(precert.is_precert().unwrap());
}
#[test]
fn fingerprint() {
let cert = Certificate::from_pem(CERT_GOOGLE_COM).unwrap();
let fp = cert.fingerprint_sha256();
assert_eq!(format!("{fp}"), GOOGLE_COM_FINGERPRINT);
}
#[test]
fn leaf_index() {
let cert = Certificate::from_pem(CERT_GEOMYS_ORG).unwrap();
let scts = cert.extract_scts_v1().unwrap();
assert_eq!(scts.len(), 2);
assert!(scts[0].extensions.leaf_index().is_none());
assert!(scts[1].extensions.leaf_index().is_some());
}
}