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    /// Validate the configured C Tor keystores.
350    ///
351    // Note: the config is validated even if the keymgr feature is not enabled
352    // (i.e. if keystore use is disabled)
353    fn validate(&self) -> Result<(), ConfigBuildError> {
354        use itertools::Itertools as _;
355        use itertools::chain;
356
357        let Self { services, clients } = self;
358        let mut ctor_store_ids = chain![
359            services.stores.iter().flatten().map(|s| &s.id),
360            clients.stores.iter().flatten().map(|s| &s.id)
361        ];
362
363        // This is also validated by the KeyMgrBuilder (but it's a good idea to catch this sort of
364        // mistake at configuration-time regardless).
365        if !ctor_store_ids.all_unique() {
366            return Err(ConfigBuildError::Inconsistent {
367                fields: ["id"].map(Into::into).into_iter().collect(),
368                problem: "the C Tor keystores do not have unique IDs".into(),
369            });
370        }
371
372        Ok(())
373    }
374
375    /// Add a `CTorServiceKeystoreConfigBuilder` to this builder.
376    pub fn ctor_service(&mut self, builder: CTorServiceKeystoreConfigBuilder) -> &mut Self {
377        if let Some(ref mut stores) = self.services.stores {
378            stores.push(builder);
379        } else {
380            self.services.stores = Some(vec![builder]);
381        }
382
383        self
384    }
385}
386
387#[cfg(test)]
388mod test {
389    // @@ begin test lint list maintained by maint/add_warning @@
390    #![allow(clippy::bool_assert_comparison)]
391    #![allow(clippy::clone_on_copy)]
392    #![allow(clippy::dbg_macro)]
393    #![allow(clippy::mixed_attributes_style)]
394    #![allow(clippy::print_stderr)]
395    #![allow(clippy::print_stdout)]
396    #![allow(clippy::single_char_pattern)]
397    #![allow(clippy::unwrap_used)]
398    #![allow(clippy::unchecked_time_subtraction)]
399    #![allow(clippy::useless_vec)]
400    #![allow(clippy::needless_pass_by_value)]
401    #![allow(clippy::string_slice)] // See arti#2571
402    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
403
404    use super::*;
405
406    use std::path::PathBuf;
407    use std::str::FromStr as _;
408    use tor_config::assert_config_error;
409
410    /// Helper for creating [`CTorServiceKeystoreConfigBuilders`].
411    fn svc_config_builder(
412        id: &str,
413        path: &str,
414        nickname: &str,
415    ) -> CTorServiceKeystoreConfigBuilder {
416        let mut b = CTorServiceKeystoreConfigBuilder::default();
417        b.id(KeystoreId::from_str(id).unwrap());
418        b.path(PathBuf::from(path));
419        b.nickname(HsNickname::from_str(nickname).unwrap());
420        b
421    }
422
423    /// Helper for creating [`CTorClientKeystoreConfigBuilders`].
424    fn client_config_builder(id: &str, path: &str) -> CTorClientKeystoreConfigBuilder {
425        let mut b = CTorClientKeystoreConfigBuilder::default();
426        b.id(KeystoreId::from_str(id).unwrap());
427        b.path(PathBuf::from(path));
428        b
429    }
430
431    #[test]
432    fn invalid_config() {
433        let mut builder = ArtiKeystoreConfigBuilder::default();
434        // Push two clients with the same (default) ID:
435        builder
436            .ctor()
437            .clients()
438            .access()
439            .push(client_config_builder("foo", "/var/lib/foo"));
440
441        builder
442            .ctor()
443            .clients()
444            .access()
445            .push(client_config_builder("foo", "/var/lib/bar"));
446        let err = builder.build().unwrap_err();
447
448        assert_config_error!(
449            err,
450            Inconsistent,
451            "the C Tor keystores do not have unique IDs"
452        );
453
454        let mut builder = ArtiKeystoreConfigBuilder::default();
455        // Push two services with the same ID:
456        builder
457            .ctor_service(svc_config_builder("foo", "/var/lib/foo", "pungent"))
458            .ctor_service(svc_config_builder("foo", "/var/lib/foo", "pungent"));
459        let err = builder.build().unwrap_err();
460
461        assert_config_error!(
462            err,
463            Inconsistent,
464            "the C Tor keystores do not have unique IDs"
465        );
466
467        let mut builder = ArtiKeystoreConfigBuilder::default();
468        // Push two services with different IDs but same nicknames:
469        builder
470            .ctor_service(svc_config_builder("foo", "/var/lib/foo", "pungent"))
471            .ctor_service(svc_config_builder("bar", "/var/lib/bar", "pungent"));
472        let err = builder.build().unwrap_err();
473
474        assert_config_error!(
475            err,
476            Inconsistent,
477            "Multiple C Tor service keystores for service with nickname pungent"
478        );
479    }
480
481    #[test]
482    #[cfg(not(feature = "keymgr"))]
483    fn keystore_use_requires_keymgr_feat() {
484        let mut builder = ArtiKeystoreConfigBuilder::default();
485        builder.enabled(BoolOrAuto::Explicit(true));
486
487        let err = builder.build().unwrap_err();
488        assert_config_error!(
489            err,
490            Inconsistent,
491            "keystore enabled=true, but keymgr feature not enabled"
492        );
493    }
494
495    #[test]
496    #[cfg(feature = "keymgr")]
497    fn valid_config() {
498        let mut builder = ArtiKeystoreConfigBuilder::default();
499        builder
500            .enabled(BoolOrAuto::Explicit(true))
501            .primary()
502            .kind(ExplicitOrAuto::Explicit(ArtiKeystoreKind::Native));
503
504        builder
505            .ctor()
506            .clients()
507            .access()
508            .push(client_config_builder("foo", "/var/lib/foo"));
509        builder
510            .ctor()
511            .clients()
512            .access()
513            .push(client_config_builder("bar", "/var/lib/bar"));
514
515        let res = builder.build();
516        assert!(res.is_ok(), "{:?}", res);
517    }
518}