1
//! Shared OpenSSH helpers.
2

            
3
use ssh_key::{
4
    Algorithm, LineEnding, PrivateKey, PublicKey, private::KeypairData, public::KeyData,
5
};
6
use tor_error::{internal, into_internal};
7
use tor_llcrypto::pk::{curve25519, ed25519, rsa};
8

            
9
use crate::{ErasedKey, Error, KeyType, Result};
10

            
11
/// The algorithm string for x25519 SSH keys.
12
///
13
/// See <https://spec.torproject.org/ssh-protocols.html>
14
pub(crate) const X25519_ALGORITHM_NAME: &str = "x25519@spec.torproject.org";
15

            
16
/// The algorithm string for expanded ed25519 SSH keys.
17
///
18
/// See <https://spec.torproject.org/ssh-protocols.html>
19
pub(crate) const ED25519_EXPANDED_ALGORITHM_NAME: &str = "ed25519-expanded@spec.torproject.org";
20

            
21
/// SSH key algorithms.
22
//
23
// Note: this contains all the types supported by ssh_key, plus variants representing
24
// x25519 and expanded ed25519 keys.
25
#[derive(Clone, Debug, PartialEq, derive_more::Display)]
26
#[non_exhaustive]
27
pub enum SshKeyAlgorithm {
28
    /// Digital Signature Algorithm
29
    Dsa,
30
    /// Elliptic Curve Digital Signature Algorithm
31
    Ecdsa,
32
    /// Ed25519
33
    Ed25519,
34
    /// Expanded Ed25519
35
    Ed25519Expanded,
36
    /// X25519
37
    X25519,
38
    /// RSA
39
    Rsa,
40
    /// FIDO/U2F key with ECDSA/NIST-P256 + SHA-256
41
    SkEcdsaSha2NistP256,
42
    /// FIDO/U2F key with Ed25519
43
    SkEd25519,
44
    /// An unrecognized [`ssh_key::Algorithm`].
45
    Unknown(ssh_key::Algorithm),
46
}
47

            
48
impl From<Algorithm> for SshKeyAlgorithm {
49
96583
    fn from(algo: Algorithm) -> SshKeyAlgorithm {
50
96583
        match &algo {
51
236
            Algorithm::Dsa => SshKeyAlgorithm::Dsa,
52
            Algorithm::Ecdsa { .. } => SshKeyAlgorithm::Ecdsa,
53
34633
            Algorithm::Ed25519 => SshKeyAlgorithm::Ed25519,
54
            Algorithm::Rsa { .. } => SshKeyAlgorithm::Rsa,
55
            Algorithm::SkEcdsaSha2NistP256 => SshKeyAlgorithm::SkEcdsaSha2NistP256,
56
            Algorithm::SkEd25519 => SshKeyAlgorithm::SkEd25519,
57
61714
            Algorithm::Other(name) => match name.as_str() {
58
61714
                X25519_ALGORITHM_NAME => SshKeyAlgorithm::X25519,
59
56168
                ED25519_EXPANDED_ALGORITHM_NAME => SshKeyAlgorithm::Ed25519Expanded,
60
236
                _ => SshKeyAlgorithm::Unknown(algo),
61
            },
62
            // Note: ssh_key::Algorithm is non_exhaustive, so we need this catch-all variant
63
            _ => SshKeyAlgorithm::Unknown(algo),
64
        }
65
96583
    }
66
}
67

            
68
/// Convert ssh_key KeyData or KeypairData to one of our key types.
69
macro_rules! ssh_to_internal_erased {
70
    (PRIVATE $key:expr, $algo:expr) => {{
71
        ssh_to_internal_erased!(
72
            $key,
73
            $algo,
74
            convert_ed25519_kp,
75
            convert_expanded_ed25519_kp,
76
            convert_x25519_kp,
77
            convert_rsa_kp,
78
            KeypairData
79
        )
80
    }};
81

            
82
    (PUBLIC $key:expr, $algo:expr) => {{
83
        ssh_to_internal_erased!(
84
            $key,
85
            $algo,
86
            convert_ed25519_pk,
87
            convert_expanded_ed25519_pk,
88
            convert_x25519_pk,
89
            convert_rsa_pk,
90
            KeyData
91
        )
92
    }};
93

            
94
    ($key:expr, $algo:expr, $ed25519_fn:path, $expanded_ed25519_fn:path, $x25519_fn:path, $rsa_fn:path, $key_data_ty:tt) => {{
95
        let key = $key;
96
        let algo = SshKeyAlgorithm::from($algo);
97

            
98
        // Build the expected key type (i.e. convert ssh_key key types to the key types
99
        // we're using internally).
100
        match key {
101
            $key_data_ty::Ed25519(key) => Ok($ed25519_fn(&key).map(Box::new)?),
102
            $key_data_ty::Rsa(key) => Ok($rsa_fn(&key).map(Box::new)?),
103
            $key_data_ty::Other(other) => match algo {
104
                SshKeyAlgorithm::X25519 => Ok($x25519_fn(&other).map(Box::new)?),
105
                SshKeyAlgorithm::Ed25519Expanded => Ok($expanded_ed25519_fn(&other).map(Box::new)?),
106
                _ => Err(Error::UnsupportedKeyAlgorithm(algo)),
107
            },
108
            _ => Err(Error::UnsupportedKeyAlgorithm(algo)),
109
        }
110
    }};
111
}
112

            
113
/// Try to convert an [`Ed25519Keypair`](ssh_key::private::Ed25519Keypair) to an [`ed25519::Keypair`].
114
// TODO remove this allow?
115
// clippy wants this whole function to be infallible because
116
// nowadays ed25519::Keypair can be made infallibly from bytes,
117
// but is that really right?
118
#[allow(clippy::unnecessary_fallible_conversions)]
119
5605
fn convert_ed25519_kp(key: &ssh_key::private::Ed25519Keypair) -> Result<ed25519::Keypair> {
120
5605
    Ok(ed25519::Keypair::try_from(&key.private.to_bytes())
121
5605
        .map_err(|_| internal!("bad ed25519 keypair"))?)
122
5605
}
123

            
124
/// Try to convert an [`OpaqueKeypair`](ssh_key::private::OpaqueKeypair) to a [`curve25519::StaticKeypair`].
125
1180
fn convert_x25519_kp(key: &ssh_key::private::OpaqueKeypair) -> Result<curve25519::StaticKeypair> {
126
1180
    let public: [u8; 32] = key
127
1180
        .public
128
1180
        .as_ref()
129
1180
        .try_into()
130
1180
        .map_err(|_| internal!("bad x25519 public key length"))?;
131

            
132
1180
    let secret: [u8; 32] = key
133
1180
        .private
134
1180
        .as_ref()
135
1180
        .try_into()
136
1180
        .map_err(|_| internal!("bad x25519 secret key length"))?;
137

            
138
1180
    Ok(curve25519::StaticKeypair {
139
1180
        public: public.into(),
140
1180
        secret: secret.into(),
141
1180
    })
142
1180
}
143

            
144
/// Try to convert an [`OpaqueKeypair`](ssh_key::private::OpaqueKeypair) to an [`ed25519::ExpandedKeypair`].
145
18349
fn convert_expanded_ed25519_kp(
146
18349
    key: &ssh_key::private::OpaqueKeypair,
147
18349
) -> Result<ed25519::ExpandedKeypair> {
148
18349
    let public = ed25519::PublicKey::try_from(key.public.as_ref())
149
18349
        .map_err(|_| internal!("bad expanded ed25519 public key "))?;
150

            
151
18349
    let keypair = ed25519::ExpandedKeypair::from_secret_key_bytes(
152
18349
        key.private
153
18349
            .as_ref()
154
18349
            .try_into()
155
18349
            .map_err(|_| internal!("bad length on expanded ed25519 secret key ",))?,
156
    )
157
18349
    .ok_or_else(|| internal!("bad expanded ed25519 secret key "))?;
158

            
159
18349
    if &public != keypair.public() {
160
        return Err(internal!("mismatched ed25519 keypair",).into());
161
18349
    }
162

            
163
18349
    Ok(keypair)
164
18349
}
165

            
166
/// Try to convert an [`RsaKeypair`](ssh_key::private::RsaKeypair) to a [`rsa::KeyPair`].
167
fn convert_rsa_kp(key: &ssh_key::private::RsaKeypair) -> Result<rsa::KeyPair> {
168
    // TODO #1598:
169
    //
170
    // Right now, this will always fail, because ssh-key doesn't support keys less than 2048 bits.
171
    //
172
    // However, this will be lowered in the future to allow the 1024-bit keys that we use:
173
    //
174
    // https://github.com/RustCrypto/SSH/issues/336
175
    Ok(TryInto::<::rsa::RsaPrivateKey>::try_into(key)
176
        .map_err(|_| internal!("bad RSA keypair"))?
177
        .into())
178
}
179

            
180
/// Try to convert an [`Ed25519PublicKey`](ssh_key::public::Ed25519PublicKey) to an [`ed25519::PublicKey`].
181
4720
fn convert_ed25519_pk(key: &ssh_key::public::Ed25519PublicKey) -> Result<ed25519::PublicKey> {
182
4720
    Ok(ed25519::PublicKey::from_bytes(key.as_ref())
183
4720
        .map_err(|_| internal!("bad ed25519 public key "))?)
184
4720
}
185

            
186
/// Try to convert an [`OpaquePublicKey`](ssh_key::public::OpaquePublicKey) to an [`ed25519::PublicKey`].
187
///
188
/// This function always returns an error because the custom `ed25519-expanded@spec.torproject.org`
189
/// SSH algorithm should not be used for ed25519 public keys (only for expanded ed25519 key
190
/// _pairs_). This function is needed for the [`ssh_to_internal_erased!`] macro.
191
fn convert_expanded_ed25519_pk(
192
    _key: &ssh_key::public::OpaquePublicKey,
193
) -> Result<ed25519::PublicKey> {
194
    Err(internal!(
195
        "invalid ed25519 public key (ed25519 public keys should be stored as ssh-ed25519)",
196
    )
197
    .into())
198
}
199

            
200
/// Try to convert an [`OpaquePublicKey`](ssh_key::public::OpaquePublicKey) to a [`curve25519::PublicKey`].
201
118
fn convert_x25519_pk(key: &ssh_key::public::OpaquePublicKey) -> Result<curve25519::PublicKey> {
202
118
    let public: [u8; 32] = key
203
118
        .as_ref()
204
118
        .try_into()
205
118
        .map_err(|_| internal!("bad x25519 public key length"))?;
206

            
207
118
    Ok(curve25519::PublicKey::from(public))
208
118
}
209

            
210
/// Try to convert an [`RsaKeypair`](ssh_key::public::RsaPublicKey) to a [`rsa::PublicKey`].
211
fn convert_rsa_pk(key: &ssh_key::public::RsaPublicKey) -> Result<rsa::PublicKey> {
212
    Ok(TryInto::<::rsa::RsaPublicKey>::try_into(key)
213
        .map_err(|_| internal!("bad RSA keypair"))?
214
        .into())
215
}
216

            
217
/// A public key or a keypair.
218
#[derive(Clone, Debug)]
219
#[non_exhaustive]
220
pub struct SshKeyData(SshKeyDataInner);
221

            
222
/// The inner representation of a public key or a keypair.
223
#[derive(Clone, Debug)]
224
#[non_exhaustive]
225
enum SshKeyDataInner {
226
    /// The [`KeyData`] of a public key.
227
    Public(KeyData),
228
    /// The [`KeypairData`] of a private key.
229
    Private(KeypairData),
230
}
231

            
232
impl SshKeyData {
233
    /// Try to convert a [`KeyData`] to [`SshKeyData`].
234
    ///
235
    /// Returns an error if this type of [`KeyData`] is not supported.
236
5487
    pub fn try_from_key_data(key: KeyData) -> Result<Self> {
237
5487
        let algo = SshKeyAlgorithm::from(key.algorithm());
238
5487
        let () = match key {
239
5310
            KeyData::Ed25519(_) => Ok(()),
240
            KeyData::Rsa(_) => Ok(()),
241
177
            KeyData::Other(_) => match algo {
242
177
                SshKeyAlgorithm::X25519 => Ok(()),
243
                _ => Err(Error::UnsupportedKeyAlgorithm(algo)),
244
            },
245
            _ => Err(Error::UnsupportedKeyAlgorithm(algo)),
246
        }?;
247

            
248
5487
        Ok(Self(SshKeyDataInner::Public(key)))
249
5487
    }
250

            
251
    /// Try to convert a [`KeypairData`] to [`SshKeyData`].
252
    ///
253
    /// Returns an error if this type of [`KeypairData`] is not supported.
254
30798
    pub fn try_from_keypair_data(key: KeypairData) -> Result<Self> {
255
30798
        let algo = SshKeyAlgorithm::from(
256
30798
            key.algorithm()
257
30798
                .map_err(into_internal!("encrypted keys are not yet supported"))?,
258
        );
259
30798
        let () = match key {
260
8791
            KeypairData::Ed25519(_) => Ok(()),
261
            KeypairData::Rsa(_) => Ok(()),
262
22007
            KeypairData::Other(_) => match algo {
263
2773
                SshKeyAlgorithm::X25519 => Ok(()),
264
19234
                SshKeyAlgorithm::Ed25519Expanded => Ok(()),
265
                _ => Err(Error::UnsupportedKeyAlgorithm(algo)),
266
            },
267
            _ => Err(Error::UnsupportedKeyAlgorithm(algo)),
268
        }?;
269

            
270
30798
        Ok(Self(SshKeyDataInner::Private(key)))
271
30798
    }
272

            
273
    /// Encode this key as an OpenSSH-formatted key using the specified `comment`
274
4484
    pub fn to_openssh_string(&self, comment: &str) -> Result<String> {
275
4484
        let openssh_key = match &self.0 {
276
649
            SshKeyDataInner::Public(key_data) => {
277
649
                let openssh_key = PublicKey::new(key_data.clone(), comment);
278

            
279
649
                openssh_key
280
649
                    .to_openssh()
281
649
                    .map_err(|_| tor_error::internal!("failed to encode SSH key"))?
282
            }
283
3835
            SshKeyDataInner::Private(keypair) => {
284
3835
                let openssh_key = PrivateKey::new(keypair.clone(), comment)
285
3835
                    .map_err(|_| tor_error::internal!("failed to create SSH private key"))?;
286

            
287
3835
                openssh_key
288
3835
                    .to_openssh(LineEnding::LF)
289
3835
                    .map_err(|_| tor_error::internal!("failed to encode SSH key"))?
290
3835
                    .to_string()
291
            }
292
        };
293

            
294
4484
        Ok(openssh_key)
295
4484
    }
296

            
297
    /// Convert the key material into a known key type,
298
    /// and return the type-erased value.
299
    ///
300
    /// The caller is expected to downcast the value returned to the correct concrete type.
301
29972
    pub fn into_erased(self) -> Result<ErasedKey> {
302
29972
        match self.0 {
303
25134
            SshKeyDataInner::Private(key) => {
304
25134
                let algorithm = key
305
25134
                    .algorithm()
306
25134
                    .map_err(into_internal!("unsupported key type"))?;
307
25134
                ssh_to_internal_erased!(PRIVATE key, algorithm)
308
            }
309
4838
            SshKeyDataInner::Public(key) => {
310
4838
                let algorithm = key.algorithm();
311
4838
                ssh_to_internal_erased!(PUBLIC key, algorithm)
312
            }
313
        }
314
29972
    }
315

            
316
    /// Return the [`KeyType`] of this OpenSSH key.
317
    ///
318
    /// Returns an error if the underlying key material is [`KeypairData::Encrypted`],
319
    /// or if its algorithm is unsupported.
320
6195
    pub fn key_type(&self) -> Result<KeyType> {
321
6195
        match &self.0 {
322
472
            SshKeyDataInner::Public(k) => KeyType::try_from_key_data(k),
323
5723
            SshKeyDataInner::Private(k) => KeyType::try_from_keypair_data(k),
324
        }
325
6195
    }
326
}