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
// TODO DIRAUTH abolish poc
14
use 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)]
92
pub struct EncodedAuthCert(#[as_ref(str)] String);
93

            
94
/// State (machine) for checking item sequence
95
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
96
enum 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
106
struct IsOurStructural;
107

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

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

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

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

            
131
232
        let mut change_state = |from, to| {
132
96
            if *self == from {
133
80
                *self = to;
134
80
                Ok(Some(IsOurStructural))
135
            } else {
136
16
                Err(EP::OtherBadDocument("authcert bad structure"))
137
            }
138
96
        };
139

            
140
184
        if kw == INTRO_KEYWORD {
141
72
            change_state(Intro, Body)
142
112
        } else if kw == FINAL_KEYWORD {
143
24
            change_state(Body, End)
144
88
        } else if *self != Body {
145
8
            Err(EP::OtherBadDocument(
146
8
                "authcert loose body item or missing intro keyword",
147
8
            ))
148
        } else if let Some(IsStructural) =
149
80
            NetworkStatusVoteUnverifiedParsedBody::is_structural_keyword(kw)
150
        {
151
18
            Err(EP::OtherBadDocument(
152
18
                "authcert with vote structural keyword",
153
18
            ))
154
62
        } else if kw == "fingerprint" || kw.as_str().starts_with("dir-") {
155
56
            Ok(None)
156
        } else {
157
6
            Err(EP::OtherBadDocument(
158
6
                "authcert body keyword not dir- or fingerprint",
159
6
            ))
160
        }
161
184
    }
162

            
163
    /// Finish up, on EOF
164
12
    fn finish(self) -> Result<(), EP> {
165
        use ItemSequenceChecker::*;
166
12
        match self {
167
12
            End => Ok(()),
168
            _other => Err(EP::OtherBadDocument(
169
                "authcert missing end (signature) item",
170
            )),
171
        }
172
12
    }
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.
179
56
fn extra_lexical_checks(s: &str) -> Result<(), EP> {
180
    // Lexical checks (beyond those done by the lexer)
181

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

            
187
48
    Ok(())
188
56
}
189

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

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

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

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

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

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

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

            
239
66
            let kw = item.keyword();
240

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

            
255
6
        let text = input
256
6
            .whole_input()
257
6
            .get(start_pos..end_pos)
258
6
            .expect("start_pos wasn't included in the body so far?!");
259

            
260
6
        extra_lexical_checks(text)?;
261

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

            
270
2
        Ok(EncodedAuthCert(text.to_string()))
271
22
    }
272
}
273

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

            
282
#[cfg(test)]
283
mod test {
284
    // @@ begin test lint list maintained by maint/add_warning @@
285
    #![allow(clippy::bool_assert_comparison)]
286
    #![allow(clippy::clone_on_copy)]
287
    #![allow(clippy::dbg_macro)]
288
    #![allow(clippy::mixed_attributes_style)]
289
    #![allow(clippy::print_stderr)]
290
    #![allow(clippy::print_stdout)]
291
    #![allow(clippy::single_char_pattern)]
292
    #![allow(clippy::unwrap_used)]
293
    #![allow(clippy::unchecked_time_subtraction)]
294
    #![allow(clippy::useless_vec)]
295
    #![allow(clippy::needless_pass_by_value)]
296
    #![allow(clippy::string_slice)] // See arti#2571
297
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
298
    use super::*;
299
    use crate::parse2::parse_netdoc;
300
    use derive_deftly::Deftly;
301
    use std::fmt::{Debug, Display};
302

            
303
    #[derive(Debug, Deftly)]
304
    #[derive_deftly(NetdocParseable)]
305
    #[allow(unused)]
306
    struct Embeds {
307
        e_intro: (),
308
        #[deftly(netdoc(subdoc))]
309
        cert: EncodedAuthCert,
310
        #[deftly(netdoc(subdoc))]
311
        subdocs: Vec<Subdoc>,
312
    }
313
    #[derive(Debug, Deftly)]
314
    #[derive_deftly(NetdocParseable)]
315
    #[allow(unused)]
316
    struct Subdoc {
317
        dir_e_subdoc: (),
318
    }
319

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

            
368
    #[test]
369
    fn bad_authcerts() {
370
        NetworkStatusVoteUnverifiedParsedBody::is_structural_keyword(
371
            KeywordRef::new("dir-source").unwrap(),
372
        )
373
        .expect("structural dir-source");
374

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

            
379
        chk(
380
            Err("missing final newline"),
381
            Err("missing item encoded authority key certificate"),
382
            r"",
383
        );
384
        chk(
385
            Err("authcert loose body item or missing intro keyword"),
386
            Err("missing item encoded authority key certificate"),
387
            r"wrong-intro
388
",
389
        );
390
        chk(
391
            Err("missing final newline"),
392
            Err("missing item dir-key-certification"),
393
            r"dir-key-certificate-version
394
dir-missing-nl",
395
        );
396
        chk(
397
            Err("authcert bad structure"),
398
            Err("authcert bad structure"),
399
            r"dir-key-certificate-version
400
dir-key-certificate-version
401
",
402
        );
403
        chk(
404
            Err("authcert body keyword not dir- or fingerprint"),
405
            Err("authcert body keyword not dir- or fingerprint"),
406
            r"dir-key-certificate-version
407
wrong-item
408
dir-key-certification
409
",
410
        );
411
        chk(
412
            Err("authcert with vote structural keyword"),
413
            Err("authcert with vote structural keyword"),
414
            r"dir-key-certificate-version
415
r
416
dir-key-certification
417
",
418
        );
419
        chk(
420
            Err("authcert with vote structural keyword"),
421
            Err("authcert with vote structural keyword"),
422
            r"dir-key-certificate-version
423
dir-source
424
dir-key-certification
425
",
426
        );
427
        chk(
428
            Ok(()), // Simulate bug where EncodedAuthCert doesn't know about our dir-e-subdoc
429
            Err("bug! parent document structural keyword found"),
430
            r"dir-key-certificate-version
431
dir-e-subdoc
432
dir-key-certification
433
",
434
        );
435
        chk(
436
            Err("authcert with vote structural keyword"),
437
            Err("authcert with vote structural keyword"),
438
            r"dir-key-certificate-version
439
dir-example-item
440
r
441
",
442
        );
443
        chk(
444
            Err("authcert loose body item or missing intro keyword"),
445
            Err("unexpected loose items after embedded authcert"),
446
            r"dir-key-certificate-version
447
dir-example-item
448
dir-key-certification
449
dir-extra-item
450
r
451
",
452
        );
453
        chk(
454
            Err("authcert bad structure"),
455
            Err("authcert bad structure"),
456
            r"dir-key-certificate-version
457
dir-key-certificate-version
458
dir-example-item
459
dir-key-certification
460
dir-key-certification
461
r
462
",
463
        );
464
        chk(
465
            Err("authcert bad structure"),
466
            Err("unexpected loose items after embedded authcert"),
467
            r"dir-key-certificate-version
468
dir-example-item
469
dir-key-certification
470
dir-key-certification
471
r
472
",
473
        );
474
    }
475
}