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: More testing is needed!
41//!
42//! TODO: There should be accessor functions for most of the fields here.
43//! As with the other tor-netdoc types, I'm deferring those till I know what
44//! they should be.
45
46mod rs;
47
48pub mod md;
49#[cfg(feature = "plain-consensus")]
50pub mod plain;
51#[cfg(feature = "ns-vote")]
52pub mod vote;
53
54#[cfg(feature = "build_docs")]
55mod build;
56
57#[cfg(feature = "parse2")]
58use {
59    crate::parse2::{self, ArgumentStream}, //
60};
61
62#[cfg(feature = "parse2")]
63pub use {
64    parse2_impls::ProtoStatusesNetdocParseAccumulator, //
65};
66
67use crate::doc::authcert::{AuthCert, AuthCertKeyIds};
68use crate::parse::keyword::Keyword;
69use crate::parse::parser::{Section, SectionRules, SectionRulesBuilder};
70use crate::parse::tokenize::{Item, ItemResult, NetDocReader};
71use crate::types::misc::*;
72use crate::util::PeekableIterator;
73use crate::{Error, KeywordEncodable, NetdocErrorKind as EK, NormalItemArgument, Pos, Result};
74use std::collections::{BTreeSet, HashMap, HashSet};
75use std::fmt::{self, Display};
76use std::result::Result as StdResult;
77use std::str::FromStr;
78use std::sync::Arc;
79use std::{net, result, time};
80use tor_error::{HasKind, internal};
81use tor_protover::Protocols;
82
83use derive_deftly::{Deftly, define_derive_deftly};
84use digest::Digest;
85use std::sync::LazyLock;
86use tor_checkable::{ExternallySigned, timed::TimerangeBound};
87use tor_llcrypto as ll;
88use tor_llcrypto::pk::rsa::RsaIdentity;
89
90use serde::{Deserialize, Deserializer};
91
92#[cfg(feature = "build_docs")]
93pub use build::MdConsensusBuilder;
94#[cfg(all(feature = "build_docs", feature = "plain-consensus"))]
95pub use build::PlainConsensusBuilder;
96#[cfg(feature = "build_docs")]
97ns_export_each_flavor! {
98    ty: RouterStatusBuilder;
99}
100
101ns_export_each_variety! {
102    ty: RouterStatus, Preamble;
103}
104
105#[deprecated]
106#[cfg(feature = "ns_consensus")]
107pub use PlainConsensus as NsConsensus;
108#[deprecated]
109#[cfg(feature = "ns_consensus")]
110pub use PlainRouterStatus as NsRouterStatus;
111#[deprecated]
112#[cfg(feature = "ns_consensus")]
113pub use UncheckedPlainConsensus as UncheckedNsConsensus;
114#[deprecated]
115#[cfg(feature = "ns_consensus")]
116pub use UnvalidatedPlainConsensus as UnvalidatedNsConsensus;
117
118#[cfg(feature = "ns-vote")]
119pub use rs::{RouterStatusMdDigestsVote, SoftwareVersion};
120
121/// `publiscation` field in routerstatus entry intro item other than in votes
122///
123/// Two arguments which are both ignored.
124/// This used to be an ISO8601 timestamp in anomalous two-argument format.
125///
126/// Nowadays, according to the spec, it can be a dummy value.
127/// So it can be a unit type.
128///
129/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:r>,
130/// except in votes which use [`Iso8601TimeSp`] instead.
131///
132/// **Not the same as** the `published` item:
133/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:published>
134#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Default)]
135#[allow(clippy::exhaustive_structs)]
136pub struct IgnoredPublicationTimeSp;
137
138/// The lifetime of a networkstatus document.
139///
140/// In a consensus, this type describes when the consensus may safely
141/// be used.  In a vote, this type describes the proposed lifetime for a
142/// consensus.
143///
144/// Aggregate of three netdoc preamble fields.
145#[derive(Clone, Debug, Deftly)]
146#[derive_deftly(Lifetime)]
147#[cfg_attr(feature = "parse2", derive_deftly(NetdocParseableFields))]
148pub struct Lifetime {
149    /// `valid-after` --- Time at which the document becomes valid
150    ///
151    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:published>
152    ///
153    /// (You might see a consensus a little while before this time,
154    /// since voting tries to finish up before the.)
155    #[cfg_attr(feature = "parse2", deftly(netdoc(single_arg)))]
156    valid_after: Iso8601TimeSp,
157    /// `fresh-until` --- Time after which there is expected to be a better version
158    /// of this consensus
159    ///
160    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:published>
161    ///
162    /// You can use the consensus after this time, but there is (or is
163    /// supposed to be) a better one by this point.
164    #[cfg_attr(feature = "parse2", deftly(netdoc(single_arg)))]
165    fresh_until: Iso8601TimeSp,
166    /// `valid-until` --- Time after which this consensus is expired.
167    ///
168    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:published>
169    ///
170    /// You should try to get a better consensus after this time,
171    /// though it's okay to keep using this one if no more recent one
172    /// can be found.
173    #[cfg_attr(feature = "parse2", deftly(netdoc(single_arg)))]
174    valid_until: Iso8601TimeSp,
175}
176
177define_derive_deftly! {
178    /// Bespoke derive for `Lifetime`, for `new` and accessors
179    Lifetime:
180
181    impl Lifetime {
182        /// Construct a new Lifetime.
183        pub fn new(
184            $( $fname: time::SystemTime, )
185        ) -> Result<Self> {
186            // Make this now because otherwise literal `valid_after` here in the body
187            // has the wrong span - the compiler refuses to look at the argument.
188            // But we can refer to the field names.
189            let self_ = Lifetime {
190                $( $fname: $fname.into(), )
191            };
192            if self_.valid_after < self_.fresh_until && self_.fresh_until < self_.valid_until {
193                Ok(self_)
194            } else {
195                Err(EK::InvalidLifetime.err())
196            }
197        }
198      $(
199        ${fattrs doc}
200        pub fn $fname(&self) -> time::SystemTime {
201            *self.$fname
202        }
203      )
204        /// Return true if this consensus is officially valid at the provided time.
205        pub fn valid_at(&self, when: time::SystemTime) -> bool {
206            *self.valid_after <= when && when <= *self.valid_until
207        }
208
209        /// Return the voting period implied by this lifetime.
210        ///
211        /// (The "voting period" is the amount of time in between when a consensus first
212        /// becomes valid, and when the next consensus is expected to become valid)
213        pub fn voting_period(&self) -> time::Duration {
214            let valid_after = self.valid_after();
215            let fresh_until = self.fresh_until();
216            fresh_until
217                .duration_since(valid_after)
218                .expect("Mis-formed lifetime")
219        }
220    }
221}
222use derive_deftly_template_Lifetime;
223
224/// A single consensus method
225///
226/// These are integers, but we don't do arithmetic on them.
227///
228/// As defined here:
229/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:consensus-methods>
230/// <https://spec.torproject.org/dir-spec/computing-consensus.html#flavor:microdesc>
231///
232/// As used in a `consensus-method` item:
233/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:consensus-method>
234#[derive(Debug, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Copy)] //
235#[derive(derive_more::From, derive_more::Into, derive_more::Display, derive_more::FromStr)]
236pub struct ConsensusMethod(u32);
237impl NormalItemArgument for ConsensusMethod {}
238
239/// A set of consensus methods
240///
241/// Implements `ItemValueParseable` as required for `consensus-methods`,
242/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:consensus-methods>
243///
244/// There is also [`consensus_methods_comma_separated`] for `m` lines in votes.
245#[derive(Debug, Clone, Default, Eq, PartialEq)]
246#[cfg_attr(feature = "parse2", derive(Deftly), derive_deftly(ItemValueParseable))]
247#[non_exhaustive]
248pub struct ConsensusMethods {
249    /// Consensus methods.
250    pub methods: BTreeSet<ConsensusMethod>,
251}
252
253/// Module for use with parse2's `with`, to parse one argument of comma-separated consensus methods
254///
255/// As found in an `m` item in a vote:
256/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:m>
257#[cfg(feature = "parse2")]
258pub mod consensus_methods_comma_separated {
259    use super::*;
260    use parse2::ArgumentError as AE;
261    use std::result::Result;
262
263    /// Parse
264    pub fn from_args<'s>(args: &mut ArgumentStream<'s>) -> Result<ConsensusMethods, AE> {
265        let mut methods = BTreeSet::new();
266        for ent in args.next().ok_or(AE::Missing)?.split(',') {
267            let ent = ent.parse().map_err(|_| AE::Invalid)?;
268            if !methods.insert(ent) {
269                return Err(AE::Invalid);
270            }
271        }
272        Ok(ConsensusMethods { methods })
273    }
274}
275
276/// A set of named network parameters.
277///
278/// These are used to describe current settings for the Tor network,
279/// current weighting parameters for path selection, and so on.  They're
280/// encoded with a space-separated K=V format.
281///
282/// A `NetParams<i32>` is part of the validated directory manager configuration,
283/// where it is built (in the builder-pattern sense) from a transparent HashMap.
284///
285/// As found in `params` in a network status:
286/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:params>
287///
288/// The same syntax is also used, and this type used for parsing, in various other places,
289/// for example routerstatus entry `w` items (bandwith weights):
290/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:w>
291#[derive(Debug, Clone, Default, Eq, PartialEq)]
292pub struct NetParams<T> {
293    /// Map from keys to values.
294    params: HashMap<String, T>,
295}
296
297impl<T> NetParams<T> {
298    /// Create a new empty list of NetParams.
299    #[allow(unused)]
300    pub fn new() -> Self {
301        NetParams {
302            params: HashMap::new(),
303        }
304    }
305    /// Retrieve a given network parameter, if it is present.
306    pub fn get<A: AsRef<str>>(&self, v: A) -> Option<&T> {
307        self.params.get(v.as_ref())
308    }
309    /// Return an iterator over all key value pairs in an arbitrary order.
310    pub fn iter(&self) -> impl Iterator<Item = (&String, &T)> {
311        self.params.iter()
312    }
313    /// Set or replace the value of a network parameter.
314    pub fn set(&mut self, k: String, v: T) {
315        self.params.insert(k, v);
316    }
317}
318
319impl<K: Into<String>, T> FromIterator<(K, T)> for NetParams<T> {
320    fn from_iter<I: IntoIterator<Item = (K, T)>>(i: I) -> Self {
321        NetParams {
322            params: i.into_iter().map(|(k, v)| (k.into(), v)).collect(),
323        }
324    }
325}
326
327impl<T> std::iter::Extend<(String, T)> for NetParams<T> {
328    fn extend<I: IntoIterator<Item = (String, T)>>(&mut self, iter: I) {
329        self.params.extend(iter);
330    }
331}
332
333impl<'de, T> Deserialize<'de> for NetParams<T>
334where
335    T: Deserialize<'de>,
336{
337    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
338    where
339        D: Deserializer<'de>,
340    {
341        let params = HashMap::deserialize(deserializer)?;
342        Ok(NetParams { params })
343    }
344}
345
346/// A list of subprotocol versions that implementors should/must provide.
347///
348/// This struct represents a pair of (optional) items:
349/// `recommended-FOO-protocols` and `required-FOO-protocols`.
350///
351/// Each consensus has two of these: one for relays, and one for clients.
352///
353/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:required-relay-protocols>
354#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
355pub struct ProtoStatus {
356    /// Set of protocols that are recommended; if we're missing a protocol
357    /// in this list we should warn the user.
358    ///
359    /// `recommended-client-protocols` or `recommended-relay-protocols`
360    recommended: Protocols,
361    /// Set of protocols that are required; if we're missing a protocol
362    /// in this list we should refuse to start.
363    ///
364    /// `required-client-protocols` or `required-relay-protocols`
365    required: Protocols,
366}
367
368impl ProtoStatus {
369    /// Check whether the list of supported protocols
370    /// is sufficient to satisfy this list of recommendations and requirements.
371    ///
372    /// If any required protocol is missing, returns [`ProtocolSupportError::MissingRequired`].
373    ///
374    /// Otherwise, if no required protocol is missing, but some recommended protocol is missing,
375    /// returns [`ProtocolSupportError::MissingRecommended`].
376    ///
377    /// Otherwise, if no recommended or required protocol is missing, returns `Ok(())`.
378    pub fn check_protocols(
379        &self,
380        supported_protocols: &Protocols,
381    ) -> StdResult<(), ProtocolSupportError> {
382        // Required protocols take precedence, so we check them first.
383        let missing_required = self.required.difference(supported_protocols);
384        if !missing_required.is_empty() {
385            return Err(ProtocolSupportError::MissingRequired(missing_required));
386        }
387        let missing_recommended = self.recommended.difference(supported_protocols);
388        if !missing_recommended.is_empty() {
389            return Err(ProtocolSupportError::MissingRecommended(
390                missing_recommended,
391            ));
392        }
393
394        Ok(())
395    }
396}
397
398/// A subprotocol that is recommended or required in the consensus was not present.
399#[derive(Clone, Debug, thiserror::Error)]
400#[cfg_attr(test, derive(PartialEq))]
401#[non_exhaustive]
402pub enum ProtocolSupportError {
403    /// At least one required protocol was not in our list of supported protocols.
404    #[error("Required protocols are not implemented: {0}")]
405    MissingRequired(Protocols),
406
407    /// At least one recommended protocol was not in our list of supported protocols.
408    ///
409    /// Also implies that no _required_ protocols were missing.
410    #[error("Recommended protocols are not implemented: {0}")]
411    MissingRecommended(Protocols),
412}
413
414impl ProtocolSupportError {
415    /// Return true if the suggested behavior for this error is a shutdown.
416    pub fn should_shutdown(&self) -> bool {
417        matches!(self, Self::MissingRequired(_))
418    }
419}
420
421impl HasKind for ProtocolSupportError {
422    fn kind(&self) -> tor_error::ErrorKind {
423        tor_error::ErrorKind::SoftwareDeprecated
424    }
425}
426
427/// A set of recommended and required protocols when running
428/// in various scenarios.
429///
430/// Represents the collection of four items: `{recommended,required}-{client,relay}-protocols`.
431///
432/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:required-relay-protocols>
433#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
434pub struct ProtoStatuses {
435    /// Lists of recommended and required subprotocol versions for clients
436    client: ProtoStatus,
437    /// Lists of recommended and required subprotocol versions for relays
438    relay: ProtoStatus,
439}
440
441impl ProtoStatuses {
442    /// Return the list of recommended and required protocols for running as a client.
443    pub fn client(&self) -> &ProtoStatus {
444        &self.client
445    }
446
447    /// Return the list of recommended and required protocols for running as a relay.
448    pub fn relay(&self) -> &ProtoStatus {
449        &self.relay
450    }
451}
452
453/// A recognized 'flavor' of consensus document.
454///
455/// The enum is exhaustive because the addition/removal of a consensus flavor
456/// should indeed be a breaking change, as it would inevitable require
457/// interfacing code to think about the handling of it.
458///
459/// <https://spec.torproject.org/dir-spec/computing-consensus.html#flavors>
460#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
461#[allow(clippy::exhaustive_enums)]
462pub enum ConsensusFlavor {
463    /// A "microdesc"-flavored consensus.  This is the one that
464    /// clients and relays use today.
465    Microdesc,
466    /// A "networkstatus"-flavored consensus.  It's used for
467    /// historical and network-health purposes.  Instead of listing
468    /// microdescriptor digests, it lists digests of full relay
469    /// descriptors.
470    Plain,
471}
472
473impl ConsensusFlavor {
474    /// Return the name of this consensus flavor.
475    pub fn name(&self) -> &'static str {
476        match self {
477            ConsensusFlavor::Plain => "ns", // spec bug, now baked in
478            ConsensusFlavor::Microdesc => "microdesc",
479        }
480    }
481    /// Try to find the flavor whose name is `name`.
482    ///
483    /// For historical reasons, an unnamed flavor indicates an "Plain"
484    /// document.
485    pub fn from_opt_name(name: Option<&str>) -> Result<Self> {
486        match name {
487            Some("microdesc") => Ok(ConsensusFlavor::Microdesc),
488            Some("ns") | None => Ok(ConsensusFlavor::Plain),
489            Some(other) => {
490                Err(EK::BadDocumentType.with_msg(format!("unrecognized flavor {:?}", other)))
491            }
492        }
493    }
494}
495
496/// The signature of a single directory authority on a networkstatus document.
497#[derive(Debug, Clone)]
498#[non_exhaustive]
499pub struct Signature {
500    /// The name of the digest algorithm used to make the signature.
501    ///
502    /// Currently sha1 and sh256 are recognized.  Here we only support
503    /// sha256.
504    pub digestname: String,
505    /// Fingerprints of the keys for the authority that made
506    /// this signature.
507    pub key_ids: AuthCertKeyIds,
508    /// The signature itself.
509    pub signature: Vec<u8>,
510}
511
512/// A collection of signatures that can be checked on a networkstatus document
513#[derive(Debug, Clone)]
514#[non_exhaustive]
515pub struct SignatureGroup {
516    /// The sha256 of the document itself
517    pub sha256: Option<[u8; 32]>,
518    /// The sha1 of the document itself
519    pub sha1: Option<[u8; 20]>,
520    /// The signatures listed on the document.
521    pub signatures: Vec<Signature>,
522}
523
524/// A shared random value produced by the directory authorities.
525#[derive(
526    Debug, Clone, Copy, Eq, PartialEq, derive_more::From, derive_more::Into, derive_more::AsRef,
527)]
528// (This doesn't need to use CtByteArray; we don't really need to compare these.)
529pub struct SharedRandVal([u8; 32]);
530
531/// A shared-random value produced by the directory authorities,
532/// along with meta-information about that value.
533#[derive(Debug, Clone, Deftly)]
534#[non_exhaustive]
535#[cfg_attr(feature = "parse2", derive_deftly(ItemValueParseable))]
536#[cfg_attr(feature = "encode", derive_deftly(ItemValueEncodable))]
537pub struct SharedRandStatus {
538    /// How many authorities revealed shares that contributed to this value.
539    pub n_reveals: u8,
540    /// The current random value.
541    ///
542    /// The properties of the secure shared-random system guarantee
543    /// that this value isn't predictable before it first becomes
544    /// live, and that a hostile party could not have forced it to
545    /// have any more than a small number of possible random values.
546    pub value: SharedRandVal,
547
548    /// The time when this SharedRandVal becomes (or became) the latest.
549    ///
550    /// (This is added per proposal 342, assuming that gets accepted.)
551    pub timestamp: Option<Iso8601TimeNoSp>,
552}
553
554/// Description of an authority's identity and address.
555///
556/// (Corresponds to a dir-source line.)
557#[derive(Debug, Clone)]
558#[non_exhaustive]
559pub struct DirSource {
560    /// human-readable nickname for this authority.
561    pub nickname: String,
562    /// Fingerprint for the _authority_ identity key of this
563    /// authority.
564    ///
565    /// This is the same key as the one that signs the authority's
566    /// certificates.
567    pub identity: RsaIdentity,
568    /// IP address for the authority
569    pub ip: net::IpAddr,
570    /// HTTP directory port for this authority
571    pub dir_port: u16,
572    /// OR port for this authority.
573    pub or_port: u16,
574}
575
576/// Recognized weight fields on a single relay in a consensus
577#[non_exhaustive]
578#[derive(Debug, Clone, Copy)]
579pub enum RelayWeight {
580    /// An unmeasured weight for a relay.
581    Unmeasured(u32),
582    /// An measured weight for a relay.
583    Measured(u32),
584}
585
586impl RelayWeight {
587    /// Return true if this weight is the result of a successful measurement
588    pub fn is_measured(&self) -> bool {
589        matches!(self, RelayWeight::Measured(_))
590    }
591    /// Return true if this weight is nonzero
592    pub fn is_nonzero(&self) -> bool {
593        !matches!(self, RelayWeight::Unmeasured(0) | RelayWeight::Measured(0))
594    }
595}
596
597/// All information about a single authority, as represented in a consensus
598#[derive(Debug, Clone)]
599#[non_exhaustive]
600pub struct ConsensusVoterInfo {
601    /// Contents of the dirsource line about an authority
602    pub dir_source: DirSource,
603    /// Human-readable contact information about the authority
604    pub contact: String,
605    /// Digest of the vote that the authority cast to contribute to
606    /// this consensus.
607    pub vote_digest: Vec<u8>,
608}
609
610/// The signed footer of a consensus netstatus.
611#[derive(Debug, Clone)]
612#[non_exhaustive]
613pub struct Footer {
614    /// Weights to be applied to certain classes of relays when choosing
615    /// for different roles.
616    ///
617    /// For example, we want to avoid choosing exits for non-exit
618    /// roles when overall the proportion of exits is small.
619    pub weights: NetParams<i32>,
620}
621
622/// A consensus document that lists relays along with their
623/// microdescriptor documents.
624pub type MdConsensus = md::Consensus;
625
626/// An MdConsensus that has been parsed and checked for timeliness,
627/// but not for signatures.
628pub type UnvalidatedMdConsensus = md::UnvalidatedConsensus;
629
630/// An MdConsensus that has been parsed but not checked for signatures
631/// and timeliness.
632pub type UncheckedMdConsensus = md::UncheckedConsensus;
633
634#[cfg(feature = "plain-consensus")]
635/// A consensus document that lists relays along with their
636/// router descriptor documents.
637pub type PlainConsensus = plain::Consensus;
638
639#[cfg(feature = "plain-consensus")]
640/// An PlainConsensus that has been parsed and checked for timeliness,
641/// but not for signatures.
642pub type UnvalidatedPlainConsensus = plain::UnvalidatedConsensus;
643
644#[cfg(feature = "plain-consensus")]
645/// An PlainConsensus that has been parsed but not checked for signatures
646/// and timeliness.
647pub type UncheckedPlainConsensus = plain::UncheckedConsensus;
648
649decl_keyword! {
650    /// Keywords that can be used in votes and consensuses.
651    // TODO: This is public because otherwise we can't use it in the
652    // ParseRouterStatus crate.  But I'd rather find a way to make it
653    // private.
654    #[non_exhaustive]
655    #[allow(missing_docs)]
656    pub NetstatusKwd {
657        // Header
658        "network-status-version" => NETWORK_STATUS_VERSION,
659        "vote-status" => VOTE_STATUS,
660        "consensus-methods" => CONSENSUS_METHODS,
661        "consensus-method" => CONSENSUS_METHOD,
662        "published" => PUBLISHED,
663        "valid-after" => VALID_AFTER,
664        "fresh-until" => FRESH_UNTIL,
665        "valid-until" => VALID_UNTIL,
666        "voting-delay" => VOTING_DELAY,
667        "client-versions" => CLIENT_VERSIONS,
668        "server-versions" => SERVER_VERSIONS,
669        "known-flags" => KNOWN_FLAGS,
670        "flag-thresholds" => FLAG_THRESHOLDS,
671        "recommended-client-protocols" => RECOMMENDED_CLIENT_PROTOCOLS,
672        "required-client-protocols" => REQUIRED_CLIENT_PROTOCOLS,
673        "recommended-relay-protocols" => RECOMMENDED_RELAY_PROTOCOLS,
674        "required-relay-protocols" => REQUIRED_RELAY_PROTOCOLS,
675        "params" => PARAMS,
676        "bandwidth-file-headers" => BANDWIDTH_FILE_HEADERS,
677        "bandwidth-file-digest" => BANDWIDTH_FILE_DIGEST,
678        // "package" is now ignored.
679
680        // header in consensus, voter section in vote?
681        "shared-rand-previous-value" => SHARED_RAND_PREVIOUS_VALUE,
682        "shared-rand-current-value" => SHARED_RAND_CURRENT_VALUE,
683
684        // Voter section (both)
685        "dir-source" => DIR_SOURCE,
686        "contact" => CONTACT,
687
688        // voter section (vote, but not consensus)
689        "legacy-dir-key" => LEGACY_DIR_KEY,
690        "shared-rand-participate" => SHARED_RAND_PARTICIPATE,
691        "shared-rand-commit" => SHARED_RAND_COMMIT,
692
693        // voter section (consensus, but not vote)
694        "vote-digest" => VOTE_DIGEST,
695
696        // voter cert beginning (but only the beginning)
697        "dir-key-certificate-version" => DIR_KEY_CERTIFICATE_VERSION,
698
699        // routerstatus
700        "r" => RS_R,
701        "a" => RS_A,
702        "s" => RS_S,
703        "v" => RS_V,
704        "pr" => RS_PR,
705        "w" => RS_W,
706        "p" => RS_P,
707        "m" => RS_M,
708        "id" => RS_ID,
709
710        // footer
711        "directory-footer" => DIRECTORY_FOOTER,
712        "bandwidth-weights" => BANDWIDTH_WEIGHTS,
713        "directory-signature" => DIRECTORY_SIGNATURE,
714    }
715}
716
717/// Shared parts of rules for all kinds of netstatus headers
718static NS_HEADER_RULES_COMMON_: LazyLock<SectionRulesBuilder<NetstatusKwd>> = LazyLock::new(|| {
719    use NetstatusKwd::*;
720    let mut rules = SectionRules::builder();
721    rules.add(NETWORK_STATUS_VERSION.rule().required().args(1..=2));
722    rules.add(VOTE_STATUS.rule().required().args(1..));
723    rules.add(VALID_AFTER.rule().required());
724    rules.add(FRESH_UNTIL.rule().required());
725    rules.add(VALID_UNTIL.rule().required());
726    rules.add(VOTING_DELAY.rule().args(2..));
727    rules.add(CLIENT_VERSIONS.rule());
728    rules.add(SERVER_VERSIONS.rule());
729    rules.add(KNOWN_FLAGS.rule().required());
730    rules.add(RECOMMENDED_CLIENT_PROTOCOLS.rule().args(1..));
731    rules.add(RECOMMENDED_RELAY_PROTOCOLS.rule().args(1..));
732    rules.add(REQUIRED_CLIENT_PROTOCOLS.rule().args(1..));
733    rules.add(REQUIRED_RELAY_PROTOCOLS.rule().args(1..));
734    rules.add(PARAMS.rule());
735    rules
736});
737/// Rules for parsing the header of a consensus.
738static NS_HEADER_RULES_CONSENSUS: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
739    use NetstatusKwd::*;
740    let mut rules = NS_HEADER_RULES_COMMON_.clone();
741    rules.add(CONSENSUS_METHOD.rule().args(1..=1));
742    rules.add(SHARED_RAND_PREVIOUS_VALUE.rule().args(2..));
743    rules.add(SHARED_RAND_CURRENT_VALUE.rule().args(2..));
744    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
745    rules.build()
746});
747/*
748/// Rules for parsing the header of a vote.
749static NS_HEADER_RULES_VOTE: SectionRules<NetstatusKwd> = {
750    use NetstatusKwd::*;
751    let mut rules = NS_HEADER_RULES_COMMON_.clone();
752    rules.add(CONSENSUS_METHODS.rule().args(1..));
753    rules.add(FLAG_THRESHOLDS.rule());
754    rules.add(BANDWIDTH_FILE_HEADERS.rule());
755    rules.add(BANDWIDTH_FILE_DIGEST.rule().args(1..));
756    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
757    rules
758};
759/// Rules for parsing a single voter's information in a vote.
760static NS_VOTERINFO_RULES_VOTE: SectionRules<NetstatusKwd> = {
761    use NetstatusKwd::*;
762    let mut rules = SectionRules::new();
763    rules.add(DIR_SOURCE.rule().required().args(6..));
764    rules.add(CONTACT.rule().required());
765    rules.add(LEGACY_DIR_KEY.rule().args(1..));
766    rules.add(SHARED_RAND_PARTICIPATE.rule().no_args());
767    rules.add(SHARED_RAND_COMMIT.rule().may_repeat().args(4..));
768    rules.add(SHARED_RAND_PREVIOUS_VALUE.rule().args(2..));
769    rules.add(SHARED_RAND_CURRENT_VALUE.rule().args(2..));
770    // then comes an entire cert: When we implement vote parsing,
771    // we should use the authcert code for handling that.
772    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
773    rules
774};
775 */
776/// Rules for parsing a single voter's information in a consensus
777static NS_VOTERINFO_RULES_CONSENSUS: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
778    use NetstatusKwd::*;
779    let mut rules = SectionRules::builder();
780    rules.add(DIR_SOURCE.rule().required().args(6..));
781    rules.add(CONTACT.rule().required());
782    rules.add(VOTE_DIGEST.rule().required());
783    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
784    rules.build()
785});
786/// Shared rules for parsing a single routerstatus
787static NS_ROUTERSTATUS_RULES_COMMON_: LazyLock<SectionRulesBuilder<NetstatusKwd>> =
788    LazyLock::new(|| {
789        use NetstatusKwd::*;
790        let mut rules = SectionRules::builder();
791        rules.add(RS_A.rule().may_repeat().args(1..));
792        rules.add(RS_S.rule().required());
793        rules.add(RS_V.rule());
794        rules.add(RS_PR.rule().required());
795        rules.add(RS_W.rule());
796        rules.add(RS_P.rule().args(2..));
797        rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
798        rules
799    });
800
801/// Rules for parsing a single routerstatus in an NS consensus
802static NS_ROUTERSTATUS_RULES_PLAIN: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
803    use NetstatusKwd::*;
804    let mut rules = NS_ROUTERSTATUS_RULES_COMMON_.clone();
805    rules.add(RS_R.rule().required().args(8..));
806    rules.build()
807});
808
809/*
810/// Rules for parsing a single routerstatus in a vote
811static NS_ROUTERSTATUS_RULES_VOTE: SectionRules<NetstatusKwd> = {
812    use NetstatusKwd::*;
813        let mut rules = NS_ROUTERSTATUS_RULES_COMMON_.clone();
814        rules.add(RS_R.rule().required().args(8..));
815        rules.add(RS_M.rule().may_repeat().args(2..));
816        rules.add(RS_ID.rule().may_repeat().args(2..)); // may-repeat?
817        rules
818    };
819*/
820/// Rules for parsing a single routerstatus in a microdesc consensus
821static NS_ROUTERSTATUS_RULES_MDCON: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
822    use NetstatusKwd::*;
823    let mut rules = NS_ROUTERSTATUS_RULES_COMMON_.clone();
824    rules.add(RS_R.rule().required().args(6..));
825    rules.add(RS_M.rule().required().args(1..));
826    rules.build()
827});
828/// Rules for parsing consensus fields from a footer.
829static NS_FOOTER_RULES: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
830    use NetstatusKwd::*;
831    let mut rules = SectionRules::builder();
832    rules.add(DIRECTORY_FOOTER.rule().required().no_args());
833    // consensus only
834    rules.add(BANDWIDTH_WEIGHTS.rule());
835    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
836    rules.build()
837});
838
839impl ProtoStatus {
840    /// Construct a ProtoStatus from two chosen keywords in a section.
841    fn from_section(
842        sec: &Section<'_, NetstatusKwd>,
843        recommend_token: NetstatusKwd,
844        required_token: NetstatusKwd,
845    ) -> Result<ProtoStatus> {
846        /// Helper: extract a Protocols entry from an item's arguments.
847        fn parse(t: Option<&Item<'_, NetstatusKwd>>) -> Result<Protocols> {
848            if let Some(item) = t {
849                item.args_as_str()
850                    .parse::<Protocols>()
851                    .map_err(|e| EK::BadArgument.at_pos(item.pos()).with_source(e))
852            } else {
853                Ok(Protocols::new())
854            }
855        }
856
857        let recommended = parse(sec.get(recommend_token))?;
858        let required = parse(sec.get(required_token))?;
859        Ok(ProtoStatus {
860            recommended,
861            required,
862        })
863    }
864
865    /// Return the protocols that are listed as "required" in this `ProtoStatus`.
866    ///
867    /// Implementations may assume that relays on the network implement all the
868    /// protocols in the relays' required-protocols list.  Implementations should
869    /// refuse to start if they do not implement all the protocols on their own
870    /// (client or relay) required-protocols list.
871    pub fn required_protocols(&self) -> &Protocols {
872        &self.required
873    }
874
875    /// Return the protocols that are listed as "recommended" in this `ProtoStatus`.
876    ///
877    /// Implementations should warn if they do not implement all the protocols
878    /// on their own (client or relay) recommended-protocols list.
879    pub fn recommended_protocols(&self) -> &Protocols {
880        &self.recommended
881    }
882}
883
884impl<T> std::str::FromStr for NetParams<T>
885where
886    T: std::str::FromStr,
887    T::Err: std::error::Error,
888{
889    type Err = Error;
890    fn from_str(s: &str) -> Result<Self> {
891        /// Helper: parse a single K=V pair.
892        fn parse_pair<U>(p: &str) -> Result<(String, U)>
893        where
894            U: std::str::FromStr,
895            U::Err: std::error::Error,
896        {
897            let parts: Vec<_> = p.splitn(2, '=').collect();
898            if parts.len() != 2 {
899                return Err(EK::BadArgument
900                    .at_pos(Pos::at(p))
901                    .with_msg("Missing = in key=value list"));
902            }
903            let num = parts[1].parse::<U>().map_err(|e| {
904                EK::BadArgument
905                    .at_pos(Pos::at(parts[1]))
906                    .with_msg(e.to_string())
907            })?;
908            Ok((parts[0].to_string(), num))
909        }
910
911        let params = s
912            .split(' ')
913            .filter(|p| !p.is_empty())
914            .map(parse_pair)
915            .collect::<Result<HashMap<_, _>>>()?;
916        Ok(NetParams { params })
917    }
918}
919
920impl FromStr for SharedRandVal {
921    type Err = Error;
922    fn from_str(s: &str) -> Result<Self> {
923        let val: B64 = s.parse()?;
924        let val = SharedRandVal(val.into_array()?);
925        Ok(val)
926    }
927}
928impl Display for SharedRandVal {
929    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
930        Display::fmt(&B64::from(Vec::from(self.0)), f)
931    }
932}
933impl NormalItemArgument for SharedRandVal {}
934
935impl SharedRandStatus {
936    /// Parse a current or previous shared rand value from a given
937    /// SharedRandPreviousValue or SharedRandCurrentValue.
938    fn from_item(item: &Item<'_, NetstatusKwd>) -> Result<Self> {
939        match item.kwd() {
940            NetstatusKwd::SHARED_RAND_PREVIOUS_VALUE | NetstatusKwd::SHARED_RAND_CURRENT_VALUE => {}
941            _ => {
942                return Err(Error::from(internal!(
943                    "wrong keyword {:?} on shared-random value",
944                    item.kwd()
945                ))
946                .at_pos(item.pos()));
947            }
948        }
949        let n_reveals: u8 = item.parse_arg(0)?;
950        let value: SharedRandVal = item.parse_arg(1)?;
951        // Added in proposal 342
952        let timestamp = item.parse_optional_arg::<Iso8601TimeNoSp>(2)?;
953        Ok(SharedRandStatus {
954            n_reveals,
955            value,
956            timestamp,
957        })
958    }
959
960    /// Return the actual shared random value.
961    pub fn value(&self) -> &SharedRandVal {
962        &self.value
963    }
964
965    /// Return the timestamp (if any) associated with this `SharedRandValue`.
966    pub fn timestamp(&self) -> Option<std::time::SystemTime> {
967        self.timestamp.map(|t| t.0)
968    }
969}
970
971impl DirSource {
972    /// Parse a "dir-source" item
973    fn from_item(item: &Item<'_, NetstatusKwd>) -> Result<Self> {
974        if item.kwd() != NetstatusKwd::DIR_SOURCE {
975            return Err(
976                Error::from(internal!("Bad keyword {:?} on dir-source", item.kwd()))
977                    .at_pos(item.pos()),
978            );
979        }
980        let nickname = item.required_arg(0)?.to_string();
981        let identity = item.parse_arg::<Fingerprint>(1)?.into();
982        let ip = item.parse_arg(3)?;
983        let dir_port = item.parse_arg(4)?;
984        let or_port = item.parse_arg(5)?;
985
986        Ok(DirSource {
987            nickname,
988            identity,
989            ip,
990            dir_port,
991            or_port,
992        })
993    }
994}
995
996impl ConsensusVoterInfo {
997    /// Parse a single ConsensusVoterInfo from a voter info section.
998    fn from_section(sec: &Section<'_, NetstatusKwd>) -> Result<ConsensusVoterInfo> {
999        use NetstatusKwd::*;
1000        // this unwrap should be safe because if there is not at least one
1001        // token in the section, the section is unparsable.
1002        #[allow(clippy::unwrap_used)]
1003        let first = sec.first_item().unwrap();
1004        if first.kwd() != DIR_SOURCE {
1005            return Err(Error::from(internal!(
1006                "Wrong keyword {:?} at start of voter info",
1007                first.kwd()
1008            ))
1009            .at_pos(first.pos()));
1010        }
1011        let dir_source = DirSource::from_item(sec.required(DIR_SOURCE)?)?;
1012
1013        let contact = sec.required(CONTACT)?.args_as_str().to_string();
1014
1015        let vote_digest = sec.required(VOTE_DIGEST)?.parse_arg::<B16>(0)?.into();
1016
1017        Ok(ConsensusVoterInfo {
1018            dir_source,
1019            contact,
1020            vote_digest,
1021        })
1022    }
1023}
1024
1025impl Default for RelayWeight {
1026    fn default() -> RelayWeight {
1027        RelayWeight::Unmeasured(0)
1028    }
1029}
1030
1031impl RelayWeight {
1032    /// Parse a routerweight from a "w" line.
1033    fn from_item(item: &Item<'_, NetstatusKwd>) -> Result<RelayWeight> {
1034        if item.kwd() != NetstatusKwd::RS_W {
1035            return Err(
1036                Error::from(internal!("Wrong keyword {:?} on W line", item.kwd()))
1037                    .at_pos(item.pos()),
1038            );
1039        }
1040
1041        let params = item.args_as_str().parse()?;
1042
1043        Self::from_net_params(&params).map_err(|e| e.at_pos(item.pos()))
1044    }
1045
1046    /// Parse a routerweight from partially-parsed `w` line in the form of a `NetParams`
1047    ///
1048    /// This function is the common part shared between `parse2` and `parse`.
1049    fn from_net_params(params: &NetParams<u32>) -> Result<RelayWeight> {
1050        let bw = params.params.get("Bandwidth");
1051        let unmeas = params.params.get("Unmeasured");
1052
1053        let bw = match bw {
1054            None => return Ok(RelayWeight::Unmeasured(0)),
1055            Some(b) => *b,
1056        };
1057
1058        match unmeas {
1059            None | Some(0) => Ok(RelayWeight::Measured(bw)),
1060            Some(1) => Ok(RelayWeight::Unmeasured(bw)),
1061            _ => Err(EK::BadArgument.with_msg("unmeasured value")),
1062        }
1063    }
1064}
1065
1066/// `parse2` impls for types in this module
1067///
1068/// Separate module to save on repeated `cfg` and for a separate namespace.
1069#[cfg(feature = "parse2")]
1070mod parse2_impls {
1071    use super::*;
1072    use parse2::ArgumentError as AE;
1073    use parse2::ErrorProblem as EP;
1074    use parse2::{ArgumentStream, ItemArgumentParseable, ItemValueParseable};
1075    use parse2::{KeywordRef, NetdocParseableFields, UnparsedItem};
1076    use paste::paste;
1077    use std::result::Result;
1078
1079    /// Implements `NetdocParseableFields` for `ProtoStatuses`
1080    ///
1081    /// We have this macro so that it's impossible to write things like
1082    /// ```text
1083    ///      ProtoStatuses {
1084    ///          client: ProtoStatus {
1085    ///              recommended: something something recommended_relay_versions something,
1086    /// ```
1087    ///
1088    /// (The structure of `ProtoStatuses` means the normal parse2 derive won't work for it.
1089    /// Note the bug above: the recommended *relay* version info is put in the *client* field.
1090    /// Preventing this bug must involve: avoiding writing twice the field name elements,
1091    /// such as `relay` and `client`, during this kind of construction/conversion.)
1092    macro_rules! impl_proto_statuses { { $( $rr:ident $cr:ident; )* } => { paste! {
1093        #[derive(Deftly)]
1094        #[derive_deftly(NetdocParseableFields)]
1095        // Only ProtoStatusesParseNetdocParseAccumulator is exposed.
1096        #[allow(unreachable_pub)]
1097        pub struct ProtoStatusesParseHelper {
1098            $(
1099                #[deftly(netdoc(default))]
1100                [<$rr _ $cr _protocols>]: Protocols,
1101            )*
1102        }
1103
1104        /// Partially parsed `ProtoStatuses`
1105        pub use ProtoStatusesParseHelperNetdocParseAccumulator
1106            as ProtoStatusesNetdocParseAccumulator;
1107
1108        impl NetdocParseableFields for ProtoStatuses {
1109            type Accumulator = ProtoStatusesNetdocParseAccumulator;
1110            fn is_item_keyword(kw: KeywordRef<'_>) -> bool {
1111                ProtoStatusesParseHelper::is_item_keyword(kw)
1112            }
1113            fn accumulate_item(
1114                acc: &mut Self::Accumulator,
1115                item: UnparsedItem<'_>,
1116            ) -> Result<(), EP> {
1117                ProtoStatusesParseHelper::accumulate_item(acc, item)
1118            }
1119            fn finish(acc: Self::Accumulator) -> Result<Self, EP> {
1120                let parse = ProtoStatusesParseHelper::finish(acc)?;
1121                let mut out = ProtoStatuses::default();
1122                $(
1123                    out.$cr.$rr = parse.[< $rr _ $cr _protocols >];
1124                )*
1125                Ok(out)
1126            }
1127        }
1128    } } }
1129
1130    impl_proto_statuses! {
1131        required client;
1132        required relay;
1133        recommended client;
1134        recommended relay;
1135    }
1136
1137    impl ItemValueParseable for NetParams<i32> {
1138        fn from_unparsed(item: parse2::UnparsedItem<'_>) -> Result<Self, EP> {
1139            item.check_no_object()?;
1140            item.args_copy()
1141                .into_remaining()
1142                .parse()
1143                .map_err(item.invalid_argument_handler("parameters"))
1144        }
1145    }
1146
1147    impl ItemValueParseable for RelayWeight {
1148        fn from_unparsed(item: parse2::UnparsedItem<'_>) -> Result<Self, EP> {
1149            item.check_no_object()?;
1150            (|| {
1151                let params = item.args_copy().into_remaining().parse()?;
1152                Self::from_net_params(&params)
1153            })()
1154            .map_err(item.invalid_argument_handler("weights"))
1155        }
1156    }
1157
1158    impl ItemValueParseable for rs::SoftwareVersion {
1159        fn from_unparsed(mut item: parse2::UnparsedItem<'_>) -> Result<Self, EP> {
1160            item.check_no_object()?;
1161            item.args_mut()
1162                .into_remaining()
1163                .parse()
1164                .map_err(item.invalid_argument_handler("version"))
1165        }
1166    }
1167
1168    impl ItemArgumentParseable for IgnoredPublicationTimeSp {
1169        fn from_args(a: &mut ArgumentStream) -> Result<IgnoredPublicationTimeSp, AE> {
1170            let mut next_arg = || a.next().ok_or(AE::Missing);
1171            let _: &str = next_arg()?;
1172            let _: &str = next_arg()?;
1173            Ok(IgnoredPublicationTimeSp)
1174        }
1175    }
1176}
1177
1178impl Footer {
1179    /// Parse a directory footer from a footer section.
1180    fn from_section(sec: &Section<'_, NetstatusKwd>) -> Result<Footer> {
1181        use NetstatusKwd::*;
1182        sec.required(DIRECTORY_FOOTER)?;
1183
1184        let weights = sec
1185            .maybe(BANDWIDTH_WEIGHTS)
1186            .args_as_str()
1187            .unwrap_or("")
1188            .parse()?;
1189
1190        Ok(Footer { weights })
1191    }
1192}
1193
1194/// Result of checking a single authority signature.
1195enum SigCheckResult {
1196    /// The signature checks out.  Great!
1197    Valid,
1198    /// The signature is invalid; no additional information could make it
1199    /// valid.
1200    Invalid,
1201    /// We can't check the signature because we don't have a
1202    /// certificate with the right signing key.
1203    MissingCert,
1204}
1205
1206impl Signature {
1207    /// Parse a Signature from a directory-signature section
1208    fn from_item(item: &Item<'_, NetstatusKwd>) -> Result<Signature> {
1209        if item.kwd() != NetstatusKwd::DIRECTORY_SIGNATURE {
1210            return Err(Error::from(internal!(
1211                "Wrong keyword {:?} for directory signature",
1212                item.kwd()
1213            ))
1214            .at_pos(item.pos()));
1215        }
1216
1217        let (alg, id_fp, sk_fp) = if item.n_args() > 2 {
1218            (
1219                item.required_arg(0)?,
1220                item.required_arg(1)?,
1221                item.required_arg(2)?,
1222            )
1223        } else {
1224            ("sha1", item.required_arg(0)?, item.required_arg(1)?)
1225        };
1226
1227        let digestname = alg.to_string();
1228        let id_fingerprint = id_fp.parse::<Fingerprint>()?.into();
1229        let sk_fingerprint = sk_fp.parse::<Fingerprint>()?.into();
1230        let key_ids = AuthCertKeyIds {
1231            id_fingerprint,
1232            sk_fingerprint,
1233        };
1234        let signature = item.obj("SIGNATURE")?;
1235
1236        Ok(Signature {
1237            digestname,
1238            key_ids,
1239            signature,
1240        })
1241    }
1242
1243    /// Return true if this signature has the identity key and signing key
1244    /// that match a given cert.
1245    fn matches_cert(&self, cert: &AuthCert) -> bool {
1246        cert.key_ids() == self.key_ids
1247    }
1248
1249    /// If possible, find the right certificate for checking this signature
1250    /// from among a slice of certificates.
1251    fn find_cert<'a>(&self, certs: &'a [AuthCert]) -> Option<&'a AuthCert> {
1252        certs.iter().find(|&c| self.matches_cert(c))
1253    }
1254
1255    /// Try to check whether this signature is a valid signature of a
1256    /// provided digest, given a slice of certificates that might contain
1257    /// its signing key.
1258    fn check_signature(&self, signed_digest: &[u8], certs: &[AuthCert]) -> SigCheckResult {
1259        match self.find_cert(certs) {
1260            None => SigCheckResult::MissingCert,
1261            Some(cert) => {
1262                let key = cert.signing_key();
1263                match key.verify(signed_digest, &self.signature[..]) {
1264                    Ok(()) => SigCheckResult::Valid,
1265                    Err(_) => SigCheckResult::Invalid,
1266                }
1267            }
1268        }
1269    }
1270}
1271
1272impl SignatureGroup {
1273    // TODO: these functions are pretty similar and could probably stand to be
1274    // refactored a lot.
1275
1276    /// Helper: Return a pair of the number of possible authorities'
1277    /// signatures in this object for which we _could_ find certs, and
1278    /// a list of the signatures we couldn't find certificates for.
1279    fn list_missing(&self, certs: &[AuthCert]) -> (usize, Vec<&Signature>) {
1280        let mut ok: HashSet<RsaIdentity> = HashSet::new();
1281        let mut missing = Vec::new();
1282        for sig in &self.signatures {
1283            let id_fingerprint = &sig.key_ids.id_fingerprint;
1284            if ok.contains(id_fingerprint) {
1285                continue;
1286            }
1287            if sig.find_cert(certs).is_some() {
1288                ok.insert(*id_fingerprint);
1289                continue;
1290            }
1291
1292            missing.push(sig);
1293        }
1294        (ok.len(), missing)
1295    }
1296
1297    /// Given a list of authority identity key fingerprints, return true if
1298    /// this signature group is _potentially_ well-signed according to those
1299    /// authorities.
1300    fn could_validate(&self, authorities: &[&RsaIdentity]) -> bool {
1301        let mut signed_by: HashSet<RsaIdentity> = HashSet::new();
1302        for sig in &self.signatures {
1303            let id_fp = &sig.key_ids.id_fingerprint;
1304            if signed_by.contains(id_fp) {
1305                // Already found this in the list.
1306                continue;
1307            }
1308            if authorities.contains(&id_fp) {
1309                signed_by.insert(*id_fp);
1310            }
1311        }
1312
1313        signed_by.len() > (authorities.len() / 2)
1314    }
1315
1316    /// Return true if the signature group defines a valid signature.
1317    ///
1318    /// A signature is valid if it signed by more than half of the
1319    /// authorities.  This API requires that `n_authorities` is the number of
1320    /// authorities we believe in, and that every cert in `certs` belongs
1321    /// to a real authority.
1322    fn validate(&self, n_authorities: usize, certs: &[AuthCert]) -> bool {
1323        // A set of the authorities (by identity) who have have signed
1324        // this document.  We use a set here in case `certs` has more
1325        // than one certificate for a single authority.
1326        let mut ok: HashSet<RsaIdentity> = HashSet::new();
1327
1328        for sig in &self.signatures {
1329            let id_fingerprint = &sig.key_ids.id_fingerprint;
1330            if ok.contains(id_fingerprint) {
1331                // We already checked at least one signature using this
1332                // authority's identity fingerprint.
1333                continue;
1334            }
1335
1336            let d: Option<&[u8]> = match sig.digestname.as_ref() {
1337                "sha256" => self.sha256.as_ref().map(|a| &a[..]),
1338                "sha1" => self.sha1.as_ref().map(|a| &a[..]),
1339                _ => None, // We don't know how to find this digest.
1340            };
1341            if d.is_none() {
1342                // We don't support this kind of digest for this kind
1343                // of document.
1344                continue;
1345            }
1346
1347            // Unwrap should be safe because of above `d.is_none()` check
1348            #[allow(clippy::unwrap_used)]
1349            match sig.check_signature(d.as_ref().unwrap(), certs) {
1350                SigCheckResult::Valid => {
1351                    ok.insert(*id_fingerprint);
1352                }
1353                _ => continue,
1354            }
1355        }
1356
1357        ok.len() > (n_authorities / 2)
1358    }
1359}
1360
1361#[cfg(test)]
1362mod test {
1363    // @@ begin test lint list maintained by maint/add_warning @@
1364    #![allow(clippy::bool_assert_comparison)]
1365    #![allow(clippy::clone_on_copy)]
1366    #![allow(clippy::dbg_macro)]
1367    #![allow(clippy::mixed_attributes_style)]
1368    #![allow(clippy::print_stderr)]
1369    #![allow(clippy::print_stdout)]
1370    #![allow(clippy::single_char_pattern)]
1371    #![allow(clippy::unwrap_used)]
1372    #![allow(clippy::unchecked_time_subtraction)]
1373    #![allow(clippy::useless_vec)]
1374    #![allow(clippy::needless_pass_by_value)]
1375    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
1376    use super::*;
1377    use hex_literal::hex;
1378    #[cfg(all(feature = "ns-vote", feature = "parse2"))]
1379    use {
1380        crate::parse2::{NetdocSigned as _, ParseInput, parse_netdoc},
1381        std::fs,
1382    };
1383
1384    const CERTS: &str = include_str!("../../testdata/authcerts2.txt");
1385    const CONSENSUS: &str = include_str!("../../testdata/mdconsensus1.txt");
1386
1387    #[cfg(feature = "plain-consensus")]
1388    const PLAIN_CERTS: &str = include_str!("../../testdata2/cached-certs");
1389    #[cfg(feature = "plain-consensus")]
1390    const PLAIN_CONSENSUS: &str = include_str!("../../testdata2/cached-consensus");
1391
1392    fn read_bad(fname: &str) -> String {
1393        use std::fs;
1394        use std::path::PathBuf;
1395        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1396        path.push("testdata");
1397        path.push("bad-mdconsensus");
1398        path.push(fname);
1399
1400        fs::read_to_string(path).unwrap()
1401    }
1402
1403    #[test]
1404    fn parse_and_validate_md() -> Result<()> {
1405        use std::net::SocketAddr;
1406        use tor_checkable::{SelfSigned, Timebound};
1407        let mut certs = Vec::new();
1408        for cert in AuthCert::parse_multiple(CERTS)? {
1409            let cert = cert?.check_signature()?.dangerously_assume_timely();
1410            certs.push(cert);
1411        }
1412        let auth_ids: Vec<_> = certs.iter().map(|c| c.id_fingerprint()).collect();
1413
1414        assert_eq!(certs.len(), 3);
1415
1416        let (_, _, consensus) = MdConsensus::parse(CONSENSUS)?;
1417        let consensus = consensus.dangerously_assume_timely().set_n_authorities(3);
1418
1419        // The set of authorities we know _could_ validate this cert.
1420        assert!(consensus.authorities_are_correct(&auth_ids));
1421        // A subset would also work.
1422        assert!(consensus.authorities_are_correct(&auth_ids[0..1]));
1423        {
1424            // If we only believe in an authority that isn't listed,
1425            // that won't work.
1426            let bad_auth_id = (*b"xxxxxxxxxxxxxxxxxxxx").into();
1427            assert!(!consensus.authorities_are_correct(&[&bad_auth_id]));
1428        }
1429
1430        let missing = consensus.key_is_correct(&[]).err().unwrap();
1431        assert_eq!(3, missing.len());
1432        assert!(consensus.key_is_correct(&certs).is_ok());
1433        let missing = consensus.key_is_correct(&certs[0..1]).err().unwrap();
1434        assert_eq!(2, missing.len());
1435
1436        // here is a trick that had better not work.
1437        let same_three_times = vec![certs[0].clone(), certs[0].clone(), certs[0].clone()];
1438        let missing = consensus.key_is_correct(&same_three_times).err().unwrap();
1439
1440        assert_eq!(2, missing.len());
1441        assert!(consensus.is_well_signed(&same_three_times).is_err());
1442
1443        assert!(consensus.key_is_correct(&certs).is_ok());
1444        let consensus = consensus.check_signature(&certs)?;
1445
1446        assert_eq!(6, consensus.relays().len());
1447        let r0 = &consensus.relays()[0];
1448        assert_eq!(
1449            r0.md_digest(),
1450            &hex!("73dabe0a0468f4f7a67810a18d11e36731bb1d2ec3634db459100609f3b3f535")
1451        );
1452        assert_eq!(
1453            r0.rsa_identity().as_bytes(),
1454            &hex!("0a3057af2910415794d8ea430309d9ac5f5d524b")
1455        );
1456        assert!(!r0.weight().is_measured());
1457        assert!(!r0.weight().is_nonzero());
1458        let pv = &r0.protovers();
1459        assert!(pv.supports_subver("HSDir", 2));
1460        assert!(!pv.supports_subver("HSDir", 3));
1461        let ip4 = "127.0.0.1:5002".parse::<SocketAddr>().unwrap();
1462        let ip6 = "[::1]:5002".parse::<SocketAddr>().unwrap();
1463        assert!(r0.addrs().any(|a| a == ip4));
1464        assert!(r0.addrs().any(|a| a == ip6));
1465
1466        Ok(())
1467    }
1468
1469    #[test]
1470    #[cfg(feature = "plain-consensus")]
1471    fn parse_and_validate_ns() -> Result<()> {
1472        use tor_checkable::{SelfSigned, Timebound};
1473        let mut certs = Vec::new();
1474        for cert in AuthCert::parse_multiple(PLAIN_CERTS)? {
1475            let cert = cert?.check_signature()?.dangerously_assume_timely();
1476            certs.push(cert);
1477        }
1478        let auth_ids: Vec<_> = certs.iter().map(|c| c.id_fingerprint()).collect();
1479        assert_eq!(certs.len(), 4);
1480
1481        let (_, _, consensus) = PlainConsensus::parse(PLAIN_CONSENSUS)?;
1482        let consensus = consensus.dangerously_assume_timely().set_n_authorities(3);
1483        // The set of authorities we know _could_ validate this cert.
1484        assert!(consensus.authorities_are_correct(&auth_ids));
1485        // A subset would also work.
1486        assert!(consensus.authorities_are_correct(&auth_ids[0..1]));
1487
1488        assert!(consensus.key_is_correct(&certs).is_ok());
1489
1490        let _consensus = consensus.check_signature(&certs)?;
1491
1492        Ok(())
1493    }
1494
1495    #[test]
1496    #[cfg(all(feature = "ns-vote", feature = "parse2"))]
1497    fn parse2_vote() -> anyhow::Result<()> {
1498        let file = "testdata2/v3-status-votes--1";
1499        let text = fs::read_to_string(file)?;
1500
1501        // TODO replace the poc struct here when we have parsing of proper whole votes
1502        use crate::parse2::poc::netstatus::NetworkStatusSignedVote;
1503
1504        let input = ParseInput::new(&text, file);
1505        let doc: NetworkStatusSignedVote = parse_netdoc(&input)?;
1506
1507        println!("{doc:?}");
1508        println!("{:#?}", doc.inspect_unverified().0.r[0]);
1509
1510        Ok(())
1511    }
1512
1513    #[test]
1514    fn test_bad() {
1515        use crate::Pos;
1516        fn check(fname: &str, e: &Error) {
1517            let content = read_bad(fname);
1518            let res = MdConsensus::parse(&content);
1519            assert!(res.is_err());
1520            assert_eq!(&res.err().unwrap(), e);
1521        }
1522
1523        check(
1524            "bad-flags",
1525            &EK::BadArgument
1526                .at_pos(Pos::from_line(27, 1))
1527                .with_msg("Flags out of order"),
1528        );
1529        check(
1530            "bad-md-digest",
1531            &EK::BadArgument
1532                .at_pos(Pos::from_line(40, 3))
1533                .with_msg("Invalid base64"),
1534        );
1535        check(
1536            "bad-weight",
1537            &EK::BadArgument
1538                .at_pos(Pos::from_line(67, 141))
1539                .with_msg("invalid digit found in string"),
1540        );
1541        check(
1542            "bad-weights",
1543            &EK::BadArgument
1544                .at_pos(Pos::from_line(51, 13))
1545                .with_msg("invalid digit found in string"),
1546        );
1547        check(
1548            "wrong-order",
1549            &EK::WrongSortOrder.at_pos(Pos::from_line(52, 1)),
1550        );
1551        check(
1552            "wrong-start",
1553            &EK::UnexpectedToken
1554                .with_msg("vote-status")
1555                .at_pos(Pos::from_line(1, 1)),
1556        );
1557        check("wrong-version", &EK::BadDocumentVersion.with_msg("10"));
1558    }
1559
1560    fn gettok(s: &str) -> Result<Item<'_, NetstatusKwd>> {
1561        let mut reader = NetDocReader::new(s)?;
1562        let tok = reader.next().unwrap();
1563        assert!(reader.next().is_none());
1564        tok
1565    }
1566
1567    #[test]
1568    fn test_weight() {
1569        let w = gettok("w Unmeasured=1 Bandwidth=6\n").unwrap();
1570        let w = RelayWeight::from_item(&w).unwrap();
1571        assert!(!w.is_measured());
1572        assert!(w.is_nonzero());
1573
1574        let w = gettok("w Bandwidth=10\n").unwrap();
1575        let w = RelayWeight::from_item(&w).unwrap();
1576        assert!(w.is_measured());
1577        assert!(w.is_nonzero());
1578
1579        let w = RelayWeight::default();
1580        assert!(!w.is_measured());
1581        assert!(!w.is_nonzero());
1582
1583        let w = gettok("w Mustelid=66 Cheato=7 Unmeasured=1\n").unwrap();
1584        let w = RelayWeight::from_item(&w).unwrap();
1585        assert!(!w.is_measured());
1586        assert!(!w.is_nonzero());
1587
1588        let w = gettok("r foo\n").unwrap();
1589        let w = RelayWeight::from_item(&w);
1590        assert!(w.is_err());
1591
1592        let w = gettok("r Bandwidth=6 Unmeasured=Frog\n").unwrap();
1593        let w = RelayWeight::from_item(&w);
1594        assert!(w.is_err());
1595
1596        let w = gettok("r Bandwidth=6 Unmeasured=3\n").unwrap();
1597        let w = RelayWeight::from_item(&w);
1598        assert!(w.is_err());
1599    }
1600
1601    #[test]
1602    fn test_netparam() {
1603        let p = "Hello=600 Goodbye=5 Fred=7"
1604            .parse::<NetParams<u32>>()
1605            .unwrap();
1606        assert_eq!(p.get("Hello"), Some(&600_u32));
1607
1608        let p = "Hello=Goodbye=5 Fred=7".parse::<NetParams<u32>>();
1609        assert!(p.is_err());
1610
1611        let p = "Hello=Goodbye Fred=7".parse::<NetParams<u32>>();
1612        assert!(p.is_err());
1613    }
1614
1615    #[test]
1616    fn test_sharedrand() {
1617        let sr =
1618            gettok("shared-rand-previous-value 9 5LodY4yWxFhTKtxpV9wAgNA9N8flhUCH0NqQv1/05y4\n")
1619                .unwrap();
1620        let sr = SharedRandStatus::from_item(&sr).unwrap();
1621
1622        assert_eq!(sr.n_reveals, 9);
1623        assert_eq!(
1624            sr.value.0,
1625            hex!("e4ba1d638c96c458532adc6957dc0080d03d37c7e5854087d0da90bf5ff4e72e")
1626        );
1627        assert!(sr.timestamp.is_none());
1628
1629        let sr2 = gettok(
1630            "shared-rand-current-value 9 \
1631                    5LodY4yWxFhTKtxpV9wAgNA9N8flhUCH0NqQv1/05y4 2022-01-20T12:34:56\n",
1632        )
1633        .unwrap();
1634        let sr2 = SharedRandStatus::from_item(&sr2).unwrap();
1635        assert_eq!(sr2.n_reveals, sr.n_reveals);
1636        assert_eq!(sr2.value.0, sr.value.0);
1637        assert_eq!(
1638            sr2.timestamp.unwrap().0,
1639            humantime::parse_rfc3339("2022-01-20T12:34:56Z").unwrap()
1640        );
1641
1642        let sr = gettok("foo bar\n").unwrap();
1643        let sr = SharedRandStatus::from_item(&sr);
1644        assert!(sr.is_err());
1645    }
1646
1647    #[test]
1648    fn test_protostatus() {
1649        let my_protocols: Protocols = "Link=7 Cons=1-5 Desc=3-10".parse().unwrap();
1650
1651        let outcome = ProtoStatus {
1652            recommended: "Link=7".parse().unwrap(),
1653            required: "Desc=5".parse().unwrap(),
1654        }
1655        .check_protocols(&my_protocols);
1656        assert!(outcome.is_ok());
1657
1658        let outcome = ProtoStatus {
1659            recommended: "Microdesc=4 Link=7".parse().unwrap(),
1660            required: "Desc=5".parse().unwrap(),
1661        }
1662        .check_protocols(&my_protocols);
1663        assert_eq!(
1664            outcome,
1665            Err(ProtocolSupportError::MissingRecommended(
1666                "Microdesc=4".parse().unwrap()
1667            ))
1668        );
1669
1670        let outcome = ProtoStatus {
1671            recommended: "Microdesc=4 Link=7".parse().unwrap(),
1672            required: "Desc=5 Cons=5-12 Wombat=15".parse().unwrap(),
1673        }
1674        .check_protocols(&my_protocols);
1675        assert_eq!(
1676            outcome,
1677            Err(ProtocolSupportError::MissingRequired(
1678                "Cons=6-12 Wombat=15".parse().unwrap()
1679            ))
1680        );
1681    }
1682
1683    #[test]
1684    fn serialize_protostatus() {
1685        let ps = ProtoStatuses {
1686            client: ProtoStatus {
1687                recommended: "Link=1-5 LinkAuth=2-5".parse().unwrap(),
1688                required: "Link=5 LinkAuth=3".parse().unwrap(),
1689            },
1690            relay: ProtoStatus {
1691                recommended: "Wombat=20-30 Knish=20-30".parse().unwrap(),
1692                required: "Wombat=20-22 Knish=25-27".parse().unwrap(),
1693            },
1694        };
1695        let json = serde_json::to_string(&ps).unwrap();
1696        let ps2 = serde_json::from_str(json.as_str()).unwrap();
1697        assert_eq!(ps, ps2);
1698
1699        let ps3: ProtoStatuses = serde_json::from_str(
1700            r#"{
1701            "client":{
1702                "required":"Link=5 LinkAuth=3",
1703                "recommended":"Link=1-5 LinkAuth=2-5"
1704            },
1705            "relay":{
1706                "required":"Wombat=20-22 Knish=25-27",
1707                "recommended":"Wombat=20-30 Knish=20-30"
1708            }
1709        }"#,
1710        )
1711        .unwrap();
1712        assert_eq!(ps, ps3);
1713    }
1714}