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};
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Consensus {
pub flavor: ConsensusFlavor,
pub preamble: Preamble,
pub voters: Vec<ConsensusAuthorityEntry>,
pub relays: Vec<RouterStatus>,
pub footer: ConsensusFooterFields,
}
impl Consensus {
pub fn lifetime(&self) -> &Lifetime {
&self.preamble.lifetime
}
pub fn relays(&self) -> &[RouterStatus] {
&self.relays[..]
}
pub fn bandwidth_weights(&self) -> &NetParams<i32> {
&self.footer.bandwidth_weights
}
pub fn params(&self) -> &NetParams<i32> {
&self.preamble.params
}
pub fn shared_rand_cur(&self) -> Option<&SharedRandStatus> {
self.preamble.shared_rand.shared_rand_current_value.as_ref()
}
pub fn shared_rand_prev(&self) -> Option<&SharedRandStatus> {
self.preamble.shared_rand.shared_rand_previous_value.as_ref()
}
pub fn relay_protocol_status(&self) -> &ProtoStatus {
&self.preamble.proto_statuses.relay
}
pub fn client_protocol_status(&self) -> &ProtoStatus {
&self.preamble.proto_statuses.client
}
pub fn protocol_statuses(&self) -> &Arc<ProtoStatuses> {
&self.preamble.proto_statuses
}
}
impl Consensus {
#[cfg(feature = "build_docs")]
pub fn builder() -> ConsensusBuilder {
ConsensusBuilder::new(RouterStatus::flavor())
}
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))
}
fn take_voterinfo(
r: &mut NetDocReader<'_, NetstatusKwd>,
) -> crate::Result<Option<ConsensusAuthorityEntry>> {
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;
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))
}
fn take_footer(r: &mut NetDocReader<'_, NetstatusKwd>) -> crate::Result<ConsensusFooterFields> {
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)
}
fn take_routerstatus(r: &mut NetDocReader<'_, NetstatusKwd>) -> crate::Result<Option<(Pos, RouterStatus)>> {
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)))
}
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)?;
#[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<RouterStatus> = 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,
};
let mut first_sig: Option<Item<'_, NetstatusKwd>> = 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")),
#[allow(clippy::unwrap_used)]
Some(sig) => sig.offset_in(r.str()).unwrap() + "directory-signature ".len(),
};
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,
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 {
fn from_section(sec: &Section<'_, NetstatusKwd>) -> crate::Result<(ConsensusFlavor, Preamble)> {
use NetstatusKwd::*;
{
#[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::<Iso8601TimeSp>()?
.into();
let fresh_until = sec
.required(FRESH_UNTIL)?
.args_as_str()
.parse::<Iso8601TimeSp>()?
.into();
let valid_until = sec
.required(VALID_UNTIL)?
.args_as_str()
.parse::<Iso8601TimeSp>()?
.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("")
.trim();
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());
}
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))
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct UnvalidatedConsensus {
pub consensus: Consensus,
pub siggroup: SignatureGroup,
pub n_authorities: Option<usize>,
}
impl UnvalidatedConsensus {
#[must_use]
pub fn set_n_authorities(self, n_authorities: usize) -> Self {
UnvalidatedConsensus {
n_authorities: Some(n_authorities),
..self
}
}
pub fn signing_cert_ids(&self) -> impl Iterator<Item = AuthCertKeyIds> {
match self.key_is_correct(&[]) {
Ok(()) => Vec::new(),
Err(missing) => missing,
}
.into_iter()
}
pub fn peek_lifetime(&self) -> &Lifetime {
self.consensus.lifetime()
}
pub fn authorities_are_correct(&self, authorities: &[&RsaIdentity]) -> bool {
self.siggroup.could_validate(authorities)
}
#[cfg(feature = "experimental-api")]
pub fn n_relays(&self) -> usize {
self.consensus.relays.len()
}
#[cfg(feature = "experimental-api")]
pub fn modify_relays<F>(&mut self, func: F)
where
F: FnOnce(&mut Vec<RouterStatus>),
{
func(&mut self.consensus.relays);
}
}
impl ExternallySigned<Consensus> for UnvalidatedConsensus {
type Key = [AuthCert];
type KeyHint = Vec<AuthCertKeyIds>;
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
}
}
pub type UncheckedConsensus = TimeRangeBound<UnvalidatedConsensus>;
impl NetworkStatusUnverified {
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| {
Ok(SignatureVerifiedIfIntended {})
},
)?;
Ok(())
}
pub fn verify(
self,
trusted_authorities: &[RsaIdentity],
certs: &[AuthCert],
) -> Result<TimeRangeBound<NetworkStatus>, 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,
))
}
fn verify_general<E>(
sigs: &parse2::SignaturesData<Self>,
trusted: &[RsaIdentity],
certs: &[AuthCert],
do_verify: impl Fn(ConsensusSignatureToVerify) -> Result<SignatureVerifiedIfIntended, E>,
) -> Result<(), E>
where ConsensusVerifiabilityError: Into<E>,
{
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::<NetworkStatusUnverified>(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)*)
};
} }
assert_consensus_verifiability_error! {
(&authorities, &certs[..1]),
ConsensusVerifiabilityError::MissingAuthCerts { deficit, missing } => {
assert_eq!(deficit, authorities.len() / 2); itertools::assert_equal(
missing.into_iter().sorted(),
certs[1..].iter().map(|a| a.key_ids()).sorted(),
);
}
}
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
}
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(_)
}
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))
}
doc.sigs.sigs.directory_signature.remove(0);
assert_consensus_verifiability_error! {
(&authorities, &certs),
CVE::InsufficientTrustedSigners
}
Ok(())
}