tor_keymgr/keystore/arti/
ssh.rs1use tor_error::internal;
7use tor_key_forge::{ErasedKey, KeyType, SshKeyAlgorithm, SshKeyData};
8
9use crate::Result;
10use crate::keystore::arti::err::ArtiNativeKeystoreError;
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 KeyType::RsaKeypair | KeyType::RsaPublicKey => Ok(SshKeyAlgorithm::Rsa),
79 &_ => {
80 Err(ArtiNativeKeystoreError::Bug(internal!("Unknown SSH key type {key_type:?}")).into())
81 }
82 }
83}
84
85impl UnparsedOpenSshKey {
86 pub(crate) fn new(inner: String, path: PathBuf) -> Self {
90 Self {
91 inner: Zeroizing::new(inner),
92 path,
93 }
94 }
95
96 pub(crate) fn parse_ssh_format_erased(self, key_type: &KeyType) -> Result<ErasedKey> {
101 match key_type {
102 KeyType::Ed25519Keypair
103 | KeyType::X25519StaticKeypair
104 | KeyType::Ed25519ExpandedKeypair
105 | KeyType::RsaKeypair => Ok(parse_openssh!(PRIVATE self, key_type).into_erased()?),
106 KeyType::Ed25519PublicKey | KeyType::X25519PublicKey | KeyType::RsaPublicKey => {
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_time_subtraction)]
126 #![allow(clippy::useless_vec)]
127 #![allow(clippy::needless_pass_by_value)]
128 #![allow(clippy::string_slice)] use crate::test_utils::ssh_keys::*;
132 use crate::test_utils::sshkeygen_ed25519_strings;
133
134 use tor_key_forge::{EncodableItem, KeystoreItem};
135 use tor_llcrypto::pk::{curve25519, ed25519};
136
137 use super::*;
138
139 const ED25519_OPENSSH_COMMENT: &str = "armadillo@example.com";
144 const ED25519_EXPANDED_OPENSSH_COMMENT: &str = "armadillo@example.com";
145 const X25519_OPENSSH_COMMENT: &str = "test-key";
146 const ED25519_SSHKEYGEN_COMMENT: &str = "";
147
148 trait ToBytes {
150 type Bytes;
151 fn to_bytes(&self) -> Self::Bytes;
152 }
153
154 impl ToBytes for ed25519::Keypair {
155 type Bytes = [u8; 32];
156 fn to_bytes(&self) -> Self::Bytes {
157 self.to_bytes()
158 }
159 }
160
161 impl ToBytes for ed25519::PublicKey {
162 type Bytes = [u8; 32];
163 fn to_bytes(&self) -> Self::Bytes {
164 self.to_bytes()
165 }
166 }
167
168 impl ToBytes for ed25519::ExpandedKeypair {
169 type Bytes = [u8; 64];
170 fn to_bytes(&self) -> Self::Bytes {
171 self.to_secret_key_bytes()
172 }
173 }
174
175 impl ToBytes for curve25519::StaticKeypair {
176 type Bytes = [u8; 32];
177 fn to_bytes(&self) -> Self::Bytes {
178 self.secret.to_bytes()
179 }
180 }
181
182 impl ToBytes for curve25519::PublicKey {
183 type Bytes = [u8; 32];
184 fn to_bytes(&self) -> Self::Bytes {
185 self.to_bytes()
186 }
187 }
188
189 fn mangle_ed25519(key: &mut String) {
191 if key.len() > 150 {
192 key.replace_range(107..178, "hello");
194 } else {
195 key.insert_str(12, "garbage");
197 }
198 }
199
200 macro_rules! test_parse_ssh_format_erased {
207 ($key_ty:tt, $key:expr, err = $expect_err:expr) => {{
208 let key_type = KeyType::$key_ty;
209 let key = UnparsedOpenSshKey::new($key.into(), PathBuf::from("/dummy/path"));
210 let err = key
211 .parse_ssh_format_erased(&key_type)
212 .map(|_| "<type erased key>")
213 .unwrap_err();
214
215 assert_eq!(err.to_string(), $expect_err);
216 }};
217
218 ($key_ty:tt, $enc1:expr, $expected_ty:path, $comment:expr) => {{
219 let enc1 = $enc1.trim();
220 let key_type = KeyType::$key_ty;
221 let key = UnparsedOpenSshKey::new(enc1.into(), PathBuf::from("/test/path"));
222 let erased_key = key.parse_ssh_format_erased(&key_type).unwrap();
223
224 let Ok(dec1) = erased_key.downcast::<$expected_ty>() else {
225 panic!("failed to downcast");
226 };
227
228 let keystore_item = EncodableItem::as_keystore_item(&*dec1).unwrap();
229 let enc2 = match keystore_item {
230 KeystoreItem::Key(key) => key.to_openssh_string($comment).unwrap(),
231 _ => panic!("unexpected keystore item type {keystore_item:?}"),
232 };
233 let enc2 = enc2.trim();
234
235 match key_type {
246 KeyType::Ed25519Keypair |
247 KeyType::X25519StaticKeypair |
248 KeyType::Ed25519ExpandedKeypair => (),
249 _ => assert_eq!(enc1, enc2),
250 }
251
252 let key = UnparsedOpenSshKey::new(enc2.into(), PathBuf::from("/test/path"));
253 let erased_key = key.parse_ssh_format_erased(&key_type).unwrap();
254 let Ok(dec2) = erased_key.downcast::<$expected_ty>() else {
255 panic!("failed to downcast");
256 };
257
258 assert_eq!(dec1.to_bytes(), dec2.to_bytes());
259 }};
260 }
261
262 #[test]
263 fn wrong_key_type() {
264 let key_type = KeyType::Ed25519Keypair;
265 let key = UnparsedOpenSshKey::new(DSA_OPENSSH.into(), PathBuf::from("/test/path"));
266 let err = key
267 .parse_ssh_format_erased(&key_type)
268 .map(|_| "<type erased key>")
269 .unwrap_err();
270
271 assert_eq!(
272 err.to_string(),
273 format!(
274 "Unexpected OpenSSH key type: wanted {}, found {}",
275 SshKeyAlgorithm::Ed25519,
276 SshKeyAlgorithm::Dsa
277 )
278 );
279
280 test_parse_ssh_format_erased!(
281 Ed25519Keypair,
282 DSA_OPENSSH,
283 err = format!(
284 "Unexpected OpenSSH key type: wanted {}, found {}",
285 SshKeyAlgorithm::Ed25519,
286 SshKeyAlgorithm::Dsa
287 )
288 );
289 }
290
291 #[test]
292 fn invalid_ed25519_key() {
293 test_parse_ssh_format_erased!(
294 Ed25519Keypair,
295 ED25519_OPENSSH_BAD,
296 err = "Failed to parse OpenSSH with type Ed25519Keypair"
297 );
298
299 test_parse_ssh_format_erased!(
300 Ed25519Keypair,
301 ED25519_OPENSSH_BAD_PUB,
302 err = "Failed to parse OpenSSH with type Ed25519Keypair"
303 );
304
305 test_parse_ssh_format_erased!(
306 Ed25519PublicKey,
307 ED25519_OPENSSH_BAD_PUB,
308 err = "Failed to parse OpenSSH with type Ed25519PublicKey"
309 );
310
311 if let Ok((mut bad, mut bad_pub)) = sshkeygen_ed25519_strings() {
312 mangle_ed25519(&mut bad);
313 mangle_ed25519(&mut bad_pub);
314
315 test_parse_ssh_format_erased!(
316 Ed25519Keypair,
317 &bad,
318 err = "Failed to parse OpenSSH with type Ed25519Keypair"
319 );
320
321 test_parse_ssh_format_erased!(
322 Ed25519Keypair,
323 &bad_pub,
324 err = "Failed to parse OpenSSH with type Ed25519Keypair"
325 );
326
327 test_parse_ssh_format_erased!(
328 Ed25519PublicKey,
329 &bad_pub,
330 err = "Failed to parse OpenSSH with type Ed25519PublicKey"
331 );
332 }
333 }
334
335 #[test]
336 fn ed25519_key() {
337 test_parse_ssh_format_erased!(
338 Ed25519Keypair,
339 ED25519_OPENSSH,
340 ed25519::Keypair,
341 ED25519_OPENSSH_COMMENT
342 );
343 test_parse_ssh_format_erased!(
344 Ed25519PublicKey,
345 ED25519_OPENSSH_PUB,
346 ed25519::PublicKey,
347 ED25519_OPENSSH_COMMENT
348 );
349
350 if let Ok((enc1, enc1_pub)) = sshkeygen_ed25519_strings() {
351 test_parse_ssh_format_erased!(
352 Ed25519Keypair,
353 enc1,
354 ed25519::Keypair,
355 ED25519_SSHKEYGEN_COMMENT
356 );
357 test_parse_ssh_format_erased!(
358 Ed25519PublicKey,
359 enc1_pub,
360 ed25519::PublicKey,
361 ED25519_SSHKEYGEN_COMMENT
362 );
363 }
364 }
365
366 #[test]
367 fn invalid_expanded_ed25519_key() {
368 test_parse_ssh_format_erased!(
369 Ed25519ExpandedKeypair,
370 ED25519_EXPANDED_OPENSSH_BAD,
371 err = "Failed to parse OpenSSH with type Ed25519ExpandedKeypair"
372 );
373 }
374
375 #[test]
376 fn expanded_ed25519_key() {
377 test_parse_ssh_format_erased!(
378 Ed25519ExpandedKeypair,
379 ED25519_EXPANDED_OPENSSH,
380 ed25519::ExpandedKeypair,
381 ED25519_EXPANDED_OPENSSH_COMMENT
382 );
383
384 test_parse_ssh_format_erased!(
385 Ed25519PublicKey,
386 ED25519_EXPANDED_OPENSSH_PUB, err = "Failed to parse OpenSSH with type Ed25519PublicKey"
388 );
389 }
390
391 #[test]
392 fn x25519_key() {
393 test_parse_ssh_format_erased!(
394 X25519StaticKeypair,
395 X25519_OPENSSH,
396 curve25519::StaticKeypair,
397 X25519_OPENSSH_COMMENT
398 );
399
400 test_parse_ssh_format_erased!(
401 X25519PublicKey,
402 X25519_OPENSSH_PUB,
403 curve25519::PublicKey,
404 X25519_OPENSSH_COMMENT
405 );
406 }
407
408 #[test]
409 fn invalid_x25519_key() {
410 test_parse_ssh_format_erased!(
411 X25519StaticKeypair,
412 X25519_OPENSSH_UNKNOWN_ALGORITHM,
413 err = "Unexpected OpenSSH key type: wanted X25519, found pangolin@torproject.org"
414 );
415
416 test_parse_ssh_format_erased!(
417 X25519PublicKey,
418 X25519_OPENSSH_UNKNOWN_ALGORITHM, err = "Failed to parse OpenSSH with type X25519PublicKey"
420 );
421
422 test_parse_ssh_format_erased!(
423 X25519PublicKey,
424 X25519_OPENSSH_UNKNOWN_ALGORITHM_PUB,
425 err = "Unexpected OpenSSH key type: wanted X25519, found armadillo@torproject.org"
426 );
427 }
428}