acme_proxy/signer/local_ca/
key.rs1use std::path::Path;
21
22use rcgen::{KeyPair, PublicKeyData, SignatureAlgorithm, SigningKey};
23
24use crate::config::LocalCaConfig;
25
26pub enum CaSigningKey {
28 Software(KeyPair),
31 #[cfg(feature = "hsm")]
33 Pkcs11(super::pkcs11::Pkcs11SigningKey),
34}
35
36impl PublicKeyData for CaSigningKey {
37 fn der_bytes(&self) -> &[u8] {
38 match self {
39 Self::Software(key) => key.der_bytes(),
40 #[cfg(feature = "hsm")]
41 Self::Pkcs11(key) => key.der_bytes(),
42 }
43 }
44
45 fn algorithm(&self) -> &'static SignatureAlgorithm {
46 match self {
47 Self::Software(key) => key.algorithm(),
48 #[cfg(feature = "hsm")]
49 Self::Pkcs11(key) => key.algorithm(),
50 }
51 }
52}
53
54impl SigningKey for CaSigningKey {
55 fn sign(&self, msg: &[u8]) -> Result<Vec<u8>, rcgen::Error> {
56 match self {
57 Self::Software(key) => key.sign(msg),
58 #[cfg(feature = "hsm")]
59 Self::Pkcs11(key) => key.sign(msg),
60 }
61 }
62}
63
64impl std::fmt::Debug for CaSigningKey {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 match self {
69 Self::Software(_) => f.write_str("CaSigningKey::Software([elided])"),
70 #[cfg(feature = "hsm")]
71 Self::Pkcs11(key) => write!(f, "CaSigningKey::Pkcs11({key:?})"),
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum KeySource {
80 File,
83 Pkcs11,
85}
86
87impl KeySource {
88 pub fn parse(value: &str) -> anyhow::Result<Self> {
94 match value {
95 "file" => Ok(Self::File),
96 "pkcs11" => Ok(Self::Pkcs11),
97 other => anyhow::bail!(
98 "unsupported local_ca key_source: `{other}` (expected \"file\" or \"pkcs11\")"
99 ),
100 }
101 }
102}
103
104#[cfg_attr(not(feature = "hsm"), allow(dead_code))]
116pub fn read_pin(cfg: &LocalCaConfig) -> anyhow::Result<String> {
117 let pkcs11 = &cfg.pkcs11;
118
119 if !pkcs11.pin_file.is_empty() {
120 let path = Path::new(&pkcs11.pin_file);
121 crate::pemfile::warn_if_key_is_readable("local_ca_pkcs11_pin_permissive", path);
124 let raw = std::fs::read_to_string(path).map_err(|error| {
125 anyhow::anyhow!(
126 "local_ca pkcs11 pin_file `{}` could not be read: {error}",
127 pkcs11.pin_file
128 )
129 })?;
130 let pin = raw.trim_end().to_string();
131 if pin.is_empty() {
132 anyhow::bail!("local_ca pkcs11 pin_file `{}` is empty", pkcs11.pin_file);
133 }
134 return Ok(pin);
135 }
136
137 if !pkcs11.pin.is_empty() {
138 return Ok(pkcs11.pin.clone());
139 }
140
141 anyhow::bail!(
142 "local_ca key_source = \"pkcs11\" needs a user PIN: set \
143 signer.local_ca.pkcs11.pin_file, or the \
144 ACME_PROXY_SIGNER__LOCAL_CA__PKCS11__PIN environment variable"
145 )
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use crate::config::Pkcs11Config;
152 use crate::testutil::TempDir;
153 use rcgen::{CertificateParams, Issuer};
154
155 fn config_with(pkcs11: Pkcs11Config) -> LocalCaConfig {
156 LocalCaConfig {
157 pkcs11,
158 ..LocalCaConfig::default()
159 }
160 }
161
162 #[test]
163 fn key_source_parses_the_two_known_values() {
164 assert_eq!(KeySource::parse("file").unwrap(), KeySource::File);
165 assert_eq!(KeySource::parse("pkcs11").unwrap(), KeySource::Pkcs11);
166 }
167
168 #[test]
171 fn an_unknown_key_source_is_an_error_naming_it() {
172 let error = KeySource::parse("hsm").unwrap_err().to_string();
173 assert!(error.contains("hsm"), "{error}");
174 assert!(error.contains("pkcs11"), "{error}");
175 }
176
177 #[test]
182 fn the_software_variant_signs_exactly_as_the_bare_key_pair_does() {
183 let key_pair = KeyPair::generate().unwrap();
184 let wrapped = CaSigningKey::Software(KeyPair::from_pem(&key_pair.serialize_pem()).unwrap());
187
188 assert_eq!(wrapped.der_bytes(), key_pair.der_bytes());
191 assert_eq!(
192 wrapped.subject_public_key_info(),
193 key_pair.subject_public_key_info()
194 );
195 assert_eq!(wrapped.algorithm(), key_pair.algorithm());
196
197 let mut ca_params = CertificateParams::new(Vec::<String>::new()).unwrap();
201 ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Constrained(0));
202 let ca_cert = ca_params.self_signed(&wrapped).unwrap();
203
204 let issuer = Issuer::new(ca_params, wrapped);
205 let leaf_key = KeyPair::generate().unwrap();
206 let csr = CertificateParams::new(vec!["example.com".to_string()])
207 .unwrap()
208 .serialize_request(&leaf_key)
209 .unwrap();
210 let parsed =
211 rcgen::CertificateSigningRequestParams::from_der(&csr.der().to_vec().into()).unwrap();
212 let leaf = parsed.signed_by(&issuer).unwrap();
213
214 let (_, parsed_leaf) = x509_parser::parse_x509_certificate(leaf.der()).unwrap();
217 let (_, parsed_ca) = x509_parser::parse_x509_certificate(ca_cert.der()).unwrap();
218 assert!(
219 parsed_leaf
220 .verify_signature(Some(parsed_ca.public_key()))
221 .is_ok()
222 );
223 }
224
225 #[test]
226 fn debug_never_renders_key_material() {
227 let key = CaSigningKey::Software(KeyPair::generate().unwrap());
228 let rendered = format!("{key:?}");
229 assert!(rendered.contains("elided"), "{rendered}");
230 }
231
232 #[test]
233 fn a_pin_file_wins_over_the_pin_field() {
234 let dir = TempDir::new("pin");
235 let path = dir.write("hsm.pin", "from-file\n");
236 let cfg = config_with(Pkcs11Config {
237 pin: "from-config".to_string(),
238 pin_file: path.to_string_lossy().into_owned(),
239 ..Pkcs11Config::default()
240 });
241 assert_eq!(read_pin(&cfg).unwrap(), "from-file");
242 }
243
244 #[test]
247 fn a_trailing_newline_is_not_part_of_the_pin() {
248 let dir = TempDir::new("pin");
249 for written in ["1234\n", "1234\r\n", "1234"] {
250 let path = dir.write("hsm.pin", written);
251 let cfg = config_with(Pkcs11Config {
252 pin_file: path.to_string_lossy().into_owned(),
253 ..Pkcs11Config::default()
254 });
255 assert_eq!(read_pin(&cfg).unwrap(), "1234", "for {written:?}");
256 }
257 }
258
259 #[test]
260 fn the_pin_field_is_used_when_no_file_is_configured() {
261 let cfg = config_with(Pkcs11Config {
262 pin: "1234".to_string(),
263 ..Pkcs11Config::default()
264 });
265 assert_eq!(read_pin(&cfg).unwrap(), "1234");
266 }
267
268 #[test]
269 fn no_pin_at_all_is_an_error_naming_both_ways_to_set_one() {
270 let error = read_pin(&config_with(Pkcs11Config::default()))
271 .unwrap_err()
272 .to_string();
273 assert!(error.contains("pin_file"), "{error}");
274 assert!(
275 error.contains("ACME_PROXY_SIGNER__LOCAL_CA__PKCS11__PIN"),
276 "{error}"
277 );
278 }
279
280 #[test]
281 fn a_missing_pin_file_is_an_error_naming_the_path() {
282 let cfg = config_with(Pkcs11Config {
283 pin_file: "/nonexistent/acme-proxy/hsm.pin".to_string(),
284 ..Pkcs11Config::default()
285 });
286 let error = read_pin(&cfg).unwrap_err().to_string();
287 assert!(error.contains("/nonexistent/acme-proxy/hsm.pin"), "{error}");
288 }
289
290 #[test]
294 fn an_empty_pin_file_is_an_error() {
295 let dir = TempDir::new("pin");
296 let path = dir.write("hsm.pin", "\n");
297 let cfg = config_with(Pkcs11Config {
298 pin_file: path.to_string_lossy().into_owned(),
299 ..Pkcs11Config::default()
300 });
301 assert!(read_pin(&cfg).is_err());
302 }
303}