//! `EncodedAuthCert` use std::str::FromStr; use tor_error::Bug; use crate::encode::{NetdocEncodable, NetdocEncoder}; use crate::parse2::{ ErrorProblem, IsStructural, ItemStream, KeywordRef, NetdocParseable, ParseInput, }; use ErrorProblem as EP; use crate::doc::netstatus::vote::NetworkStatusUnverifiedParsedBody as NetworkStatusVoteUnverifiedParsedBody; /// Entire authority key certificate, encoded and signed /// /// This is a newtype around `String`. /// /// # Invariants /// /// * Is a complete document in netdoc metasyntax including trailing newline. /// * Starts with one `dir-key-certificate-version` /// * Ends with one `dir-key-certification` /// * No other items are structural in a vote /// * Every item keyword starts `dir-` or is `fingerprint` /// /// See /// /// /// ## Non-invariant /// /// * **Signature and timeliness have not been checked**. /// /// # Functionality /// /// Implements `TryFrom` and `FromStr`. /// /// Implements `NetdocParseable`: /// parser matches `dir-key-certificate-version` and `dir-key-certification`, /// but also calls `Bug` if the caller's `stop_at` /// reports that this keyword is structural for its container. /// (This could happen if an `EncodedAuthCert` existedd in some other /// document but a vote. We do not check this property during encoding.) /// /// # Rationale /// /// Unlike most sub-documents found within netdocs, an authcert is a /// signed document. We expect to be able to copy an authcert into a /// vote, encode, convey and parse the vote, and extract the /// authcert, and verify the authcert's signature. /// /// Additionally, the fact that authcerts have their own signatures means /// that they need to be constructed separately from the surrounding /// document, and then embedded in it later. /// /// When parsing a vote, we need to be able to see *which parts* are /// the authcert, and we need to be able to extract the specific document /// text, but we maybe don't want to parse the authcert. /// /// Conversely, signature verification of authcerts during decoding of a /// vote is fairly complex. We don't want to do signature /// verification during parsing, because signature verification involves /// the time, and we don't want parsing to need to know the time. /// // ## Generics (possible future expansion) // // If we discover other similar document nestings we could genericise things: // // ``` // /// Invariant: // /// // /// * Can be lexed as a netdoc // /// * First item is `Y:is_intro_item_keyword` // /// * Last item is (one) `YS:is_intro_item_keyword` // /// * No other item is any `N::is_structual_item_keyword` // /// // pub struct EncodedNetdoc(String); // // pub type EncodedAuthCert = EncodedNetdoc< // AuthCert, AuthCertSignatures, // (NetworkStatusVote, NetworkStatusSignaturesVote) // >; // ``` // // Details TBD. // #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, derive_more::AsRef)] pub struct EncodedAuthCert(#[as_ref(str)] String); /// State (machine) for checking item sequence #[derive(Copy, Clone, Debug, Eq, PartialEq)] enum ItemSequenceChecker { /// Expecting intro item Intro, /// Expecting body item Body, /// Expecting no more items End, } /// Token indicating keyword is structural for us struct IsOurStructural; /// auth cert's intro item const INTRO_KEYWORD: &str = "dir-key-certificate-version"; /// auth cert's final item, used for bracketing const FINAL_KEYWORD: &str = "dir-key-certification"; impl EncodedAuthCert { /// Obtain the document text as a `str` pub fn as_str(&self) -> &str { &self.0 } } impl ItemSequenceChecker { /// Start the state machine fn start() -> Self { use ItemSequenceChecker::*; Intro } /// Process and check an item (given the keyword) fn keyword(&mut self, kw: KeywordRef<'_>) -> Result, EP> { use ItemSequenceChecker::*; let mut change_state = |from, to| { if *self == from { *self = to; Ok(Some(IsOurStructural)) } else { Err(EP::OtherBadDocument("authcert bad structure")) } }; if kw == INTRO_KEYWORD { change_state(Intro, Body) } else if kw == FINAL_KEYWORD { change_state(Body, End) } else if *self != Body { Err(EP::OtherBadDocument( "authcert loose body item or missing intro keyword", )) } else if let Some(IsStructural) = NetworkStatusVoteUnverifiedParsedBody::is_structural_keyword(kw) { Err(EP::OtherBadDocument( "authcert with vote structural keyword", )) } else if kw == "fingerprint" || kw.as_str().starts_with("dir-") { Ok(None) } else { Err(EP::OtherBadDocument( "authcert body keyword not dir- or fingerprint", )) } } /// Finish up, on EOF fn finish(self) -> Result<(), EP> { use ItemSequenceChecker::*; match self { End => Ok(()), _other => Err(EP::OtherBadDocument( "authcert missing end (signature) item", )), } } } /// Additional lexical checks /// /// These might or might not be done by `parse2::lex`. /// We do them here to be sure. fn extra_lexical_checks(s: &str) -> Result<(), EP> { // Lexical checks (beyond those done by the lexer) let _without_trailing_nl = s // In case our lexer tolerates this .strip_suffix("\n") .ok_or(EP::OtherBadDocument("missing final newline"))?; Ok(()) } /// Check that `s` meets the constraints fn check(s: &str) -> Result<(), EP> { extra_lexical_checks(s)?; // Structural checks let input = ParseInput::new(s, ""); let mut lex = ItemStream::new(&input).map_err(|e| e.problem)?; let mut seq = ItemSequenceChecker::start(); while let Some(item) = lex.next_item()? { seq.keyword(item.keyword())?; } seq.finish() } impl TryFrom for EncodedAuthCert { type Error = ErrorProblem; fn try_from(s: String) -> Result { check(&s)?; Ok(EncodedAuthCert(s)) } } impl FromStr for EncodedAuthCert { type Err = ErrorProblem; fn from_str(s: &str) -> Result { s.to_owned().try_into() } } impl NetdocParseable for EncodedAuthCert { fn doctype_for_error() -> &'static str { "encoded authority key certificate" } fn is_intro_item_keyword(kw: KeywordRef<'_>) -> bool { kw == INTRO_KEYWORD } fn is_structural_keyword(kw: KeywordRef<'_>) -> Option { (Self::is_intro_item_keyword(kw) || kw == FINAL_KEYWORD).then_some(IsStructural) } fn from_items(input: &mut ItemStream<'_>, stop_at: stop_at!()) -> Result { let start_pos = input.byte_position(); let mut seq = ItemSequenceChecker::start(); while seq != ItemSequenceChecker::End { let item = input.next_item()?.ok_or(EP::MissingItem { keyword: FINAL_KEYWORD, })?; let kw = item.keyword(); match seq.keyword(kw)? { Some(IsOurStructural) => {} // already checked None => { if stop_at.stop_at(kw) { return Err(EP::Internal( "bug! parent document structural keyword found while trying to process an embedded authcert, but was accepted by ItemSequenceChecker; authcert embedded in something other than a vote?", )); } } } } seq.finish()?; let end_pos = input.byte_position(); let text = input .whole_input() .get(start_pos..end_pos) .expect("start_pos wasn't included in the body so far?!"); extra_lexical_checks(text)?; if let Some(next_item) = input.peek_keyword()? { if !stop_at.stop_at(next_item) { return Err(EP::OtherBadDocument( "unexpected loose items after embedded authcert", )); } } Ok(EncodedAuthCert(text.to_string())) } } impl NetdocEncodable for EncodedAuthCert { fn encode_unsigned(&self, out: &mut NetdocEncoder) -> Result<(), Bug> { // OK because invariants include the right syntax including a trailing newline. out.push_raw_string(&self.as_str()); Ok(()) } } #[cfg(test)] mod test { // @@ begin test lint list maintained by maint/add_warning @@ #![allow(clippy::bool_assert_comparison)] #![allow(clippy::clone_on_copy)] #![allow(clippy::dbg_macro)] #![allow(clippy::mixed_attributes_style)] #![allow(clippy::print_stderr)] #![allow(clippy::print_stdout)] #![allow(clippy::single_char_pattern)] #![allow(clippy::unwrap_used)] #![allow(clippy::unchecked_time_subtraction)] #![allow(clippy::useless_vec)] #![allow(clippy::needless_pass_by_value)] #![allow(clippy::string_slice)] // See arti#2571 //! use super::*; use crate::parse2::parse_netdoc; use derive_deftly::Deftly; use std::fmt::{Debug, Display}; #[derive(Debug, Deftly)] #[derive_deftly(NetdocParseable)] #[allow(unused)] struct Embeds { e_intro: (), #[deftly(netdoc(subdoc))] cert: EncodedAuthCert, #[deftly(netdoc(subdoc))] subdocs: Vec, } #[derive(Debug, Deftly)] #[derive_deftly(NetdocParseable)] #[allow(unused)] struct Subdoc { dir_e_subdoc: (), } fn chk(exp_sole: Result<(), &str>, exp_embed: Result<(), &str>, doc: &str) { fn chk1( exp: Result<(), &str>, doc: &str, what: &str, got: Result, ) { eprintln!("==========\n---- {what} 8<- ----\n{doc}---- ->8 {what} ----\n"); match got { Err(got_e) => { let got_m = got_e.report().to_string(); eprintln!("{what}, got error: {got_e:?}"); eprintln!("{what}, got error: {got_m:?}"); let exp_m = exp.expect_err("expected success!"); assert!( got_m.contains(exp_m), "{what}, expected different error: {exp_m:?}" ); } y @ Ok(_) => { eprintln!("got {y:?}"); assert!(exp.is_ok(), "{what}, unexpected success; expected: {exp:?}"); } } } chk1(exp_sole, doc, "from_str", EncodedAuthCert::from_str(doc)); chk1( exp_sole, doc, "From", EncodedAuthCert::try_from(doc.to_owned()), ); let embeds = format!( r"e-intro ignored {doc}dir-e-subdoc dir-ignored-2 " ); let parse_input = ParseInput::new(&embeds, ""); chk1( exp_embed, &embeds, "embedded", parse_netdoc::(&parse_input), ); } #[test] fn bad_authcerts() { NetworkStatusVoteUnverifiedParsedBody::is_structural_keyword( KeywordRef::new("dir-source").unwrap(), ) .expect("structural dir-source"); // These documents are all very skeleton: none of the items have arguments, or objects. // It works anyway because we don't actually parse as an authcert, when reading an // EncodedAuthCert. We just check the item keyword sequence. chk( Err("missing final newline"), Err("missing item encoded authority key certificate"), r"", ); chk( Err("authcert loose body item or missing intro keyword"), Err("missing item encoded authority key certificate"), r"wrong-intro ", ); chk( Err("missing final newline"), Err("missing item dir-key-certification"), r"dir-key-certificate-version dir-missing-nl", ); chk( Err("authcert bad structure"), Err("authcert bad structure"), r"dir-key-certificate-version dir-key-certificate-version ", ); chk( Err("authcert body keyword not dir- or fingerprint"), Err("authcert body keyword not dir- or fingerprint"), r"dir-key-certificate-version wrong-item dir-key-certification ", ); chk( Err("authcert with vote structural keyword"), Err("authcert with vote structural keyword"), r"dir-key-certificate-version r dir-key-certification ", ); chk( Err("authcert with vote structural keyword"), Err("authcert with vote structural keyword"), r"dir-key-certificate-version dir-source dir-key-certification ", ); chk( Ok(()), // Simulate bug where EncodedAuthCert doesn't know about our dir-e-subdoc Err("bug! parent document structural keyword found"), r"dir-key-certificate-version dir-e-subdoc dir-key-certification ", ); chk( Err("authcert with vote structural keyword"), Err("authcert with vote structural keyword"), r"dir-key-certificate-version dir-example-item r ", ); chk( Err("authcert loose body item or missing intro keyword"), Err("unexpected loose items after embedded authcert"), r"dir-key-certificate-version dir-example-item dir-key-certification dir-extra-item r ", ); chk( Err("authcert bad structure"), Err("authcert bad structure"), r"dir-key-certificate-version dir-key-certificate-version dir-example-item dir-key-certification dir-key-certification r ", ); chk( Err("authcert bad structure"), Err("unexpected loose items after embedded authcert"), r"dir-key-certificate-version dir-example-item dir-key-certification dir-key-certification r ", ); } }