1
//! Parsing implementation for networkstatus documents.
2
//!
3
//! In Tor, a networkstatus documents describes a complete view of the
4
//! relays in the network: how many there are, how to contact them,
5
//! and so forth.
6
//!
7
//! A networkstatus document can either be a "votes" -- an authority's
8
//! view of the network, used as input to the voting process -- or a
9
//! "consensus" -- a combined view of the network based on multiple
10
//! authorities' votes, and signed by multiple authorities.
11
//!
12
//! A consensus document can itself come in two different flavors: a
13
//! plain (unflavoured) consensus has references to router descriptors, and
14
//! a "microdesc"-flavored consensus ("md") has references to
15
//! microdescriptors.
16
//!
17
//! To keep an up-to-date view of the network, clients download
18
//! microdescriptor-flavored consensuses periodically, and then
19
//! download whatever microdescriptors the consensus lists that the
20
//! client doesn't already have.
21
//!
22
//! For full information about the network status format, see
23
//! [dir-spec.txt](https://spec.torproject.org/dir-spec).
24
//!
25
//! # Limitations
26
//!
27
//! NOTE: The consensus format has changes time, using a
28
//! "consensus-method" mechanism.  This module is does not yet handle all
29
//! all historical consensus-methods.
30
//!
31
//! NOTE: This module _does_ parse some fields that are not in current
32
//! use, like relay nicknames, and the "published" times on
33
//! microdescriptors. We should probably decide whether we actually
34
//! want to do this.
35
//!
36
//! TODO: This module doesn't implement vote parsing at all yet.
37
//!
38
//! TODO: This module doesn't implement plain consensuses.
39
//!
40
//! TODO: We need an object safe trait that combines the common operations found
41
//! on netstatus documents, so we can store one in a `Box<dyn CommonNs>` or
42
//! something similar; otherwise interfacing applications have a hard time to
43
//! process netstatus documents in a flavor agnostic fashion.
44
//!
45
//! TODO: More testing is needed!
46
//!
47
//! TODO: There should be accessor functions for most of the fields here.
48
//! As with the other tor-netdoc types, I'm deferring those till I know what
49
//! they should be.
50

            
51
mod dir_source;
52
mod rs;
53

            
54
pub mod md;
55
pub mod plain;
56
pub mod vote;
57

            
58
#[cfg(feature = "build_docs")]
59
mod build;
60

            
61
pub use proto_statuses_parse2_encode::ProtoStatusesNetdocParseAccumulator;
62

            
63
use crate::doc::authcert::EncodedAuthCert;
64

            
65
use crate::doc::authcert::{self, AuthCert, AuthCertKeyIds, AuthCertUnverified};
66
use crate::encode::{
67
    EncodeOrd, ItemArgument, ItemEncoder, ItemValueEncodable, NetdocEncodable, NetdocEncoder,
68
};
69
use crate::parse::keyword::Keyword;
70
use crate::parse::parser::{Section, SectionRules, SectionRulesBuilder};
71
use crate::parse::tokenize::{Item, ItemResult, NetDocReader};
72
use crate::parse2::{
73
    self, ArgumentError, ArgumentStream, ErrorProblem, IsStructural, ItemArgumentParseable,
74
    ItemStream, ItemValueParseable, KeywordRef, NetdocParseable, NetdocParseableUnverified,
75
    SignatureHashInputs, SignatureItemParseable, StopAt, UnparsedItem, VerifyFailed,
76
};
77
use crate::types::relay_flags::{self, DocRelayFlags};
78
use crate::types::{self, *};
79
use crate::util::PeekableIterator;
80
use crate::{Error, KeywordEncodable, NetdocErrorKind as EK, NormalItemArgument, Pos};
81
use std::collections::{BTreeSet, HashMap, HashSet};
82
use std::fmt::{self, Display};
83
use std::slice;
84
use std::str::FromStr;
85
use std::sync::Arc;
86
use std::time::{self, SystemTime};
87
use std::{net, result};
88
use tor_basic_utils::iter_join;
89
use tor_error::{Bug, HasKind, bad_api_usage, internal};
90
use tor_protover::Protocols;
91
use void::ResultVoidExt as _;
92

            
93
use derive_deftly::{Deftly, define_derive_deftly};
94
use digest::Digest;
95
use educe::Educe;
96
use itertools::Itertools;
97
use saturating_time::SaturatingTime as _;
98
use std::sync::LazyLock;
99
use tor_checkable::{ExternallySigned, TimeBound, timed::TimeRangeBound};
100
use tor_llcrypto as ll;
101
use tor_llcrypto::pk::rsa::RsaIdentity;
102

            
103
use serde::{Deserialize, Deserializer};
104

            
105
#[cfg(feature = "build_docs")]
106
pub use build::MdConsensusBuilder;
107
#[cfg(feature = "build_docs")]
108
pub use build::PlainConsensusBuilder;
109
#[cfg(feature = "build_docs")]
110
ns_export_each_flavor! {
111
    ty: RouterStatusBuilder;
112
}
113

            
114
ns_export_each_variety! {
115
    ty: Footer, RouterStatus, Preamble;
116
}
117

            
118
#[deprecated]
119
pub use PlainConsensus as NsConsensus;
120
#[deprecated]
121
pub use PlainRouterStatus as NsRouterStatus;
122
#[deprecated]
123
pub use UncheckedPlainConsensus as UncheckedNsConsensus;
124
#[deprecated]
125
pub use UnvalidatedPlainConsensus as UnvalidatedNsConsensus;
126

            
127
pub use rs::{RouterStatusMdDigestsVote, SoftwareVersion};
128

            
129
pub use dir_source::{
130
    ConsensusAuthoritySection, ConsensusAuthoritySectionConstructor, DirSource,
131
    DirSourceConstructor, SupersededAuthorityKey,
132
};
133

            
134
define_constant_string! {
135
    /// `network-status-version` version value
136
    ///
137
    /// This is the fixed string `3`.
138
    ///
139
    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:network-status-version>
140
    //
141
    // IMO this is nicer than the formulation with an enum.
142
    // In practice we are not going to support other versions with the same parsing approach;
143
    // probably not even with the same code.
144
    NetworkStatusVersion = "3";
145
}
146

            
147
define_constant_string! {
148
    /// The `status` value in a `vote-status` line in a consensus
149
    ///
150
    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:vote-status>
151
    VoteStatusConsensus = "consensus";
152
}
153

            
154
define_constant_string! {
155
    /// The `vote` value in a `vote-status` line in a vote
156
    ///
157
    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:vote-status>
158
    VoteStatusVote = "vote";
159
}
160

            
161
/// `publiscation` field in routerstatus entry intro item other than in votes
162
///
163
/// Two arguments which are both ignored.
164
/// This used to be an ISO8601 timestamp in anomalous two-argument format.
165
///
166
/// Nowadays, according to the spec, it can be a dummy value.
167
/// So it can be a unit type.
168
///
169
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:r>,
170
/// except in votes which use [`Iso8601TimeSp`] instead.
171
///
172
/// **Not the same as** the `published` item:
173
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:published>
174
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Default)]
175
#[allow(clippy::exhaustive_structs)]
176
pub struct IgnoredPublicationTimeSp;
177

            
178
/// The lifetime of a networkstatus document.
179
///
180
/// In a consensus, this type describes when the consensus may safely
181
/// be used.  In a vote, this type describes the proposed lifetime for a
182
/// consensus.
183
///
184
/// Aggregate of three netdoc preamble fields.
185
#[derive(Clone, Debug, PartialEq, Eq, Deftly)]
186
#[derive_deftly(Constructor, NetdocEncodableFields, NetdocParseableFields)]
187
#[derive_deftly(Lifetime)]
188
#[allow(clippy::exhaustive_structs)]
189
pub struct Lifetime {
190
    /// `valid-after` --- Time at which the document becomes valid
191
    ///
192
    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:published>
193
    ///
194
    /// (You might see a consensus a little while before this time,
195
    /// since voting tries to finish up before the.)
196
    #[deftly(constructor)]
197
    #[deftly(netdoc(single_arg))]
198
    pub valid_after: Iso8601TimeSp,
199
    /// `fresh-until` --- Time after which there is expected to be a better version
200
    /// of this consensus
201
    ///
202
    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:published>
203
    ///
204
    /// You can use the consensus after this time, but there is (or is
205
    /// supposed to be) a better one by this point.
206
    #[deftly(constructor)]
207
    #[deftly(netdoc(single_arg))]
208
    pub fresh_until: Iso8601TimeSp,
209
    /// `valid-until` --- Time after which this consensus is expired.
210
    ///
211
    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:published>
212
    ///
213
    /// You should try to get a better consensus after this time,
214
    /// though it's okay to keep using this one if no more recent one
215
    /// can be found.
216
    #[deftly(constructor)]
217
    #[deftly(netdoc(single_arg))]
218
    pub valid_until: Iso8601TimeSp,
219

            
220
    #[doc(hidden)]
221
    #[deftly(netdoc(skip))]
222
    pub __non_exhaustive: (),
223
}
224

            
225
define_derive_deftly! {
226
    /// Bespoke derive for `Lifetime`, for `new` and accessors
227
    Lifetime:
228

            
229
    ${defcond FIELD not(approx_equal($fname, __non_exhaustive))}
230

            
231
    impl Lifetime {
232
        /// Construct a new Lifetime.
233
14690
        pub fn new(
234
14690
            $( ${when FIELD} $fname: time::SystemTime, )
235
14690
        ) -> crate::Result<Self> {
236
            // Make this now because otherwise literal `valid_after` here in the body
237
            // has the wrong span - the compiler refuses to look at the argument.
238
            // But we can refer to the field names.
239
            let self_ = Lifetime {
240
                $( ${when FIELD} $fname: $fname.into(), )
241
                __non_exhaustive: (),
242
            };
243
            if self_.valid_after < self_.fresh_until && self_.fresh_until < self_.valid_until {
244
                Ok(self_)
245
            } else {
246
                Err(EK::InvalidLifetime.err())
247
            }
248
        }
249
      $(
250
        ${when FIELD}
251

            
252
        ${fattrs doc}
253
148718
        pub fn $fname(&self) -> time::SystemTime {
254
            *self.$fname
255
        }
256
      )
257
        /// Return true if this consensus is officially valid at the provided time.
258
530
        pub fn valid_at(&self, when: time::SystemTime) -> bool {
259
            *self.valid_after <= when && when <= *self.valid_until
260
        }
261

            
262
        /// Return the voting period implied by this lifetime.
263
        ///
264
        /// (The "voting period" is the amount of time in between when a consensus first
265
        /// becomes valid, and when the next consensus is expected to become valid)
266
54908
        pub fn voting_period(&self) -> time::Duration {
267
            let valid_after = self.valid_after();
268
            let fresh_until = self.fresh_until();
269
            fresh_until
270
                .duration_since(valid_after)
271
                .expect("Mis-formed lifetime")
272
        }
273
    }
274
}
275
use derive_deftly_template_Lifetime;
276

            
277
/// A single consensus method
278
///
279
/// These are integers, but we don't do arithmetic on them.
280
///
281
/// As defined here:
282
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:consensus-methods>
283
/// <https://spec.torproject.org/dir-spec/computing-consensus.html#flavor:microdesc>
284
///
285
/// As used in a `consensus-method` item:
286
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:consensus-method>
287
#[derive(Debug, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Copy)] //
288
#[derive(derive_more::From, derive_more::Into, derive_more::Display, derive_more::FromStr)]
289
#[allow(clippy::exhaustive_structs)] // we're v unlikely to want to change this to u16 or u64
290
pub struct ConsensusMethod(pub u32);
291
impl NormalItemArgument for ConsensusMethod {}
292

            
293
/// A set of consensus methods
294
///
295
/// Implements `ItemValueParseable` as required for `consensus-methods`,
296
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:consensus-methods>
297
///
298
/// There is also [`consensus_methods_comma_separated`] for `m` lines in votes.
299
#[derive(Debug, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Deftly)]
300
#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
301
#[non_exhaustive]
302
pub struct ConsensusMethods {
303
    /// Consensus methods.
304
    pub methods: BTreeSet<ConsensusMethod>,
305
}
306

            
307
/// Module for use with parse2's `with`, to parse one argument of comma-separated consensus methods
308
///
309
/// As found in an `m` item in a vote:
310
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:m>
311
pub mod consensus_methods_comma_separated {
312
    use super::*;
313
    use parse2::ArgumentError as AE;
314
    use std::result::Result;
315

            
316
    /// Parse
317
469
    pub fn from_args<'s>(args: &mut ArgumentStream<'s>) -> Result<ConsensusMethods, AE> {
318
469
        let mut methods = BTreeSet::new();
319
1204
        for ent in args.next().ok_or(AE::Missing)?.split(',') {
320
1204
            let ent = ent.parse().map_err(|_| AE::Invalid)?;
321
1204
            if !methods.insert(ent) {
322
                return Err(AE::Invalid);
323
1204
            }
324
        }
325
469
        Ok(ConsensusMethods { methods })
326
469
    }
327

            
328
    /// Encode
329
15
    pub fn write_arg_onto(self_: &ConsensusMethods, out: &mut ItemEncoder) -> Result<(), Bug> {
330
15
        out.args_raw_string(&iter_join(",", &self_.methods));
331
15
        Ok(())
332
15
    }
333
}
334

            
335
/// Contents of a `voting-delay` item in the network status preamble
336
///
337
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:voting-delay>
338
#[derive(Debug, Clone, Eq, PartialEq, Hash, Copy, Deftly)]
339
#[derive_deftly(Constructor, ItemValueEncodable, ItemValueParseable)]
340
#[allow(clippy::exhaustive_structs)]
341
pub struct VotingDelay {
342
    /// VoteSeconds
343
    #[deftly(constructor)]
344
    pub vote_seconds: u32,
345

            
346
    /// DistSeconds
347
    #[deftly(constructor)]
348
    pub dist_seconds: u32,
349

            
350
    #[doc(hidden)]
351
    #[deftly(netdoc(skip))]
352
    pub __non_exhaustive: (),
353
}
354

            
355
/// A set of named network parameters.
356
///
357
/// These are used to describe current settings for the Tor network,
358
/// current weighting parameters for path selection, and so on.  They're
359
/// encoded with a space-separated K=V format.
360
///
361
/// A `NetParams<i32>` is part of the validated directory manager configuration,
362
/// where it is built (in the builder-pattern sense) from a transparent HashMap.
363
///
364
/// As found in `params` in a network status:
365
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:params>
366
///
367
/// The same syntax is also used, and this type used for parsing, in various other places,
368
/// for example routerstatus entry `w` items (bandwidth weights):
369
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:w>
370
//
371
// TODO DIRAUTH torspec#401 Replace `String` with a suitable newtype
372
// Currently:
373
//  - Our parser allows any keyword that makes it into a netdoc argument,
374
//    but it splits on the *first* `=` so a `NetParams<i32>` cannot parse a keyword with a `=`.
375
//  - We provide constructors that allow any `String`, even ones containing space, `=`,
376
//    newline, etc.
377
//  - Encoding throws `Bug` if the resulting document will be clearly garbage,
378
//    forbidding `=`, whitespace, and controls.  If the supplied keywords are bizarre,
379
//    it may generate surprising documents (eg, containing exciting Unicode).
380
#[derive(Debug, Clone, Educe, Eq, PartialEq)]
381
#[educe(Default)]
382
pub struct NetParams<T> {
383
    /// Map from keys to values.
384
    params: HashMap<String, T>,
385
}
386

            
387
impl<T> NetParams<T> {
388
    /// Create a new empty list of NetParams.
389
    #[allow(unused)]
390
29790
    pub fn new() -> Self {
391
29790
        NetParams {
392
29790
            params: HashMap::new(),
393
29790
        }
394
29790
    }
395
    /// Retrieve a given network parameter, if it is present.
396
187091
    pub fn get<A: AsRef<str>>(&self, v: A) -> Option<&T> {
397
187091
        self.params.get(v.as_ref())
398
187091
    }
399
    /// Return an iterator over all key value pairs in an arbitrary order.
400
24866
    pub fn iter(&self) -> impl Iterator<Item = (&String, &T)> {
401
24866
        self.params.iter()
402
24866
    }
403
    /// Set or replace the value of a network parameter.
404
9921
    pub fn set(&mut self, k: String, v: T) {
405
9921
        self.params.insert(k, v);
406
9921
    }
407
}
408

            
409
impl<K: Into<String>, T> FromIterator<(K, T)> for NetParams<T> {
410
6635
    fn from_iter<I: IntoIterator<Item = (K, T)>>(i: I) -> Self {
411
        NetParams {
412
6723
            params: i.into_iter().map(|(k, v)| (k.into(), v)).collect(),
413
        }
414
6635
    }
415
}
416

            
417
impl<T> std::iter::Extend<(String, T)> for NetParams<T> {
418
3633
    fn extend<I: IntoIterator<Item = (String, T)>>(&mut self, iter: I) {
419
3633
        self.params.extend(iter);
420
3633
    }
421
}
422

            
423
impl<'de, T> Deserialize<'de> for NetParams<T>
424
where
425
    T: Deserialize<'de>,
