1use std::fmt;
2use std::sync::Arc;
3
4use rand_chacha::ChaCha20Rng;
5use rand_chacha::rand_core::SeedableRng;
6use ssh_key::{Algorithm, LineEnding, PrivateKey};
7use uselesskey_core::Factory;
8
9use crate::SshSpec;
10
11pub const DOMAIN_SSH_KEYPAIR: &str = "uselesskey:ssh:keypair";
13
14#[derive(Clone)]
15pub struct SshKeyPair {
16 label: String,
17 spec: SshSpec,
18 inner: Arc<Inner>,
19}
20
21struct Inner {
22 private_key_openssh: String,
23 public_key_openssh: String,
24}
25
26impl fmt::Debug for SshKeyPair {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 f.debug_struct("SshKeyPair")
29 .field("label", &self.label)
30 .field("spec", &self.spec)
31 .finish_non_exhaustive()
32 }
33}
34
35pub trait SshFactoryExt {
36 fn ssh_key(&self, label: impl AsRef<str>, spec: SshSpec) -> SshKeyPair;
37}
38
39impl SshFactoryExt for Factory {
40 fn ssh_key(&self, label: impl AsRef<str>, spec: SshSpec) -> SshKeyPair {
41 SshKeyPair::new(self, label.as_ref(), spec)
42 }
43}
44
45impl SshKeyPair {
46 fn new(factory: &Factory, label: &str, spec: SshSpec) -> Self {
47 let spec_bytes = spec.stable_bytes();
48 let inner = factory.get_or_init(DOMAIN_SSH_KEYPAIR, label, &spec_bytes, "good", |seed| {
49 let mut rng = ChaCha20Rng::from_seed(*seed.bytes());
50 let private_key = PrivateKey::random(&mut rng, to_algorithm(spec))
51 .expect("SSH private key generation failed");
52 let public_key = private_key.public_key();
53
54 Inner {
55 private_key_openssh: private_key
56 .to_openssh(LineEnding::LF)
57 .expect("OpenSSH private key encoding failed")
58 .to_string(),
59 public_key_openssh: public_key
60 .to_openssh()
61 .expect("OpenSSH public key encoding failed"),
62 }
63 });
64
65 Self {
66 label: label.to_string(),
67 spec,
68 inner,
69 }
70 }
71
72 pub fn label(&self) -> &str {
73 &self.label
74 }
75
76 pub fn spec(&self) -> SshSpec {
77 self.spec
78 }
79
80 pub fn private_key_openssh(&self) -> &str {
82 &self.inner.private_key_openssh
83 }
84
85 pub fn authorized_key_line(&self) -> &str {
87 &self.inner.public_key_openssh
88 }
89}
90
91fn to_algorithm(spec: SshSpec) -> Algorithm {
92 match spec {
93 SshSpec::Ed25519 => Algorithm::Ed25519,
94 SshSpec::Rsa => Algorithm::Rsa { hash: None },
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use ssh_key::{PrivateKey, PublicKey};
101 use uselesskey_core::Seed;
102
103 use super::*;
104
105 #[test]
106 fn deterministic_authorized_key_lines() {
107 let fx = Factory::deterministic(Seed::from_env_value("ssh-det-lines").unwrap());
108 let a = fx.ssh_key("deploy", SshSpec::ed25519());
109 let b = fx.ssh_key("deploy", SshSpec::ed25519());
110 assert_eq!(a.authorized_key_line(), b.authorized_key_line());
111 }
112
113 #[test]
114 fn round_trip_parse_private_and_public() {
115 let fx = Factory::random();
116 let k = fx.ssh_key("infra", SshSpec::rsa());
117
118 let parsed_private = PrivateKey::from_openssh(k.private_key_openssh()).unwrap();
119 let parsed_public = PublicKey::from_openssh(k.authorized_key_line()).unwrap();
120
121 assert_eq!(parsed_private.public_key(), &parsed_public);
122 }
123}