Skip to main content

tor_hsservice/config/
restricted_discovery.rs

1//! Configuration for hidden service restricted discovery mode.
2//!
3//! By default, hidden services are accessible by anyone that knows their `.onion` address,
4//! this exposure making them vulnerable to DoS attacks.
5//! For added DoS resistance, services can hide their discovery information
6//! (the list of introduction points, recognized handshake types, etc.)
7//! from unauthorized clients by enabling restricted discovery mode.
8//!
9//! Services running in this mode are only discoverable
10//! by the clients configured in the [`RestrictedDiscoveryConfig`].
11//! Everyone else will be unable to reach the service,
12//! as the discovery information from the service's descriptor
13//! is encrypted with the keys of the authorized clients.
14//!
15//! Each authorized client must generate a service discovery keypair ([KS_hsc_desc_enc])
16//! and share the public part of the keypair with the service.
17//! The service can then authorize the client by adding its public key
18//! to the `static_keys` list, or as an entry in one of the `key_dirs` specified
19//! in its [`RestrictedDiscoveryConfig`].
20//!
21//! Restricted discovery mode is only suitable for services that have a known set
22//! of no more than [`MAX_RESTRICTED_DISCOVERY_CLIENTS`] users.
23//! Hidden services that do not have a fixed, well-defined set of users,
24//! or that have more than [`MAX_RESTRICTED_DISCOVERY_CLIENTS`] users,
25//! should use other DoS resistance measures instead.
26//!
27//! # Live reloading
28//!
29//! The restricted discovery configuration is automatically reloaded
30//! if `watch_configuration` is `true`.
31//!
32//! This means that any changes to `static_keys` or to the `.auth` files
33//! from the configured `key_dirs` will be automatically detected,
34//! so you don't need to restart your service in order for them to take effect.
35//!
36//! ## Best practices
37//!
38//! Each change you make to the authorized clients can result in a new descriptor
39//! being published. If you make multiple changes to your restricted discovery configuration
40//! (or to the other parts of the onion service configuration
41//! that trigger the publishing of a new descriptor, such as `anonymity`),
42//! those changes may not take effect immediately due to the descriptor publishing rate limiting.
43//!
44//! To avoid generating unnecessary traffic, you should try to batch
45//! your changes as much as possible, or, alternatively, disable `watch_configuration`
46//! until you are satisfied with your configured authorized clients.
47//!
48//! ## Caveats
49//!
50//! ### Unauthorizing previously authorized clients
51//!
52//! Removing a previously authorized client from `static_keys` or `key_dirs`
53//! does **not** guarantee its access will be revoked.
54//! This is because the client might still be able to reach your service via
55//! its current introduction points (the introduction points are not rotated
56//! when the authorized clients change). Moreover, even if the introduction points
57//! are rotated by chance, your changes are not guaranteed to take effect immediately,
58//! so it is possible for the service to publish its new introduction points
59//! in a descriptor that is readable by the recently unauthorized client.
60//!
61//! **Restricted discovery mode is a DoS resistance mechanism,
62//! _not_ a substitute for conventional access control.**
63//!
64//! ### Moving `key_dir`s
65//!
66//! If you move a `key_dir` (i.e. rename it), all of the authorized clients contained
67//! within it are removed (the descriptor is rebuilt and republished,
68//! without being encrypted for those clients).
69//! Any further changes to the moved directory will be ignored,
70//! unless the directory is moved back or a `key_dir` entry for its new location is added.
71//!
72//! Moving the directory back to its original location (configured in `key_dirs`),
73//! will cause those clients to be added back and a new descriptor to be generated.
74//!
75//! # Key providers
76//!
77//! The [`RestrictedDiscoveryConfig`] supports two key providers:
78//!   * [`StaticKeyProvider`], where keys are specified as a static mapping from nicknames to keys
79//!   * [`DirectoryKeyProvider`], which represents a directory of client keys.
80//!
81//! # Limitations
82//!
83//! Hidden service descriptors are not allowed to exceed
84//! the maximum size specified in the [`HSV3MaxDescriptorSize`] consensus parameter,
85//! so there is an implicit upper limit for the number of clients you can authorize
86//! (the `encrypted` section of the descriptor is encrypted
87//! for each authorized client, so the more clients there are, the larger the descriptor will be).
88//! While we recommend configuring no more than [`MAX_RESTRICTED_DISCOVERY_CLIENTS`] clients,
89//! the *actual* limit for your service depends on the rest of its configuration
90//! (such as the number of introduction points).
91//!
92//! [KS_hsc_desc_enc]: https://spec.torproject.org/rend-spec/protocol-overview.html#CLIENT-AUTH
93//! [`HSV3MaxDescriptorSize`]: https://spec.torproject.org/param-spec.html?highlight=maximum%20descriptor#onion-service
94
95mod key_provider;
96
97pub use key_provider::{
98    DirectoryKeyProvider, DirectoryKeyProviderBuilder, DirectoryKeyProviderList,
99    DirectoryKeyProviderListBuilder, StaticKeyProvider, StaticKeyProviderBuilder,
100};
101
102use crate::internal_prelude::*;
103use derive_more::From;
104
105use std::collections::BTreeMap;
106use std::collections::btree_map::Entry;
107
108use amplify::Getters;
109use derive_more::{Display, Into};
110
111use tor_config::derive::prelude::*;
112use tor_config_path::CfgPathResolver;
113use tor_error::warn_report;
114use tor_persist::slug::BadSlug;
115
116/// The recommended maximum number of restricted mode clients.
117///
118/// See the [module-level documentation](self) for an explanation of this limitation.
119///
120/// Note: this is an approximate, one-size-fits-all figure.
121/// In practice, the maximum number of clients depends on the rest of the service's configuration,
122/// and may in fact be higher, or lower, than this value.
123//
124// TODO: we should come up with a more accurate upper limit. The actual limit might be even lower,
125// depending on the service's configuration (i.e. number of intro points).
126//
127// This figure is an approximation. It was obtained by filling a descriptor
128// with as much information as possible. The descriptor was built with
129//   * `single-onion-service` set
130//   * 20 intro points (which is the current upper limit), where each intro point had a
131//   single link specifier (I'm not sure if there's a limit for the number of link specifiers)
132//   * 165 authorized clients
133// and had a size of 42157 bytes. Adding one more client tipped it over the limit (with 166
134// authorized clients, the size of the descriptor was 56027 bytes).
135//
136// See also tor#29134
137pub const MAX_RESTRICTED_DISCOVERY_CLIENTS: usize = 160;
138
139/// Nickname (local identifier) for a hidden service client.
140///
141/// An `HsClientNickname` must be a valid [`Slug`].
142/// See [slug](tor_persist::slug) for the syntactic requirements.
143//
144// TODO: when we implement the arti hsc CLI for managing the configured client keys,
145// we will use the nicknames to identify individual clients.
146#[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] //
147#[derive(Display, From, Into, Serialize, Deserialize)]
148#[serde(transparent)]
149pub struct HsClientNickname(Slug);
150
151/// A list of client discovery keys.
152pub(crate) type RestrictedDiscoveryKeys = BTreeMap<HsClientNickname, HsClientDescEncKey>;
153
154impl FromStr for HsClientNickname {
155    type Err = BadSlug;
156
157    fn from_str(s: &str) -> Result<Self, Self::Err> {
158        let slug = Slug::try_from(s.to_string())?;
159
160        Ok(Self(slug))
161    }
162}
163
164/// Configuration for enabling restricted discovery mode.
165///
166/// # Client nickname uniqueness
167///
168/// The client nicknames specified in `key_dirs` and `static_keys`
169/// **must** be unique. Any nickname occurring in `static_keys` must not
170/// already have an entry in any of the configured `key_dirs`,
171/// and any one nickname must not occur in more than one of the `key_dirs`.
172///
173/// Violating this rule will cause the additional keys to be ignored.
174/// If there are multiple entries for the same nickname,
175/// the entry with the highest precedence will be used, and all the others will be ignored.
176/// The precedence rules are as follows:
177///   * the `static_keys` take precedence over the keys from `key_dirs`
178///   * the ordering of the directories in `key_dirs` represents the order of precedence
179///
180/// # Reloading the configuration
181///
182/// Currently, the `static_keys` and `key_dirs` directories will *not* be monitored for updates,
183/// even when automatic config reload is enabled. We hope to change that in the future.
184/// In the meantime, you will need to restart your service every time you update
185/// its restricted discovery settings in order for the changes to be applied.
186///
187/// See the [module-level documentation](self) for more details.
188#[derive(Debug, Clone, Deftly, Eq, PartialEq, Getters)]
189#[derive_deftly(TorConfig)]
190#[deftly(tor_config(post_build = "Self::post_build_validate"))]
191pub struct RestrictedDiscoveryConfig {
192    /// Whether to enable restricted discovery mode.
193    ///
194    /// Services running in restricted discovery mode are only discoverable
195    /// by the configured clients.
196    ///
197    /// Can only be enabled if the `restricted-discovery` feature is enabled.
198    ///
199    /// If you enable this, you must also specify the authorized clients (via `static_keys`),
200    /// or the directories where the authorized client keys should be read from (via `key_dirs`).
201    ///
202    /// Restricted discovery mode is disabled by default.
203    #[deftly(tor_config(default))]
204    pub(crate) enabled: bool,
205
206    /// If true, the provided `key_dirs` will be watched for changes.
207    #[deftly(tor_config(default, serde = "skip"))]
208    #[getter(as_mut, as_copy)]
209    watch_configuration: bool,
210
211    /// Directories containing the client keys, each in the
212    /// `descriptor:x25519:<base32-encoded-x25519-public-key>` format.
213    ///
214    /// Each file in this directory must have a file name of the form `<nickname>.auth`,
215    /// where `<nickname>` is a valid [`HsClientNickname`].
216    //
217    // TODO: Use the list-builder pattern instead. (Right now this uses the sub_builder pattern
218    // directly, since before migration to TorConfig, this builder didn't declare list accessors.)
219    #[deftly(tor_config(sub_builder, no_magic))]
220    key_dirs: DirectoryKeyProviderList,
221
222    /// A static mapping from client nicknames to keys.
223    ///
224    /// Each client key must be in the `descriptor:x25519:<base32-encoded-x25519-public-key>`
225    /// format.
226    //
227    // TODO: We could eventually migrate to use the map-builder pattern, but that would
228    // be a breaking change.
229    #[deftly(tor_config(sub_builder))]
230    static_keys: StaticKeyProvider,
231}
232
233impl RestrictedDiscoveryConfig {
234    /// Read the client keys from all the configured key providers.
235    ///
236    /// Returns `None` if restricted mode is disabled.
237    ///
238    // TODO: this is not currently implemented (reconfigure() doesn't call read_keys)
239    /// When reconfiguring a [`RunningOnionService`](crate::RunningOnionService),
240    /// call this function to obtain an up-to-date view of the authorized clients.
241    ///
242    // TODO: this is a footgun. We might want to rethink this before we make
243    // the restricted-discovery feature non-experimental:
244    /// Note: if there are multiple entries for the same [`HsClientNickname`],
245    /// only one of them will be used (the others are ignored).
246    /// The deduplication logic is as follows:
247    ///   * the `static_keys` take precedence over the keys from `key_dirs`
248    ///   * the ordering of the directories in `key_dirs` represents the order of precedence
249    pub(crate) fn read_keys(
250        &self,
251        path_resolver: &CfgPathResolver,
252    ) -> Option<RestrictedDiscoveryKeys> {
253        if !self.enabled {
254            return None;
255        }
256
257        let mut authorized_clients = BTreeMap::new();
258
259        // The static_keys are inserted first, so they have precedence over
260        // the keys from key_dirs.
261        extend_key_map(
262            &mut authorized_clients,
263            RestrictedDiscoveryKeys::from(self.static_keys.clone()),
264        );
265
266        // The key_dirs are read in order of appearance,
267        // which is also the order of precedence.
268        for dir in &self.key_dirs {
269            match dir.read_keys(path_resolver) {
270                Ok(keys) => extend_key_map(&mut authorized_clients, keys),
271                Err(e) => {
272                    warn_report!(e, "Failed to read keys at {}", dir.path());
273                }
274            }
275        }
276
277        if authorized_clients.len() > MAX_RESTRICTED_DISCOVERY_CLIENTS {
278            warn!(
279                "You have configured over {} restricted discovery clients. Your service's descriptor is likely to exceed the 50kB limit",
280                MAX_RESTRICTED_DISCOVERY_CLIENTS
281            );
282        }
283
284        Some(authorized_clients)
285    }
286}
287
288/// Helper for extending a key map with additional keys.
289///
290/// Logs a warning if any of the keys are already present in the map.
291fn extend_key_map(
292    key_map: &mut RestrictedDiscoveryKeys,
293    keys: impl IntoIterator<Item = (HsClientNickname, HsClientDescEncKey)>,
294) {
295    for (nickname, key) in keys.into_iter() {
296        match key_map.entry(nickname.clone()) {
297            Entry::Vacant(v) => {
298                let _: &mut HsClientDescEncKey = v.insert(key);
299            }
300            Entry::Occupied(_) => {
301                warn!(
302                    client_nickname=%nickname,
303                    "Ignoring duplicate client key"
304                );
305            }
306        }
307    }
308}
309
310impl RestrictedDiscoveryConfigBuilder {
311    /// Build the [`RestrictedDiscoveryConfig`].
312    ///
313    /// Returns an error if:
314    ///   - restricted mode is enabled but the `restricted-discovery` feature is not enabled
315    ///   - restricted mode is enabled but no client key providers are configured
316    ///   - restricted mode is disabled, but some client key providers are configured
317    fn post_build_validate(
318        config: RestrictedDiscoveryConfig,
319    ) -> Result<RestrictedDiscoveryConfig, ConfigBuildError> {
320        let RestrictedDiscoveryConfig {
321            enabled,
322            key_dirs,
323            static_keys,
324            watch_configuration,
325        } = config;
326        let key_list = static_keys.as_ref().iter().collect_vec();
327
328        cfg_if::cfg_if! {
329            if #[cfg(feature = "restricted-discovery")] {
330                match (enabled, key_dirs.as_slice(), key_list.as_slice()) {
331                    (true, &[], &[]) => {
332                        return Err(ConfigBuildError::Inconsistent {
333                            fields: vec!["key_dirs".into(), "static_keys".into(), "enabled".into()],
334                            problem: "restricted_discovery not configured, but enabled is true"
335                                .into(),
336                        });
337                    },
338                    (false, &[_, ..], _) => {
339                        return Err(ConfigBuildError::Inconsistent {
340                            fields: vec!["key_dirs".into(), "enabled".into()],
341                            problem: "restricted_discovery.key_dirs configured, but enabled is false"
342                                .into(),
343                        });
344
345                    },
346                    (false, _, &[_, ..])=> {
347                        return Err(ConfigBuildError::Inconsistent {
348                            fields: vec!["static_keys".into(), "enabled".into()],
349                            problem: "restricted_discovery.static_keys configured, but enabled is false"
350                                .into(),
351                        });
352                    }
353                    (true, &[_, ..], _) | (true, _, &[_, ..]) | (false, &[], &[]) => {
354                        // The config is valid.
355                    }
356                }
357            } else {
358                // Restricted mode can only be enabled if the `experimental` feature is enabled.
359
360                // TODO: This could migrate to use tor_config(cfg) instead, but that would change
361                // these errors into warnings.  We could add error support for this into
362                // tor_config.
363                if enabled {
364                    return Err(ConfigBuildError::NoCompileTimeSupport {
365                        field: "enabled".into(),
366                        problem:
367                            "restricted_discovery.enabled=true, but restricted-discovery feature not enabled"
368                                .into(),
369                    });
370                }
371
372                match (key_dirs.as_slice(), key_list.as_slice()) {
373                    (&[_, ..], _) => {
374                        return Err(ConfigBuildError::NoCompileTimeSupport {
375                            field: "key_dirs".into(),
376                            problem:
377                                "restricted_discovery.key_dirs set, but restricted-discovery feature not enabled"
378                                    .into(),
379                        });
380                    },
381                    (_, &[_, ..]) => {
382                        return Err(ConfigBuildError::NoCompileTimeSupport {
383                            field: "static_keys".into(),
384                            problem:
385                                "restricted_discovery.static_keys set, but restricted-discovery feature not enabled"
386                                    .into(),
387                        });
388                    },
389                    (&[], &[]) => {
390                        // The config is valid.
391                    }
392                };
393            }
394        }
395
396        Ok(RestrictedDiscoveryConfig {
397            enabled,
398            key_dirs,
399            static_keys,
400            watch_configuration,
401        })
402    }
403}
404
405#[cfg(test)]
406mod test {
407    // @@ begin test lint list maintained by maint/add_warning @@
408    #![allow(clippy::bool_assert_comparison)]
409    #![allow(clippy::clone_on_copy)]
410    #![allow(clippy::dbg_macro)]
411    #![allow(clippy::mixed_attributes_style)]
412    #![allow(clippy::print_stderr)]
413    #![allow(clippy::print_stdout)]
414    #![allow(clippy::single_char_pattern)]
415    #![allow(clippy::unwrap_used)]
416    #![allow(clippy::unchecked_time_subtraction)]
417    #![allow(clippy::useless_vec)]
418    #![allow(clippy::needless_pass_by_value)]
419    #![allow(clippy::string_slice)] // See arti#2571
420    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
421
422    use super::*;
423
424    use std::ops::Index;
425
426    use tor_basic_utils::test_rng::Config;
427    use tor_config::assert_config_error;
428    use tor_config_path::CfgPath;
429    use tor_hscrypto::pk::HsClientDescEncKeypair;
430
431    /// A helper for creating a test (`HsClientNickname`, `HsClientDescEncKey`) pair.
432    fn make_authorized_client(nickname: &str) -> (HsClientNickname, HsClientDescEncKey) {
433        let mut rng = Config::Deterministic.into_rng();
434        let nickname: HsClientNickname = nickname.parse().unwrap();
435        let keypair = HsClientDescEncKeypair::generate(&mut rng);
436        let pk = keypair.public();
437
438        (nickname, pk.clone())
439    }
440
441    fn write_key_to_file(dir: &Path, nickname: &HsClientNickname, key: impl fmt::Display) {
442        let path = dir.join(nickname.to_string()).with_extension("auth");
443        fs::write(path, key.to_string()).unwrap();
444    }
445
446    #[test]
447    #[cfg(feature = "restricted-discovery")]
448    fn invalid_config() {
449        let err = RestrictedDiscoveryConfigBuilder::default()
450            .enabled(true)
451            .build()
452            .unwrap_err();
453
454        assert_config_error!(
455            err,
456            Inconsistent,
457            "restricted_discovery not configured, but enabled is true"
458        );
459
460        let mut builder = RestrictedDiscoveryConfigBuilder::default();
461        builder.static_keys().access().push((
462            HsClientNickname::from_str("alice").unwrap(),
463            HsClientDescEncKey::from_str(
464                "descriptor:x25519:zprrmiv6dv6sjfl7sfbsvlj5vunpgcdfevz7m23ltlvtccxjqbka",
465            )
466            .unwrap(),
467        ));
468
469        let err = builder.build().unwrap_err();
470
471        assert_config_error!(
472            err,
473            Inconsistent,
474            "restricted_discovery.static_keys configured, but enabled is false"
475        );
476
477        let mut dir_provider = DirectoryKeyProviderBuilder::default();
478        dir_provider.path(CfgPath::new("/foo".to_string()));
479        let mut builder = RestrictedDiscoveryConfigBuilder::default();
480        builder.key_dirs().access().push(dir_provider);
481
482        let err = builder.build().unwrap_err();
483
484        assert_config_error!(
485            err,
486            Inconsistent,
487            "restricted_discovery.key_dirs configured, but enabled is false"
488        );
489    }
490
491    #[test]
492    #[cfg(feature = "restricted-discovery")]
493    fn empty_providers() {
494        // It's not a configuration error to enable restricted mode is enabled
495        // without configuring any keys, but this would make the service unreachable
496        // (a different part of the code will issue a warning about this).
497        let mut builder = RestrictedDiscoveryConfigBuilder::default();
498        let dir = tempfile::TempDir::new().unwrap();
499        let mut dir_prov_builder = DirectoryKeyProviderBuilder::default();
500        dir_prov_builder
501            .path(CfgPath::new_literal(dir.path()))
502            .permissions()
503            .dangerously_trust_everyone();
504        builder
505            .enabled(true)
506            .key_dirs()
507            .access()
508            // Push a directory provider that has no keys
509            .push(dir_prov_builder);
510
511        let restricted_config = builder.build().unwrap();
512        let path_resolver = CfgPathResolver::default();
513        assert!(
514            restricted_config
515                .read_keys(&path_resolver)
516                .unwrap()
517                .is_empty()
518        );
519    }
520
521    #[test]
522    #[cfg(not(feature = "restricted-discovery"))]
523    fn invalid_config() {
524        let mut builder = RestrictedDiscoveryConfigBuilder::default();
525        let err = builder.enabled(true).build().unwrap_err();
526
527        assert_config_error!(
528            err,
529            NoCompileTimeSupport,
530            "restricted_discovery.enabled=true, but restricted-discovery feature not enabled"
531        );
532
533        let mut builder = RestrictedDiscoveryConfigBuilder::default();
534        builder.static_keys().access().push((
535            HsClientNickname::from_str("alice").unwrap(),
536            HsClientDescEncKey::from_str(
537                "descriptor:x25519:zprrmiv6dv6sjfl7sfbsvlj5vunpgcdfevz7m23ltlvtccxjqbka",
538            )
539            .unwrap(),
540        ));
541
542        let err = builder.build().unwrap_err();
543
544        assert_config_error!(
545            err,
546            NoCompileTimeSupport,
547            "restricted_discovery.static_keys set, but restricted-discovery feature not enabled"
548        );
549    }
550
551    #[test]
552    #[cfg(feature = "restricted-discovery")]
553    fn valid_config() {
554        /// The total number of clients.
555        const CLIENT_COUNT: usize = 10;
556        /// The number of client keys configured using a static provider.
557        const STATIC_CLIENT_COUNT: usize = 5;
558
559        let mut all_keys = vec![];
560        let dir = tempfile::TempDir::new().unwrap();
561
562        // A builder that only has static keys.
563        let mut builder_static_keys = RestrictedDiscoveryConfigBuilder::default();
564        builder_static_keys.enabled(true);
565
566        // A builder that only a key dir.
567        let mut builder_key_dir = RestrictedDiscoveryConfigBuilder::default();
568        builder_key_dir.enabled(true);
569
570        // A builder that has both static keys and a key dir
571        let mut builder_static_and_key_dir = RestrictedDiscoveryConfigBuilder::default();
572        builder_static_and_key_dir.enabled(true);
573
574        for i in 0..CLIENT_COUNT {
575            let (nickname, key) = make_authorized_client(&format!("client-{i}"));
576            all_keys.push((nickname.clone(), key.clone()));
577
578            if i < STATIC_CLIENT_COUNT {
579                builder_static_keys
580                    .static_keys()
581                    .access()
582                    .push((nickname.clone(), key.clone()));
583                builder_static_and_key_dir
584                    .static_keys()
585                    .access()
586                    .push((nickname, key.clone()));
587            } else {
588                let path = dir.path().join(nickname.to_string()).with_extension("auth");
589                fs::write(path, key.to_string()).unwrap();
590            }
591        }
592
593        let mut dir_builder = DirectoryKeyProviderBuilder::default();
594        dir_builder
595            .path(CfgPath::new_literal(dir.path()))
596            .permissions()
597            .dangerously_trust_everyone();
598
599        for b in &mut [&mut builder_key_dir, &mut builder_static_and_key_dir] {
600            b.key_dirs().access().push(dir_builder.clone());
601        }
602
603        let test_cases = [
604            (0..STATIC_CLIENT_COUNT, builder_static_keys),
605            (STATIC_CLIENT_COUNT..CLIENT_COUNT, builder_key_dir),
606            (0..CLIENT_COUNT, builder_static_and_key_dir),
607        ];
608
609        for (range, builder) in test_cases {
610            let config = builder.build().unwrap();
611            let path_resolver = CfgPathResolver::default();
612
613            let mut authorized_clients = config
614                .read_keys(&path_resolver)
615                .unwrap()
616                .into_iter()
617                .collect_vec();
618            authorized_clients.sort_by(|k1, k2| k1.0.cmp(&k2.0));
619
620            assert_eq!(authorized_clients.as_slice(), all_keys.index(range));
621        }
622    }
623
624    #[test]
625    #[cfg(feature = "restricted-discovery")]
626    fn key_precedence() {
627        // A builder with static keys, and two key dirs.
628        let mut builder = RestrictedDiscoveryConfigBuilder::default();
629        builder.enabled(true);
630        let (foo_nick, foo_key1) = make_authorized_client("foo");
631        builder
632            .static_keys()
633            .access()
634            .push((foo_nick.clone(), foo_key1.clone()));
635
636        // Make another client key with the same nickname
637        let (_foo_nick, foo_key2) = make_authorized_client("foo");
638
639        let dir1 = tempfile::TempDir::new().unwrap();
640        let dir2 = tempfile::TempDir::new().unwrap();
641
642        // Write a different key with the same nickname to dir1
643        // (we will check that the entry from static_keys takes precedence over it)
644        write_key_to_file(dir1.path(), &foo_nick, foo_key2);
645
646        // Write two keys sharing the same nickname to dir1 and dir2
647        // (we will check that the first dir_keys entry takes precedence over the second)
648        let (bar_nick, bar_key1) = make_authorized_client("bar");
649        write_key_to_file(dir1.path(), &bar_nick, &bar_key1);
650
651        let (_bar_nick, bar_key2) = make_authorized_client("bar");
652        write_key_to_file(dir2.path(), &bar_nick, bar_key2);
653
654        let mut key_dir1 = DirectoryKeyProviderBuilder::default();
655        key_dir1
656            .path(CfgPath::new_literal(dir1.path()))
657            .permissions()
658            .dangerously_trust_everyone();
659
660        let mut key_dir2 = DirectoryKeyProviderBuilder::default();
661        key_dir2
662            .path(CfgPath::new_literal(dir2.path()))
663            .permissions()
664            .dangerously_trust_everyone();
665
666        builder.key_dirs().access().extend([key_dir1, key_dir2]);
667        let config = builder.build().unwrap();
668        let path_resolver = CfgPathResolver::default();
669        let keys = config.read_keys(&path_resolver).unwrap();
670
671        // Check that foo is the entry we inserted into static_keys:
672        let foo_key_found = keys.get(&foo_nick).unwrap();
673        assert_eq!(foo_key_found, &foo_key1);
674
675        // Check that bar is the entry from key_dir1
676        // (dir1 takes precedence over dir2)
677        let bar_key_found = keys.get(&bar_nick).unwrap();
678        assert_eq!(bar_key_found, &bar_key1);
679    }
680
681    #[test]
682    #[cfg(feature = "restricted-discovery")]
683    fn ignore_invalid() {
684        /// The number of valid keys.
685        const VALID_COUNT: usize = 5;
686
687        let dir = tempfile::TempDir::new().unwrap();
688        for i in 0..VALID_COUNT {
689            let (nickname, key) = make_authorized_client(&format!("client-{i}"));
690            write_key_to_file(dir.path(), &nickname, &key);
691        }
692
693        // Add some malformed keys
694        let nickname: HsClientNickname = "foo".parse().unwrap();
695
696        write_key_to_file(dir.path(), &nickname, "descriptor:x25519:foobar");
697
698        let (nickname, key) = make_authorized_client("bar");
699        let path = dir
700            .path()
701            .join(nickname.to_string())
702            .with_extension("not_auth");
703        fs::write(path, key.to_string()).unwrap();
704
705        let mut dir_prov_builder = DirectoryKeyProviderBuilder::default();
706        dir_prov_builder
707            .path(CfgPath::new_literal(dir.path()))
708            .permissions()
709            .dangerously_trust_everyone();
710
711        let mut builder = RestrictedDiscoveryConfigBuilder::default();
712        builder
713            .enabled(true)
714            .key_dirs()
715            .access()
716            .push(dir_prov_builder);
717        let config = builder.build().unwrap();
718
719        let path_resolver = CfgPathResolver::default();
720        assert_eq!(config.read_keys(&path_resolver).unwrap().len(), VALID_COUNT);
721    }
722}