426
{
427
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
428
    where
429
        D: Deserializer<'de>,
430
    {
431
        let params = HashMap::deserialize(deserializer)?;
432
        Ok(NetParams { params })
433
    }
434
}
435

            
436
/// A list of subprotocol versions that implementors should/must provide.
437
///
438
/// This struct represents a pair of (optional) items:
439
/// `recommended-FOO-protocols` and `required-FOO-protocols`.
440
///
441
/// Each consensus has two of these: one for relays, and one for clients.
442
///
443
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:required-relay-protocols>
444
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
445
pub struct ProtoStatus {
446
    /// Set of protocols that are recommended; if we're missing a protocol
447
    /// in this list we should warn the user.
448
    ///
449
    /// `recommended-client-protocols` or `recommended-relay-protocols`
450
    recommended: Protocols,
451
    /// Set of protocols that are required; if we're missing a protocol
452
    /// in this list we should refuse to start.
453
    ///
454
    /// `required-client-protocols` or `required-relay-protocols`
455
    required: Protocols,
456
}
457

            
458
impl ProtoStatus {
459
    /// Check whether the list of supported protocols
460
    /// is sufficient to satisfy this list of recommendations and requirements.
461
    ///
462
    /// If any required protocol is missing, returns [`ProtocolSupportError::MissingRequired`].
463
    ///
464
    /// Otherwise, if no required protocol is missing, but some recommended protocol is missing,
465
    /// returns [`ProtocolSupportError::MissingRecommended`].
466
    ///
467
    /// Otherwise, if no recommended or required protocol is missing, returns `Ok(())`.
468
162
    pub fn check_protocols(
469
162
        &self,
470
162
        supported_protocols: &Protocols,
471
162
    ) -> Result<(), ProtocolSupportError> {
472
        // Required protocols take precedence, so we check them first.
473
162
        let missing_required = self.required.difference(supported_protocols);
474
162
        if !missing_required.is_empty() {
475
54
            return Err(ProtocolSupportError::MissingRequired(missing_required));
476
108
        }
477
108
        let missing_recommended = self.recommended.difference(supported_protocols);
478
108
        if !missing_recommended.is_empty() {
479
54
            return Err(ProtocolSupportError::MissingRecommended(
480
54
                missing_recommended,
481
54
            ));
482
54
        }
483

            
484
54
        Ok(())
485
162
    }
486
}
487

            
488
/// A subprotocol that is recommended or required in the consensus was not present.
489
#[derive(Clone, Debug, thiserror::Error)]
490
#[cfg_attr(test, derive(PartialEq))]
491
#[non_exhaustive]
492
pub enum ProtocolSupportError {
493
    /// At least one required protocol was not in our list of supported protocols.
494
    #[error("Required protocols are not implemented: {0}")]
495
    MissingRequired(Protocols),
496

            
497
    /// At least one recommended protocol was not in our list of supported protocols.
498
    ///
499
    /// Also implies that no _required_ protocols were missing.
500
    #[error("Recommended protocols are not implemented: {0}")]
501
    MissingRecommended(Protocols),
502
}
503

            
504
impl ProtocolSupportError {
505
    /// Return true if the suggested behavior for this error is a shutdown.
506
    pub fn should_shutdown(&self) -> bool {
507
        matches!(self, Self::MissingRequired(_))
508
    }
509
}
510

            
511
impl HasKind for ProtocolSupportError {
512
    fn kind(&self) -> tor_error::ErrorKind {
513
        tor_error::ErrorKind::SoftwareDeprecated
514
    }
515
}
516

            
517
/// A set of recommended and required protocols when running
518
/// in various scenarios.
519
///
520
/// Represents the collection of four items: `{recommended,required}-{client,relay}-protocols`.
521
///
522
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:required-relay-protocols>
523
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
524
pub struct ProtoStatuses {
525
    /// Lists of recommended and required subprotocol versions for clients
526
    client: ProtoStatus,
527
    /// Lists of recommended and required subprotocol versions for relays
528
    relay: ProtoStatus,
529
}
530

            
531
impl ProtoStatuses {
532
    /// Return the list of recommended and required protocols for running as a client.
533
159
    pub fn client(&self) -> &ProtoStatus {
534
159
        &self.client
535
159
    }
536

            
537
    /// Return the list of recommended and required protocols for running as a relay.
538
    pub fn relay(&self) -> &ProtoStatus {
539
        &self.relay
540
    }
541
}
542

            
543
/// List of recommended Tor versions
544
///
545
/// As seen in `client-versions` and `server-versions` in the preamble.
546
///
547
/// Technically these are supposed to be as according to
548
/// "`version-spec.txt`" but we actually allow anything that doesn't contain commas.
549
///
550
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:client-versions>
551
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:server-versions>
552
///
553
/// An empty set means no information, not no recommended versions.
554
//
555
// TODO should we have a CommaSeparated<T> type for arguments like this?
556
// But maybe we wouldn't be able to use it here anyway because of
557
// the special handling of the missing value.
558
//
559
// This is yet a third version number representation in arti!  Here it's just String.
560
// TODO unify RecommendedTorVersions, RelayPlatform, TorVersion
561
// When this is fixed, remove the workaround in netstatus::test::roundtrip_netstatus
562
#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)] //
563
#[derive(derive_more::Deref, derive_more::Into)]
564
pub struct RecommendedTorVersions(BTreeSet<String>);
565

            
566
/// Erroneous "recommended Tor versions" information
567
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
568
#[non_exhaustive]
569
pub enum InvalidRecommendedTorVersions {
570
    /// Identical version appears twice
571
    #[error("version {_0:?} contains whitespace")]
572
    ContainsWhitespace(String),
573

            
574
    /// Identical version appears twice
575
    #[error("version {_0:?} is repeated")]
576
    Repeated(String),
577
}
578

            
579
impl RecommendedTorVersions {
580
    /// Return a `RecommendedTorVersions` that has no information
581
    pub fn new_unknown() -> Self {
582
        Self::default()
583
    }
584

            
585
    /// Does this `RecommendedTorVersions` have any information?
586
    ///
587
    /// Ie, is it not empty.
588
    ///
589
    /// The opposite of [`BTreeSet::is_empty()`] (which available via deref).
590
9540
    pub fn is_known(&self) -> bool {
591
9540
        !self.is_empty()
592
9540
    }
593

            
594
    /// Construct a RecommendedTorVersions from a list of strings
595
    #[allow(clippy::should_implement_trait)] // we can't due to coherence
596
88976
    pub fn from_iter<I, S>(i: I) -> Result<Self, InvalidRecommendedTorVersions>
597
88976
    where
598
88976
        I: IntoIterator<Item = S>,
599
88976
        S: AsRef<str>,
600
    {
601
88976
        let mut set = BTreeSet::new();
602
89060
        for v in i {
603
65662
            let v = v.as_ref();
604
65662
            if v.is_empty() {
605
63834
                continue;
606
1828
            }
607
13936
            if v.chars().any(|c| c.is_whitespace()) {
608
                return Err(InvalidRecommendedTorVersions::ContainsWhitespace(
609
                    v.to_owned(),
610
                ));
611
1828
            }
612
1828
            if !set.insert(v.to_owned()) {
613
                return Err(InvalidRecommendedTorVersions::Repeated(v.to_owned()));
614
1828
            }
615
        }
616
88976
        Ok(RecommendedTorVersions(set))
617
88976
    }
618
}
619

            
620
impl FromStr for RecommendedTorVersions {
621
    type Err = InvalidRecommendedTorVersions;
622
64058
    fn from_str(s: &str) -> Result<Self, InvalidRecommendedTorVersions> {
623
64058
        Self::from_iter(s.split(','))
624
64058
    }
625
}
626

            
627
impl Display for RecommendedTorVersions {
628
12
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
629
12
        write!(f, "{}", iter_join(",", &self.0))
630
12
    }
631
}
632

            
633
impl NormalItemArgument for RecommendedTorVersions {}
634

            
635
impl ItemValueEncodable for RecommendedTorVersions {
636
12
    fn write_item_value_onto(&self, mut out: ItemEncoder) -> Result<(), Bug> {
637
12
        out.args_raw_string(self);
638
12
        Ok(())
639
12
    }
640
}
641

            
642
impl ItemValueParseable for RecommendedTorVersions {
643
63302
    fn from_unparsed(mut item: UnparsedItem) -> Result<Self, ErrorProblem> {
644
        const FIELD: &str = "versions";
645
63302
        item.check_no_object()?;
646
63302
        let args = item.args_mut();
647
63302
        let arg = args.next().unwrap_or("");
648
63302
        arg.parse::<Self>()
649
63302
            .map_err(|_| args.handle_error(FIELD, ArgumentError::Invalid))
650
63302
    }
651
}
652

            
653
/// A recognized 'flavor' of consensus document.
654
///
655
/// The enum is exhaustive because the addition/removal of a consensus flavor
656
/// should indeed be a breaking change, as it would inevitable require
657
/// interfacing code to think about the handling of it.
658
///
659
/// <https://spec.torproject.org/dir-spec/computing-consensus.html#flavors>
660
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
661
#[allow(clippy::exhaustive_enums)]
662
pub enum ConsensusFlavor {
663
    /// A "microdesc"-flavored consensus.  This is the one that
664
    /// clients and relays use today.
665
    Microdesc,
666
    /// A "networkstatus"-flavored consensus.  It's used for
667
    /// historical and network-health purposes.  Instead of listing
668
    /// microdescriptor digests, it lists digests of full relay
669
    /// descriptors.
670
    Plain,
671
}
672

            
673
impl ConsensusFlavor {
674
    /// Return the name of this consensus flavor.
675
17437
    pub fn name(&self) -> &'static str {
676
17437
        match self {
677
11713
            ConsensusFlavor::Plain => "ns", // spec bug, now baked in
678
5724
            ConsensusFlavor::Microdesc => "microdesc",
679
        }
680
17437
    }
681
    /// Try to find the flavor whose name is `name`.
682
    ///
683
    /// For historical reasons, an unnamed flavor indicates an "Plain"
684
    /// document.
685
378
    pub fn from_opt_name(name: Option<&str>) -> crate::Result<Self> {
686
378
        match name {
687
377
            Some("microdesc") => Ok(ConsensusFlavor::Microdesc),
688
1
            Some("ns") | None => Ok(ConsensusFlavor::Plain),
689
            Some(other) => {
690
                Err(EK::BadDocumentType.with_msg(format!("unrecognized flavor {:?}", other)))
691
            }
692
        }
693
378
    }
694
}
695

            
696
define_derive_deftly! {
697
    /// Bespoke derives applied to [`DirectorySignatureHashAlgo`]
698
    ///
699
    /// Generates:
700
    ///
701
    ///  * [`DirectorySignaturesHashesAccu`]
702
    ///  * [`DirectorySignaturesHashesAccu::update_from`]
703
    ///  * [`DirectorySignaturesHashesAccu::hash_slice_for_verification`]
704
    DirectorySignaturesHashesAccu:
705

            
706
    ${define FNAME ${paste ${snake_case $vname}} }
707

            
708
    /// `directory-signature`a hash algorithm argument
709
    #[derive(Clone, Copy, Default, Debug, Eq, PartialEq, Deftly)]
710
    #[derive_deftly(AsMutSelf)]
711
    #[non_exhaustive]
712
    pub struct DirectorySignaturesHashesAccu {
713
      $(
714
        ${vattrs doc}
715
        pub $FNAME: Option<[u8; ${vmeta(hash_len) as expr}]>,
716
      )
717
    }
718

            
719
    impl DirectorySignaturesHashesAccu {
720
        /// Calculate the hash for a signature item and update this accumulator
721
126727
        fn update_from(
722
            &mut self,
723
            algo: &DigestAlgoInSignature,
724
            body: &SignatureHashInputs,
725
        ) {
726
            // Update the hash in self.$UPDATE according to algorithm $AGLO
727
            // (uses dynamic bindings of those parameters)
728
            ${define HASH {
729
                // Avoid recalculating if we don't need to
730
31654
                self.$UPDATE.get_or_insert_with(|| {
731
                    let mut h = tor_llcrypto::d::$ALGO::new();
732
                    h.update(body.body().body());
733
                    h.update(body.signature_item_kw_spc);
734
                    h.finalize().into()
735
                });
736
            }}
737

            
738
            match &**algo {
739
              $(
740
                Some(KeywordOrString::Known($vtype)) => {
741
                    ${define UPDATE $FNAME}
742
                    ${define ALGO $vname}
743
                    $HASH
744
                }
745
              )
746
                None => {
747
                    ${define UPDATE sha1}
748
                    ${define ALGO Sha1}
749
                    $HASH
750
                }
751
                Some(KeywordOrString::Unknown(..)) => {}
752
            }
753
        }
754

            
755
        /// Return the hash value for a specific algorithm, as a slice
756
        ///
757
        /// `None` if the value wasn't computed.
758
        /// That shouldn't happen.
759
2147
        fn hash_slice_for_verification(
760
2147
            &self,
761
2147
            algo: &DigestAlgoInSignature,
762
2147
        ) -> Option<&[u8]> {
763
            match &**algo {
764
              $(
765
                Some(KeywordOrString::Known($vtype)) => Some(self.$FNAME.as_ref()?),
766
              )
767
                None => Some(self.sha1.as_ref()?),
768
                Some(KeywordOrString::Unknown(..)) => None,
769
            }
770
        }
771
    }
772
}
773

            
774
/// `directory-signature` hash algorithm argument
775
#[derive(Clone, Copy, Debug, Eq, PartialEq, strum::Display, strum::EnumString, Deftly)]
776
#[derive_deftly(DirectorySignaturesHashesAccu)]
777
#[non_exhaustive]
778
#[strum(serialize_all = "snake_case")]
779
pub enum DirectorySignatureHashAlgo {
780
    /// SHA-1
781
    #[deftly(hash_len = "20")]
782
    Sha1,
783
    /// SHA-256
784
    #[deftly(hash_len = "32")]
785
    Sha256,
786
}
787

            
788
/// `algorithm` field in a `directory-signature` item
789
///
790
/// This is extremely bizarre: it's an *optional item at the start of the arguments*!
791
// TODO SPEC #350
792
///
793
/// So we parse it with some kind of nightmarish lookahead.
794
///
795
/// Additionally, to be able to convey the signatures accurately, without breaking them,
796
/// we must remember whether the argument was present.
797
///
798
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:directory-signature>
799
#[derive(Debug, Clone, derive_more::Deref, derive_more::DerefMut)]
800
#[allow(clippy::exhaustive_structs)]
801
pub struct DigestAlgoInSignature(pub Option<KeywordOrString<DirectorySignatureHashAlgo>>);
802

            
803
impl ItemArgumentParseable for DigestAlgoInSignature {
804
126727
    fn from_args<'s>(args: &mut ArgumentStream<'s>) -> Result<Self, ArgumentError> {
805
126727
        let v = if args
806
126727
            .clone()
807
126727
            .next()
808
            // Treat it as a fingerprint if it doesn't have any non-hex characters
809
            // (including lowercase ones).  If we reuse this item for new algorithms
810
            // they should have at least one letter g-z in their name.
811
2695150
            .and_then(|s| s.chars().all(|c| c.is_ascii_hexdigit()).then_some(()))
812
126727
            .is_some()
813
        {
814
            // next argument looks enough like a fingerprint that we don't treat as an algo name
815
65857
            None
816
        } else {
817
60870
            Some(KeywordOrString::from_args(args)?)
818
        };
819
126727
        Ok(DigestAlgoInSignature(v))
820
126727
    }
