1
//! Traits for converting keys to and from OpenSSH format.
2
//
3
// TODO #902: OpenSSH keys can have passphrases. While the current implementation isn't able to
4
// handle such keys, we will eventually need to support them (this will be a breaking API change).
5

            
6
use ssh_key::private::KeypairData;
7
use ssh_key::public::KeyData;
8
use ssh_key::Algorithm;
9

            
10
use crate::keystore::arti::err::ArtiNativeKeystoreError;
11
use crate::{ErasedKey, KeyType, Result};
12

            
13
use tor_llcrypto::pk::{curve25519, ed25519};
14
use zeroize::Zeroizing;
15

            
16
use std::path::PathBuf;
17

            
18
use super::UnknownKeyTypeError;
19

            
20
/// The algorithm string for x25519 SSH keys.
21
///
22
/// See <https://spec.torproject.org/ssh-protocols.html>
23
pub(crate) const X25519_ALGORITHM_NAME: &str = "x25519@spec.torproject.org";
24

            
25
/// The algorithm string for expanded ed25519 SSH keys.
26
///
27
/// See <https://spec.torproject.org/ssh-protocols.html>
28
pub(crate) const ED25519_EXPANDED_ALGORITHM_NAME: &str = "ed25519-expanded@spec.torproject.org";
29

            
30
/// An unparsed OpenSSH key.
31
///
32
/// Note: This is a wrapper around the contents of a file we think is an OpenSSH key. The inner
33
/// value is unchecked/unvalidated, and might not actually be a valid OpenSSH key.
34
///
35
/// The inner value is zeroed on drop.
36
pub(crate) struct UnparsedOpenSshKey {
37
    /// The contents of an OpenSSH key file.
38
    inner: Zeroizing<String>,
39
    /// The path of the file (for error reporting).
40
    path: PathBuf,
41
}
42

            
43
impl UnparsedOpenSshKey {
44
    /// Create a new [`UnparsedOpenSshKey`].
45
    ///
46
    /// The contents of `inner` are erased on drop.
47
14329
    pub(crate) fn new(inner: String, path: PathBuf) -> Self {
48
14329
        Self {
49
14329
            inner: Zeroizing::new(inner),
50
14329
            path,
51
14329
        }
52
14329
    }
53
}
54

            
55
/// SSH key algorithms.
56
//
57
// Note: this contains all the types supported by ssh_key, plus variants representing
58
// x25519 and expanded ed25519 keys.
59
14319
#[derive(Clone, Debug, PartialEq, derive_more::Display)]
60
pub(crate) enum SshKeyAlgorithm {
61
    /// Digital Signature Algorithm
62
    Dsa,
63
    /// Elliptic Curve Digital Signature Algorithm
64
    Ecdsa,
65
    /// Ed25519
66
    Ed25519,
67
    /// Expanded Ed25519
68
    Ed25519Expanded,
69
    /// X25519
70
    X25519,
71
    /// RSA
72
    Rsa,
73
    /// FIDO/U2F key with ECDSA/NIST-P256 + SHA-256
74
    SkEcdsaSha2NistP256,
75
    /// FIDO/U2F key with Ed25519
76
    SkEd25519,
77
    /// An unrecognized [`ssh_key::Algorithm`].
78
    Unknown(ssh_key::Algorithm),
79
}
80

            
81
impl From<Algorithm> for SshKeyAlgorithm {
82
26206
    fn from(algo: Algorithm) -> SshKeyAlgorithm {
83
26206
        match &algo {
84
8
            Algorithm::Dsa => SshKeyAlgorithm::Dsa,
85
            Algorithm::Ecdsa { .. } => SshKeyAlgorithm::Ecdsa,
86
2432
            Algorithm::Ed25519 => SshKeyAlgorithm::Ed25519,
87
            Algorithm::Rsa { .. } => SshKeyAlgorithm::Rsa,
88
            Algorithm::SkEcdsaSha2NistP256 => SshKeyAlgorithm::SkEcdsaSha2NistP256,
89
            Algorithm::SkEd25519 => SshKeyAlgorithm::SkEd25519,
90
23766
            Algorithm::Other(name) => match name.as_str() {
91
23766
                X25519_ALGORITHM_NAME => SshKeyAlgorithm::X25519,
92
23386
                ED25519_EXPANDED_ALGORITHM_NAME => SshKeyAlgorithm::Ed25519Expanded,
93
8
                _ => SshKeyAlgorithm::Unknown(algo),
94
            },
95
            // Note: ssh_key::Algorithm is non_exhaustive, so we need this catch-all variant
96
            _ => SshKeyAlgorithm::Unknown(algo),
97
        }
98
26206
    }
99
}
100

            
101
/// Parse an OpenSSH key, returning its underlying [`KeyData`], if it's a public key, or
102
/// [`KeypairData`], if it's a private one.
103
macro_rules! parse_openssh {
104
    (PRIVATE $key:expr, $key_type:expr) => {{
105
        parse_openssh!(
106
            $key,
107
            $key_type,
108
            ssh_key::private::PrivateKey::from_openssh,
109
            convert_ed25519_kp,
110
            convert_expanded_ed25519_kp,
111
            convert_x25519_kp,
112
            KeypairData
113
        )
114
    }};
115

            
116
    (PUBLIC $key:expr, $key_type:expr) => {{
117
        parse_openssh!(
118
            $key,
119
            $key_type,
120
            ssh_key::public::PublicKey::from_openssh,
121
            convert_ed25519_pk,
122
            convert_expanded_ed25519_pk,
123
            convert_x25519_pk,
124
            KeyData
125
        )
126
    }};
127

            
128
    ($key:expr, $key_type:expr, $parse_fn:path, $ed25519_fn:path, $expanded_ed25519_fn:path, $x25519_fn:path, $key_data_ty:tt) => {{
129
        let key = $parse_fn(&*$key.inner).map_err(|e| {
130
            ArtiNativeKeystoreError::SshKeyParse {
131
                // TODO: rust thinks this clone is necessary because key.path is also used below (but
132
                // if we get to this point, we're going to return an error and never reach the other
133
                // error handling branches where we use key.path).
134
                path: $key.path.clone(),
135
                key_type: $key_type.clone().clone(),
136
                err: e.into(),
137
            }
138
        })?;
139

            
140
        let wanted_key_algo = $key_type.ssh_algorithm()?;
141

            
142
        if SshKeyAlgorithm::from(key.algorithm()) != wanted_key_algo {
143
            return Err(ArtiNativeKeystoreError::UnexpectedSshKeyType {
144
                path: $key.path,
145
                wanted_key_algo,
146
                found_key_algo: key.algorithm().into(),
147
            }.into());
148
        }
149

            
150
        // Build the expected key type (i.e. convert ssh_key key types to the key types
151
        // we're using internally).
152
        match key.key_data() {
153
            $key_data_ty::Ed25519(key) => Ok($ed25519_fn(key).map(Box::new)?),
154
            $key_data_ty::Other(other) => {
155
                match SshKeyAlgorithm::from(key.algorithm()) {
156
                    SshKeyAlgorithm::X25519 => Ok($x25519_fn(other).map(Box::new)?),
157
                    SshKeyAlgorithm::Ed25519Expanded => Ok($expanded_ed25519_fn(other).map(Box::new)?),
158
                    _ => {
159
                        Err(ArtiNativeKeystoreError::UnexpectedSshKeyType {
160
                            path: $key.path,
161
                            wanted_key_algo,
162
                            found_key_algo: key.algorithm().into(),
163
                        }.into())
164
                    }
165
                }
166
            }
167
            _ => Err(ArtiNativeKeystoreError::UnexpectedSshKeyType {
168
                path: $key.path,
169
                wanted_key_algo,
170
                found_key_algo: key.algorithm().into(),
171
            }.into())
172
        }
173
    }};
174
}
175

            
176
/// Try to convert an [`Ed25519Keypair`](ssh_key::private::Ed25519Keypair) to an [`ed25519::Keypair`].
177
// TODO remove this allow?
178
// clippy wants this whole function to be infallible because
179
// nowadays ed25519::Keypair can be made infallibly from bytes,
180
// but is that really right?
181
#[allow(clippy::unnecessary_fallible_conversions)]
182
2430
fn convert_ed25519_kp(key: &ssh_key::private::Ed25519Keypair) -> Result<ed25519::Keypair> {
183
2430
    Ok(ed25519::Keypair::try_from(&key.private.to_bytes())
184
2430
        .map_err(|_| ArtiNativeKeystoreError::InvalidSshKeyData("bad ed25519 keypair".into()))?)
185
2430
}
186

            
187
/// Try to convert an [`OpaqueKeypair`](ssh_key::private::OpaqueKeypair) to a [`curve25519::StaticKeypair`].
188
188
fn convert_x25519_kp(key: &ssh_key::private::OpaqueKeypair) -> Result<curve25519::StaticKeypair> {
189
188
    let public: [u8; 32] = key.public.as_ref().try_into().map_err(|_| {
190
        ArtiNativeKeystoreError::InvalidSshKeyData("bad x25519 public key length".into())
191
188
    })?;
192

            
193
188
    let secret: [u8; 32] = key.private.as_ref().try_into().map_err(|_| {
194
        ArtiNativeKeystoreError::InvalidSshKeyData("bad x25519 secret key length".into())
195
188
    })?;
196

            
197
188
    Ok(curve25519::StaticKeypair {
198
188
        public: public.into(),
199
188
        secret: secret.into(),
200
188
    })
201
188
}
202

            
203
/// Try to convert an [`OpaqueKeypair`](ssh_key::private::OpaqueKeypair) to an [`ed25519::ExpandedKeypair`].
204
11689
fn convert_expanded_ed25519_kp(
205
11689
    key: &ssh_key::private::OpaqueKeypair,
206
11689
) -> Result<ed25519::ExpandedKeypair> {
207
11689
    let public = ed25519::PublicKey::try_from(key.public.as_ref()).map_err(|_| {
208
        ArtiNativeKeystoreError::InvalidSshKeyData("bad expanded ed25519 public key ".into())
209
11689
    })?;
210

            
211
11689
    let keypair = ed25519::ExpandedKeypair::from_secret_key_bytes(
212
11689
        key.private.as_ref().try_into().map_err(|_| {
213
            ArtiNativeKeystoreError::InvalidSshKeyData(
214
                "bad length on expanded ed25519 secret key ".into(),
215
            )
216
11689
        })?,
217
    )
218
11689
    .ok_or_else(|| {
219
        ArtiNativeKeystoreError::InvalidSshKeyData("bad expanded ed25519 secret key ".into())
220
11689
    })?;
221

            
222
11689
    if &public != keypair.public() {
223
        return Err(ArtiNativeKeystoreError::InvalidSshKeyData(
224
            "mismatched ed25519 keypair".into(),
225
        )
226
        .into());
227
11689
    }
228
11689

            
229
11689
    Ok(keypair)
230
11689
}
231

            
232
/// Try to convert an [`Ed25519PublicKey`](ssh_key::public::Ed25519PublicKey) to an [`ed25519::PublicKey`].
233
2
fn convert_ed25519_pk(key: &ssh_key::public::Ed25519PublicKey) -> Result<ed25519::PublicKey> {
234
2
    Ok(ed25519::PublicKey::from_bytes(key.as_ref()).map_err(|_| {
235
        ArtiNativeKeystoreError::InvalidSshKeyData("bad ed25519 public key ".into())
236
2
    })?)
237
2
}
238

            
239
/// Try to convert an [`OpaquePublicKey`](ssh_key::public::OpaquePublicKey) to an [`ed25519::PublicKey`].
240
///
241
/// This function always returns an error because the custom `ed25519-expanded@spec.torproject.org`
242
/// SSH algorithm should not be used for ed25519 public keys (only for expanded ed25519 key
243
/// _pairs_). This function is needed for the [`parse_openssh!`] macro.
244
fn convert_expanded_ed25519_pk(
245
    _key: &ssh_key::public::OpaquePublicKey,
246
) -> Result<ed25519::PublicKey> {
247
    Err(ArtiNativeKeystoreError::InvalidSshKeyData(
248
        "invalid ed25519 public key (ed25519 public keys should be stored as ssh-ed25519)".into(),
249
    )
250
    .into())
251
}
252

            
253
/// Try to convert an [`OpaquePublicKey`](ssh_key::public::OpaquePublicKey) to a [`curve25519::PublicKey`].
254
2
fn convert_x25519_pk(key: &ssh_key::public::OpaquePublicKey) -> Result<curve25519::PublicKey> {
255
2
    let public: [u8; 32] = key.as_ref().try_into().map_err(|_| {
256
        ArtiNativeKeystoreError::InvalidSshKeyData("bad x25519 public key length".into())
257
2
    })?;
258

            
259
2
    Ok(curve25519::PublicKey::from(public))
260
2
}
261

            
262
impl KeyType {
263
    /// Get the algorithm of this key type.
264
14319
    pub(crate) fn ssh_algorithm(&self) -> Result<SshKeyAlgorithm> {
265
14319
        match self {
266
2436
            KeyType::Ed25519Keypair | KeyType::Ed25519PublicKey => Ok(SshKeyAlgorithm::Ed25519),
267
194
            KeyType::X25519StaticKeypair | KeyType::X25519PublicKey => Ok(SshKeyAlgorithm::X25519),
268
11689
            KeyType::Ed25519ExpandedKeypair => Ok(SshKeyAlgorithm::Ed25519Expanded),
269
            KeyType::Unknown { arti_extension } => Err(ArtiNativeKeystoreError::UnknownKeyType(
270
                UnknownKeyTypeError {
271
                    arti_extension: arti_extension.clone(),
272
                },
273
            )
274
            .into()),
275
        }
276
14319
    }
277

            
278
    /// Parse an OpenSSH key, convert the key material into a known key type, and return the
279
    /// type-erased value.
280
    ///
281
    /// The caller is expected to downcast the value returned to a concrete type.
282
14329
    pub(crate) fn parse_ssh_format_erased(&self, key: UnparsedOpenSshKey) -> Result<ErasedKey> {
283
14329
        // TODO: perhaps this needs to be a method on EncodableKey instead?
284
14329

            
285
14329
        match &self {
286
            KeyType::Ed25519Keypair
287
            | KeyType::X25519StaticKeypair
288
            | KeyType::Ed25519ExpandedKeypair => {
289
6
                parse_openssh!(PRIVATE key, self)
290
            }
291
            KeyType::Ed25519PublicKey | KeyType::X25519PublicKey => {
292
4
                parse_openssh!(PUBLIC key, self)
293
            }
294
            KeyType::Unknown { arti_extension } => Err(ArtiNativeKeystoreError::UnknownKeyType(
295
                UnknownKeyTypeError {
296
                    arti_extension: arti_extension.clone(),
297
                },
298
            )
299
            .into()),
300
        }
301
14329
    }
302
}
303

            
304
#[cfg(test)]
305
mod tests {
306
    // @@ begin test lint list maintained by maint/add_warning @@
307
    #![allow(clippy::bool_assert_comparison)]
308
    #![allow(clippy::clone_on_copy)]
309
    #![allow(clippy::dbg_macro)]
310
    #![allow(clippy::print_stderr)]
311
    #![allow(clippy::print_stdout)]
312
    #![allow(clippy::single_char_pattern)]
313
    #![allow(clippy::unwrap_used)]
314
    #![allow(clippy::unchecked_duration_subtraction)]
315
    #![allow(clippy::useless_vec)]
316
    #![allow(clippy::needless_pass_by_value)]
317
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
318
    use super::*;
319

            
320
    const OPENSSH_ED25519: &str = include_str!("../../testdata/ed25519_openssh.private");
321
    const OPENSSH_ED25519_PUB: &str = include_str!("../../testdata/ed25519_openssh.public");
322
    const OPENSSH_ED25519_BAD: &str = include_str!("../../testdata/ed25519_openssh_bad.private");
323
    const OPENSSH_ED25519_PUB_BAD: &str = include_str!("../../testdata/ed25519_openssh_bad.public");
324
    const OPENSSH_EXP_ED25519: &str =
325
        include_str!("../../testdata/ed25519_expanded_openssh.private");
326
    const OPENSSH_EXP_ED25519_PUB: &str =
327
        include_str!("../../testdata/ed25519_expanded_openssh.public");
328
    const OPENSSH_EXP_ED25519_BAD: &str =
329
        include_str!("../../testdata/ed25519_expanded_openssh_bad.private");
330
    const OPENSSH_DSA: &str = include_str!("../../testdata/dsa_openssh.private");
331
    const OPENSSH_X25519: &str = include_str!("../../testdata/x25519_openssh.private");
332
    const OPENSSH_X25519_PUB: &str = include_str!("../../testdata/x25519_openssh.public");
333
    const OPENSSH_X25519_UNKNOWN_ALGORITHM: &str =
334
        include_str!("../../testdata/x25519_openssh_unknown_algorithm.private");
335
    const OPENSSH_X25519_PUB_UNKNOWN_ALGORITHM: &str =
336
        include_str!("../../testdata/x25519_openssh_unknown_algorithm.public");
337

            
338
    macro_rules! test_parse_ssh_format_erased {
339
        ($key_ty:tt, $key:expr, $expected_ty:path) => {{
340
            let key_type = KeyType::$key_ty;
341
            let key = UnparsedOpenSshKey::new($key.into(), PathBuf::from("/test/path"));
342
            let erased_key = key_type.parse_ssh_format_erased(key).unwrap();
343

            
344
            assert!(erased_key.downcast::<$expected_ty>().is_ok());
345
        }};
346

            
347
        ($key_ty:tt, $key:expr, err = $expect_err:expr) => {{
348
            let key_type = KeyType::$key_ty;
349
            let key = UnparsedOpenSshKey::new($key.into(), PathBuf::from("/dummy/path"));
350
            let err = key_type
351
                .parse_ssh_format_erased(key)
352
                .map(|_| "<type erased key>")
353
                .unwrap_err();
354

            
355
            assert_eq!(err.to_string(), $expect_err);
356
        }};
357
    }
358

            
359
    #[test]
360
    fn wrong_key_type() {
361
        let key_type = KeyType::Ed25519Keypair;
362
        let key = UnparsedOpenSshKey::new(OPENSSH_DSA.into(), PathBuf::from("/test/path"));
363
        let err = key_type
364
            .parse_ssh_format_erased(key)
365
            .map(|_| "<type erased key>")
366
            .unwrap_err();
367

            
368
        assert_eq!(
369
            err.to_string(),
370
            format!(
371
                "Unexpected OpenSSH key type: wanted {}, found {}",
372
                SshKeyAlgorithm::Ed25519,
373
                SshKeyAlgorithm::Dsa
374
            )
375
        );
376

            
377
        test_parse_ssh_format_erased!(
378
            Ed25519Keypair,
379
            OPENSSH_DSA,
380
            err = format!(
381
                "Unexpected OpenSSH key type: wanted {}, found {}",
382
                SshKeyAlgorithm::Ed25519,
383
                SshKeyAlgorithm::Dsa
384
            )
385
        );
386
    }
387

            
388
    #[test]
389
    fn invalid_ed25519_key() {
390
        test_parse_ssh_format_erased!(
391
            Ed25519Keypair,
392
            OPENSSH_ED25519_BAD,
393
            err = "Failed to parse OpenSSH with type Ed25519Keypair"
394
        );
395

            
396
        test_parse_ssh_format_erased!(
397
            Ed25519Keypair,
398
            OPENSSH_ED25519_PUB_BAD,
399
            err = "Failed to parse OpenSSH with type Ed25519Keypair"
400
        );
401
    }
402

            
403
    #[test]
404
    fn ed25519_key() {
405
        test_parse_ssh_format_erased!(Ed25519Keypair, OPENSSH_ED25519, ed25519::Keypair);
406
        test_parse_ssh_format_erased!(Ed25519PublicKey, OPENSSH_ED25519_PUB, ed25519::PublicKey);
407
    }
408

            
409
    #[test]
410
    fn invalid_expanded_ed25519_key() {
411
        test_parse_ssh_format_erased!(
412
            Ed25519ExpandedKeypair,
413
            OPENSSH_EXP_ED25519_BAD,
414
            err = "Failed to parse OpenSSH with type Ed25519ExpandedKeypair"
415
        );
416
    }
417

            
418
    #[test]
419
    fn expanded_ed25519_key() {
420
        test_parse_ssh_format_erased!(
421
            Ed25519ExpandedKeypair,
422
            OPENSSH_EXP_ED25519,
423
            ed25519::ExpandedKeypair
424
        );
425

            
426
        test_parse_ssh_format_erased!(
427
            Ed25519PublicKey,
428
            OPENSSH_EXP_ED25519_PUB, // using ed25519-expanded for public keys doesn't make sense
429
            err = "Failed to parse OpenSSH with type Ed25519PublicKey"
430
        );
431
    }
432

            
433
    #[test]
434
    fn x25519_key() {
435
        test_parse_ssh_format_erased!(
436
            X25519StaticKeypair,
437
            OPENSSH_X25519,
438
            curve25519::StaticKeypair
439
        );
440

            
441
        test_parse_ssh_format_erased!(X25519PublicKey, OPENSSH_X25519_PUB, curve25519::PublicKey);
442
    }
443

            
444
    #[test]
445
    fn invalid_x25519_key() {
446
        test_parse_ssh_format_erased!(
447
            X25519StaticKeypair,
448
            OPENSSH_X25519_UNKNOWN_ALGORITHM,
449
            err = "Unexpected OpenSSH key type: wanted X25519, found pangolin@torproject.org"
450
        );
451

            
452
        test_parse_ssh_format_erased!(
453
            X25519PublicKey,
454
            OPENSSH_X25519_UNKNOWN_ALGORITHM, // Note: this is a private key
455
            err = "Failed to parse OpenSSH with type X25519PublicKey"
456
        );
457

            
458
        test_parse_ssh_format_erased!(
459
            X25519PublicKey,
460
            OPENSSH_X25519_PUB_UNKNOWN_ALGORITHM,
461
            err = "Unexpected OpenSSH key type: wanted X25519, found armadillo@torproject.org"
462
        );
463
    }
464
}