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