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
use 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)]
86
pub struct EncodedAuthCert(#[as_ref(str)] String);
87

            
88
/// State (machine) for checking item sequence
89
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
90
enum 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
100
struct IsOurStructural;
101

            
102
/// auth cert's intro item
103
const INTRO_KEYWORD: &str = "dir-key-certificate-version";
104
/// auth cert's final item, used for bracketing
105
const FINAL_KEYWORD: &str = "dir-key-certification";
106

            
107
impl EncodedAuthCert {
108
    /// Obtain the document text as a `str`
109
    pub fn as_str(&self) -> &str {
110
        &self.0
111
    }
112
}
113

            
114
impl ItemSequenceChecker {
115
    /// Start the state machine
116
62
    fn start() -> Self {
117
        use ItemSequenceChecker::*;
118
62
        Intro
119
62
    }
120

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

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

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

            
155
    /// Finish up, on EOF
156
10
    fn finish(self) -> Result<(), EP> {
157
        use ItemSequenceChecker::*;
158
10
        match self {
159
10
            End => Ok(()),
160
            _other => Err(EP::OtherBadDocument(
161
                "authcert missing end (signature) item",
162
            )),
163
        }
164
10
    }
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.
171
54
fn extra_lexical_checks(s: &str) -> Result<(), EP> {
172
    // Lexical checks (beyond those done by the lexer)
173

            
174
54
    let _without_trailing_nl = s
175
        // In case our lexer tolerates this
176
54
        .strip_suffix("\n")
177
54
        .ok_or(EP::OtherBadDocument("missing final newline"))?;
178

            
179
46
    Ok(())
180
54
}
181

            
182
/// Check that `s` meets the constraints
183
48
fn check(s: &str) -> Result<(), EP> {
184
48
    extra_lexical_checks(s)?;
185

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

            
196
impl TryFrom<String> for EncodedAuthCert {
197
    type Error = ErrorProblem;
198
48
    fn try_from(s: String) -> Result<Self, EP> {
199
48
        check(&s)?;
200
4
        Ok(EncodedAuthCert(s))
201
48
    }
202
}
203

            
204
impl FromStr for EncodedAuthCert {
205
    type Err = ErrorProblem;
206
24
    fn from_str(s: &str) -> Result<Self, EP> {
207
24
        s.to_owned().try_into()
208
24
    }
209
}
210

            
211
impl NetdocParseable for EncodedAuthCert {
212
4
    fn doctype_for_error() -> &'static str {
213
4
        "encoded authority key certificate"
214
4
    }
215

            
216
138
    fn is_intro_item_keyword(kw: KeywordRef<'_>) -> bool {
217
138
        kw == INTRO_KEYWORD
218
138
    }
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
22
    fn from_items(input: &mut ItemStream<'_>, stop_at: stop_at!()) -> Result<Self, EP> {
224
22
        let start_pos = input.byte_position();
225
22
        let mut seq = ItemSequenceChecker::start();
226
74
        while seq != ItemSequenceChecker::End {
227
68
            let item = input.next_item()?.ok_or(EP::MissingItem {
228
68
                keyword: FINAL_KEYWORD,
229
68
            })?;
230

            
231
66
            let kw = item.keyword();
232

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

            
246
6
        let text = input
247
6
            .whole_input()
248
6
            .get(start_pos..)
249
6
            .expect("start_pos wasn't included in the body so far?!");
250

            
251
6
        extra_lexical_checks(text)?;
252

            
253
6
        if let Some(next_item) = input.peek_keyword()? {
254
6
            if !stop_at.stop_at(next_item) {
255
4
                return Err(EP::OtherBadDocument(
256
4
                    "unexpected loose items after embedded authcert",
257
4
                ));
258
2
            }
259
        }
260

            
261
2
        Ok(EncodedAuthCert(text.to_string()))
262
22
    }
263
}
264
#[cfg(test)]
265
mod 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
335
ignored
336
{doc}dir-e-subdoc
337
dir-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
373
dir-missing-nl",
374
        );
375
        chk(
376
            Err("authcert bad structure"),
377
            Err("authcert bad structure"),
378
            r"dir-key-certificate-version
379
dir-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
386
wrong-item
387
dir-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
394
r
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
dir-source
403
dir-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
410
dir-e-subdoc
411
dir-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
418
dir-example-item
419
r
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
426
dir-example-item
427
dir-key-certification
428
dir-extra-item
429
r
430
",
431
        );
432
        chk(
433
            Err("authcert bad structure"),
434
            Err("authcert bad structure"),
435
            r"dir-key-certificate-version
436
dir-key-certificate-version
437
dir-example-item
438
dir-key-certification
439
dir-key-certification
440
r
441
",
442
        );
443
        chk(
444
            Err("authcert bad structure"),
445
            Err("unexpected loose items after embedded authcert"),
446
            r"dir-key-certificate-version
447
dir-example-item
448
dir-key-certification
449
dir-key-certification
450
r
451
",
452
        );
453
    }
454
}