Skip to main content

tor_keymgr/
test_utils.rs

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
18use std::fmt::Debug;
19
20use 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)]
28use {
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.
40pub fn check_key_specifier<S, E>(spec: &S, path: &str)
41where
42    S: KeySpecifier + Debug + PartialEq,
43    S: for<'p> TryFrom<&'p KeyPath, Error = E>,
44    E: Debug,
45{
46    let apath = ArtiPath::new(path.to_string()).unwrap();
47    assert_eq!(spec.arti_path().unwrap(), apath);
48    assert_eq!(&S::try_from(&KeyPath::Arti(apath)).unwrap(), spec, "{path}");
49}
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)]
63pub(crate) fn sshkeygen_ed25519_strings() -> std::io::Result<(String, String)> {
64    let tempdir = tempdir()?;
65    const FILENAME: &str = "tmp_id_ed25519";
66    let status = Command::new("ssh-keygen")
67        .current_dir(tempdir.path())
68        .stdout(Stdio::null())
69        .stderr(Stdio::null())
70        .args(["-q", "-P", "", "-t", "ed25519", "-f", FILENAME, "-C", ""])
71        .status()
72        .map_err(|e| match e.kind() {
73            NotFound => Error::new(NotFound, "could not find ssh-keygen"),
74            _ => e,
75        })?;
76
77    match status.code() {
78        Some(0) => {
79            let key = tempdir.path().join(FILENAME);
80            let key_pub = key.with_extension("pub");
81
82            let key = std::fs::read_to_string(key)?;
83            let key_pub = std::fs::read_to_string(key_pub)?;
84
85            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}
96
97/// OpenSSH keys used for testing.
98#[cfg(test)]
99pub(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)]
189mod 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        pub(crate) fn new(suffix: impl AsRef<str>) -> Self {
209            Self(suffix.as_ref().into())
210        }
211    }
212
213    impl KeySpecifier for TestSpecifier {
214        fn arti_path(&self) -> Result<ArtiPath, ArtiPathUnavailableError> {
215            Ok(ArtiPath::new(format!("{TEST_SPECIFIER_PATH}{}", self.0))
216                .map_err(|e| tor_error::internal!("{e}"))?)
217        }
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        fn ctor_path(&self) -> Option<CTorPath> {
238            Some(self.0.clone())
239        }
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        fn from(_: &TestDerivedKeySpecifier) -> Self {
256            Self
257        }
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)]
285mod 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)]
313pub(crate) use specifier::*;
314
315#[cfg(test)]
316pub(crate) use key::*;
317
318#[cfg(test)]
319pub(crate) use internal::assert_found;
320
321/// Private module for reexporting test helper macros macro.
322#[cfg(test)]
323mod 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}