1use std::fmt;
4use std::path::Path;
5
6use zeroize::Zeroizing;
7
8use super::{Certificate, KeyError, PrivateKey};
9use crate::error::{Error, FileOperation};
10
11#[derive(Clone)]
24pub struct SigningIdentity {
25 key: PrivateKey,
26 chain: Vec<Certificate>,
27}
28
29impl SigningIdentity {
30 pub fn new(key: PrivateKey, chain: Vec<Certificate>) -> Result<Self, KeyError> {
32 let leaf = chain.first().ok_or(KeyError::MissingCertificate)?;
33 if leaf.public_key()? != key.public_key() {
34 return Err(KeyError::CertificateMismatch);
35 }
36 Ok(Self { key, chain })
37 }
38
39 pub fn from_pem(pem: &str) -> Result<Self, KeyError> {
42 Self::from_pem_blocks(pem, None)
43 }
44
45 #[cfg(feature = "keystore")]
48 pub fn from_encrypted_pem(pem: &str, password: &str) -> Result<Self, KeyError> {
49 Self::from_pem_blocks(pem, Some(password))
50 }
51
52 pub fn from_pem_file(path: impl AsRef<Path>) -> Result<Self, Error> {
54 let path = path.as_ref();
55 let pem = Zeroizing::new(std::fs::read_to_string(path).map_err(Error::file(FileOperation::Read, path))?);
56 Ok(Self::from_pem(&pem)?)
57 }
58
59 pub fn from_der(private_key_pkcs8: &[u8], certificate: &[u8]) -> Result<Self, KeyError> {
61 Self::new(PrivateKey::from_pkcs8_der(private_key_pkcs8)?, vec![Certificate::from_der(certificate)?])
62 }
63
64 fn from_pem_blocks(pem: &str, password: Option<&str>) -> Result<Self, KeyError> {
65 let mut key = None;
66 let mut chain = Vec::new();
67 for block in pem_blocks(pem) {
68 let (label, der) = pem_rfc7468::decode_vec(block.as_bytes())?;
69 let der = Zeroizing::new(der);
70 match label {
71 "CERTIFICATE" => chain.push(Certificate::from_der(&der)?),
72 "PRIVATE KEY" if key.is_none() => key = Some(PrivateKey::from_pkcs8_der(&der)?),
73 "RSA PRIVATE KEY" if key.is_none() => key = Some(PrivateKey::from_pkcs1_der(&der)?),
74 "EC PRIVATE KEY" if key.is_none() => key = Some(PrivateKey::from_sec1_der(&der)?),
75 "ENCRYPTED PRIVATE KEY" if key.is_none() => key = Some(decrypt(&der, password)?),
76 _ => {}
77 }
78 }
79 Self::new(key.ok_or(KeyError::MissingPrivateKey)?, chain)
80 }
81
82 pub fn key(&self) -> &PrivateKey {
83 &self.key
84 }
85
86 pub fn certificate(&self) -> &Certificate {
88 &self.chain[0]
89 }
90
91 pub fn chain(&self) -> &[Certificate] {
93 &self.chain
94 }
95}
96
97impl fmt::Debug for SigningIdentity {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 f.debug_struct("SigningIdentity").field("key", &self.key).field("certificate", self.certificate()).finish()
100 }
101}
102
103fn pem_blocks(pem: &str) -> impl Iterator<Item = &str> {
105 let mut rest = pem;
106 std::iter::from_fn(move || {
107 let start = rest.find("-----BEGIN ")?;
108 let label_end = start + rest[start..].find("-----\n").or_else(|| rest[start..].find("-----\r\n"))?;
109 let label = &rest[start + "-----BEGIN ".len()..label_end];
110 let end_marker = format!("-----END {label}-----");
111 let end = label_end + rest[label_end..].find(&end_marker)? + end_marker.len();
112 let block = &rest[start..end];
113 rest = &rest[end..];
114 Some(block)
115 })
116}
117
118#[cfg(feature = "keystore")]
119fn decrypt(der: &[u8], password: Option<&str>) -> Result<PrivateKey, KeyError> {
120 use der::Decode;
121 let password = password.ok_or(KeyError::PasswordRequired)?;
122 let encrypted = pkcs8::EncryptedPrivateKeyInfoRef::from_der(der).map_err(pkcs8::Error::from)?;
123 let document = encrypted.decrypt(password).map_err(|error| match error {
124 pkcs8::Error::EncryptedPrivateKey(pkcs8::pkcs5::Error::DecryptFailed) => KeyError::WrongPassword,
125 error => KeyError::Pkcs8(error),
126 })?;
127 PrivateKey::from_pkcs8_der(document.as_bytes())
128}
129
130#[cfg(not(feature = "keystore"))]
131fn decrypt(_: &[u8], _: Option<&str>) -> Result<PrivateKey, KeyError> {
132 Err(KeyError::EncryptionDisabled)
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 const DEBUG_PEM: &str = include_str!("../../tests/fixtures/keys/debug.pem");
140 const EC_PEM: &str = include_str!("../../tests/fixtures/keys/ec-p256.pem");
141 const MISMATCHED_CERT: &str = include_str!("../../tests/fixtures/keys/mismatched-cert.pem");
142
143 #[test]
144 fn loads_rsa_and_ec_bundles() {
145 let rsa = SigningIdentity::from_pem(DEBUG_PEM).unwrap();
146 assert!(matches!(rsa.key().kind(), super::super::KeyKind::Rsa { bits: 2048 }));
147 let ec = SigningIdentity::from_pem(EC_PEM).unwrap();
148 assert_eq!(ec.key().kind(), super::super::KeyKind::EcP256);
149 }
150
151 #[test]
152 fn rejects_a_certificate_for_another_key() {
153 let key_block = pem_blocks(DEBUG_PEM).find(|b| b.contains("PRIVATE KEY")).unwrap();
154 let bundle = format!("{key_block}\n{MISMATCHED_CERT}");
155 assert!(matches!(SigningIdentity::from_pem(&bundle), Err(KeyError::CertificateMismatch)));
156 }
157
158 #[test]
159 fn requires_both_parts() {
160 let cert_only = pem_blocks(DEBUG_PEM).find(|b| b.contains("CERTIFICATE")).unwrap();
161 assert!(matches!(SigningIdentity::from_pem(cert_only), Err(KeyError::MissingPrivateKey)));
162 let key_only = pem_blocks(DEBUG_PEM).find(|b| b.contains("PRIVATE KEY")).unwrap();
163 assert!(matches!(SigningIdentity::from_pem(key_only), Err(KeyError::MissingCertificate)));
164 }
165
166 #[cfg(feature = "keystore")]
167 #[test]
168 fn encrypted_pem_requires_the_right_password_and_preserves_identity() {
169 let encrypted = include_str!("../../tests/fixtures/keys/ec-p256-encrypted.pem");
170 let plain = SigningIdentity::from_pem(EC_PEM).unwrap();
171 let loaded = SigningIdentity::from_encrypted_pem(encrypted, "test-only").unwrap();
172 assert_eq!(loaded.certificate(), plain.certificate());
173 assert_eq!(loaded.key().public_key(), plain.key().public_key());
174 assert!(matches!(SigningIdentity::from_pem(encrypted), Err(KeyError::PasswordRequired)));
175 assert!(matches!(SigningIdentity::from_encrypted_pem(encrypted, "wrong"), Err(KeyError::WrongPassword)));
176 }
177
178 #[cfg(not(feature = "keystore"))]
179 #[test]
180 fn encrypted_pem_reports_the_missing_feature() {
181 let encrypted = include_str!("../../tests/fixtures/keys/ec-p256-encrypted.pem");
182 assert!(matches!(SigningIdentity::from_pem(encrypted), Err(KeyError::EncryptionDisabled)));
183 }
184}