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 microdescriptor consensus netstatus
25///
26/// TODO: This should possibly turn into a parameterized type, to represent
27/// votes and ns consensuses.
28#[derive(Debug, Clone)]
29#[non_exhaustive]
30pub struct Consensus {
31    /// Part of the header shared by all consensus types.
32    pub header: Header,
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.header.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.header.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.header.shared_rand_cur.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.header.shared_rand_prev.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.header.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.header.proto_statuses.client
89    }
90
91    /// Return a set of all known [`ProtoStatus`] values.
92    pub fn protocol_statuses(&self) -> &Arc<ProtoStatuses> {
93        &self.header.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 (header, start_pos) = {
203            let mut h = r.pause_at(|i| i.is_ok_with_kwd_in(&[DIR_SOURCE]));
204            let header_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 = header_sec.first_item().unwrap().offset_in(r.str()).unwrap();
209            (Header::from_section(&header_sec)?, pos)
210        };
211        if RouterStatus::flavor() != header.flavor {
212            return Err(EK::BadDocumentType.with_msg(format!(
213                "Expected {:?}, got {:?}",
214                RouterStatus::flavor(),
215                header.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            header,
240            voters,
241            relays,
242            footer,
243        };
244
245        // Find the signatures.
246        let mut first_sig: Option<Item<'_, NetstatusKwd>> = None;
247        let mut signatures = Vec::new();
248        for item in &mut *r {
249            let item = item?;
250            if item.kwd() != DIRECTORY_SIGNATURE {
251                return Err(EK::UnexpectedToken
252                    .with_msg(item.kwd().to_str())
253                    .at_pos(item.pos()));
254            }
255
256            let sig = Signature::from_item(&item)?;
257            if first_sig.is_none() {
258                first_sig = Some(item);
259            }
260            signatures.push(sig);
261        }
262
263        let end_pos = match first_sig {
264            None => return Err(EK::MissingToken.with_msg("directory-signature")),
265            // Unwrap should be safe because `first_sig` was parsed from `r`
266            #[allow(clippy::unwrap_used)]
267            Some(sig) => sig.offset_in(r.str()).unwrap() + "directory-signature ".len(),
268        };
269
270        // Find the appropriate digest.
271        let signed_str = &r.str()[start_pos..end_pos];
272        let remainder = &r.str()[end_pos..];
273        let (sha256, sha1) = match RouterStatus::flavor() {
274            ConsensusFlavor::Plain => (
275                None,
276                Some(ll::d::Sha1::digest(signed_str.as_bytes()).into()),
277            ),
278            ConsensusFlavor::Microdesc => (
279                Some(ll::d::Sha256::digest(signed_str.as_bytes()).into()),
280                None,
281            ),
282        };
283        let siggroup = SignatureGroup {
284            sha256,
285            sha1,
286            signatures,
287        };
288
289        let unval = UnvalidatedConsensus {
290            consensus,
291            siggroup,
292            n_authorities: None,
293        };
294        let lifetime = unval.consensus.header.lifetime.clone();
295        let delay = unval.consensus.header.voting_delay.unwrap_or((0, 0));
296        let dist_interval = time::Duration::from_secs(delay.1.into());
297        let starting_time = lifetime.valid_after - dist_interval;
298        let timebound = TimerangeBound::new(unval, starting_time..lifetime.valid_until);
299        Ok((signed_str, remainder, timebound))
300    }
301}
302
303impl Header {
304    /// Extract the CommonHeader members from a single header section.
305    fn from_section(sec: &Section<'_, NetstatusKwd>) -> Result<Header> {
306        use NetstatusKwd::*;
307
308        {
309            // this unwrap is safe because if there is not at least one
310            // token in the section, the section is unparsable.
311            #[allow(clippy::unwrap_used)]
312            let first = sec.first_item().unwrap();
313            if first.kwd() != NETWORK_STATUS_VERSION {
314                return Err(EK::UnexpectedToken
315                    .with_msg(first.kwd().to_str())
316                    .at_pos(first.pos()));
317            }
318        }
319
320        let ver_item = sec.required(NETWORK_STATUS_VERSION)?;
321
322        let version: u32 = ver_item.parse_arg(0)?;
323        if version != 3 {
324            return Err(EK::BadDocumentVersion.with_msg(version.to_string()));
325        }
326        let flavor = ConsensusFlavor::from_opt_name(ver_item.arg(1))?;
327
328        let valid_after = sec
329            .required(VALID_AFTER)?
330            .args_as_str()
331            .parse::<Iso8601TimeSp>()?
332            .into();
333        let fresh_until = sec
334            .required(FRESH_UNTIL)?
335            .args_as_str()
336            .parse::<Iso8601TimeSp>()?
337            .into();
338        let valid_until = sec
339            .required(VALID_UNTIL)?
340            .args_as_str()
341            .parse::<Iso8601TimeSp>()?
342            .into();
343        let lifetime = Lifetime::new(valid_after, fresh_until, valid_until)?;
344
345        let client_versions = sec
346            .maybe(CLIENT_VERSIONS)
347            .args_as_str()
348            .unwrap_or("")
349            .split(',')
350            .map(str::to_string)
351            .collect();
352        let relay_versions = sec
353            .maybe(SERVER_VERSIONS)
354            .args_as_str()
355            .unwrap_or("")
356            .split(',')
357            .map(str::to_string)
358            .collect();
359
360        let proto_statuses = {
361            let client = ProtoStatus::from_section(
362                sec,
363                RECOMMENDED_CLIENT_PROTOCOLS,
364                REQUIRED_CLIENT_PROTOCOLS,
365            )?;
366            let relay = ProtoStatus::from_section(
367                sec,
368                RECOMMENDED_RELAY_PROTOCOLS,
369                REQUIRED_RELAY_PROTOCOLS,
370            )?;
371            Arc::new(ProtoStatuses { client, relay })
372        };
373
374        let params = sec.maybe(PARAMS).args_as_str().unwrap_or("").parse()?;
375
376        let status: &str = sec.required(VOTE_STATUS)?.arg(0).unwrap_or("");
377        if status != "consensus" {
378            return Err(EK::BadDocumentType.err());
379        }
380
381        // We're ignoring KNOWN_FLAGS in the consensus.
382
383        let consensus_method: u32 = sec.required(CONSENSUS_METHOD)?.parse_arg(0)?;
384
385        let shared_rand_prev = sec
386            .get(SHARED_RAND_PREVIOUS_VALUE)
387            .map(SharedRandStatus::from_item)
388            .transpose()?;
389
390        let shared_rand_cur = sec
391            .get(SHARED_RAND_CURRENT_VALUE)
392            .map(SharedRandStatus::from_item)
393            .transpose()?;
394
395        let voting_delay = if let Some(tok) = sec.get(VOTING_DELAY) {
396            let n1 = tok.parse_arg(0)?;
397            let n2 = tok.parse_arg(1)?;
398            Some((n1, n2))
399        } else {
400            None
401        };
402
403        Ok(Header {
404            flavor,
405            lifetime,
406            client_versions,
407            relay_versions,
408            proto_statuses,
409            params,
410            voting_delay,
411            consensus_method,
412            shared_rand_prev,
413            shared_rand_cur,
414        })
415    }
416}
417
418/// A Microdesc consensus whose signatures have not yet been checked.
419///
420/// To validate this object, call set_n_authorities() on it, then call
421/// check_signature() on that result with the set of certs that you
422/// have.  Make sure only to provide authority certificates representing
423/// real authorities!
424#[derive(Debug, Clone)]
425#[non_exhaustive]
426pub struct UnvalidatedConsensus {
427    /// The consensus object. We don't want to expose this until it's
428    /// validated.
429    pub consensus: Consensus,
430    /// The signatures that need to be validated before we can call
431    /// this consensus valid.
432    pub siggroup: SignatureGroup,
433    /// The total number of authorities that we believe in.  We need
434    /// this information in order to validate the signatures, since it
435    /// determines how many signatures we need to find valid in `siggroup`.
436    pub n_authorities: Option<u16>,
437}
438
439impl UnvalidatedConsensus {
440    /// Tell the unvalidated consensus how many authorities we believe in.
441    ///
442    /// Without knowing this number, we can't validate the signature.
443    #[must_use]
444    pub fn set_n_authorities(self, n_authorities: u16) -> Self {
445        UnvalidatedConsensus {
446            n_authorities: Some(n_authorities),
447            ..self
448        }
449    }
450
451    /// Return an iterator of all the certificate IDs that we might use
452    /// to validate this consensus.
453    pub fn signing_cert_ids(&self) -> impl Iterator<Item = AuthCertKeyIds> {
454        match self.key_is_correct(&[]) {
455            Ok(()) => Vec::new(),
456            Err(missing) => missing,
457        }
458        .into_iter()
459    }
460
461    /// Return the lifetime of this unvalidated consensus
462    pub fn peek_lifetime(&self) -> &Lifetime {
463        self.consensus.lifetime()
464    }
465
466    /// Return true if a client who believes in exactly the provided
467    /// set of authority IDs might might consider this consensus to be
468    /// well-signed.
469    ///
470    /// (This is the case if the consensus claims to be signed by more than
471    /// half of the authorities in the list.)
472    pub fn authorities_are_correct(&self, authorities: &[&RsaIdentity]) -> bool {
473        self.siggroup.could_validate(authorities)
474    }
475
476    /// Return the number of relays in this unvalidated consensus.
477    ///
478    /// This function is unstable. It is only enabled if the crate was
479    /// built with the `experimental-api` feature.
480    #[cfg(feature = "experimental-api")]
481    pub fn n_relays(&self) -> usize {
482        self.consensus.relays.len()
483    }
484
485    /// Modify the list of relays in this unvalidated consensus.
486    ///
487    /// A use case for this is long-lasting custom directories. To ensure Arti can still quickly
488    /// build circuits when the directory gets old, a tiny churn file can be regularly obtained,
489    /// listing no longer available Tor nodes, which can then be removed from the consensus.
490    ///
491    /// This function is unstable. It is only enabled if the crate was
492    /// built with the `experimental-api` feature.
493    #[cfg(feature = "experimental-api")]
494    pub fn modify_relays<F>(&mut self, func: F)
495    where
496        F: FnOnce(&mut Vec<RouterStatus>),
497    {
498        func(&mut self.consensus.relays);
499    }
500}
501
502impl ExternallySigned<Consensus> for UnvalidatedConsensus {
503    type Key = [AuthCert];
504    type KeyHint = Vec<AuthCertKeyIds>;
505    type Error = Error;
506
507    fn key_is_correct(&self, k: &Self::Key) -> result::Result<(), Self::KeyHint> {
508        let (n_ok, missing) = self.siggroup.list_missing(k);
509        match self.n_authorities {
510            Some(n) if n_ok > (n / 2) as usize => Ok(()),
511            _ => Err(missing.iter().map(|cert| cert.key_ids).collect()),
512        }
513    }
514    fn is_well_signed(&self, k: &Self::Key) -> result::Result<(), Self::Error> {
515        match self.n_authorities {
516            None => Err(Error::from(internal!(
517                "Didn't set authorities on consensus"
518            ))),
519            Some(authority) => {
520                if self.siggroup.validate(authority, k) {
521                    Ok(())
522                } else {
523                    Err(EK::BadSignature.err())
524                }
525            }
526        }
527    }
528    fn dangerously_assume_wellsigned(self) -> Consensus {
529        self.consensus
530    }
531}
532
533/// A Consensus object that has been parsed, but not checked for
534/// signatures and timeliness.
535pub type UncheckedConsensus = TimerangeBound<UnvalidatedConsensus>;
536