1
//! Test helpers.
2

            
3
// @@ begin test lint list maintained by maint/add_warning @@
4
#![allow(clippy::bool_assert_comparison)]
5
#![allow(clippy::clone_on_copy)]
6
#![allow(clippy::dbg_macro)]
7
#![allow(clippy::mixed_attributes_style)]
8
#![allow(clippy::print_stderr)]
9
#![allow(clippy::print_stdout)]
10
#![allow(clippy::single_char_pattern)]
11
#![allow(clippy::unwrap_used)]
12
#![allow(clippy::unchecked_time_subtraction)]
13
#![allow(clippy::useless_vec)]
14
#![allow(clippy::needless_pass_by_value)]
15
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
16

            
17
use std::fmt::Debug;
18

            
19
use crate::{ArtiPath, KeyPath, KeySpecifier};
20

            
21
// TODO: #[cfg(test)] / feature `testing`:
22
// https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/2873#note_3179873
23
// > A better overall approach would've been to split out the test utils that are not
24
// > pub into a different module (to avoid the confusing internal featute/test gating).
25

            
26
#[cfg(test)]
27
use {
28
    std::io::Error,
29
    std::io::ErrorKind::{Interrupted, NotFound},
30
    std::process::{Command, Stdio},
31
    tempfile::tempdir,
32
};
33

            
34
/// Check that `spec` produces the [`ArtiPath`] from `path`, and that `path` parses to `spec`
35
///
36
/// # Panics
37
///
38
/// Panics if `path` isn't valid as an `ArtiPath` or any of the checks fail.
39
34
pub fn check_key_specifier<S, E>(spec: &S, path: &str)
40
34
where
41
34
    S: KeySpecifier + Debug + PartialEq,
42
34
    S: for<'p> TryFrom<&'p KeyPath, Error = E>,
43
34
    E: Debug,