821
}
822
impl ItemArgument for DigestAlgoInSignature {
823
28
    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
824
28
        if let Some(y) = &self.0 {
825
13
            y.write_arg_onto(out)?;
826
15
        }
827
28
        Ok(())
828
28
    }
829
}
830
impl DigestAlgoInSignature {
831
    /// Return the actual algorithm
832
    ///
833
    /// This handles the defaulting, where an absent argument means `sha1`.
834
    pub fn algorithm(&self) -> &KeywordOrString<DirectorySignatureHashAlgo> {
835
        self.as_ref()
836
            .unwrap_or(&KeywordOrString::Known(DirectorySignatureHashAlgo::Sha1))
837
    }
838
}
839

            
840
impl NormalItemArgument for DirectorySignatureHashAlgo {}
841

            
842
/// The signature of a single directory authority on a networkstatus document.
843
///
844
/// Implements `ItemValueParseable` which parses without hashing anything;
845
/// this is mostly useful for use by the `SignatureItemParseable` implementation.
846
#[derive(Debug, Clone, Deftly)]
847
#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
848
#[non_exhaustive]
849
pub struct Signature {
850
    /// The name of the digest algorithm used to make the signature.
851
    ///
852
    /// Currently sha1 and sh256 are recognized.  Here we only support
853
    /// sha256.
854
    pub digest_algo: DigestAlgoInSignature,
855
    /// Fingerprints of the keys for the authority that made
856
    /// this signature.
857
    #[deftly(netdoc(with = authcert::keyids_directory_signature_args))]
858
    pub key_ids: AuthCertKeyIds,
859
    /// The signature itself.
860
    #[deftly(netdoc(object(label = "SIGNATURE"), with = types::raw_data_object))]
861
    pub signature: Vec<u8>,
862
}
863

            
864
impl SignatureItemParseable for Signature {
865
    type HashAccu = DirectorySignaturesHashesAccu;
866

            
867
126727
    fn from_unparsed_and_body(
868
126727
        item: UnparsedItem,
869
126727
        body: &SignatureHashInputs<'_>,
870
126727
        hash: &mut Self::HashAccu,
871
126727
    ) -> Result<Self, ErrorProblem> {
872
126727
        let signature = Signature::from_unparsed(item)?;
873
126727
        hash.update_from(&signature.digest_algo, body);
874
126727
        Ok(signature)
875
126727
    }
876
}
877

            
878
/// A collection of signatures that can be checked on a networkstatus document
879
///
880
/// This is derived from the signatures section of a netstatus,
881
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#section:signature>,
882
/// but it is not isomorphic to it, and is not directly parseable.
883
#[derive(Debug, Clone)]
884
#[non_exhaustive]
885
pub struct SignatureGroup {
886
    /// The document hashes of the signed part of the document
887
    ///
888
    /// The pre-parse2 parser always sets `hashes.sha1` and `hashes.sha1_unnamed`
889
    /// to the same value, which is wrong. which is
890
    /// [bug #2530](https://gitlab.torproject.org/tpo/core/arti/-/work_items/2530)
891
    pub hashes: DirectorySignaturesHashesAccu,
892
    /// The signatures listed on the document.
893
    pub signatures: Vec<Signature>,
894
}
895

            
896
/// Error which will prevent us from attempting to verify signatures on a consensus
897
///
898
/// This error occurs if we the consensus isn't signed by the right people,
899
/// or we are lacking authcerts.
900
///
901
/// Does not represent actual verification errors.
902
/// Those show up as `VerifyFailed`, typically [`ConsensusVerifyFailed::InvalidSignature`].
903
///
904
/// Can be converted to a `VerifyFailed`,
905
/// giving [`InsufficientTrustedSigners`](VerifyFailed::InsufficientTrustedSigners).
906
#[derive(Clone, Debug, thiserror::Error)]
907
#[allow(clippy::exhaustive_enums)]
908
pub enum ConsensusVerifiabilityError {
909
    /// Insufficient trusted signers
910
    #[error("consensus not signed by enough authorities")]
911
    InsufficientTrustedSigners,
912

            
913
    /// Insufficient trusted signers because we are missing authcerts
914
    #[error("missing auth certs mean we could not verify enough consensuis signatures (need at least {deficit} more, out of {} that are missing)", missing.len())]
915
    MissingAuthCerts {
916
        /// The number of additional useful authcerts that would be sufficient
917
        deficit: usize,
918
        /// All the authcerts that would be useful
919
        missing: HashSet<AuthCertKeyIds>,
920
    },
921
}
922

            
923
/// Error encountered while verifying a consensus
924
///
925
/// Thrown by
926
/// [`plain::NetworkStatusUnverified::verify`]
927
/// and
928
/// [`md::NetworkStatusUnverified::verify`].
929
///
930
/// Not used for problems with the validity period:
931
/// that's handled by `tor-checkable` and shows up as [`tor_checkable::TimeValidityError`].
932
///
933
/// Can be converted to a `VerifyFailed` (which, in effect, summarises the error).
934
#[derive(Clone, Debug, thiserror::Error)]
935
#[non_exhaustive]
936
pub enum ConsensusVerifyFailed {
937
    /// Certificates or signatures insufficient
938
    #[error("certs/sigs insufficient")]
939
    CertificationInsufficient(#[from] ConsensusVerifiabilityError),
940

            
941
    /// One or more signatures failed to verify
942
    #[error("invalid signature")]
943
    //
944
    // Not `#[from]` because we don't want to accidentally convert
945
    // ConsensusVerifiabilityError -> VerifyFailed -> ConsensusVerifyFailed
946
    // since that would give the wrong variant.
947
    InvalidSignature(#[source] VerifyFailed),
948
}
949

            
950
/// Error encountered while verifying a vote
951
///
952
/// Thrown by
953
/// [`vote::NetworkStatusUnverified::verify`].
954
///
955
/// Not used for problems with the validity period:
956
/// that's handled by `tor-checkable` and shows up as [`tor_checkable::TimeValidityError`].
957
///
958
/// Can be converted to a `VerifyFailed` (which, in effect, summarises the error).
959
#[derive(Clone, Debug, thiserror::Error)]
960
#[non_exhaustive]
961
pub enum VoteVerifyFailed {
962
    /// The document signature failed to verify
963
    #[error("invalid signature")]
964
    //
965
    // Not `#[from]` because we don't want to accidentally convert
966
    // VoteVerifyFailed::Something -> VerifyFailed -> VoteVerifyFailed
967
    // since that would give the wrong variant.
968
    InvalidSignature(#[source] VerifyFailed),
969

            
970
    /// Authcert couldn't be parsed
971
    #[error("unparseable authcert")]
972
    AuthCertParseError(#[source] parse2::ParseError),
973

            
974
    /// Authcert isn't valid for this vote's validity period
975
    #[error("authcert not valid for vote period")]
976
    AuthCertWrongValidity(#[source] tor_checkable::TimeValidityError),
977

            
978
    /// Authcert is for a different authority
979
    #[error("wrong authcert")]
980
    AuthCertWrongAuthority,
981
}
982

            
983
/// A shared random value produced by the directory authorities.
984
#[derive(
985
    Debug, Clone, Copy, Eq, PartialEq, derive_more::From, derive_more::Into, derive_more::AsRef,
986
)]
987
// (This doesn't need to use CtByteArray; we don't really need to compare these.)
988
pub struct SharedRandVal([u8; 32]);
989

            
990
/// A shared-random value produced by the directory authorities,
991
/// along with meta-information about that value.
992
#[derive(Debug, Clone, Deftly)]
993
#[non_exhaustive]
994
#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
995
pub struct SharedRandStatus {
996
    /// How many authorities revealed shares that contributed to this value.
997
    pub n_reveals: u8,
998
    /// The current random value.
999
    ///
    /// The properties of the secure shared-random system guarantee
    /// that this value isn't predictable before it first becomes
    /// live, and that a hostile party could not have forced it to
    /// have any more than a small number of possible random values.
    pub value: SharedRandVal,
    /// The time when this SharedRandVal becomes (or became) the latest.
    ///
    /// (This is added per proposal 342, assuming that gets accepted.)
    pub timestamp: Option<Iso8601TimeNoSp>,
}
/// The two shared random values, `shared-rand-*-value`
///
/// As found in the consensus preamble
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:shared-rand-current-value>
/// and a vote's authority section
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#authority-item-shared-rand-value>
#[derive(Debug, Clone, Default, Deftly)]
#[derive_deftly(Constructor, NetdocEncodableFields, NetdocParseableFields)]
#[allow(clippy::exhaustive_structs)]
pub struct SharedRandStatuses {
    /// Global shared-random value for the previous shared-random period.
    pub shared_rand_previous_value: Option<SharedRandStatus>,
    /// Global shared-random value for the current shared-random period.
    pub shared_rand_current_value: Option<SharedRandStatus>,
    #[doc(hidden)]
    #[deftly(netdoc(skip))]
    pub __non_exhaustive: (),
}
/// Relay weight information - `w` item in routerstatus in consensuses
///
/// This is a combination of two representations of (subsets of) the same information,
/// from an optional `w` in the document.
///
///  * [`effective`](RelayWeightsItem::effective):
///
///    Always contains the effective weight, as [`RelayWeight`].
///    This is what is used by clients.
///    It does not record whether a `w` line was actually present.
///
///  * [`params`](RelayWeightsItem::params):
///
///    Can represent the presence and whole contents of the `w` line,
///    including all the known and unknown parameters.
///    This is within [`Unknown`], so it is only present with crate `feature = "retain-unknown"`,
///    and only some constructors/parsers record it.
///
/// # Parsing
///
/// Parsing is done with `NetdocParseableFields` rather than `ItemValueParseable`.
/// The `params` are [`Retained`](Unknown::Retained) if `retain_unknown_values` is
/// selected in [`parse2::ParseOptions`].
//
// We use NetdocParseableFields because the containing document, RouterStatus,
// contains `RelayWeightsItem` rather than `Option<RelayWeightsItem>`.
// The item parsing multiplicity machinery would see plain `RelayWeightsItem` as a required item.
//
// This representation also means so that if retaining unknown information is compiled out
// (ie, in clients) each routerstatus entry stored in memory does not need to record
// whether `w` was present, merely what the implications were.
//
// We can't use ItemValueParseable with #[deftly(netdoc(default))]
// because `RelayWeightsItem::default()` is a RelayWeightsItem that definitively
// contains no pazrameters, ie with `Unknown::Retained`,
// and is therefore only conditionally available.
/// # Encoding
///
/// Encoding requires knowing whether a `w` line is to be included, and its contents,
/// so is implemented only with if `effective` is `Unknown::Retained`.
/// The encoding impl is only compiled in with `"retain-unknown"`,
/// and throws [`Bug`] if applied to a `RelayWeightsItem` whose `params` are `Discarded`.
///
/// # Constructors
///
/// An "empty" `RelayWeightsItem` can be constructed with [`RelayWeightsItem::new_no_info`].
///
/// A `RelayWeightsItem` containing only the effective `RelayWeight`
/// can be constructed using [`RelayWeightsItem::from_effective`].
///
/// With `"retain-unknown"`:
/// a `RelayWeightsItem` can be constructed from a [`NetParams<u32>`] using `TryFrom`;
/// and, implements `Default`, which yields a `RelayWeightsItem`
/// representing the (known) absence of a `w` line.
//
// Fields are private to maintain the invariant.
#[derive(Debug, Clone)]
pub struct RelayWeightsItem {
    /// The effective relay weight
    effective: RelayWeight,
    /// The complete parameter set, if available and `w` was present.
    params: Unknown<Option<NetParams<u32>>>,
}
/// Relay weight information - `w` item in routerstatus in votes
#[derive(Debug, Clone, Deftly)]
#[derive_deftly(NetdocEncodableFields, NetdocParseableFields)]
#[allow(clippy::exhaustive_structs)] // this is always just this single item
pub struct VoteRelayWeightsItem {
    /// The parameters, uninterpreted.
    pub w: Option<NetParams<u32>>,
}
/// Recognized weight fields on a single relay in a consensus
///
/// The part of a `w` item that we understand as a client.
#[non_exhaustive]
#[derive(Debug, Clone, Copy)]
pub enum RelayWeight {
    /// An unmeasured weight for a relay.
    Unmeasured(u32),
    /// An measured weight for a relay.
    Measured(u32),
}
/// Error processing a `w` line's netparams into an effective relay weight
#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
pub enum InvalidRelayWeights {
    /// Invalid value for `Unmeasured`
    #[error("invalid value for Unmeasured")]
    InvalidUnmeasured,
}
/// Authority entry in a consensus - deprecated compatibility type alias
#[deprecated = "renamed to ConsensusAuthorityEntry"]
pub type ConsensusVoterInfo = ConsensusAuthorityEntry;
/// Authority entry in a plain consensus - type alias provided for consistency
pub type PlainAuthorityEntry = ConsensusAuthorityEntry;
/// Authority entry in an md consensus - type alias provided for consistency
pub type MdAuthorityEntry = ConsensusAuthorityEntry;
/// An authority entry as found in a consensus
///
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#section:authority-entry>
///
/// See also [`VoteAuthorityEntry`]
//
// We don't use the `each_variety` system for this because:
//  1. That avoids separating the two consensus authority entry types, which are identical
//  2. The only common fields are `dir-source` and `contact`, so there is little duplication
#[derive(Debug, Clone, Deftly)]
#[derive_deftly(Constructor, NetdocEncodable, NetdocParseable)]
#[allow(clippy::exhaustive_structs)]
pub struct ConsensusAuthorityEntry {
    /// Contents of the `dir-source` line about an authority
    #[deftly(constructor)]
    pub dir_source: DirSource,
    /// Human-readable contact information about the authority
    //
    // If more non-intro fields get added that are the same in votes and cosensuses,
    // consider using each_variety.rs or breaking those fields out into
    // `AuthorityEntryCommon` implementing `NetdocParseableFields`, or something.
    #[deftly(constructor)]
    pub contact: ContactInfo,
    /// Digest of the vote that the authority cast to contribute to
    /// this consensus.
    ///
    /// This is not a fixed-length, fixed-algorithm field.
    /// Bizarrely, the algorithm is supposed to be inferred from the length!
    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:vote-digest>
    #[deftly(netdoc(single_arg))]
    #[deftly(constructor)]
    pub vote_digest: B16U,
    #[doc(hidden)]
    #[deftly(netdoc(skip))]
    pub __non_exhaustive: (),
}
/// An authority entry as found in a vote
///
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#section:authority-entry>
///
/// See also [`ConsensusAuthorityEntry`]
#[derive(Debug, Clone, Deftly)]
#[derive_deftly(Constructor, NetdocEncodable, NetdocParseable)]
#[allow(clippy::exhaustive_structs)]
pub struct VoteAuthorityEntry {
    /// Contents of the `dir-source` line about an authority
    #[deftly(constructor)]
    pub dir_source: DirSource,
    /// Human-readable contact information about the authority
    #[deftly(constructor)]
    pub contact: ContactInfo,
    /// `legacy-dir-key` - superseded authority identity key
    ///
    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:legacy-dir-key>
    #[deftly(netdoc(single_arg))]
    pub legacy_dir_key: Option<Fingerprint>,
    /// `shared-rand-participate` - Indicate shared random participation
    ///
    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:shared-rand-participate>
    pub shared_rand_participate: Option<SharedRandParticipate>,
    /// `shared-rand-commit` - Shared random commitment
    ///
    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:shared-rand-commit>
    pub shared_rand_commit: Vec<SharedRandCommit>,
    /// Global shared-random values
    #[deftly(netdoc(flatten))]
    pub shared_rand: SharedRandStatuses,
    #[doc(hidden)]
    #[deftly(netdoc(skip))]
    pub __non_exhaustive: (),
}
/// `shared-rand-participate` in a vote authority entry
///
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:shared-rand-participate>
//
// We could have done `shared_rand_participate: Option<()>` in VoteAuthorityEntry,
// but then we might end up with variables of type `&Option<()>` etc.
// whose meaning has been detached from its type.
//
// TODO DIRAUTH rework this according to the API design conclusion from !3977 when there is one
#[derive(Debug, Clone, Deftly)]
#[derive_deftly(Constructor, ItemValueEncodable, ItemValueParseable)]
#[allow(clippy::exhaustive_structs)]
pub struct SharedRandParticipate {
    #[doc(hidden)]
    #[deftly(netdoc(skip))]
    pub __non_exhaustive: (),
}
/// `shared-rand-commit` in a vote authority entry
///
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:shared-rand-commit>
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deftly)]
// If new protocols use this item with a different version, we'll call it an API break.
#[allow(clippy::exhaustive_enums)]
pub enum SharedRandCommit {
    /// Version 1, the only one supported
    V1(SharedRandCommitV1),
    /// Other versions.  Cannot be encoded.
    // It's not clear that future versions will use this version mechanism.  torspec#408.
    Unknown {},
}
/// `shared-rand-commit` in a vote authority entry
///
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:shared-rand-commit>
///
/// Version and hash are not explicitly represented.  See torspec#407.
///
/// `ItemValueEncodable` and `ItemValueParseable` impls do not include the fixed arguments;
/// in a netdoc, this type should be used within `SharedRandCommit::V1`.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deftly)]
#[derive_deftly(Constructor, ItemValueEncodable, ItemValueParseable)]
#[allow(clippy::exhaustive_structs)]
pub struct SharedRandCommitV1 {
    /// Authority id key, recapitulated.
    // TODO this field shouldn't here at all torspec#407
    #[deftly(constructor)]
    h_kp_auth_id_rsa: Fingerprint,
    /// Commitment
    ///
    /// `TIMESTAMP || SHA3_256(REVEAL)`, as per
    /// <https://spec.torproject.org/srv-spec/specification.html#COMMITREVEAL>
    //
    // TOOD we would like to replace this with a type that separates out the pieces!
    // But that would need a FixedB64 generic over some tor-bytes trait, or something.
    #[deftly(constructor)]
    commit: FixedB64<40>,
    /// Reveal
    ///
    /// `TIMESTAMP || random number`, as per
    /// <https://spec.torproject.org/srv-spec/specification.html#COMMITREVEAL>
    reveal: Option<FixedB64<40>>,
    #[doc(hidden)]
    #[deftly(netdoc(skip))]
    pub __non_exhaustive: (),
}
impl SharedRandCommitV1 {
    /// The fixed arguments that precede the actual value in `shared-rand-commit 1 ...`
    const FIXED_ARGUMENTS: &[&str] = &["1", "sha3-256"];
}
impl ItemValueEncodable for SharedRandCommit {
13
    fn write_item_value_onto(&self, mut out: ItemEncoder) -> Result<(), Bug> {
13
        match self {
13
            SharedRandCommit::V1(values) => {
26
                for fixed in SharedRandCommitV1::FIXED_ARGUMENTS {
26
                    out.args_raw_string(fixed);
26
                }
13
                values.write_item_value_onto(out)
            }
            SharedRandCommit::Unknown {} => Err(internal!("encoding SharedRandCommit::Unknown")),
        }
13
    }
}
impl ItemValueParseable for SharedRandCommit {
507
    fn from_unparsed(mut item: UnparsedItem<'_>) -> Result<Self, ErrorProblem> {
507
        let mut fixed = SharedRandCommitV1::FIXED_ARGUMENTS.iter().copied();
507
        let args = item.args_mut();
507
        let version = args
507
            .next()
507
            .ok_or_else(|| args.handle_error("version", ArgumentError::Missing))?;
507
        if version != fixed.next().expect("nonempty") {
            return Ok(SharedRandCommit::Unknown {});
507
        }
507
        for exp in fixed {
507
            let got = args
507
                .next()
507
                .ok_or_else(|| args.handle_error(exp, ArgumentError::Missing))?;
507
            if got != exp {
                Err(args.handle_error(exp, ArgumentError::Invalid))?;
507
            }
        }
507
        let values = SharedRandCommitV1::from_unparsed(item)?;
507
        Ok(SharedRandCommit::V1(values))
507
    }
}
// For `ConsensusAuthoritySection`, see `dir_source.rs`.
define_derive_deftly! {
    /// Ad-hoc derive, `impl NetdocParseable for VoteAuthoritySection`
    ///
    /// We can't derive from `VoteAuthoritySection` with the normal macros, because
    /// it's not a document, with its own intro item.  It's just a collection of sub-documents.
    /// The netdoc derive macros don't have support for that - and it would be a fairly
    /// confusing thing to support because you'd end up with nested multiplicities and a whole
    /// variety of "intro item keywords" that were keywords for arbitrary sub-documents.
    ///
    /// Instead, we do that ad-hoc here.  It's less confusing because we don't need to
    /// worry about multiplicity, and because we know what only the outer document is
    /// that will contain this.
    VoteAuthoritySection:
    ${defcond F_NORMAL not(fmeta(netdoc(skip)))}
    impl NetdocParseable for VoteAuthoritySection {
58
        fn doctype_for_error() -> &'static str {
            "vote.authority.section"
        }
8941
        fn is_intro_item_keyword(kw: KeywordRef<'_>) -> bool {
            VoteAuthorityEntry::is_intro_item_keyword(kw)
        }
385
        fn is_structural_keyword(kw: KeywordRef<'_>) -> Option<IsStructural> {
          $(
            ${when F_NORMAL}
            if let y @ Some(_) = $ftype::is_structural_keyword(kw) {
                return y;
            }
          )
            None
        }
58
        fn from_items<'s>(
58
            input: &mut ItemStream<'s>,
58
            stop_outer: stop_at!(),
58
        ) -> Result<Self, ErrorProblem> {
            let stop_inner = stop_outer
              $(
                ${when F_NORMAL}
                | StopAt($ftype::is_intro_item_keyword)
              )
            ;
            Ok(VoteAuthoritySection { $(
                ${when F_NORMAL}
                $fname: NetdocParseable::from_items(input, stop_inner)?,
            )
                __non_exhaustive: (),
            })
        }
    }
    impl NetdocEncodable for VoteAuthoritySection {
2
        fn encode_unsigned(&self, out: &mut NetdocEncoder) -> Result<(), Bug> {
          $(
            ${when F_NORMAL}
            self.$fname.encode_unsigned(out)?;
          )
          Ok(())
        }
    }
}
/// An authority section in a vote
///
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#section:authority>
//
// We have split this out to help encapsulate vote/consensus-specific
// information in a forthcoming overall network status document type.
#[derive(Deftly, Clone, Debug)]
#[derive_deftly(VoteAuthoritySection, Constructor)]
#[allow(clippy::exhaustive_structs)]
pub struct VoteAuthoritySection {
    /// Authority entry
    #[deftly(constructor)]
    pub authority: VoteAuthorityEntry,
    /// Authority key certificate
    #[deftly(constructor)]
    pub cert: EmbeddedCert<AuthCert, EncodedAuthCert>,
    #[doc(hidden)]
    #[deftly(netdoc(skip))]
    pub __non_exhaustive: (),
}
/// Fields in the footer of a consensus
///
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#section:footer>
///
/// Not the whole footer, because it lacks the `directory-footer` item.
#[derive(Debug, Clone, Deftly)]
#[derive_deftly(Constructor, NetdocEncodableFields, NetdocParseableFields)]
#[allow(clippy::exhaustive_structs)]
pub struct ConsensusFooterFields {
    /// `bandwidth-weights`
    ///
    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:bandwidth-weights>
    #[deftly(netdoc(default))]
    pub bandwidth_weights: NetParams<i32>,
    #[doc(hidden)]
    #[deftly(netdoc(skip))]
    pub __non_exhaustive: (),
}
/// A consensus document that lists relays along with their
/// microdescriptor documents.
pub type MdConsensus = md::Consensus;
/// An MdConsensus that has been parsed and checked for timeliness,
/// but not for signatures.
pub type UnvalidatedMdConsensus = md::UnvalidatedConsensus;
/// An MdConsensus that has been parsed but not checked for signatures
/// and timeliness.
pub type UncheckedMdConsensus = md::UncheckedConsensus;
/// A consensus document that lists relays along with their
/// router descriptor documents.
pub type PlainConsensus = plain::Consensus;
/// An PlainConsensus that has been parsed and checked for timeliness,
/// but not for signatures.
pub type UnvalidatedPlainConsensus = plain::UnvalidatedConsensus;
/// An PlainConsensus that has been parsed but not checked for signatures
/// and timeliness.
pub type UncheckedPlainConsensus = plain::UncheckedConsensus;
decl_keyword! {
    /// Keywords that can be used in votes and consensuses.
    // TODO: This is public because otherwise we can't use it in the
    // ParseRouterStatus crate.  But I'd rather find a way to make it
    // private.
    #[non_exhaustive]
    #[allow(missing_docs)]
    pub NetstatusKwd {
        // Header
        "network-status-version" => NETWORK_STATUS_VERSION,
        "vote-status" => VOTE_STATUS,
        "consensus-methods" => CONSENSUS_METHODS,
        "consensus-method" => CONSENSUS_METHOD,
        "published" => PUBLISHED,
        "valid-after" => VALID_AFTER,
        "fresh-until" => FRESH_UNTIL,
        "valid-until" => VALID_UNTIL,
        "voting-delay" => VOTING_DELAY,
        "client-versions" => CLIENT_VERSIONS,
        "server-versions" => SERVER_VERSIONS,
        "known-flags" => KNOWN_FLAGS,
        "flag-thresholds" => FLAG_THRESHOLDS,
        "recommended-client-protocols" => RECOMMENDED_CLIENT_PROTOCOLS,
        "required-client-protocols" => REQUIRED_CLIENT_PROTOCOLS,
        "recommended-relay-protocols" => RECOMMENDED_RELAY_PROTOCOLS,
        "required-relay-protocols" => REQUIRED_RELAY_PROTOCOLS,
        "params" => PARAMS,
        "bandwidth-file-headers" => BANDWIDTH_FILE_HEADERS,
        "bandwidth-file-digest" => BANDWIDTH_FILE_DIGEST,
        // "package" is now ignored.
        // header in consensus, voter section in vote?
        "shared-rand-previous-value" => SHARED_RAND_PREVIOUS_VALUE,
        "shared-rand-current-value" => SHARED_RAND_CURRENT_VALUE,
        // Voter section (both)
        "dir-source" => DIR_SOURCE,
        "contact" => CONTACT,
        // voter section (vote, but not consensus)
        "legacy-dir-key" => LEGACY_DIR_KEY,
        "shared-rand-participate" => SHARED_RAND_PARTICIPATE,
        "shared-rand-commit" => SHARED_RAND_COMMIT,
        // voter section (consensus, but not vote)
        "vote-digest" => VOTE_DIGEST,
        // voter cert beginning (but only the beginning)
        "dir-key-certificate-version" => DIR_KEY_CERTIFICATE_VERSION,
        // routerstatus
        "r" => RS_R,
        "a" => RS_A,
        "s" => RS_S,
        "v" => RS_V,
        "pr" => RS_PR,
        "w" => RS_W,
        "p" => RS_P,
        "m" => RS_M,
        "id" => RS_ID,
        // footer
        "directory-footer" => DIRECTORY_FOOTER,
        "bandwidth-weights" => BANDWIDTH_WEIGHTS,
        "directory-signature" => DIRECTORY_SIGNATURE,
    }
}
/// Shared parts of rules for all kinds of netstatus headers
54
static NS_HEADER_RULES_COMMON_: LazyLock<SectionRulesBuilder<NetstatusKwd>> = LazyLock::new(|| {
    use NetstatusKwd::*;
54
    let mut rules = SectionRules::builder();
54
    rules.add(NETWORK_STATUS_VERSION.rule().required().args(1..=2));
54
    rules.add(VOTE_STATUS.rule().required().args(1..));
54
    rules.add(VALID_AFTER.rule().required());
54
    rules.add(FRESH_UNTIL.rule().required());
54
    rules.add(VALID_UNTIL.rule().required());
54
    rules.add(VOTING_DELAY.rule().args(2..));
54
    rules.add(CLIENT_VERSIONS.rule());
54
    rules.add(SERVER_VERSIONS.rule());
54
    rules.add(KNOWN_FLAGS.rule().required());
54
    rules.add(RECOMMENDED_CLIENT_PROTOCOLS.rule().args(1..));
54
    rules.add(RECOMMENDED_RELAY_PROTOCOLS.rule().args(1..));
54
    rules.add(REQUIRED_CLIENT_PROTOCOLS.rule().args(1..));
54
    rules.add(REQUIRED_RELAY_PROTOCOLS.rule().args(1..));
54
    rules.add(PARAMS.rule());
54
    rules
54
});
/// Rules for parsing the header of a consensus.
54
static NS_HEADER_RULES_CONSENSUS: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
    use NetstatusKwd::*;
