Skip to main content

luct_core/
cert.rs

1use crate::{
2    utils::{
3        codec::{CodecError, Decode},
4        extract_oid_from_rdn, hex_with_colons,
5    },
6    v1,
7};
8use chrono::{DateTime, Utc};
9use const_oid::db::rfc4519::{CN, COMMON_NAME, O, ORGANIZATION, ORGANIZATION_NAME};
10use p256::pkcs8::ObjectIdentifier;
11use sha2::{Digest, Sha256};
12use std::{
13    fmt::{self, Display},
14    io::Cursor,
15};
16use thiserror::Error;
17use x509_cert::{
18    Certificate as Cert,
19    der::{Decode as CertDecode, DecodePem, Encode as CertEncode, EncodePem, asn1::OctetString},
20    ext::pkix::{AuthorityKeyIdentifier, SubjectKeyIdentifier},
21};
22
23pub(crate) const SCT_V1: ObjectIdentifier = const_oid::db::rfc6962::CT_PRECERT_SCTS;
24pub(crate) const CT_POISON: ObjectIdentifier = const_oid::db::rfc6962::CT_PRECERT_POISON;
25
26pub(crate) const SUBJECT_KEY_ID: ObjectIdentifier =
27    const_oid::db::rfc5280::ID_CE_SUBJECT_KEY_IDENTIFIER;
28pub(crate) const AUTH_KEY_ID: ObjectIdentifier =
29    const_oid::db::rfc5280::ID_CE_AUTHORITY_KEY_IDENTIFIER;
30
31/// A X.509 certificate
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct Certificate(pub(crate) Cert);
34
35impl Certificate {
36    /// Parse a PEM decoded string into a [`Certificate`]
37    pub fn from_pem(input: &str) -> Result<Self, CertificateError> {
38        Ok(Self(
39            Cert::from_pem(input.as_bytes()).map_err(CodecError::DerError)?,
40        ))
41    }
42
43    pub fn as_pem(&self) -> String {
44        self.0.to_pem(p256::pkcs8::LineEnding::LF).unwrap()
45    }
46
47    /// Parse a DER decoded string into a [`Certificate`]
48    pub fn from_der(input: &[u8]) -> Result<Self, CertificateError> {
49        Ok(Self(Cert::from_der(input).map_err(CodecError::DerError)?))
50    }
51
52    /// Extract the [SCTs](v1::SignedCertificateTimestamp) embedded into this [`Certificate`]
53    pub fn extract_scts_v1(&self) -> Result<Vec<v1::SignedCertificateTimestamp>, CertificateError> {
54        let Some(extensions) = &self.0.tbs_certificate.extensions else {
55            return Ok(vec![]);
56        };
57
58        let sct_lists = extensions
59            .iter()
60            .filter(|extension| extension.extn_id == SCT_V1)
61            .map(|sct| &sct.extn_value)
62            .map(|sct| {
63                let sct = OctetString::from_der(sct.as_bytes()).unwrap();
64                let mut reader = Cursor::new(sct.as_bytes());
65                v1::SctList::decode(&mut reader)
66            })
67            .collect::<Result<Vec<_>, _>>()?;
68
69        let scts = sct_lists
70            .into_iter()
71            .flat_map(|list| list.into_inner())
72            .collect();
73
74        Ok(scts)
75    }
76
77    pub fn is_precert(&self) -> Result<bool, CertificateError> {
78        let Some(extensions) = &self.0.tbs_certificate.extensions else {
79            return Ok(false);
80        };
81
82        let scts = extensions
83            .iter()
84            .filter(|extension| extension.extn_id == SCT_V1)
85            .count();
86
87        let poisons = extensions
88            .iter()
89            .filter(|extension| extension.extn_id == CT_POISON && extension.critical)
90            .filter(|extension| extension.extn_value.as_bytes() == [0x05, 0x00])
91            .count();
92
93        match (poisons, scts) {
94            (1, 0) => Ok(true),
95            (0, _) => Ok(false),
96            _ => Err(CertificateError::InvalidPreCert),
97        }
98    }
99
100    pub fn fingerprint_sha256(&self) -> Fingerprint {
101        let mut cert_bytes = vec![];
102        self.0.encode_to_vec(&mut cert_bytes).unwrap();
103
104        let hash: [u8; 32] = Sha256::digest(&cert_bytes).into();
105        Fingerprint(hash)
106    }
107
108    pub fn get_issuer_name(&self) -> String {
109        let issuer = &self.0.tbs_certificate.issuer;
110        extract_oid_from_rdn(issuer, O)
111            .or_else(|| extract_oid_from_rdn(issuer, ORGANIZATION))
112            .or_else(|| extract_oid_from_rdn(issuer, ORGANIZATION_NAME))
113            .unwrap_or_else(|| issuer.to_string())
114    }
115
116    pub fn get_subject_name(&self) -> String {
117        let subject = &self.0.tbs_certificate.subject;
118        extract_oid_from_rdn(subject, CN)
119            .or_else(|| extract_oid_from_rdn(subject, COMMON_NAME))
120            .unwrap_or_else(|| subject.to_string())
121    }
122
123    pub fn get_validity(&self) -> (DateTime<Utc>, DateTime<Utc>) {
124        (
125            DateTime::from(self.0.tbs_certificate.validity.not_before.to_system_time()),
126            DateTime::from(self.0.tbs_certificate.validity.not_after.to_system_time()),
127        )
128    }
129
130    pub fn get_subject_key_info(&self) -> Option<Vec<u8>> {
131        let Some(extensions) = &self.0.tbs_certificate.extensions else {
132            return None;
133        };
134
135        extensions
136            .iter()
137            .find(|extension| extension.extn_id == SUBJECT_KEY_ID)
138            .and_then(|extension| {
139                SubjectKeyIdentifier::from_der(extension.extn_value.as_bytes()).ok()
140            })
141            .map(|key_id| key_id.0.as_bytes().to_vec())
142    }
143
144    pub fn get_authority_key_info(&self) -> Option<Vec<u8>> {
145        let Some(extensions) = &self.0.tbs_certificate.extensions else {
146            return None;
147        };
148
149        extensions
150            .iter()
151            .find(|extension| extension.extn_id == AUTH_KEY_ID)
152            .and_then(|extension| {
153                AuthorityKeyIdentifier::from_der(extension.extn_value.as_bytes()).ok()
154            })
155            .and_then(|key_id| key_id.key_identifier)
156            .map(|key_id| key_id.as_bytes().to_vec())
157    }
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
161pub struct Fingerprint(pub [u8; 32]);
162
163impl Display for Fingerprint {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        write!(f, "{}", hex_with_colons(&self.0))
166    }
167}
168
169/// Error returned when parsing a [`Certificate`] or [`CertificateChain`](crate::cert_chain::CertificateChain)
170#[derive(Debug, Clone, PartialEq, Eq, Error)]
171pub enum CertificateError {
172    #[error("A precert can't have SCTs or more than one poison value")]
173    InvalidPreCert,
174
175    #[error("The certificate chain is malformed")]
176    InvalidChain,
177
178    #[error("Failed to decode a value: {0}")]
179    CodecError(#[from] CodecError),
180
181    #[error("Failed to verify certificate: {0}")]
182    VerificationError(x509_verify::Error),
183}
184
185impl From<x509_verify::Error> for CertificateError {
186    fn from(value: x509_verify::Error) -> Self {
187        Self::VerificationError(value)
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use crate::{
195        CertificateChain,
196        tests::{CERT_CHAIN_GOOGLE_COM, get_log_argon2025h2},
197        utils::codec::Encode,
198    };
199
200    const CERT_GOOGLE_COM: &str = include_str!("../../testdata/google-cert.pem");
201    const PRE_CERT_GOOGLE_COM: &str = include_str!("../../testdata/google-precert.pem");
202    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";
203
204    // This certificate contains an sct with leaf index
205    const CERT_GEOMYS_ORG: &str = include_str!("../../testdata/geomys-org.pem");
206
207    #[test]
208    fn sct_list_codec_roundtrip() {
209        let cert = CertificateChain::from_pem_chain(CERT_CHAIN_GOOGLE_COM).unwrap();
210        cert.verify_chain().unwrap();
211        let scts = cert.cert().extract_scts_v1().unwrap();
212
213        let mut writer = Cursor::new(vec![]);
214        v1::SctList::new(scts.clone()).encode(&mut writer).unwrap();
215        let scts2 = v1::SctList::decode(Cursor::new(writer.into_inner()))
216            .unwrap()
217            .into_inner();
218
219        assert_eq!(scts, scts2)
220    }
221
222    #[test]
223    fn validate_scts() {
224        let cert = CertificateChain::from_pem_chain(CERT_CHAIN_GOOGLE_COM).unwrap();
225        cert.verify_chain().unwrap();
226        let scts = cert.cert().extract_scts_v1().unwrap();
227
228        let log = get_log_argon2025h2();
229        assert_eq!(log.log_id(), &scts[0].log_id());
230
231        log.validate_sct_v1(&cert, &scts[0], true).unwrap();
232    }
233
234    #[test]
235    fn precert_transformation() {
236        let cert1 = CertificateChain::from_pem_chain(CERT_CHAIN_GOOGLE_COM).unwrap();
237        cert1.verify_chain().unwrap();
238        let cert2 = Certificate::from_pem(CERT_GOOGLE_COM).unwrap();
239
240        assert_eq!(cert1.cert(), &cert2);
241        assert!(!cert1.cert().is_precert().unwrap());
242
243        let precert = Certificate::from_pem(PRE_CERT_GOOGLE_COM).unwrap();
244        assert!(precert.is_precert().unwrap());
245    }
246
247    #[test]
248    fn fingerprint() {
249        let cert = Certificate::from_pem(CERT_GOOGLE_COM).unwrap();
250        let fp = cert.fingerprint_sha256();
251        assert_eq!(format!("{fp}"), GOOGLE_COM_FINGERPRINT);
252    }
253
254    #[test]
255    fn leaf_index() {
256        let cert = Certificate::from_pem(CERT_GEOMYS_ORG).unwrap();
257        let scts = cert.extract_scts_v1().unwrap();
258
259        assert_eq!(scts.len(), 2);
260        assert!(scts[0].extensions.leaf_index().is_none());
261        assert!(scts[1].extensions.leaf_index().is_some());
262    }
263}