Skip to main content

uselesskey_ssh/
spec.rs

1/// Supported SSH key algorithms for fixture generation.
2#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Default)]
3pub enum SshSpec {
4    /// `ssh-ed25519`
5    #[default]
6    Ed25519,
7    /// `ssh-rsa`
8    Rsa,
9}
10
11impl SshSpec {
12    pub fn ed25519() -> Self {
13        Self::Ed25519
14    }
15
16    pub fn rsa() -> Self {
17        Self::Rsa
18    }
19
20    pub fn stable_bytes(&self) -> [u8; 1] {
21        match self {
22            Self::Ed25519 => [1],
23            Self::Rsa => [2],
24        }
25    }
26}
27
28/// SSH certificate type.
29#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Default)]
30pub enum SshCertType {
31    /// User cert (principal = username).
32    #[default]
33    User,
34    /// Host cert (principal = hostname).
35    Host,
36}
37
38impl SshCertType {
39    pub fn stable_byte(&self) -> u8 {
40        match self {
41            Self::User => 1,
42            Self::Host => 2,
43        }
44    }
45}
46
47/// Validity window (Unix seconds).
48#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
49pub struct SshValidity {
50    pub valid_after: u64,
51    pub valid_before: u64,
52}
53
54impl SshValidity {
55    pub fn new(valid_after: u64, valid_before: u64) -> Self {
56        Self {
57            valid_after,
58            valid_before,
59        }
60    }
61}
62
63/// SSH certificate fixture specification.
64#[derive(Clone, Debug, Eq, PartialEq, Hash)]
65pub struct SshCertSpec {
66    pub principals: Vec<String>,
67    pub validity: SshValidity,
68    pub cert_type: SshCertType,
69    pub critical_options: Vec<(String, String)>,
70    pub extensions: Vec<(String, String)>,
71}
72
73impl SshCertSpec {
74    pub fn user(
75        principals: impl IntoIterator<Item = impl Into<String>>,
76        validity: SshValidity,
77    ) -> Self {
78        Self {
79            principals: principals.into_iter().map(Into::into).collect(),
80            validity,
81            cert_type: SshCertType::User,
82            critical_options: Vec::new(),
83            extensions: Vec::new(),
84        }
85    }
86
87    pub fn host(
88        principals: impl IntoIterator<Item = impl Into<String>>,
89        validity: SshValidity,
90    ) -> Self {
91        Self {
92            principals: principals.into_iter().map(Into::into).collect(),
93            validity,
94            cert_type: SshCertType::Host,
95            critical_options: Vec::new(),
96            extensions: Vec::new(),
97        }
98    }
99
100    pub fn stable_bytes(&self) -> Vec<u8> {
101        fn push_str(buf: &mut Vec<u8>, s: &str) {
102            let len = u32::try_from(s.len()).unwrap_or(u32::MAX);
103            buf.extend_from_slice(&len.to_be_bytes());
104            buf.extend_from_slice(s.as_bytes());
105        }
106
107        let mut out = Vec::new();
108        out.push(self.cert_type.stable_byte());
109        out.extend_from_slice(&self.validity.valid_after.to_be_bytes());
110        out.extend_from_slice(&self.validity.valid_before.to_be_bytes());
111
112        out.extend_from_slice(
113            &u32::try_from(self.principals.len())
114                .unwrap_or(u32::MAX)
115                .to_be_bytes(),
116        );
117        for principal in &self.principals {
118            push_str(&mut out, principal);
119        }
120
121        out.extend_from_slice(
122            &u32::try_from(self.critical_options.len())
123                .unwrap_or(u32::MAX)
124                .to_be_bytes(),
125        );
126        for (name, value) in &self.critical_options {
127            push_str(&mut out, name);
128            push_str(&mut out, value);
129        }
130
131        out.extend_from_slice(
132            &u32::try_from(self.extensions.len())
133                .unwrap_or(u32::MAX)
134                .to_be_bytes(),
135        );
136        for (name, value) in &self.extensions {
137            push_str(&mut out, name);
138            push_str(&mut out, value);
139        }
140
141        out
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn ssh_spec_stable_bytes_are_unique() {
151        assert_ne!(
152            SshSpec::ed25519().stable_bytes(),
153            SshSpec::rsa().stable_bytes()
154        );
155    }
156
157    #[test]
158    fn cert_spec_stable_bytes_change_with_principal() {
159        let a = SshCertSpec::user(["alice"], SshValidity::new(1, 2)).stable_bytes();
160        let b = SshCertSpec::user(["bob"], SshValidity::new(1, 2)).stable_bytes();
161        assert_ne!(a, b);
162    }
163}