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