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_duration_subtraction)]
13#![allow(clippy::useless_vec)]
14#![allow(clippy::needless_pass_by_value)]
15//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
16
17use std::fmt::Debug;
18
19use 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)]
27use {
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.
39pub fn check_key_specifier<S, E>(spec: &S, path: &str)
40where
41    S: KeySpecifier + Debug + PartialEq,
42    S: for<'p> TryFrom<&'p KeyPath, Error = E>,
43    E: Debug,
44{
45    let apath = ArtiPath::new(path.to_string()).unwrap();
46    assert_eq!(spec.arti_path().unwrap(), apath);
47    assert_eq!(&S::try_from(&KeyPath::Arti(apath)).unwrap(), spec, "{path}");
48}
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)]
62pub(crate) fn sshkeygen_ed25519_strings() -> std::io::Result<(String, String)> {
63    let tempdir = tempdir()?;
64    const FILENAME: &str = "tmp_id_ed25519";
65    let status = Command::new("ssh-keygen")
66        .current_dir(tempdir.path())
67        .stdout(Stdio::null())
68        .stderr(Stdio::null())
69        .args(["-q", "-P", "", "-t", "ed25519", "-f", FILENAME, "-C", ""])
70        .status()
71        .map_err(|e| match e.kind() {
72            NotFound => Error::new(NotFound, "could not find ssh-keygen"),
73            _ => e,
74        })?;
75
76    match status.code() {
77        Some(0) => {
78            let key = tempdir.path().join(FILENAME);
79            let key_pub = key.with_extension("pub");
80
81            let key = std::fs::read_to_string(key)?;
82            let key_pub = std::fs::read_to_string(key_pub)?;
83
84            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}
95
96/// OpenSSH keys used for testing.
97#[cfg(test)]
98pub(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)]
188mod specifier {
189    use crate::{
190        ArtiPath, ArtiPathUnavailableError, CTorPath, KeyCertificateSpecifier, KeySpecifier,
191        KeySpecifierComponent,
192    };
193
194    /// A key specifier path.
195    pub(crate) const TEST_SPECIFIER_PATH: &str = "parent1/parent2/parent3/test-specifier";
196
197    /// A [`KeySpecifier`] with a fixed [`ArtiPath`] prefix and custom suffix.
198    ///
199    /// The inner String is the suffix of its `ArtiPath`.
200    #[derive(Default, PartialEq, Eq)]
201    pub(crate) struct TestSpecifier(String);
202
203    impl TestSpecifier {
204        /// Create a new [`TestSpecifier`] with the supplied `suffix`.
205        pub(crate) fn new(suffix: impl AsRef<str>) -> Self {
206            Self(suffix.as_ref().into())
207        }
208    }
209
210    impl KeySpecifier for TestSpecifier {
211        fn arti_path(&self) -> Result<ArtiPath, ArtiPathUnavailableError> {
212            Ok(ArtiPath::new(format!("{TEST_SPECIFIER_PATH}{}", self.0))
213                .map_err(|e| tor_error::internal!("{e}"))?)
214        }
215
216        fn ctor_path(&self) -> Option<CTorPath> {
217            None
218        }
219
220        fn keypair_specifier(&self) -> Option<Box<dyn KeySpecifier>> {
221            None
222        }
223    }
224
225    /// A test client key specifiier
226    #[derive(Debug, Clone)]
227    pub(crate) struct TestCTorSpecifier(pub(crate) CTorPath);
228
229    impl KeySpecifier for TestCTorSpecifier {
230        fn arti_path(&self) -> Result<ArtiPath, ArtiPathUnavailableError> {
231            unimplemented!()
232        }
233
234        fn ctor_path(&self) -> Option<CTorPath> {
235            Some(self.0.clone())
236        }
237
238        fn keypair_specifier(&self) -> Option<Box<dyn KeySpecifier>> {
239            unimplemented!()
240        }
241    }
242
243    /// A test certificate specifier.
244    pub(crate) struct TestCertSpecifier<SUBJ: KeySpecifier, SIGN: KeySpecifier> {
245        /// The key specifier of the subject key.
246        pub(crate) subject_key_spec: SUBJ,
247        /// The key specifier of the signing key.
248        pub(crate) signing_key_spec: SIGN,
249        /// A list of denotators for distinguishing certs of this type.
250        pub(crate) denotator: Vec<String>,
251    }
252
253    impl<SUBJ: KeySpecifier, SIGN: KeySpecifier> KeyCertificateSpecifier
254        for TestCertSpecifier<SUBJ, SIGN>
255    {
256        fn cert_denotators(&self) -> Vec<&dyn KeySpecifierComponent> {
257            self.denotator
258                .iter()
259                .map(|s| s as &dyn KeySpecifierComponent)
260                .collect()
261        }
262
263        fn signing_key_specifier(&self) -> Option<&dyn KeySpecifier> {
264            Some(&self.signing_key_spec)
265        }
266
267        /// The key specifier of the subject key.
268        fn subject_key_specifier(&self) -> &dyn KeySpecifier {
269            &self.subject_key_spec
270        }
271    }
272}
273
274/// A module exporting key implementations used for testing.
275#[cfg(test)]
276mod key {
277    use crate::EncodableItem;
278    use tor_key_forge::{ItemType, KeystoreItem, KeystoreItemType};
279
280    /// A dummy key.
281    ///
282    /// Used as an argument placeholder for calling functions that require an [`EncodableItem`].
283    ///
284    /// Panics if its `EncodableItem` implementation is called.
285    pub(crate) struct DummyKey;
286
287    impl ItemType for DummyKey {
288        fn item_type() -> KeystoreItemType
289        where
290            Self: Sized,
291        {
292            todo!()
293        }
294    }
295
296    impl EncodableItem for DummyKey {
297        fn as_keystore_item(&self) -> tor_key_forge::Result<KeystoreItem> {
298            todo!()
299        }
300    }
301}
302
303#[cfg(test)]
304pub(crate) use specifier::*;
305
306#[cfg(test)]
307pub(crate) use key::*;
308
309#[cfg(test)]
310pub(crate) use internal::assert_found;
311
312/// Private module for reexporting test helper macros macro.
313#[cfg(test)]
314mod internal {
315    /// Assert that the specified key can be found (or not) in `key_store`.
316    macro_rules! assert_found {
317        ($key_store:expr, $key_spec:expr, $key_type:expr, $found:expr) => {{
318            let res = $key_store
319                .get($key_spec, &$key_type.clone().into())
320                .unwrap();
321            if $found {
322                assert!(res.is_some());
323                // Ensure contains() agrees with get()
324                assert!($key_store
325                    .contains($key_spec, &$key_type.clone().into())
326                    .unwrap());
327            } else {
328                assert!(res.is_none());
329            }
330        }};
331    }
332
333    pub(crate) use assert_found;
334}