1use rustls::crypto::CryptoProvider;
5use rustls::pki_types::pem::{self, PemObject};
6use rustls::pki_types::{CertificateDer, PrivateKeyDer};
7use rustls::sign::CertifiedKey;
8use time::OffsetDateTime;
9
10#[derive(Debug, thiserror::Error)]
13pub enum CertError {
14 #[error("no usable certificate in the chain: {0}")]
17 Chain(pem::Error),
18 #[error("no private key found in the key PEM")]
20 NoKey,
21 #[error("could not parse the private key: {0}")]
23 Key(pem::Error),
24 #[error("the crypto provider could not load this private key: {0}")]
27 UnsupportedKey(rustls::Error),
28 #[error("the private key does not match the certificate: {0}")]
30 KeyMismatch(rustls::Error),
31 #[error("could not read the certificate's expiry: {0}")]
33 Expiry(String),
34}
35
36pub fn certified_key_from_pem(
46 cert_chain_pem: &str,
47 key_pem: &str,
48 provider: &CryptoProvider,
49) -> Result<CertifiedKey, CertError> {
50 let chain: Vec<CertificateDer<'static>> =
51 CertificateDer::pem_slice_iter(cert_chain_pem.as_bytes())
52 .collect::<Result<_, _>>()
53 .map_err(CertError::Chain)?;
54 if chain.is_empty() {
55 return Err(CertError::Chain(pem::Error::NoItemsFound));
56 }
57
58 let key = PrivateKeyDer::from_pem_slice(key_pem.as_bytes()).map_err(|err| match err {
59 pem::Error::NoItemsFound => CertError::NoKey,
60 other => CertError::Key(other),
61 })?;
62
63 CertifiedKey::from_der(chain, key, provider).map_err(|err| match err {
64 rustls::Error::InconsistentKeys(_) => CertError::KeyMismatch(err),
65 other => CertError::UnsupportedKey(other),
66 })
67}
68
69pub fn leaf_expiry(chain: &[CertificateDer<'_>]) -> Result<OffsetDateTime, CertError> {
73 let leaf = chain
74 .first()
75 .ok_or_else(|| CertError::Expiry("empty chain".to_owned()))?;
76 let (_, parsed) = x509_parser::parse_x509_certificate(leaf.as_ref())
77 .map_err(|err| CertError::Expiry(err.to_string()))?;
78 let not_after = parsed.validity().not_after;
79 OffsetDateTime::from_unix_timestamp(not_after.timestamp())
80 .map_err(|err| CertError::Expiry(err.to_string()))
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use crate::test_support::self_signed_pem;
87
88 #[test]
89 fn round_trips_a_self_signed_pair() {
90 let (cert_pem, key_pem) = self_signed_pem("round-trip.example.invalid");
91 let provider = rustls::crypto::ring::default_provider();
92 let certified = certified_key_from_pem(&cert_pem, &key_pem, &provider).unwrap();
93 assert_eq!(certified.cert.len(), 1);
94 }
95
96 #[test]
97 fn reads_the_expiry_back_from_the_der() {
98 let (cert_pem, _key_pem) = self_signed_pem("expiry.example.invalid");
99 let chain: Vec<CertificateDer<'static>> =
100 CertificateDer::pem_slice_iter(cert_pem.as_bytes())
101 .collect::<Result<_, _>>()
102 .unwrap();
103 let expiry = leaf_expiry(&chain).unwrap();
104 assert!(expiry > OffsetDateTime::now_utc());
105 }
106
107 #[test]
108 fn refuses_an_empty_chain() {
109 let (_cert_pem, key_pem) = self_signed_pem("empty-chain.example.invalid");
110 let provider = rustls::crypto::ring::default_provider();
111 let err = certified_key_from_pem("", &key_pem, &provider).unwrap_err();
112 assert!(matches!(err, CertError::Chain(_)));
113 }
114
115 #[test]
116 fn refuses_a_key_from_another_pair() {
117 let (cert_pem, _) = self_signed_pem("leaf.example.invalid");
118 let (_, other_key_pem) = self_signed_pem("other.example.invalid");
119 let provider = rustls::crypto::ring::default_provider();
120 let err = certified_key_from_pem(&cert_pem, &other_key_pem, &provider).unwrap_err();
121 assert!(matches!(err, CertError::KeyMismatch(_)), "{err:?}");
122 }
123
124 #[test]
125 fn refuses_a_pem_with_no_key() {
126 let (cert_pem, _key_pem) = self_signed_pem("no-key.example.invalid");
127 let provider = rustls::crypto::ring::default_provider();
128 let err = certified_key_from_pem(&cert_pem, "", &provider).unwrap_err();
129 assert!(matches!(err, CertError::NoKey));
130 }
131}