tor_keymgr/keystore/arti/
ssh.rs
1use tor_error::internal;
7use tor_key_forge::{ErasedKey, KeyType, SshKeyAlgorithm, SshKeyData};
8
9use crate::keystore::arti::err::ArtiNativeKeystoreError;
10use crate::Result;
11
12use std::path::PathBuf;
13use zeroize::Zeroizing;
14
15pub(super) struct UnparsedOpenSshKey {
22 inner: Zeroizing<String>,
24 path: PathBuf,
26}
27
28macro_rules! parse_openssh {
30 (PRIVATE $key:expr, $key_type:expr) => {{
31 SshKeyData::try_from_keypair_data(parse_openssh!(
32 $key,
33 $key_type,
34 ssh_key::private::PrivateKey::from_openssh
35 ).key_data().clone())?
36 }};
37
38 (PUBLIC $key:expr, $key_type:expr) => {{
39 SshKeyData::try_from_key_data(parse_openssh!(
40 $key,
41 $key_type,
42 ssh_key::public::PublicKey::from_openssh
43 ).key_data().clone())?
44 }};
45
46 ($key:expr, $key_type:expr, $parse_fn:path) => {{
47 let key = $parse_fn(&*$key.inner).map_err(|e| {
48 ArtiNativeKeystoreError::SshKeyParse {
49 path: $key.path.clone(),
53 key_type: $key_type.clone().clone(),
54 err: e.into(),
55 }
56 })?;
57
58 let wanted_key_algo = ssh_algorithm($key_type)?;
59
60 if SshKeyAlgorithm::from(key.algorithm()) != wanted_key_algo {
61 return Err(ArtiNativeKeystoreError::UnexpectedSshKeyType {
62 path: $key.path,
63 wanted_key_algo,
64 found_key_algo: key.algorithm().into(),
65 }.into());
66 }
67
68 key
69 }};
70}
71
72fn ssh_algorithm(key_type: &KeyType) -> Result<SshKeyAlgorithm> {
74 match key_type {
75 KeyType::Ed25519Keypair | KeyType::Ed25519PublicKey => Ok(SshKeyAlgorithm::Ed25519),
76 KeyType::X25519StaticKeypair | KeyType::X25519PublicKey => Ok(SshKeyAlgorithm::X25519),
77 KeyType::Ed25519ExpandedKeypair => Ok(SshKeyAlgorithm::Ed25519Expanded),
78 &_ => {
79 Err(ArtiNativeKeystoreError::Bug(internal!("Unknown SSH key type {key_type:?}")).into())
80 }
81 }
82}
83
84impl UnparsedOpenSshKey {
85 pub(crate) fn new(inner: String, path: PathBuf) -> Self {
89 Self {
90 inner: Zeroizing::new(inner),
91 path,
92 }
93 }
94
95 pub(crate) fn parse_ssh_format_erased(self, key_type: &KeyType) -> Result<ErasedKey> {
100 match key_type {
101 KeyType::Ed25519Keypair
102 | KeyType::X25519StaticKeypair
103 | KeyType::Ed25519ExpandedKeypair => {
104 Ok(parse_openssh!(PRIVATE self, key_type).into_erased()?)
105 }
106 KeyType::Ed25519PublicKey | KeyType::X25519PublicKey => {
107 Ok(parse_openssh!(PUBLIC self, key_type).into_erased()?)
108 }
109 &_ => Err(ArtiNativeKeystoreError::Bug(internal!("Unknown SSH key type")).into()),
110 }
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 #![allow(clippy::bool_assert_comparison)]
118 #![allow(clippy::clone_on_copy)]
119 #![allow(clippy::dbg_macro)]
120 #![allow(clippy::mixed_attributes_style)]
121 #![allow(clippy::print_stderr)]
122 #![allow(clippy::print_stdout)]
123 #![allow(clippy::single_char_pattern)]
124 #![allow(clippy::unwrap_used)]
125 #![allow(clippy::unchecked_duration_subtraction)]
126 #![allow(clippy::useless_vec)]
127 #![allow(clippy::needless_pass_by_value)]
128 use crate::test_utils::ssh_keys::*;
131 use crate::test_utils::sshkeygen_ed25519_strings;
132
133 use tor_key_forge::{EncodableItem, KeystoreItem};
134 use tor_llcrypto::pk::{curve25519, ed25519};
135
136 use super::*;
137
138 const ED25519_OPENSSH_COMMENT: &str = "armadillo@example.com";
143 const ED25519_EXPANDED_OPENSSH_COMMENT: &str = "armadillo@example.com";
144 const X25519_OPENSSH_COMMENT: &str = "test-key";
145 const ED25519_SSHKEYGEN_COMMENT: &str = "";
146
147 trait ToBytes {
149 type Bytes;
150 fn to_bytes(&self) -> Self::Bytes;
151 }
152
153 impl ToBytes for ed25519::Keypair {
154 type Bytes = [u8; 32];
155 fn to_bytes(&self) -> Self::Bytes {
156 self.to_bytes()
157 }
158 }
159
160 impl ToBytes for ed25519::PublicKey {
161 type Bytes = [u8; 32];
162 fn to_bytes(&self) -> Self::Bytes {
163 self.to_bytes()
164 }
165 }
166
167 impl ToBytes for ed25519::ExpandedKeypair {
168 type Bytes = [u8; 64];
169 fn to_bytes(&self) -> Self::Bytes {
170 self.to_secret_key_bytes()
171 }
172 }
173
174 impl ToBytes for curve25519::StaticKeypair {
175 type Bytes = [u8; 32];
176 fn to_bytes(&self) -> Self::Bytes {
177 self.secret.to_bytes()
178 }
179 }
180
181 impl ToBytes for curve25519::PublicKey {
182 type Bytes = [u8; 32];
183 fn to_bytes(&self) -> Self::Bytes {
184 self.to_bytes()
185 }
186 }
187
188 fn mangle_ed25519(key: &mut String) {
190 if key.len() > 150 {
191 key.replace_range(107..178, "hello");
193 } else {
194 key.insert_str(12, "garbage");
196 }
197 }
198
199 macro_rules! test_parse_ssh_format_erased {
206 ($key_ty:tt, $key:expr, err = $expect_err:expr) => {{
207 let key_type = KeyType::$key_ty;
208 let key = UnparsedOpenSshKey::new($key.into(), PathBuf::from("/dummy/path"));
209 let err = key
210 .parse_ssh_format_erased(&key_type)
211 .map(|_| "<type erased key>")
212 .unwrap_err();
213
214 assert_eq!(err.to_string(), $expect_err);
215 }};
216
217 ($key_ty:tt, $enc1:expr, $expected_ty:path, $comment:expr) => {{
218 let enc1 = $enc1.trim();
219 let key_type = KeyType::$key_ty;
220 let key = UnparsedOpenSshKey::new(enc1.into(), PathBuf::from("/test/path"));
221 let erased_key = key.parse_ssh_format_erased(&key_type).unwrap();
222
223 let Ok(dec1) = erased_key.downcast::<$expected_ty>() else {
224 panic!("failed to downcast");
225 };
226
227 let keystore_item = EncodableItem::as_keystore_item(&*dec1).unwrap();
228 let enc2 = match keystore_item {
229 KeystoreItem::Key(key) => key.to_openssh_string($comment).unwrap(),
230 _ => panic!("unexpected keystore item type {keystore_item:?}"),
231 };
232 let enc2 = enc2.trim();
233
234 match key_type {
245 KeyType::Ed25519Keypair |
246 KeyType::X25519StaticKeypair |
247 KeyType::Ed25519ExpandedKeypair => (),
248 _ => assert_eq!(enc1, enc2),
249 }
250
251 let key = UnparsedOpenSshKey::new(enc2.into(), PathBuf::from("/test/path"));
252 let erased_key = key.parse_ssh_format_erased(&key_type).unwrap();
253 let Ok(dec2) = erased_key.downcast::<$expected_ty>() else {
254 panic!("failed to downcast");
255 };
256
257 assert_eq!(dec1.to_bytes(), dec2.to_bytes());
258 }};
259 }
260
261 #[test]
262 fn wrong_key_type() {
263 let key_type = KeyType::Ed25519Keypair;
264 let key = UnparsedOpenSshKey::new(DSA_OPENSSH.into(), PathBuf::from("/test/path"));
265 let err = key
266 .parse_ssh_format_erased(&key_type)
267 .map(|_| "<type erased key>")
268 .unwrap_err();
269
270 assert_eq!(
271 err.to_string(),
272 format!(
273 "Unexpected OpenSSH key type: wanted {}, found {}",
274 SshKeyAlgorithm::Ed25519,
275 SshKeyAlgorithm::Dsa
276 )
277 );
278
279 test_parse_ssh_format_erased!(
280 Ed25519Keypair,
281 DSA_OPENSSH,
282 err = format!(
283 "Unexpected OpenSSH key type: wanted {}, found {}",
284 SshKeyAlgorithm::Ed25519,
285 SshKeyAlgorithm::Dsa
286 )
287 );
288 }
289
290 #[test]
291 fn invalid_ed25519_key() {
292 test_parse_ssh_format_erased!(
293 Ed25519Keypair,
294 ED25519_OPENSSH_BAD,
295 err = "Failed to parse OpenSSH with type Ed25519Keypair"
296 );
297
298 test_parse_ssh_format_erased!(
299 Ed25519Keypair,
300 ED25519_OPENSSH_BAD_PUB,
301 err = "Failed to parse OpenSSH with type Ed25519Keypair"
302 );
303
304 test_parse_ssh_format_erased!(
305 Ed25519PublicKey,
306 ED25519_OPENSSH_BAD_PUB,
307 err = "Failed to parse OpenSSH with type Ed25519PublicKey"
308 );
309
310 if let Ok((mut bad, mut bad_pub)) = sshkeygen_ed25519_strings() {
311 mangle_ed25519(&mut bad);
312 mangle_ed25519(&mut bad_pub);
313
314 test_parse_ssh_format_erased!(
315 Ed25519Keypair,
316 &bad,
317 err = "Failed to parse OpenSSH with type Ed25519Keypair"
318 );
319
320 test_parse_ssh_format_erased!(
321 Ed25519Keypair,
322 &bad_pub,
323 err = "Failed to parse OpenSSH with type Ed25519Keypair"
324 );
325
326 test_parse_ssh_format_erased!(
327 Ed25519PublicKey,
328 &bad_pub,
329 err = "Failed to parse OpenSSH with type Ed25519PublicKey"
330 );
331 }
332 }
333
334 #[test]
335 fn ed25519_key() {
336 test_parse_ssh_format_erased!(
337 Ed25519Keypair,
338 ED25519_OPENSSH,
339 ed25519::Keypair,
340 ED25519_OPENSSH_COMMENT
341 );
342 test_parse_ssh_format_erased!(
343 Ed25519PublicKey,
344 ED25519_OPENSSH_PUB,
345 ed25519::PublicKey,
346 ED25519_OPENSSH_COMMENT
347 );
348
349 if let Ok((enc1, enc1_pub)) = sshkeygen_ed25519_strings() {
350 test_parse_ssh_format_erased!(
351 Ed25519Keypair,
352 enc1,
353 ed25519::Keypair,
354 ED25519_SSHKEYGEN_COMMENT
355 );
356 test_parse_ssh_format_erased!(
357 Ed25519PublicKey,
358 enc1_pub,
359 ed25519::PublicKey,
360 ED25519_SSHKEYGEN_COMMENT
361 );
362 }
363 }
364
365 #[test]
366 fn invalid_expanded_ed25519_key() {
367 test_parse_ssh_format_erased!(
368 Ed25519ExpandedKeypair,
369 ED25519_EXPANDED_OPENSSH_BAD,
370 err = "Failed to parse OpenSSH with type Ed25519ExpandedKeypair"
371 );
372 }
373
374 #[test]
375 fn expanded_ed25519_key() {
376 test_parse_ssh_format_erased!(
377 Ed25519ExpandedKeypair,
378 ED25519_EXPANDED_OPENSSH,
379 ed25519::ExpandedKeypair,
380 ED25519_EXPANDED_OPENSSH_COMMENT
381 );
382
383 test_parse_ssh_format_erased!(
384 Ed25519PublicKey,
385 ED25519_EXPANDED_OPENSSH_PUB, err = "Failed to parse OpenSSH with type Ed25519PublicKey"
387 );
388 }
389
390 #[test]
391 fn x25519_key() {
392 test_parse_ssh_format_erased!(
393 X25519StaticKeypair,
394 X25519_OPENSSH,
395 curve25519::StaticKeypair,
396 X25519_OPENSSH_COMMENT
397 );
398
399 test_parse_ssh_format_erased!(
400 X25519PublicKey,
401 X25519_OPENSSH_PUB,
402 curve25519::PublicKey,
403 X25519_OPENSSH_COMMENT
404 );
405 }
406
407 #[test]
408 fn invalid_x25519_key() {
409 test_parse_ssh_format_erased!(
410 X25519StaticKeypair,
411 X25519_OPENSSH_UNKNOWN_ALGORITHM,
412 err = "Unexpected OpenSSH key type: wanted X25519, found pangolin@torproject.org"
413 );
414
415 test_parse_ssh_format_erased!(
416 X25519PublicKey,
417 X25519_OPENSSH_UNKNOWN_ALGORITHM, err = "Failed to parse OpenSSH with type X25519PublicKey"
419 );
420
421 test_parse_ssh_format_erased!(
422 X25519PublicKey,
423 X25519_OPENSSH_UNKNOWN_ALGORITHM_PUB,
424 err = "Unexpected OpenSSH key type: wanted X25519, found armadillo@torproject.org"
425 );
426 }
427}