//! consensus documents - items that vary by consensus flavor //! //! **This file is reincluded multiple times**, //! by the macros in [`crate::doc::ns_variety_definition_macros`], //! once for votes, and once for each consensus flavour. //! It is *not* a module `crate::doc::netstatus::rs::each_flavor`. //! //! Each time this file is included by one of the macros mentioned above, //! the `ns_***` macros (such as `ns_const_name!`) may expand to different values. //! //! See [`crate::doc::ns_variety_definition_macros`]. use super::*; ns_use_this_variety! { use [crate::doc::netstatus::rs]::?::{RouterStatus}; } #[cfg(feature = "build_docs")] ns_use_this_variety! { pub(crate) use [crate::doc::netstatus::build]::?::{ConsensusBuilder}; pub use [crate::doc::netstatus::rs::build]::?::{RouterStatusBuilder}; } /// A single consensus netstatus, as produced by the old parser. #[derive(Debug, Clone)] #[non_exhaustive] pub struct Consensus { /// What kind of consensus document is this? Absent in votes and /// in ns-flavored consensuses. pub flavor: ConsensusFlavor, /// The preamble, except for the intro item. pub preamble: Preamble, /// List of voters whose votes contributed to this consensus. pub voters: Vec, /// A list of routerstatus entries for the relays on the network, /// with one entry per relay. /// /// These are currently ordered by the router's RSA identity, but this is not /// to be relied on, since we may want to even abolish RSA at some point! pub relays: Vec, /// Footer for the consensus object. pub footer: ConsensusFooterFields, } impl Consensus { /// Return the Lifetime for this consensus. pub fn lifetime(&self) -> &Lifetime { &self.preamble.lifetime } /// Return a slice of all the routerstatus entries in this consensus. pub fn relays(&self) -> &[RouterStatus] { &self.relays[..] } /// Return a mapping from keywords to integers representing how /// to weight different kinds of relays in different path positions. pub fn bandwidth_weights(&self) -> &NetParams { &self.footer.bandwidth_weights } /// Return the map of network parameters that this consensus advertises. pub fn params(&self) -> &NetParams { &self.preamble.params } /// Return the latest shared random value, if the consensus /// contains one. pub fn shared_rand_cur(&self) -> Option<&SharedRandStatus> { self.preamble.shared_rand.shared_rand_current_value.as_ref() } /// Return the previous shared random value, if the consensus /// contains one. pub fn shared_rand_prev(&self) -> Option<&SharedRandStatus> { self.preamble.shared_rand.shared_rand_previous_value.as_ref() } /// Return a [`ProtoStatus`] that lists the network's current requirements and /// recommendations for the list of protocols that every relay must implement. pub fn relay_protocol_status(&self) -> &ProtoStatus { &self.preamble.proto_statuses.relay } /// Return a [`ProtoStatus`] that lists the network's current requirements and /// recommendations for the list of protocols that every client must implement. pub fn client_protocol_status(&self) -> &ProtoStatus { &self.preamble.proto_statuses.client } /// Return a set of all known [`ProtoStatus`] values. pub fn protocol_statuses(&self) -> &Arc { &self.preamble.proto_statuses } } impl Consensus { /// Return a new ConsensusBuilder for building test consensus objects. /// /// This function is only available when the `build_docs` feature has /// been enabled. #[cfg(feature = "build_docs")] pub fn builder() -> ConsensusBuilder { ConsensusBuilder::new(RouterStatus::flavor()) } /// Try to parse a single networkstatus document from a string. pub fn parse(s: &str) -> crate::Result<(&str, &str, UncheckedConsensus)> { let mut reader = NetDocReader::new(s)?; Self::parse_from_reader(&mut reader).map_err(|e| e.within(s)) } /// Extract a voter-info section from the reader; return /// Ok(None) when we are out of voter-info sections. fn take_voterinfo( r: &mut NetDocReader<'_, NetstatusKwd>, ) -> crate::Result> { use NetstatusKwd::*; match r.peek() { None => return Ok(None), Some(e) if e.is_ok_with_kwd_in(&[RS_R, DIRECTORY_FOOTER]) => return Ok(None), _ => (), }; let mut first_dir_source = true; // TODO: Extract this pattern into a "pause at second"??? // Pause at the first 'r', or the second 'dir-source'. let mut p = r.pause_at(|i| match i { Err(_) => false, Ok(item) => { item.kwd() == RS_R || if item.kwd() == DIR_SOURCE { let was_first = first_dir_source; first_dir_source = false; !was_first } else { false } } }); let voter_sec = NS_VOTERINFO_RULES_CONSENSUS.parse(&mut p)?; let voter = ConsensusAuthorityEntry::from_section(&voter_sec)?; Ok(Some(voter)) } /// Extract the footer (but not signatures) from the reader. fn take_footer(r: &mut NetDocReader<'_, NetstatusKwd>) -> crate::Result { use NetstatusKwd::*; let mut p = r.pause_at(|i| i.is_ok_with_kwd_in(&[DIRECTORY_SIGNATURE])); let footer_sec = NS_FOOTER_RULES.parse(&mut p)?; let footer = ConsensusFooterFields::from_section(&footer_sec)?; Ok(footer) } /// Extract a routerstatus from the reader. Return Ok(None) if we're /// out of routerstatus entries. fn take_routerstatus(r: &mut NetDocReader<'_, NetstatusKwd>) -> crate::Result> { use NetstatusKwd::*; match r.peek() { None => return Ok(None), Some(e) if e.is_ok_with_kwd_in(&[DIRECTORY_FOOTER]) => return Ok(None), _ => (), }; let pos = r.pos(); let mut first_r = true; let mut p = r.pause_at(|i| match i { Err(_) => false, Ok(item) => { item.kwd() == DIRECTORY_FOOTER || if item.kwd() == RS_R { let was_first = first_r; first_r = false; !was_first } else { false } } }); let rules = match RouterStatus::flavor() { ConsensusFlavor::Microdesc => &NS_ROUTERSTATUS_RULES_MDCON, ConsensusFlavor::Plain => &NS_ROUTERSTATUS_RULES_PLAIN, }; let rs_sec = rules.parse(&mut p)?; let rs = RouterStatus::from_section(&rs_sec)?; Ok(Some((pos, rs))) } /// Extract an entire UncheckedConsensus from a reader. /// /// Returns the signed portion of the string, the remainder of the /// string, and an UncheckedConsensus. fn parse_from_reader<'a>( r: &mut NetDocReader<'a, NetstatusKwd>, ) -> crate::Result<(&'a str, &'a str, UncheckedConsensus)> { use NetstatusKwd::*; let ((flavor, preamble), start_pos) = { let mut h = r.pause_at(|i| i.is_ok_with_kwd_in(&[DIR_SOURCE])); let preamble_sec = NS_HEADER_RULES_CONSENSUS.parse(&mut h)?; // Unwrapping should be safe because above `.parse` would have // returned an Error #[allow(clippy::unwrap_used)] let pos = preamble_sec.first_item().unwrap().offset_in(r.str()).unwrap(); (Preamble::from_section(&preamble_sec)?, pos) }; if RouterStatus::flavor() != flavor { return Err(EK::BadDocumentType.with_msg(format!( "Expected {:?}, got {:?}", RouterStatus::flavor(), flavor ))); } let mut voters = Vec::new(); while let Some(voter) = Self::take_voterinfo(r)? { voters.push(voter); } let mut relays: Vec = Vec::new(); while let Some((pos, routerstatus)) = Self::take_routerstatus(r)? { if let Some(prev) = relays.last() { if prev.rsa_identity() >= routerstatus.rsa_identity() { return Err(EK::WrongSortOrder.at_pos(pos)); } } relays.push(routerstatus); } relays.shrink_to_fit(); let footer = Self::take_footer(r)?; let consensus = Consensus { flavor, preamble, voters, relays, footer, }; // Find the signatures. let mut first_sig: Option> = None; let mut signatures = Vec::new(); for item in &mut *r { let item = item?; if item.kwd() != DIRECTORY_SIGNATURE { return Err(EK::UnexpectedToken .with_msg(item.kwd().to_str()) .at_pos(item.pos())); } let sig = Signature::from_item(&item)?; if first_sig.is_none() { first_sig = Some(item); } signatures.push(sig); } let end_pos = match first_sig { None => return Err(EK::MissingToken.with_msg("directory-signature")), // Unwrap should be safe because `first_sig` was parsed from `r` #[allow(clippy::unwrap_used)] Some(sig) => sig.offset_in(r.str()).unwrap() + "directory-signature ".len(), }; // Find the appropriate digest. let signed_str = r.str().get(start_pos..end_pos).ok_or(internal!("chopped utf8"))?; let remainder = r.str().get(end_pos..).ok_or(internal!("chopped utf8"))?; let (sha256, sha1) = match RouterStatus::flavor() { ConsensusFlavor::Plain => ( None, Some(ll::d::Sha1::digest(signed_str.as_bytes()).into()), ), ConsensusFlavor::Microdesc => ( Some(ll::d::Sha256::digest(signed_str.as_bytes()).into()), None, ), }; let hashes = DirectorySignaturesHashesAccu { sha256, sha1, // TODO #2530 This is wrong. There isn't one hash, there's two. sha1_unnamed: sha1, }; let siggroup = SignatureGroup { hashes, signatures, }; let unval = UnvalidatedConsensus { consensus, siggroup, n_authorities: None, }; let timebound_range = unval.consensus.preamble.validity_time_range(); let timebound = TimeRangeBound::new(unval, timebound_range); Ok((signed_str, remainder, timebound)) } } impl Preamble { /// Extract the CommonPreamble members from a single preamble section. fn from_section(sec: &Section<'_, NetstatusKwd>) -> crate::Result<(ConsensusFlavor, Preamble)> { use NetstatusKwd::*; { // this unwrap is safe because if there is not at least one // token in the section, the section is unparsable. #[allow(clippy::unwrap_used)] let first = sec.first_item().unwrap(); if first.kwd() != NETWORK_STATUS_VERSION { return Err(EK::UnexpectedToken .with_msg(first.kwd().to_str()) .at_pos(first.pos())); } } let ver_item = sec.required(NETWORK_STATUS_VERSION)?; let version: u32 = ver_item.parse_arg(0)?; if version != 3 { return Err(EK::BadDocumentVersion.with_msg(version.to_string())); } let flavor = ConsensusFlavor::from_opt_name(ver_item.arg(1))?; let valid_after = sec .required(VALID_AFTER)? .args_as_str() .parse::()? .into(); let fresh_until = sec .required(FRESH_UNTIL)? .args_as_str() .parse::()? .into(); let valid_until = sec .required(VALID_UNTIL)? .args_as_str() .parse::()? .into(); let lifetime = Lifetime::new(valid_after, fresh_until, valid_until)?; let parse_rec_versions = |item| { let item = sec .maybe(item); let args = item .args_as_str() .unwrap_or("") // C Tor emits an item with trailing whitespace which we must ignore .trim(); // We want only the first arg, according to the spec. // We could want to use MaybeItem::parse_arg, but it treats absence of the // argument as an error. There is no parse_optional_arg on `MaybeItem`. // We could add that, but I am trying to avoid adding code to the old parser. // So instead we reimplement argument splitting (again). args .split_once(|c: char| c.is_ascii_whitespace()).map(|(l, _r)| l).unwrap_or(args) .parse() .map_err(|_e| EK::BadArgument.at_pos(item.pos())) }; let client_versions = parse_rec_versions(CLIENT_VERSIONS)?; let server_versions = parse_rec_versions(SERVER_VERSIONS)?; let proto_statuses = { let client = ProtoStatus::from_section( sec, RECOMMENDED_CLIENT_PROTOCOLS, REQUIRED_CLIENT_PROTOCOLS, )?; let relay = ProtoStatus::from_section( sec, RECOMMENDED_RELAY_PROTOCOLS, REQUIRED_RELAY_PROTOCOLS, )?; Arc::new(ProtoStatuses { client, relay }) }; let params = sec.maybe(PARAMS).args_as_str().unwrap_or("").parse()?; let status: &str = sec.required(VOTE_STATUS)?.arg(0).unwrap_or(""); if status != "consensus" { return Err(EK::BadDocumentType.err()); } // We're ignoring KNOWN_FLAGS in the consensus. let consensus_method: u32 = sec.required(CONSENSUS_METHOD)?.parse_arg(0)?; let shared_rand_previous_value = sec .get(SHARED_RAND_PREVIOUS_VALUE) .map(SharedRandStatus::from_item) .transpose()?; let shared_rand_current_value = sec .get(SHARED_RAND_CURRENT_VALUE) .map(SharedRandStatus::from_item) .transpose()?; let voting_delay = if let Some(tok) = sec.get(VOTING_DELAY) { let n1 = tok.parse_arg(0)?; let n2 = tok.parse_arg(1)?; Some((n1, n2)) } else { None }; let shared_rand = SharedRandStatuses { shared_rand_previous_value, shared_rand_current_value, __non_exhaustive: (), }; let preamble = Preamble { lifetime, client_versions, server_versions, proto_statuses, params, voting_delay, consensus_method: (consensus_method,), published: NotPresent, consensus_methods: NotPresent, known_flags: DocRelayFlags::new_empty_unknown_discarded(), shared_rand, __non_exhaustive: (), }; Ok((flavor, preamble)) } } /// A Microdesc consensus whose signatures have not yet been checked. /// /// To validate this object, call set_n_authorities() on it, then call /// check_signature() on that result with the set of certs that you /// have. Make sure only to provide authority certificates representing /// real authorities! #[derive(Debug, Clone)] #[non_exhaustive] pub struct UnvalidatedConsensus { /// The consensus object. We don't want to expose this until it's /// validated. pub consensus: Consensus, /// The signatures that need to be validated before we can call /// this consensus valid. pub siggroup: SignatureGroup, /// The total number of authorities that we believe in. We need /// this information in order to validate the signatures, since it /// determines how many signatures we need to find valid in `siggroup`. pub n_authorities: Option, } impl UnvalidatedConsensus { /// Tell the unvalidated consensus how many authorities we believe in. /// /// Without knowing this number, we can't validate the signature. #[must_use] pub fn set_n_authorities(self, n_authorities: usize) -> Self { UnvalidatedConsensus { n_authorities: Some(n_authorities), ..self } } /// Return an iterator of all the certificate IDs that we might use /// to validate this consensus. pub fn signing_cert_ids(&self) -> impl Iterator { match self.key_is_correct(&[]) { Ok(()) => Vec::new(), Err(missing) => missing, } .into_iter() } /// Return the lifetime of this unvalidated consensus pub fn peek_lifetime(&self) -> &Lifetime { self.consensus.lifetime() } /// Return true if a client who believes in exactly the provided /// set of authority IDs might consider this consensus to be /// well-signed. /// /// (This is the case if the consensus claims to be signed by more than /// half of the authorities in the list.) pub fn authorities_are_correct(&self, authorities: &[&RsaIdentity]) -> bool { self.siggroup.could_validate(authorities) } /// Return the number of relays in this unvalidated consensus. /// /// This function is unstable. It is only enabled if the crate was /// built with the `experimental-api` feature. #[cfg(feature = "experimental-api")] pub fn n_relays(&self) -> usize { self.consensus.relays.len() } /// Modify the list of relays in this unvalidated consensus. /// /// A use case for this is long-lasting custom directories. To ensure Arti can still quickly /// build circuits when the directory gets old, a tiny churn file can be regularly obtained, /// listing no longer available Tor nodes, which can then be removed from the consensus. /// /// This function is unstable. It is only enabled if the crate was /// built with the `experimental-api` feature. #[cfg(feature = "experimental-api")] pub fn modify_relays(&mut self, func: F) where F: FnOnce(&mut Vec), { func(&mut self.consensus.relays); } } impl ExternallySigned for UnvalidatedConsensus { type Key = [AuthCert]; type KeyHint = Vec; type Error = Error; fn key_is_correct(&self, k: &Self::Key) -> result::Result<(), Self::KeyHint> { let (n_ok, missing) = self.siggroup.list_missing(k); match self.n_authorities { Some(n) if consensus_threshold(n).contains(&n_ok) => Ok(()), _ => Err(missing.iter().map(|cert| cert.key_ids).collect()), } } fn is_well_signed(&self, k: &Self::Key) -> result::Result<(), Self::Error> { match self.n_authorities { None => Err(Error::from(internal!( "Didn't set authorities on consensus" ))), Some(authority) => { self.siggroup.validate(authority, k) .map_err(|_: VerifyFailed| EK::BadSignature.err()) } } } fn dangerously_assume_wellsigned(self) -> Consensus { self.consensus } } /// A Consensus object that has been parsed, but not checked for /// signatures and timeliness. pub type UncheckedConsensus = TimeRangeBound; impl NetworkStatusUnverified { /// Could we verify this consensus or do we need more authcerts? /// /// `Ok` means that we have enough authcerts to verify the signature. /// /// `Err` means that we have not enough authcerts, /// or the consensus has not enough signatures. /// The [`ConsensusVerifiabilityError`] error gives the details. pub fn can_verify( &self, trusted_authorities: &[RsaIdentity], certs_already: &[AuthCert], ) -> Result<(), ConsensusVerifiabilityError> { let sigs = self.inspect_unverified().1; Self::verify_general( sigs, trusted_authorities, certs_already, |_signature| { // indeed, we don't actuaally verify, so this is a no-op Ok(SignatureVerifiedIfIntended {}) }, )?; Ok(()) } /// Verify the signatures /// /// Doesn't check the validity period: /// the document is wrapped in [`TimeRangeBound`], /// ensuring that the caller does that check. pub fn verify( self, trusted_authorities: &[RsaIdentity], certs: &[AuthCert], ) -> Result, ConsensusVerifyFailed> { let (body, sigs) = self.unwrap_unverified(); Self::verify_general( &sigs, trusted_authorities, certs, |tv| tv.verify().map_err(ConsensusVerifyFailed::InvalidSignature), )?; let time_range = body.preamble.validity_time_range(); Ok(TimeRangeBound::new( body, time_range, )) } /// Glue to call `SignatureGroup::verify_general` given our `SignaturesData` /// /// [`SignatureGroup::verify_general`] contains the actual verification code, /// shared between the old parser and the new. fn verify_general( sigs: &parse2::SignaturesData, trusted: &[RsaIdentity], certs: &[AuthCert], do_verify: impl Fn(ConsensusSignatureToVerify) -> Result, ) -> Result<(), E> where ConsensusVerifiabilityError: Into, { SignatureGroup { hashes: sigs.hashes, signatures: sigs.sigs.directory_signature.clone(), }.verify_general( VerifyGeneralTrustedAuthorities::TrustThese { trusted }, certs, do_verify, ) } } #[cfg(feature = "retain-unknown")] #[test] fn verify_error_netstatus() -> Result<(), anyhow::Error> { use assert_matches::assert_matches; use ConsensusVerifiabilityError as CVE; let file = ns_expr!( "testdata2/cached-consensus", "testdata2/cached-microdesc-consensus", unreachable(), ); let (mut doc, _text, certs, authorities, _now) = super::test::prep_netstatus_verify::(file)?; macro_rules! assert_consensus_verifiability_error { { ( $($verify_args:tt)* ), $($assert_matches_rhs:tt)* } => { assert_matches! { doc.can_verify($($verify_args)*), Err(e) => assert_matches!(e, $($assert_matches_rhs)*) }; assert_matches! { doc.clone().verify($($verify_args)*), Err(ConsensusVerifyFailed::CertificationInsufficient(e)) => assert_matches!(e, $($assert_matches_rhs)*) }; } } // missing authcerts assert_consensus_verifiability_error! { (&authorities, &certs[..1]), ConsensusVerifiabilityError::MissingAuthCerts { deficit, missing } => { assert_eq!(deficit, authorities.len() / 2); // one short of strict majority itertools::assert_equal( missing.into_iter().sorted(), certs[1..].iter().map(|a| a.key_ids()).sorted(), ); } } // wrong signers let wrong_authorities = authorities .iter() .cloned() .enumerate() .map(|(i, a)| { if i < 2 { [i as u8; _].into() } else { a } }) .collect_vec(); assert_consensus_verifiability_error! { (&wrong_authorities, &certs), CVE::InsufficientTrustedSigners } // one broken signature, but enough others doc.sigs.sigs.directory_signature[0].signature.fill(0xff); assert_matches! { doc.can_verify(&authorities, &certs), Ok(()) } assert_matches! { doc.clone().verify(&authorities, &certs), Ok(_) } // too few signatories, and one broken signature doc.sigs.sigs.directory_signature.truncate(authorities.len() / 2 + 1); assert_matches! { doc.can_verify(&authorities, &certs), Ok(()) } assert_matches! { doc.clone().verify(&authorities, &certs), Err(ConsensusVerifyFailed::InvalidSignature(VerifyFailed::VerifyFailed)) } // too few signatures, no broken signatures doc.sigs.sigs.directory_signature.remove(0); assert_consensus_verifiability_error! { (&authorities, &certs), CVE::InsufficientTrustedSigners } Ok(()) }