tor_key_forge/
ssh.rs

1//! Shared OpenSSH helpers.
2
3use ssh_key::{
4    Algorithm, LineEnding, PrivateKey, PublicKey, private::KeypairData, public::KeyData,
5};
6use tor_error::{internal, into_internal};
7use tor_llcrypto::pk::{curve25519, ed25519, rsa};
8
9use crate::{ErasedKey, Error, KeyType, Result};
10
11/// The algorithm string for x25519 SSH keys.
12///
13/// See <https://spec.torproject.org/ssh-protocols.html>
14pub(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>
19pub(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]
27pub 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
48impl From<Algorithm> for SshKeyAlgorithm {
49    fn from(algo: Algorithm) -> SshKeyAlgorithm {
50        match &algo {
51            Algorithm::Dsa => SshKeyAlgorithm::Dsa,
52            Algorithm::Ecdsa { .. } => SshKeyAlgorithm::Ecdsa,
53            Algorithm::Ed25519 => SshKeyAlgorithm::Ed25519,
54            Algorithm::Rsa { .. } => SshKeyAlgorithm::Rsa,
55            Algorithm::SkEcdsaSha2NistP256 => SshKeyAlgorithm::SkEcdsaSha2NistP256,
56            Algorithm::SkEd25519 => SshKeyAlgorithm::SkEd25519,
57            Algorithm::Other(name) => match name.as_str() {
58                X25519_ALGORITHM_NAME => SshKeyAlgorithm::X25519,
59                ED25519_EXPANDED_ALGORITHM_NAME => SshKeyAlgorithm::Ed25519Expanded,
60                _ => 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    }
66}
67
68/// Convert ssh_key KeyData or KeypairData to one of our key types.
69macro_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)]
119fn convert_ed25519_kp(key: &ssh_key::private::Ed25519Keypair) -> Result<ed25519::Keypair> {
120    Ok(ed25519::Keypair::try_from(&key.private.to_bytes())
121        .map_err(|_| internal!("bad ed25519 keypair"))?)
122}
123
124/// Try to convert an [`OpaqueKeypair`](ssh_key::private::OpaqueKeypair) to a [`curve25519::StaticKeypair`].
125fn convert_x25519_kp(key: &ssh_key::private::OpaqueKeypair) -> Result<curve25519::StaticKeypair> {
126    let public: [u8; 32] = key
127        .public
128        .as_ref()
129        .try_into()
130        .map_err(|_| internal!("bad x25519 public key length"))?;
131
132    let secret: [u8; 32] = key
133        .private
134        .as_ref()
135        .try_into()
136        .map_err(|_| internal!("bad x25519 secret key length"))?;
137
138    Ok(curve25519::StaticKeypair {
139        public: public.into(),
140        secret: secret.into(),
141    })
142}
143
144/// Try to convert an [`OpaqueKeypair`](ssh_key::private::OpaqueKeypair) to an [`ed25519::ExpandedKeypair`].
145fn convert_expanded_ed25519_kp(
146    key: &ssh_key::private::OpaqueKeypair,
147) -> Result<ed25519::ExpandedKeypair> {
148    let public = ed25519::PublicKey::try_from(key.public.as_ref())
149        .map_err(|_| internal!("bad expanded ed25519 public key "))?;
150
151    let keypair = ed25519::ExpandedKeypair::from_secret_key_bytes(
152        key.private
153            .as_ref()
154            .try_into()
155            .map_err(|_| internal!("bad length on expanded ed25519 secret key ",))?,
156    )
157    .ok_or_else(|| internal!("bad expanded ed25519 secret key "))?;
158
159    if &public != keypair.public() {
160        return Err(internal!("mismatched ed25519 keypair",).into());
161    }
162
163    Ok(keypair)
164}
165
166/// Try to convert an [`RsaKeypair`](ssh_key::private::RsaKeypair) to a [`rsa::KeyPair`].
167fn 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`].
181fn convert_ed25519_pk(key: &ssh_key::public::Ed25519PublicKey) -> Result<ed25519::PublicKey> {
182    Ok(ed25519::PublicKey::from_bytes(key.as_ref())
183        .map_err(|_| internal!("bad ed25519 public key "))?)
184}
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.
191fn 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`].
201fn convert_x25519_pk(key: &ssh_key::public::OpaquePublicKey) -> Result<curve25519::PublicKey> {
202    let public: [u8; 32] = key
203        .as_ref()
204        .try_into()
205        .map_err(|_| internal!("bad x25519 public key length"))?;
206
207    Ok(curve25519::PublicKey::from(public))
208}
209
210/// Try to convert an [`RsaKeypair`](ssh_key::public::RsaPublicKey) to a [`rsa::PublicKey`].
211fn 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]
220pub struct SshKeyData(SshKeyDataInner);
221
222/// The inner representation of a public key or a keypair.
223#[derive(Clone, Debug)]
224#[non_exhaustive]
225enum SshKeyDataInner {
226    /// The [`KeyData`] of a public key.
227    Public(KeyData),
228    /// The [`KeypairData`] of a private key.
229    Private(KeypairData),
230}
231
232impl SshKeyData {
233    /// Try to convert a [`KeyData`] to [`SshKeyData`].
234    ///
235    /// Returns an error if this type of [`KeyData`] is not supported.
236    pub fn try_from_key_data(key: KeyData) -> Result<Self> {
237        let algo = SshKeyAlgorithm::from(key.algorithm());
238        let () = match key {
239            KeyData::Ed25519(_) => Ok(()),
240            KeyData::Rsa(_) => Ok(()),
241            KeyData::Other(_) => match algo {
242                SshKeyAlgorithm::X25519 => Ok(()),
243                _ => Err(Error::UnsupportedKeyAlgorithm(algo)),
244            },
245            _ => Err(Error::UnsupportedKeyAlgorithm(algo)),
246        }?;
247
248        Ok(Self(SshKeyDataInner::Public(key)))
249    }
250
251    /// Try to convert a [`KeypairData`] to [`SshKeyData`].
252    ///
253    /// Returns an error if this type of [`KeypairData`] is not supported.
254    pub fn try_from_keypair_data(key: KeypairData) -> Result<Self> {
255        let algo = SshKeyAlgorithm::from(
256            key.algorithm()
257                .map_err(into_internal!("encrypted keys are not yet supported"))?,
258        );
259        let () = match key {
260            KeypairData::Ed25519(_) => Ok(()),
261            KeypairData::Rsa(_) => Ok(()),
262            KeypairData::Other(_) => match algo {
263                SshKeyAlgorithm::X25519 => Ok(()),
264                SshKeyAlgorithm::Ed25519Expanded => Ok(()),
265                _ => Err(Error::UnsupportedKeyAlgorithm(algo)),
266            },
267            _ => Err(Error::UnsupportedKeyAlgorithm(algo)),
268        }?;
269
270        Ok(Self(SshKeyDataInner::Private(key)))
271    }
272
273    /// Encode this key as an OpenSSH-formatted key using the specified `comment`
274    pub fn to_openssh_string(&self, comment: &str) -> Result<String> {
275        let openssh_key = match &self.0 {
276            SshKeyDataInner::Public(key_data) => {
277                let openssh_key = PublicKey::new(key_data.clone(), comment);
278
279                openssh_key
280                    .to_openssh()
281                    .map_err(|_| tor_error::internal!("failed to encode SSH key"))?
282            }
283            SshKeyDataInner::Private(keypair) => {
284                let openssh_key = PrivateKey::new(keypair.clone(), comment)
285                    .map_err(|_| tor_error::internal!("failed to create SSH private key"))?;
286
287                openssh_key
288                    .to_openssh(LineEnding::LF)
289                    .map_err(|_| tor_error::internal!("failed to encode SSH key"))?
290                    .to_string()
291            }
292        };
293
294        Ok(openssh_key)
295    }
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    pub fn into_erased(self) -> Result<ErasedKey> {
302        match self.0 {
303            SshKeyDataInner::Private(key) => {
304                let algorithm = key
305                    .algorithm()
306                    .map_err(into_internal!("unsupported key type"))?;
307                ssh_to_internal_erased!(PRIVATE key, algorithm)
308            }
309            SshKeyDataInner::Public(key) => {
310                let algorithm = key.algorithm();
311                ssh_to_internal_erased!(PUBLIC key, algorithm)
312            }
313        }
314    }
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    pub fn key_type(&self) -> Result<KeyType> {
321        match &self.0 {
322            SshKeyDataInner::Public(k) => KeyType::try_from_key_data(k),
323            SshKeyDataInner::Private(k) => KeyType::try_from_keypair_data(k),
324        }
325    }
326}