1
//! Shared OpenSSH helpers.
2

            
3
use ssh_key::{
4
    private::KeypairData, public::KeyData, Algorithm, LineEnding, PrivateKey, PublicKey,
5
};
6
use tor_error::{internal, into_internal};
7
use tor_llcrypto::pk::{curve25519, ed25519};
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
71616
    fn from(algo: Algorithm) -> SshKeyAlgorithm {
50
71616
        match &algo {
51
192
            Algorithm::Dsa => SshKeyAlgorithm::Dsa,
52
            Algorithm::Ecdsa { .. } => SshKeyAlgorithm::Ecdsa,
53
25248
            Algorithm::Ed25519 => SshKeyAlgorithm::Ed25519,
54
            Algorithm::Rsa { .. } => SshKeyAlgorithm::Rsa,
55
            Algorithm::SkEcdsaSha2NistP256 => SshKeyAlgorithm::SkEcdsaSha2NistP256,
56
            Algorithm::SkEd25519 => SshKeyAlgorithm::SkEd25519,
57
46176
            Algorithm::Other(name) => match name.as_str() {
58
46176
                X25519_ALGORITHM_NAME => SshKeyAlgorithm::X25519,
59
44208
                ED25519_EXPANDED_ALGORITHM_NAME => SshKeyAlgorithm::Ed25519Expanded,
60
192
                _ => 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
71616
    }
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
            KeypairData
78
        )
79
    }};
80

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

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

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

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

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

            
129
384
    let secret: [u8; 32] = key
130
384
        .private
131
384
        .as_ref()
132
384
        .try_into()
133
384
        .map_err(|_| internal!("bad x25519 secret key length"))?;
134

            
135
384
    Ok(curve25519::StaticKeypair {
136
384
        public: public.into(),
137
384
        secret: secret.into(),
138
384
    })
139
384
}
140

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

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

            
156
14496
    if &public != keypair.public() {
157
        return Err(internal!("mismatched ed25519 keypair",).into());
158
14496
    }
159
14496

            
160
14496
    Ok(keypair)
