tor_netdoc/doc/netstatus/
each_flavor.rs

1//! consensus documents - items that vary by consensus flavor
2//!
3//! **This file is reincluded multiple times**,
4//! by the macros in [`crate::doc::ns_variety_definition_macros`],
5//! once for votes, and once for each consensus flavour.
6//! It is *not* a module `crate::doc::netstatus::rs::each_flavor`.
7//!
8//! Each time this file is included by one of the macros mentioned above,
9//! the `ns_***` macros (such as `ns_const_name!`) may expand to different values.
10//!
11//! See [`crate::doc::ns_variety_definition_macros`].
12
13use super::*;
14
15ns_use_this_variety! {
16    use [crate::doc::netstatus::rs]::?::{RouterStatus};
17}
18#[cfg(feature = "build_docs")]
19ns_use_this_variety! {
20    pub(crate) use [crate::doc::netstatus::build]::?::{ConsensusBuilder};
21    pub use [crate::doc::netstatus::rs::build]::?::{RouterStatusBuilder};
22}
23
24/// A single consensus netstatus, as produced by the old parser.
25#[derive(Debug, Clone)]
26#[non_exhaustive]
27pub struct Consensus {
28    /// What kind of consensus document is this?  Absent in votes and
29    /// in ns-flavored consensuses.
30    pub flavor: ConsensusFlavor,
31    /// The preamble, except for the intro item.
32    pub preamble: Preamble,
33    /// List of voters whose votes contributed to this consensus.
34    pub voters: Vec<ConsensusVoterInfo>,
35    /// A list of routerstatus entries for the relays on the network,
36    /// with one entry per relay.
37    ///
38    /// These are currently ordered by the router's RSA identity, but this is not
39    /// to be relied on, since we may want to even abolish RSA at some point!
40    pub relays: Vec<RouterStatus>,
41    /// Footer for the consensus object.
42    pub footer: Footer,
43}
44
45impl Consensus {
46    /// Return the Lifetime for this consensus.
47    pub fn lifetime(&self) -> &Lifetime {
48        &self.preamble.lifetime
49    }
50
51    /// Return a slice of all the routerstatus entries in this consensus.
52    pub fn relays(&self) -> &[RouterStatus] {
53        &self.relays[..]
54    }
55
56    /// Return a mapping from keywords to integers representing how
57    /// to weight different kinds of relays in different path positions.
58    pub fn bandwidth_weights(&self) -> &NetParams<i32> {
59        &self.footer.weights
60    }
61
62    /// Return the map of network parameters that this consensus advertises.
63    pub fn params(&self) -> &NetParams<i32> {
64        &self.preamble.params
65    }
66
67    /// Return the latest shared random value, if the consensus
68    /// contains one.
69    pub fn shared_rand_cur(&self) -> Option<&SharedRandStatus> {
70        self.preamble.shared_rand_current_value.as_ref()
71    }
72
73    /// Return the previous shared random value, if the consensus
74    /// contains one.
75    pub fn shared_rand_prev(&self) -> Option<&SharedRandStatus> {
76        self.preamble.shared_rand_previous_value.as_ref()
77    }
78
79    /// Return a [`ProtoStatus`] that lists the network's current requirements and
80    /// recommendations for the list of protocols that every relay must implement.  
81    pub fn relay_protocol_status(&self) -> &ProtoStatus {
82        &self.preamble.proto_statuses.relay
83    }
84
85    /// Return a [`ProtoStatus`] that lists the network's current requirements and
86    /// recommendations for the list of protocols that every client must implement.
87    pub fn client_protocol_status(&self) -> &ProtoStatus {
88        &self.preamble.proto_statuses.client
89    }
90
91    /// Return a set of all known [`ProtoStatus`] values.
92    pub fn protocol_statuses(&self) -> &Arc<ProtoStatuses> {
93        &self.preamble.proto_statuses
94    }
95}
96
97impl Consensus {
98    /// Return a new ConsensusBuilder for building test consensus objects.
99    ///
100    /// This function is only available when the `build_docs` feature has
101    /// been enabled.
102    #[cfg(feature = "build_docs")]
103    pub fn builder() -> ConsensusBuilder {
104        ConsensusBuilder::new(RouterStatus::flavor())
105    }
106
107    /// Try to parse a single networkstatus document from a string.
108    pub fn parse(s: &str) -> Result<(&str, &str, UncheckedConsensus)> {
109        let mut reader = NetDocReader::new(s)?;
110        Self::parse_from_reader(&mut reader).map_err(|e| e.within(s))
111    }
112    /// Extract a voter-info section from the reader; return
113    /// Ok(None) when we are out of voter-info sections.
114    fn take_voterinfo(
115        r: &mut NetDocReader<'_, NetstatusKwd>,
116    ) -> Result<Option<ConsensusVoterInfo>> {
117        use NetstatusKwd::*;
118
119        match r.peek() {
120            None => return Ok(None),
121            Some(e) if e.is_ok_with_kwd_in(&[RS_R, DIRECTORY_FOOTER]) => return Ok(None),
122            _ => (),
123        };
124
125        let mut first_dir_source = true;
126        // TODO: Extract this pattern into a "pause at second"???
127        // Pause at the first 'r', or the second 'dir-source'.
128        let mut p = r.pause_at(|i| match i {
129            Err(_) => false,
130            Ok(item) => {
131                item.kwd() == RS_R
132                    || if item.kwd() == DIR_SOURCE {
133                        let was_first = first_dir_source;
134                        first_dir_source = false;
135                        !was_first
136                    } else {
137                        false
138                    }
139            }
140        });
141
142        let voter_sec = NS_VOTERINFO_RULES_CONSENSUS.parse(&mut p)?;
143        let voter = ConsensusVoterInfo::from_section(&voter_sec)?;
144
145        Ok(Some(voter))
146    }
147
148    /// Extract the footer (but not signatures) from the reader.
149    fn take_footer(r: &mut NetDocReader<'_, NetstatusKwd>) -> Result<Footer> {
150        use NetstatusKwd::*;
151        let mut p = r.pause_at(|i| i.is_ok_with_kwd_in(&[DIRECTORY_SIGNATURE]));
152        let footer_sec = NS_FOOTER_RULES.parse(&mut p)?;
153        let footer = Footer::from_section(&footer_sec)?;
154        Ok(footer)
155    }
156
157    /// Extract a routerstatus from the reader.  Return Ok(None) if we're
158    /// out of routerstatus entries.
159    fn take_routerstatus(r: &mut NetDocReader<'_, NetstatusKwd>) -> Result<Option<(Pos, RouterStatus)>> {
160        use NetstatusKwd::*;
161        match r.peek() {
162            None => return Ok(None),
163            Some(e) if e.is_ok_with_kwd_in(&[DIRECTORY_FOOTER]) => return Ok(None),
164            _ => (),
165        };
166
167        let pos = r.pos();
168
169        let mut first_r = true;
170        let mut p = r.pause_at(|i| match i {
171            Err(_) => false,
172            Ok(item) => {
173                item.kwd() == DIRECTORY_FOOTER
174                    || if item.kwd() == RS_R {
175                        let was_first = first_r;
176                        first_r = false;
177                        !was_first
178                    } else {
179                        false
180                    }
181            }
182        });
183
184        let rules = match RouterStatus::flavor() {
185            ConsensusFlavor::Microdesc => &NS_ROUTERSTATUS_RULES_MDCON,
186            ConsensusFlavor::Plain => &NS_ROUTERSTATUS_RULES_PLAIN,
187        };
188
189        let rs_sec = rules.parse(&mut p)?;
190        let rs = RouterStatus::from_section(&rs_sec)?;
191        Ok(Some((pos, rs)))
192    }
193
194    /// Extract an entire UncheckedConsensus from a reader.
195    ///
196    /// Returns the signed portion of the string, the remainder of the
197    /// string, and an UncheckedConsensus.
198    fn parse_from_reader<'a>(
199        r: &mut NetDocReader<'a, NetstatusKwd>,
200    ) -> Result<(&'a str, &'a str, UncheckedConsensus)> {
201        use NetstatusKwd::*;
202        let ((flavor, preamble), start_pos) = {
203            let mut h = r.pause_at(|i| i.is_ok_with_kwd_in(&[DIR_SOURCE]));
204            let preamble_sec = NS_HEADER_RULES_CONSENSUS.parse(&mut h)?;
205            // Unwrapping should be safe because above `.parse` would have
206            // returned an Error
207            #[allow(clippy::unwrap_used)]
208            let pos = preamble_sec.first_item().unwrap().offset_in(r.str()).unwrap();
209            (Preamble::from_section(&preamble_sec)?, pos)
210        };
211        if RouterStatus::flavor() != flavor {
212            return Err(EK::BadDocumentType.with_msg(format!(
213                "Expected {:?}, got {:?}",
214                RouterStatus::flavor(),
215                flavor
216            )));
217        }
218
219        let mut voters = Vec::new();
220
221        while let Some(voter) = Self::take_voterinfo(r)? {
222            voters.push(voter);
223        }
224
225        let mut relays: Vec<RouterStatus> = Vec::new();
226        while let Some((pos, routerstatus)) = Self::take_routerstatus(r)? {
227            if let Some(prev) = relays.last() {
228                if prev.rsa_identity() >= routerstatus.rsa_identity() {
229                    return Err(EK::WrongSortOrder.at_pos(pos));
230                }
231            }
232            relays.push(routerstatus);
233        }
234        relays.shrink_to_fit();
235
236        let footer = Self::take_footer(r)?;
237
238        let consensus = Consensus {
239            flavor,
240            preamble,
241            voters,
242            relays,
243            footer,
244        };
245
246        // Find the signatures.
247        let mut first_sig: Option<Item<'_, NetstatusKwd>> = None;
248        let mut signatures = Vec::new();
249        for item in &mut *r {
250            let item = item?;
251            if item.kwd() != DIRECTORY_SIGNATURE {
252                return Err(EK::UnexpectedToken
253                    .with_msg(item.kwd().to_str())
254                    .at_pos(item.pos()));
255            }
256
257            let sig = Signature::from_item(&item)?;
258            if first_sig.is_none() {
259                first_sig = Some(item);
260            }
261            signatures.push(sig);
262        }
263
264        let end_pos = match first_sig {
265            None => return Err(EK::MissingToken.with_msg("directory-signature")),
266            // Unwrap should be safe because `first_sig` was parsed from `r`
267            #[allow(clippy::unwrap_used)]
268            Some(sig) => sig.offset_in(r.str()).unwrap() + "directory-signature ".len(),
269        };
270
271        // Find the appropriate digest.
272        let signed_str = &r.str()[start_pos..end_pos];
273        let remainder = &r.str()[end_pos..];
274        let (sha256, sha1) = match RouterStatus::flavor() {
275            ConsensusFlavor::Plain => (
276                None,
277                Some(ll::d::Sha1::digest(signed_str.as_bytes()).into()),
278            ),
279            ConsensusFlavor::Microdesc => (
280                Some(ll::d::Sha256::digest(signed_str.as_bytes()).into()),
281                None,
282            ),
283        };
284        let siggroup = SignatureGroup {
285            sha256,
286            sha1,
287            signatures,
288        };
289
290        let unval = UnvalidatedConsensus {
291            consensus,
292            siggroup,
293            n_authorities: None,
294        };
295        let lifetime = unval.consensus.preamble.lifetime.clone();
296        let delay = unval.consensus.preamble.voting_delay.unwrap_or((0, 0));
297        let dist_interval = time::Duration::from_secs(delay.1.into());
298        let starting_time = *lifetime.valid_after - dist_interval;
299        let timebound = TimerangeBound::new(unval, starting_time..*lifetime.valid_until);
300        Ok((signed_str, remainder, timebound))
301    }
302}
303
304impl Preamble {
305    /// Extract the CommonPreamble members from a single preamble section.
306    fn from_section(sec: &Section<'_, NetstatusKwd>) -> Result<(ConsensusFlavor, Preamble)> {
307        use NetstatusKwd::*;
308
309        {
310            // this unwrap is safe because if there is not at least one
311            // token in the section, the section is unparsable.
312            #[allow(clippy::unwrap_used)]
313            let first = sec.first_item().unwrap();
314            if first.kwd() != NETWORK_STATUS_VERSION {
315                return Err(EK::UnexpectedToken
316                    .with_msg(first.kwd().to_str())
317                    .at_pos(first.pos()));
318            }
319        }
320
321        let ver_item = sec.required(NETWORK_STATUS_VERSION)?;
322
323        let version: u32 = ver_item.parse_arg(0)?;
324        if version != 3 {
325            return Err(EK::BadDocumentVersion.with_msg(version.to_string()));
326        }
327        let flavor = ConsensusFlavor::from_opt_name(ver_item.arg(1))?;
328
329        let valid_after = sec
330            .required(VALID_AFTER)?
331            .args_as_str()
332            .parse::<Iso8601TimeSp>()?
333            .into();
334        let fresh_until = sec
335            .required(FRESH_UNTIL)?
336            .args_as_str()
337            .parse::<Iso8601TimeSp>()?
338            .into();
339        let valid_until = sec
340            .required(VALID_UNTIL)?
341            .args_as_str()
342            .parse::<Iso8601TimeSp>()?
343            .into();
344        let lifetime = Lifetime::new(valid_after, fresh_until, valid_until)?;
345
346        let client_versions = sec
347            .maybe(CLIENT_VERSIONS)
348            .args_as_str()
349            .unwrap_or("")
350            .split(',')
351            .map(str::to_string)
352            .collect();
353        let server_versions = sec
354            .maybe(SERVER_VERSIONS)
355            .args_as_str()
356            .unwrap_or("")
357            .split(',')
358            .map(str::to_string)
359            .collect();
360
361        let proto_statuses = {
362            let client = ProtoStatus::from_section(
363                sec,
364                RECOMMENDED_CLIENT_PROTOCOLS,
365                REQUIRED_CLIENT_PROTOCOLS,
366            )?;
367            let relay = ProtoStatus::from_section(
368                sec,
369                RECOMMENDED_RELAY_PROTOCOLS,
370                REQUIRED_RELAY_PROTOCOLS,
371            )?;
372            Arc::new(ProtoStatuses { client, relay })
373        };
374
375        let params = sec.maybe(PARAMS).args_as_str().unwrap_or("").parse()?;
376
377        let status: &str = sec.required(VOTE_STATUS)?.arg(0).unwrap_or("");
378        if status != "consensus" {
379            return Err(EK::BadDocumentType.err());
380        }
381
382        // We're ignoring KNOWN_FLAGS in the consensus.
383
384        let consensus_method: u32 = sec.required(CONSENSUS_METHOD)?.parse_arg(0)?;
385
386        let shared_rand_previous_value = sec
387            .get(SHARED_RAND_PREVIOUS_VALUE)
388            .map(SharedRandStatus::from_item)
389            .transpose()?;
390
391        let shared_rand_current_value = sec
392            .get(SHARED_RAND_CURRENT_VALUE)
393            .map(SharedRandStatus::from_item)
394            .transpose()?;
395
396        let voting_delay = if let Some(tok) = sec.get(VOTING_DELAY) {
397            let n1 = tok.parse_arg(0)?;
398            let n2 = tok.parse_arg(1)?;
399            Some((n1, n2))
400        } else {
401            None
402        };
403
404        let preamble = Preamble {
405            lifetime,
406            client_versions,
407            server_versions,
408            proto_statuses,
409            params,
410            voting_delay,
411            consensus_method,
412            shared_rand_previous_value,
413            shared_rand_current_value,
414        };
415
416        Ok((flavor, preamble))
417    }
418}
419
420/// A Microdesc consensus whose signatures have not yet been checked.
421///
422/// To validate this object, call set_n_authorities() on it, then call
423/// check_signature() on that result with the set of certs that you
424/// have.  Make sure only to provide authority certificates representing
425/// real authorities!
426#[derive(Debug, Clone)]
427#[non_exhaustive]
428pub struct UnvalidatedConsensus {
429    /// The consensus object. We don't want to expose this until it's
430    /// validated.
431    pub consensus: Consensus,
432    /// The signatures that need to be validated before we can call
433    /// this consensus valid.
434    pub siggroup: SignatureGroup,
435    /// The total number of authorities that we believe in.  We need
436    /// this information in order to validate the signatures, since it
437    /// determines how many signatures we need to find valid in `siggroup`.
438    pub n_authorities: Option<u16>,
439}
440
441impl UnvalidatedConsensus {
442    /// Tell the unvalidated consensus how many authorities we believe in.
443    ///
444    /// Without knowing this number, we can't validate the signature.
445    #[must_use]
446    pub fn set_n_authorities(self, n_authorities: u16) -> Self {
447        UnvalidatedConsensus {
448            n_authorities: Some(n_authorities),
449            ..self
450        }
451    }
452
453    /// Return an iterator of all the certificate IDs that we might use
454    /// to validate this consensus.
455    pub fn signing_cert_ids(&self) -> impl Iterator<Item = AuthCertKeyIds> {
456        match self.key_is_correct(&[]) {
457            Ok(()) => Vec::new(),
458            Err(missing) => missing,
459        }
460        .into_iter()
461    }
462
463    /// Return the lifetime of this unvalidated consensus
464    pub fn peek_lifetime(&self) -> &Lifetime {
465        self.consensus.lifetime()
466    }
467
468    /// Return true if a client who believes in exactly the provided
469    /// set of authority IDs might might consider this consensus to be
470    /// well-signed.
471    ///
472    /// (This is the case if the consensus claims to be signed by more than
473    /// half of the authorities in the list.)
474    pub fn authorities_are_correct(&self, authorities: &[&RsaIdentity]) -> bool {
475        self.siggroup.could_validate(authorities)
476    }
477
478    /// Return the number of relays in this unvalidated consensus.
479    ///
480    /// This function is unstable. It is only enabled if the crate was
481    /// built with the `experimental-api` feature.
482    #[cfg(feature = "experimental-api")]
483    pub fn n_relays(&self) -> usize {
484        self.consensus.relays.len()
485    }
486
487    /// Modify the list of relays in this unvalidated consensus.
488    ///
489    /// A use case for this is long-lasting custom directories. To ensure Arti can still quickly
490    /// build circuits when the directory gets old, a tiny churn file can be regularly obtained,
491    /// listing no longer available Tor nodes, which can then be removed from the consensus.
492    ///
493    /// This function is unstable. It is only enabled if the crate was
494    /// built with the `experimental-api` feature.
495    #[cfg(feature = "experimental-api")]
496    pub fn modify_relays<F>(&mut self, func: F)
497    where
498        F: FnOnce(&mut Vec<RouterStatus>),
499    {
500        func(&mut self.consensus.relays);
501    }
502}
503
504impl ExternallySigned<Consensus> for UnvalidatedConsensus {
505    type Key = [AuthCert];
506    type KeyHint = Vec<AuthCertKeyIds>;
507    type Error = Error;
508
509    fn key_is_correct(&self, k: &Self::Key) -> result::Result<(), Self::KeyHint> {
510        let (n_ok, missing) = self.siggroup.list_missing(k);
511        match self.n_authorities {
512            Some(n) if n_ok > (n / 2) as usize => Ok(()),
513            _ => Err(missing.iter().map(|cert| cert.key_ids).collect()),
514        }
515    }
516    fn is_well_signed(&self, k: &Self::Key) -> result::Result<(), Self::Error> {
517        match self.n_authorities {
518            None => Err(Error::from(internal!(
519                "Didn't set authorities on consensus"
520            ))),
521            Some(authority) => {
522                if self.siggroup.validate(authority, k) {
523                    Ok(())
524                } else {
525                    Err(EK::BadSignature.err())
526                }
527            }
528        }
529    }
530    fn dangerously_assume_wellsigned(self) -> Consensus {
531        self.consensus
532    }
533}
534
535/// A Consensus object that has been parsed, but not checked for
536/// signatures and timeliness.
537pub type UncheckedConsensus = TimerangeBound<UnvalidatedConsensus>;
538