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
#![allow(clippy::string_slice)] // See arti#2571
16
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
17

            
18
use std::fmt::Debug;
19

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

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

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

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

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

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

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

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

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

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

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

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

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

            
195
    use derive_deftly::Deftly;
196

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
344
    pub(crate) use assert_found;
345
}