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