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

            
95
mod key_provider;
96

            
97
pub use key_provider::{
98
    DirectoryKeyProvider, DirectoryKeyProviderBuilder, DirectoryKeyProviderList,
99
    DirectoryKeyProviderListBuilder, StaticKeyProvider, StaticKeyProviderBuilder,
100
};
101

            
102
use crate::internal_prelude::*;
103
use derive_more::From;
104

            
105
use std::collections::BTreeMap;
106
use std::collections::btree_map::Entry;
107

            
108
use amplify::Getters;
109
use derive_more::{Display, Into};
110

            
111
use tor_config::derive::prelude::*;
112
use tor_config_path::CfgPathResolver;
113
use tor_error::warn_report;
114
use 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
137
pub 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)]
149
pub struct HsClientNickname(Slug);
150

            
151
/// A list of client discovery keys.
152
pub(crate) type RestrictedDiscoveryKeys = BTreeMap<HsClientNickname, HsClientDescEncKey>;
153

            
154
impl FromStr for HsClientNickname {
155
    type Err = BadSlug;
156

            
157
230
    fn from_str(s: &str) -> Result<Self, Self::Err> {
158
230
        let slug = Slug::try_from(s.to_string())?;
159

            
160
230
        Ok(Self(slug))
161
230
    }
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"))]
191
pub 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

            
233
impl 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
28
    pub(crate) fn read_keys(
250
28
        &self,
251
28
        path_resolver: &CfgPathResolver,
252
28
    ) -> Option<RestrictedDiscoveryKeys> {
253
28
        if !self.enabled {
254
16
            return None;
255
12
        }
256

            
257
12
        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
12
        extend_key_map(
262
12
            &mut authorized_clients,
263
12
            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
12
        for dir in &self.key_dirs {
269
12
            match dir.read_keys(path_resolver) {
270
12
                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
12
        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
12
        }
283

            
284
12
        Some(authorized_clients)
285
28
    }
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.
291
24
fn extend_key_map(
292
24
    key_map: &mut RestrictedDiscoveryKeys,
293
24
    keys: impl IntoIterator<Item = (HsClientNickname, HsClientDescEncKey)>,
294
24
) {
295
58
    for (nickname, key) in keys.into_iter() {
296
58
        match key_map.entry(nickname.clone()) {
297
54
            Entry::Vacant(v) => {
298
54
                let _: &mut HsClientDescEncKey = v.insert(key);
299
54
            }
300
            Entry::Occupied(_) => {
301
4
                warn!(
302
                    client_nickname=%nickname,
303
                    "Ignoring duplicate client key"
304
                );
305
            }
306
        }
307
    }
308
24
}
309

            
310
impl 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
1478
    fn post_build_validate(
318
1478
        config: RestrictedDiscoveryConfig,
319
1478
    ) -> Result<RestrictedDiscoveryConfig, ConfigBuildError> {
320
        let RestrictedDiscoveryConfig {
321
1478
            enabled,
322
1478
            key_dirs,
323
1478
            static_keys,
324
1478
            watch_configuration,
325
1478
        } = config;
326
1478
        let key_list = static_keys.as_ref().iter().collect_vec();
327

            
328
        cfg_if::cfg_if! {
329
            if #[cfg(feature = "restricted-discovery")] {
330
1478
                match (enabled, key_dirs.as_slice(), key_list.as_slice()) {
331
88
                    (true, &[], &[]) => {
332
2
                        return Err(ConfigBuildError::Inconsistent {
333
2
                            fields: vec!["key_dirs".into(), "static_keys".into(), "enabled".into()],
334
2
                            problem: "restricted_discovery not configured, but enabled is true"
335
2
                                .into(),
336
2
                        });
337
                    },
338
1390
                    (false, &[_, ..], _) => {
339
2
                        return Err(ConfigBuildError::Inconsistent {
340
2
                            fields: vec!["key_dirs".into(), "enabled".into()],
341
2
                            problem: "restricted_discovery.key_dirs configured, but enabled is false"
342
2
                                .into(),
343
2
                        });
344

            
345
                    },
346
1388
                    (false, _, &[_, ..])=> {
347
2
                        return Err(ConfigBuildError::Inconsistent {
348
2
                            fields: vec!["static_keys".into(), "enabled".into()],
349
2
                            problem: "restricted_discovery.static_keys configured, but enabled is false"
350
2
                                .into(),
351
2
                        });
352
                    }
353
1472
                    (true, &[_, ..], _) | (true, _, &[_, ..]) | (false, &[], &[]) => {
354
1472
                        // The config is valid.
355
1472
                    }
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
1472
        Ok(RestrictedDiscoveryConfig {
397
1472
            enabled,
398
1472
            key_dirs,
399
1472
            static_keys,
400
1472
            watch_configuration,
401
1472
        })
402
1478
    }
403
}
404

            
405
#[cfg(test)]
406
mod 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
}