1use bytes::Bytes;
16use pem::{self as pem_crate, LineEnding};
17use thiserror::Error;
18use valuable::Valuable;
19use x509_parser::{
20 error::{PEMError, X509Error},
21 nom::Parser,
22 pem::Pem,
23 prelude::X509CertificateParser,
24};
25
26use crate::{
27 certificate::{
28 Fingerprint, SerialNumber, Validity,
29 id::{CertId, InvalidCertId, InvalidIssuerCertId, IssuerCertId},
30 },
31 keys::PublicKey,
32};
33
34#[derive(Debug, Error)]
36pub enum InvalidPem {
37 #[error("no PEM block found")]
39 NoPEM,
40
41 #[error("invalid PEM block: {0}")]
43 DeserialisePEM(#[from] PEMError),
44
45 #[error("pem deserialised to: {0}")]
47 ParseX509(#[from] InvalidDer),
48
49 #[error("expected 1 PEM block but more provided")]
51 TooManyBlocks,
52}
53
54#[derive(Debug, Error)]
56#[error("invalid der when parsing x509 cert: {0}")]
57pub enum InvalidDer {
58 Parse(#[from] x509_parser::nom::Err<X509Error>),
61
62 #[error("excess der bytes")]
65 ExcessDER,
66
67 #[error("invalid timestamp in certificate validity: {0}")]
69 InvalidTimestamp(#[from] jiff::Error),
70
71 #[error("cert ID missing or invalid length in certificate: {0}")]
77 CertId(#[from] InvalidCertId),
78
79 #[error("issuer cert ID missing or invalid length in certificate: {0}")]
85 IssuerCertId(#[from] InvalidIssuerCertId),
86}
87
88#[derive(Debug, Clone, Valuable)]
96pub struct Certificate {
97 #[valuable(skip)]
99 der: Bytes,
100
101 #[valuable(skip)]
103 public_key_der: Bytes,
104
105 serial_number: SerialNumber,
107
108 fingerprint: Fingerprint,
110
111 validity: Validity,
113
114 #[valuable(skip)] cert_id: CertId,
117
118 #[valuable(skip)] issuer_cert_id: IssuerCertId,
121}
122
123impl Certificate {
124 pub fn from_pem(pem: &[u8]) -> Result<Self, InvalidPem> {
126 let mut pem_iter = Pem::iter_from_buffer(pem);
127 let pem = pem_iter.next().ok_or(InvalidPem::NoPEM)??;
128
129 if pem_iter.next().is_some() {
131 return Err(InvalidPem::TooManyBlocks);
132 }
133
134 Self::from_der(pem.contents).map_err(InvalidPem::from)
135 }
136
137 pub fn from_der(der: impl Into<Bytes>) -> Result<Self, InvalidDer> {
140 let der = der.into();
141
142 let (rem, cert) = X509CertificateParser::new()
143 .with_deep_parse_extensions(true)
144 .parse(&der)
145 .map_err(InvalidDer::Parse)?;
146 if !rem.is_empty() {
147 return Err(InvalidDer::ExcessDER);
149 }
150
151 let fingerprint = Fingerprint::from(&cert);
152 let serial_number = SerialNumber::from(&cert);
153 let validity = Validity::try_from(&cert)?;
154 let cert_id = CertId::try_from(&cert)?;
155 let issuer_cert_id = IssuerCertId::try_from(&cert)?;
156
157 let public_key_der = Bytes::from(cert.public_key().subject_public_key.data.to_vec());
159
160 Ok(Self {
161 der,
162 serial_number,
163 fingerprint,
164 public_key_der,
165 validity,
166 cert_id,
167 issuer_cert_id,
168 })
169 }
170
171 pub fn as_der(&self) -> Bytes {
173 self.der.clone() }
175
176 pub fn generate_pem(&self) -> String {
178 let pem = pem_crate::Pem::new("CERTIFICATE", self.der.as_ref());
179 pem_crate::encode_config(
180 &pem,
181 pem_crate::EncodeConfig::new().set_line_ending(LineEnding::LF),
182 )
183 }
184
185 pub fn serial_number(&self) -> &SerialNumber {
187 &self.serial_number
188 }
189
190 pub fn fingerprint(&self) -> &Fingerprint {
192 &self.fingerprint
193 }
194
195 pub fn validity(&self) -> &Validity {
197 &self.validity
198 }
199
200 pub fn public_key<'a>(&'a self) -> PublicKey<'a> {
202 PublicKey::new(self.public_key_der.as_ref())
203 }
204
205 pub fn cert_id(&self) -> &CertId {
207 &self.cert_id
208 }
209
210 pub fn issuer_cert_id(&self) -> &IssuerCertId {
212 &self.issuer_cert_id
213 }
214}
215
216impl From<rcgen::Certificate> for Certificate {
217 fn from(value: rcgen::Certificate) -> Self {
218 Certificate::from_pem(value.pem().as_bytes()).expect("valid cert round-trip")
219 }
220}
221
222#[cfg(test)]
223pub(super) mod tests {
224 use std::fmt::Display;
225
226 use rc_x509_test_helpers::assert_valuable_repr;
227
228 use super::*;
229
230 use proptest::prelude::*;
231 use valuable::Valuable;
232
233 const CERT_PEM_DATA: &str = "\
256MIICWjCCAgCgAwIBAgIRAOJ7lLc8PQi630WNVnql4WQwCgYIKoZIzj0EAwIwVjEh
257MB8GA1UECgwYTGEgRsOhYnJpY2EgZGUgUGzDoXRhbm9zMTEwLwYDVQQDDChMYSBG
258w6FicmljYSBkZSBQbMOhdGFub3MgSW50ZXJtZWRpYXRlIENBMB4XDTI1MDgxMzE0
259NTg0MFoXDTM1MDgxMTE0NTk0MFowGzEZMBcGA1UEAxMQaXRzYWxsYnJva2VuLmNv
260bTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEHLJcMR9Px/OfC9kXFCOqxlPe4Z
261sQa9wW3V8mMwxzwdDCvH7PWfW+uKof7LPw9XZ6F1fmTTw1YxG1NZ56GPpUGjgekw
262geYwDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
263AjAdBgNVHQ4EFgQU3I22J1J4WEz9okPbyyvgV2huK44wHwYDVR0jBBgwFoAUIGyO
264z+Qhp//tI8g9Nw93gYRxDhUwGwYDVR0RBBQwEoIQaXRzYWxsYnJva2VuLmNvbTBY
265BgwrBgEEAYKkZMYoQAEESDBGAgEBBBRkb21AaXRzYWxsYnJva2VuLmNvbQQrbGhN
266WDU2VVFVQjVlMnNvR1hzN2RRcE5wXy1jb19BUzd0dkpoQmstaHFJazAKBggqhkjO
267PQQDAgNIADBFAiANQrCtWI0ejFhyydcpsrqQ5vSlL26PIWBjurEsF7i9JwIhAMTX
268YxZ1HPGBZ43mYEaEdMR47YlQlNwwK+43yTDBRgd7\
269";
270
271 fn assert_certs_equal(a: &Certificate, b: &Certificate) {
275 assert_eq!(a.der, b.der);
276 assert_eq!(a.public_key_der, b.public_key_der);
277 assert_eq!(a.fingerprint, b.fingerprint);
278 }
279
280 pub(crate) fn cert_fixture() -> Certificate {
282 let pem =
283 format!("-----BEGIN CERTIFICATE-----\n{CERT_PEM_DATA}\n-----END CERTIFICATE-----");
284
285 Certificate::from_pem(pem.as_bytes()).expect("valid PEM")
286 }
287
288 #[test]
289 fn test_fixture() {
290 let pem =
291 format!("-----BEGIN CERTIFICATE-----\n{CERT_PEM_DATA}\n-----END CERTIFICATE-----\n");
292
293 let cert = Certificate::from_pem(pem.as_bytes()).expect("valid PEM");
294
295 assert_eq!(
296 cert.serial_number().as_hex_str(),
297 "00:e2:7b:94:b7:3c:3d:08:ba:df:45:8d:56:7a:a5:e1:64"
298 );
299
300 assert_eq!(
309 *cert.public_key().key_id(),
310 [
311 79, 76, 105, 90, 163, 235, 170, 81, 228, 220, 126, 244, 31, 241, 56, 133, 220, 5,
312 215, 45, 202, 124, 72, 64, 131, 33, 152, 138, 94, 248, 14, 204
313 ]
314 );
315
316 assert_eq!(cert.generate_pem(), pem);
318
319 assert_eq!(
321 cert.cert_id().as_hex_str(),
322 "dc:8d:b6:27:52:78:58:4c:fd:a2:43:db:cb:2b:e0:57:68:6e:2b:8e"
323 );
324 assert_eq!(
325 cert.issuer_cert_id().as_hex_str(),
326 "20:6c:8e:cf:e4:21:a7:ff:ed:23:c8:3d:37:0f:77:81:84:71:0e:15"
327 );
328 }
329
330 #[test]
331 fn test_valuable_repr() {
332 let cert = cert_fixture();
333
334 #[derive(Valuable)]
335 struct Wrapper {
336 cert: Certificate,
337 }
338
339 let cert = Wrapper { cert };
342
343 assert_valuable_repr(
344 &cert,
345 "\
346- cert:
347 Certificate {}:
348 - serial_number:
349 00:e2:7b:94:b7:3c:3d:08:ba:df:45:8d:56:7a:a5:e1:64
350 - fingerprint:
351 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
352 - validity:
353 2025-08-13T14:58:40Z..2035-08-11T14:59:40Z
354",
355 );
356 }
357
358 #[test]
359 fn test_validity_fixture() {
360 let cert = cert_fixture();
361
362 assert_eq!(
363 cert.validity().not_before_as_timestamp().to_string(),
364 "2025-08-13T14:58:40Z"
365 );
366 assert_eq!(
367 cert.validity().not_after_as_timestamp().to_string(),
368 "2035-08-11T14:59:40Z"
369 );
370 }
371
372 #[test]
373 fn test_fingerprint_fixture() {
374 let cert = cert_fixture();
375
376 assert_eq!(
377 cert.fingerprint().as_hex_str(),
378 "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"
379 );
380 }
381
382 #[test]
383 fn test_round_trip_pem() {
384 let cert = cert_fixture();
385
386 let got = Certificate::from_pem(cert.generate_pem().as_bytes()).expect("valid cert");
387 assert_certs_equal(&got, &cert);
388 }
389
390 #[test]
391 fn test_der_zero_copy_construction() {
392 let buf = Bytes::from(cert_fixture().as_der().to_vec());
394 assert!(buf.is_unique());
395
396 let cert = Certificate::from_der(buf.clone()).expect("valid DER");
398
399 assert!(!buf.is_unique());
409
410 assert!(!cert.as_der().is_unique());
413 }
414
415 #[derive(Debug, Clone)]
417 enum PemPart {
418 Start,
420 End,
422 RandomData(String),
424 ValidCertData,
426 }
427
428 impl Display for PemPart {
430 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
431 match self {
432 PemPart::Start => f.write_str("-----BEGIN CERTIFICATE-----\n"),
433 PemPart::End => f.write_str("-----END CERTIFICATE-----\n"),
434 PemPart::RandomData(s) => f.write_str(s),
435 PemPart::ValidCertData => f.write_str(CERT_PEM_DATA),
436 }
437 }
438 }
439
440 fn arbitrary_pem_part() -> impl Strategy<Value = PemPart> {
442 prop_oneof![
443 Just(PemPart::Start),
444 Just(PemPart::End),
445 any::<String>().prop_map(PemPart::RandomData),
446 Just(PemPart::ValidCertData),
447 ]
448 }
449
450 proptest! {
451 #[test]
454 fn prop_from_pem(
455 parts in prop::collection::vec(arbitrary_pem_part(), 0..5),
456 ) {
457 prop_from_pem_test(parts);
458 }
459
460 #[test]
463 fn prop_invalid_pem_bytes(
464 binary in prop::collection::vec(any::<u8>(), 0..200),
465 ) {
466 let _ = Certificate::from_pem(&binary).expect_err("non-pem input");
467 }
468 }
469
470 fn prop_from_pem_test(parts: Vec<PemPart>) {
471 let pem: String = parts.iter().map(ToString::to_string).collect();
473
474 let cert = match Certificate::from_pem(pem.as_bytes()) {
476 Ok(cert) => {
477 cert
479 }
480 Err(_) => {
481 return;
483 }
484 };
485
486 assert_certs_equal(
488 &cert,
489 &Certificate::from_pem(cert.generate_pem().as_bytes()).unwrap(),
490 );
491
492 assert_certs_equal(&cert, &Certificate::from_der(cert.as_der()).unwrap());
494
495 assert!(matches!(
501 parts.as_slice(),
502 [PemPart::Start, PemPart::ValidCertData, PemPart::End]
503 ));
504 }
505}