44
{
45
34
    let apath = ArtiPath::new(path.to_string()).unwrap();
46
34
    assert_eq!(spec.arti_path().unwrap(), apath);
47
34
    assert_eq!(&S::try_from(&KeyPath::Arti(apath)).unwrap(), spec, "{path}");
48
34
}
49

            
50
/// Generates a pair of encoded OpenSSH-formatted Ed25519 keys using `ssh-keygen`.
51
/// Field `.0` is the Private Key, and field `.1` is the Public Key.
52
///
53
/// # Errors
54
///
55
/// Will return an error if
56
///
57
/// * A temporary directory could be not created to generate keys in
58
/// * `ssh-keygen` was not found, it exited with a non-zero status
59
///   code, or it was terminated by a signal
60
/// * The generated keys could not be read from the temporary directory
61
#[cfg(test)]
62
10
pub(crate) fn sshkeygen_ed25519_strings() -> std::io::Result<(String, String)> {
63
10
    let tempdir = tempdir()?;
64
    const FILENAME: &str = "tmp_id_ed25519";
65
10
    let status = Command::new("ssh-keygen")
66
10
        .current_dir(tempdir.path())
67
10
        .stdout(Stdio::null())
68
10
        .stderr(Stdio::null())
69
10
        .args(["-q", "-P", "", "-t", "ed25519", "-f", FILENAME, "-C", ""])
70
10
        .status()
71
10
        .map_err(|e| match e.kind() {
72
            NotFound => Error::new(NotFound, "could not find ssh-keygen"),
73
            _ => e,
74
        })?;
75

            
76
10
    match status.code() {
77
        Some(0) => {
78
10
            let key = tempdir.path().join(FILENAME);
79
10
            let key_pub = key.with_extension("pub");
80

            
81
10
            let key = std::fs::read_to_string(key)?;
82
10
            let key_pub = std::fs::read_to_string(key_pub)?;
83

            
84
10
            Ok((key, key_pub))
85
        }
86
        Some(code) => Err(Error::other(format!(
87
            "ssh-keygen exited with status code: {code}"
88
        ))),
89
        None => Err(Error::new(
90
            Interrupted,
91
            "ssh-keygen was terminated by a signal",
92
        )),
93
    }
94
10
}
95

            
96
/// OpenSSH keys used for testing.
97
#[cfg(test)]
98
pub(crate) mod ssh_keys {
99
    /// Helper macro for defining test key constants.
100
    ///
101
    /// Defines constants for the public and private key files
102
    /// specified in the `PUB` and `PRIV` lists, respectively.
103
    ///
104
    /// The entries from the `PUB` and `PRIV` lists must specify the documentation of the constant,
105
    /// and the basename of the file to include (`include_str`) from "../testdata".
106
    /// The path of each key file is built like so:
107
    ///
108
    ///   * `PUB` keys: `../testdata/<BASENAME>.public`
109
    ///   * `PRIV` keys: `../testdata/<BASENAME>.private`
110
    ///
111
    /// The names of the constants are derived from the basename:
112
    ///   * for `PUB` entries, the name is the uppercased basename, followed by `_PUB`
113
    ///   * for `PRIV` entries, the name is the uppercased basename
114
    macro_rules! define_key_consts {
115
        (
116
            PUB => { $($(#[ $docs_and_attrs:meta ])* $basename:literal,)* },
117
            PRIV => { $($(#[ $docs_and_attrs_priv:meta ])* $basename_priv:literal,)* }
118
        ) => {
119
            $(
120
                paste::paste! {
121
                    define_key_consts!(
122
                        @ $(#[ $docs_and_attrs ])*
123
                        [< $basename:upper _PUB >], $basename, ".public"
124
                    );
125
                }
126
            )*
127

            
128
            $(
129
                paste::paste! {
130
                    define_key_consts!(
131
                        @ $(#[ $docs_and_attrs_priv ])*
132
                        [< $basename_priv:upper >], $basename_priv, ".private"
133
                    );
134
                }
135
            )*
136
        };
137

            
138
        (
139
            @ $($(#[ $docs_and_attrs:meta ])*
140
            $const_name:ident, $basename:literal, $extension:literal)*
141
        ) => {
142
            $(
143
                $(#[ $docs_and_attrs ])*
144
                pub(crate) const $const_name: &str =
145
                    include_str!(concat!("../testdata/", $basename, $extension));
146
            )*
147
        }
148
    }
149

            
150
    define_key_consts! {
151
        // Public key constants
152
        PUB => {
153
            /// An Ed25519 public key.
154
            "ed25519_openssh",
155
            /// An Ed25519 public key that fails to parse.
156
            "ed25519_openssh_bad",
157
            /// A public key using the ed25519-expanded@spec.torproject.org algorithm.
158
            ///
159
            /// Not valid because Ed25519 public keys can't be "expanded".
160
            "ed25519_expanded_openssh",
161
            /// A X25519 public key.
162
            "x25519_openssh",
163
            /// An invalid public key using the armadillo@torproject.org algorithm.
164
            "x25519_openssh_unknown_algorithm",
165
        },
166
        // Keypair constants
167
        PRIV => {
168
            /// An Ed25519 keypair.
169
            "ed25519_openssh",
170
            /// An Ed25519 keypair that fails to parse.
171
            "ed25519_openssh_bad",
172
            /// An expanded Ed25519 keypair.
173
            "ed25519_expanded_openssh",
174
            /// An expanded Ed25519 keypair that fails to parse.
175
            "ed25519_expanded_openssh_bad",
176
            /// A DSA keypair.
177
            "dsa_openssh",
178
            /// A X25519 keypair.
179
            "x25519_openssh",
180
            /// An invalid keypair using the pangolin@torproject.org algorithm.
181
            "x25519_openssh_unknown_algorithm",
182
        }
183
    }
184
}
185

            
186
/// A module exporting a key specifier used for testing.
187
#[cfg(test)]
188
mod specifier {
189
    #[cfg(feature = "experimental-api")]
190
    use crate::key_specifier::derive::derive_deftly_template_CertSpecifier;
191
    use crate::key_specifier::derive::derive_deftly_template_KeySpecifier;
192
    use crate::{ArtiPath, ArtiPathUnavailableError, CTorPath, KeySpecifier};
193

            
194
    use derive_deftly::Deftly;
195

            
196
    /// A key specifier path.
197
    pub(crate) const TEST_SPECIFIER_PATH: &str = "parent1/parent2/parent3/test-specifier";
198

            
199
    /// A [`KeySpecifier`] with a fixed [`ArtiPath`] prefix and custom suffix.
200
    ///
201
    /// The inner String is the suffix of its `ArtiPath`.
202
    #[derive(Default, PartialEq, Eq)]
203
    pub(crate) struct TestSpecifier(String);
204

            
205
    impl TestSpecifier {
206
        /// Create a new [`TestSpecifier`] with the supplied `suffix`.
207
8
        pub(crate) fn new(suffix: impl AsRef<str>) -> Self {
208
8
            Self(suffix.as_ref().into())
209
8
        }
210
    }
211

            
212
    impl KeySpecifier for TestSpecifier {
213
146
        fn arti_path(&self) -> Result<ArtiPath, ArtiPathUnavailableError> {
214
146
            Ok(ArtiPath::new(format!("{TEST_SPECIFIER_PATH}{}", self.0))
215
146
                .map_err(|e| tor_error::internal!("{e}"))?)
216
146
        }
217

            
218
        fn ctor_path(&self) -> Option<CTorPath> {
219
            None
220
        }
221

            
222
        fn keypair_specifier(&self) -> Option<Box<dyn KeySpecifier>> {
223
            None
224
        }
225
    }
226

            
227
    /// A test client key specifiier
228
    #[derive(Debug, Clone)]
229
    pub(crate) struct TestCTorSpecifier(pub(crate) CTorPath);
230

            
231
    impl KeySpecifier for TestCTorSpecifier {
232
        fn arti_path(&self) -> Result<ArtiPath, ArtiPathUnavailableError> {
233
            unimplemented!()
234
        }
235

            
236
24
        fn ctor_path(&self) -> Option<CTorPath> {
237
24
            Some(self.0.clone())
238
24
        }
239

            
240
        fn keypair_specifier(&self) -> Option<Box<dyn KeySpecifier>> {
241
            unimplemented!()
242
        }
243
    }
244

            
245
    /// A test keypair specifier.
246
    #[derive(Deftly)]
247
    #[derive_deftly(KeySpecifier)]
248
    #[deftly(prefix = "test")]
249
    #[deftly(role = "simple_keypair")]
250
    #[deftly(summary = "A test keypair specifier")]
251
    pub(crate) struct TestDerivedKeypairSpecifier;
252

            
253
    impl From<&TestDerivedKeySpecifier> for TestDerivedKeypairSpecifier {
254
66
        fn from(_: &TestDerivedKeySpecifier) -> Self {
255
66
            Self
256
66
        }
257
    }
258

            
259
    /// The public part of a `TestDerivedKeypairSpecifier`.
260
    #[derive(Deftly)]
261
    #[derive_deftly(KeySpecifier)]
262
    #[deftly(prefix = "test")]
263
    #[deftly(role = "simple_key")]
264
    #[deftly(summary = "A test key specifier")]
265
    #[deftly(keypair_specifier = "TestDerivedKeypairSpecifier")]
266
    pub(crate) struct TestDerivedKeySpecifier;
267

            
268
    /// A test certificate specifier.
269
    #[derive(Deftly)]
270
    #[derive_deftly(CertSpecifier)]
271
    #[cfg(feature = "experimental-api")]
272
    pub(crate) struct TestCertSpecifier {
273
        /// The key specifier of the subject key.
274
        #[deftly(subject)]
275
        pub(crate) subject_key_spec: TestDerivedKeySpecifier,
276
        /// A denotators for distinguishing certs of this type.
277
        #[deftly(denotator)]
278
        pub(crate) denotator: String,
279
    }
280
}
281

            
282
/// A module exporting key implementations used for testing.
283
#[cfg(test)]
284
mod key {
285
    use crate::EncodableItem;
286
    use tor_key_forge::{ItemType, KeystoreItem, KeystoreItemType};
287

            
288
    /// A dummy key.
289
    ///
290
    /// Used as an argument placeholder for calling functions that require an [`EncodableItem`].
291
    ///
292
    /// Panics if its `EncodableItem` implementation is called.
293
    pub(crate) struct DummyKey;
294

            
295
    impl ItemType for DummyKey {
296
        fn item_type() -> KeystoreItemType
297
        where
298
            Self: Sized,
299
        {
300
            todo!()
301
        }
302
    }
303

            
304
    impl EncodableItem for DummyKey {
305
        fn as_keystore_item(&self) -> tor_key_forge::Result<KeystoreItem> {
306
            todo!()
307
        }
308
    }
309
}
310

            
311
#[cfg(test)]
312
pub(crate) use specifier::*;
313

            
314
#[cfg(test)]
315
pub(crate) use key::*;
316

            
317
#[cfg(test)]
318
pub(crate) use internal::assert_found;
319

            
320
/// Private module for reexporting test helper macros macro.
321
#[cfg(test)]
322
mod internal {
323
    /// Assert that the specified key can be found (or not) in `key_store`.
324
    macro_rules! assert_found {
325
        ($key_store:expr, $key_spec:expr, $key_type:expr, $found:expr) => {{
326
            let res = $key_store
327
                .get($key_spec, &$key_type.clone().into())
328
                .unwrap();
329
            if $found {
330
                assert!(res.is_some());
331
                // Ensure contains() agrees with get()
332
                assert!(
333
                    $key_store
334
                        .contains($key_spec, &$key_type.clone().into())
335
                        .unwrap()
336
                );
337
            } else {
338
                assert!(res.is_none());
339
            }
340
        }};
341
    }
342

            
343
    pub(crate) use assert_found;
344
}