Skip to main content

tor_keymgr/
config.rs

1//! Configuration options for types implementing [`Keystore`](crate::Keystore)
2
3pub use tor_config::{ConfigBuildError, ConfigurationSource, Reconfigure};
4pub use tor_config_path::{CfgPath, CfgPathError};
5
6use amplify::Getters;
7use derive_deftly::Deftly;
8use serde::{Deserialize, Serialize};
9use tor_config::derive::prelude::*;
10use tor_config::{BoolOrAuto, ExplicitOrAuto, define_list_builder_helper, impl_not_auto_value};
11use tor_persist::hsnickname::HsNickname;
12
13use std::collections::BTreeMap;
14use std::path::PathBuf;
15
16use crate::KeystoreId;
17
18/// The kind of keystore to use
19#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
20#[serde(rename_all = "lowercase")]
21#[non_exhaustive]
22pub enum ArtiKeystoreKind {
23    /// Use the [`ArtiNativeKeystore`](crate::ArtiNativeKeystore).
24    Native,
25    /// Use the [`ArtiEphemeralKeystore`](crate::ArtiEphemeralKeystore).
26    #[cfg(feature = "ephemeral-keystore")]
27    Ephemeral,
28}
29impl_not_auto_value! {ArtiKeystoreKind}
30
31/// [`ArtiNativeKeystore`](crate::ArtiNativeKeystore) configuration
32#[derive(Debug, Clone, Deftly, Eq, PartialEq, Serialize, Deserialize, Getters)]
33#[derive_deftly(TorConfig)]
34#[deftly(tor_config(pre_build = "Self::validate"))]
35pub struct ArtiKeystoreConfig {
36    /// Whether keystore use is enabled.
37    #[deftly(tor_config(default))]
38    enabled: BoolOrAuto,
39
40    /// The primary keystore.
41    #[deftly(tor_config(sub_builder))]
42    primary: PrimaryKeystoreConfig,
43
44    /// Optionally configure C Tor keystores for arti to use.
45    ///
46    /// Note: The keystores listed here are read-only (keys are only
47    /// ever written to the primary keystore, configured in
48    /// `storage.keystore.primary`).
49    ///
50    /// Each C Tor keystore **must** have a unique identifier.
51    /// It is an error to configure multiple keystores with the same [`KeystoreId`].
52    #[deftly(tor_config(sub_builder))]
53    ctor: CTorKeystoreConfig,
54}
55
56/// [`ArtiNativeKeystore`](crate::ArtiNativeKeystore) configuration
57#[derive(Debug, Clone, Deftly, Eq, PartialEq, Serialize, Deserialize, Getters)]
58#[derive_deftly(TorConfig)]
59#[deftly(tor_config(pre_build = "Self::validate"))]
60pub struct CTorKeystoreConfig {
61    /// C Tor hidden service keystores.
62    //
63    // NOTE: This could become a map builder, but it would change the API.
64    #[deftly(tor_config(sub_builder))]
65    services: CTorServiceKeystoreConfigMap,
66
67    /// C Tor hidden service client keystores.
68    //
69    // NOTE: This could become a list builder, but it would change the API.
70    #[deftly(tor_config(no_magic, sub_builder))]
71    clients: CTorClientKeystoreConfigList,
72}
73
74/// Primary [`ArtiNativeKeystore`](crate::ArtiNativeKeystore) configuration
75#[derive(Debug, Clone, Deftly, Eq, PartialEq, Serialize, Deserialize)]
76#[derive_deftly(TorConfig)]
77pub struct PrimaryKeystoreConfig {
78    /// The type of keystore to use, or none at all.
79    #[deftly(tor_config(default))]
80    kind: ExplicitOrAuto<ArtiKeystoreKind>,
81}
82
83/// C Tor [`ArtiNativeKeystore`](crate::ArtiNativeKeystore) configuration
84#[derive(Debug, Clone, Deftly, Eq, PartialEq, Serialize, Deserialize, Getters)]
85#[derive_deftly(TorConfig)]
86#[deftly(tor_config(no_default_trait))]
87pub struct CTorServiceKeystoreConfig {
88    /// The identifier of this keystore.
89    ///
90    /// Each C Tor keystore **must**:
91    ///
92    ///   * have a unique identifier. It is an error to configure multiple keystores
93    ///     with the same [`KeystoreId`].
94    ///   * have a corresponding arti hidden service configured in the
95    ///     `[onion_services]` section with the same nickname
96    #[deftly(tor_config(no_default))]
97    id: KeystoreId,
98
99    /// The root directory of this keystore.
100    ///
101    /// This should be set to the `HiddenServiceDirectory` of your hidden service.
102    /// Arti will read `HiddenServiceDirectory/hostname` and `HiddenServiceDirectory/private_key`.
103    /// (Note: if your service is running in restricted discovery mode, you must also set the
104    /// `[[onion_services."<the nickname of your svc>".restricted_discovery.key_dirs]]`
105    /// to `HiddenServiceDirectory/client_keys`).
106    #[deftly(tor_config(no_default))]
107    path: PathBuf,
108
109    /// The nickname of the service this keystore is to be used with.
110    #[deftly(tor_config(no_default))]
111    nickname: HsNickname,
112}
113
114/// Alias for a `BTreeMap` of `CTorServiceKeystoreConfig`; used to make derive_builder
115/// happy.
116pub(crate) type CTorServiceKeystoreConfigMap = BTreeMap<HsNickname, CTorServiceKeystoreConfig>;
117
118/// The serialized format of an CTorServiceKeystoreConfigListBuilder:
119/// a map from nickname to `CTorServiceKeystoreConfigBuilder`
120type CTorServiceKeystoreConfigBuilderMap = BTreeMap<HsNickname, CTorServiceKeystoreConfigBuilder>;
121
122define_list_builder_helper! {
123    pub struct CTorServiceKeystoreConfigMapBuilder {
124        stores: [CTorServiceKeystoreConfigBuilder],
125    }
126    built: CTorServiceKeystoreConfigMap = build_ctor_service_list(stores)?;
127    default = vec![];
128    #[serde(try_from="CTorServiceKeystoreConfigBuilderMap", into="CTorServiceKeystoreConfigBuilderMap")]
129}
130
131impl TryFrom<CTorServiceKeystoreConfigBuilderMap> for CTorServiceKeystoreConfigMapBuilder {
132    type Error = ConfigBuildError;
133
134    fn try_from(value: CTorServiceKeystoreConfigBuilderMap) -> Result<Self, Self::Error> {
135        let mut list_builder = CTorServiceKeystoreConfigMapBuilder::default();
136        for (nickname, mut cfg) in value {
137            match &cfg.nickname {
138                Some(n) if n == &nickname => (),
139                None => (),
140                Some(other) => {
141                    return Err(ConfigBuildError::Inconsistent {
142                        fields: vec![nickname.to_string(), format!("{nickname}.{other}")],
143                        problem: "mismatched nicknames on onion service.".into(),
144                    });
145                }
146            }
147            cfg.nickname = Some(nickname);
148            list_builder.access().push(cfg);
149        }
150        Ok(list_builder)
151    }
152}
153
154impl From<CTorServiceKeystoreConfigMapBuilder> for CTorServiceKeystoreConfigBuilderMap {
155    // Note: this is *similar* to the OnionServiceProxyConfigMap implementation (it duplicates much
156    // of that logic, so perhaps at some point it's worth abstracting all of it away behind a
157    // general-purpose map builder API).
158    //
159    /// Convert our Builder representation of a set of C Tor service configs into the
160    /// format that serde will serialize.
161    ///
162    /// Note: This is a potentially lossy conversion, since the serialized format
163    /// can't represent partially-built configs without a nickname, or
164    /// a collection of configs with duplicate nicknames.
165    fn from(value: CTorServiceKeystoreConfigMapBuilder) -> CTorServiceKeystoreConfigBuilderMap {
166        let mut map = BTreeMap::new();
167        for cfg in value.stores.into_iter().flatten() {
168            let nickname = cfg.nickname.clone().unwrap_or_else(|| {
169                "Unnamed"
170                    .to_string()
171                    .try_into()
172                    .expect("'Unnamed' was not a valid nickname")
173            });
174            map.insert(nickname, cfg);
175        }
176        map
177    }
178}
179
180/// Construct a CTorServiceKeystoreConfigList from a vec of CTorServiceKeystoreConfig;
181/// enforce that nicknames are unique.
182///
183/// Returns an error if the [`KeystoreId`] of the `CTorServiceKeystoreConfig`s are not unique.
184fn build_ctor_service_list(
185    ctor_stores: Vec<CTorServiceKeystoreConfig>,
186) -> Result<CTorServiceKeystoreConfigMap, ConfigBuildError> {
187    use itertools::Itertools as _;
188
189    if !ctor_stores.iter().map(|s| &s.id).all_unique() {
190        return Err(ConfigBuildError::Inconsistent {
191            fields: ["id"].map(Into::into).into_iter().collect(),
192            problem: "the C Tor keystores do not have unique IDs".into(),
193        });
194    }
195
196    let mut map = BTreeMap::new();
197    for service in ctor_stores {
198        if let Some(previous_value) = map.insert(service.nickname.clone(), service) {
199            return Err(ConfigBuildError::Inconsistent {
200                fields: vec!["nickname".into()],
201                problem: format!(
202                    "Multiple C Tor service keystores for service with nickname {}",
203                    previous_value.nickname
204                ),
205            });
206        };
207    }
208
209    Ok(map)
210}
211
212/// C Tor [`ArtiNativeKeystore`](crate::ArtiNativeKeystore) configuration
213#[derive(Debug, Clone, Deftly, Eq, PartialEq, Serialize, Deserialize, Getters)]
214#[derive_deftly(TorConfig)]
215#[deftly(tor_config(no_default_trait))]
216pub struct CTorClientKeystoreConfig {
217    /// The identifier of this keystore.
218    ///
219    /// Each keystore **must** have a unique identifier.
220    /// It is an error to configure multiple keystores with the same [`KeystoreId`].
221    #[deftly(tor_config(no_default))]
222    id: KeystoreId,
223
224    /// The root directory of this keystore.
225    ///
226    /// This should be set to the `ClientOnionAuthDir` of your client.
227    /// If Arti is configured to run as a client (i.e. if it runs in SOCKS proxy mode),
228    /// it will read the client restricted discovery keys from this path.
229    ///
230    /// The key files are expected to have the `.auth_private` extension,
231    /// and their content **must** be of the form:
232    /// `<56-char-onion-addr-without-.onion-part>:descriptor:x25519:<x25519 private key in base32>`.
233    ///
234    /// Malformed files, and files that don't have the `.auth_private` extension, will be ignored.
235    #[deftly(tor_config(no_default))]
236    path: PathBuf,
237}
238
239/// The serialized format of a [`CTorClientKeystoreConfigListBuilder`]:
240pub type CTorClientKeystoreConfigList = Vec<CTorClientKeystoreConfig>;
241
242define_list_builder_helper! {
243    pub struct CTorClientKeystoreConfigListBuilder {
244        stores: [CTorClientKeystoreConfigBuilder],
245    }
246    built: CTorClientKeystoreConfigList = build_ctor_client_store_config(stores)?;
247    default = vec![];
248}
249
250/// Helper for building and validating a [`CTorClientKeystoreConfigList`].
251///
252/// Returns an error if the [`KeystoreId`]s of the `CTorClientKeystoreConfig`s are not unique.
253fn build_ctor_client_store_config(
254    ctor_stores: Vec<CTorClientKeystoreConfig>,
255) -> Result<CTorClientKeystoreConfigList, ConfigBuildError> {
256    use itertools::Itertools as _;
257
258    if !ctor_stores.iter().map(|s| &s.id).all_unique() {
259        return Err(ConfigBuildError::Inconsistent {
260            fields: ["id"].map(Into::into).into_iter().collect(),
261            problem: "the C Tor keystores do not have unique IDs".into(),
262        });
263    }
264
265    Ok(ctor_stores)
266}
267
268impl ArtiKeystoreConfig {
269    /// Whether the keystore is enabled.
270    pub fn is_enabled(&self) -> bool {
271        let default = cfg!(feature = "keymgr");
272
273        self.enabled.as_bool().unwrap_or(default)
274    }
275
276    /// The type of keystore to use
277    ///
278    /// Returns `None` if keystore use is disabled.
279    pub fn primary_kind(&self) -> Option<ArtiKeystoreKind> {
280        use ExplicitOrAuto as EoA;
281
282        if !self.is_enabled() {
283            return None;
284        }
285
286        let kind = match self.primary.kind {
287            EoA::Explicit(kind) => kind,
288            EoA::Auto => ArtiKeystoreKind::Native,
289        };
290
291        Some(kind)
292    }
293
294    /// The ctor keystore configs
295    pub fn ctor_svc_stores(&self) -> impl Iterator<Item = &CTorServiceKeystoreConfig> {
296        self.ctor.services.values()
297    }
298
299    /// The ctor client keystore configs
300    pub fn ctor_client_stores(&self) -> impl Iterator<Item = &CTorClientKeystoreConfig> {
301        self.ctor.clients.iter()
302    }
303}
304
305impl ArtiKeystoreConfigBuilder {
306    /// Check that the keystore configuration is valid
307    #[cfg(not(feature = "keymgr"))]
308    #[allow(clippy::unnecessary_wraps)]
309    fn validate(&self) -> Result<(), ConfigBuildError> {
310        use BoolOrAuto as BoA;
311        use ExplicitOrAuto as EoA;
312        // NOTE: This could use #[deftly(tor_config(cfg))], but that would change the behavior a little.
313
314        // Keystore support is disabled unless the `keymgr` feature is enabled.
315        if self.enabled == Some(BoA::Explicit(true)) {
316            return Err(ConfigBuildError::Inconsistent {
317                fields: ["enabled"].map(Into::into).into_iter().collect(),
318                problem: "keystore enabled=true, but keymgr feature not enabled".into(),
319            });
320        }
321
322        let () = match self.primary.kind {
323            // only enabled OR kind may be set, and when keymgr is not enabled they must be false|disabled
324            None | Some(EoA::Auto) => Ok(()),
325            _ => Err(ConfigBuildError::Inconsistent {
326                fields: ["enabled", "kind"].map(Into::into).into_iter().collect(),
327                problem: "kind!=auto, but keymgr feature not enabled".into(),
328            }),
329        }?;
330
331        Ok(())
332    }
333
334    /// Check that the keystore configuration is valid
335    #[cfg(feature = "keymgr")]
336    #[allow(clippy::unnecessary_wraps)]
337    fn validate(&self) -> Result<(), ConfigBuildError> {
338        Ok(())
339    }
340
341    /// Add a `CTorServiceKeystoreConfigBuilder` to this builder.
342    pub fn ctor_service(&mut self, builder: CTorServiceKeystoreConfigBuilder) -> &mut Self {
343        self.ctor.ctor_service(builder);
344        self
345    }
346}
347
348impl CTorKeystoreConfigBuilder {
349    /// Ensure no C Tor keystores are configured.
350    /// (C Tor keystores are only supported if the `ctor-keystore` is enabled).
351    #[cfg(not(feature = "ctor-keystore"))]
352    fn validate(&self) -> Result<(), ConfigBuildError> {
353        let no_compile_time_support = |field: &str| ConfigBuildError::NoCompileTimeSupport {
354            field: field.into(),
355            problem: format!("{field} configured but ctor-keystore feature not enabled"),
356        };
357
358        if self
359            .services
360            .stores
361            .as_ref()
362            .map(|s| !s.is_empty())
363            .unwrap_or_default()
364        {
365            return Err(no_compile_time_support("C Tor service keystores"));
366        }
367
368        if self
369            .clients
370            .stores
371            .as_ref()
372            .map(|s| !s.is_empty())
373            .unwrap_or_default()
374        {
375            return Err(no_compile_time_support("C Tor client keystores"));
376        }
377
378        Ok(())
379    }
380
381    /// Validate the configured C Tor keystores.
382    #[cfg(feature = "ctor-keystore")]
383    fn validate(&self) -> Result<(), ConfigBuildError> {
384        use itertools::Itertools as _;
385        use itertools::chain;
386
387        let Self { services, clients } = self;
388        let mut ctor_store_ids = chain![
389            services.stores.iter().flatten().map(|s| &s.id),
390            clients.stores.iter().flatten().map(|s| &s.id)
391        ];
392
393        // This is also validated by the KeyMgrBuilder (but it's a good idea to catch this sort of
394        // mistake at configuration-time regardless).
395        if !ctor_store_ids.all_unique() {
396            return Err(ConfigBuildError::Inconsistent {
397                fields: ["id"].map(Into::into).into_iter().collect(),
398                problem: "the C Tor keystores do not have unique IDs".into(),
399            });
400        }
401
402        Ok(())
403    }
404
405    /// Add a `CTorServiceKeystoreConfigBuilder` to this builder.
406    pub fn ctor_service(&mut self, builder: CTorServiceKeystoreConfigBuilder) -> &mut Self {
407        if let Some(ref mut stores) = self.services.stores {
408            stores.push(builder);
409        } else {
410            self.services.stores = Some(vec![builder]);
411        }
412
413        self
414    }
415}
416
417#[cfg(test)]
418mod test {
419    // @@ begin test lint list maintained by maint/add_warning @@
420    #![allow(clippy::bool_assert_comparison)]
421    #![allow(clippy::clone_on_copy)]
422    #![allow(clippy::dbg_macro)]
423    #![allow(clippy::mixed_attributes_style)]
424    #![allow(clippy::print_stderr)]
425    #![allow(clippy::print_stdout)]
426    #![allow(clippy::single_char_pattern)]
427    #![allow(clippy::unwrap_used)]
428    #![allow(clippy::unchecked_time_subtraction)]
429    #![allow(clippy::useless_vec)]
430    #![allow(clippy::needless_pass_by_value)]
431    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
432
433    use super::*;
434
435    use std::path::PathBuf;
436    use std::str::FromStr as _;
437    use tor_config::assert_config_error;
438
439    /// Helper for creating [`CTorServiceKeystoreConfigBuilders`].
440    fn svc_config_builder(
441        id: &str,
442        path: &str,
443        nickname: &str,
444    ) -> CTorServiceKeystoreConfigBuilder {
445        let mut b = CTorServiceKeystoreConfigBuilder::default();
446        b.id(KeystoreId::from_str(id).unwrap());
447        b.path(PathBuf::from(path));
448        b.nickname(HsNickname::from_str(nickname).unwrap());
449        b
450    }
451
452    /// Helper for creating [`CTorClientKeystoreConfigBuilders`].
453    fn client_config_builder(id: &str, path: &str) -> CTorClientKeystoreConfigBuilder {
454        let mut b = CTorClientKeystoreConfigBuilder::default();
455        b.id(KeystoreId::from_str(id).unwrap());
456        b.path(PathBuf::from(path));
457        b
458    }
459
460    #[test]
461    #[cfg(all(feature = "ctor-keystore", feature = "keymgr"))]
462    fn invalid_config() {
463        let mut builder = ArtiKeystoreConfigBuilder::default();
464        // Push two clients with the same (default) ID:
465        builder
466            .ctor()
467            .clients()
468            .access()
469            .push(client_config_builder("foo", "/var/lib/foo"));
470
471        builder
472            .ctor()
473            .clients()
474            .access()
475            .push(client_config_builder("foo", "/var/lib/bar"));
476        let err = builder.build().unwrap_err();
477
478        assert_config_error!(
479            err,
480            Inconsistent,
481            "the C Tor keystores do not have unique IDs"
482        );
483
484        let mut builder = ArtiKeystoreConfigBuilder::default();
485        // Push two services with the same ID:
486        builder
487            .ctor_service(svc_config_builder("foo", "/var/lib/foo", "pungent"))
488            .ctor_service(svc_config_builder("foo", "/var/lib/foo", "pungent"));
489        let err = builder.build().unwrap_err();
490
491        assert_config_error!(
492            err,
493            Inconsistent,
494            "the C Tor keystores do not have unique IDs"
495        );
496
497        let mut builder = ArtiKeystoreConfigBuilder::default();
498        // Push two services with different IDs but same nicknames:
499        builder
500            .ctor_service(svc_config_builder("foo", "/var/lib/foo", "pungent"))
501            .ctor_service(svc_config_builder("bar", "/var/lib/bar", "pungent"));
502        let err = builder.build().unwrap_err();
503
504        assert_config_error!(
505            err,
506            Inconsistent,
507            "Multiple C Tor service keystores for service with nickname pungent"
508        );
509    }
510
511    #[test]
512    #[cfg(all(not(feature = "ctor-keystore"), feature = "keymgr"))]
513    fn invalid_config() {
514        let mut builder = ArtiKeystoreConfigBuilder::default();
515        builder
516            .ctor()
517            .clients()
518            .access()
519            .push(client_config_builder("foo", "/var/lib/foo"));
520        let err = builder.build().unwrap_err();
521
522        assert_config_error!(
523            err,
524            NoCompileTimeSupport,
525            "C Tor client keystores configured but ctor-keystore feature not enabled"
526        );
527
528        let mut builder = ArtiKeystoreConfigBuilder::default();
529        builder.ctor_service(svc_config_builder("foo", "/var/lib/foo", "pungent"));
530        let err = builder.build().unwrap_err();
531
532        assert_config_error!(
533            err,
534            NoCompileTimeSupport,
535            "C Tor service keystores configured but ctor-keystore feature not enabled"
536        );
537    }
538
539    #[test]
540    #[cfg(not(feature = "keymgr"))]
541    fn invalid_config() {
542        let mut builder = ArtiKeystoreConfigBuilder::default();
543        builder.enabled(BoolOrAuto::Explicit(true));
544
545        let err = builder.build().unwrap_err();
546        assert_config_error!(
547            err,
548            Inconsistent,
549            "keystore enabled=true, but keymgr feature not enabled"
550        );
551    }
552
553    #[test]
554    #[cfg(feature = "ctor-keystore")]
555    fn valid_config() {
556        let mut builder = ArtiKeystoreConfigBuilder::default();
557        builder
558            .ctor()
559            .clients()
560            .access()
561            .push(client_config_builder("foo", "/var/lib/foo"));
562        builder
563            .ctor()
564            .clients()
565            .access()
566            .push(client_config_builder("bar", "/var/lib/bar"));
567
568        let res = builder.build();
569        assert!(res.is_ok(), "{:?}", res);
570    }
571
572    #[test]
573    #[cfg(all(not(feature = "ctor-keystore"), feature = "keymgr"))]
574    fn valid_config() {
575        let mut builder = ArtiKeystoreConfigBuilder::default();
576        builder
577            .enabled(BoolOrAuto::Explicit(true))
578            .primary()
579            .kind(ExplicitOrAuto::Explicit(ArtiKeystoreKind::Native));
580
581        let res = builder.build();
582        assert!(res.is_ok(), "{:?}", res);
583    }
584}