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 pub fn subject(&self) -> String {
224 let (_, cert) = X509CertificateParser::new()
225 .with_deep_parse_extensions(false)
226 .parse(&self.der)
227 .expect("valid DER: already parsed at construction");
228 cert.subject().to_string()
229 }
230}
231
232impl From<rcgen::Certificate> for Certificate {
233 fn from(value: rcgen::Certificate) -> Self {
234 Certificate::from_pem(value.pem().as_bytes()).expect("valid cert round-trip")
235 }
236}
237
238#[cfg(test)]
239pub(super) mod tests {
240 use std::fmt::Display;
241
242 use rc_x509_test_helpers::assert_valuable_repr;
243
244 use super::*;
245
246 use proptest::prelude::*;
247 use valuable::Valuable;
248
249 const CERT_PEM_DATA: &str = "\
272MIICWjCCAgCgAwIBAgIRAOJ7lLc8PQi630WNVnql4WQwCgYIKoZIzj0EAwIwVjEh
273MB8GA1UECgwYTGEgRsOhYnJpY2EgZGUgUGzDoXRhbm9zMTEwLwYDVQQDDChMYSBG
274w6FicmljYSBkZSBQbMOhdGFub3MgSW50ZXJtZWRpYXRlIENBMB4XDTI1MDgxMzE0
275NTg0MFoXDTM1MDgxMTE0NTk0MFowGzEZMBcGA1UEAxMQaXRzYWxsYnJva2VuLmNv
276bTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEHLJcMR9Px/OfC9kXFCOqxlPe4Z
277sQa9wW3V8mMwxzwdDCvH7PWfW+uKof7LPw9XZ6F1fmTTw1YxG1NZ56GPpUGjgekw
278geYwDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
279AjAdBgNVHQ4EFgQU3I22J1J4WEz9okPbyyvgV2huK44wHwYDVR0jBBgwFoAUIGyO
280z+Qhp//tI8g9Nw93gYRxDhUwGwYDVR0RBBQwEoIQaXRzYWxsYnJva2VuLmNvbTBY
281BgwrBgEEAYKkZMYoQAEESDBGAgEBBBRkb21AaXRzYWxsYnJva2VuLmNvbQQrbGhN
282WDU2VVFVQjVlMnNvR1hzN2RRcE5wXy1jb19BUzd0dkpoQmstaHFJazAKBggqhkjO
283PQQDAgNIADBFAiANQrCtWI0ejFhyydcpsrqQ5vSlL26PIWBjurEsF7i9JwIhAMTX
284YxZ1HPGBZ43mYEaEdMR47YlQlNwwK+43yTDBRgd7\
285";
286
287 fn assert_certs_equal(a: &Certificate, b: &Certificate) {
291 assert_eq!(a.der, b.der);
292 assert_eq!(a.public_key_der, b.public_key_der);
293 assert_eq!(a.fingerprint, b.fingerprint);
294 }
295
296 pub(crate) fn cert_fixture() -> Certificate {
298 let pem =
299 format!("-----BEGIN CERTIFICATE-----\n{CERT_PEM_DATA}\n-----END CERTIFICATE-----");
300
301 Certificate::from_pem(pem.as_bytes()).expect("valid PEM")
302 }
303
304 #[test]
305 fn test_fixture() {
306 let pem =
307 format!("-----BEGIN CERTIFICATE-----\n{CERT_PEM_DATA}\n-----END CERTIFICATE-----\n");
308
309 let cert = Certificate::from_pem(pem.as_bytes()).expect("valid PEM");
310
311 assert_eq!(
312 cert.serial_number().as_hex_str(),
313 "00:e2:7b:94:b7:3c:3d:08:ba:df:45:8d:56:7a:a5:e1:64"
314 );
315
316 assert_eq!(
325 *cert.public_key().key_id(),
326 [
327 79, 76, 105, 90, 163, 235, 170, 81, 228, 220, 126, 244, 31, 241, 56, 133, 220, 5,
328 215, 45, 202, 124, 72, 64, 131, 33, 152, 138, 94, 248, 14, 204
329 ]
330 );
331
332 assert_eq!(cert.generate_pem(), pem);
334
335 assert_eq!(
337 cert.cert_id().as_hex_str(),
338 "dc:8d:b6:27:52:78:58:4c:fd:a2:43:db:cb:2b:e0:57:68:6e:2b:8e"
339 );
340 assert_eq!(
341 cert.issuer_cert_id().as_hex_str(),
342 "20:6c:8e:cf:e4:21:a7:ff:ed:23:c8:3d:37:0f:77:81:84:71:0e:15"
343 );
344 }
345
346 #[test]
347 fn test_valuable_repr() {
348 let cert = cert_fixture();
349
350 #[derive(Valuable)]
351 struct Wrapper {
352 cert: Certificate,
353 }
354
355 let cert = Wrapper { cert };
358
359 assert_valuable_repr(
360 &cert,
361 "\
362- cert:
363 Certificate {}:
364 - serial_number:
365 00:e2:7b:94:b7:3c:3d:08:ba:df:45:8d:56:7a:a5:e1:64
366 - fingerprint:
367 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
368 - validity:
369 2025-08-13T14:58:40Z..2035-08-11T14:59:40Z
370",
371 );
372 }
373
374 #[test]
375 fn test_validity_fixture() {
376 let cert = cert_fixture();
377
378 assert_eq!(
379 cert.validity().not_before_as_timestamp().to_string(),
380 "2025-08-13T14:58:40Z"
381 );
382 assert_eq!(
383 cert.validity().not_after_as_timestamp().to_string(),
384 "2035-08-11T14:59:40Z"
385 );
386 }
387
388 #[test]
389 fn test_subject_fixture() {
390 let cert = cert_fixture();
391 assert_eq!(cert.subject(), "CN=itsallbroken.com");
392 }
393
394 #[test]
395 fn test_fingerprint_fixture() {
396 let cert = cert_fixture();
397
398 assert_eq!(
399 cert.fingerprint().as_hex_str(),
400 "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"
401 );
402 }
403
404 #[test]
405 fn test_round_trip_pem() {
406 let cert = cert_fixture();
407
408 let got = Certificate::from_pem(cert.generate_pem().as_bytes()).expect("valid cert");
409 assert_certs_equal(&got, &cert);
410 }
411
412 #[test]
413 fn test_der_zero_copy_construction() {
414 let buf = Bytes::from(cert_fixture().as_der().to_vec());
416 assert!(buf.is_unique());
417
418 let cert = Certificate::from_der(buf.clone()).expect("valid DER");
420
421 assert!(!buf.is_unique());
431
432 assert!(!cert.as_der().is_unique());
435 }
436
437 #[derive(Debug, Clone)]
439 enum PemPart {
440 Start,
442 End,
444 RandomData(String),
446 ValidCertData,
448 }
449
450 impl Display for PemPart {
452 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
453 match self {
454 PemPart::Start => f.write_str("-----BEGIN CERTIFICATE-----\n"),
455 PemPart::End => f.write_str("-----END CERTIFICATE-----\n"),
456 PemPart::RandomData(s) => f.write_str(s),
457 PemPart::ValidCertData => f.write_str(CERT_PEM_DATA),
458 }
459 }
460 }
461
462 fn arbitrary_pem_part() -> impl Strategy<Value = PemPart> {
464 prop_oneof![
465 Just(PemPart::Start),
466 Just(PemPart::End),
467 any::<String>().prop_map(PemPart::RandomData),
468 Just(PemPart::ValidCertData),
469 ]
470 }
471
472 proptest! {
473 #[test]
476 fn prop_from_pem(
477 parts in prop::collection::vec(arbitrary_pem_part(), 0..5),
478 ) {
479 prop_from_pem_test(parts);
480 }
481
482 #[test]
485 fn prop_invalid_pem_bytes(
486 binary in prop::collection::vec(any::<u8>(), 0..200),
487 ) {
488 let _ = Certificate::from_pem(&binary).expect_err("non-pem input");
489 }
490 }
491
492 fn prop_from_pem_test(parts: Vec<PemPart>) {
493 let pem: String = parts.iter().map(ToString::to_string).collect();
495
496 let cert = match Certificate::from_pem(pem.as_bytes()) {
498 Ok(cert) => {
499 cert
501 }
502 Err(_) => {
503 return;
505 }
506 };
507
508 assert_certs_equal(
510 &cert,
511 &Certificate::from_pem(cert.generate_pem().as_bytes()).unwrap(),
512 );
513
514 assert_certs_equal(&cert, &Certificate::from_der(cert.as_der()).unwrap());
516
517 assert!(matches!(
523 parts.as_slice(),
524 [PemPart::Start, PemPart::ValidCertData, PemPart::End]
525 ));
526 }
527}