161
14496
}
162

            
163
/// Try to convert an [`Ed25519PublicKey`](ssh_key::public::Ed25519PublicKey) to an [`ed25519::PublicKey`].
164
3504
fn convert_ed25519_pk(key: &ssh_key::public::Ed25519PublicKey) -> Result<ed25519::PublicKey> {
165
3504
    Ok(ed25519::PublicKey::from_bytes(key.as_ref())
166
3504
        .map_err(|_| internal!("bad ed25519 public key "))?)
167
3504
}
168

            
169
/// Try to convert an [`OpaquePublicKey`](ssh_key::public::OpaquePublicKey) to an [`ed25519::PublicKey`].
170
///
171
/// This function always returns an error because the custom `ed25519-expanded@spec.torproject.org`
172
/// SSH algorithm should not be used for ed25519 public keys (only for expanded ed25519 key
173
/// _pairs_). This function is needed for the [`ssh_to_internal_erased!`] macro.
174
fn convert_expanded_ed25519_pk(
175
    _key: &ssh_key::public::OpaquePublicKey,
176
) -> Result<ed25519::PublicKey> {
177
    Err(internal!(
178
        "invalid ed25519 public key (ed25519 public keys should be stored as ssh-ed25519)",
179
    )
180
    .into())
181
}
182

            
183
/// Try to convert an [`OpaquePublicKey`](ssh_key::public::OpaquePublicKey) to a [`curve25519::PublicKey`].
184
48
fn convert_x25519_pk(key: &ssh_key::public::OpaquePublicKey) -> Result<curve25519::PublicKey> {
185
48
    let public: [u8; 32] = key
186
48
        .as_ref()
187
48
        .try_into()
188
48
        .map_err(|_| internal!("bad x25519 public key length"))?;
189

            
190
48
    Ok(curve25519::PublicKey::from(public))
191
48
}
192

            
193
/// A public key or a keypair.
194
#[derive(Clone, Debug)]
195
#[non_exhaustive]
196
pub struct SshKeyData(SshKeyDataInner);
197

            
198
/// The inner representation of a public key or a keypair.
199
#[derive(Clone, Debug)]
200
#[non_exhaustive]
201
enum SshKeyDataInner {
202
    /// The [`KeyData`] of a public key.
203
    Public(KeyData),
204
    /// The [`KeypairData`] of a private key.
205
    Private(KeypairData),
206
}
207

            
208
impl SshKeyData {
209
    /// Try to convert a [`KeyData`] to [`SshKeyData`].
210
    ///
211
    /// Returns an error if this type of [`KeyData`] is not supported.
212
3936
    pub fn try_from_key_data(key: KeyData) -> Result<Self> {
213
3936
        let algo = SshKeyAlgorithm::from(key.algorithm());
214
3936
        let () = match key {
215
3888
            KeyData::Ed25519(_) => Ok(()),
216
48
            KeyData::Other(_) => match algo {
217
48
                SshKeyAlgorithm::X25519 => Ok(()),
218
                _ => Err(Error::UnsupportedKeyAlgorithm(algo)),
219
            },
220
            _ => Err(Error::UnsupportedKeyAlgorithm(algo)),
221
        }?;
222

            
223
3936
        Ok(Self(SshKeyDataInner::Public(key)))
224
3936
    }
225

            
226
    /// Try to convert a [`KeypairData`] to [`SshKeyData`].
227
    ///
228
    /// Returns an error if this type of [`KeypairData`] is not supported.
229
22272
    pub fn try_from_keypair_data(key: KeypairData) -> Result<Self> {
230
22272
        let algo = SshKeyAlgorithm::from(
231
22272
            key.algorithm()
232
22272
                .map_err(into_internal!("encrypted keys are not yet supported"))?,
233
        );
234
22272
        let () = match key {
235
6192
            KeypairData::Ed25519(_) => Ok(()),
236
16080
            KeypairData::Other(_) => match algo {
237
1056
                SshKeyAlgorithm::X25519 => Ok(()),
238
15024
                SshKeyAlgorithm::Ed25519Expanded => Ok(()),
239
                _ => Err(Error::UnsupportedKeyAlgorithm(algo)),
240
            },
241
            _ => Err(Error::UnsupportedKeyAlgorithm(algo)),
242
        }?;
243

            
244
22272
        Ok(Self(SshKeyDataInner::Private(key)))
245
22272
    }
246

            
247
    /// Encode this key as an OpenSSH-formatted key using the specified `comment`
248
2544
    pub fn to_openssh_string(&self, comment: &str) -> Result<String> {
249
2544
        let openssh_key = match &self.0 {
250
384
            SshKeyDataInner::Public(key_data) => {
251
384
                let openssh_key = PublicKey::new(key_data.clone(), comment);
252
384

            
253
384
                openssh_key
254
384
                    .to_openssh()
255
384
                    .map_err(|_| tor_error::internal!("failed to encode SSH key"))?
256
            }
257
2160
            SshKeyDataInner::Private(keypair) => {
258
2160
                let openssh_key = PrivateKey::new(keypair.clone(), comment)
259
2160
                    .map_err(|_| tor_error::internal!("failed to create SSH private key"))?;
260

            
261
2160
                openssh_key
262
2160
                    .to_openssh(LineEnding::LF)
263
2160
                    .map_err(|_| tor_error::internal!("failed to encode SSH key"))?
264
2160
                    .to_string()
265
            }
266
        };
267

            
268
2544
        Ok(openssh_key)
269
2544
    }
270

            
271
    /// Convert the key material into a known key type,
272
    /// and return the type-erased value.
273
    ///
274
    /// The caller is expected to downcast the value returned to the correct concrete type.
275
22560
    pub fn into_erased(self) -> Result<ErasedKey> {
276
22560
        match self.0 {
277
19008
            SshKeyDataInner::Private(key) => {
278
19008
                let algorithm = key
279
19008
                    .algorithm()
280
19008
                    .map_err(into_internal!("unsupported key type"))?;
281
19008
                ssh_to_internal_erased!(PRIVATE key, algorithm)
282
            }
283
3552
            SshKeyDataInner::Public(key) => {
284
3552
                let algorithm = key.algorithm();
285
3552
                ssh_to_internal_erased!(PUBLIC key, algorithm)
286
            }
287
        }
288
22560
    }
289

            
290
    /// Return the [`KeyType`] of this OpenSSH key.
291
    ///
292
    /// Returns an error if the underlying key material is [`KeypairData::Encrypted`],
293
    /// or if its algorithm is unsupported.
294
288
    pub fn key_type(&self) -> Result<KeyType> {
295
288
        match &self.0 {
296
            SshKeyDataInner::Public(k) => KeyType::try_from_key_data(k),
297
288
            SshKeyDataInner::Private(k) => KeyType::try_from_keypair_data(k),
298
        }
299
288
    }
300
}