Skip to main content

tor_netdoc/doc/authcert/
encoded.rs

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