54
    let mut rules = NS_HEADER_RULES_COMMON_.clone();
54
    rules.add(CONSENSUS_METHOD.rule().args(1..=1));
54
    rules.add(SHARED_RAND_PREVIOUS_VALUE.rule().args(2..));
54
    rules.add(SHARED_RAND_CURRENT_VALUE.rule().args(2..));
54
    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
54
    rules.build()
54
});
/*
/// Rules for parsing the header of a vote.
static NS_HEADER_RULES_VOTE: SectionRules<NetstatusKwd> = {
    use NetstatusKwd::*;
    let mut rules = NS_HEADER_RULES_COMMON_.clone();
    rules.add(CONSENSUS_METHODS.rule().args(1..));
    rules.add(FLAG_THRESHOLDS.rule());
    rules.add(BANDWIDTH_FILE_HEADERS.rule());
    rules.add(BANDWIDTH_FILE_DIGEST.rule().args(1..));
    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
    rules
};
/// Rules for parsing a single voter's information in a vote.
static NS_VOTERINFO_RULES_VOTE: SectionRules<NetstatusKwd> = {
    use NetstatusKwd::*;
    let mut rules = SectionRules::new();
    rules.add(DIR_SOURCE.rule().required().args(6..));
    rules.add(CONTACT.rule().required());
    rules.add(LEGACY_DIR_KEY.rule().args(1..));
    rules.add(SHARED_RAND_PARTICIPATE.rule().no_args());
    rules.add(SHARED_RAND_COMMIT.rule().may_repeat().args(4..));
    rules.add(SHARED_RAND_PREVIOUS_VALUE.rule().args(2..));
    rules.add(SHARED_RAND_CURRENT_VALUE.rule().args(2..));
    // then comes an entire cert: When we implement vote parsing,
    // we should use the authcert code for handling that.
    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
    rules
};
 */
/// Rules for parsing a single voter's information in a consensus
54
static NS_VOTERINFO_RULES_CONSENSUS: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
    use NetstatusKwd::*;
54
    let mut rules = SectionRules::builder();
54
    rules.add(DIR_SOURCE.rule().required().args(6..));
54
    rules.add(CONTACT.rule().required());
54
    rules.add(VOTE_DIGEST.rule().required());
54
    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
54
    rules.build()
