Skip to main content

tor_netdoc/doc/authcert/
encoded.rs

1//! `EncodedAuthCert`
2
3use std::str::FromStr;
4
5use crate::parse2::{
6    ErrorProblem, IsStructural, ItemStream, KeywordRef, NetdocParseable, ParseInput,
7};
8
9use ErrorProblem as EP;
10
11use crate::parse2::poc::netstatus::NetworkStatusVote; // TODO DIRAUTH abolish poc
12
13/// Entire authority key certificate, encoded and signed
14///
15/// This is a newtype around `String`.
16///
17/// # Invariants
18///
19///  * Is a complete document in netdoc metasyntax including trailing newline.
20///  * Starts with one `dir-key-certificate-version`
21///  * Ends with one `dir-key-certification`
22///  * No other items are structural in a vote
23///  * Every item keyword starts `dir-` or is `fingerprint`
24///
25/// See
26/// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#nesting>
27///
28/// ## Non-invariant
29///
30///  * **Signature and timeliness have not been checked**.
31///
32/// # Functionality
33///
34/// Implements `TryFrom<String>` and `FromStr`.
35///
36/// Implements `NetdocParseable`:
37/// parser matches `dir-key-certificate-version` and `dir-key-certification`,
38/// but also calls `Bug` if the caller's `stop_at`
39/// reports that this keyword is structural for its container.
40/// (This could happen if an `EncodedAuthCert` existedd in some other
41/// document but a vote.  We do not check this property during encoding.)
42///
43/// # Rationale
44///
45/// Unlike most sub-documents found within netdocs, an authcert is a
46/// signed document.  We expect to be able to copy an authcert into a
47/// vote, encode, convey and parse the vote, and extract the
48/// authcert, and verify the authcert's signature.
49///
50/// Additionally, the fact that authcerts have their own signatures means
51/// that they need to be constructed separately from the surrounding
52/// document, and then embedded in it later.
53///
54/// When parsing a vote, we need to be able to see *which parts* are
55/// the authcert, and we need to be able to extract the specific document
56/// text, but we maybe don't want to parse the authcert.
57///
58/// Conversely, signature verification of authcerts during decoding of a
59/// vote is fairly complex.  We don't want to do signature
60/// verification during parsing, because signature verification involves
61/// the time, and we don't want parsing to need to know the time.
62///
63// ## Generics (possible future expansion)
64//
65// If we discover other similar document nestings we could genericise things:
66//
67// ```
68// /// Invariant:
69// ///
70// ///  * Can be lexed as a netdoc
71// ///  * First item is `Y:is_intro_item_keyword`
72// ///  * Last item is (one) `YS:is_intro_item_keyword`
73// ///  * No other item is any `N::is_structual_item_keyword`
74// ///
75// pub struct EncodedNetdoc<Y, YS, (N0, N1 ..)>(String);
76//
77// pub type EncodedAuthCert = EncodedNetdoc<
78//     AuthCert, AuthCertSignatures,
79//     (NetworkStatusVote, NetworkStatusSignaturesVote)
80// >;
81// ```
82//
83// Details TBD.
84//
85#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, derive_more::AsRef)]
86pub struct EncodedAuthCert(#[as_ref(str)] String);
87
88/// State (machine) for checking item sequence
89#[derive(Copy, Clone, Debug, Eq, PartialEq)]
90enum ItemSequenceChecker {
91    /// Expecting intro item
92    Intro,
93    /// Expecting body item
94    Body,
95    /// Expecting no more items
96    End,
97}
98
99/// Token indicating keyword is structural for us
100struct IsOurStructural;
101
102/// auth cert's intro item
103const INTRO_KEYWORD: &str = "dir-key-certificate-version";
104/// auth cert's final item, used for bracketing
105const FINAL_KEYWORD: &str = "dir-key-certification";
106
107impl EncodedAuthCert {
108    /// Obtain the document text as a `str`
109    pub fn as_str(&self) -> &str {
110        &self.0
111    }
112}
113
114impl ItemSequenceChecker {
115    /// Start the state machine
116    fn start() -> Self {
117        use ItemSequenceChecker::*;
118        Intro
119    }
120
121    /// Process and check an item (given the keyword)
122    fn keyword(&mut self, kw: KeywordRef<'_>) -> Result<Option<IsOurStructural>, EP> {
123        use ItemSequenceChecker::*;
124
125        let mut change_state = |from, to| {
126            if *self == from {
127                *self = to;
128                Ok(Some(IsOurStructural))
129            } else {
130                Err(EP::OtherBadDocument("authcert bad structure"))
131            }
132        };
133
134        if kw == INTRO_KEYWORD {
135            change_state(Intro, Body)
136        } else if kw == FINAL_KEYWORD {
137            change_state(Body, End)
138        } else if *self != Body {
139            Err(EP::OtherBadDocument(
140                "authcert loose body item or missing intro keyword",
141            ))
142        } else if let Some(IsStructural) = NetworkStatusVote::is_structural_keyword(kw) {
143            Err(EP::OtherBadDocument(
144                "authcert with vote structural keyword",
145            ))
146        } else if kw == "fingerprint" || kw.as_str().starts_with("dir-") {
147            Ok(None)
148        } else {
149            Err(EP::OtherBadDocument(
150                "authcert body keyword not dir- or fingerprint",
151            ))
152        }
153    }
154
155    /// Finish up, on EOF
156    fn finish(self) -> Result<(), EP> {
157        use ItemSequenceChecker::*;
158        match self {
159            End => Ok(()),
160            _other => Err(EP::OtherBadDocument(
161                "authcert missing end (signature) item",
162            )),
163        }
164    }
165}
166
167/// Additional lexical checks
168///
169/// These might or might not be done by `parse2::lex`.
170/// We do them here to be sure.
171fn extra_lexical_checks(s: &str) -> Result<(), EP> {
172    // Lexical checks (beyond those done by the lexer)
173
174    let _without_trailing_nl = s
175        // In case our lexer tolerates this
176        .strip_suffix("\n")
177        .ok_or(EP::OtherBadDocument("missing final newline"))?;
178
179    Ok(())
180}
181
182/// Check that `s` meets the constraints
183fn check(s: &str) -> Result<(), EP> {
184    extra_lexical_checks(s)?;
185
186    // Structural checks
187    let input = ParseInput::new(s, "<authcert string>");
188    let mut lex = ItemStream::new(&input).map_err(|e| e.problem)?;
189    let mut seq = ItemSequenceChecker::start();
190    while let Some(item) = lex.next_item()? {
191        seq.keyword(item.keyword())?;
192    }
193    seq.finish()
194}
195
196impl TryFrom<String> for EncodedAuthCert {
197    type Error = ErrorProblem;
198    fn try_from(s: String) -> Result<Self, EP> {
199        check(&s)?;
200        Ok(EncodedAuthCert(s))
201    }
202}
203
204impl FromStr for EncodedAuthCert {
205    type Err = ErrorProblem;
206    fn from_str(s: &str) -> Result<Self, EP> {
207        s.to_owned().try_into()
208    }
209}
210
211impl NetdocParseable for EncodedAuthCert {
212    fn doctype_for_error() -> &'static str {
213        "encoded authority key certificate"
214    }
215
216    fn is_intro_item_keyword(kw: KeywordRef<'_>) -> bool {
217        kw == INTRO_KEYWORD
218    }
219    fn is_structural_keyword(kw: KeywordRef<'_>) -> Option<IsStructural> {
220        (Self::is_intro_item_keyword(kw) || kw == FINAL_KEYWORD).then_some(IsStructural)
221    }
222
223    fn from_items(input: &mut ItemStream<'_>, stop_at: stop_at!()) -> Result<Self, EP> {
224        let start_pos = input.byte_position();
225        let mut seq = ItemSequenceChecker::start();
226        while seq != ItemSequenceChecker::End {
227            let item = input.next_item()?.ok_or(EP::MissingItem {
228                keyword: FINAL_KEYWORD,
229            })?;
230
231            let kw = item.keyword();
232
233            match seq.keyword(kw)? {
234                Some(IsOurStructural) => {} // already checked
235                None => {
236                    if stop_at.stop_at(kw) {
237                        return Err(EP::Internal(
238                            "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?",
239                        ));
240                    }
241                }
242            }
243        }
244        seq.finish()?;
245
246        let text = input
247            .whole_input()
248            .get(start_pos..)
249            .expect("start_pos wasn't included in the body so far?!");
250
251        extra_lexical_checks(text)?;
252
253        if let Some(next_item) = input.peek_keyword()? {
254            if !stop_at.stop_at(next_item) {
255                return Err(EP::OtherBadDocument(
256                    "unexpected loose items after embedded authcert",
257                ));
258            }
259        }
260
261        Ok(EncodedAuthCert(text.to_string()))
262    }
263}
264#[cfg(test)]
265mod test {
266    // @@ begin test lint list maintained by maint/add_warning @@
267    #![allow(clippy::bool_assert_comparison)]
268    #![allow(clippy::clone_on_copy)]
269    #![allow(clippy::dbg_macro)]
270    #![allow(clippy::mixed_attributes_style)]
271    #![allow(clippy::print_stderr)]
272    #![allow(clippy::print_stdout)]
273    #![allow(clippy::single_char_pattern)]
274    #![allow(clippy::unwrap_used)]
275    #![allow(clippy::unchecked_time_subtraction)]
276    #![allow(clippy::useless_vec)]
277    #![allow(clippy::needless_pass_by_value)]
278    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
279    use super::*;
280    use crate::parse2::parse_netdoc;
281    use derive_deftly::Deftly;
282    use std::fmt::{Debug, Display};
283
284    #[derive(Debug, Deftly)]
285    #[derive_deftly(NetdocParseable)]
286    #[allow(unused)]
287    struct Embeds {
288        e_intro: (),
289        #[deftly(netdoc(subdoc))]
290        cert: EncodedAuthCert,
291        #[deftly(netdoc(subdoc))]
292        subdocs: Vec<Subdoc>,
293    }
294    #[derive(Debug, Deftly)]
295    #[derive_deftly(NetdocParseable)]
296    #[allow(unused)]
297    struct Subdoc {
298        dir_e_subdoc: (),
299    }
300
301    fn chk(exp_sole: Result<(), &str>, exp_embed: Result<(), &str>, doc: &str) {
302        fn chk1<T: Debug, E: Debug + tor_error::ErrorReport + Display>(
303            exp: Result<(), &str>,
304            doc: &str,
305            what: &str,
306            got: Result<T, E>,
307        ) {
308            eprintln!("==========\n---- {what} 8<- ----\n{doc}---- ->8 {what} ----\n");
309            match got {
310                Err(got_e) => {
311                    let got_m = got_e.report().to_string();
312                    eprintln!("{what}, got error: {got_e:?}");
313                    eprintln!("{what}, got error: {got_m:?}");
314                    let exp_m = exp.expect_err("expected success!");
315                    assert!(
316                        got_m.contains(exp_m),
317                        "{what}, expected different error: {exp_m:?}"
318                    );
319                }
320                y @ Ok(_) => {
321                    eprintln!("got {y:?}");
322                    assert!(exp.is_ok(), "{what}, unexpected success; expected: {exp:?}");
323                }
324            }
325        }
326        chk1(exp_sole, doc, "from_str", EncodedAuthCert::from_str(doc));
327        chk1(
328            exp_sole,
329            doc,
330            "From<String>",
331            EncodedAuthCert::try_from(doc.to_owned()),
332        );
333        let embeds = format!(
334            r"e-intro
335ignored
336{doc}dir-e-subdoc
337dir-ignored-2
338"
339        );
340        let parse_input = ParseInput::new(&embeds, "<embeds>");
341        chk1(
342            exp_embed,
343            &embeds,
344            "embedded",
345            parse_netdoc::<Embeds>(&parse_input),
346        );
347    }
348
349    #[test]
350    fn bad_authcerts() {
351        NetworkStatusVote::is_structural_keyword(KeywordRef::new("dir-source").unwrap())
352            .expect("structural dir-source");
353
354        // These documents are all very skeleton: none of the items have arguments, or objects.
355        // It works anyway because we don't actually parse as an authcert, when reading an
356        // EncodedAuthCert.  We just check the item keyword sequence.
357
358        chk(
359            Err("missing final newline"),
360            Err("missing item encoded authority key certificate"),
361            r"",
362        );
363        chk(
364            Err("authcert loose body item or missing intro keyword"),
365            Err("missing item encoded authority key certificate"),
366            r"wrong-intro
367",
368        );
369        chk(
370            Err("missing final newline"),
371            Err("missing item dir-key-certification"),
372            r"dir-key-certificate-version
373dir-missing-nl",
374        );
375        chk(
376            Err("authcert bad structure"),
377            Err("authcert bad structure"),
378            r"dir-key-certificate-version
379dir-key-certificate-version
380",
381        );
382        chk(
383            Err("authcert body keyword not dir- or fingerprint"),
384            Err("authcert body keyword not dir- or fingerprint"),
385            r"dir-key-certificate-version
386wrong-item
387dir-key-certification
388",
389        );
390        chk(
391            Err("authcert with vote structural keyword"),
392            Err("authcert with vote structural keyword"),
393            r"dir-key-certificate-version
394r
395dir-key-certification
396",
397        );
398        chk(
399            Err("authcert with vote structural keyword"),
400            Err("authcert with vote structural keyword"),
401            r"dir-key-certificate-version
402dir-source
403dir-key-certification
404",
405        );
406        chk(
407            Ok(()), // Simulate bug where EncodedAuthCert doesn't know about our dir-e-subdoc
408            Err("bug! parent document structural keyword found"),
409            r"dir-key-certificate-version
410dir-e-subdoc
411dir-key-certification
412",
413        );
414        chk(
415            Err("authcert with vote structural keyword"),
416            Err("authcert with vote structural keyword"),
417            r"dir-key-certificate-version
418dir-example-item
419r
420",
421        );
422        chk(
423            Err("authcert loose body item or missing intro keyword"),
424            Err("unexpected loose items after embedded authcert"),
425            r"dir-key-certificate-version
426dir-example-item
427dir-key-certification
428dir-extra-item
429r
430",
431        );
432        chk(
433            Err("authcert bad structure"),
434            Err("authcert bad structure"),
435            r"dir-key-certificate-version
436dir-key-certificate-version
437dir-example-item
438dir-key-certification
439dir-key-certification
440r
441",
442        );
443        chk(
444            Err("authcert bad structure"),
445            Err("unexpected loose items after embedded authcert"),
446            r"dir-key-certificate-version
447dir-example-item
448dir-key-certification
449dir-key-certification
450r
451",
452        );
453    }
454}