Skip to main content

tor_keymgr/
arti_path.rs

1//! [`ArtiPath`] and its associated helpers.
2
3use std::str::FromStr;
4
5use derive_deftly::{Deftly, define_derive_deftly};
6use derive_more::{Deref, Display, Into};
7use serde::{Deserialize, Serialize};
8use tor_persist::slug::{self, BadSlug};
9
10use crate::{ArtiPathRange, ArtiPathSyntaxError, KeySpecifierComponent};
11
12// TODO: this is only used for ArtiPaths (we should consider turning this
13// intro a regular impl ArtiPath {} and removing the macro).
14define_derive_deftly! {
15    /// Implement `new()`, `TryFrom<String>` in terms of `validate_str`, and `as_ref<str>`
16    //
17    // TODO maybe this is generally useful?  Or maybe we should find a crate?
18    ValidatedString for struct, expect items:
19
20    impl $ttype {
21        #[doc = concat!("Create a new [`", stringify!($tname), "`].")]
22        ///
23        /// This function returns an error if `inner` is not in the right syntax.
24        pub fn new(inner: String) -> Result<Self, ArtiPathSyntaxError> {
25            Self::validate_str(&inner)?;
26            Ok(Self(inner))
27        }
28    }
29
30    impl TryFrom<String> for $ttype {
31        type Error = ArtiPathSyntaxError;
32
33        fn try_from(s: String) -> Result<Self, ArtiPathSyntaxError> {
34            Self::new(s)
35        }
36    }
37
38    impl FromStr for $ttype {
39        type Err = ArtiPathSyntaxError;
40
41        fn from_str(s: &str) -> Result<Self, ArtiPathSyntaxError> {
42            Self::validate_str(s)?;
43            Ok(Self(s.to_owned()))
44        }
45    }
46
47    impl AsRef<str> for $ttype {
48        fn as_ref(&self) -> &str {
49            &self.0.as_str()
50        }
51    }
52}
53
54/// A unique identifier for a particular instance of a key.
55///
56/// In an [`ArtiNativeKeystore`](crate::ArtiNativeKeystore), this also represents the path of the
57/// key relative to the root of the keystore, minus the file extension.
58///
59/// An `ArtiPath` is a nonempty sequence of [`Slug`](tor_persist::slug::Slug)s, separated by `/`.  Path
60/// components may contain lowercase ASCII alphanumerics, and  `-` or `_`.
61/// See [slug] for the full syntactic requirements.
62/// Consequently, leading or trailing or duplicated / are forbidden.
63///
64/// The last component of the path may optionally contain the encoded (string) representation
65/// of one or more *denotator groups*.
66/// A denotator group consists
67/// of one or more
68/// [`KeySpecifierComponent`]
69/// s representing the denotators of the key.
70/// [`DENOTATOR_SEP`] denotes the beginning of the denotator groups.
71///
72/// Within a denotator group, denotators are separated
73/// by [`DENOTATOR_SEP`] characters.
74///
75/// Denotator groups are separated from each other
76/// by [`DENOTATOR_GROUP_SEP`] characters.
77///
78/// Empty denotator groups are allowed,
79/// but trailing empty denotator groups are not represented in `ArtiPath`s.
80/// Consequently, two abstract paths which differ only
81/// in trailing empty denotator groups cannot be distinguished;
82/// or to put it another way, the number of denotator groups
83/// is not recoverable from the path.
84///
85/// Denotators are encoded using their
86/// [`KeySpecifierComponent::to_slug`]
87/// implementation.
88/// The denotators **must** come after all the other fields.
89/// Denotator strings are validated in the same way as [`Slug`](tor-persist::slug::Slug)s.
90///
91/// For example, the last component of the path `"foo/bar/bax+denotator_example+1"`
92/// is the denotator group `"denotator_example+1"`.
93/// Its denotators are `"denotator_example"` and `"1"` (encoded as strings).
94/// As another example, the path `"foo/bar/bax+denotator_example+1@foo+bar@baz"`
95/// has three denotator groups, separated by `@`,
96/// `"denotator_example+1"`, `foo+bar`, and `baz`.
97///
98/// NOTE: There is a 1:1 mapping between a value that implements `KeySpecifier` and its
99/// corresponding `ArtiPath`. A `KeySpecifier` can be converted to an `ArtiPath`, but the reverse
100/// conversion is not supported.
101#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Deref, Into, Display)] //
102#[derive(Serialize, Deserialize)]
103#[serde(try_from = "String", into = "String")]
104#[derive(Deftly)]
105#[derive_deftly(ValidatedString)]
106pub struct ArtiPath(String);
107
108/// A separator for `ArtiPath`s.
109pub(crate) const PATH_SEP: char = '/';
110
111/// A separator for that marks the beginning of the keys denotators
112/// within an [`ArtiPath`].
113///
114/// This separator can only appear within the last component of an [`ArtiPath`],
115/// and the substring that follows it is assumed to be the string representation
116/// of the denotator groups of the path.
117pub const DENOTATOR_SEP: char = '+';
118
119/// A separator for separating individual denotator groups from each other.
120pub const DENOTATOR_GROUP_SEP: char = '@';
121
122impl ArtiPath {
123    /// Validate the underlying representation of an `ArtiPath`
124    fn validate_str(inner: &str) -> Result<(), ArtiPathSyntaxError> {
125        // Validate the denotators, if there are any.
126        let path = if let Some((main_part, denotator_groups)) = inner.split_once(DENOTATOR_SEP) {
127            for denotators in denotator_groups.split(DENOTATOR_GROUP_SEP) {
128                let () = validate_denotator_group(denotators)?;
129            }
130
131            main_part
132        } else {
133            inner
134        };
135
136        if let Some(e) = path
137            .split(PATH_SEP)
138            .map(|s| {
139                if s.is_empty() {
140                    Err(BadSlug::EmptySlugNotAllowed.into())
141                } else {
142                    Ok(slug::check_syntax(s)?)
143                }
144            })
145            .find(|e| e.is_err())
146        {
147            return e;
148        }
149
150        Ok(())
151    }
152
153    /// Return the substring corresponding to the specified `range`.
154    ///
155    /// Returns `None` if `range` is not within the bounds of this `ArtiPath`.
156    ///
157    /// ### Example
158    /// ```
159    /// # use tor_keymgr::{ArtiPath, ArtiPathRange, ArtiPathSyntaxError};
160    /// # fn demo() -> Result<(), ArtiPathSyntaxError> {
161    /// let path = ArtiPath::new("foo_bar_bax_1".into())?;
162    ///
163    /// let range = ArtiPathRange::from(2..5);
164    /// assert_eq!(path.substring(&range), Some("o_b"));
165    ///
166    /// let range = ArtiPathRange::from(22..50);
167    /// assert_eq!(path.substring(&range), None);
168    /// # Ok(())
169    /// # }
170    /// #
171    /// # demo().unwrap();
172    /// ```
173    pub fn substring(&self, range: &ArtiPathRange) -> Option<&str> {
174        self.0.get(range.0.clone())
175    }
176
177    /// Create an `ArtiPath` from an `ArtiPath` and a list of denotators.
178    ///
179    /// If `cert_denotators` is empty, returns the specified `path` as-is.
180    /// Otherwise, returns an `ArtiPath` that consists of the specified `path`
181    /// followed by a [`DENOTATOR_GROUP_SEP`] character and the specified denotators
182    /// (the denotators are encoded as described in the [`ArtiPath`] docs).
183    ///
184    /// Returns an error if any of the specified denotators are not valid `Slug`s.
185    //
186    /// ### Example
187    /// ```nocompile
188    /// # // `nocompile` because this function is not pub
189    /// # use tor_keymgr::{
190    /// #    ArtiPath, ArtiPathRange, ArtiPathSyntaxError, KeySpecifierComponent,
191    /// #    KeySpecifierComponentViaDisplayFromStr,
192    /// # };
193    /// # use derive_more::{Display, FromStr};
194    /// # #[derive(Display, FromStr)]
195    /// # struct Denotator(String);
196    /// # impl KeySpecifierComponentViaDisplayFromStr for Denotator {}
197    /// # fn demo() -> Result<(), ArtiPathSyntaxError> {
198    /// let path = ArtiPath::new("my_key_path".into())?;
199    /// let denotators = [
200    ///    &Denotator("foo".to_string()) as &dyn KeySpecifierComponent,
201    ///    &Denotator("bar".to_string()) as &dyn KeySpecifierComponent,
202    /// ];
203    ///
204    /// let expected_path = ArtiPath::new("my_key_path+foo+bar".into())?;
205    ///
206    /// assert_eq!(
207    ///    ArtiPath::from_path_and_denotators(path.clone(), &denotators[..])?,
208    ///    expected_path
209    /// );
210    ///
211    /// assert_eq!(
212    ///    ArtiPath::from_path_and_denotators(path.clone(), &[])?,
213    ///    path
214    /// );
215    /// # Ok(())
216    /// # }
217    /// #
218    /// # demo().unwrap();
219    /// ```
220    pub(crate) fn from_path_and_denotators(
221        path: ArtiPath,
222        cert_denotators: &[&dyn KeySpecifierComponent],
223    ) -> Result<ArtiPath, ArtiPathSyntaxError> {
224        if cert_denotators.is_empty() {
225            return Ok(path);
226        }
227
228        let cert_denotators = cert_denotators
229            .iter()
230            .map(|s| s.to_slug().map(|s| s.to_string()))
231            .collect::<Result<Vec<_>, _>>()?
232            .join(&DENOTATOR_SEP.to_string());
233
234        let path = if cert_denotators.is_empty() {
235            format!("{path}")
236        } else {
237            // If the path already contains some denotators,
238            // we need to use the denotator group separator
239            // to separate them from the certificate denotators.
240            // Otherwise, we simply use the regular DENOTATOR_SEP
241            // to indicate the start of the denotator section.
242            if path.contains(DENOTATOR_SEP) {
243                format!("{path}{DENOTATOR_GROUP_SEP}{cert_denotators}")
244            } else {
245                // If the key path has no denotators, we need to manually insert
246                // an empty denotator group before the `cert_denotators` denotator group.
247                // This ensures the origin (key vs cert specifier) of the denotators is unambiguous.
248                format!("{path}{DENOTATOR_SEP}{DENOTATOR_GROUP_SEP}{cert_denotators}")
249            }
250        };
251
252        ArtiPath::new(path)
253    }
254}
255
256/// Validate a single denotator group.
257fn validate_denotator_group(denotators: &str) -> Result<(), ArtiPathSyntaxError> {
258    // Empty denotator groups are allowed
259    if denotators.is_empty() {
260        return Ok(());
261    }
262
263    for d in denotators.split(DENOTATOR_SEP) {
264        let () = slug::check_syntax(d)?;
265    }
266
267    Ok(())
268}
269
270#[cfg(test)]
271mod tests {
272    // @@ begin test lint list maintained by maint/add_warning @@
273    #![allow(clippy::bool_assert_comparison)]
274    #![allow(clippy::clone_on_copy)]
275    #![allow(clippy::dbg_macro)]
276    #![allow(clippy::mixed_attributes_style)]
277    #![allow(clippy::print_stderr)]
278    #![allow(clippy::print_stdout)]
279    #![allow(clippy::single_char_pattern)]
280    #![allow(clippy::unwrap_used)]
281    #![allow(clippy::unchecked_time_subtraction)]
282    #![allow(clippy::useless_vec)]
283    #![allow(clippy::needless_pass_by_value)]
284    #![allow(clippy::string_slice)] // See arti#2571
285    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
286    use super::*;
287
288    use derive_more::{Display, FromStr};
289    use itertools::chain;
290
291    use crate::KeySpecifierComponentViaDisplayFromStr;
292
293    impl PartialEq for ArtiPathSyntaxError {
294        fn eq(&self, other: &Self) -> bool {
295            use ArtiPathSyntaxError::*;
296
297            match (self, other) {
298                (Slug(err1), Slug(err2)) => err1 == err2,
299                _ => false,
300            }
301        }
302    }
303
304    macro_rules! assert_ok {
305        ($ty:ident, $inner:expr) => {{
306            let path = $ty::new($inner.to_string());
307            let path_fromstr: Result<$ty, _> = $ty::try_from($inner.to_string());
308            let path_tryfrom: Result<$ty, _> = $inner.to_string().try_into();
309            assert!(path.is_ok(), "{} should be valid", $inner);
310            assert_eq!(path.as_ref().unwrap().to_string(), *$inner);
311            assert_eq!(path, path_fromstr);
312            assert_eq!(path, path_tryfrom);
313        }};
314    }
315
316    fn assert_err(path: &str, error_kind: ArtiPathSyntaxError) {
317        let path_anew = ArtiPath::new(path.to_string());
318        let path_fromstr = ArtiPath::try_from(path.to_string());
319        let path_tryfrom: Result<ArtiPath, _> = path.to_string().try_into();
320        assert!(path_anew.is_err(), "{} should be invalid", path);
321        let actual_err = path_anew.as_ref().unwrap_err();
322        assert_eq!(actual_err, &error_kind);
323        assert_eq!(path_anew, path_fromstr);
324        assert_eq!(path_anew, path_tryfrom);
325    }
326
327    #[derive(Display, FromStr)]
328    struct Denotator(String);
329
330    impl KeySpecifierComponentViaDisplayFromStr for Denotator {}
331
332    #[test]
333    fn arti_path_from_path_and_denotators() {
334        let denotators = [
335            &Denotator("foo".to_string()) as &dyn KeySpecifierComponent,
336            &Denotator("bar".to_string()) as &dyn KeySpecifierComponent,
337            &Denotator("baz".to_string()) as &dyn KeySpecifierComponent,
338        ];
339
340        /// Base ArtiPaths and the expected outcome from concatenating
341        /// the base with the denotator group above.
342        const TEST_PATHS: &[(&str, &str)] = &[
343            // A base path with no denotator groups
344            ("my_key_path", "my_key_path+@foo+bar+baz"),
345            // A base path with a single denotator groups
346            ("my_key_path+dino+saur", "my_key_path+dino+saur@foo+bar+baz"),
347            // A base path with two denotator groups
348            ("my_key_path+dino@saur", "my_key_path+dino@saur@foo+bar+baz"),
349            // A base path with two empty denotator groups
350            (
351                "my_key_path+dino@@@saur",
352                "my_key_path+dino@@@saur@foo+bar+baz",
353            ),
354        ];
355
356        for (base_path, expected_path) in TEST_PATHS {
357            let path = ArtiPath::new(base_path.to_string()).unwrap();
358            let expected_path = ArtiPath::new(expected_path.to_string()).unwrap();
359
360            assert_eq!(
361                ArtiPath::from_path_and_denotators(path.clone(), &denotators[..]).unwrap(),
362                expected_path
363            );
364
365            assert_eq!(
366                ArtiPath::from_path_and_denotators(path.clone(), &[]).unwrap(),
367                path
368            );
369        }
370    }
371
372    #[test]
373    #[allow(clippy::cognitive_complexity)]
374    fn arti_path_validation() {
375        const VALID_ARTI_PATH_COMPONENTS: &[&str] = &["my-hs-client-2", "hs_client"];
376        const VALID_ARTI_PATHS: &[&str] = &[
377            "path/to/client+subvalue+fish",
378            "_hs_client",
379            "hs_client-",
380            "hs_client_",
381            "_",
382            // A path with an empty denotator group
383            "my_key_path+dino@@saur",
384            // Paths with a trailing empty denotator group.
385            // Our implementation doesn't encode empty trailing
386            // denotator groups in ArtiPaths, but our parsing rules
387            // don't forbid them.
388            "my_key_path+dino@",
389            "my_key_path+@",
390        ];
391
392        const BAD_FIRST_CHAR_ARTI_PATHS: &[&str] = &["-hs_client", "-"];
393
394        const DISALLOWED_CHAR_ARTI_PATHS: &[(&str, char)] = &[
395            ("client?", '?'),
396            ("no spaces please", ' '),
397            ("client٣¾", '٣'),
398            ("clientß", 'ß'),
399            // Invalid paths: the main component of the path
400            // must be separated from the denotator groups by a `+` character
401            ("my_key_path@", '@'),
402            ("my_key_path@dino+saur", '@'),
403        ];
404
405        const EMPTY_PATH_COMPONENT: &[&str] =
406            &["/////", "/alice/bob", "alice//bob", "alice/bob/", "/"];
407
408        for path in chain!(VALID_ARTI_PATH_COMPONENTS, VALID_ARTI_PATHS) {
409            assert_ok!(ArtiPath, path);
410        }
411
412        for (path, bad_char) in DISALLOWED_CHAR_ARTI_PATHS {
413            assert_err(
414                path,
415                ArtiPathSyntaxError::Slug(BadSlug::BadCharacter(*bad_char)),
416            );
417        }
418
419        for path in BAD_FIRST_CHAR_ARTI_PATHS {
420            assert_err(
421                path,
422                ArtiPathSyntaxError::Slug(BadSlug::BadFirstCharacter(path.chars().next().unwrap())),
423            );
424        }
425
426        for path in EMPTY_PATH_COMPONENT {
427            assert_err(
428                path,
429                ArtiPathSyntaxError::Slug(BadSlug::EmptySlugNotAllowed),
430            );
431        }
432
433        const SEP: char = PATH_SEP;
434        // This is a valid ArtiPath, but not a valid Slug
435        let path = format!("a{SEP}client{SEP}key+private");
436        assert_ok!(ArtiPath, path);
437
438        const PATH_WITH_TRAVERSAL: &str = "alice/../bob";
439        assert_err(
440            PATH_WITH_TRAVERSAL,
441            ArtiPathSyntaxError::Slug(BadSlug::BadCharacter('.')),
442        );
443
444        const REL_PATH: &str = "./bob";
445        assert_err(
446            REL_PATH,
447            ArtiPathSyntaxError::Slug(BadSlug::BadCharacter('.')),
448        );
449
450        const EMPTY_DENOTATOR: &str = "c++";
451        assert_err(
452            EMPTY_DENOTATOR,
453            ArtiPathSyntaxError::Slug(BadSlug::EmptySlugNotAllowed),
454        );
455    }
456
457    #[test]
458    #[allow(clippy::cognitive_complexity)]
459    fn arti_path_with_denotator() {
460        const VALID_ARTI_DENOTATORS: &[&str] = &[
461            "foo",
462            "one_two_three-f0ur",
463            "1-2-3-",
464            "1-2-3_",
465            "1-2-3",
466            "_1-2-3",
467            "1-2-3",
468        ];
469
470        const BAD_OUTER_CHAR_DENOTATORS: &[&str] = &["-1-2-3"];
471
472        for denotator in VALID_ARTI_DENOTATORS {
473            let path = format!("foo/bar/qux+{denotator}");
474            assert_ok!(ArtiPath, path);
475        }
476
477        for denotator in BAD_OUTER_CHAR_DENOTATORS {
478            let path = format!("foo/bar/qux+{denotator}");
479
480            assert_err(
481                &path,
482                ArtiPathSyntaxError::Slug(BadSlug::BadFirstCharacter(
483                    denotator.chars().next().unwrap(),
484                )),
485            );
486        }
487
488        // An ArtiPath with multiple denotators
489        let path = format!(
490            "foo/bar/qux+{}+{}+foo",
491            VALID_ARTI_DENOTATORS[0], VALID_ARTI_DENOTATORS[1]
492        );
493        assert_ok!(ArtiPath, path);
494
495        // An invalid ArtiPath with multiple valid denotators and
496        // an empty (invalid) denotator
497        let path = format!(
498            "foo/bar/qux+{}+{}+foo+",
499            VALID_ARTI_DENOTATORS[0], VALID_ARTI_DENOTATORS[1]
500        );
501        assert_err(
502            &path,
503            ArtiPathSyntaxError::Slug(BadSlug::EmptySlugNotAllowed),
504        );
505    }
506
507    #[test]
508    fn substring() {
509        const KEY_PATH: &str = "hello";
510        let path = ArtiPath::new(KEY_PATH.to_string()).unwrap();
511
512        assert_eq!(path.substring(&(0..1).into()).unwrap(), "h");
513        assert_eq!(path.substring(&(2..KEY_PATH.len()).into()).unwrap(), "llo");
514        assert_eq!(
515            path.substring(&(0..KEY_PATH.len()).into()).unwrap(),
516            "hello"
517        );
518        assert_eq!(path.substring(&(0..KEY_PATH.len() + 1).into()), None);
519        assert_eq!(path.substring(&(0..0).into()).unwrap(), "");
520    }
521}