1use crate::entropy::{EntropyRng, Source};
12use rcgen::{CertificateParams, DistinguishedName, DnType, KeyPair};
13use time::Duration;
14
15const ED25519_PKCS8_PREFIX: [u8; 16] = [
22 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20,
23];
24
25pub fn generate_certificate(
29 common_name: &str,
30 validity_days: u32,
31 source: &Source,
32) -> Result<serde_json::Value, String> {
33 let key_pair = if source.is_external() {
34 let mut seed = [0u8; 32];
37 crate::entropy::fill(source, "cert-ed25519", &mut seed)?;
38 let mut der = Vec::with_capacity(48);
39 der.extend_from_slice(&ED25519_PKCS8_PREFIX);
40 der.extend_from_slice(&seed);
41 KeyPair::from_pkcs8_der_and_sign_algo(&der.into(), &rcgen::PKCS_ED25519)
42 .map_err(|e| e.to_string())?
43 } else {
44 KeyPair::generate().map_err(|e| e.to_string())?
45 };
46 let mut params =
47 CertificateParams::new(vec![common_name.to_string()]).map_err(|e| e.to_string())?;
48 let mut dn = DistinguishedName::new();
49 dn.push(DnType::CommonName, common_name);
50 params.distinguished_name = dn;
51 params.not_after =
52 time::OffsetDateTime::now_utc() + Duration::days(validity_days.max(1) as i64);
53 let cert = params.self_signed(&key_pair).map_err(|e| e.to_string())?;
54 Ok(serde_json::json!({
55 "cert_pem": cert.pem(),
56 "key_pem": key_pair.serialize_pem(),
57 "entropy_source": source.label(),
58 }))
59}
60
61pub fn generate_ssh_keypair(comment: &str, source: &Source) -> Result<serde_json::Value, String> {
65 use ssh_key::{Algorithm, LineEnding, PrivateKey};
66
67 if let crate::entropy::Availability::Missing(why) = source.availability() {
72 return Err(format!("Entropy source {source} is unavailable: {why}"));
73 }
74 let mut rng = EntropyRng::new(source.clone(), "ssh-ed25519");
75 let mut private_key =
76 PrivateKey::random(&mut rng, Algorithm::Ed25519).map_err(|e| e.to_string())?;
77 private_key.set_comment(comment);
78 let public_key_str = private_key
79 .public_key()
80 .to_openssh()
81 .map_err(|e| e.to_string())?;
82 let private_key_str = private_key
83 .to_openssh(LineEnding::LF)
84 .map_err(|e| e.to_string())?;
85 Ok(serde_json::json!({
86 "public_key": public_key_str,
87 "private_key": private_key_str.to_string(),
88 "entropy_source": source.label(),
89 }))
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn a_cert_from_our_own_entropy_is_still_a_valid_cert() {
98 let src = Source::File {
102 path: "/dev/urandom".into(),
103 };
104 if src.availability() != crate::entropy::Availability::Ready {
105 return;
106 }
107 let v = generate_certificate("example.test", 30, &src).expect("generate");
108 assert!(v["cert_pem"]
109 .as_str()
110 .unwrap()
111 .starts_with("-----BEGIN CERTIFICATE-----"));
112 assert!(v["key_pem"].as_str().unwrap().contains("PRIVATE KEY"));
113 assert_eq!(v["entropy_source"], "file:/dev/urandom");
114 }
115
116 #[test]
117 fn ssh_keys_differ_between_generations() {
118 let a = generate_ssh_keypair("a", &Source::Os).unwrap();
119 let b = generate_ssh_keypair("b", &Source::Os).unwrap();
120 assert_ne!(a["private_key"], b["private_key"]);
121 }
122
123 #[test]
124 fn an_unavailable_source_refuses_before_generating() {
125 let src = Source::File {
126 path: "/nonexistent/device".into(),
127 };
128 assert!(generate_ssh_keypair("x", &src).is_err());
129 assert!(generate_certificate("x.test", 30, &src).is_err());
130 }
131}