54
});
/// Shared rules for parsing a single routerstatus
static NS_ROUTERSTATUS_RULES_COMMON_: LazyLock<SectionRulesBuilder<NetstatusKwd>> =
54
    LazyLock::new(|| {
        use NetstatusKwd::*;
54
        let mut rules = SectionRules::builder();
54
        rules.add(RS_A.rule().may_repeat().args(1..));
54
        rules.add(RS_S.rule().required());
54
        rules.add(RS_V.rule());
54
        rules.add(RS_PR.rule().required());
54
        rules.add(RS_W.rule());
54
        rules.add(RS_P.rule().args(2..));
54
        rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
54
        rules
54
    });
/// Rules for parsing a single routerstatus in an NS consensus
1
static NS_ROUTERSTATUS_RULES_PLAIN: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
    use NetstatusKwd::*;
1
    let mut rules = NS_ROUTERSTATUS_RULES_COMMON_.clone();
1
    rules.add(RS_R.rule().required().args(8..));
1
    rules.build()
1
});
/*
/// Rules for parsing a single routerstatus in a vote
static NS_ROUTERSTATUS_RULES_VOTE: SectionRules<NetstatusKwd> = {
    use NetstatusKwd::*;
        let mut rules = NS_ROUTERSTATUS_RULES_COMMON_.clone();
        rules.add(RS_R.rule().required().args(8..));
        rules.add(RS_M.rule().may_repeat().args(2..));
        rules.add(RS_ID.rule().may_repeat().args(2..)); // may-repeat?
        rules
    };
*/
/// Rules for parsing a single routerstatus in a microdesc consensus
54
static NS_ROUTERSTATUS_RULES_MDCON: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
    use NetstatusKwd::*;
54
    let mut rules = NS_ROUTERSTATUS_RULES_COMMON_.clone();
54
    rules.add(RS_R.rule().required().args(6..));
54
    rules.add(RS_M.rule().required().args(1..));
54
    rules.build()
54
});
/// Rules for parsing consensus fields from a footer.
54
static NS_FOOTER_RULES: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
    use NetstatusKwd::*;
54
    let mut rules = SectionRules::builder();
54
    rules.add(DIRECTORY_FOOTER.rule().required().no_args());
    // consensus only
54
    rules.add(BANDWIDTH_WEIGHTS.rule());
54
    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
54
    rules.build()
54
});
impl ProtoStatus {
    /// Construct a ProtoStatus from two chosen keywords in a section.
756
    fn from_section(
756
        sec: &Section<'_, NetstatusKwd>,
756
        recommend_token: NetstatusKwd,
756
        required_token: NetstatusKwd,
756
    ) -> crate::Result<ProtoStatus> {
        /// Helper: extract a Protocols entry from an item's arguments.
1512
        fn parse(t: Option<&Item<'_, NetstatusKwd>>) -> crate::Result<Protocols> {
1512
            if let Some(item) = t {
1512
                item.args_as_str()
1512
                    .parse::<Protocols>()
1512
                    .map_err(|e| EK::BadArgument.at_pos(item.pos()).with_source(e))
            } else {
                Ok(Protocols::new())
            }
1512
        }
756
        let recommended = parse(sec.get(recommend_token))?;
756
        let required = parse(sec.get(required_token))?;
756
        Ok(ProtoStatus {
756
            recommended,
756
            required,
756
        })
756
    }
    /// Return the protocols that are listed as "required" in this `ProtoStatus`.
    ///
    /// Implementations may assume that relays on the network implement all the
    /// protocols in the relays' required-protocols list.  Implementations should
    /// refuse to start if they do not implement all the protocols on their own
    /// (client or relay) required-protocols list.
2385
    pub fn required_protocols(&self) -> &Protocols {
2385
        &self.required
2385
    }
    /// Return the protocols that are listed as "recommended" in this `ProtoStatus`.
    ///
    /// Implementations should warn if they do not implement all the protocols
    /// on their own (client or relay) recommended-protocols list.
    pub fn recommended_protocols(&self) -> &Protocols {
        &self.recommended
    }
}
impl<T> std::str::FromStr for NetParams<T>
where
    T: std::str::FromStr,
    T::Err: std::error::Error,
{
    type Err = Error;
301715
    fn from_str(s: &str) -> crate::Result<Self> {
        /// Helper: parse a single K=V pair.
1129236
        fn parse_pair<U>(p: &str) -> crate::Result<(String, U)>
1129236
        where
1129236
            U: std::str::FromStr,
1129236
            U::Err: std::error::Error,
        {
1129236
            let parts: Vec<_> = p.splitn(2, '=').collect();
1129236
            if parts.len() != 2 {
                return Err(EK::BadArgument
                    .at_pos(Pos::at(p))
                    .with_msg("Missing = in key=value list"));
1129236
            }
1129236
            let num = parts[1].parse::<U>().map_err(|e| {
4
                EK::BadArgument
4
                    .at_pos(Pos::at(parts[1]))
4
                    .with_msg(e.to_string())
4
            })?;
1129232
            Ok((parts[0].to_string(), num))
1129236
        }
301715
        let params = s
301715
            .split(' ')
1142015
            .filter(|p| !p.is_empty())
301715
            .map(parse_pair)
301715
            .try_collect()?;
301711
        Ok(NetParams { params })
301715
    }
}
impl FromStr for SharedRandVal {
    type Err = Error;
226
    fn from_str(s: &str) -> crate::Result<Self> {
226
        let val: B64 = s.parse()?;
226
        let val = SharedRandVal(val.into_array()?);
226
        Ok(val)
226
    }
}
impl Display for SharedRandVal {
6
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
6
        Display::fmt(&B64::from(Vec::from(self.0)), f)
6
    }
}
impl NormalItemArgument for SharedRandVal {}
impl SharedRandStatus {
    /// Parse a current or previous shared rand value from a given
    /// SharedRandPreviousValue or SharedRandCurrentValue.
3
    fn from_item(item: &Item<'_, NetstatusKwd>) -> crate::Result<Self> {
3
        match item.kwd() {
2
            NetstatusKwd::SHARED_RAND_PREVIOUS_VALUE | NetstatusKwd::SHARED_RAND_CURRENT_VALUE => {}
            _ => {
1
                return Err(Error::from(internal!(
1
                    "wrong keyword {:?} on shared-random value",
1
                    item.kwd()
1
                ))
1
                .at_pos(item.pos()));
            }
        }
2
        let n_reveals: u8 = item.parse_arg(0)?;
2
        let value: SharedRandVal = item.parse_arg(1)?;
        // Added in proposal 342
2
        let timestamp = item.parse_optional_arg::<Iso8601TimeNoSp>(2)?;
2
        Ok(SharedRandStatus {
2
            n_reveals,
2
            value,
2
            timestamp,
2
        })
3
    }
    /// Return the actual shared random value.
1272
    pub fn value(&self) -> &SharedRandVal {
1272
        &self.value
1272
    }
    /// Return the timestamp (if any) associated with this `SharedRandValue`.
3498
    pub fn timestamp(&self) -> Option<std::time::SystemTime> {
3498
        self.timestamp.map(|t| t.0)
3498
    }
}
impl DirSource {
    /// Parse a "dir-source" item
1135
    fn from_item(item: &Item<'_, NetstatusKwd>) -> crate::Result<Self> {
1135
        if item.kwd() != NetstatusKwd::DIR_SOURCE {
            return Err(
                Error::from(internal!("Bad keyword {:?} on dir-source", item.kwd()))
                    .at_pos(item.pos()),
            );
1135
        }
1135
        let nickname = item
1135
            .required_arg(0)?
1135
            .parse()
1135
            .map_err(|e: InvalidNickname| {
                EK::BadArgument.at_pos(item.pos()).with_msg(e.to_string())
            })?;
1135
        let identity = item.parse_arg(1)?;
1135
        let hostname = item
1135
            .required_arg(2)?
1135
            .parse()
1135
            .map_err(|e: InvalidInternetHost| {
                EK::BadArgument.at_pos(item.pos()).with_msg(e.to_string())
            })?;
1135
        let ip = item.parse_arg(3)?;
1135
        let dir_port = item.parse_arg(4)?;
1135
        let or_port = item.parse_arg(5)?;
1135
        Ok(DirSource {
1135
            nickname,
1135
            identity,
1135
            hostname,
1135
            ip,
1135
            dir_port,
1135
            or_port,
1135
            __non_exhaustive: (),
1135
        })
1135
    }
}
impl ConsensusAuthorityEntry {
    /// Parse a single ConsensusAuthorityEntry from a voter info section.
1135
    fn from_section(sec: &Section<'_, NetstatusKwd>) -> crate::Result<ConsensusAuthorityEntry> {
        use NetstatusKwd::*;
        // this unwrap should be safe because if there is not at least one
        // token in the section, the section is unparsable.
        #[allow(clippy::unwrap_used)]
1135
        let first = sec.first_item().unwrap();
1135
        if first.kwd() != DIR_SOURCE {
            return Err(Error::from(internal!(
                "Wrong keyword {:?} at start of voter info",
                first.kwd()
            ))
            .at_pos(first.pos()));
1135
        }
1135
        let dir_source = DirSource::from_item(sec.required(DIR_SOURCE)?)?;
1135
        let contact = sec.required(CONTACT)?;
        // Ideally we would parse_args_as_str but that requires us to
        // impl From<InvalidContactInfo> for crate::Error which is wrong
        // because many it's a footgun which lets you just write ? here
        // resulting in lack of position information.
        // (This is a general problem with the error handling in crate::parse.)
1135
        let contact = contact
1135
            .args_as_str()
1135
            .parse()
1135
            .map_err(|err: InvalidContactInfo| {
                EK::BadArgument
                    .with_msg(err.to_string())
                    .at_pos(contact.pos())
            })?;
1135
        let vote_digest = sec.required(VOTE_DIGEST)?.parse_arg::<B16U>(0)?;
1135
        Ok(ConsensusAuthorityEntry {
1135
            dir_source,
1135
            contact,
1135
            vote_digest,
1135
            __non_exhaustive: (),
1135
        })
1135
    }
}
impl RelayWeightsItem {
    /// Return a new `RelayWeightsItem` containing no information
    ///
    /// As if parsed from a document with no `w` line, discarding unknown information.
1
    pub fn new_no_info() -> Self {
1
        RelayWeightsItem {
1
            effective: RelayWeight::default(),
1
            params: Unknown::new_discard(),
1
        }
1
    }
    /// Return a new `RelayWeightsItem` containing only the effective weight
    pub fn from_effective(effective: RelayWeight) -> Self {
        RelayWeightsItem {
            effective,
            params: Unknown::new_discard(),
        }
    }
    /// Get the effective relay weight (bandwidth estimate) for path selection.
    ///
    /// Invariant: consistent with from [`params`](RelayWeightsItem::params),
    /// if `parsed` isn't [`Discarded`](Unknown::Discarded).
    //
    // We open-code this rather than deriving it so we can provide better docs.
    pub fn effective(&self) -> RelayWeight {
        self.effective
    }
    /// Get the complete parameter set, if this information is available.
    ///
    /// After parsing, this is the parsed but not interpreted `w` item,
    /// or `None` if the document contained no `w` item.
    //
    // We open-code this rather than deriving it because we want to return
    // `Unknown<&...>` rather than `&Unknown<..>`, which the user would just have to .as_ref().
    pub fn params(&self) -> Unknown<&Option<NetParams<u32>>> {
        self.params.as_ref()
    }
    /// Parse a routerweight from a "w" line.
2051
    fn from_item(item: &Item<'_, NetstatusKwd>) -> crate::Result<RelayWeightsItem> {
2051
        if item.kwd() != NetstatusKwd::RS_W {
3
            return Err(
3
                Error::from(internal!("Wrong keyword {:?} on W line", item.kwd()))
3
                    .at_pos(item.pos()),
3
            );
2048
        }
2048
        let params = item.args_as_str().parse()?;
2047
        let effective = RelayWeight::from_net_params(&params).map_err(|e| e.at_pos(item.pos()))?;
2047
        Ok(RelayWeightsItem {
2047
            effective,
2047
            params: Unknown::new_discard(),
2047
        })
2051
    }
    /// The keyword for parsing and encoding
    const KEYWORD: &str = "w";
}
#[cfg(feature = "retain-unknown")]
impl Default for RelayWeightsItem {
    fn default() -> Self {
        RelayWeightsItem {
            effective: RelayWeight::default(),
            params: Unknown::Retained(None),
        }
    }
}
impl RelayWeight {
    /// Return true if this weight is the result of a successful measurement
26558
    pub fn is_measured(&self) -> bool {
26558
        matches!(self, RelayWeight::Measured(_))
26558
    }
    /// Return true if this weight is nonzero
24756
    pub fn is_nonzero(&self) -> bool {
24756
        !matches!(self, RelayWeight::Unmeasured(0) | RelayWeight::Measured(0))
24756
    }
    /// Parse a routerweight from partially-parsed `w` line in the form of a `NetParams`
    ///
    /// This function is the common part shared between `parse2` and `parse`.
2047
    fn from_net_params(params: &NetParams<u32>) -> crate::Result<RelayWeight> {
2047
        params
2047
            .try_into()
2047
            .map_err(|e: InvalidRelayWeights| EK::BadArgument.with_msg(e.to_string()))
2047
    }
}
impl Default for RelayWeight {
1
    fn default() -> RelayWeight {
1
        RelayWeight::Unmeasured(0)
1
    }
}
impl TryFrom<&NetParams<u32>> for RelayWeight {
    type Error = InvalidRelayWeights;
223115
    fn try_from(params: &NetParams<u32>) -> Result<RelayWeight, InvalidRelayWeights> {
223115
        let bw = params.params.get("Bandwidth");
223115
        let unmeas = params.params.get("Unmeasured");
223115
        let bw = match bw {
1
            None => return Ok(RelayWeight::Unmeasured(0)),
223114
            Some(b) => *b,
        };
223114
        match unmeas {
237
            None | Some(0) => Ok(RelayWeight::Measured(bw)),
222877
            Some(1) => Ok(RelayWeight::Unmeasured(bw)),
            _ => Err(InvalidRelayWeights::InvalidUnmeasured),
        }
223115
    }
}
#[cfg(feature = "retain-unknown")]
impl TryFrom<NetParams<u32>> for RelayWeightsItem {
    type Error = InvalidRelayWeights;
    fn try_from(params: NetParams<u32>) -> Result<RelayWeightsItem, InvalidRelayWeights> {
        Ok(RelayWeightsItem {
            effective: (&params).try_into()?,
            params: Unknown::Retained(Some(params)),
        })
    }
}
/// `parse2` impls for types in this modulea
///
/// Separate module for a separate namespace.
mod parse2_impls {
    use super::*;
    pub(super) use parse2::{
        ArgumentError as AE, ArgumentStream, ErrorProblem as EP, ItemArgumentParseable,
        ItemValueParseable, NetdocParseableFields,
    };
    use std::result::Result;
    // The NormalItemArgument bound ensures that this is applied only to sane types eg integers
    impl<T: FromStr + NormalItemArgument> ItemValueParseable for NetParams<T>
    where
        T::Err: std::error::Error,
    {
284920
        fn from_unparsed(item: parse2::UnparsedItem<'_>) -> Result<Self, EP> {
284920
            item.check_no_object()?;
284920
            item.args_copy()
284920
                .into_remaining()
284920
                .parse()
284920
                .map_err(item.invalid_argument_handler("parameters"))
284920
        }
    }
    impl NetdocParseableFields for RelayWeightsItem {
        type Accumulator = Option<NetParams<u32>>;
221068
        fn is_item_keyword(kw: KeywordRef) -> bool {
221068
            kw == Self::KEYWORD
221068
        }
221068
        fn accumulate_item(acc: &mut Self::Accumulator, item: UnparsedItem) -> Result<(), EP> {
221068
            if acc.is_some() {
                return Err(EP::ItemRepeated);
221068
            }
221068
            item.check_no_object()?;
221068
            let params = NetParams::from_unparsed(item)?;
221068
            *acc = Some(params);
221068
            Ok(())
221068
        }
221068
        fn finish(params: Self::Accumulator, items: &ItemStream) -> Result<Self, EP> {
221068
            let effective = params
221068
                .as_ref()
221068
                .map(TryFrom::try_from)
221068
                .transpose()
221068
                .map_err(|_| EP::OtherBadDocument("invalid information in `w` item"))?
221068
                .unwrap_or_default();
221068
            let params = items.parse_options().retain_unknown_values.map(|()| params);
221068
            Ok(RelayWeightsItem { effective, params })
221068
        }
    }
    impl ItemValueParseable for rs::SoftwareVersion {
221369
        fn from_unparsed(mut item: parse2::UnparsedItem<'_>) -> Result<Self, EP> {
221369
            item.check_no_object()?;
221369
            item.args_mut()
221369
                .into_remaining()
221369
                .parse()
221369
                .map_err(item.invalid_argument_handler("version"))
221369
        }
    }
    impl ItemArgumentParseable for IgnoredPublicationTimeSp {
221068
        fn from_args(a: &mut ArgumentStream) -> Result<IgnoredPublicationTimeSp, AE> {
442136
            let mut next_arg = || a.next().ok_or(AE::Missing);
221068
            let _: &str = next_arg()?;
221068
            let _: &str = next_arg()?;
221068
            Ok(IgnoredPublicationTimeSp)
221068
        }
    }
}
/// `encode` impls for types in this modulea
///
/// Separate module for a separate namespace.
mod encode_impls {
    use super::*;
    use std::result::Result;
    pub(crate) use {
        crate::encode::{ItemEncoder, ItemValueEncodable, NetdocEncodableFields},
        tor_error::Bug,
    };
    impl NetdocEncodableFields for RelayWeightsItem {
24
        fn encode_fields(&self, out: &mut NetdocEncoder) -> Result<(), Bug> {
24
            if let Some(w) = self.params.as_ref().into_retained()? {
24
                w.write_item_value_onto(out.item(Self::KEYWORD))?;
            }
24
            Ok(())
24
        }
    }
    // The NormalItemArgument bound ensures that this is applied only to sane types eg integers
    impl<T: NormalItemArgument + Ord + Display> ItemValueEncodable for NetParams<T> {
62
        fn write_item_value_onto(&self, mut out: ItemEncoder) -> Result<(), Bug> {
314
            for (k, v) in self.iter().collect::<BTreeSet<_>>() {
314
                if k.is_empty()
313
                    || k.chars()
3753
                        .any(|c| c.is_whitespace() || c.is_control() || c == '=')
                {
                    // TODO torspec#401 see TODO in NetParams<T> definition
4
                    return Err(bad_api_usage!(
4
                        "tried to encode NetParms with unreasonable keyword {k:?}"
4
                    ));
310
                }
310
                out.args_raw_string(&format_args!("{k}={v}"));
            }
58
            Ok(())
62
        }
    }
    impl ItemValueEncodable for rs::SoftwareVersion {
36
        fn write_item_value_onto(&self, mut out: ItemEncoder) -> Result<(), Bug> {
36
            out.args_raw_string(self);
36
            Ok(())
36
        }
    }
    impl ItemArgument for IgnoredPublicationTimeSp {
24
        fn write_arg_onto(&self, out: &mut ItemEncoder) -> Result<(), Bug> {
24
            out.args_raw_string(&"2000-01-01 00:00:01");
24
            Ok(())
24
        }
    }
}
impl ConsensusFooterFields {
    /// Parse a directory footer from a footer section.
374
    fn from_section(sec: &Section<'_, NetstatusKwd>) -> crate::Result<ConsensusFooterFields> {
        use NetstatusKwd::*;
374
        sec.required(DIRECTORY_FOOTER)?;
374
        let bandwidth_weights = sec
374
            .maybe(BANDWIDTH_WEIGHTS)
374
            .args_as_str()
374
            .unwrap_or("")
374
            .parse()?;
373
        Ok(ConsensusFooterFields {
373
            bandwidth_weights,
373
            __non_exhaustive: (),
373
        })
374
    }
}
/// `ProtoStatuses` parsing and encoding
///
/// Separate module for separate namespace
mod proto_statuses_parse2_encode {
    use super::encode_impls::*;
    use super::parse2_impls::*;
    use super::*;
    use paste::paste;
    use std::result::Result;
    /// Implements `NetdocParseableFields` for `ProtoStatuses`
    ///
    /// We have this macro so that it's impossible to write things like
    /// ```text
    ///      ProtoStatuses {
    ///          client: ProtoStatus {
    ///              recommended: something something recommended_relay_versions something,
    /// ```
    ///
    /// (The structure of `ProtoStatuses` means the normal parse2 derive won't work for it.
    /// Note the bug above: the recommended *relay* version info is put in the *client* field.
    /// Preventing this bug must involve: avoiding writing twice the field name elements,
    /// such as `relay` and `client`, during this kind of construction/conversion.)
    macro_rules! impl_proto_statuses { { $( $rr:ident $cr:ident; )* } => { paste! {
        #[derive(Deftly)]
        #[derive_deftly(NetdocParseableFields)]
        // Only ProtoStatusesParseNetdocParseAccumulator is exposed.
        #[allow(unreachable_pub)]
        pub struct ProtoStatusesParseHelper {
            $(
                #[deftly(netdoc(default))]
                [<$rr _ $cr _protocols>]: Protocols,
            )*
        }
        /// Partially parsed `ProtoStatuses`
        pub use ProtoStatusesParseHelperNetdocParseAccumulator
            as ProtoStatusesNetdocParseAccumulator;
        impl NetdocParseableFields for ProtoStatuses {
            type Accumulator = ProtoStatusesNetdocParseAccumulator;
253628
            fn is_item_keyword(kw: KeywordRef<'_>) -> bool {
253628
                ProtoStatusesParseHelper::is_item_keyword(kw)
253628
            }
126616
            fn accumulate_item(
126616
                acc: &mut Self::Accumulator,
126616
                item: UnparsedItem<'_>,
126616
            ) -> Result<(), EP> {
126616
                ProtoStatusesParseHelper::accumulate_item(acc, item)
126616
            }
31654
            fn finish(acc: Self::Accumulator, items: &ItemStream<'_>) -> Result<Self, EP> {
31654
                let parse = ProtoStatusesParseHelper::finish(acc, items)?;
31654
                let mut out = ProtoStatuses::default();
                $(
31654
                    out.$cr.$rr = parse.[< $rr _ $cr _protocols >];
                )*
31654
                Ok(out)
31654
            }
        }
        impl NetdocEncodableFields for ProtoStatuses {
6
            fn encode_fields(&self, out: &mut NetdocEncoder) -> Result<(), Bug> {
              $(
6
                self.$cr.$rr.write_item_value_onto(
6
                    out.item(concat!(stringify!($rr), "-", stringify!($cr), "-protocols"))
                )?;
              )*
6
                Ok(())
6
            }
        }
    } } }
    impl_proto_statuses! {
        recommended client;
        recommended relay;
        required client;
        required relay;
    }
}
impl Signature {
    /// Parse a Signature from a directory-signature section
1120
    fn from_item(item: &Item<'_, NetstatusKwd>) -> crate::Result<Signature> {
1120
        if item.kwd() != NetstatusKwd::DIRECTORY_SIGNATURE {
            return Err(Error::from(internal!(
                "Wrong keyword {:?} for directory signature",
                item.kwd()
            ))
            .at_pos(item.pos()));
1120
        }
1120
        let (digest_algo, id_fp, sk_fp) = if item.n_args() > 2 {
            (
1116
                item.required_arg(0)?,
1116
                item.required_arg(1)?,
1116
                item.required_arg(2)?,
            )
        } else {
            // TODO #2530 digest_algo needs to depend on whether SHA1 was stated
4
            ("sha1", item.required_arg(0)?, item.required_arg(1)?)
        };
1120
        let digest_algo = digest_algo.to_string().parse().void_unwrap();
1120
        let digest_algo = DigestAlgoInSignature(Some(digest_algo));
1120
        let id_fingerprint = id_fp.parse::<Fingerprint>()?.into();
1120
        let sk_fingerprint = sk_fp.parse::<Fingerprint>()?.into();
1120
        let key_ids = AuthCertKeyIds {
1120
            id_fingerprint,
1120
            sk_fingerprint,
1120
        };
1120
        let signature = item.obj("SIGNATURE")?;
1120
        Ok(Signature {
1120
            digest_algo,
1120
            key_ids,
1120
            signature,
1120
        })
1120
    }
    /// Return true if this signature has the identity key and signing key
    /// that match a given cert.
4082
    fn matches_cert(&self, cert: &AuthCert) -> bool {
4082
        cert.key_ids() == self.key_ids
4082
    }
    /// If possible, find the right certificate for checking this signature
    /// from among a slice of certificates.
3120
    fn find_cert<'a>(&self, certs: &'a [AuthCert]) -> Option<&'a AuthCert> {
