1
//! `EncodedAuthCert`
2

            
3
use std::str::FromStr;
4

            
5
use crate::parse2::{
6
    ErrorProblem, IsStructural, ItemStream, KeywordRef, NetdocParseable, ParseInput,
7
};
8

            
9
use ErrorProblem as EP;
10

            
11
// TODO DIRAUTH abolish poc
12
use crate::parse2::poc::netstatus::vote::{
13
    //
14
    NetworkStatusUnverifiedParsedBody as NetworkStatusVoteUnverifiedParsedBody,
15
};
16

            
17
/// Entire authority key certificate, encoded and signed
18
///
19
/// This is a newtype around `String`.
20
///
21
/// # Invariants
22
///
23
///  * Is a complete document in netdoc metasyntax including trailing newline.
24
///  * Starts with one `dir-key-certificate-version`
25
///  * Ends with one `dir-key-certification`
26
///  * No other items are structural in a vote
27
///  * Every item keyword starts `dir-` or is `fingerprint`
28
///
29
/// See
30
/// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#nesting>
31
///
32
/// ## Non-invariant
33
///
34
///  * **Signature and timeliness have not been checked**.
35
///
36
/// # Functionality
37
///
38
/// Implements `TryFrom<String>` and `FromStr`.
39
///
40
/// Implements `NetdocParseable`:
41
/// parser matches `dir-key-certificate-version` and `dir-key-certification`,
42
/// but also calls `Bug` if the caller's `stop_at`
43
/// reports that this keyword is structural for its container.
44
/// (This could happen if an `EncodedAuthCert` existedd in some other
45
/// document but a vote.  We do not check this property during encoding.)
46
///
47
/// # Rationale
48
///
49
/// Unlike most sub-documents found within netdocs, an authcert is a
50
/// signed document.  We expect to be able to copy an authcert into a
51
/// vote, encode, convey and parse the vote, and extract the
52
/// authcert, and verify the authcert's signature.
53
///
54
/// Additionally, the fact that authcerts have their own signatures means
55
/// that they need to be constructed separately from the surrounding
56
/// document, and then embedded in it later.
57
///
58
/// When parsing a vote, we need to be able to see *which parts* are
59
/// the authcert, and we need to be able to extract the specific document
60
/// text, but we maybe don't want to parse the authcert.
61
///
62
/// Conversely, signature verification of authcerts during decoding of a
63
/// vote is fairly complex.  We don't want to do signature
64
/// verification during parsing, because signature verification involves
65
/// the time, and we don't want parsing to need to know the time.
66
///
67
// ## Generics (possible future expansion)
68
//
69
// If we discover other similar document nestings we could genericise things:
70
//
71
// ```
72
// /// Invariant:
73
// ///
74
// ///  * Can be lexed as a netdoc
75
// ///  * First item is `Y:is_intro_item_keyword`
76
// ///  * Last item is (one) `YS:is_intro_item_keyword`
77
// ///  * No other item is any `N::is_structual_item_keyword`
78
// ///
79
// pub struct EncodedNetdoc<Y, YS, (N0, N1 ..)>(String);
80
//
81
// pub type EncodedAuthCert = EncodedNetdoc<
82
//     AuthCert, AuthCertSignatures,
83
//     (NetworkStatusVote, NetworkStatusSignaturesVote)
84
// >;
85
// ```
86
//
87
// Details TBD.
88
//
89
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, derive_more::AsRef)]
90
pub struct EncodedAuthCert(#[as_ref(str)] String);
91

            
92
/// State (machine) for checking item sequence
93
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
94
enum ItemSequenceChecker {
95
    /// Expecting intro item
96
    Intro,
97
    /// Expecting body item
98
    Body,
99
    /// Expecting no more items
100
    End,
101
}
102

            
103
/// Token indicating keyword is structural for us
104
struct IsOurStructural;
105

            
106
/// auth cert's intro item
107
const INTRO_KEYWORD: &str = "dir-key-certificate-version";
108
/// auth cert's final item, used for bracketing
109
const FINAL_KEYWORD: &str = "dir-key-certification";
110

            
111
impl EncodedAuthCert {
112
    /// Obtain the document text as a `str`
113
    pub fn as_str(&self) -> &str {
114
        &self.0
115
    }
116
}
117

            
118
impl ItemSequenceChecker {
119
    /// Start the state machine
120
62
    fn start() -> Self {
121
        use ItemSequenceChecker::*;
122
62
        Intro
123
62
    }
124

            
125
    /// Process and check an item (given the keyword)
126
166
    fn keyword(&mut self, kw: KeywordRef<'_>) -> Result<Option<IsOurStructural>, EP> {
127
        use ItemSequenceChecker::*;
128

            
129
212
        let mut change_state = |from, to| {
130
92
            if *self == from {
131
76
                *self = to;
132
76
                Ok(Some(IsOurStructural))
133
            } else {
134
16
                Err(EP::OtherBadDocument("authcert bad structure"))
135
            }
136
92
        };
137

            
138
166
        if kw == INTRO_KEYWORD {
139
70
            change_state(Intro, Body)
140
96
        } else if kw == FINAL_KEYWORD {
141
22
            change_state(Body, End)
142
74
        } else if *self != Body {
143
8
            Err(EP::OtherBadDocument(
144
8
                "authcert loose body item or missing intro keyword",
145
8
            ))
146
        } else if let Some(IsStructural) =
147
66
            NetworkStatusVoteUnverifiedParsedBody::is_structural_keyword(kw)
148
        {
149
18
            Err(EP::OtherBadDocument(
150
18
                "authcert with vote structural keyword",
151
18
            ))
152
48
        } else if kw == "fingerprint" || kw.as_str().starts_with("dir-") {
153
42
            Ok(None)
154
        } else {
155
6
            Err(EP::OtherBadDocument(
156
6
                "authcert body keyword not dir- or fingerprint",
157
6
            ))
158
        }
159
166
    }
160

            
161
    /// Finish up, on EOF
162
10
    fn finish(self) -> Result<(), EP> {
163
        use ItemSequenceChecker::*;
164
10
        match self {
165
10
            End => Ok(()),
166
            _other => Err(EP::OtherBadDocument(
167
                "authcert missing end (signature) item",
168
            )),
169
        }
170
10
    }
171
}
172

            
173
/// Additional lexical checks
174
///
175
/// These might or might not be done by `parse2::lex`.
176
/// We do them here to be sure.
177
54
fn extra_lexical_checks(s: &str) -> Result<(), EP> {
178
    // Lexical checks (beyond those done by the lexer)
179

            
180
54
    let _without_trailing_nl = s
181
        // In case our lexer tolerates this
182
54
        .strip_suffix("\n")
183
54
        .ok_or(EP::OtherBadDocument("missing final newline"))?;
184

            
185
46
    Ok(())
186
54
}
187

            
188
/// Check that `s` meets the constraints
189
48
fn check(s: &str) -> Result<(), EP> {
190
48
    extra_lexical_checks(s)?;
191

            
192
    // Structural checks
193
40
    let input = ParseInput::new(s, "<authcert string>");
194
40
    let mut lex = ItemStream::new(&input).map_err(|e| e.problem)?;
195
40
    let mut seq = ItemSequenceChecker::start();
196
104
    while let Some(item) = lex.next_item()? {
197
100
        seq.keyword(item.keyword())?;
198
    }
199
4
    seq.finish()
200
48
}
201

            
202
impl TryFrom<String> for EncodedAuthCert {
203
    type Error = ErrorProblem;
204
48
    fn try_from(s: String) -> Result<Self, EP> {
205
48
        check(&s)?;
206
4
        Ok(EncodedAuthCert(s))
207
48
    }
208
}
209

            
210
impl FromStr for EncodedAuthCert {
211
    type Err = ErrorProblem;
212
24
    fn from_str(s: &str) -> Result<Self, EP> {
213
24
        s.to_owned().try_into()
214
24
    }
215
}
216

            
217
impl NetdocParseable for EncodedAuthCert {
218
4
    fn doctype_for_error() -> &'static str {
219
4
        "encoded authority key certificate"
220
4
    }
221

            
222
138
    fn is_intro_item_keyword(kw: KeywordRef<'_>) -> bool {
223
138
        kw == INTRO_KEYWORD
224
138
    }
225
    fn is_structural_keyword(kw: KeywordRef<'_>) -> Option<IsStructural> {
226
        (Self::is_intro_item_keyword(kw) || kw == FINAL_KEYWORD).then_some(IsStructural)
227
    }
228

            
229
22
    fn from_items(input: &mut ItemStream<'_>, stop_at: stop_at!()) -> Result<Self, EP> {
230
22
        let start_pos = input.byte_position();
231
22
        let mut seq = ItemSequenceChecker::start();
232
74
        while seq != ItemSequenceChecker::End {
233
68
            let item = input.next_item()?.ok_or(EP::MissingItem {
234
68
                keyword: FINAL_KEYWORD,
235
68
            })?;
236

            
237
66
            let kw = item.keyword();
238

            
239
66
            match seq.keyword(kw)? {
240
28
                Some(IsOurStructural) => {} // already checked
241
                None => {
242
26
                    if stop_at.stop_at(kw) {
243
2
                        return Err(EP::Internal(
244
2
                            "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?",
245
2
                        ));
246
24
                    }
247
                }
248
            }
249
        }
250
6
        seq.finish()?;
251

            
252
6
        let text = input
253
6
            .whole_input()
254
6
            .get(start_pos..)
255
6
            .expect("start_pos wasn't included in the body so far?!");
256

            
257
6
        extra_lexical_checks(text)?;
258

            
259
6
        if let Some(next_item) = input.peek_keyword()? {
260
6
            if !stop_at.stop_at(next_item) {
261
4
                return Err(EP::OtherBadDocument(
262
4
                    "unexpected loose items after embedded authcert",
263
4
                ));
264
2
            }
265
        }
266

            
267
2
        Ok(EncodedAuthCert(text.to_string()))
268
22
    }
269
}
270
#[cfg(test)]
271
mod test {
272
    // @@ begin test lint list maintained by maint/add_warning @@
273
    #![allow(clippy::bool_assert_comparison)]
274
    #![allow(clippy::clone_on_copy)]
275
    #![allow(clippy::dbg_macro)]
276
    #![allow(clippy::mixed_attributes_style)]
277
    #![allow(clippy::print_stderr)]
278
    #![allow(clippy::print_stdout)]
279
    #![allow(clippy::single_char_pattern)]
280
    #![allow(clippy::unwrap_used)]
281
    #![allow(clippy::unchecked_time_subtraction)]
282
    #![allow(clippy::useless_vec)]
283
    #![allow(clippy::needless_pass_by_value)]
284
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
285
    use super::*;
286
    use crate::parse2::parse_netdoc;
287
    use derive_deftly::Deftly;
288
    use std::fmt::{Debug, Display};
289

            
290
    #[derive(Debug, Deftly)]
291
    #[derive_deftly(NetdocParseable)]
292
    #[allow(unused)]
293
    struct Embeds {
294
        e_intro: (),
295
        #[deftly(netdoc(subdoc))]
296
        cert: EncodedAuthCert,
297
        #[deftly(netdoc(subdoc))]
298
        subdocs: Vec<Subdoc>,
299
    }
300
    #[derive(Debug, Deftly)]
301
    #[derive_deftly(NetdocParseable)]
302
    #[allow(unused)]
303
    struct Subdoc {
304
        dir_e_subdoc: (),
305
    }
306

            
307
    fn chk(exp_sole: Result<(), &str>, exp_embed: Result<(), &str>, doc: &str) {
308
        fn chk1<T: Debug, E: Debug + tor_error::ErrorReport + Display>(
309
            exp: Result<(), &str>,
310
            doc: &str,
311
            what: &str,
312
            got: Result<T, E>,
313
        ) {
314
            eprintln!("==========\n---- {what} 8<- ----\n{doc}---- ->8 {what} ----\n");
315
            match got {
316
                Err(got_e) => {
317
                    let got_m = got_e.report().to_string();
318
                    eprintln!("{what}, got error: {got_e:?}");
319
                    eprintln!("{what}, got error: {got_m:?}");
320
                    let exp_m = exp.expect_err("expected success!");
321
                    assert!(
322
                        got_m.contains(exp_m),
323
                        "{what}, expected different error: {exp_m:?}"
324
                    );
325
                }
326
                y @ Ok(_) => {
327
                    eprintln!("got {y:?}");
328
                    assert!(exp.is_ok(), "{what}, unexpected success; expected: {exp:?}");
329
                }
330
            }
331
        }
332
        chk1(exp_sole, doc, "from_str", EncodedAuthCert::from_str(doc));
333
        chk1(
334
            exp_sole,
335
            doc,
336
            "From<String>",
337
            EncodedAuthCert::try_from(doc.to_owned()),
338
        );
339
        let embeds = format!(
340
            r"e-intro
341
ignored
342
{doc}dir-e-subdoc
343
dir-ignored-2
344
"
345
        );
346
        let parse_input = ParseInput::new(&embeds, "<embeds>");
347
        chk1(
348
            exp_embed,
349
            &embeds,
350
            "embedded",
351
            parse_netdoc::<Embeds>(&parse_input),
352
        );
353
    }
354

            
355
    #[test]
356
    fn bad_authcerts() {
357
        NetworkStatusVoteUnverifiedParsedBody::is_structural_keyword(
358
            KeywordRef::new("dir-source").unwrap(),
359
        )
360
        .expect("structural dir-source");
361

            
362
        // These documents are all very skeleton: none of the items have arguments, or objects.
363
        // It works anyway because we don't actually parse as an authcert, when reading an
364
        // EncodedAuthCert.  We just check the item keyword sequence.
365

            
366
        chk(
367
            Err("missing final newline"),
368
            Err("missing item encoded authority key certificate"),
369
            r"",
370
        );
371
        chk(
372
            Err("authcert loose body item or missing intro keyword"),
373
            Err("missing item encoded authority key certificate"),
374
            r"wrong-intro
375
",
376
        );
377
        chk(
378
            Err("missing final newline"),
379
            Err("missing item dir-key-certification"),
380
            r"dir-key-certificate-version
381
dir-missing-nl",
382
        );
383
        chk(
384
            Err("authcert bad structure"),
385
            Err("authcert bad structure"),
386
            r"dir-key-certificate-version
387
dir-key-certificate-version
388
",
389
        );
390
        chk(
391
            Err("authcert body keyword not dir- or fingerprint"),
392
            Err("authcert body keyword not dir- or fingerprint"),
393
            r"dir-key-certificate-version
394
wrong-item
395
dir-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
402
r
403
dir-key-certification
404
",
405
        );
406
        chk(
407
            Err("authcert with vote structural keyword"),
408
            Err("authcert with vote structural keyword"),
409
            r"dir-key-certificate-version
410
dir-source
411
dir-key-certification
412
",
413
        );
414
        chk(
415
            Ok(()), // Simulate bug where EncodedAuthCert doesn't know about our dir-e-subdoc
416
            Err("bug! parent document structural keyword found"),
417
            r"dir-key-certificate-version
418
dir-e-subdoc
419
dir-key-certification
420
",
421
        );
422
        chk(
423
            Err("authcert with vote structural keyword"),
424
            Err("authcert with vote structural keyword"),
425
            r"dir-key-certificate-version
426
dir-example-item
427
r
428
",
429
        );
430
        chk(
431
            Err("authcert loose body item or missing intro keyword"),
432
            Err("unexpected loose items after embedded authcert"),
433
            r"dir-key-certificate-version
434
dir-example-item
435
dir-key-certification
436
dir-extra-item
437
r
438
",
439
        );
440
        chk(
441
            Err("authcert bad structure"),
442
            Err("authcert bad structure"),
443
            r"dir-key-certificate-version
444
dir-key-certificate-version
445
dir-example-item
446
dir-key-certification
447
dir-key-certification
448
r
449
",
450
        );
451
        chk(
452
            Err("authcert bad structure"),
453
            Err("unexpected loose items after embedded authcert"),
454
            r"dir-key-certificate-version
455
dir-example-item
456
dir-key-certification
457
dir-key-certification
458
r
459
",
460
        );
461
    }
462
}