1
//! `EncodedAuthCert`
2

            
3
use std::str::FromStr;
4
use tor_error::Bug;
5

            
6
use crate::encode::{NetdocEncodable, NetdocEncoder};
7
use crate::parse2::{
8
    ErrorProblem, IsStructural, ItemStream, KeywordRef, NetdocParseable, ParseInput,
9
};
10

            
11
use ErrorProblem as EP;
12

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

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

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

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

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

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

            
127
272
        let mut change_state = |from, to| {
128
104
            if *self == from {
129
88
                *self = to;
130
88
                Ok(Some(IsOurStructural))
131
            } else {
132
16
                Err(EP::OtherBadDocument("authcert bad structure"))
133
            }
134
104
        };
135

            
136
220
        if kw == INTRO_KEYWORD {
137
76
            change_state(Intro, Body)
138
144
        } else if kw == FINAL_KEYWORD {
139
28
            change_state(Body, End)
140
116
        } else if *self != Body {
141
8
            Err(EP::OtherBadDocument(
142
8
                "authcert loose body item or missing intro keyword",
143
8
            ))
144
        } else if let Some(IsStructural) =
145
108
            NetworkStatusVoteUnverifiedParsedBody::is_structural_keyword(kw)
146
        {
147
18
            Err(EP::OtherBadDocument(
148
18
                "authcert with vote structural keyword",
149
18
            ))
150
90
        } else if kw == "fingerprint" || kw.as_str().starts_with("dir-") {
151
84
            Ok(None)
152
        } else {
153
6
            Err(EP::OtherBadDocument(
154
6
                "authcert body keyword not dir- or fingerprint",
155
6
            ))
156
        }
157
220
    }
158

            
159
    /// Finish up, on EOF
160
16
    fn finish(self) -> Result<(), EP> {
161
        use ItemSequenceChecker::*;
162
16
        match self {
163
16
            End => Ok(()),
164
            _other => Err(EP::OtherBadDocument(
165
                "authcert missing end (signature) item",
166
            )),
167
        }
168
16
    }
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.
175
60
fn extra_lexical_checks(s: &str) -> Result<(), EP> {
176
    // Lexical checks (beyond those done by the lexer)
177

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

            
183
52
    Ok(())
184
60
}
185

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

            
190
    // Structural checks
191
42
    let input = ParseInput::new(s, "<authcert string>");
192
42
    let mut lex = ItemStream::new(&input).map_err(|e| e.problem)?;
193
42
    let mut seq = ItemSequenceChecker::start();
194
124
    while let Some(item) = lex.next_item()? {
195
118
        seq.keyword(item.keyword())?;
196
    }
197
6
    seq.finish()
198
50
}
199

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

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

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

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

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

            
235
102
            let kw = item.keyword();
236

            
237
102
            match seq.keyword(kw)? {
238
36
                Some(IsOurStructural) => {} // already checked
239
                None => {
240
54
                    if stop_at.stop_at(kw) {
241
2
                        return Err(EP::Internal(
242
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?",
243
2
                        ));
244
52
                    }
245
                }
246
            }
247
        }
248
10
        seq.finish()?;
249
10
        let end_pos = input.byte_position();
250

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

            
256
10
        extra_lexical_checks(text)?;
257

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

            
266
6
        Ok(EncodedAuthCert(text.to_string()))
267
26
    }
268
}
269

            
270
impl NetdocEncodable for EncodedAuthCert {
271
2
    fn encode_unsigned(&self, out: &mut NetdocEncoder) -> Result<(), Bug> {
272
        // OK because invariants include the right syntax including a trailing newline.
273
2
        out.push_raw_string(&self.as_str());
274
2
        Ok(())
275
2
    }
276
}
277

            
278
#[cfg(test)]
279
mod 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
350
ignored
351
{doc}dir-e-subdoc
352
dir-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
390
dir-missing-nl",
391
        );
392
        chk(
393
            Err("authcert bad structure"),
394
            Err("authcert bad structure"),
395
            r"dir-key-certificate-version
396
dir-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
403
wrong-item
404
dir-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
411
r
412
dir-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
419
dir-source
420
dir-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
427
dir-e-subdoc
428
dir-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
435
dir-example-item
436
r
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
443
dir-example-item
444
dir-key-certification
445
dir-extra-item
446
r
447
",
448
        );
449
        chk(
450
            Err("authcert bad structure"),
451
            Err("authcert bad structure"),
452
            r"dir-key-certificate-version
453
dir-key-certificate-version
454
dir-example-item
455
dir-key-certification
456
dir-key-certification
457
r
458
",
459
        );
460
        chk(
461
            Err("authcert bad structure"),
462
            Err("unexpected loose items after embedded authcert"),
463
            r"dir-key-certificate-version
464
dir-example-item
465
dir-key-certification
466
dir-key-certification
467
r
468
",
469
        );
470
    }
471
}