1#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Default)]
3pub enum SshSpec {
4 #[default]
6 Ed25519,
7 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#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Default)]
30pub enum SshCertType {
31 #[default]
33 User,
34 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#[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#[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}