Skip to main content

tor_netdoc/doc/
netstatus.rs

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
51mod dir_source;
52mod rs;
53
54pub mod md;
55pub mod plain;
56#[cfg(feature = "incomplete")]
57pub mod vote;
58
59#[cfg(feature = "build_docs")]
60mod build;
61
62pub use proto_statuses_parse2_encode::ProtoStatusesNetdocParseAccumulator;
63
64#[cfg(feature = "incomplete")]
65use crate::doc::authcert::EncodedAuthCert;
66
67use crate::doc::authcert::{self, AuthCert, AuthCertKeyIds, AuthCertUnverified};
68use crate::encode::{
69    EncodeOrd, ItemArgument, ItemEncoder, ItemValueEncodable, NetdocEncodable, NetdocEncoder,
70};
71use crate::parse::keyword::Keyword;
72use crate::parse::parser::{Section, SectionRules, SectionRulesBuilder};
73use crate::parse::tokenize::{Item, ItemResult, NetDocReader};
74use crate::parse2::{
75    self, ArgumentError, ArgumentStream, ErrorProblem, IsStructural, ItemArgumentParseable,
76    ItemStream, ItemValueParseable, KeywordRef, NetdocParseable, NetdocParseableUnverified,
77    SignatureHashInputs, SignatureItemParseable, StopAt, UnparsedItem, VerifyFailed,
78};
79use crate::types::relay_flags::{self, DocRelayFlags};
80use crate::types::{self, *};
81use crate::util::PeekableIterator;
82use crate::{Error, KeywordEncodable, NetdocErrorKind as EK, NormalItemArgument, Pos};
83use std::collections::{BTreeSet, HashMap, HashSet};
84use std::fmt::{self, Display};
85use std::slice;
86use std::str::FromStr;
87use std::sync::Arc;
88use std::time::{self, SystemTime};
89use std::{net, result};
90use tor_basic_utils::iter_join;
91use tor_error::{Bug, HasKind, bad_api_usage, internal};
92use tor_protover::Protocols;
93use void::ResultVoidExt as _;
94
95use derive_deftly::{Deftly, define_derive_deftly};
96use digest::Digest;
97use itertools::Itertools;
98use saturating_time::SaturatingTime as _;
99use std::sync::LazyLock;
100use tor_checkable::{ExternallySigned, Timebound, timed::TimerangeBound};
101use tor_llcrypto as ll;
102use tor_llcrypto::pk::rsa::RsaIdentity;
103
104use serde::{Deserialize, Deserializer};
105
106#[cfg(feature = "build_docs")]
107pub use build::MdConsensusBuilder;
108#[cfg(feature = "build_docs")]
109pub use build::PlainConsensusBuilder;
110#[cfg(feature = "build_docs")]
111ns_export_each_flavor! {
112    ty: RouterStatusBuilder;
113}
114
115ns_export_each_variety! {
116    ty: Footer, RouterStatus, Preamble;
117}
118
119#[deprecated]
120pub use PlainConsensus as NsConsensus;
121#[deprecated]
122pub use PlainRouterStatus as NsRouterStatus;
123#[deprecated]
124pub use UncheckedPlainConsensus as UncheckedNsConsensus;
125#[deprecated]
126pub use UnvalidatedPlainConsensus as UnvalidatedNsConsensus;
127
128pub use rs::{RouterStatusMdDigestsVote, SoftwareVersion};
129
130pub use dir_source::{ConsensusAuthoritySection, DirSource, SupersededAuthorityKey};
131
132define_constant_string! {
133    /// `network-status-version` version value
134    ///
135    /// This is the fixed string `3`.
136    ///
137    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:network-status-version>
138    //
139    // IMO this is nicer than the formulation with an enum.
140    // In practice we are not going to support other versions with the same parsing approach;
141    // probably not even with the same code.
142    NetworkStatusVersion = "3";
143}
144
145define_constant_string! {
146    /// The `status` value in a `vote-status` line in a consensus
147    ///
148    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:vote-status>
149    VoteStatusConsensus = "consensus";
150}
151
152define_constant_string! {
153    /// The `vote` value in a `vote-status` line in a vote
154    ///
155    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:vote-status>
156    VoteStatusVote = "vote";
157}
158
159/// `publiscation` field in routerstatus entry intro item other than in votes
160///
161/// Two arguments which are both ignored.
162/// This used to be an ISO8601 timestamp in anomalous two-argument format.
163///
164/// Nowadays, according to the spec, it can be a dummy value.
165/// So it can be a unit type.
166///
167/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:r>,
168/// except in votes which use [`Iso8601TimeSp`] instead.
169///
170/// **Not the same as** the `published` item:
171/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:published>
172#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Default)]
173#[allow(clippy::exhaustive_structs)]
174pub struct IgnoredPublicationTimeSp;
175
176/// The lifetime of a networkstatus document.
177///
178/// In a consensus, this type describes when the consensus may safely
179/// be used.  In a vote, this type describes the proposed lifetime for a
180/// consensus.
181///
182/// Aggregate of three netdoc preamble fields.
183#[derive(Clone, Debug, Deftly)]
184#[derive_deftly(Constructor, NetdocEncodableFields, NetdocParseableFields)]
185#[derive_deftly(Lifetime)]
186#[allow(clippy::exhaustive_structs)]
187pub struct Lifetime {
188    /// `valid-after` --- Time at which the document becomes valid
189    ///
190    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:published>
191    ///
192    /// (You might see a consensus a little while before this time,
193    /// since voting tries to finish up before the.)
194    #[deftly(constructor)]
195    #[deftly(netdoc(single_arg))]
196    pub valid_after: Iso8601TimeSp,
197    /// `fresh-until` --- Time after which there is expected to be a better version
198    /// of this consensus
199    ///
200    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:published>
201    ///
202    /// You can use the consensus after this time, but there is (or is
203    /// supposed to be) a better one by this point.
204    #[deftly(constructor)]
205    #[deftly(netdoc(single_arg))]
206    pub fresh_until: Iso8601TimeSp,
207    /// `valid-until` --- Time after which this consensus is expired.
208    ///
209    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:published>
210    ///
211    /// You should try to get a better consensus after this time,
212    /// though it's okay to keep using this one if no more recent one
213    /// can be found.
214    #[deftly(constructor)]
215    #[deftly(netdoc(single_arg))]
216    pub valid_until: Iso8601TimeSp,
217
218    #[doc(hidden)]
219    #[deftly(netdoc(skip))]
220    pub __non_exhaustive: (),
221}
222
223define_derive_deftly! {
224    /// Bespoke derive for `Lifetime`, for `new` and accessors
225    Lifetime:
226
227    ${defcond FIELD not(approx_equal($fname, __non_exhaustive))}
228
229    impl Lifetime {
230        /// Construct a new Lifetime.
231        pub fn new(
232            $( ${when FIELD} $fname: time::SystemTime, )
233        ) -> crate::Result<Self> {
234            // Make this now because otherwise literal `valid_after` here in the body
235            // has the wrong span - the compiler refuses to look at the argument.
236            // But we can refer to the field names.
237            let self_ = Lifetime {
238                $( ${when FIELD} $fname: $fname.into(), )
239                __non_exhaustive: (),
240            };
241            if self_.valid_after < self_.fresh_until && self_.fresh_until < self_.valid_until {
242                Ok(self_)
243            } else {
244                Err(EK::InvalidLifetime.err())
245            }
246        }
247      $(
248        ${when FIELD}
249
250        ${fattrs doc}
251        pub fn $fname(&self) -> time::SystemTime {
252            *self.$fname
253        }
254      )
255        /// Return true if this consensus is officially valid at the provided time.
256        pub fn valid_at(&self, when: time::SystemTime) -> bool {
257            *self.valid_after <= when && when <= *self.valid_until
258        }
259
260        /// Return the voting period implied by this lifetime.
261        ///
262        /// (The "voting period" is the amount of time in between when a consensus first
263        /// becomes valid, and when the next consensus is expected to become valid)
264        pub fn voting_period(&self) -> time::Duration {
265            let valid_after = self.valid_after();
266            let fresh_until = self.fresh_until();
267            fresh_until
268                .duration_since(valid_after)
269                .expect("Mis-formed lifetime")
270        }
271    }
272}
273use derive_deftly_template_Lifetime;
274
275/// A single consensus method
276///
277/// These are integers, but we don't do arithmetic on them.
278///
279/// As defined here:
280/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:consensus-methods>
281/// <https://spec.torproject.org/dir-spec/computing-consensus.html#flavor:microdesc>
282///
283/// As used in a `consensus-method` item:
284/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:consensus-method>
285#[derive(Debug, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Copy)] //
286#[derive(derive_more::From, derive_more::Into, derive_more::Display, derive_more::FromStr)]
287pub struct ConsensusMethod(u32);
288impl NormalItemArgument for ConsensusMethod {}
289
290/// A set of consensus methods
291///
292/// Implements `ItemValueParseable` as required for `consensus-methods`,
293/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:consensus-methods>
294///
295/// There is also [`consensus_methods_comma_separated`] for `m` lines in votes.
296#[derive(Debug, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Deftly)]
297#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
298#[non_exhaustive]
299pub struct ConsensusMethods {
300    /// Consensus methods.
301    pub methods: BTreeSet<ConsensusMethod>,
302}
303
304/// Module for use with parse2's `with`, to parse one argument of comma-separated consensus methods
305///
306/// As found in an `m` item in a vote:
307/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:m>
308pub mod consensus_methods_comma_separated {
309    use super::*;
310    use parse2::ArgumentError as AE;
311    use std::result::Result;
312
313    /// Parse
314    pub fn from_args<'s>(args: &mut ArgumentStream<'s>) -> Result<ConsensusMethods, AE> {
315        let mut methods = BTreeSet::new();
316        for ent in args.next().ok_or(AE::Missing)?.split(',') {
317            let ent = ent.parse().map_err(|_| AE::Invalid)?;
318            if !methods.insert(ent) {
319                return Err(AE::Invalid);
320            }
321        }
322        Ok(ConsensusMethods { methods })
323    }
324
325    /// Encode
326    #[cfg(feature = "incomplete")] // untested
327    pub fn write_arg_onto(self_: &ConsensusMethods, out: &mut ItemEncoder) -> Result<(), Bug> {
328        out.args_raw_string(&iter_join(",", &self_.methods));
329        Ok(())
330    }
331}
332
333/// A set of named network parameters.
334///
335/// These are used to describe current settings for the Tor network,
336/// current weighting parameters for path selection, and so on.  They're
337/// encoded with a space-separated K=V format.
338///
339/// A `NetParams<i32>` is part of the validated directory manager configuration,
340/// where it is built (in the builder-pattern sense) from a transparent HashMap.
341///
342/// As found in `params` in a network status:
343/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:params>
344///
345/// The same syntax is also used, and this type used for parsing, in various other places,
346/// for example routerstatus entry `w` items (bandwidth weights):
347/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:w>
348//
349// TODO DIRAUTH torspec#401 Replace `String` with a suitable newtype
350// Currently:
351//  - Our parser allows any keyword that makes it into a netdoc argument,
352//    but it splits on the *first* `=` so a `NetParams<i32>` cannot parse a keyword with a `=`.
353//  - We provide constructors that allow any `String`, even ones containing space, `=`,
354//    newline, etc.
355//  - Encoding throws `Bug` if the resulting document will be clearly garbage,
356//    forbidding `=`, whitespace, and controls.  If the supplied keywords are bizarre,
357//    it may generate surprising documents (eg, containing exciting Unicode).
358#[derive(Debug, Clone, Default, Eq, PartialEq)]
359pub struct NetParams<T> {
360    /// Map from keys to values.
361    params: HashMap<String, T>,
362}
363
364impl<T> NetParams<T> {
365    /// Create a new empty list of NetParams.
366    #[allow(unused)]
367    pub fn new() -> Self {
368        NetParams {
369            params: HashMap::new(),
370        }
371    }
372    /// Retrieve a given network parameter, if it is present.
373    pub fn get<A: AsRef<str>>(&self, v: A) -> Option<&T> {
374        self.params.get(v.as_ref())
375    }
376    /// Return an iterator over all key value pairs in an arbitrary order.
377    pub fn iter(&self) -> impl Iterator<Item = (&String, &T)> {
378        self.params.iter()
379    }
380    /// Set or replace the value of a network parameter.
381    pub fn set(&mut self, k: String, v: T) {
382        self.params.insert(k, v);
383    }
384}
385
386impl<K: Into<String>, T> FromIterator<(K, T)> for NetParams<T> {
387    fn from_iter<I: IntoIterator<Item = (K, T)>>(i: I) -> Self {
388        NetParams {
389            params: i.into_iter().map(|(k, v)| (k.into(), v)).collect(),
390        }
391    }
392}
393
394impl<T> std::iter::Extend<(String, T)> for NetParams<T> {
395    fn extend<I: IntoIterator<Item = (String, T)>>(&mut self, iter: I) {
396        self.params.extend(iter);
397    }
398}
399
400impl<'de, T> Deserialize<'de> for NetParams<T>
401where
402    T: Deserialize<'de>,
403{
404    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
405    where
406        D: Deserializer<'de>,
407    {
408        let params = HashMap::deserialize(deserializer)?;
409        Ok(NetParams { params })
410    }
411}
412
413/// A list of subprotocol versions that implementors should/must provide.
414///
415/// This struct represents a pair of (optional) items:
416/// `recommended-FOO-protocols` and `required-FOO-protocols`.
417///
418/// Each consensus has two of these: one for relays, and one for clients.
419///
420/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:required-relay-protocols>
421#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
422pub struct ProtoStatus {
423    /// Set of protocols that are recommended; if we're missing a protocol
424    /// in this list we should warn the user.
425    ///
426    /// `recommended-client-protocols` or `recommended-relay-protocols`
427    recommended: Protocols,
428    /// Set of protocols that are required; if we're missing a protocol
429    /// in this list we should refuse to start.
430    ///
431    /// `required-client-protocols` or `required-relay-protocols`
432    required: Protocols,
433}
434
435impl ProtoStatus {
436    /// Check whether the list of supported protocols
437    /// is sufficient to satisfy this list of recommendations and requirements.
438    ///
439    /// If any required protocol is missing, returns [`ProtocolSupportError::MissingRequired`].
440    ///
441    /// Otherwise, if no required protocol is missing, but some recommended protocol is missing,
442    /// returns [`ProtocolSupportError::MissingRecommended`].
443    ///
444    /// Otherwise, if no recommended or required protocol is missing, returns `Ok(())`.
445    pub fn check_protocols(
446        &self,
447        supported_protocols: &Protocols,
448    ) -> Result<(), ProtocolSupportError> {
449        // Required protocols take precedence, so we check them first.
450        let missing_required = self.required.difference(supported_protocols);
451        if !missing_required.is_empty() {
452            return Err(ProtocolSupportError::MissingRequired(missing_required));
453        }
454        let missing_recommended = self.recommended.difference(supported_protocols);
455        if !missing_recommended.is_empty() {
456            return Err(ProtocolSupportError::MissingRecommended(
457                missing_recommended,
458            ));
459        }
460
461        Ok(())
462    }
463}
464
465/// A subprotocol that is recommended or required in the consensus was not present.
466#[derive(Clone, Debug, thiserror::Error)]
467#[cfg_attr(test, derive(PartialEq))]
468#[non_exhaustive]
469pub enum ProtocolSupportError {
470    /// At least one required protocol was not in our list of supported protocols.
471    #[error("Required protocols are not implemented: {0}")]
472    MissingRequired(Protocols),
473
474    /// At least one recommended protocol was not in our list of supported protocols.
475    ///
476    /// Also implies that no _required_ protocols were missing.
477    #[error("Recommended protocols are not implemented: {0}")]
478    MissingRecommended(Protocols),
479}
480
481impl ProtocolSupportError {
482    /// Return true if the suggested behavior for this error is a shutdown.
483    pub fn should_shutdown(&self) -> bool {
484        matches!(self, Self::MissingRequired(_))
485    }
486}
487
488impl HasKind for ProtocolSupportError {
489    fn kind(&self) -> tor_error::ErrorKind {
490        tor_error::ErrorKind::SoftwareDeprecated
491    }
492}
493
494/// A set of recommended and required protocols when running
495/// in various scenarios.
496///
497/// Represents the collection of four items: `{recommended,required}-{client,relay}-protocols`.
498///
499/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:required-relay-protocols>
500#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
501pub struct ProtoStatuses {
502    /// Lists of recommended and required subprotocol versions for clients
503    client: ProtoStatus,
504    /// Lists of recommended and required subprotocol versions for relays
505    relay: ProtoStatus,
506}
507
508impl ProtoStatuses {
509    /// Return the list of recommended and required protocols for running as a client.
510    pub fn client(&self) -> &ProtoStatus {
511        &self.client
512    }
513
514    /// Return the list of recommended and required protocols for running as a relay.
515    pub fn relay(&self) -> &ProtoStatus {
516        &self.relay
517    }
518}
519
520/// List of recommended Tor versions
521///
522/// As seen in `client-versions` and `server-versions` in the preamble.
523///
524/// Technically these are supposed to be as according to
525/// "`version-spec.txt`" but we actually allow anything that doesn't contain commas.
526///
527/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:client-versions>
528/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:server-versions>
529///
530/// An empty set means no information, not no recommended versions.
531//
532// TODO should we have a CommaSeparated<T> type for arguments like this?
533// But maybe we wouldn't be able to use it here anyway because of
534// the special handling of the missing value.
535//
536// This is yet a third version number representation in arti!  Here it's just String.
537// TODO unify RecommendedTorVersions, RelayPlatform, TorVersion
538#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)] //
539#[derive(derive_more::Deref, derive_more::Into)]
540pub struct RecommendedTorVersions(BTreeSet<String>);
541
542/// Erroneous "recommended Tor versions" information
543#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
544#[non_exhaustive]
545pub enum InvalidRecommendedTorVersions {
546    /// Identical version appears twice
547    #[error("version {_0:?} contains whitespace")]
548    ContainsWhitespace(String),
549
550    /// Identical version appears twice
551    #[error("version {_0:?} is repeated")]
552    Repeated(String),
553}
554
555impl RecommendedTorVersions {
556    /// Return a `RecommendedTorVersions` that has no information
557    pub fn new_unknown() -> Self {
558        Self::default()
559    }
560
561    /// Does this `RecommendedTorVersions` have any information?
562    ///
563    /// Ie, is it not empty.
564    ///
565    /// The opposite of [`BTreeSet::is_empty()`] (which available via deref).
566    pub fn is_known(&self) -> bool {
567        !self.is_empty()
568    }
569
570    /// Construct a RecommendedTorVersions from a list of strings
571    #[allow(clippy::should_implement_trait)] // we can't due to coherence
572    pub fn from_iter<I, S>(i: I) -> Result<Self, InvalidRecommendedTorVersions>
573    where
574        I: IntoIterator<Item = S>,
575        S: AsRef<str>,
576    {
577        let mut set = BTreeSet::new();
578        for v in i {
579            let v = v.as_ref();
580            if v.is_empty() {
581                continue;
582            }
583            if v.chars().any(|c| c.is_whitespace()) {
584                return Err(InvalidRecommendedTorVersions::ContainsWhitespace(
585                    v.to_owned(),
586                ));
587            }
588            if !set.insert(v.to_owned()) {
589                return Err(InvalidRecommendedTorVersions::Repeated(v.to_owned()));
590            }
591        }
592        Ok(RecommendedTorVersions(set))
593    }
594}
595
596impl FromStr for RecommendedTorVersions {
597    type Err = InvalidRecommendedTorVersions;
598    fn from_str(s: &str) -> Result<Self, InvalidRecommendedTorVersions> {
599        Self::from_iter(s.split(','))
600    }
601}
602
603impl Display for RecommendedTorVersions {
604    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
605        write!(f, "{}", iter_join(",", &self.0))
606    }
607}
608
609impl NormalItemArgument for RecommendedTorVersions {}
610
611impl ItemValueEncodable for RecommendedTorVersions {
612    fn write_item_value_onto(&self, mut out: ItemEncoder) -> Result<(), Bug> {
613        out.args_raw_string(self);
614        Ok(())
615    }
616}
617
618impl ItemValueParseable for RecommendedTorVersions {
619    fn from_unparsed(mut item: UnparsedItem) -> Result<Self, ErrorProblem> {
620        const FIELD: &str = "versions";
621        item.check_no_object()?;
622        let args = item.args_mut();
623        let arg = args.next().unwrap_or("");
624        arg.parse::<Self>()
625            .map_err(|_| args.handle_error(FIELD, ArgumentError::Invalid))
626    }
627}
628
629/// A recognized 'flavor' of consensus document.
630///
631/// The enum is exhaustive because the addition/removal of a consensus flavor
632/// should indeed be a breaking change, as it would inevitable require
633/// interfacing code to think about the handling of it.
634///
635/// <https://spec.torproject.org/dir-spec/computing-consensus.html#flavors>
636#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
637#[allow(clippy::exhaustive_enums)]
638pub enum ConsensusFlavor {
639    /// A "microdesc"-flavored consensus.  This is the one that
640    /// clients and relays use today.
641    Microdesc,
642    /// A "networkstatus"-flavored consensus.  It's used for
643    /// historical and network-health purposes.  Instead of listing
644    /// microdescriptor digests, it lists digests of full relay
645    /// descriptors.
646    Plain,
647}
648
649impl ConsensusFlavor {
650    /// Return the name of this consensus flavor.
651    pub fn name(&self) -> &'static str {
652        match self {
653            ConsensusFlavor::Plain => "ns", // spec bug, now baked in
654            ConsensusFlavor::Microdesc => "microdesc",
655        }
656    }
657    /// Try to find the flavor whose name is `name`.
658    ///
659    /// For historical reasons, an unnamed flavor indicates an "Plain"
660    /// document.
661    pub fn from_opt_name(name: Option<&str>) -> crate::Result<Self> {
662        match name {
663            Some("microdesc") => Ok(ConsensusFlavor::Microdesc),
664            Some("ns") | None => Ok(ConsensusFlavor::Plain),
665            Some(other) => {
666                Err(EK::BadDocumentType.with_msg(format!("unrecognized flavor {:?}", other)))
667            }
668        }
669    }
670}
671
672define_derive_deftly! {
673    /// Bespoke derives applied to [`DirectorySignatureHashAlgo`]
674    ///
675    /// Generates:
676    ///
677    ///  * [`DirectorySignaturesHashesAccu`]
678    ///  * [`DirectorySignaturesHashesAccu::update_from`]
679    ///  * [`DirectorySignaturesHashesAccu::hash_slice_for_verification`]
680    DirectorySignaturesHashesAccu:
681
682    ${define FNAME ${paste ${snake_case $vname}} }
683
684    /// `directory-signature`a hash algorithm argument
685    #[derive(Clone, Copy, Default, Debug, Eq, PartialEq, Deftly)]
686    #[derive_deftly(AsMutSelf)]
687    #[non_exhaustive]
688    pub struct DirectorySignaturesHashesAccu {
689      $(
690        ${vattrs doc}
691        pub $FNAME: Option<[u8; ${vmeta(hash_len) as expr}]>,
692      )
693
694      /// `sha1` but without the algorithm name
695      ///
696      /// This is needed because the hash includes the whole signature item keyword line,
697      /// and therefore a signature with the `sha1` explicitly stated,
698      /// and one without, have different hashes!
699      ///
700      /// So we mustn't use the `sha1` field for both implicit and explicit use of SHA-1,
701      /// or multiple signatures with different syntax would overwrite each others'
702      /// different hashes.
703      pub sha1_unnamed: Option<[u8; 20]>,
704    }
705
706    impl DirectorySignaturesHashesAccu {
707        /// Calculate the hash for a signature item and update this accumulator
708        fn update_from(
709            &mut self,
710            algo: &DigestAlgoInSignature,
711            body: &SignatureHashInputs,
712        ) {
713            // Update the hash in self.$UPDATE according to algorithm $AGLO
714            // (uses dynamic bindings of those parameters)
715            ${define HASH {
716                // Avoid recalculating if we don't need to
717                self.$UPDATE.get_or_insert_with(|| {
718                    let mut h = tor_llcrypto::d::$ALGO::new();
719                    h.update(body.body().body());
720                    h.update(body.signature_item_kw_spc);
721                    h.finalize().into()
722                });
723            }}
724
725            match &**algo {
726              $(
727                Some(KeywordOrString::Known($vtype)) => {
728                    ${define UPDATE $FNAME}
729                    ${define ALGO $vname}
730                    $HASH
731                }
732              )
733                None => {
734                    ${define UPDATE sha1_unnamed}
735                    ${define ALGO Sha1}
736                    $HASH
737                }
738                Some(KeywordOrString::Unknown(..)) => {}
739            }
740        }
741
742        /// Return the hash value for a specific algorithm, as a slice
743        ///
744        /// `None` if the value wasn't computed.
745        /// That shouldn't happen.
746        // TODO DIRAUTH make private when poc's verification is abolished
747        pub(crate) fn hash_slice_for_verification(
748            &self,
749            algo: &DigestAlgoInSignature,
750        ) -> Option<&[u8]> {
751            match &**algo {
752              $(
753                Some(KeywordOrString::Known($vtype)) => Some(self.$FNAME.as_ref()?),
754              )
755                None => Some(self.sha1_unnamed.as_ref()?),
756                Some(KeywordOrString::Unknown(..)) => None,
757            }
758        }
759    }
760}
761
762/// `directory-signature` hash algorithm argument
763#[derive(Clone, Copy, Debug, Eq, PartialEq, strum::Display, strum::EnumString, Deftly)]
764#[derive_deftly(DirectorySignaturesHashesAccu)]
765#[non_exhaustive]
766#[strum(serialize_all = "snake_case")]
767pub enum DirectorySignatureHashAlgo {
768    /// SHA-1
769    #[deftly(hash_len = "20")]
770    Sha1,
771    /// SHA-256
772    #[deftly(hash_len = "32")]
773    Sha256,
774}
775
776/// `algorithm` field in a `directory-signature` item
777///
778/// This is extremely bizarre: it's an *optional item at the start of the arguments*!
779// TODO SPEC #350
780///
781/// So we parse it with some kind of nightmarish lookahead.
782///
783/// Additionally, to be able to convey the signatures accurately, without breaking them,
784/// we must remember whether the argument was present.
785///
786/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:directory-signature>
787#[derive(Debug, Clone, derive_more::Deref, derive_more::DerefMut)]
788#[allow(clippy::exhaustive_structs)]
789pub struct DigestAlgoInSignature(pub Option<KeywordOrString<DirectorySignatureHashAlgo>>);
790
791impl ItemArgumentParseable for DigestAlgoInSignature {
792    fn from_args<'s>(args: &mut ArgumentStream<'s>) -> Result<Self, ArgumentError> {
793        let v = if args
794            .clone()
795            .next()
796            // Treat it as a fingerprint if it doesn't have any non-hex characters
797            // (including lowercase ones).  If we reuse this item for new algorithms
798            // they should have at least one letter g-z in their name.
799            .and_then(|s| s.chars().all(|c| c.is_ascii_hexdigit()).then_some(()))
800            .is_some()
801        {
802            // next argument looks enough like a fingerprint that we don't treat as an algo name
803            None
804        } else {
805            Some(KeywordOrString::from_args(args)?)
806        };
807        Ok(DigestAlgoInSignature(v))
808    }
809}
810impl ItemArgument for DigestAlgoInSignature {
811    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
812        if let Some(y) = &self.0 {
813            y.write_arg_onto(out)?;
814        }
815        Ok(())
816    }
817}
818impl DigestAlgoInSignature {
819    /// Return the actual algorithm
820    ///
821    /// This handles the defaulting, where an absent argument means `sha1`.
822    pub fn algorithm(&self) -> &KeywordOrString<DirectorySignatureHashAlgo> {
823        self.as_ref()
824            .unwrap_or(&KeywordOrString::Known(DirectorySignatureHashAlgo::Sha1))
825    }
826}
827
828impl NormalItemArgument for DirectorySignatureHashAlgo {}
829
830/// The signature of a single directory authority on a networkstatus document.
831///
832/// Implements `ItemValueParseable` which parses without hashing anything;
833/// this is mostly useful for use by the `SignatureItemParseable` implementation.
834#[derive(Debug, Clone, Deftly)]
835#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
836#[non_exhaustive]
837pub struct Signature {
838    /// The name of the digest algorithm used to make the signature.
839    ///
840    /// Currently sha1 and sh256 are recognized.  Here we only support
841    /// sha256.
842    pub digest_algo: DigestAlgoInSignature,
843    /// Fingerprints of the keys for the authority that made
844    /// this signature.
845    #[deftly(netdoc(with = authcert::keyids_directory_signature_args))]
846    pub key_ids: AuthCertKeyIds,
847    /// The signature itself.
848    #[deftly(netdoc(object(label = "SIGNATURE"), with = types::raw_data_object))]
849    pub signature: Vec<u8>,
850}
851
852impl SignatureItemParseable for Signature {
853    type HashAccu = DirectorySignaturesHashesAccu;
854
855    fn from_unparsed_and_body(
856        item: UnparsedItem,
857        body: &SignatureHashInputs<'_>,
858        hash: &mut Self::HashAccu,
859    ) -> Result<Self, ErrorProblem> {
860        let signature = Signature::from_unparsed(item)?;
861        hash.update_from(&signature.digest_algo, body);
862        Ok(signature)
863    }
864}
865
866/// A collection of signatures that can be checked on a networkstatus document
867///
868/// This is derived from the signatures section of a netstatus,
869/// <https://spec.torproject.org/dir-spec/consensus-formats.html#section:signature>,
870/// but it is not isomorphic to it, and is not directly parseable.
871#[derive(Debug, Clone)]
872#[non_exhaustive]
873pub struct SignatureGroup {
874    /// The document hashes of the signed part of the document
875    ///
876    /// The pre-parse2 parser always sets `hashes.sha1` and `hashes.sha1_unnamed`
877    /// to the same value, which is wrong. which is
878    /// [bug #2530](https://gitlab.torproject.org/tpo/core/arti/-/work_items/2530)
879    pub hashes: DirectorySignaturesHashesAccu,
880    /// The signatures listed on the document.
881    pub signatures: Vec<Signature>,
882}
883
884/// Error which will prevent us from attempting to verify signatures on a consensus
885///
886/// This error occurs if we the consensus isn't signed by the right people,
887/// or we are lacking authcerts.
888///
889/// Does not represent actual verification errors.
890/// Those show up as `VerifyFailed`, typically [`ConsensusVerifyFailed::InvalidSignature`].
891///
892/// Can be converted to a `VerifyFailed`,
893/// giving [`InsufficientTrustedSigners`](VerifyFailed::InsufficientTrustedSigners).
894#[derive(Clone, Debug, thiserror::Error)]
895#[non_exhaustive]
896// TODO DIRAUTH nothing tests that values in here are right, but there are no
897// public entrypoints that return one, so we don't need to cfg it "incomplete".
898pub enum ConsensusVerifiabilityError {
899    /// Insufficient trusted signers
900    #[error("consensus not signed by enough authorities")]
901    InsufficientTrustedSigners,
902
903    /// Insufficient trusted signers because we are missing authcerts
904    #[error("missing auth certs mean we could not verify enough consensuis signatures (need at least {deficit} more, out of {} that are missing)", missing.len())]
905    MissingAuthCerts {
906        /// The number of additional useful authcerts that would be sufficient
907        deficit: usize,
908        /// All the authcerts that would be useful
909        missing: HashSet<AuthCertKeyIds>,
910    },
911}
912
913/// Error encountered while verifying a consensus
914///
915/// Thrown by
916/// [`plain::NetworkStatusUnverified::verify`]
917/// and
918/// [`md::NetworkStatusUnverified::verify`].
919///
920/// Not used for problems with the validity period:
921/// that's handled by `tor-checkable` and shows up as [`tor_checkable::TimeValidityError`].
922///
923/// Can be converted to a `VerifyFailed` (which, in effect, summarises the error).
924#[derive(Clone, Debug, thiserror::Error)]
925#[non_exhaustive]
926pub enum ConsensusVerifyFailed {
927    /// Certificates or signatures insufficient
928    #[error("certs/sigs insufficient")]
929    CertificationInsufficient(#[from] ConsensusVerifiabilityError),
930
931    /// One or more signatures failed to verify
932    #[error("invalid signature")]
933    //
934    // Not `#[from]` because we don't want to accidentally convert
935    // ConsensusVerifiabilityError -> VerifyFailed -> ConsensusVerifyFailed
936    // since that would give the wrong variant.
937    InvalidSignature(#[source] VerifyFailed),
938}
939
940/// Error encountered while verifying a vote
941///
942/// Thrown by
943/// [`vote::NetworkStatusUnverified::verify`].
944///
945/// Not used for problems with the validity period:
946/// that's handled by `tor-checkable` and shows up as [`tor_checkable::TimeValidityError`].
947///
948/// Can be converted to a `VerifyFailed` (which, in effect, summarises the error).
949#[derive(Clone, Debug, thiserror::Error)]
950#[non_exhaustive]
951pub enum VoteVerifyFailed {
952    /// The document signature failed to verify
953    #[error("invalid signature")]
954    //
955    // Not `#[from]` because we don't want to accidentally convert
956    // VoteVerifyFailed::Something -> VerifyFailed -> VoteVerifyFailed
957    // since that would give the wrong variant.
958    InvalidSignature(#[source] VerifyFailed),
959
960    /// Authcert couldn't be parsed
961    #[error("unparseable authcert")]
962    AuthCertParseError(#[source] parse2::ParseError),
963
964    /// Authcert isn't valid for this vote's validity period
965    #[error("authcert not valid for vote period")]
966    AuthCertWrongValidity(#[source] tor_checkable::TimeValidityError),
967
968    /// Authcert is for a different authority
969    #[error("wrong authcert")]
970    AuthCertWrongAuthority,
971}
972
973/// A shared random value produced by the directory authorities.
974#[derive(
975    Debug, Clone, Copy, Eq, PartialEq, derive_more::From, derive_more::Into, derive_more::AsRef,
976)]
977// (This doesn't need to use CtByteArray; we don't really need to compare these.)
978pub struct SharedRandVal([u8; 32]);
979
980/// A shared-random value produced by the directory authorities,
981/// along with meta-information about that value.
982#[derive(Debug, Clone, Deftly)]
983#[non_exhaustive]
984#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
985pub struct SharedRandStatus {
986    /// How many authorities revealed shares that contributed to this value.
987    pub n_reveals: u8,
988    /// The current random value.
989    ///
990    /// The properties of the secure shared-random system guarantee
991    /// that this value isn't predictable before it first becomes
992    /// live, and that a hostile party could not have forced it to
993    /// have any more than a small number of possible random values.
994    pub value: SharedRandVal,
995
996    /// The time when this SharedRandVal becomes (or became) the latest.
997    ///
998    /// (This is added per proposal 342, assuming that gets accepted.)
999    pub timestamp: Option<Iso8601TimeNoSp>,
1000}
1001
1002/// The two shared random values, `shared-rand-*-value`
1003///
1004/// As found in the consensus preamble
1005/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:shared-rand-current-value>
1006/// and a vote's authority section
1007/// <https://spec.torproject.org/dir-spec/consensus-formats.html#authority-item-shared-rand-value>
1008#[derive(Debug, Clone, Default, Deftly)]
1009#[derive_deftly(Constructor, NetdocEncodableFields, NetdocParseableFields)]
1010#[allow(clippy::exhaustive_structs)]
1011pub struct SharedRandStatuses {
1012    /// Global shared-random value for the previous shared-random period.
1013    pub shared_rand_previous_value: Option<SharedRandStatus>,
1014
1015    /// Global shared-random value for the current shared-random period.
1016    pub shared_rand_current_value: Option<SharedRandStatus>,
1017
1018    #[doc(hidden)]
1019    #[deftly(netdoc(skip))]
1020    pub __non_exhaustive: (),
1021}
1022
1023/// Relay weight information - `w` item in routerstatus
1024///
1025/// This is a combination of two representations of (subsets of) the same information,
1026/// from an optional `w` in the document.
1027///
1028///  * [`effective`](RelayWeightsItem::effective):
1029///
1030///    Always contains the effective weight, as [`RelayWeight`].
1031///    This is what is used by clients.
1032///    It does not record whether a `w` line was actually present.
1033///
1034///  * [`params`](RelayWeightsItem::params):
1035///
1036///    Can represent the presence and whole contents of the `w` line,
1037///    including all the known and unknown parameters.
1038///    This is within [`Unknown`], so it is only present with crate `feature = "retain-unknown"`,
1039///    and only some constructors/parsers record it.
1040///
1041/// # Parsing
1042///
1043/// Parsing is done with `NetdocParseableFields` rather than `ItemValueParseable`.
1044/// The `params` are [`Retained`](Unknown::Retained) if `retain_unknown_values` is
1045/// selected in [`parse2::ParseOptions`].
1046//
1047// We use NetdocParseableFields because the containing document, RouterStatus,
1048// contains `RelayWeightsItem` rather than `Option<RelayWeightsItem>`.
1049// The item parsing multiplicity machinery would see plain `RelayWeightsItem` as a required item.
1050//
1051// This representation also means so that if retaining unknown information is compiled out
1052// (ie, in clients) each routerstatus entry stored in memory does not need to record
1053// whether `w` was present, merely what the implications were.
1054//
1055// We can't use ItemValueParseable with #[deftly(netdoc(default))]
1056// because `RelayWeightsItem::default()` is a RelayWeightsItem that definitively
1057// contains no pazrameters, ie with `Unknown::Retained`,
1058// and is therefore only conditionally available.
1059/// # Encoding
1060///
1061/// Encoding requires knowing whether a `w` line is to be included, and its contents,
1062/// so is implemented only with if `effective` is `Unknown::Retained`.
1063/// The encoding impl is only compiled in with `"retain-unknown"`,
1064/// and throws [`Bug`] if applied to a `RelayWeightsItem` whose `params` are `Discarded`.
1065///
1066/// # Constructors
1067///
1068/// An "empty" `RelayWeightsItem` can be constructed with [`RelayWeightsItem::new_no_info`].
1069///
1070/// A `RelayWeightsItem` containing only the effective `RelayWeight`
1071/// can be constructed using [`RelayWeightsItem::from_effective`].
1072///
1073/// With `"retain-unknown"`:
1074/// a `RelayWeightsItem` can be constructed from a [`NetParams<u32>`] using `TryFrom`;
1075/// and, implements `Default`, which yields a `RelayWeightsItem`
1076/// representing the (known) absence of a `w` line.
1077//
1078// Fields are private to maintain the invariant.
1079#[derive(Debug, Clone)]
1080pub struct RelayWeightsItem {
1081    /// The effective relay weight
1082    effective: RelayWeight,
1083
1084    /// The complete parameter set, if available and `w` was present.
1085    params: Unknown<Option<NetParams<u32>>>,
1086}
1087
1088/// Recognized weight fields on a single relay in a consensus
1089///
1090/// The part of a `w` item that we understand as a client.
1091#[non_exhaustive]
1092#[derive(Debug, Clone, Copy)]
1093pub enum RelayWeight {
1094    /// An unmeasured weight for a relay.
1095    Unmeasured(u32),
1096    /// An measured weight for a relay.
1097    Measured(u32),
1098}
1099
1100/// Error processing a `w` line's netparams into an effective relay weight
1101#[derive(Debug, Clone, thiserror::Error)]
1102#[non_exhaustive]
1103pub enum InvalidRelayWeights {
1104    /// Invalid value for `Unmeasured`
1105    #[error("invalid value for Unmeasured")]
1106    InvalidUnmeasured,
1107}
1108
1109/// Authority entry in a consensus - deprecated compatibility type alias
1110#[deprecated = "renamed to ConsensusAuthorityEntry"]
1111pub type ConsensusVoterInfo = ConsensusAuthorityEntry;
1112
1113/// Authority entry in a plain consensus - type alias provided for consistency
1114pub type PlainAuthorityEntry = ConsensusAuthorityEntry;
1115/// Authority entry in an md consensus - type alias provided for consistency
1116pub type MdAuthorityEntry = ConsensusAuthorityEntry;
1117
1118/// An authority entry as found in a consensus
1119///
1120/// <https://spec.torproject.org/dir-spec/consensus-formats.html#section:authority-entry>
1121///
1122/// See also [`VoteAuthorityEntry`]
1123//
1124// We don't use the `each_variety` system for this because:
1125//  1. That avoids separating the two consensus authority entry types, which are identical
1126//  2. The only common fields are `dir-source` and `contact`, so there is little duplication
1127#[derive(Debug, Clone, Deftly)]
1128#[derive_deftly(Constructor, NetdocEncodable, NetdocParseable)]
1129#[allow(clippy::exhaustive_structs)]
1130pub struct ConsensusAuthorityEntry {
1131    /// Contents of the `dir-source` line about an authority
1132    #[deftly(constructor)]
1133    pub dir_source: DirSource,
1134
1135    /// Human-readable contact information about the authority
1136    //
1137    // If more non-intro fields get added that are the same in votes and cosensuses,
1138    // consider using each_variety.rs or breaking those fields out into
1139    // `AuthorityEntryCommon` implementing `NetdocParseableFields`, or something.
1140    #[deftly(constructor)]
1141    pub contact: ContactInfo,
1142
1143    /// Digest of the vote that the authority cast to contribute to
1144    /// this consensus.
1145    ///
1146    /// This is not a fixed-length, fixed-algorithm field.
1147    /// Bizarrely, the algorithm is supposed to be inferred from the length!
1148    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:vote-digest>
1149    #[deftly(netdoc(single_arg))]
1150    #[deftly(constructor)]
1151    pub vote_digest: B16U,
1152
1153    #[doc(hidden)]
1154    #[deftly(netdoc(skip))]
1155    pub __non_exhaustive: (),
1156}
1157
1158/// An authority entry as found in a vote
1159///
1160/// <https://spec.torproject.org/dir-spec/consensus-formats.html#section:authority-entry>
1161///
1162/// See also [`ConsensusAuthorityEntry`]
1163#[derive(Debug, Clone, Deftly)]
1164#[derive_deftly(Constructor, NetdocEncodable, NetdocParseable)]
1165#[allow(clippy::exhaustive_structs)]
1166pub struct VoteAuthorityEntry {
1167    /// Contents of the `dir-source` line about an authority
1168    #[deftly(constructor)]
1169    pub dir_source: DirSource,
1170
1171    /// Human-readable contact information about the authority
1172    #[deftly(constructor)]
1173    pub contact: ContactInfo,
1174
1175    /// `legacy-dir-key` - superseded authority identity key
1176    ///
1177    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:legacy-dir-key>
1178    #[deftly(netdoc(single_arg))]
1179    pub legacy_dir_key: Option<Fingerprint>,
1180
1181    /// `shared-rand-participate` - Indicate shared random participation
1182    ///
1183    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:shared-rand-participate>
1184    pub shared_rand_participate: Option<SharedRandParticipate>,
1185
1186    /// `shared-rand-commit` - Shared random commitment
1187    ///
1188    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:shared-rand-commit>
1189    pub shared_rand_commit: Vec<SharedRandCommit>,
1190
1191    /// Global shared-random values
1192    #[deftly(netdoc(flatten))]
1193    pub shared_rand: SharedRandStatuses,
1194
1195    #[doc(hidden)]
1196    #[deftly(netdoc(skip))]
1197    pub __non_exhaustive: (),
1198}
1199
1200/// `shared-rand-participate` in a vote authority entry
1201///
1202/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:shared-rand-participate>
1203//
1204// We could have done `shared_rand_participate: Option<()>` in VoteAuthorityEntry,
1205// but then we might end up with variables of type `&Option<()>` etc.
1206// whose meaning has been detached from its type.
1207//
1208// TODO DIRAUTH rework this according to the API design conclusion from !3977 when there is one
1209#[derive(Debug, Clone, Deftly)]
1210#[derive_deftly(Constructor, ItemValueEncodable, ItemValueParseable)]
1211#[allow(clippy::exhaustive_structs)]
1212pub struct SharedRandParticipate {
1213    #[doc(hidden)]
1214    #[deftly(netdoc(skip))]
1215    pub __non_exhaustive: (),
1216}
1217
1218/// `shared-rand-commit` in a vote authority entry
1219///
1220/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:shared-rand-commit>
1221#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deftly)]
1222// If new protocols use this item with a different version, we'll call it an API break.
1223#[allow(clippy::exhaustive_enums)]
1224pub enum SharedRandCommit {
1225    /// Version 1, the only one supported
1226    V1(SharedRandCommitV1),
1227
1228    /// Other versions.  Cannot be encoded.
1229    // It's not clear that future versions will use this version mechanism.  torspec#408.
1230    Unknown {},
1231}
1232
1233/// `shared-rand-commit` in a vote authority entry
1234///
1235/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:shared-rand-commit>
1236///
1237/// Version and hash are not explicitly represented.  See torspec#407.
1238///
1239/// `ItemValueEncodable` and `ItemValueParseable` impls do not include the fixed arguments;
1240/// in a netdoc, this type should be used within `SharedRandCommit::V1`.
1241#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deftly)]
1242#[derive_deftly(Constructor, ItemValueEncodable, ItemValueParseable)]
1243#[allow(clippy::exhaustive_structs)]
1244pub struct SharedRandCommitV1 {
1245    /// Authority id key, recapitulated.
1246    // TODO this field shouldn't here at all torspec#407
1247    #[deftly(constructor)]
1248    h_kp_auth_id_rsa: Fingerprint,
1249
1250    /// Commitment
1251    ///
1252    /// `TIMESTAMP || SHA3_256(REVEAL)`, as per
1253    /// <https://spec.torproject.org/srv-spec/specification.html#COMMITREVEAL>
1254    //
1255    // TOOD we would like to replace this with a type that separates out the pieces!
1256    // But that would need a FixedB64 generic over some tor-bytes trait, or something.
1257    #[deftly(constructor)]
1258    commit: FixedB64<40>,
1259
1260    /// Reveal
1261    ///
1262    /// `TIMESTAMP || random number`, as per
1263    /// <https://spec.torproject.org/srv-spec/specification.html#COMMITREVEAL>
1264    reveal: Option<FixedB64<40>>,
1265
1266    #[doc(hidden)]
1267    #[deftly(netdoc(skip))]
1268    pub __non_exhaustive: (),
1269}
1270
1271impl SharedRandCommitV1 {
1272    /// The fixed arguments that precede the actual value in `shared-rand-commit 1 ...`
1273    const FIXED_ARGUMENTS: &[&str] = &["1", "sha3-256"];
1274}
1275impl ItemValueEncodable for SharedRandCommit {
1276    fn write_item_value_onto(&self, mut out: ItemEncoder) -> Result<(), Bug> {
1277        match self {
1278            SharedRandCommit::V1(values) => {
1279                for fixed in SharedRandCommitV1::FIXED_ARGUMENTS {
1280                    out.args_raw_string(fixed);
1281                }
1282                values.write_item_value_onto(out)
1283            }
1284            SharedRandCommit::Unknown {} => Err(internal!("encoding SharedRandCommit::Unknown")),
1285        }
1286    }
1287}
1288impl ItemValueParseable for SharedRandCommit {
1289    fn from_unparsed(mut item: UnparsedItem<'_>) -> Result<Self, ErrorProblem> {
1290        let mut fixed = SharedRandCommitV1::FIXED_ARGUMENTS.iter().copied();
1291        let args = item.args_mut();
1292        let version = args
1293            .next()
1294            .ok_or_else(|| args.handle_error("version", ArgumentError::Missing))?;
1295        if version != fixed.next().expect("nonempty") {
1296            return Ok(SharedRandCommit::Unknown {});
1297        }
1298        for exp in fixed {
1299            let got = args
1300                .next()
1301                .ok_or_else(|| args.handle_error(exp, ArgumentError::Missing))?;
1302            if got != exp {
1303                return Err(args.handle_error(exp, ArgumentError::Invalid))?;
1304            }
1305        }
1306        let values = SharedRandCommitV1::from_unparsed(item)?;
1307        Ok(SharedRandCommit::V1(values))
1308    }
1309}
1310
1311// For `ConsensusAuthoritySection`, see `dir_source.rs`.
1312
1313define_derive_deftly! {
1314    /// Ad-hoc derive, `impl NetdocParseable for VoteAuthoritySection`
1315    ///
1316    /// We can't derive from `VoteAuthoritySection` with the normal macros, because
1317    /// it's not a document, with its own intro item.  It's just a collection of sub-documents.
1318    /// The netdoc derive macros don't have support for that - and it would be a fairly
1319    /// confusing thing to support because you'd end up with nested multiplicities and a whole
1320    /// variety of "intro item keywords" that were keywords for arbitrary sub-documents.
1321    ///
1322    /// Instead, we do that ad-hoc here.  It's less confusing because we don't need to
1323    /// worry about multiplicity, and because we know what only the outer document is
1324    /// that will contain this.
1325    VoteAuthoritySection:
1326
1327    ${defcond F_NORMAL not(fmeta(netdoc(skip)))}
1328
1329    #[cfg(feature = "incomplete")] // needs EncodedAuthCert, otherwise complete
1330    impl NetdocParseable for VoteAuthoritySection {
1331        fn doctype_for_error() -> &'static str {
1332            "vote.authority.section"
1333        }
1334        fn is_intro_item_keyword(kw: KeywordRef<'_>) -> bool {
1335            VoteAuthorityEntry::is_intro_item_keyword(kw)
1336        }
1337        fn is_structural_keyword(kw: KeywordRef<'_>) -> Option<IsStructural> {
1338          $(
1339            ${when F_NORMAL}
1340            if let y @ Some(_) = $ftype::is_structural_keyword(kw) {
1341                return y;
1342            }
1343          )
1344            None
1345        }
1346        fn from_items<'s>(
1347            input: &mut ItemStream<'s>,
1348            stop_outer: stop_at!(),
1349        ) -> Result<Self, ErrorProblem> {
1350            let stop_inner = stop_outer
1351              $(
1352                ${when F_NORMAL}
1353                | StopAt($ftype::is_intro_item_keyword)
1354              )
1355            ;
1356            Ok(VoteAuthoritySection { $(
1357                ${when F_NORMAL}
1358                $fname: NetdocParseable::from_items(input, stop_inner)?,
1359            )
1360                __non_exhaustive: (),
1361            })
1362        }
1363    }
1364
1365    #[cfg(feature = "incomplete")]
1366    impl NetdocEncodable for VoteAuthoritySection {
1367        fn encode_unsigned(&self, out: &mut NetdocEncoder) -> Result<(), Bug> {
1368          $(
1369            ${when F_NORMAL}
1370            self.$fname.encode_unsigned(out)?;
1371          )
1372          Ok(())
1373        }
1374    }
1375}
1376
1377/// An authority section in a vote
1378///
1379/// <https://spec.torproject.org/dir-spec/consensus-formats.html#section:authority>
1380//
1381// We have split this out to help encapsulate vote/consensus-specific
1382// information in a forthcoming overall network status document type.
1383#[derive(Deftly, Clone, Debug)]
1384#[derive_deftly(VoteAuthoritySection, Constructor)]
1385#[allow(clippy::exhaustive_structs)]
1386#[cfg(feature = "incomplete")] // needs EncodedAuthCert, otherwise complete
1387pub struct VoteAuthoritySection {
1388    /// Authority entry
1389    #[deftly(constructor)]
1390    pub authority: VoteAuthorityEntry,
1391
1392    /// Authority key certificate
1393    #[deftly(constructor)]
1394    pub cert: EmbeddedCert<AuthCert, EncodedAuthCert>,
1395
1396    #[doc(hidden)]
1397    #[deftly(netdoc(skip))]
1398    pub __non_exhaustive: (),
1399}
1400
1401/// Fields in the footer of a consensus
1402///
1403/// <https://spec.torproject.org/dir-spec/consensus-formats.html#section:footer>
1404///
1405/// Not the whole footer, because it lacks the `directory-footer` item.
1406#[derive(Debug, Clone, Deftly)]
1407#[derive_deftly(Constructor, NetdocEncodableFields, NetdocParseableFields)]
1408#[allow(clippy::exhaustive_structs)]
1409pub struct ConsensusFooterFields {
1410    /// `bandwidth-weights`
1411    ///
1412    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:bandwidth-weights>
1413    #[deftly(netdoc(default))]
1414    pub bandwidth_weights: NetParams<i32>,
1415
1416    #[doc(hidden)]
1417    #[deftly(netdoc(skip))]
1418    pub __non_exhaustive: (),
1419}
1420
1421/// A consensus document that lists relays along with their
1422/// microdescriptor documents.
1423pub type MdConsensus = md::Consensus;
1424
1425/// An MdConsensus that has been parsed and checked for timeliness,
1426/// but not for signatures.
1427pub type UnvalidatedMdConsensus = md::UnvalidatedConsensus;
1428
1429/// An MdConsensus that has been parsed but not checked for signatures
1430/// and timeliness.
1431pub type UncheckedMdConsensus = md::UncheckedConsensus;
1432
1433/// A consensus document that lists relays along with their
1434/// router descriptor documents.
1435pub type PlainConsensus = plain::Consensus;
1436
1437/// An PlainConsensus that has been parsed and checked for timeliness,
1438/// but not for signatures.
1439pub type UnvalidatedPlainConsensus = plain::UnvalidatedConsensus;
1440
1441/// An PlainConsensus that has been parsed but not checked for signatures
1442/// and timeliness.
1443pub type UncheckedPlainConsensus = plain::UncheckedConsensus;
1444
1445decl_keyword! {
1446    /// Keywords that can be used in votes and consensuses.
1447    // TODO: This is public because otherwise we can't use it in the
1448    // ParseRouterStatus crate.  But I'd rather find a way to make it
1449    // private.
1450    #[non_exhaustive]
1451    #[allow(missing_docs)]
1452    pub NetstatusKwd {
1453        // Header
1454        "network-status-version" => NETWORK_STATUS_VERSION,
1455        "vote-status" => VOTE_STATUS,
1456        "consensus-methods" => CONSENSUS_METHODS,
1457        "consensus-method" => CONSENSUS_METHOD,
1458        "published" => PUBLISHED,
1459        "valid-after" => VALID_AFTER,
1460        "fresh-until" => FRESH_UNTIL,
1461        "valid-until" => VALID_UNTIL,
1462        "voting-delay" => VOTING_DELAY,
1463        "client-versions" => CLIENT_VERSIONS,
1464        "server-versions" => SERVER_VERSIONS,
1465        "known-flags" => KNOWN_FLAGS,
1466        "flag-thresholds" => FLAG_THRESHOLDS,
1467        "recommended-client-protocols" => RECOMMENDED_CLIENT_PROTOCOLS,
1468        "required-client-protocols" => REQUIRED_CLIENT_PROTOCOLS,
1469        "recommended-relay-protocols" => RECOMMENDED_RELAY_PROTOCOLS,
1470        "required-relay-protocols" => REQUIRED_RELAY_PROTOCOLS,
1471        "params" => PARAMS,
1472        "bandwidth-file-headers" => BANDWIDTH_FILE_HEADERS,
1473        "bandwidth-file-digest" => BANDWIDTH_FILE_DIGEST,
1474        // "package" is now ignored.
1475
1476        // header in consensus, voter section in vote?
1477        "shared-rand-previous-value" => SHARED_RAND_PREVIOUS_VALUE,
1478        "shared-rand-current-value" => SHARED_RAND_CURRENT_VALUE,
1479
1480        // Voter section (both)
1481        "dir-source" => DIR_SOURCE,
1482        "contact" => CONTACT,
1483
1484        // voter section (vote, but not consensus)
1485        "legacy-dir-key" => LEGACY_DIR_KEY,
1486        "shared-rand-participate" => SHARED_RAND_PARTICIPATE,
1487        "shared-rand-commit" => SHARED_RAND_COMMIT,
1488
1489        // voter section (consensus, but not vote)
1490        "vote-digest" => VOTE_DIGEST,
1491
1492        // voter cert beginning (but only the beginning)
1493        "dir-key-certificate-version" => DIR_KEY_CERTIFICATE_VERSION,
1494
1495        // routerstatus
1496        "r" => RS_R,
1497        "a" => RS_A,
1498        "s" => RS_S,
1499        "v" => RS_V,
1500        "pr" => RS_PR,
1501        "w" => RS_W,
1502        "p" => RS_P,
1503        "m" => RS_M,
1504        "id" => RS_ID,
1505
1506        // footer
1507        "directory-footer" => DIRECTORY_FOOTER,
1508        "bandwidth-weights" => BANDWIDTH_WEIGHTS,
1509        "directory-signature" => DIRECTORY_SIGNATURE,
1510    }
1511}
1512
1513/// Shared parts of rules for all kinds of netstatus headers
1514static NS_HEADER_RULES_COMMON_: LazyLock<SectionRulesBuilder<NetstatusKwd>> = LazyLock::new(|| {
1515    use NetstatusKwd::*;
1516    let mut rules = SectionRules::builder();
1517    rules.add(NETWORK_STATUS_VERSION.rule().required().args(1..=2));
1518    rules.add(VOTE_STATUS.rule().required().args(1..));
1519    rules.add(VALID_AFTER.rule().required());
1520    rules.add(FRESH_UNTIL.rule().required());
1521    rules.add(VALID_UNTIL.rule().required());
1522    rules.add(VOTING_DELAY.rule().args(2..));
1523    rules.add(CLIENT_VERSIONS.rule());
1524    rules.add(SERVER_VERSIONS.rule());
1525    rules.add(KNOWN_FLAGS.rule().required());
1526    rules.add(RECOMMENDED_CLIENT_PROTOCOLS.rule().args(1..));
1527    rules.add(RECOMMENDED_RELAY_PROTOCOLS.rule().args(1..));
1528    rules.add(REQUIRED_CLIENT_PROTOCOLS.rule().args(1..));
1529    rules.add(REQUIRED_RELAY_PROTOCOLS.rule().args(1..));
1530    rules.add(PARAMS.rule());
1531    rules
1532});
1533/// Rules for parsing the header of a consensus.
1534static NS_HEADER_RULES_CONSENSUS: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
1535    use NetstatusKwd::*;
1536    let mut rules = NS_HEADER_RULES_COMMON_.clone();
1537    rules.add(CONSENSUS_METHOD.rule().args(1..=1));
1538    rules.add(SHARED_RAND_PREVIOUS_VALUE.rule().args(2..));
1539    rules.add(SHARED_RAND_CURRENT_VALUE.rule().args(2..));
1540    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
1541    rules.build()
1542});
1543/*
1544/// Rules for parsing the header of a vote.
1545static NS_HEADER_RULES_VOTE: SectionRules<NetstatusKwd> = {
1546    use NetstatusKwd::*;
1547    let mut rules = NS_HEADER_RULES_COMMON_.clone();
1548    rules.add(CONSENSUS_METHODS.rule().args(1..));
1549    rules.add(FLAG_THRESHOLDS.rule());
1550    rules.add(BANDWIDTH_FILE_HEADERS.rule());
1551    rules.add(BANDWIDTH_FILE_DIGEST.rule().args(1..));
1552    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
1553    rules
1554};
1555/// Rules for parsing a single voter's information in a vote.
1556static NS_VOTERINFO_RULES_VOTE: SectionRules<NetstatusKwd> = {
1557    use NetstatusKwd::*;
1558    let mut rules = SectionRules::new();
1559    rules.add(DIR_SOURCE.rule().required().args(6..));
1560    rules.add(CONTACT.rule().required());
1561    rules.add(LEGACY_DIR_KEY.rule().args(1..));
1562    rules.add(SHARED_RAND_PARTICIPATE.rule().no_args());
1563    rules.add(SHARED_RAND_COMMIT.rule().may_repeat().args(4..));
1564    rules.add(SHARED_RAND_PREVIOUS_VALUE.rule().args(2..));
1565    rules.add(SHARED_RAND_CURRENT_VALUE.rule().args(2..));
1566    // then comes an entire cert: When we implement vote parsing,
1567    // we should use the authcert code for handling that.
1568    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
1569    rules
1570};
1571 */
1572/// Rules for parsing a single voter's information in a consensus
1573static NS_VOTERINFO_RULES_CONSENSUS: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
1574    use NetstatusKwd::*;
1575    let mut rules = SectionRules::builder();
1576    rules.add(DIR_SOURCE.rule().required().args(6..));
1577    rules.add(CONTACT.rule().required());
1578    rules.add(VOTE_DIGEST.rule().required());
1579    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
1580    rules.build()
1581});
1582/// Shared rules for parsing a single routerstatus
1583static NS_ROUTERSTATUS_RULES_COMMON_: LazyLock<SectionRulesBuilder<NetstatusKwd>> =
1584    LazyLock::new(|| {
1585        use NetstatusKwd::*;
1586        let mut rules = SectionRules::builder();
1587        rules.add(RS_A.rule().may_repeat().args(1..));
1588        rules.add(RS_S.rule().required());
1589        rules.add(RS_V.rule());
1590        rules.add(RS_PR.rule().required());
1591        rules.add(RS_W.rule());
1592        rules.add(RS_P.rule().args(2..));
1593        rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
1594        rules
1595    });
1596
1597/// Rules for parsing a single routerstatus in an NS consensus
1598static NS_ROUTERSTATUS_RULES_PLAIN: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
1599    use NetstatusKwd::*;
1600    let mut rules = NS_ROUTERSTATUS_RULES_COMMON_.clone();
1601    rules.add(RS_R.rule().required().args(8..));
1602    rules.build()
1603});
1604
1605/*
1606/// Rules for parsing a single routerstatus in a vote
1607static NS_ROUTERSTATUS_RULES_VOTE: SectionRules<NetstatusKwd> = {
1608    use NetstatusKwd::*;
1609        let mut rules = NS_ROUTERSTATUS_RULES_COMMON_.clone();
1610        rules.add(RS_R.rule().required().args(8..));
1611        rules.add(RS_M.rule().may_repeat().args(2..));
1612        rules.add(RS_ID.rule().may_repeat().args(2..)); // may-repeat?
1613        rules
1614    };
1615*/
1616/// Rules for parsing a single routerstatus in a microdesc consensus
1617static NS_ROUTERSTATUS_RULES_MDCON: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
1618    use NetstatusKwd::*;
1619    let mut rules = NS_ROUTERSTATUS_RULES_COMMON_.clone();
1620    rules.add(RS_R.rule().required().args(6..));
1621    rules.add(RS_M.rule().required().args(1..));
1622    rules.build()
1623});
1624/// Rules for parsing consensus fields from a footer.
1625static NS_FOOTER_RULES: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
1626    use NetstatusKwd::*;
1627    let mut rules = SectionRules::builder();
1628    rules.add(DIRECTORY_FOOTER.rule().required().no_args());
1629    // consensus only
1630    rules.add(BANDWIDTH_WEIGHTS.rule());
1631    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
1632    rules.build()
1633});
1634
1635impl ProtoStatus {
1636    /// Construct a ProtoStatus from two chosen keywords in a section.
1637    fn from_section(
1638        sec: &Section<'_, NetstatusKwd>,
1639        recommend_token: NetstatusKwd,
1640        required_token: NetstatusKwd,
1641    ) -> crate::Result<ProtoStatus> {
1642        /// Helper: extract a Protocols entry from an item's arguments.
1643        fn parse(t: Option<&Item<'_, NetstatusKwd>>) -> crate::Result<Protocols> {
1644            if let Some(item) = t {
1645                item.args_as_str()
1646                    .parse::<Protocols>()
1647                    .map_err(|e| EK::BadArgument.at_pos(item.pos()).with_source(e))
1648            } else {
1649                Ok(Protocols::new())
1650            }
1651        }
1652
1653        let recommended = parse(sec.get(recommend_token))?;
1654        let required = parse(sec.get(required_token))?;
1655        Ok(ProtoStatus {
1656            recommended,
1657            required,
1658        })
1659    }
1660
1661    /// Return the protocols that are listed as "required" in this `ProtoStatus`.
1662    ///
1663    /// Implementations may assume that relays on the network implement all the
1664    /// protocols in the relays' required-protocols list.  Implementations should
1665    /// refuse to start if they do not implement all the protocols on their own
1666    /// (client or relay) required-protocols list.
1667    pub fn required_protocols(&self) -> &Protocols {
1668        &self.required
1669    }
1670
1671    /// Return the protocols that are listed as "recommended" in this `ProtoStatus`.
1672    ///
1673    /// Implementations should warn if they do not implement all the protocols
1674    /// on their own (client or relay) recommended-protocols list.
1675    pub fn recommended_protocols(&self) -> &Protocols {
1676        &self.recommended
1677    }
1678}
1679
1680impl<T> std::str::FromStr for NetParams<T>
1681where
1682    T: std::str::FromStr,
1683    T::Err: std::error::Error,
1684{
1685    type Err = Error;
1686    fn from_str(s: &str) -> crate::Result<Self> {
1687        /// Helper: parse a single K=V pair.
1688        fn parse_pair<U>(p: &str) -> crate::Result<(String, U)>
1689        where
1690            U: std::str::FromStr,
1691            U::Err: std::error::Error,
1692        {
1693            let parts: Vec<_> = p.splitn(2, '=').collect();
1694            if parts.len() != 2 {
1695                return Err(EK::BadArgument
1696                    .at_pos(Pos::at(p))
1697                    .with_msg("Missing = in key=value list"));
1698            }
1699            let num = parts[1].parse::<U>().map_err(|e| {
1700                EK::BadArgument
1701                    .at_pos(Pos::at(parts[1]))
1702                    .with_msg(e.to_string())
1703            })?;
1704            Ok((parts[0].to_string(), num))
1705        }
1706
1707        let params = s
1708            .split(' ')
1709            .filter(|p| !p.is_empty())
1710            .map(parse_pair)
1711            .try_collect()?;
1712        Ok(NetParams { params })
1713    }
1714}
1715
1716impl FromStr for SharedRandVal {
1717    type Err = Error;
1718    fn from_str(s: &str) -> crate::Result<Self> {
1719        let val: B64 = s.parse()?;
1720        let val = SharedRandVal(val.into_array()?);
1721        Ok(val)
1722    }
1723}
1724impl Display for SharedRandVal {
1725    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1726        Display::fmt(&B64::from(Vec::from(self.0)), f)
1727    }
1728}
1729impl NormalItemArgument for SharedRandVal {}
1730
1731impl SharedRandStatus {
1732    /// Parse a current or previous shared rand value from a given
1733    /// SharedRandPreviousValue or SharedRandCurrentValue.
1734    fn from_item(item: &Item<'_, NetstatusKwd>) -> crate::Result<Self> {
1735        match item.kwd() {
1736            NetstatusKwd::SHARED_RAND_PREVIOUS_VALUE | NetstatusKwd::SHARED_RAND_CURRENT_VALUE => {}
1737            _ => {
1738                return Err(Error::from(internal!(
1739                    "wrong keyword {:?} on shared-random value",
1740                    item.kwd()
1741                ))
1742                .at_pos(item.pos()));
1743            }
1744        }
1745        let n_reveals: u8 = item.parse_arg(0)?;
1746        let value: SharedRandVal = item.parse_arg(1)?;
1747        // Added in proposal 342
1748        let timestamp = item.parse_optional_arg::<Iso8601TimeNoSp>(2)?;
1749        Ok(SharedRandStatus {
1750            n_reveals,
1751            value,
1752            timestamp,
1753        })
1754    }
1755
1756    /// Return the actual shared random value.
1757    pub fn value(&self) -> &SharedRandVal {
1758        &self.value
1759    }
1760
1761    /// Return the timestamp (if any) associated with this `SharedRandValue`.
1762    pub fn timestamp(&self) -> Option<std::time::SystemTime> {
1763        self.timestamp.map(|t| t.0)
1764    }
1765}
1766
1767impl DirSource {
1768    /// Parse a "dir-source" item
1769    fn from_item(item: &Item<'_, NetstatusKwd>) -> crate::Result<Self> {
1770        if item.kwd() != NetstatusKwd::DIR_SOURCE {
1771            return Err(
1772                Error::from(internal!("Bad keyword {:?} on dir-source", item.kwd()))
1773                    .at_pos(item.pos()),
1774            );
1775        }
1776        let nickname = item
1777            .required_arg(0)?
1778            .parse()
1779            .map_err(|e: InvalidNickname| {
1780                EK::BadArgument.at_pos(item.pos()).with_msg(e.to_string())
1781            })?;
1782        let identity = item.parse_arg(1)?;
1783        let hostname = item
1784            .required_arg(2)?
1785            .parse()
1786            .map_err(|e: InvalidInternetHost| {
1787                EK::BadArgument.at_pos(item.pos()).with_msg(e.to_string())
1788            })?;
1789        let ip = item.parse_arg(3)?;
1790        let dir_port = item.parse_arg(4)?;
1791        let or_port = item.parse_arg(5)?;
1792
1793        Ok(DirSource {
1794            nickname,
1795            identity,
1796            hostname,
1797            ip,
1798            dir_port,
1799            or_port,
1800            __non_exhaustive: (),
1801        })
1802    }
1803}
1804
1805impl ConsensusAuthorityEntry {
1806    /// Parse a single ConsensusAuthorityEntry from a voter info section.
1807    fn from_section(sec: &Section<'_, NetstatusKwd>) -> crate::Result<ConsensusAuthorityEntry> {
1808        use NetstatusKwd::*;
1809        // this unwrap should be safe because if there is not at least one
1810        // token in the section, the section is unparsable.
1811        #[allow(clippy::unwrap_used)]
1812        let first = sec.first_item().unwrap();
1813        if first.kwd() != DIR_SOURCE {
1814            return Err(Error::from(internal!(
1815                "Wrong keyword {:?} at start of voter info",
1816                first.kwd()
1817            ))
1818            .at_pos(first.pos()));
1819        }
1820        let dir_source = DirSource::from_item(sec.required(DIR_SOURCE)?)?;
1821
1822        let contact = sec.required(CONTACT)?;
1823        // Ideally we would parse_args_as_str but that requires us to
1824        // impl From<InvalidContactInfo> for crate::Error which is wrong
1825        // because many it's a footgun which lets you just write ? here
1826        // resulting in lack of position information.
1827        // (This is a general problem with the error handling in crate::parse.)
1828        let contact = contact
1829            .args_as_str()
1830            .parse()
1831            .map_err(|err: InvalidContactInfo| {
1832                EK::BadArgument
1833                    .with_msg(err.to_string())
1834                    .at_pos(contact.pos())
1835            })?;
1836
1837        let vote_digest = sec.required(VOTE_DIGEST)?.parse_arg::<B16U>(0)?;
1838
1839        Ok(ConsensusAuthorityEntry {
1840            dir_source,
1841            contact,
1842            vote_digest,
1843            __non_exhaustive: (),
1844        })
1845    }
1846}
1847
1848impl RelayWeightsItem {
1849    /// Return a new `RelayWeightsItem` containing no information
1850    ///
1851    /// As if parsed from a document with no `w` line, discarding unknown information.
1852    pub fn new_no_info() -> Self {
1853        RelayWeightsItem {
1854            effective: RelayWeight::default(),
1855            params: Unknown::new_discard(),
1856        }
1857    }
1858
1859    /// Return a new `RelayWeightsItem` containing only the effective weight
1860    pub fn from_effective(effective: RelayWeight) -> Self {
1861        RelayWeightsItem {
1862            effective,
1863            params: Unknown::new_discard(),
1864        }
1865    }
1866
1867    /// Get the effective relay weight (bandwidth estimate) for path selection.
1868    ///
1869    /// Invariant: consistent with from [`params`](RelayWeightsItem::params),
1870    /// if `parsed` isn't [`Discarded`](Unknown::Discarded).
1871    //
1872    // We open-code this rather than deriving it so we can provide better docs.
1873    pub fn effective(&self) -> RelayWeight {
1874        self.effective
1875    }
1876
1877    /// Get the complete parameter set, if this information is available.
1878    ///
1879    /// After parsing, this is the parsed but not interpreted `w` item,
1880    /// or `None` if the document contained no `w` item.
1881    //
1882    // We open-code this rather than deriving it because we want to return
1883    // `Unknown<&...>` rather than `&Unknown<..>`, which the user would just have to .as_ref().
1884    pub fn params(&self) -> Unknown<&Option<NetParams<u32>>> {
1885        self.params.as_ref()
1886    }
1887
1888    /// Parse a routerweight from a "w" line.
1889    fn from_item(item: &Item<'_, NetstatusKwd>) -> crate::Result<RelayWeightsItem> {
1890        if item.kwd() != NetstatusKwd::RS_W {
1891            return Err(
1892                Error::from(internal!("Wrong keyword {:?} on W line", item.kwd()))
1893                    .at_pos(item.pos()),
1894            );
1895        }
1896
1897        let params = item.args_as_str().parse()?;
1898        let effective = RelayWeight::from_net_params(&params).map_err(|e| e.at_pos(item.pos()))?;
1899
1900        Ok(RelayWeightsItem {
1901            effective,
1902            params: Unknown::new_discard(),
1903        })
1904    }
1905
1906    /// The keyword for parsing and encoding
1907    const KEYWORD: &str = "w";
1908}
1909
1910#[cfg(feature = "retain-unknown")]
1911impl Default for RelayWeightsItem {
1912    fn default() -> Self {
1913        RelayWeightsItem {
1914            effective: RelayWeight::default(),
1915            params: Unknown::Retained(None),
1916        }
1917    }
1918}
1919
1920impl RelayWeight {
1921    /// Return true if this weight is the result of a successful measurement
1922    pub fn is_measured(&self) -> bool {
1923        matches!(self, RelayWeight::Measured(_))
1924    }
1925
1926    /// Return true if this weight is nonzero
1927    pub fn is_nonzero(&self) -> bool {
1928        !matches!(self, RelayWeight::Unmeasured(0) | RelayWeight::Measured(0))
1929    }
1930
1931    /// Parse a routerweight from partially-parsed `w` line in the form of a `NetParams`
1932    ///
1933    /// This function is the common part shared between `parse2` and `parse`.
1934    fn from_net_params(params: &NetParams<u32>) -> crate::Result<RelayWeight> {
1935        params
1936            .try_into()
1937            .map_err(|e: InvalidRelayWeights| EK::BadArgument.with_msg(e.to_string()))
1938    }
1939}
1940
1941impl Default for RelayWeight {
1942    fn default() -> RelayWeight {
1943        RelayWeight::Unmeasured(0)
1944    }
1945}
1946
1947impl TryFrom<&NetParams<u32>> for RelayWeight {
1948    type Error = InvalidRelayWeights;
1949
1950    fn try_from(params: &NetParams<u32>) -> Result<RelayWeight, InvalidRelayWeights> {
1951        let bw = params.params.get("Bandwidth");
1952        let unmeas = params.params.get("Unmeasured");
1953
1954        let bw = match bw {
1955            None => return Ok(RelayWeight::Unmeasured(0)),
1956            Some(b) => *b,
1957        };
1958
1959        match unmeas {
1960            None | Some(0) => Ok(RelayWeight::Measured(bw)),
1961            Some(1) => Ok(RelayWeight::Unmeasured(bw)),
1962            _ => Err(InvalidRelayWeights::InvalidUnmeasured),
1963        }
1964    }
1965}
1966
1967#[cfg(feature = "retain-unknown")]
1968impl TryFrom<NetParams<u32>> for RelayWeightsItem {
1969    type Error = InvalidRelayWeights;
1970
1971    fn try_from(params: NetParams<u32>) -> Result<RelayWeightsItem, InvalidRelayWeights> {
1972        Ok(RelayWeightsItem {
1973            effective: (&params).try_into()?,
1974            params: Unknown::Retained(Some(params)),
1975        })
1976    }
1977}
1978
1979/// `parse2` impls for types in this modulea
1980///
1981/// Separate module for a separate namespace.
1982mod parse2_impls {
1983    use super::*;
1984    pub(super) use parse2::{
1985        ArgumentError as AE, ArgumentStream, ErrorProblem as EP, ItemArgumentParseable,
1986        ItemValueParseable, NetdocParseableFields,
1987    };
1988    use std::result::Result;
1989
1990    // The NormalItemArgument bound ensures that this is applied only to sane types eg integers
1991    impl<T: FromStr + NormalItemArgument> ItemValueParseable for NetParams<T>
1992    where
1993        T::Err: std::error::Error,
1994    {
1995        fn from_unparsed(item: parse2::UnparsedItem<'_>) -> Result<Self, EP> {
1996            item.check_no_object()?;
1997            item.args_copy()
1998                .into_remaining()
1999                .parse()
2000                .map_err(item.invalid_argument_handler("parameters"))
2001        }
2002    }
2003
2004    impl NetdocParseableFields for RelayWeightsItem {
2005        type Accumulator = Option<NetParams<u32>>;
2006
2007        fn is_item_keyword(kw: KeywordRef) -> bool {
2008            kw == Self::KEYWORD
2009        }
2010
2011        fn accumulate_item(acc: &mut Self::Accumulator, item: UnparsedItem) -> Result<(), EP> {
2012            if acc.is_some() {
2013                return Err(EP::ItemRepeated);
2014            }
2015            item.check_no_object()?;
2016            let params = NetParams::from_unparsed(item)?;
2017            *acc = Some(params);
2018            Ok(())
2019        }
2020
2021        fn finish(params: Self::Accumulator, items: &ItemStream) -> Result<Self, EP> {
2022            let effective = params
2023                .as_ref()
2024                .map(TryFrom::try_from)
2025                .transpose()
2026                .map_err(|_| EP::OtherBadDocument("invalid information in `w` item"))?
2027                .unwrap_or_default();
2028
2029            let params = items.parse_options().retain_unknown_values.map(|()| params);
2030
2031            Ok(RelayWeightsItem { effective, params })
2032        }
2033    }
2034
2035    impl ItemValueParseable for rs::SoftwareVersion {
2036        fn from_unparsed(mut item: parse2::UnparsedItem<'_>) -> Result<Self, EP> {
2037            item.check_no_object()?;
2038            item.args_mut()
2039                .into_remaining()
2040                .parse()
2041                .map_err(item.invalid_argument_handler("version"))
2042        }
2043    }
2044
2045    impl ItemArgumentParseable for IgnoredPublicationTimeSp {
2046        fn from_args(a: &mut ArgumentStream) -> Result<IgnoredPublicationTimeSp, AE> {
2047            let mut next_arg = || a.next().ok_or(AE::Missing);
2048            let _: &str = next_arg()?;
2049            let _: &str = next_arg()?;
2050            Ok(IgnoredPublicationTimeSp)
2051        }
2052    }
2053}
2054
2055/// `encode` impls for types in this modulea
2056///
2057/// Separate module for a separate namespace.
2058mod encode_impls {
2059    use super::*;
2060    use std::result::Result;
2061    pub(crate) use {
2062        crate::encode::{ItemEncoder, ItemValueEncodable, NetdocEncodableFields},
2063        tor_error::Bug,
2064    };
2065
2066    #[cfg(feature = "incomplete")] // untested
2067    impl NetdocEncodableFields for RelayWeightsItem {
2068        fn encode_fields(&self, out: &mut NetdocEncoder) -> Result<(), Bug> {
2069            if let Some(w) = self.params.as_ref().into_retained()? {
2070                w.write_item_value_onto(out.item(Self::KEYWORD))?;
2071            }
2072            Ok(())
2073        }
2074    }
2075
2076    // The NormalItemArgument bound ensures that this is applied only to sane types eg integers
2077    impl<T: NormalItemArgument + Ord + Display> ItemValueEncodable for NetParams<T> {
2078        fn write_item_value_onto(&self, mut out: ItemEncoder) -> Result<(), Bug> {
2079            for (k, v) in self.iter().collect::<BTreeSet<_>>() {
2080                if k.is_empty()
2081                    || k.chars()
2082                        .any(|c| c.is_whitespace() || c.is_control() || c == '=')
2083                {
2084                    // TODO torspec#401 see TODO in NetParams<T> definition
2085                    return Err(bad_api_usage!(
2086                        "tried to encode NetParms with unreasonable keyword {k:?}"
2087                    ));
2088                }
2089                out.args_raw_string(&format_args!("{k}={v}"));
2090            }
2091            Ok(())
2092        }
2093    }
2094
2095    impl ItemValueEncodable for rs::SoftwareVersion {
2096        fn write_item_value_onto(&self, mut out: ItemEncoder) -> Result<(), Bug> {
2097            out.args_raw_string(self);
2098            Ok(())
2099        }
2100    }
2101
2102    impl ItemArgument for IgnoredPublicationTimeSp {
2103        fn write_arg_onto(&self, out: &mut ItemEncoder) -> Result<(), Bug> {
2104            out.args_raw_string(&"2000-01-01 00:00:01");
2105            Ok(())
2106        }
2107    }
2108}
2109
2110impl ConsensusFooterFields {
2111    /// Parse a directory footer from a footer section.
2112    fn from_section(sec: &Section<'_, NetstatusKwd>) -> crate::Result<ConsensusFooterFields> {
2113        use NetstatusKwd::*;
2114        sec.required(DIRECTORY_FOOTER)?;
2115
2116        let bandwidth_weights = sec
2117            .maybe(BANDWIDTH_WEIGHTS)
2118            .args_as_str()
2119            .unwrap_or("")
2120            .parse()?;
2121
2122        Ok(ConsensusFooterFields {
2123            bandwidth_weights,
2124            __non_exhaustive: (),
2125        })
2126    }
2127}
2128
2129/// `ProtoStatuses` parsing and encoding
2130///
2131/// Separate module for separate namespace
2132mod proto_statuses_parse2_encode {
2133    use super::encode_impls::*;
2134    use super::parse2_impls::*;
2135    use super::*;
2136    use paste::paste;
2137    use std::result::Result;
2138
2139    /// Implements `NetdocParseableFields` for `ProtoStatuses`
2140    ///
2141    /// We have this macro so that it's impossible to write things like
2142    /// ```text
2143    ///      ProtoStatuses {
2144    ///          client: ProtoStatus {
2145    ///              recommended: something something recommended_relay_versions something,
2146    /// ```
2147    ///
2148    /// (The structure of `ProtoStatuses` means the normal parse2 derive won't work for it.
2149    /// Note the bug above: the recommended *relay* version info is put in the *client* field.
2150    /// Preventing this bug must involve: avoiding writing twice the field name elements,
2151    /// such as `relay` and `client`, during this kind of construction/conversion.)
2152    macro_rules! impl_proto_statuses { { $( $rr:ident $cr:ident; )* } => { paste! {
2153        #[derive(Deftly)]
2154        #[derive_deftly(NetdocParseableFields)]
2155        // Only ProtoStatusesParseNetdocParseAccumulator is exposed.
2156        #[allow(unreachable_pub)]
2157        pub struct ProtoStatusesParseHelper {
2158            $(
2159                #[deftly(netdoc(default))]
2160                [<$rr _ $cr _protocols>]: Protocols,
2161            )*
2162        }
2163
2164        /// Partially parsed `ProtoStatuses`
2165        pub use ProtoStatusesParseHelperNetdocParseAccumulator
2166            as ProtoStatusesNetdocParseAccumulator;
2167
2168        impl NetdocParseableFields for ProtoStatuses {
2169            type Accumulator = ProtoStatusesNetdocParseAccumulator;
2170            fn is_item_keyword(kw: KeywordRef<'_>) -> bool {
2171                ProtoStatusesParseHelper::is_item_keyword(kw)
2172            }
2173            fn accumulate_item(
2174                acc: &mut Self::Accumulator,
2175                item: UnparsedItem<'_>,
2176            ) -> Result<(), EP> {
2177                ProtoStatusesParseHelper::accumulate_item(acc, item)
2178            }
2179            fn finish(acc: Self::Accumulator, items: &ItemStream<'_>) -> Result<Self, EP> {
2180                let parse = ProtoStatusesParseHelper::finish(acc, items)?;
2181                let mut out = ProtoStatuses::default();
2182                $(
2183                    out.$cr.$rr = parse.[< $rr _ $cr _protocols >];
2184                )*
2185                Ok(out)
2186            }
2187        }
2188
2189        impl NetdocEncodableFields for ProtoStatuses {
2190            fn encode_fields(&self, out: &mut NetdocEncoder) -> Result<(), Bug> {
2191              $(
2192                self.$cr.$rr.write_item_value_onto(
2193                    out.item(concat!(stringify!($rr), "-", stringify!($cr), "-protocols"))
2194                )?;
2195              )*
2196                Ok(())
2197            }
2198        }
2199    } } }
2200
2201    impl_proto_statuses! {
2202        recommended client;
2203        recommended relay;
2204        required client;
2205        required relay;
2206    }
2207}
2208
2209impl Signature {
2210    /// Parse a Signature from a directory-signature section
2211    fn from_item(item: &Item<'_, NetstatusKwd>) -> crate::Result<Signature> {
2212        if item.kwd() != NetstatusKwd::DIRECTORY_SIGNATURE {
2213            return Err(Error::from(internal!(
2214                "Wrong keyword {:?} for directory signature",
2215                item.kwd()
2216            ))
2217            .at_pos(item.pos()));
2218        }
2219
2220        let (digest_algo, id_fp, sk_fp) = if item.n_args() > 2 {
2221            (
2222                item.required_arg(0)?,
2223                item.required_arg(1)?,
2224                item.required_arg(2)?,
2225            )
2226        } else {
2227            // TODO #2530 digest_algo needs to depend on whether SHA1 was stated
2228            ("sha1", item.required_arg(0)?, item.required_arg(1)?)
2229        };
2230
2231        let digest_algo = digest_algo.to_string().parse().void_unwrap();
2232        let digest_algo = DigestAlgoInSignature(Some(digest_algo));
2233        let id_fingerprint = id_fp.parse::<Fingerprint>()?.into();
2234        let sk_fingerprint = sk_fp.parse::<Fingerprint>()?.into();
2235        let key_ids = AuthCertKeyIds {
2236            id_fingerprint,
2237            sk_fingerprint,
2238        };
2239        let signature = item.obj("SIGNATURE")?;
2240
2241        Ok(Signature {
2242            digest_algo,
2243            key_ids,
2244            signature,
2245        })
2246    }
2247
2248    /// Return true if this signature has the identity key and signing key
2249    /// that match a given cert.
2250    fn matches_cert(&self, cert: &AuthCert) -> bool {
2251        cert.key_ids() == self.key_ids
2252    }
2253
2254    /// If possible, find the right certificate for checking this signature
2255    /// from among a slice of certificates.
2256    fn find_cert<'a>(&self, certs: &'a [AuthCert]) -> Option<&'a AuthCert> {
2257        certs.iter().find(|&c| self.matches_cert(c))
2258    }
2259
2260    /// Find the certificate and assemble the pieces ready for verification
2261    ///
2262    /// `None` means precisely that we're missing the authcert.
2263    fn signature_to_verify<'r>(
2264        &'r self,
2265        signed_digest: &'r [u8],
2266        certs: &'r [AuthCert],
2267    ) -> Option<ConsensusSignatureToVerify> {
2268        let cert = self.find_cert(certs)?;
2269        let key = cert.signing_key();
2270        Some(ConsensusSignatureToVerify {
2271            key,
2272            signed_digest,
2273            signature: &self.signature,
2274        })
2275    }
2276}
2277
2278impl EncodeOrd for Signature {
2279    fn encode_cmp(&self, other: &Self) -> std::cmp::Ordering {
2280        let k: for<'s> fn(&'_ Signature) -> (&'_ _, &'_ _) = |s| (&s.key_ids, &s.signature);
2281        Ord::cmp(&k(self), &k(other))
2282    }
2283}
2284
2285/// Signature information in a consensus, to be verified
2286///
2287/// Used by callers of [`SignatureGroup::verify_general`],
2288/// to allow verification to be suppressed if all we wanted to know was
2289/// whether we have enough signatures and enough authcerts.
2290//
2291// TODO DIRAUTH make this module-private when poc is abolished
2292#[derive(Debug, Clone, Copy)]
2293pub(crate) struct ConsensusSignatureToVerify<'r> {
2294    /// KP_auth_sign_rsa
2295    key: &'r ll::pk::rsa::PublicKey,
2296
2297    /// The digest (actual RSA signature payload, before PKCS#11 padding)
2298    signed_digest: &'r [u8],
2299
2300    /// The RSA signature value
2301    signature: &'r [u8],
2302}
2303
2304/// Token indicating that signature verification has been done, if required
2305///
2306/// Prevents accidentally passing an unintended no-op function as
2307/// `do_verify` to [`SignatureGroup::verify_general`].
2308///
2309/// Write `SignatureVerifiedIfIntended {}` to construct this,
2310/// only in code which has actually done the verification,
2311/// or code which is deliberately not verifying at all.
2312pub(crate) struct SignatureVerifiedIfIntended {}
2313
2314impl<'r> ConsensusSignatureToVerify<'r> {
2315    /// Verify this signature
2316    ///
2317    // TODO DIRAUTH make this module-private when poc is abolished
2318    pub(crate) fn verify(self) -> Result<SignatureVerifiedIfIntended, VerifyFailed> {
2319        self.key.verify(self.signed_digest, self.signature)?;
2320        Ok(SignatureVerifiedIfIntended {})
2321    }
2322}
2323
2324/// How `verify_general` should decide who is a trusted authority
2325///
2326/// Don't use this for other purposes
2327#[derive(Debug, Clone, Copy)]
2328pub(crate) enum VerifyGeneralTrustedAuthorities<'r> {
2329    /// Trust these authorities.
2330    TrustThese {
2331        /// The HKP_auth_id_rsa
2332        trusted: &'r [RsaIdentity],
2333    },
2334
2335    /// Document is a a vote, so OK if signed by any one of the listed authorities
2336    AnyOneOfThese {
2337        /// The HKP_auth_id_rsa
2338        trusted: &'r [RsaIdentity],
2339    },
2340
2341    /// For the benefit of `SignatureGroup::validate`, used by the old parser, only
2342    ///
2343    /// Every `AuthCert` passed to `verify_general` is a real authority (!)
2344    /// (But not necessarily a different one!)
2345    HazardouslyAssumeAllAuthCertsAreReal {
2346        /// Total number of authorities that we trust
2347        ///
2348        /// Used only to calculate the threshold
2349        n_authorities: usize,
2350    },
2351}
2352
2353/// Return the minimum number of authorities that we need signatures from
2354///
2355/// Enough is strictly more than half.
2356///
2357/// The returned value is a [`RangeFrom`](std::ops::RangeFrom), ie an inclusive range.
2358/// Its `start` value is the minimum acceptable number of authorities
2359/// from whom we have good signatures.
2360///
2361/// Should usually be followed by
2362/// [`.contains`](std::ops::RangeFrom::contains)`(&actual_number)`.
2363///
2364/// # Example
2365///
2366/// ```
2367/// use tor_netdoc::{doc::netstatus::consensus_threshold, parse2::VerifyFailed};
2368/// # fn main() -> Result<(), VerifyFailed> {
2369///
2370/// let n_trusted_authorities = 3;
2371/// let n_good_signatures_from_different_authorities = 2;
2372///
2373/// if consensus_threshold(n_trusted_authorities)
2374///      .contains(&n_good_signatures_from_different_authorities)
2375/// {
2376///     Ok(())
2377/// } else {
2378///     Err(VerifyFailed::InsufficientTrustedSigners)
2379/// }
2380/// # }
2381/// ```
2382pub fn consensus_threshold(n_authorities: usize) -> std::ops::RangeFrom<usize> {
2383    (n_authorities / 2) + 1 // strict majority
2384        ..
2385}
2386
2387impl SignatureGroup {
2388    // TODO: these functions are pretty similar and could probably stand to be
2389    // refactored a lot.
2390
2391    /// Helper: Return a pair of the number of possible authorities'
2392    /// signatures in this object for which we _could_ find certs, and
2393    /// a list of the signatures we couldn't find certificates for.
2394    fn list_missing(&self, certs: &[AuthCert]) -> (usize, Vec<&Signature>) {
2395        let mut ok: HashSet<RsaIdentity> = HashSet::new();
2396        let mut missing = Vec::new();
2397        for sig in &self.signatures {
2398            let id_fingerprint = &sig.key_ids.id_fingerprint;
2399            if ok.contains(id_fingerprint) {
2400                continue;
2401            }
2402            if sig.find_cert(certs).is_some() {
2403                ok.insert(*id_fingerprint);
2404                continue;
2405            }
2406
2407            missing.push(sig);
2408        }
2409        (ok.len(), missing)
2410    }
2411
2412    /// Given a list of authority identity key fingerprints, return true if
2413    /// this signature group is _potentially_ well-signed according to those
2414    /// authorities.
2415    fn could_validate(&self, authorities: &[&RsaIdentity]) -> bool {
2416        let mut signed_by: HashSet<RsaIdentity> = HashSet::new();
2417        for sig in &self.signatures {
2418            let id_fp = &sig.key_ids.id_fingerprint;
2419            if signed_by.contains(id_fp) {
2420                // Already found this in the list.
2421                continue;
2422            }
2423            if authorities.contains(&id_fp) {
2424                signed_by.insert(*id_fp);
2425            }
2426        }
2427
2428        consensus_threshold(authorities.len()).contains(&signed_by.len())
2429    }
2430
2431    /// Return true if the signature group defines a valid signature.
2432    ///
2433    /// A signature is valid if it signed by more than half of the
2434    /// authorities.  This API requires that `n_authorities` is the number of
2435    /// authorities we believe in, and that every cert in `certs` belongs
2436    /// to a real authority.
2437    fn validate(&self, n_authorities: usize, certs: &[AuthCert]) -> Result<(), VerifyFailed> {
2438        // TODO we ought to take the set of trusted authorities as an argument,
2439        // rather than use VGTA::HazardouslyAssumeAllAuthCertsAreReal.
2440        self.verify_general(
2441            VerifyGeneralTrustedAuthorities::HazardouslyAssumeAllAuthCertsAreReal { n_authorities },
2442            certs,
2443            |tv| tv.verify(),
2444        )
2445    }
2446
2447    /// Check signatures (maybe), but not timeliness
2448    ///
2449    /// Examines the signatures and collates them with authcerts.
2450    /// Performs the necessary consensus signature verifications, via `do_verify`.
2451    ///
2452    /// If there are not enough authcerts or not enough signatures,
2453    /// throws a `ConsensusVerifiabilityError`.
2454    ///
2455    /// Differs from [`SignatureGroup::validate`]:
2456    ///
2457    ///  * Intended also for use with types from parse2.
2458    ///
2459    ///  * Yields information about missing authcerts directly in the return value,
2460    ///    and can be used without actually doing the verification,
2461    ///    so there's no need for a separate "which certs are we missing" function.
2462    ///
2463    ///  * Threshold is passed as a parameter (wanted for votes).
2464    ///
2465    ///  * Ability to check authority identities, by passing `trusted_authorities`.
2466    ///    (done with `authorities_are_correct` in old parser,
2467    ///    apparently with no engineered safeguard against consensus user omitting to do so).
2468    ///
2469    ///    **If `trusted_authorities` is None, all authorities in `certs` are treated as trusted**.
2470    ///
2471    ///  * Returns `Result`, not a boolean
2472    ///
2473    ///  * We prefer the term `verify` to `validate`.  All this does is signature verification.
2474    ///
2475    // TODO DIRAUTH make this module-private when poc is abolished
2476    pub(crate) fn verify_general<E>(
2477        &self,
2478        trusted_authorities: VerifyGeneralTrustedAuthorities,
2479        certs: &[AuthCert],
2480        do_verify: impl Fn(ConsensusSignatureToVerify) -> Result<SignatureVerifiedIfIntended, E>,
2481    ) -> Result<(), E>
2482    where
2483        ConsensusVerifiabilityError: Into<E>,
2484    {
2485        use VerifyGeneralTrustedAuthorities as TA;
2486
2487        // A set of the authorities (by identity) who have have signed
2488        // this document.  We use a set here in case `certs` has more
2489        // than one certificate for a single authority.
2490        let mut ok: HashSet<RsaIdentity> = HashSet::new();
2491        let mut missing = HashSet::new();
2492        let mut verify_failed = Ok(());
2493
2494        for sig in &self.signatures {
2495            // Exhaustive pattern makes it hard to accidentally ignore a field.
2496            let Signature {
2497                digest_algo,
2498                key_ids:
2499                    AuthCertKeyIds {
2500                        id_fingerprint,
2501                        // h_kp_auth_sign_rsa, which Signature::check_signature
2502                        // checks against the authcert.
2503                        sk_fingerprint: _,
2504                    },
2505                // Used by Signature::check_signature
2506                signature: _,
2507            } = sig;
2508
2509            match trusted_authorities {
2510                TA::TrustThese { trusted } | TA::AnyOneOfThese { trusted } => {
2511                    if !trusted.contains(id_fingerprint) {
2512                        continue;
2513                    }
2514                }
2515                TA::HazardouslyAssumeAllAuthCertsAreReal { .. } => {
2516                    // OK then!
2517                }
2518            }
2519
2520            if ok.contains(id_fingerprint) {
2521                // We already checked at least one signature using this
2522                // authority's identity fingerprint.
2523                continue;
2524            }
2525
2526            let Some(d) = self.hashes.hash_slice_for_verification(digest_algo) else {
2527                // We don't support this kind of digest for this kind
2528                // of document.
2529                continue;
2530            };
2531
2532            let Some(tv) = sig.signature_to_verify(d, certs) else {
2533                missing.insert(sig.key_ids);
2534                continue;
2535            };
2536            match do_verify(tv) {
2537                Ok::<SignatureVerifiedIfIntended, _>(_) => {
2538                    ok.insert(*id_fingerprint);
2539                }
2540                Err(e) => {
2541                    verify_failed = Err(e);
2542                }
2543            }
2544        }
2545
2546        let n_authorities = match trusted_authorities {
2547            TA::TrustThese { trusted } => trusted.len(),
2548            TA::HazardouslyAssumeAllAuthCertsAreReal { n_authorities: n } => n,
2549            TA::AnyOneOfThese { .. } => {
2550                // strict majority of 1 is 1, so n_authorites being 1 leads to threshold of 1
2551                // (doing it this way avoids having both thresholds and authority counts
2552                // in the same code area, which might lead to confusing one with the other.
2553                1
2554            }
2555        };
2556        let threshold = consensus_threshold(n_authorities);
2557
2558        if threshold.contains(&ok.len()) {
2559            Ok(())
2560        } else {
2561            // Throw the verification error if any of the verifications failed
2562            verify_failed?;
2563
2564            // Otherwise report that we're missing certs and/or signers
2565            Err(if missing.is_empty() {
2566                ConsensusVerifiabilityError::InsufficientTrustedSigners
2567            } else {
2568                let deficit = threshold.start - ok.len();
2569                ConsensusVerifiabilityError::MissingAuthCerts { missing, deficit }
2570            }
2571            .into())
2572        }
2573    }
2574}
2575
2576impl From<ConsensusVerifiabilityError> for VerifyFailed {
2577    fn from(cve: ConsensusVerifiabilityError) -> VerifyFailed {
2578        use ConsensusVerifiabilityError as CVE;
2579        use VerifyFailed as VF;
2580        match cve {
2581            CVE::InsufficientTrustedSigners => VF::InsufficientTrustedSigners,
2582            CVE::MissingAuthCerts { .. } => VF::InsufficientTrustedSigners,
2583        }
2584    }
2585}
2586
2587impl From<ConsensusVerifyFailed> for VerifyFailed {
2588    fn from(cvf: ConsensusVerifyFailed) -> VerifyFailed {
2589        use ConsensusVerifyFailed as CVF;
2590        use VerifyFailed as VF;
2591        match cvf {
2592            CVF::CertificationInsufficient { .. } => VF::InsufficientTrustedSigners,
2593            CVF::InvalidSignature { .. } => VF::VerifyFailed,
2594        }
2595    }
2596}
2597
2598#[cfg(test)]
2599mod test {
2600    // @@ begin test lint list maintained by maint/add_warning @@
2601    #![allow(clippy::bool_assert_comparison)]
2602    #![allow(clippy::clone_on_copy)]
2603    #![allow(clippy::dbg_macro)]
2604    #![allow(clippy::mixed_attributes_style)]
2605    #![allow(clippy::print_stderr)]
2606    #![allow(clippy::print_stdout)]
2607    #![allow(clippy::single_char_pattern)]
2608    #![allow(clippy::unwrap_used)]
2609    #![allow(clippy::unchecked_time_subtraction)]
2610    #![allow(clippy::useless_vec)]
2611    #![allow(clippy::needless_pass_by_value)]
2612    #![allow(clippy::string_slice)] // See arti#2571
2613    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
2614    use super::*;
2615    use crate::doc::authcert::AuthCertUnverified;
2616    use crate::encode::{NetdocEncodable, NetdocEncodableFields};
2617    use crate::parse2::{ParseInput, parse_netdoc, parse_netdoc_multiple};
2618    use crate::util::regsub;
2619    use anyhow::Context as _;
2620    use assert_matches::assert_matches;
2621    use hex_literal::hex;
2622    use humantime::parse_rfc3339;
2623    use std::fmt::Debug;
2624    use std::fs;
2625    use std::time::Duration;
2626    use tor_checkable::Timebound;
2627
2628    const CERTS: &str = include_str!("../../testdata/authcerts2.txt");
2629    const CONSENSUS: &str = include_str!("../../testdata/mdconsensus1.txt");
2630
2631    const PLAIN_CERTS: &str = include_str!("../../testdata2/cached-certs");
2632    const PLAIN_CONSENSUS: &str = include_str!("../../testdata2/cached-consensus");
2633
2634    fn read_bad(fname: &str) -> String {
2635        use std::fs;
2636        use std::path::PathBuf;
2637        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
2638        path.push("testdata");
2639        path.push("bad-mdconsensus");
2640        path.push(fname);
2641
2642        fs::read_to_string(path).unwrap()
2643    }
2644
2645    #[test]
2646    fn parse_and_validate_md() -> crate::Result<()> {
2647        use std::net::SocketAddr;
2648        use tor_checkable::{SelfSigned, Timebound};
2649        let mut certs = Vec::new();
2650        for cert in AuthCert::parse_multiple(CERTS)? {
2651            let cert = cert?.check_signature()?.dangerously_assume_timely();
2652            certs.push(cert);
2653        }
2654        let auth_ids: Vec<_> = certs.iter().map(|c| c.id_fingerprint()).collect();
2655
2656        assert_eq!(certs.len(), 3);
2657
2658        let (_, _, consensus) = MdConsensus::parse(CONSENSUS)?;
2659        let consensus = consensus.dangerously_assume_timely().set_n_authorities(3);
2660
2661        // The set of authorities we know _could_ validate this cert.
2662        assert!(consensus.authorities_are_correct(&auth_ids));
2663        // A subset would also work.
2664        assert!(consensus.authorities_are_correct(&auth_ids[0..1]));
2665        {
2666            // If we only believe in an authority that isn't listed,
2667            // that won't work.
2668            let bad_auth_id = (*b"xxxxxxxxxxxxxxxxxxxx").into();
2669            assert!(!consensus.authorities_are_correct(&[&bad_auth_id]));
2670        }
2671
2672        let missing = consensus.key_is_correct(&[]).err().unwrap();
2673        assert_eq!(3, missing.len());
2674        assert!(consensus.key_is_correct(&certs).is_ok());
2675        let missing = consensus.key_is_correct(&certs[0..1]).err().unwrap();
2676        assert_eq!(2, missing.len());
2677
2678        // here is a trick that had better not work.
2679        let same_three_times = vec![certs[0].clone(), certs[0].clone(), certs[0].clone()];
2680        let missing = consensus.key_is_correct(&same_three_times).err().unwrap();
2681
2682        assert_eq!(2, missing.len());
2683        assert!(consensus.is_well_signed(&same_three_times).is_err());
2684
2685        assert!(consensus.key_is_correct(&certs).is_ok());
2686        let consensus = consensus.check_signature(&certs)?;
2687
2688        assert_eq!(6, consensus.relays().len());
2689        let r0 = &consensus.relays()[0];
2690        assert_eq!(
2691            r0.md_digest(),
2692            &hex!("73dabe0a0468f4f7a67810a18d11e36731bb1d2ec3634db459100609f3b3f535")
2693        );
2694        assert_eq!(
2695            r0.rsa_identity().as_bytes(),
2696            &hex!("0a3057af2910415794d8ea430309d9ac5f5d524b")
2697        );
2698        assert!(!r0.weight().is_measured());
2699        assert!(!r0.weight().is_nonzero());
2700        let pv = &r0.protovers();
2701        assert!(pv.supports_subver("HSDir", 2));
2702        assert!(!pv.supports_subver("HSDir", 3));
2703        let ip4 = "127.0.0.1:5002".parse::<SocketAddr>().unwrap();
2704        let ip6 = "[::1]:5002".parse::<SocketAddr>().unwrap();
2705        assert!(r0.addrs().any(|a| a == ip4));
2706        assert!(r0.addrs().any(|a| a == ip6));
2707
2708        Ok(())
2709    }
2710
2711    #[test]
2712    fn parse_and_validate_ns() -> crate::Result<()> {
2713        use tor_checkable::{SelfSigned, Timebound};
2714        let mut certs = Vec::new();
2715        for cert in AuthCert::parse_multiple(PLAIN_CERTS)? {
2716            let cert = cert?.check_signature()?.dangerously_assume_timely();
2717            certs.push(cert);
2718        }
2719        let auth_ids: Vec<_> = certs.iter().map(|c| c.id_fingerprint()).collect();
2720        assert_eq!(certs.len(), 4);
2721
2722        let (_, _, consensus) = PlainConsensus::parse(PLAIN_CONSENSUS)?;
2723        let consensus = consensus.dangerously_assume_timely().set_n_authorities(3);
2724        // The set of authorities we know _could_ validate this cert.
2725        assert!(consensus.authorities_are_correct(&auth_ids));
2726        // A subset would also work.
2727        assert!(consensus.authorities_are_correct(&auth_ids[0..1]));
2728
2729        assert!(consensus.key_is_correct(&certs).is_ok());
2730
2731        let _consensus = consensus.check_signature(&certs)?;
2732
2733        Ok(())
2734    }
2735
2736    #[test]
2737    fn test_bad() {
2738        use crate::Pos;
2739        fn check(fname: &str, e: &Error) {
2740            let content = read_bad(fname);
2741            let res = MdConsensus::parse(&content);
2742            assert!(res.is_err());
2743            assert_eq!(&res.err().unwrap(), e);
2744        }
2745
2746        check(
2747            "bad-flags",
2748            &EK::BadArgument
2749                .at_pos(Pos::from_line(27, 1))
2750                .with_msg("Flags out of order"),
2751        );
2752        check(
2753            "bad-md-digest",
2754            &EK::BadArgument
2755                .at_pos(Pos::from_line(40, 3))
2756                .with_msg("Invalid base64"),
2757        );
2758        check(
2759            "bad-weight",
2760            &EK::BadArgument
2761                .at_pos(Pos::from_line(67, 141))
2762                .with_msg("invalid digit found in string"),
2763        );
2764        check(
2765            "bad-weights",
2766            &EK::BadArgument
2767                .at_pos(Pos::from_line(51, 13))
2768                .with_msg("invalid digit found in string"),
2769        );
2770        check(
2771            "wrong-order",
2772            &EK::WrongSortOrder.at_pos(Pos::from_line(52, 1)),
2773        );
2774        check(
2775            "wrong-start",
2776            &EK::UnexpectedToken
2777                .with_msg("vote-status")
2778                .at_pos(Pos::from_line(1, 1)),
2779        );
2780        check("wrong-version", &EK::BadDocumentVersion.with_msg("10"));
2781    }
2782
2783    fn gettok(s: &str) -> crate::Result<Item<'_, NetstatusKwd>> {
2784        let mut reader = NetDocReader::new(s)?;
2785        let tok = reader.next().unwrap();
2786        assert!(reader.next().is_none());
2787        tok
2788    }
2789
2790    #[test]
2791    fn test_weight() {
2792        let w = gettok("w Unmeasured=1 Bandwidth=6\n").unwrap();
2793        let w = RelayWeightsItem::from_item(&w).unwrap();
2794        assert!(!w.effective.is_measured());
2795        assert!(w.effective.is_nonzero());
2796
2797        let w = gettok("w Bandwidth=10\n").unwrap();
2798        let w = RelayWeightsItem::from_item(&w).unwrap();
2799        assert!(w.effective.is_measured());
2800        assert!(w.effective.is_nonzero());
2801
2802        let w = RelayWeightsItem::new_no_info();
2803        assert!(!w.effective.is_measured());
2804        assert!(!w.effective.is_nonzero());
2805
2806        let w = gettok("w Mustelid=66 Cheato=7 Unmeasured=1\n").unwrap();
2807        let w = RelayWeightsItem::from_item(&w).unwrap();
2808        assert!(!w.effective.is_measured());
2809        assert!(!w.effective.is_nonzero());
2810
2811        let w = gettok("r foo\n").unwrap();
2812        let w = RelayWeightsItem::from_item(&w);
2813        assert!(w.is_err());
2814
2815        let w = gettok("r Bandwidth=6 Unmeasured=Frog\n").unwrap();
2816        let w = RelayWeightsItem::from_item(&w);
2817        assert!(w.is_err());
2818
2819        let w = gettok("r Bandwidth=6 Unmeasured=3\n").unwrap();
2820        let w = RelayWeightsItem::from_item(&w);
2821        assert!(w.is_err());
2822    }
2823
2824    #[test]
2825    fn test_netparam() {
2826        let p = "Hello=600 Goodbye=5 Fred=7"
2827            .parse::<NetParams<u32>>()
2828            .unwrap();
2829        assert_eq!(p.get("Hello"), Some(&600_u32));
2830
2831        let p = "Hello=Goodbye=5 Fred=7".parse::<NetParams<u32>>();
2832        assert!(p.is_err());
2833
2834        let p = "Hello=Goodbye Fred=7".parse::<NetParams<u32>>();
2835        assert!(p.is_err());
2836
2837        for bad_kw in ["What=The", "", "\n", "\0"] {
2838            let p = [(bad_kw, 42)].into_iter().collect::<NetParams<i32>>();
2839            let mut d = NetdocEncoder::new();
2840            let d = (|| {
2841                let i = d.item("bad-psrams");
2842                p.write_item_value_onto(i)?;
2843                d.finish()
2844            })();
2845            let _: tor_error::Bug = d.expect_err(bad_kw);
2846        }
2847    }
2848
2849    #[test]
2850    fn test_sharedrand() {
2851        let sr =
2852            gettok("shared-rand-previous-value 9 5LodY4yWxFhTKtxpV9wAgNA9N8flhUCH0NqQv1/05y4\n")
2853                .unwrap();
2854        let sr = SharedRandStatus::from_item(&sr).unwrap();
2855
2856        assert_eq!(sr.n_reveals, 9);
2857        assert_eq!(
2858            sr.value.0,
2859            hex!("e4ba1d638c96c458532adc6957dc0080d03d37c7e5854087d0da90bf5ff4e72e")
2860        );
2861        assert!(sr.timestamp.is_none());
2862
2863        let sr2 = gettok(
2864            "shared-rand-current-value 9 \
2865                    5LodY4yWxFhTKtxpV9wAgNA9N8flhUCH0NqQv1/05y4 2022-01-20T12:34:56\n",
2866        )
2867        .unwrap();
2868        let sr2 = SharedRandStatus::from_item(&sr2).unwrap();
2869        assert_eq!(sr2.n_reveals, sr.n_reveals);
2870        assert_eq!(sr2.value.0, sr.value.0);
2871        assert_eq!(
2872            sr2.timestamp.unwrap().0,
2873            humantime::parse_rfc3339("2022-01-20T12:34:56Z").unwrap()
2874        );
2875
2876        let sr = gettok("foo bar\n").unwrap();
2877        let sr = SharedRandStatus::from_item(&sr);
2878        assert!(sr.is_err());
2879    }
2880
2881    #[test]
2882    fn test_protostatus() {
2883        let my_protocols: Protocols = "Link=7 Cons=1-5 Desc=3-10".parse().unwrap();
2884
2885        let outcome = ProtoStatus {
2886            recommended: "Link=7".parse().unwrap(),
2887            required: "Desc=5".parse().unwrap(),
2888        }
2889        .check_protocols(&my_protocols);
2890        assert!(outcome.is_ok());
2891
2892        let outcome = ProtoStatus {
2893            recommended: "Microdesc=4 Link=7".parse().unwrap(),
2894            required: "Desc=5".parse().unwrap(),
2895        }
2896        .check_protocols(&my_protocols);
2897        assert_eq!(
2898            outcome,
2899            Err(ProtocolSupportError::MissingRecommended(
2900                "Microdesc=4".parse().unwrap()
2901            ))
2902        );
2903
2904        let outcome = ProtoStatus {
2905            recommended: "Microdesc=4 Link=7".parse().unwrap(),
2906            required: "Desc=5 Cons=5-12 Wombat=15".parse().unwrap(),
2907        }
2908        .check_protocols(&my_protocols);
2909        assert_eq!(
2910            outcome,
2911            Err(ProtocolSupportError::MissingRequired(
2912                "Cons=6-12 Wombat=15".parse().unwrap()
2913            ))
2914        );
2915    }
2916
2917    #[test]
2918    fn serialize_protostatus() {
2919        let ps = ProtoStatuses {
2920            client: ProtoStatus {
2921                recommended: "Link=1-5 LinkAuth=2-5".parse().unwrap(),
2922                required: "Link=5 LinkAuth=3".parse().unwrap(),
2923            },
2924            relay: ProtoStatus {
2925                recommended: "Wombat=20-30 Knish=20-30".parse().unwrap(),
2926                required: "Wombat=20-22 Knish=25-27".parse().unwrap(),
2927            },
2928        };
2929        let json = serde_json::to_string(&ps).unwrap();
2930        let ps2 = serde_json::from_str(json.as_str()).unwrap();
2931        assert_eq!(ps, ps2);
2932
2933        let ps3: ProtoStatuses = serde_json::from_str(
2934            r#"{
2935            "client":{
2936                "required":"Link=5 LinkAuth=3",
2937                "recommended":"Link=1-5 LinkAuth=2-5"
2938            },
2939            "relay":{
2940                "required":"Wombat=20-22 Knish=25-27",
2941                "recommended":"Wombat=20-30 Knish=20-30"
2942            }
2943        }"#,
2944        )
2945        .unwrap();
2946        assert_eq!(ps, ps3);
2947    }
2948
2949    // TODO DIRAUTH test parse2 consensus verify functions
2950    #[test]
2951    #[cfg(feature = "incomplete")]
2952    fn verify_error_netstatus_vote() -> Result<(), anyhow::Error> {
2953        use VerifyFailed as VF;
2954        use VoteVerifyFailed as VVF;
2955        use vote::NetworkStatusUnverified as UV;
2956
2957        let file = "testdata2/v3-status-votes--1";
2958        let text = fs::read_to_string(file).with_context(|| file.to_owned())?;
2959        let input = ParseInput::new(&text, file);
2960        let doc: UV = parse_netdoc(&input)?;
2961        let trusted = [doc.peek_alleged_authority()];
2962
2963        let edit_body = |f: &dyn Fn(&mut _)| {
2964            let (mut body, sigs) = doc.clone().unwrap_unverified();
2965            f(&mut body);
2966            UV::from_parts(body, sigs)
2967        };
2968
2969        // sabotage the overall signature
2970        {
2971            let mut doc = doc.clone();
2972            for b in &mut doc.sigs.sigs.directory_signature.signature {
2973                *b = 0xff;
2974            }
2975            assert_matches! {
2976                doc.verify(&trusted),
2977                Err(VVF::InvalidSignature(VF::VerifyFailed))
2978            }
2979        }
2980
2981        // wrong authority
2982        {
2983            let doc = doc.clone();
2984            assert_matches! {
2985                doc.verify(&[[0x55; _].into()]),
2986                Err(VVF::InvalidSignature(VF::InsufficientTrustedSigners))
2987            }
2988        }
2989
2990        // authcert is for a different authority
2991        {
2992            let doc = edit_body(&|body| {
2993                body.authority.authority.dir_source.identity.0 = [0x55; _].into();
2994            });
2995            assert_matches! {
2996                doc.verify(&trusted),
2997                Err(VVF::AuthCertWrongAuthority)
2998            }
2999        }
3000
3001        // authcert is from a different time
3002        let with_mutated_lifetime = |f: &dyn Fn(&mut Lifetime)| {
3003            let doc = edit_body(&|body| f(&mut body.preamble.lifetime));
3004            assert_matches! {
3005                doc.verify(&trusted),
3006                Err(VVF::AuthCertWrongValidity(_))
3007            }
3008        };
3009        let t_past = parse_rfc3339("1990-01-01T00:02:25Z")?;
3010        let t_future = parse_rfc3339("2010-01-01T00:02:25Z")?;
3011        with_mutated_lifetime(&|lifetime| lifetime.valid_after.0 = t_future);
3012        with_mutated_lifetime(&|lifetime| lifetime.fresh_until.0 = t_past);
3013        with_mutated_lifetime(&|lifetime| lifetime.valid_until.0 = t_past);
3014
3015        // syntactically invalid authcert
3016        {
3017            let mut text = text.clone();
3018            regsub(&mut text, "^dir-key-expires ", "dir-key-expires-SABOTAGED ");
3019            let input = ParseInput::new(&text, file);
3020            let doc: UV = parse_netdoc(&input)?;
3021            assert_matches! {
3022                doc.verify(&trusted),
3023                Err(VVF::AuthCertParseError(..))
3024            }
3025        }
3026
3027        Ok(())
3028    }
3029
3030    /// Check that a network document can be parsed and regenerated, mostly identically
3031    ///
3032    /// The regenerated encoded form doesn't need to be 100% identical:
3033    /// it is compared with a *munged* version of the the original input file,
3034    /// to cope with differences between C Tor and Arti.
3035    ///
3036    /// The mungings are:
3037    ///
3038    ///  * Some fields' syntax are adjusted, where C Tor and Arti disagree
3039    ///    in all kinds of network document.
3040    ///
3041    ///  * Document-specific, [`MungeForRoundtrip::adjust_exp`]
3042    #[cfg(feature = "incomplete")]
3043    fn roundtrip_netstatus<UV, V, VE>(
3044        // TODO DIRAUTH use include_str!, so, at call sites
3045        // https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/4121#note_3428675
3046        file: &str,
3047        verify: impl FnOnce(UV, &[RsaIdentity], &[AuthCert]) -> Result<TimerangeBound<V>, VE>,
3048        adjust_now: Duration,
3049    ) -> anyhow::Result<()>
3050    where
3051        UV: NetdocParseable + NetdocParseableUnverified + MungeForRoundtrip,
3052        UV::Signatures: Clone + Debug + NetdocEncodableFields,
3053        VE: Debug + std::error::Error + Send + Sync + 'static,
3054        V: Debug + NetdocEncodable,
3055    {
3056        let text = fs::read_to_string(file).with_context(|| file.to_owned())?;
3057        let now = parse_rfc3339("2000-01-01T00:02:25Z")? + adjust_now;
3058
3059        let mut input = ParseInput::new(&text, file);
3060        input.retain_unknown_values();
3061
3062        let doc: UV = parse_netdoc(&input)?;
3063
3064        let certs = {
3065            let file = "testdata2/cached-certs";
3066            let text = fs::read_to_string(file)?;
3067            let input = ParseInput::new(&text, file);
3068            let certs: Vec<AuthCertUnverified> = parse_netdoc_multiple(&input)?;
3069            certs
3070                .into_iter()
3071                .map(|cert| cert.verify_selfcert(now))
3072                .collect::<Result<Vec<AuthCert>, _>>()?
3073        };
3074
3075        let sigs = doc.inspect_unverified().1.sigs.clone();
3076
3077        let doc = verify(
3078            doc,
3079            &certs.iter().map(|cert| *cert.fingerprint).collect_vec(),
3080            &certs,
3081        )?
3082        .check_valid_at(&now)?;
3083
3084        println!("{doc:?}");
3085
3086        let mut enc = NetdocEncoder::new();
3087        doc.encode_unsigned(&mut enc)?;
3088        sigs.encode_fields(&mut enc)?;
3089        let enc = enc.finish()?;
3090
3091        let mut exp: String = text.clone();
3092
3093        // TODO DIRAUTH torspec!507 C Tor emits padded base64 in shared-rand-* items.
3094        regsub(
3095            //
3096            &mut exp,
3097            r#"^(shared-rand-.*)$"#,
3098            |c: &regex::Captures| {
3099                let mut s = c[1].to_owned();
3100                regsub(&mut s, r#"="#, "");
3101                s
3102            },
3103        );
3104
3105        let mut regsub = |re, repl| regsub(&mut exp, re, repl);
3106
3107        // C Tor writes empty versions lines with trailing space
3108        regsub(
3109            //
3110            r#"^((?:client|server)-versions) $"#,
3111            "$1",
3112        );
3113
3114        // C Tor emits `m` in varying places: after `a` in votes,
3115        // and at the end of each routerstatus in md consensuses.
3116        // We emit it at the start of each routerstatus, right after `r`.
3117        regsub(
3118            r#"(?x)
3119                   ( ^    r\ .* \n     )  #  ( r  )  $1, part before where we want to put m's
3120                   ( (?:     .* \n )*? )  #  (.*? )  $2, the rest, before the m's
3121                   ( (?:  m\ .* \n )+  )  #  ( m+ )  $3, one or more m's
3122            "#,
3123            r#"$1$3$2"#,
3124        );
3125
3126        UV::adjust_exp(&mut exp);
3127
3128        assert_eq_or_diff!(&exp, &enc);
3129
3130        Ok(())
3131    }
3132
3133    trait MungeForRoundtrip {
3134        /// Munge `s` so that it resembles the output of C Tor
3135        fn adjust_exp(exp: &mut String);
3136    }
3137
3138    /// Test that we can re-encode the consensus we parsed, and that we get the same thing back.
3139    ///
3140    /// Well, roughly the same thing.
3141    //
3142    // TODO DIRAUTH want more comprehensive test; testdata2's netstatus lacks many things
3143    #[cfg(feature = "incomplete")]
3144    #[test]
3145    fn roundtrip_netstatus_plain() -> anyhow::Result<()> {
3146        roundtrip_netstatus::<plain::NetworkStatusUnverified, _, _>(
3147            "testdata2/cached-consensus",
3148            plain::NetworkStatusUnverified::verify,
3149            Duration::ZERO,
3150        )
3151    }
3152
3153    #[cfg(feature = "incomplete")]
3154    impl MungeForRoundtrip for plain::NetworkStatusUnverified {
3155        fn adjust_exp(exp: &mut String) {
3156            let mut regsub = |re, repl| regsub(exp, re, repl);
3157
3158            // We emit the optional `ns`
3159            // https://spec.torproject.org/dir-spec/consensus-formats.html#item:network-status-version
3160            regsub(
3161                r#"^network-status-version 3$"#,
3162                "network-status-version 3 ns",
3163            );
3164
3165            // C Tor writes nontrivial values for `publication` in rs `r` items,
3166            // but we use a fixed string.
3167            // https://spec.torproject.org/dir-spec/consensus-formats.html#item:r
3168            regsub(
3169                r#"^(r \S+ \S+ \S+) \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}"#,
3170                "$1 2000-01-01 00:00:01",
3171            );
3172        }
3173    }
3174
3175    #[cfg(feature = "incomplete")]
3176    #[test]
3177    fn roundtrip_netstatus_md() -> anyhow::Result<()> {
3178        roundtrip_netstatus::<md::NetworkStatusUnverified, _, _>(
3179            "testdata2/cached-microdesc-consensus",
3180            md::NetworkStatusUnverified::verify,
3181            Duration::ZERO,
3182        )
3183    }
3184
3185    #[cfg(feature = "incomplete")]
3186    impl MungeForRoundtrip for md::NetworkStatusUnverified {
3187        fn adjust_exp(exp: &mut String) {
3188            let mut regsub = |re, repl| regsub(exp, re, repl);
3189
3190            // C Tor writes nontrivial values for `publication` in rs `r` items,
3191            // but we use a fixed string.
3192            // https://spec.torproject.org/dir-spec/consensus-formats.html#item:r
3193            //
3194            // Not the same as in plain consensus: one fewer fields!
3195            regsub(
3196                r#"^(r \S+ \S+) \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}"#,
3197                "$1 2000-01-01 00:00:01",
3198            );
3199        }
3200    }
3201
3202    #[cfg(feature = "incomplete")]
3203    #[test]
3204    fn roundtrip_netstatus_vote() -> anyhow::Result<()> {
3205        roundtrip_netstatus::<vote::NetworkStatusUnverified, _, _>(
3206            "testdata2/v3-status-votes--1",
3207            |doc, trusted, _| vote::NetworkStatusUnverified::verify(doc, trusted),
3208            Duration::from_secs(20),
3209        )
3210    }
3211
3212    #[cfg(feature = "incomplete")]
3213    impl MungeForRoundtrip for vote::NetworkStatusUnverified {
3214        fn adjust_exp(exp: &mut String) {
3215            // C Tor writes items in consensuses a different order to in votes!
3216
3217            // C Tor writes different stats items with different floating point formats!
3218            let stats_massage_entry = |e: &str| {
3219                let mut e = e.to_owned();
3220                if e.contains('.') {
3221                    regsub(
3222                        &mut e,
3223                        // strip trailing 0's and then trailing `.`
3224                        r#"(?x)^ ( (?:wfu) = [0-9.]*? )( \.? 0+ ) $"#,
3225                        "$1",
3226                    );
3227                }
3228                e
3229            };
3230
3231            // C Tor writes stats items in votes in an apparently arbitrarily chosen order
3232            regsub(exp, r#"^stats (.+)$"#, |c: &regex::Captures| -> String {
3233                format!(
3234                    "stats {}",
3235                    iter_join(" ", c[1].split(' ').sorted().map(stats_massage_entry)),
3236                )
3237            });
3238
3239            let mut regsub = |re: &_, repl| regsub(exp, re, repl);
3240
3241            // C Tor writes *-protocols in an apparently arbitrarily chosen order
3242            regsub(
3243                r#"(?x)
3244                       ^ (recommended-relay-protocols\ .*)  \n
3245                         (recommended-client-protocols\ .*) \n
3246                         (required-relay-protocols\ .*)     \n
3247                         (required-client-protocols\ .*)    \n
3248                         (known-flags .*)$                  \n
3249                    "#,
3250                r#"$5
3251$2
3252$1
3253$4
3254$3
3255"#,
3256            );
3257
3258            // C Tor emits empty `client-versions` in consensuses, but not in votes.
3259            // (See also the fixup in `roundtrip_netstatus`, which relates to the *syntax*)
3260            //
3261            // Some of our inputs (eg the testdata2 votes) don't contain meaningful
3262            // info, so to make the C Tor output match our output, add them.
3263            regsub(
3264                r#"(?x) ^ (voting-delay\ .*) \n
3265                          (known-flags\ .*) \n"#,
3266                "$1
3267client-versions
3268server-versions
3269$2
3270",
3271            );
3272
3273            //#                         (?:  a\ .* \n )?    )   #    a? )           we want to put m's
3274
3275            for missing_field in [
3276                "bandwidth-file-headers", // TODO DIRAUTH implement
3277                "bandwidth-file-digest",  // TODO DIRAUTH implement
3278                "flag-thresholds",        // TODO DIRAUTH implement
3279            ] {
3280                regsub(&format!(r#"^{missing_field} .*\n"#), "");
3281            }
3282        }
3283    }
3284}