4082
        certs.iter().find(|&c| self.matches_cert(c))
3120
    }
    /// Find the certificate and assemble the pieces ready for verification
    ///
    /// `None` means precisely that we're missing the authcert.
2147
    fn signature_to_verify<'r>(
2147
        &'r self,
2147
        signed_digest: &'r [u8],
2147
        certs: &'r [AuthCert],
2147
    ) -> Option<ConsensusSignatureToVerify> {
2147
        let cert = self.find_cert(certs)?;
1444
        let key = cert.signing_key();
1444
        Some(ConsensusSignatureToVerify {
1444
            key,
1444
            signed_digest,
1444
            signature: &self.signature,
1444
        })
2147
    }
}
impl EncodeOrd for Signature {
22
    fn encode_cmp(&self, other: &Self) -> std::cmp::Ordering {
44
        let k: for<'s> fn(&'_ Signature) -> (&'_ _, &'_ _) = |s| (&s.key_ids, &s.signature);
22
        Ord::cmp(&k(self), &k(other))
22
    }
}
/// Signature information in a consensus, to be verified
///
/// Used by callers of [`SignatureGroup::verify_general`],
/// to allow verification to be suppressed if all we wanted to know was
/// whether we have enough signatures and enough authcerts.
#[derive(Debug, Clone, Copy)]
struct ConsensusSignatureToVerify<'r> {
    /// KP_auth_sign_rsa
    key: &'r ll::pk::rsa::PublicKey,
    /// The digest (actual RSA signature payload, before PKCS#11 padding)
    signed_digest: &'r [u8],
    /// The RSA signature value
    signature: &'r [u8],
}
/// Token indicating that signature verification has been done, if required
///
/// Prevents accidentally passing an unintended no-op function as
/// `do_verify` to [`SignatureGroup::verify_general`].
///
/// Write `SignatureVerifiedIfIntended {}` to construct this,
/// only in code which has actually done the verification,
/// or code which is deliberately not verifying at all.
pub(crate) struct SignatureVerifiedIfIntended {}
impl<'r> ConsensusSignatureToVerify<'r> {
    /// Verify this signature
    ///
996
    fn verify(self) -> Result<SignatureVerifiedIfIntended, VerifyFailed> {
996
        self.key.verify(self.signed_digest, self.signature)?;
991
        Ok(SignatureVerifiedIfIntended {})
996
    }
}
/// How `verify_general` should decide who is a trusted authority
///
/// Don't use this for other purposes
#[derive(Debug, Clone, Copy)]
pub(crate) enum VerifyGeneralTrustedAuthorities<'r> {
    /// Trust these authorities.
    TrustThese {
        /// The HKP_auth_id_rsa
        trusted: &'r [RsaIdentity],
    },
    /// Document is a a vote, so OK if signed by any one of the listed authorities
    AnyOneOfThese {
        /// The HKP_auth_id_rsa
        trusted: &'r [RsaIdentity],
    },
    /// For the benefit of `SignatureGroup::validate`, used by the old parser, only
    ///
    /// Every `AuthCert` passed to `verify_general` is a real authority (!)
    /// (But not necessarily a different one!)
    HazardouslyAssumeAllAuthCertsAreReal {
        /// Total number of authorities that we trust
        ///
        /// Used only to calculate the threshold
        n_authorities: usize,
    },
}
/// Return the minimum number of authorities that we need signatures from
///
/// Enough is strictly more than half.
///
/// The returned value is a [`RangeFrom`](std::ops::RangeFrom), ie an inclusive range.
/// Its `start` value is the minimum acceptable number of authorities
/// from whom we have good signatures.
///
/// Should usually be followed by
/// [`.contains`](std::ops::RangeFrom::contains)`(&actual_number)`.
///
/// # Example
///
/// ```
/// use tor_netdoc::{doc::netstatus::consensus_threshold, parse2::VerifyFailed};
/// # fn main() -> Result<(), VerifyFailed> {
///
/// let n_trusted_authorities = 3;
/// let n_good_signatures_from_different_authorities = 2;
///
/// if consensus_threshold(n_trusted_authorities)
///      .contains(&n_good_signatures_from_different_authorities)
/// {
///     Ok(())
/// } else {
///     Err(VerifyFailed::InsufficientTrustedSigners)
/// }
/// # }
/// ```
1204
pub fn consensus_threshold(n_authorities: usize) -> std::ops::RangeFrom<usize> {
1204
    (n_authorities / 2) + 1 // strict majority
1204
        ..
1204
}
impl SignatureGroup {
    // TODO: these functions are pretty similar and could probably stand to be
    // refactored a lot.
    /// Helper: Return a pair of the number of possible authorities'
    /// signatures in this object for which we _could_ find certs, and
    /// a list of the signatures we couldn't find certificates for.
324
    fn list_missing(&self, certs: &[AuthCert]) -> (usize, Vec<&Signature>) {
324
        let mut ok: HashSet<RsaIdentity> = HashSet::new();
324
        let mut missing = Vec::new();
973
        for sig in &self.signatures {
973
            let id_fingerprint = &sig.key_ids.id_fingerprint;
973
            if ok.contains(id_fingerprint) {
                continue;
973
            }
973
            if sig.find_cert(certs).is_some() {
171
                ok.insert(*id_fingerprint);
171
                continue;
802
            }
802
            missing.push(sig);
        }
324
        (ok.len(), missing)
324
    }
    /// Given a list of authority identity key fingerprints, return true if
    /// this signature group is _potentially_ well-signed according to those
    /// authorities.
270
    fn could_validate(&self, authorities: &[&RsaIdentity]) -> bool {
270
        let mut signed_by: HashSet<RsaIdentity> = HashSet::new();
812
        for sig in &self.signatures {
812
            let id_fp = &sig.key_ids.id_fingerprint;
812
            if signed_by.contains(id_fp) {
                // Already found this in the list.
                continue;
812
            }
812
            if authorities.contains(&id_fp) {
433
                signed_by.insert(*id_fp);
433
            }
        }
270
        consensus_threshold(authorities.len()).contains(&signed_by.len())
270
    }
    /// Return true if the signature group defines a valid signature.
    ///
    /// A signature is valid if it signed by more than half of the
    /// authorities.  This API requires that `n_authorities` is the number of
    /// authorities we believe in, and that every cert in `certs` belongs
    /// to a real authority.
56
    fn validate(&self, n_authorities: usize, certs: &[AuthCert]) -> Result<(), VerifyFailed> {
        // TODO we ought to take the set of trusted authorities as an argument,
        // rather than use VGTA::HazardouslyAssumeAllAuthCertsAreReal.
56
        self.verify_general(
56
            VerifyGeneralTrustedAuthorities::HazardouslyAssumeAllAuthCertsAreReal { n_authorities },
56
            certs,
114
            |tv| tv.verify(),
        )
56
    }
    /// Check signatures (maybe), but not timeliness
    ///
    /// Examines the signatures and collates them with authcerts.
    /// Performs the necessary consensus signature verifications, via `do_verify`.
    ///
    /// If there are not enough authcerts or not enough signatures,
    /// throws a `ConsensusVerifiabilityError`.
    ///
    /// Differs from [`SignatureGroup::validate`]:
    ///
    ///  * Intended also for use with types from parse2.
    ///
    ///  * Yields information about missing authcerts directly in the return value,
    ///    and can be used without actually doing the verification,
    ///    so there's no need for a separate "which certs are we missing" function.
    ///
    ///  * Threshold is passed as a parameter (wanted for votes).
    ///
    ///  * Ability to check authority identities, by passing `trusted_authorities`.
    ///    (done with `authorities_are_correct` in old parser,
    ///    apparently with no engineered safeguard against consensus user omitting to do so).
    ///
    ///    **If `trusted_authorities` is None, all authorities in `certs` are treated as trusted**.
    ///
    ///  * Returns `Result`, not a boolean
    ///
    ///  * We prefer the term `verify` to `validate`.  All this does is signature verification.
    ///
610
    fn verify_general<E>(
610
        &self,
610
        trusted_authorities: VerifyGeneralTrustedAuthorities,
610
        certs: &[AuthCert],
610
        do_verify: impl Fn(ConsensusSignatureToVerify) -> Result<SignatureVerifiedIfIntended, E>,
610
    ) -> Result<(), E>
610
    where
610
        ConsensusVerifiabilityError: Into<E>,
    {
        use VerifyGeneralTrustedAuthorities as TA;
        // A set of the authorities (by identity) who have have signed
        // this document.  We use a set here in case `certs` has more
        // than one certificate for a single authority.
610
        let mut ok: HashSet<RsaIdentity> = HashSet::new();
610
        let mut missing = HashSet::new();
610
        let mut verify_failed = Ok(());
2367
        for sig in &self.signatures {
            // Exhaustive pattern makes it hard to accidentally ignore a field.
            let Signature {
2367
                digest_algo,
                key_ids:
                    AuthCertKeyIds {
2367
                        id_fingerprint,
                        // h_kp_auth_sign_rsa, which Signature::check_signature
                        // checks against the authcert.
                        sk_fingerprint: _,
                    },
                // Used by Signature::check_signature
                signature: _,
2367
            } = sig;
2367
            match trusted_authorities {
2196
                TA::TrustThese { trusted } | TA::AnyOneOfThese { trusted } => {
2198
                    if !trusted.contains(id_fingerprint) {
220
                        continue;
1978
                    }
                }
169
                TA::HazardouslyAssumeAllAuthCertsAreReal { .. } => {
169
                    // OK then!
169
                }
            }
2147
            if ok.contains(id_fingerprint) {
                // We already checked at least one signature using this
                // authority's identity fingerprint.
                continue;
2147
            }
2147
            let Some(d) = self.hashes.hash_slice_for_verification(digest_algo) else {
                // We don't support this kind of digest for this kind
                // of document.
                continue;
            };
2147
            let Some(tv) = sig.signature_to_verify(d, certs) else {
703
                missing.insert(sig.key_ids);
703
                continue;
            };
1444
            match do_verify(tv) {
1439
                Ok::<SignatureVerifiedIfIntended, _>(_) => {
1439
                    ok.insert(*id_fingerprint);
1439
                }
5
                Err(e) => {
5
                    verify_failed = Err(e);
5
                }
            }
        }
610
        let n_authorities = match trusted_authorities {
552
            TA::TrustThese { trusted } => trusted.len(),
56
            TA::HazardouslyAssumeAllAuthCertsAreReal { n_authorities: n } => n,
            TA::AnyOneOfThese { .. } => {
                // strict majority of 1 is 1, so n_authorites being 1 leads to threshold of 1
                // (doing it this way avoids having both thresholds and authority counts
                // in the same code area, which might lead to confusing one with the other.
2
                1
            }
        };
610
        let threshold = consensus_threshold(n_authorities);
610
        if threshold.contains(&ok.len()) {
382
            Ok(())
        } else {
            // Throw the verification error if any of the verifications failed
228
            verify_failed?;
            // Otherwise report that we're missing certs and/or signers
225
            Err(if missing.is_empty() {
61
                ConsensusVerifiabilityError::InsufficientTrustedSigners
            } else {
164
                let deficit = threshold.start - ok.len();
164
                ConsensusVerifiabilityError::MissingAuthCerts { missing, deficit }
            }
225
            .into())
        }
610
    }
}
impl From<ConsensusVerifiabilityError> for VerifyFailed {
1
    fn from(cve: ConsensusVerifiabilityError) -> VerifyFailed {
        use ConsensusVerifiabilityError as CVE;
        use VerifyFailed as VF;
1
        match cve {
            CVE::InsufficientTrustedSigners => VF::InsufficientTrustedSigners,
1
            CVE::MissingAuthCerts { .. } => VF::InsufficientTrustedSigners,
        }
1
    }
}
impl From<ConsensusVerifyFailed> for VerifyFailed {
53
    fn from(cvf: ConsensusVerifyFailed) -> VerifyFailed {
        use ConsensusVerifyFailed as CVF;
        use VerifyFailed as VF;
53
        match cvf {
53
            CVF::CertificationInsufficient { .. } => VF::InsufficientTrustedSigners,
            CVF::InvalidSignature { .. } => VF::VerifyFailed,
        }
53
    }
}
#[cfg(test)]
mod test {
    // @@ begin test lint list maintained by maint/add_warning @@
    #![allow(clippy::bool_assert_comparison)]
    #![allow(clippy::clone_on_copy)]
    #![allow(clippy::dbg_macro)]
    #![allow(clippy::mixed_attributes_style)]
    #![allow(clippy::print_stderr)]
    #![allow(clippy::print_stdout)]
    #![allow(clippy::single_char_pattern)]
    #![allow(clippy::unwrap_used)]
    #![allow(clippy::unchecked_time_subtraction)]
    #![allow(clippy::useless_vec)]
    #![allow(clippy::needless_pass_by_value)]
    #![allow(clippy::string_slice)] // See arti#2571
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
    use super::*;
    use crate::doc::authcert::AuthCertUnverified;
    use crate::encode::{NetdocEncodable, NetdocEncodableFields};
    use crate::parse2::{ParseInput, parse_netdoc, parse_netdoc_multiple};
    use crate::test_support::regsub;
    use anyhow::Context as _;
    use assert_matches::assert_matches;
    use hex_literal::hex;
    use humantime::parse_rfc3339;
    use std::fmt::Debug;
    use std::fs;
    use std::time::Duration;
    use tor_checkable::TimeBound;
    const CERTS: &str = include_str!("../../testdata/authcerts2.txt");
    const CONSENSUS: &str = include_str!("../../testdata/mdconsensus1.txt");
    const PLAIN_CERTS: &str = include_str!("../../testdata2/cached-certs");
    const PLAIN_CONSENSUS: &str = include_str!("../../testdata2/cached-consensus");
    fn read_bad(fname: &str) -> String {
        use std::fs;
        use std::path::PathBuf;
        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        path.push("testdata");
        path.push("bad-mdconsensus");
        path.push(fname);
        fs::read_to_string(path).unwrap()
    }
    #[test]
    fn parse_and_validate_md() -> crate::Result<()> {
        use std::net::SocketAddr;
        use tor_checkable::{SelfSigned, TimeBound};
        let mut certs = Vec::new();
        for cert in AuthCert::parse_multiple(CERTS)? {
            let cert = cert?.check_signature()?.dangerously_assume_timely();
            certs.push(cert);
        }
        let auth_ids: Vec<_> = certs.iter().map(|c| c.id_fingerprint()).collect();
        assert_eq!(certs.len(), 3);
        let (_, _, consensus) = MdConsensus::parse(CONSENSUS)?;
        let consensus = consensus.dangerously_assume_timely().set_n_authorities(3);
        // The set of authorities we know _could_ validate this cert.
        assert!(consensus.authorities_are_correct(&auth_ids));
        // A subset would also work.
        assert!(consensus.authorities_are_correct(&auth_ids[0..1]));
        {
            // If we only believe in an authority that isn't listed,
            // that won't work.
            let bad_auth_id = (*b"xxxxxxxxxxxxxxxxxxxx").into();
            assert!(!consensus.authorities_are_correct(&[&bad_auth_id]));
        }
        let missing = consensus.key_is_correct(&[]).err().unwrap();
        assert_eq!(3, missing.len());
        assert!(consensus.key_is_correct(&certs).is_ok());
        let missing = consensus.key_is_correct(&certs[0..1]).err().unwrap();
        assert_eq!(2, missing.len());
        // here is a trick that had better not work.
        let same_three_times = vec![certs[0].clone(), certs[0].clone(), certs[0].clone()];
        let missing = consensus.key_is_correct(&same_three_times).err().unwrap();
        assert_eq!(2, missing.len());
        assert!(consensus.is_well_signed(&same_three_times).is_err());
        assert!(consensus.key_is_correct(&certs).is_ok());
        let consensus = consensus.check_signature(&certs)?;
        assert_eq!(6, consensus.relays().len());
        let r0 = &consensus.relays()[0];
        assert_eq!(
            r0.md_digest(),
            &hex!("73dabe0a0468f4f7a67810a18d11e36731bb1d2ec3634db459100609f3b3f535")
        );
        assert_eq!(
            r0.rsa_identity().as_bytes(),
            &hex!("0a3057af2910415794d8ea430309d9ac5f5d524b")
        );
        assert!(!r0.weight().is_measured());
        assert!(!r0.weight().is_nonzero());
        let pv = &r0.protovers();
        assert!(pv.supports_subver("HSDir", 2));
        assert!(!pv.supports_subver("HSDir", 3));
        let ip4 = "127.0.0.1:5002".parse::<SocketAddr>().unwrap();
        let ip6 = "[::1]:5002".parse::<SocketAddr>().unwrap();
        assert!(r0.addrs().any(|a| a == ip4));
        assert!(r0.addrs().any(|a| a == ip6));
        Ok(())
    }
    #[test]
    fn parse_and_validate_ns() -> crate::Result<()> {
        use tor_checkable::{SelfSigned, TimeBound};
        let mut certs = Vec::new();
        for cert in AuthCert::parse_multiple(PLAIN_CERTS)? {
            let cert = cert?.check_signature()?.dangerously_assume_timely();
            certs.push(cert);
        }
        let auth_ids: Vec<_> = certs.iter().map(|c| c.id_fingerprint()).collect();
        assert_eq!(certs.len(), 4);
        let (_, _, consensus) = PlainConsensus::parse(PLAIN_CONSENSUS)?;
        let consensus = consensus.dangerously_assume_timely().set_n_authorities(3);
        // The set of authorities we know _could_ validate this cert.
        assert!(consensus.authorities_are_correct(&auth_ids));
        // A subset would also work.
        assert!(consensus.authorities_are_correct(&auth_ids[0..1]));
        assert!(consensus.key_is_correct(&certs).is_ok());
        let _consensus = consensus.check_signature(&certs)?;
        Ok(())
    }
    #[test]
    fn test_bad() {
        use crate::Pos;
        fn check(fname: &str, e: &Error) {
            let content = read_bad(fname);
            let res = MdConsensus::parse(&content);
            assert!(res.is_err());
            assert_eq!(&res.err().unwrap(), e);
        }
        check(
            "bad-flags",
            &EK::BadArgument
                .at_pos(Pos::from_line(27, 1))
                .with_msg("Flags out of order"),
        );
        check(
            "bad-md-digest",
            &EK::BadArgument
                .at_pos(Pos::from_line(40, 3))
                .with_msg("Invalid base64"),
        );
        check(
            "bad-weight",
            &EK::BadArgument
                .at_pos(Pos::from_line(67, 141))
                .with_msg("invalid digit found in string"),
        );
        check(
            "bad-weights",
            &EK::BadArgument
                .at_pos(Pos::from_line(51, 13))
                .with_msg("invalid digit found in string"),
        );
        check(
            "wrong-order",
            &EK::WrongSortOrder.at_pos(Pos::from_line(52, 1)),
        );
        check(
            "wrong-start",
            &EK::UnexpectedToken
                .with_msg("vote-status")
                .at_pos(Pos::from_line(1, 1)),
        );
        check("wrong-version", &EK::BadDocumentVersion.with_msg("10"));
    }
    fn gettok(s: &str) -> crate::Result<Item<'_, NetstatusKwd>> {
        let mut reader = NetDocReader::new(s)?;
        let tok = reader.next().unwrap();
        assert!(reader.next().is_none());
        tok
    }
    #[test]
    fn test_weight() {
        let w = gettok("w Unmeasured=1 Bandwidth=6\n").unwrap();
        let w = RelayWeightsItem::from_item(&w).unwrap();
        assert!(!w.effective.is_measured());
        assert!(w.effective.is_nonzero());
        let w = gettok("w Bandwidth=10\n").unwrap();
        let w = RelayWeightsItem::from_item(&w).unwrap();
        assert!(w.effective.is_measured());
        assert!(w.effective.is_nonzero());
        let w = RelayWeightsItem::new_no_info();
        assert!(!w.effective.is_measured());
        assert!(!w.effective.is_nonzero());
        let w = gettok("w Mustelid=66 Cheato=7 Unmeasured=1\n").unwrap();
        let w = RelayWeightsItem::from_item(&w).unwrap();
        assert!(!w.effective.is_measured());
        assert!(!w.effective.is_nonzero());
        let w = gettok("r foo\n").unwrap();
        let w = RelayWeightsItem::from_item(&w);
        assert!(w.is_err());
        let w = gettok("r Bandwidth=6 Unmeasured=Frog\n").unwrap();
        let w = RelayWeightsItem::from_item(&w);
        assert!(w.is_err());
        let w = gettok("r Bandwidth=6 Unmeasured=3\n").unwrap();
        let w = RelayWeightsItem::from_item(&w);
        assert!(w.is_err());
    }
    #[test]
    fn test_netparam() {
        let p = "Hello=600 Goodbye=5 Fred=7"
            .parse::<NetParams<u32>>()
            .unwrap();
        assert_eq!(p.get("Hello"), Some(&600_u32));
        let p = "Hello=Goodbye=5 Fred=7".parse::<NetParams<u32>>();
        assert!(p.is_err());
        let p = "Hello=Goodbye Fred=7".parse::<NetParams<u32>>();
        assert!(p.is_err());
        for bad_kw in ["What=The", "", "\n", "\0"] {
            let p = [(bad_kw, 42)].into_iter().collect::<NetParams<i32>>();
            let mut d = NetdocEncoder::new();
            let d = (|| {
                let i = d.item("bad-psrams");
                p.write_item_value_onto(i)?;
                d.finish()
            })();
            let _: tor_error::Bug = d.expect_err(bad_kw);
        }
    }
    #[test]
    fn test_sharedrand() {
        let sr =
            gettok("shared-rand-previous-value 9 5LodY4yWxFhTKtxpV9wAgNA9N8flhUCH0NqQv1/05y4\n")
                .unwrap();
        let sr = SharedRandStatus::from_item(&sr).unwrap();
        assert_eq!(sr.n_reveals, 9);
        assert_eq!(
            sr.value.0,
            hex!("e4ba1d638c96c458532adc6957dc0080d03d37c7e5854087d0da90bf5ff4e72e")
        );
        assert!(sr.timestamp.is_none());
        let sr2 = gettok(
            "shared-rand-current-value 9 \
                    5LodY4yWxFhTKtxpV9wAgNA9N8flhUCH0NqQv1/05y4 2022-01-20T12:34:56\n",
        )
        .unwrap();
        let sr2 = SharedRandStatus::from_item(&sr2).unwrap();
        assert_eq!(sr2.n_reveals, sr.n_reveals);
        assert_eq!(sr2.value.0, sr.value.0);
        assert_eq!(
            sr2.timestamp.unwrap().0,
            humantime::parse_rfc3339("2022-01-20T12:34:56Z").unwrap()
        );
        let sr = gettok("foo bar\n").unwrap();
        let sr = SharedRandStatus::from_item(&sr);
        assert!(sr.is_err());
    }
    #[test]
    fn test_protostatus() {
        let my_protocols: Protocols = "Link=7 Cons=1-5 Desc=3-10".parse().unwrap();
        let outcome = ProtoStatus {
            recommended: "Link=7".parse().unwrap(),
            required: "Desc=5".parse().unwrap(),
        }
        .check_protocols(&my_protocols);
        assert!(outcome.is_ok());
        let outcome = ProtoStatus {
            recommended: "Microdesc=4 Link=7".parse().unwrap(),
            required: "Desc=5".parse().unwrap(),
        }
        .check_protocols(&my_protocols);
        assert_eq!(
            outcome,
            Err(ProtocolSupportError::MissingRecommended(
                "Microdesc=4".parse().unwrap()
            ))
        );
        let outcome = ProtoStatus {
            recommended: "Microdesc=4 Link=7".parse().unwrap(),
            required: "Desc=5 Cons=5-12 Wombat=15".parse().unwrap(),
        }
        .check_protocols(&my_protocols);
        assert_eq!(
            outcome,
            Err(ProtocolSupportError::MissingRequired(
                "Cons=6-12 Wombat=15".parse().unwrap()
            ))
        );
    }
    #[test]
    fn serialize_protostatus() {
        let ps = ProtoStatuses {
            client: ProtoStatus {
                recommended: "Link=1-5 LinkAuth=2-5".parse().unwrap(),
                required: "Link=5 LinkAuth=3".parse().unwrap(),
            },
            relay: ProtoStatus {
                recommended: "Wombat=20-30 Knish=20-30".parse().unwrap(),
                required: "Wombat=20-22 Knish=25-27".parse().unwrap(),
            },
        };
        let json = serde_json::to_string(&ps).unwrap();
        let ps2 = serde_json::from_str(json.as_str()).unwrap();
        assert_eq!(ps, ps2);
        let ps3: ProtoStatuses = serde_json::from_str(
            r#"{
            "client":{
                "required":"Link=5 LinkAuth=3",
                "recommended":"Link=1-5 LinkAuth=2-5"
            },
            "relay":{
                "required":"Wombat=20-22 Knish=25-27",
                "recommended":"Wombat=20-30 Knish=20-30"
            }
        }"#,
        )
        .unwrap();
        assert_eq!(ps, ps3);
    }
    // consensuses are done in each_flavor.rs: see verify_error_netstatus
    #[test]
    fn verify_error_netstatus_vote() -> Result<(), anyhow::Error> {
        use VerifyFailed as VF;
        use VoteVerifyFailed as VVF;
        use vote::NetworkStatusUnverified as UV;
        let file = "testdata2/v3-status-votes--1";
        let text = fs::read_to_string(file).with_context(|| file.to_owned())?;
        let input = ParseInput::new(&text, file);
        let doc: UV = parse_netdoc(&input)?;
        let trusted = [doc.peek_alleged_authority()];
        let edit_body = |f: &dyn Fn(&mut _)| {
            let (mut body, sigs) = doc.clone().unwrap_unverified();
            f(&mut body);
            UV::from_parts(body, sigs)
        };
        // sabotage the overall signature
        {
            let mut doc = doc.clone();
            doc.sigs.sigs.directory_signature.signature.fill(0xff);
            assert_matches!(
                doc.verify(&trusted),
                Err(VVF::InvalidSignature(VF::VerifyFailed))
            );
        }
        // wrong authority
        {
            let doc = doc.clone();
            assert_matches!(
                doc.verify(&[[0x55; _].into()]),
                Err(VVF::InvalidSignature(VF::InsufficientTrustedSigners))
            );
        }
        // authcert is for a different authority
        {
            let doc = edit_body(&|body| {
                body.authority.authority.dir_source.identity.0 = [0x55; _].into();
            });
            assert_matches!(
                //
                doc.verify(&trusted),
                Err(VVF::AuthCertWrongAuthority)
            );
        }
        // authcert is from a different time
        let with_mutated_lifetime = |f: &dyn Fn(&mut Lifetime)| {
            let doc = edit_body(&|body| f(&mut body.preamble.lifetime));
            assert_matches!(
                //
                doc.verify(&trusted),
                Err(VVF::AuthCertWrongValidity(_))
            );
        };
        let t_past = parse_rfc3339("1990-01-01T00:02:25Z")?;
        let t_future = parse_rfc3339("2010-01-01T00:02:25Z")?;
        with_mutated_lifetime(&|lifetime| lifetime.valid_after.0 = t_future);
        with_mutated_lifetime(&|lifetime| lifetime.fresh_until.0 = t_past);
        with_mutated_lifetime(&|lifetime| lifetime.valid_until.0 = t_past);
        // syntactically invalid authcert
        {
            let mut text = text.clone();
            regsub(&mut text, "^dir-key-expires ", "dir-key-expires-SABOTAGED ");
            let input = ParseInput::new(&text, file);
            let doc: UV = parse_netdoc(&input)?;
            assert_matches!(
                //
                doc.verify(&trusted),
                Err(VVF::AuthCertParseError(..))
            );
        }
        Ok(())
    }
    #[cfg(feature = "retain-unknown")]
    #[allow(clippy::type_complexity)]
    pub(super) fn prep_netstatus_verify<UV: NetdocParseable>(
        file: &str,
    ) -> anyhow::Result<(UV, String, Vec<AuthCert>, Vec<RsaIdentity>, SystemTime)> {
        let text = fs::read_to_string(file).with_context(|| file.to_owned())?;
        let now = parse_rfc3339("2000-01-01T00:02:25Z")?;
        let mut input = ParseInput::new(&text, file);
        input.retain_unknown_values();
        let doc: UV = parse_netdoc(&input)?;
        let certs = {
            let file = "testdata2/cached-certs";
            let text = fs::read_to_string(file)?;
            let input = ParseInput::new(&text, file);
            let certs: Vec<AuthCertUnverified> = parse_netdoc_multiple(&input)?;
            certs
                .into_iter()
                .map(|cert| cert.verify_selfcert(now))
                .collect::<Result<Vec<AuthCert>, _>>()?
        };
        let authorities = certs.iter().map(|cert| *cert.fingerprint).collect_vec();
        Ok((doc, text, certs, authorities, now))
    }
    /// Check that a network document can be parsed and regenerated, mostly identically
    ///
    /// The regenerated encoded form doesn't need to be 100% identical:
    /// it is compared with a *munged* version of the the original input file,
    /// to cope with differences between C Tor and Arti.
    ///
    /// The mungings are:
    ///
    ///  * Some fields' syntax are adjusted, where C Tor and Arti disagree
    ///    in all kinds of network document.
    ///
    ///  * Document-specific, [`MungeForRoundtrip::adjust_exp`]
    #[cfg(feature = "retain-unknown")]
    fn roundtrip_netstatus<UV, V, VE>(
        // TODO DIRAUTH use include_str!, so, at call sites
        // https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/4121#note_3428675
        file: &str,
        verify: impl FnOnce(UV, &[RsaIdentity], &[AuthCert]) -> Result<TimeRangeBound<V>, VE>,
        adjust_now: Duration,
    ) -> anyhow::Result<()>
    where
        UV: NetdocParseable + NetdocParseableUnverified + MungeForRoundtrip,
        UV::Signatures: Clone + Debug + NetdocEncodableFields,
        VE: Debug + std::error::Error + Send + Sync + 'static,
        V: Debug + NetdocEncodable,
    {
        let (doc, text, certs, authorities, now) = prep_netstatus_verify::<UV>(file)?;
        let now = now + adjust_now;
        let sigs = doc.inspect_unverified().1.sigs.clone();
        let doc = verify(doc, &authorities, &certs)?.if_valid_at(&now)?;
        println!("{doc:#?}");
        let mut enc = NetdocEncoder::new();
        doc.encode_unsigned(&mut enc)?;
        sigs.encode_fields(&mut enc)?;
        let enc = enc.finish()?;
        let mut exp: String = text.clone();
        // TODO DIRAUTH torspec!507 C Tor emits padded base64 in shared-rand-* items.
        regsub(
            //
            &mut exp,
            r#"^(shared-rand-.*)$"#,
            |c: &regex::Captures| {
                let mut s = c[1].to_owned();
                regsub(&mut s, r#"="#, "");
                s
            },
        );
        // We don't manage proper numerical sorting of version numbers.
        // Doing so is awkward.  See the (2nd) TODO on RecommendedTorVersions.
        regsub(
            //
            &mut exp,
            r#"^(client|server)-versions (.+)$"#,
            |c: &regex::Captures| -> String {
                format!(
                    "{}-versions {}",
                    &c[1],
                    iter_join(",", c[2].split(',').sorted()),
                )
            },
        );
        let mut regsub = |re, repl| regsub(&mut exp, re, repl);
        // C Tor writes empty versions lines with trailing space
        regsub(
            //
            r#"^((?:client|server)-versions) $"#,
            "$1",
        );
        // C Tor emits `m` in varying places: after `a` in votes,
        // and at the end of each routerstatus in md consensuses.
        // We emit it at the start of each routerstatus, right after `r`.
        regsub(
            r#"(?x)
                   ( ^    r\ .* \n     )  #  ( r  )  $1, part before where we want to put m's
                   ( (?:     .* \n )*? )  #  (.*? )  $2, the rest, before the m's
                   ( (?:  m\ .* \n )+  )  #  ( m+ )  $3, one or more m's
            "#,
            r#"$1$3$2"#,
        );
        UV::adjust_exp(&mut exp);
        assert_eq_or_diff!(&exp, &enc);
        Ok(())
    }
    trait MungeForRoundtrip {
        /// Munge `s` so that it resembles the output of C Tor
        fn adjust_exp(exp: &mut String);
    }
    /// Test that we can re-encode the consensus we parsed, and that we get the same thing back.
    ///
    /// Well, roughly the same thing.
    #[cfg(feature = "retain-unknown")]
    #[test]
    fn roundtrip_netstatus_plain() -> anyhow::Result<()> {
        roundtrip_netstatus::<plain::NetworkStatusUnverified, _, _>(
            "testdata2/cached-consensus",
            plain::NetworkStatusUnverified::verify,
            Duration::ZERO,
        )
    }
    impl MungeForRoundtrip for plain::NetworkStatusUnverified {
        fn adjust_exp(exp: &mut String) {
            let mut regsub = |re, repl| regsub(exp, re, repl);
            // We emit the optional `ns`
            // https://spec.torproject.org/dir-spec/consensus-formats.html#item:network-status-version
            regsub(
                r#"^network-status-version 3$"#,
                "network-status-version 3 ns",
            );
            // C Tor writes nontrivial values for `publication` in rs `r` items,
            // but we use a fixed string.
            // https://spec.torproject.org/dir-spec/consensus-formats.html#item:r
            regsub(
                r#"^(r \S+ \S+ \S+) \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}"#,
                "$1 2000-01-01 00:00:01",
            );
        }
    }
    #[cfg(feature = "retain-unknown")]
    #[test]
    fn roundtrip_netstatus_md() -> anyhow::Result<()> {
        roundtrip_netstatus::<md::NetworkStatusUnverified, _, _>(
            "testdata2/cached-microdesc-consensus",
            md::NetworkStatusUnverified::verify,
            Duration::ZERO,
        )
    }
    impl MungeForRoundtrip for md::NetworkStatusUnverified {
        fn adjust_exp(exp: &mut String) {
            let mut regsub = |re, repl| regsub(exp, re, repl);
            // C Tor writes nontrivial values for `publication` in rs `r` items,
            // but we use a fixed string.
            // https://spec.torproject.org/dir-spec/consensus-formats.html#item:r
            //
            // Not the same as in plain consensus: one fewer fields!
            regsub(
                r#"^(r \S+ \S+) \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}"#,
                "$1 2000-01-01 00:00:01",
            );
        }
    }
    #[cfg(feature = "retain-unknown")]
    #[test]
    fn roundtrip_netstatus_vote() -> anyhow::Result<()> {
        roundtrip_netstatus::<vote::NetworkStatusUnverified, _, _>(
            "testdata2/v3-status-votes--1",
            |doc, trusted, _| vote::NetworkStatusUnverified::verify(doc, trusted),
            Duration::from_secs(20),
        )
    }
    impl MungeForRoundtrip for vote::NetworkStatusUnverified {
        fn adjust_exp(exp: &mut String) {
            // C Tor writes items in consensuses a different order to in votes!
            // C Tor writes different stats items with different floating point formats!
            let stats_massage_entry = |e: &str| {
                let mut e = e.to_owned();
                if e.contains('.') {
                    regsub(
                        &mut e,
                        // strip trailing 0's and then trailing `.`
                        r#"(?x)^ ( (?:wfu) = [0-9.]*? )( \.? 0+ ) $"#,
                        "$1",
                    );
                }
                e
            };
            // C Tor writes stats items in votes in an apparently arbitrarily chosen order
            regsub(exp, r#"^stats (.+)$"#, |c: &regex::Captures| -> String {
                format!(
                    "stats {}",
                    iter_join(" ", c[1].split(' ').sorted().map(stats_massage_entry)),
                )
            });
            let mut regsub = |re: &_, repl| regsub(exp, re, repl);
            // C Tor writes *-protocols in an apparently arbitrarily chosen order
            regsub(
                r#"(?x)
                       ^ (recommended-relay-protocols\ .*)  \n
                         (recommended-client-protocols\ .*) \n
                         (required-relay-protocols\ .*)     \n
                         (required-client-protocols\ .*)    \n
                         (known-flags .*)$                  \n
                    "#,
                r#"$5
$2
$1
$4
$3
"#,
            );
            // C Tor emits empty `client-versions` in consensuses, but not in votes.
            // (See also the fixup in `roundtrip_netstatus`, which relates to the *syntax*)
            //
            // Some of our inputs (eg the testdata2 votes) don't contain meaningful
            // info, so to make the C Tor output match our output, add them.
            regsub(
                r#"(?x) ^ (voting-delay\ .*) \n
                          (known-flags\ .*) \n"#,
                "$1
client-versions
server-versions
$2
",
            );
            //#                         (?:  a\ .* \n )?    )   #    a? )           we want to put m's
            for missing_field in [
                "bandwidth-file-headers", // TODO DIRAUTH implement
                "bandwidth-file-digest",  // TODO DIRAUTH implement
                "flag-thresholds",        // TODO DIRAUTH implement
            ] {
                regsub(&format!(r#"^{missing_field} .*\n"#), "");
            }
        }
    }
    fn testdata_live(f: &str) -> String {
        // We implement an overrideable *prefix* rather than suffix, here,
        // so that we can access the files in a totally different directory.
        // (This is helpful with nailing-cargo, amongst other things.)
        let var = "TOR_NETDOC_TESTDATA_LIVE_PREFIX";
        let prefix = std::env::var_os(var)
            .map(|s| s.into_string().expect(var))
            .unwrap_or_else(|| "testdata-live/".into());
        format!("{prefix}{f}")
    }
    #[allow(clippy::unnecessary_wraps)] // signature needs to match for roundtrip_netstatus
    fn unwrap_unverified_for_test<UV: NetdocParseableUnverified>(
        uv: UV,
        _ids: &[RsaIdentity],
        _certs: &[AuthCert],
    ) -> Result<TimeRangeBound<UV::Body>, std::convert::Infallible> {
        Ok(TimeRangeBound::new(uv.unwrap_unverified().0, ..))
    }
    #[cfg(feature = "retain-unknown")]
    #[test]
    fn roundtrip_live_plain() -> anyhow::Result<()> {
        roundtrip_netstatus::<plain::NetworkStatusUnverified, _, _>(
            &testdata_live("consensus"),
            unwrap_unverified_for_test,
            Duration::ZERO,
        )
    }
    #[cfg(feature = "retain-unknown")]
    #[test]
    fn roundtrip_live_md() -> anyhow::Result<()> {
        roundtrip_netstatus::<md::NetworkStatusUnverified, _, _>(
            &testdata_live("consensus-microdesc"),
            unwrap_unverified_for_test,
            Duration::ZERO,
        )
    }
    #[cfg(feature = "retain-unknown")]
    #[test]
    fn roundtrip_live_vote() -> anyhow::Result<()> {
        roundtrip_netstatus::<vote::NetworkStatusUnverified, _, _>(
            &testdata_live("authority"),
            unwrap_unverified_for_test,
            Duration::ZERO,
        )
    }
}