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::*;
103
104use std::collections::btree_map::Entry;
105use std::collections::BTreeMap;
106
107use amplify::Getters;
108use derive_more::{Display, Into};
109
110use tor_config_path::CfgPathResolver;
111use tor_error::warn_report;
112use tor_persist::slug::BadSlug;
113
114/// The recommended maximum number of restricted mode clients.
115///
116/// See the [module-level documentation](self) for an explanation of this limitation.
117///
118/// Note: this is an approximate, one-size-fits-all figure.
119/// In practice, the maximum number of clients depends on the rest of the service's configuration,
120/// and may in fact be higher, or lower, than this value.
121//
122// TODO: we should come up with a more accurate upper limit. The actual limit might be even lower,
123// depending on the service's configuration (i.e. number of intro points).
124//
125// This figure is an approximation. It was obtained by filling a descriptor
126// with as much information as possible. The descriptor was built with
127// * `single-onion-service` set
128// * 20 intro points (which is the current upper limit), where each intro point had a
129// single link specifier (I'm not sure if there's a limit for the number of link specifiers)
130// * 165 authorized clients
131// and had a size of 42157 bytes. Adding one more client tipped it over the limit (with 166
132// authorized clients, the size of the descriptor was 56027 bytes).
133//
134// See also tor#29134
135pub const MAX_RESTRICTED_DISCOVERY_CLIENTS: usize = 160;
136
137/// Nickname (local identifier) for a hidden service client.
138///
139/// An `HsClientNickname` must be a valid [`Slug`].
140/// See [slug](tor_persist::slug) for the syntactic requirements.
141//
142// TODO: when we implement the arti hsc CLI for managing the configured client keys,
143// we will use the nicknames to identify individual clients.
144#[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] //
145#[derive(Display, From, Into, Serialize, Deserialize)]
146#[serde(transparent)]
147pub struct HsClientNickname(Slug);
148
149/// A list of client discovery keys.
150pub(crate) type RestrictedDiscoveryKeys = BTreeMap<HsClientNickname, HsClientDescEncKey>;
151
152impl FromStr for HsClientNickname {
153 type Err = BadSlug;
154
155 fn from_str(s: &str) -> Result<Self, Self::Err> {
156 let slug = Slug::try_from(s.to_string())?;
157
158 Ok(Self(slug))
159 }
160}
161
162/// Configuration for enabling restricted discovery mode.
163///
164/// # Client nickname uniqueness
165///
166/// The client nicknames specified in `key_dirs` and `static_keys`
167/// **must** be unique. Any nickname occurring in `static_keys` must not
168/// already have an entry in any of the configured `key_dirs`,
169/// and any one nickname must not occur in more than one of the `key_dirs`.
170///
171/// Violating this rule will cause the additional keys to be ignored.
172/// If there are multiple entries for the same nickname,
173/// the entry with the highest precedence will be used, and all the others will be ignored.
174/// The precedence rules are as follows:
175/// * the `static_keys` take precedence over the keys from `key_dirs`
176/// * the ordering of the directories in `key_dirs` represents the order of precedence
177///
178/// # Reloading the configuration
179///
180/// Currently, the `static_keys` and `key_dirs` directories will *not* be monitored for updates,
181/// even when automatic config reload is enabled. We hope to change that in the future.
182/// In the meantime, you will need to restart your service every time you update
183/// its restricted discovery settings in order for the changes to be applied.
184///
185/// See the [module-level documentation](self) for more details.
186#[derive(Debug, Clone, Builder, Eq, PartialEq, Getters)]
187#[builder(build_fn(error = "ConfigBuildError", name = "build_unvalidated"))]
188#[builder(derive(Serialize, Deserialize, Debug, Deftly))]
189#[non_exhaustive]
190pub struct RestrictedDiscoveryConfig {
191 /// Whether to enable restricted discovery mode.
192 ///
193 /// Services running in restricted discovery mode are only discoverable
194 /// by the configured clients.
195 ///
196 /// Can only be enabled if the `restricted-discovery` feature is enabled.
197 ///
198 /// If you enable this, you must also specify the authorized clients (via `static_keys`),
199 /// or the directories where the authorized client keys should be read from (via `key_dirs`).
200 ///
201 /// Restricted discovery mode is disabled by default.
202 #[builder(default)]
203 pub(crate) enabled: bool,
204
205 /// If true, the provided `key_dirs` will be watched for changes.
206 #[builder(default)]
207 #[builder_field_attr(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 #[builder(default, sub_builder(fn_name = "build"))]
217 #[builder_field_attr(serde(default))]
218 key_dirs: DirectoryKeyProviderList,
219
220 /// A static mapping from client nicknames to keys.
221 ///
222 /// Each client key must be in the `descriptor:x25519:<base32-encoded-x25519-public-key>`
223 /// format.
224 #[builder(default, sub_builder(fn_name = "build"))]
225 #[builder_field_attr(serde(default))]
226 static_keys: StaticKeyProvider,
227}
228
229impl RestrictedDiscoveryConfig {
230 /// Read the client keys from all the configured key providers.
231 ///
232 /// Returns `None` if restricted mode is disabled.
233 ///
234 // TODO: this is not currently implemented (reconfigure() doesn't call read_keys)
235 /// When reconfiguring a [`RunningOnionService`](crate::RunningOnionService),
236 /// call this function to obtain an up-to-date view of the authorized clients.
237 ///
238 // TODO: this is a footgun. We might want to rethink this before we make
239 // the restricted-discovery feature non-experimental:
240 /// Note: if there are multiple entries for the same [`HsClientNickname`],
241 /// only one of them will be used (the others are ignored).
242 /// The deduplication logic is as follows:
243 /// * the `static_keys` take precedence over the keys from `key_dirs`
244 /// * the ordering of the directories in `key_dirs` represents the order of precedence
245 pub(crate) fn read_keys(
246 &self,
247 path_resolver: &CfgPathResolver,
248 ) -> Option<RestrictedDiscoveryKeys> {
249 if !self.enabled {
250 return None;
251 }
252
253 let mut authorized_clients = BTreeMap::new();
254
255 // The static_keys are inserted first, so they have precedence over
256 // the keys from key_dirs.
257 extend_key_map(
258 &mut authorized_clients,
259 RestrictedDiscoveryKeys::from(self.static_keys.clone()),
260 );
261
262 // The key_dirs are read in order of appearance,
263 // which is also the order of precedence.
264 for dir in &self.key_dirs {
265 match dir.read_keys(path_resolver) {
266 Ok(keys) => extend_key_map(&mut authorized_clients, keys),
267 Err(e) => {
268 warn_report!(e, "Failed to read keys at {}", dir.path());
269 }
270 }
271 }
272
273 if authorized_clients.len() > MAX_RESTRICTED_DISCOVERY_CLIENTS {
274 warn!(
275 "You have configured over {} restricted discovery clients. Your service's descriptor is likely to exceed the 50kB limit",
276 MAX_RESTRICTED_DISCOVERY_CLIENTS
277 );
278 }
279
280 Some(authorized_clients)
281 }
282}
283
284/// Helper for extending a key map with additional keys.
285///
286/// Logs a warning if any of the keys are already present in the map.
287fn extend_key_map(
288 key_map: &mut RestrictedDiscoveryKeys,
289 keys: impl IntoIterator<Item = (HsClientNickname, HsClientDescEncKey)>,
290) {
291 for (nickname, key) in keys.into_iter() {
292 match key_map.entry(nickname.clone()) {
293 Entry::Vacant(v) => {
294 let _: &mut HsClientDescEncKey = v.insert(key);
295 }
296 Entry::Occupied(_) => {
297 warn!(
298 client_nickname=%nickname,
299 "Ignoring duplicate client key"
300 );
301 }
302 }
303 }
304}
305
306impl RestrictedDiscoveryConfigBuilder {
307 /// Build the [`RestrictedDiscoveryConfig`].
308 ///
309 /// Returns an error if:
310 /// - restricted mode is enabled but the `restricted-discovery` feature is not enabled
311 /// - restricted mode is enabled but no client key providers are configured
312 /// - restricted mode is disabled, but some client key providers are configured
313 pub fn build(&self) -> Result<RestrictedDiscoveryConfig, ConfigBuildError> {
314 let RestrictedDiscoveryConfig {
315 enabled,
316 key_dirs,
317 static_keys,
318 watch_configuration,
319 } = self.build_unvalidated()?;
320 let key_list = static_keys.as_ref().iter().collect_vec();
321
322 cfg_if::cfg_if! {
323 if #[cfg(feature = "restricted-discovery")] {
324 match (enabled, key_dirs.as_slice(), key_list.as_slice()) {
325 (true, &[], &[]) => {
326 return Err(ConfigBuildError::Inconsistent {
327 fields: vec!["key_dirs".into(), "static_keys".into(), "enabled".into()],
328 problem: "restricted_discovery not configured, but enabled is true"
329 .into(),
330 });
331 },
332 (false, &[_, ..], _) => {
333 return Err(ConfigBuildError::Inconsistent {
334 fields: vec!["key_dirs".into(), "enabled".into()],
335 problem: "restricted_discovery.key_dirs configured, but enabled is false"
336 .into(),
337 });
338
339 },
340 (false, _, &[_, ..])=> {
341 return Err(ConfigBuildError::Inconsistent {
342 fields: vec!["static_keys".into(), "enabled".into()],
343 problem: "restricted_discovery.static_keys configured, but enabled is false"
344 .into(),
345 });
346 }
347 (true, &[_, ..], _) | (true, _, &[_, ..]) | (false, &[], &[]) => {
348 // The config is valid.
349 }
350 }
351 } else {
352 // Restricted mode can only be enabled if the `experimental` feature is enabled.
353 if enabled {
354 return Err(ConfigBuildError::NoCompileTimeSupport {
355 field: "enabled".into(),
356 problem:
357 "restricted_discovery.enabled=true, but restricted-discovery feature not enabled"
358 .into(),
359 });
360 }
361
362 match (key_dirs.as_slice(), key_list.as_slice()) {
363 (&[_, ..], _) => {
364 return Err(ConfigBuildError::NoCompileTimeSupport {
365 field: "key_dirs".into(),
366 problem:
367 "restricted_discovery.key_dirs set, but restricted-discovery feature not enabled"
368 .into(),
369 });
370 },
371 (_, &[_, ..]) => {
372 return Err(ConfigBuildError::NoCompileTimeSupport {
373 field: "static_keys".into(),
374 problem:
375 "restricted_discovery.static_keys set, but restricted-discovery feature not enabled"
376 .into(),
377 });
378 },
379 (&[], &[]) => {
380 // The config is valid.
381 }
382 };
383 }
384 }
385
386 Ok(RestrictedDiscoveryConfig {
387 enabled,
388 key_dirs,
389 static_keys,
390 watch_configuration,
391 })
392 }
393}
394
395#[cfg(test)]
396mod test {
397 // @@ begin test lint list maintained by maint/add_warning @@
398 #![allow(clippy::bool_assert_comparison)]
399 #![allow(clippy::clone_on_copy)]
400 #![allow(clippy::dbg_macro)]
401 #![allow(clippy::mixed_attributes_style)]
402 #![allow(clippy::print_stderr)]
403 #![allow(clippy::print_stdout)]
404 #![allow(clippy::single_char_pattern)]
405 #![allow(clippy::unwrap_used)]
406 #![allow(clippy::unchecked_duration_subtraction)]
407 #![allow(clippy::useless_vec)]
408 #![allow(clippy::needless_pass_by_value)]
409 //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
410
411 use super::*;
412
413 use std::ops::Index;
414
415 use tor_basic_utils::test_rng::Config;
416 use tor_config::assert_config_error;
417 use tor_config_path::CfgPath;
418 use tor_hscrypto::pk::HsClientDescEncKeypair;
419
420 /// A helper for creating a test (`HsClientNickname`, `HsClientDescEncKey`) pair.
421 fn make_authorized_client(nickname: &str) -> (HsClientNickname, HsClientDescEncKey) {
422 let mut rng = Config::Deterministic.into_rng();
423 let nickname: HsClientNickname = nickname.parse().unwrap();
424 let keypair = HsClientDescEncKeypair::generate(&mut rng);
425 let pk = keypair.public();
426
427 (nickname, pk.clone())
428 }
429
430 fn write_key_to_file(dir: &Path, nickname: &HsClientNickname, key: impl fmt::Display) {
431 let path = dir.join(nickname.to_string()).with_extension("auth");
432 fs::write(path, key.to_string()).unwrap();
433 }
434
435 #[test]
436 #[cfg(feature = "restricted-discovery")]
437 fn invalid_config() {
438 let err = RestrictedDiscoveryConfigBuilder::default()
439 .enabled(true)
440 .build()
441 .unwrap_err();
442
443 assert_config_error!(
444 err,
445 Inconsistent,
446 "restricted_discovery not configured, but enabled is true"
447 );
448
449 let mut builder = RestrictedDiscoveryConfigBuilder::default();
450 builder.static_keys().access().push((
451 HsClientNickname::from_str("alice").unwrap(),
452 HsClientDescEncKey::from_str(
453 "descriptor:x25519:zprrmiv6dv6sjfl7sfbsvlj5vunpgcdfevz7m23ltlvtccxjqbka",
454 )
455 .unwrap(),
456 ));
457
458 let err = builder.build().unwrap_err();
459
460 assert_config_error!(
461 err,
462 Inconsistent,
463 "restricted_discovery.static_keys configured, but enabled is false"
464 );
465
466 let mut dir_provider = DirectoryKeyProviderBuilder::default();
467 dir_provider.path(CfgPath::new("/foo".to_string()));
468 let mut builder = RestrictedDiscoveryConfigBuilder::default();
469 builder.key_dirs().access().push(dir_provider);
470
471 let err = builder.build().unwrap_err();
472
473 assert_config_error!(
474 err,
475 Inconsistent,
476 "restricted_discovery.key_dirs configured, but enabled is false"
477 );
478 }
479
480 #[test]
481 #[cfg(feature = "restricted-discovery")]
482 fn empty_providers() {
483 // It's not a configuration error to enable restricted mode is enabled
484 // without configuring any keys, but this would make the service unreachable
485 // (a different part of the code will issue a warning about this).
486 let mut builder = RestrictedDiscoveryConfigBuilder::default();
487 let dir = tempfile::TempDir::new().unwrap();
488 let mut dir_prov_builder = DirectoryKeyProviderBuilder::default();
489 dir_prov_builder
490 .path(CfgPath::new_literal(dir.path()))
491 .permissions()
492 .dangerously_trust_everyone();
493 builder
494 .enabled(true)
495 .key_dirs()
496 .access()
497 // Push a directory provider that has no keys
498 .push(dir_prov_builder);
499
500 let restricted_config = builder.build().unwrap();
501 let path_resolver = CfgPathResolver::default();
502 assert!(restricted_config
503 .read_keys(&path_resolver)
504 .unwrap()
505 .is_empty());
506 }
507
508 #[test]
509 #[cfg(not(feature = "restricted-discovery"))]
510 fn invalid_config() {
511 let mut builder = RestrictedDiscoveryConfigBuilder::default();
512 let err = builder.enabled(true).build().unwrap_err();
513
514 assert_config_error!(
515 err,
516 NoCompileTimeSupport,
517 "restricted_discovery.enabled=true, but restricted-discovery feature not enabled"
518 );
519
520 let mut builder = RestrictedDiscoveryConfigBuilder::default();
521 builder.static_keys().access().push((
522 HsClientNickname::from_str("alice").unwrap(),
523 HsClientDescEncKey::from_str(
524 "descriptor:x25519:zprrmiv6dv6sjfl7sfbsvlj5vunpgcdfevz7m23ltlvtccxjqbka",
525 )
526 .unwrap(),
527 ));
528
529 let err = builder.build().unwrap_err();
530
531 assert_config_error!(
532 err,
533 NoCompileTimeSupport,
534 "restricted_discovery.static_keys set, but restricted-discovery feature not enabled"
535 );
536 }
537
538 #[test]
539 #[cfg(feature = "restricted-discovery")]
540 fn valid_config() {
541 /// The total number of clients.
542 const CLIENT_COUNT: usize = 10;
543 /// The number of client keys configured using a static provider.
544 const STATIC_CLIENT_COUNT: usize = 5;
545
546 let mut all_keys = vec![];
547 let dir = tempfile::TempDir::new().unwrap();
548
549 // A builder that only has static keys.
550 let mut builder_static_keys = RestrictedDiscoveryConfigBuilder::default();
551 builder_static_keys.enabled(true);
552
553 // A builder that only a key dir.
554 let mut builder_key_dir = RestrictedDiscoveryConfigBuilder::default();
555 builder_key_dir.enabled(true);
556
557 // A builder that has both static keys and a key dir
558 let mut builder_static_and_key_dir = RestrictedDiscoveryConfigBuilder::default();
559 builder_static_and_key_dir.enabled(true);
560
561 for i in 0..CLIENT_COUNT {
562 let (nickname, key) = make_authorized_client(&format!("client-{i}"));
563 all_keys.push((nickname.clone(), key.clone()));
564
565 if i < STATIC_CLIENT_COUNT {
566 builder_static_keys
567 .static_keys()
568 .access()
569 .push((nickname.clone(), key.clone()));
570 builder_static_and_key_dir
571 .static_keys()
572 .access()
573 .push((nickname, key.clone()));
574 } else {
575 let path = dir.path().join(nickname.to_string()).with_extension("auth");
576 fs::write(path, key.to_string()).unwrap();
577 }
578 }
579
580 let mut dir_builder = DirectoryKeyProviderBuilder::default();
581 dir_builder
582 .path(CfgPath::new_literal(dir.path()))
583 .permissions()
584 .dangerously_trust_everyone();
585
586 for b in &mut [&mut builder_key_dir, &mut builder_static_and_key_dir] {
587 b.key_dirs().access().push(dir_builder.clone());
588 }
589
590 let test_cases = [
591 (0..STATIC_CLIENT_COUNT, builder_static_keys),
592 (STATIC_CLIENT_COUNT..CLIENT_COUNT, builder_key_dir),
593 (0..CLIENT_COUNT, builder_static_and_key_dir),
594 ];
595
596 for (range, builder) in test_cases {
597 let config = builder.build().unwrap();
598 let path_resolver = CfgPathResolver::default();
599
600 let mut authorized_clients = config
601 .read_keys(&path_resolver)
602 .unwrap()
603 .into_iter()
604 .collect_vec();
605 authorized_clients.sort_by(|k1, k2| k1.0.cmp(&k2.0));
606
607 assert_eq!(authorized_clients.as_slice(), all_keys.index(range));
608 }
609 }
610
611 #[test]
612 #[cfg(feature = "restricted-discovery")]
613 fn key_precedence() {
614 // A builder with static keys, and two key dirs.
615 let mut builder = RestrictedDiscoveryConfigBuilder::default();
616 builder.enabled(true);
617 let (foo_nick, foo_key1) = make_authorized_client("foo");
618 builder
619 .static_keys()
620 .access()
621 .push((foo_nick.clone(), foo_key1.clone()));
622
623 // Make another client key with the same nickname
624 let (_foo_nick, foo_key2) = make_authorized_client("foo");
625
626 let dir1 = tempfile::TempDir::new().unwrap();
627 let dir2 = tempfile::TempDir::new().unwrap();
628
629 // Write a different key with the same nickname to dir1
630 // (we will check that the entry from static_keys takes precedence over it)
631 write_key_to_file(dir1.path(), &foo_nick, foo_key2);
632
633 // Write two keys sharing the same nickname to dir1 and dir2
634 // (we will check that the first dir_keys entry takes precedence over the second)
635 let (bar_nick, bar_key1) = make_authorized_client("bar");
636 write_key_to_file(dir1.path(), &bar_nick, &bar_key1);
637
638 let (_bar_nick, bar_key2) = make_authorized_client("bar");
639 write_key_to_file(dir2.path(), &bar_nick, bar_key2);
640
641 let mut key_dir1 = DirectoryKeyProviderBuilder::default();
642 key_dir1
643 .path(CfgPath::new_literal(dir1.path()))
644 .permissions()
645 .dangerously_trust_everyone();
646
647 let mut key_dir2 = DirectoryKeyProviderBuilder::default();
648 key_dir2
649 .path(CfgPath::new_literal(dir2.path()))
650 .permissions()
651 .dangerously_trust_everyone();
652
653 builder.key_dirs().access().extend([key_dir1, key_dir2]);
654 let config = builder.build().unwrap();
655 let path_resolver = CfgPathResolver::default();
656 let keys = config.read_keys(&path_resolver).unwrap();
657
658 // Check that foo is the entry we inserted into static_keys:
659 let foo_key_found = keys.get(&foo_nick).unwrap();
660 assert_eq!(foo_key_found, &foo_key1);
661
662 // Check that bar is the entry from key_dir1
663 // (dir1 takes precedence over dir2)
664 let bar_key_found = keys.get(&bar_nick).unwrap();
665 assert_eq!(bar_key_found, &bar_key1);
666 }
667
668 #[test]
669 #[cfg(feature = "restricted-discovery")]
670 fn ignore_invalid() {
671 /// The number of valid keys.
672 const VALID_COUNT: usize = 5;
673
674 let dir = tempfile::TempDir::new().unwrap();
675 for i in 0..VALID_COUNT {
676 let (nickname, key) = make_authorized_client(&format!("client-{i}"));
677 write_key_to_file(dir.path(), &nickname, &key);
678 }
679
680 // Add some malformed keys
681 let nickname: HsClientNickname = "foo".parse().unwrap();
682
683 write_key_to_file(dir.path(), &nickname, "descriptor:x25519:foobar");
684
685 let (nickname, key) = make_authorized_client("bar");
686 let path = dir
687 .path()
688 .join(nickname.to_string())
689 .with_extension("not_auth");
690 fs::write(path, key.to_string()).unwrap();
691
692 let mut dir_prov_builder = DirectoryKeyProviderBuilder::default();
693 dir_prov_builder
694 .path(CfgPath::new_literal(dir.path()))
695 .permissions()
696 .dangerously_trust_everyone();
697
698 let mut builder = RestrictedDiscoveryConfigBuilder::default();
699 builder
700 .enabled(true)
701 .key_dirs()
702 .access()
703 .push(dir_prov_builder);
704 let config = builder.build().unwrap();
705
706 let path_resolver = CfgPathResolver::default();
707 assert_eq!(config.read_keys(&path_resolver).unwrap().len(), VALID_COUNT);
708 }
709}