1
//! Support for encoding the network document meta-format
2
//!
3
//! Implements writing documents according to
4
//! [dir-spec.txt](https://spec.torproject.org/dir-spec).
5
//! section 1.2 and 1.3.
6
//!
7
//! This facility processes output that complies with the meta-document format,
8
//! (`dir-spec.txt` section 1.2) -
9
//! unless `raw` methods are called with improper input.
10
//!
11
//! However, no checks are done on keyword presence/absence, multiplicity, or ordering,
12
//! so the output may not necessarily conform to the format of the particular intended document.
13
//! It is the caller's responsibility to call `.item()` in the right order,
14
//! with the right keywords and arguments.
15

            
16
// TODO Plan for encoding signed documents:
17
//
18
//  * Derive an encoder function for Foo; the encoder gives you Encoded<Foo>.
19
//  * Write code ad-hoc to construct FooSignatures.
20
//  * Call encoder-core-provided method on Encoded to add the signatures
21
//
22
// Method(s) on Encoded<Foo> are provided centrally to let you get the &str to hash it.
23
//
24
// Nothing cooked is provided to help with the signature encoding layering violation:
25
// the central encoding derives do not provide any way to obtain a partly-encoded
26
// signature item so that it can be added to the hash.
27
//
28
// So the signing code must recapitulate some of the item encoding.  This will generally
29
// be simply a const str (or similar) with the encoded item name and any parameters,
30
// in precisely the form that needs to be appended to the hash.
31
//
32
// This does leave us open to bugs where the hashed data doesn't match what ends up
33
// being encoded, but since it's a fixed string, such a bug couldn't survive a smoke test.
34
//
35
// If there are items where the layering violation involves encoding
36
// of variable parameters, this would need further work, either ad-hoc,
37
// or additional traits/macrology/etc. if there's enough cases where it's needed.
38

            
39
mod multiplicity;
40
#[macro_use]
41
mod derive;
42

            
43
use std::cmp;
44
use std::collections::BTreeSet;
45
use std::fmt::Write;
46
use std::iter;
47
use std::marker::PhantomData;
48

            
49
use base64ct::{Base64, Base64Unpadded, Encoding};
50
use educe::Educe;
51
use itertools::Itertools;
52
use paste::paste;
53
use rand::{CryptoRng, RngCore};
54
use tor_bytes::EncodeError;
55
use tor_error::internal;
56
use void::Void;
57

            
58
use crate::KeywordEncodable;
59
use crate::parse::tokenize::tag_keywords_ok;
60
use crate::types::misc::Iso8601TimeSp;
61

            
62
// Exports used by macros, which treat this module as a prelude
63
#[doc(hidden)]
64
pub use {
65
    derive::{DisplayHelper, RestMustComeLastMarker},
66
    multiplicity::{
67
        MultiplicityMethods, MultiplicitySelector, OptionalityMethods,
68
        SingletonMultiplicitySelector,
69
    },
70
    std::fmt::{self, Display},
71
    std::result::Result,
72
    tor_error::{Bug, into_internal},
73
};
74

            
75
/// Encoder, representing a partially-built document.
76
///
77
/// For example usage, see the tests in this module, or a descriptor building
78
/// function in tor-netdoc (such as `hsdesc::build::inner::HsDescInner::build_sign`).
79
#[derive(Debug, Clone)]
80
pub struct NetdocEncoder {
81
    /// The being-built document, with everything accumulated so far
82
    ///
83
    /// If an [`ItemEncoder`] exists, it will add a newline when it's dropped.
84
    ///
85
    /// `Err` means bad values passed to some builder function.
86
    /// Such errors are accumulated here for the benefit of handwritten document encoders.
87
    built: Result<String, Bug>,
88
}
89

            
90
/// Encoder for an individual item within a being-built document
91
///
92
/// Returned by [`NetdocEncoder::item()`].
93
#[derive(Debug)]
94
pub struct ItemEncoder<'n> {
95
    /// The document including the partial item that we're building
96
    ///
97
    /// We will always add a newline when we're dropped
98
    doc: &'n mut NetdocEncoder,
99
}
100

            
101
/// Position within a (perhaps partially-) built document
102
///
103
/// This is provided mainly to allow the caller to perform signature operations
104
/// on the part of the document that is to be signed.
105
/// (Sometimes this is only part of it.)
106
///
107
/// There is no enforced linkage between this and the document it refers to.
108
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
109
pub struct Cursor {
110
    /// The offset (in bytes, as for `&str`)
111
    ///
112
    /// Can be out of range if the corresponding `NetdocEncoder` is contains an `Err`.
113
    offset: usize,
114
}
115

            
116
/// Types that can be added as argument(s) to item keyword lines
117
///
118
/// Implemented for strings, and various other types.
119
///
120
/// This is a separate trait so we can control the formatting of (eg) [`Iso8601TimeSp`],
121
/// without having a method on `ItemEncoder` for each argument type.
122
//
123
// TODO consider renaming this to ItemArgumentEncodable to mirror all the other related traits.
124
pub trait ItemArgument {
125
    /// Format as a string suitable for including as a netdoc keyword line argument
126
    ///
127
    /// The implementation is responsible for checking that the syntax is legal.
128
    /// For example, if `self` is a string, it must check that the string is
129
    /// in legal as a single argument.
130
    ///
131
    /// Some netdoc values (eg times) turn into several arguments; in that case,
132
    /// one `ItemArgument` may format into multiple arguments, and this method
133
    /// is responsible for writing them all, with the necessary spaces.
134
    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug>;
135
}
136

            
137
impl NetdocEncoder {
138
    /// Start encoding a document
139
10240
    pub fn new() -> Self {
140
10240
        NetdocEncoder {
141
10240
            built: Ok(String::new()),
142
10240
        }
143
10240
    }
144

            
145
    /// Adds an item to the being-built document
146
    ///
147
    /// The item can be further extended with arguments or an object,
148
    /// using the returned `ItemEncoder`.
149
55972
    pub fn item(&mut self, keyword: impl KeywordEncodable) -> ItemEncoder {
150
55972
        self.raw(&keyword.to_str());
151
55972
        ItemEncoder { doc: self }
152
55972
    }
153

            
154
    /// Internal name for `push_raw_string()`
155
262416
    fn raw(&mut self, s: &dyn Display) {
156
267960
        self.write_with(|b| {
157
262416
            write!(b, "{}", s).expect("write! failed on String");
158
262416
            Ok(())
159
262416
        });
160
262416
    }
161

            
162
    /// Extend the being-built document with a fallible function `f`
163
    ///
164
    /// Doesn't call `f` if the building has already failed,
165
    /// and handles the error if `f` fails.
166
263652
    fn write_with(&mut self, f: impl FnOnce(&mut String) -> Result<(), Bug>) {
167
263652
        let Ok(build) = &mut self.built else {
168
            return;
169
        };
170
263652
        match f(build) {
171
263652
            Ok(()) => (),
172
            Err(e) => {
173
                self.built = Err(e);
174
            }
175
        }
176
263652
    }
177

            
178
    /// Adds raw text to the being-built document
179
    ///
180
    /// `s` is added as raw text, after the newline ending the previous item.
181
    /// If `item` is subsequently called, the start of that item
182
    /// will immediately follow `s`.
183
    ///
184
    /// It is the responsibility of the caller to obey the metadocument syntax.
185
    /// In particular, `s` should end with a newline.
186
    /// No checks are performed.
187
    /// Incorrect use might lead to malformed documents, or later errors.
188
    pub fn push_raw_string(&mut self, s: &dyn Display) {
189
        self.raw(s);
190
    }
191

            
192
    /// Return a cursor, pointing to just after the last item (if any)
193
6804
    pub fn cursor(&self) -> Cursor {
194
6804
        let offset = match &self.built {
195
6804
            Ok(b) => b.len(),
196
            Err(_) => usize::MAX,
197
        };
198
6804
        Cursor { offset }
199
6804
    }
200

            
201
    /// Obtain the text of a section of the document
202
    ///
203
    /// Useful for making a signature.
204
3402
    pub fn slice(&self, begin: Cursor, end: Cursor) -> Result<&str, Bug> {
205
3402
        self.built
206
3402
            .as_ref()
207
3402
            .map_err(Clone::clone)?
208
3402
            .get(begin.offset..end.offset)
209
3402
            .ok_or_else(|| internal!("NetdocEncoder::slice out of bounds, Cursor mismanaged"))
210
3402
    }
211

            
212
    /// Build the document into textual form
213
10236
    pub fn finish(self) -> Result<String, Bug> {
214
10236
        self.built
215
10236
    }
216
}
217

            
218
impl Default for NetdocEncoder {
219
16
    fn default() -> Self {
220
        // We must open-code this because the actual encoder contains Result, which isn't Default
221
16
        NetdocEncoder::new()
222
16
    }
223
}
224

            
225
impl ItemArgument for str {
226
85176
    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
227
        // Implements this
228
        // https://gitlab.torproject.org/tpo/core/torspec/-/merge_requests/106
229
2585620
        if self.is_empty() || self.chars().any(|c| !c.is_ascii_graphic()) {
230
            return Err(internal!(
231
                "invalid netdoc keyword line argument syntax {:?}",
232
                self
233
            ));
234
85176
        }
235
85176
        out.args_raw_nonempty(&self);
236
85176
        Ok(())
237
85176
    }
238
}
239

            
240
impl ItemArgument for &str {
241
27230
    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
242
27230
        <str as ItemArgument>::write_arg_onto(self, out)
243
27230
    }
244
}
245

            
246
impl<T: crate::NormalItemArgument> ItemArgument for T {
247
51418
    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
248
51418
        (*self.to_string()).write_arg_onto(out)
249
51418
    }
250
}
251

            
252
impl ItemArgument for Iso8601TimeSp {
253
    // Unlike the macro'd formats, contains a space while still being one argument
254
6
    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
255
6
        let arg = self.to_string();
256
6
        out.args_raw_nonempty(&arg.as_str());
257
6
        Ok(())
258
6
    }
259
}
260

            
261
#[cfg(feature = "hs-pow-full")]
262
impl ItemArgument for tor_hscrypto::pow::v1::Seed {
263
2
    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
264
2
        let mut seed_bytes = vec![];
265
2
        tor_bytes::Writer::write(&mut seed_bytes, &self)?;
266
2
        out.add_arg(&Base64Unpadded::encode_string(&seed_bytes));
267
2
        Ok(())
268
2
    }
269
}
270

            
271
#[cfg(feature = "hs-pow-full")]
272
impl ItemArgument for tor_hscrypto::pow::v1::Effort {
273
2
    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
274
2
        out.add_arg(&<Self as Into<u32>>::into(*self));
275
2
        Ok(())
276
2
    }
277
}
278

            
279
impl<'n> ItemEncoder<'n> {
280
    /// Add a single argument.
281
    ///
282
    /// Convenience method that defers error handling, for use in infallible contexts.
283
    /// Consider whether to use `ItemArgument::write_arg_onto` directly, instead.
284
    ///
285
    /// If the argument is not in the correct syntax, a `Bug`
286
    /// error will be reported (later).
287
    //
288
    // This is not a hot path.  `dyn` for smaller code size.
289
85076
    pub fn arg(mut self, arg: &dyn ItemArgument) -> Self {
290
85076
        self.add_arg(arg);
291
85076
        self
292
85076
    }
293

            
294
    /// Add a single argument, to a borrowed `ItemEncoder`
295
    ///
296
    /// If the argument is not in the correct syntax, a `Bug`
297
    /// error will be reported (later).
298
    //
299
    // Needed for implementing `ItemArgument`
300
85082
    pub(crate) fn add_arg(&mut self, arg: &dyn ItemArgument) {
301
85082
        let () = arg
302
85082
            .write_arg_onto(self)
303
85082
            .unwrap_or_else(|err| self.doc.built = Err(err));
304
85082
    }
305

            
306
    /// Add zero or more arguments, supplied as a single string.
307
    ///
308
    /// `args` should zero or more valid argument strings,
309
    /// separated by (single) spaces.
310
    /// This is not (properly) checked.
311
    /// Incorrect use might lead to malformed documents, or later errors.
312
10
    pub fn args_raw_string(&mut self, args: &dyn Display) {
313
10
        let args = args.to_string();
314
10
        if !args.is_empty() {
315
10
            self.args_raw_nonempty(&args);
316
10
        }
317
10
    }
318

            
319
    /// Add one or more arguments, supplied as a single string, without any checking
320
85192
    fn args_raw_nonempty(&mut self, args: &dyn Display) {
321
85192
        self.doc.raw(&format_args!(" {}", args));
322
85192
    }
323

            
324
    /// Add an object to the item
325
    ///
326
    /// Checks that `keywords` is in the correct syntax.
327
    /// Doesn't check that it makes semantic sense for the position of the document.
328
    /// `data` will be PEM (base64) encoded.
329
    //
330
    // If keyword is not in the correct syntax, a `Bug` is stored in self.doc.
331
1236
    pub fn object(
332
1236
        self,
333
1236
        keywords: &str,
334
1236
        // Writeable isn't dyn-compatible
335
1236
        data: impl tor_bytes::WriteableOnce,
336
1236
    ) {
337
        use crate::parse::tokenize::object::*;
338

            
339
1236
        self.doc.write_with(|out| {
340
1236
            if keywords.is_empty() || !tag_keywords_ok(keywords) {
341
                return Err(internal!("bad object keywords string {:?}", keywords));
342
1236
            }
343
1236
            let data = {
344
1236
                let mut bytes = vec![];
345
1236
                data.write_into(&mut bytes)?;
346
1236
                Base64::encode_string(&bytes)
347
            };
348
1236
            let mut data = &data[..];
349
1236
            writeln!(out, "\n{BEGIN_STR}{keywords}{TAG_END}").expect("write!");
350
39600
            while !data.is_empty() {
351
38364
                let (l, r) = if data.len() > BASE64_PEM_MAX_LINE {
352
37132
                    data.split_at(BASE64_PEM_MAX_LINE)
353
                } else {
354
1232
                    (data, "")
355
                };
356
38364
                writeln!(out, "{l}").expect("write!");
357
38364
                data = r;
358
            }
359
            // final newline will be written by Drop impl
360
1236
            write!(out, "{END_STR}{keywords}{TAG_END}").expect("write!");
361
1236
            Ok(())
362
1236
        });
363
1236
    }
364

            
365
    /// Finish encoding this item
366
    ///
367
    /// The item will also automatically be finished if the `ItemEncoder` is dropped.
368
36
    pub fn finish(self) {}
369
}
370

            
371
impl Drop for ItemEncoder<'_> {
372
88612
    fn drop(&mut self) {
373
88612
        self.doc.raw(&'\n');
374
88612
    }
375
}
376

            
377
/// Ordering, to be used when encoding network documents
378
///
379
/// Implemented for anything `Ord`.
380
///
381
/// Can also be implemented manually, for if a type cannot be `Ord`
382
/// (perhaps for trait coherence reasons).
383
pub trait EncodeOrd {
384
    /// Compare `self` and `other`
385
    ///
386
    /// As `Ord::cmp`.
387
    fn encode_cmp(&self, other: &Self) -> cmp::Ordering;
388
}
389
impl<T: Ord> EncodeOrd for T {
390
20
    fn encode_cmp(&self, other: &Self) -> cmp::Ordering {
391
20
        self.cmp(other)
392
20
    }
393
}
394

            
395
/// Documents (or sub-documents) that can be encoded in the netdoc metaformat
396
pub trait NetdocEncodable {
397
    /// Append the document onto `out`
398
    fn encode_unsigned(&self, out: &mut NetdocEncoder) -> Result<(), Bug>;
399
}
400

            
401
/// Collections of fields that can be encoded in the netdoc metaformat
402
///
403
/// Whole documents have structure; a `NetdocEncodableFields` does not.
404
pub trait NetdocEncodableFields {
405
    /// Append the document onto `out`
406
    fn encode_fields(&self, out: &mut NetdocEncoder) -> Result<(), Bug>;
407
}
408

            
409
/// Items that can be encoded in network documents
410
pub trait ItemValueEncodable {
411
    /// Write the item's arguments, and any object, onto `out`
412
    ///
413
    /// `out` will have been freshly returned from [`NetdocEncoder::item`].
414
    fn write_item_value_onto(&self, out: ItemEncoder) -> Result<(), Bug>;
415
}
416

            
417
/// An Object value that be encoded into a netdoc
418
pub trait ItemObjectEncodable {
419
    /// The label (keyword(s) in `BEGIN` and `END`)
420
    fn label(&self) -> &str;
421

            
422
    /// Represent the actual value as bytes.
423
    ///
424
    /// The caller, not the object, is responsible for base64 encoding.
425
    //
426
    // This is not a tor_bytes::Writeable supertrait because tor_bytes's writer argument
427
    // is generic, which prevents many deisrable manipulations of an `impl Writeable`.
428
    fn write_object_onto(&self, b: &mut Vec<u8>) -> Result<(), Bug>;
429
}
430

            
431
/// Builders for network documents.
432
///
433
/// This trait is a bit weird, because its `Self` type must contain the *private* keys
434
/// necessary to sign the document!
435
///
436
/// So it is implemented for "builders", not for documents themselves.
437
/// Some existing documents can be constructed only via these builders.
438
/// The newer approach is for documents to be transparent data, at the Rust level,
439
/// and to derive an encoder.
440
/// TODO this derive approach is not yet implemented!
441
///
442
/// Actual document types, which only contain the information in the document,
443
/// don't implement this trait.
444
pub trait NetdocBuilder {
445
    /// Build the document into textual form.
446
    fn build_sign<R: RngCore + CryptoRng>(self, rng: &mut R) -> Result<String, EncodeError>;
447
}
448

            
449
impl ItemValueEncodable for Void {
450
    fn write_item_value_onto(&self, _out: ItemEncoder) -> Result<(), Bug> {
451
        void::unreachable(*self)
452
    }
453
}
454

            
455
impl ItemObjectEncodable for Void {
456
    fn label(&self) -> &str {
457
        void::unreachable(*self)
458
    }
459
    fn write_object_onto(&self, _: &mut Vec<u8>) -> Result<(), Bug> {
460
        void::unreachable(*self)
461
    }
462
}
463

            
464
/// implement [`ItemValueEncodable`] for a particular tuple size
465
macro_rules! item_value_encodable_for_tuple {
466
    { $($i:literal)* } => { paste! {
467
        impl< $( [<T$i>]: ItemArgument, )* > ItemValueEncodable for ( $( [<T$i>], )* ) {
468
74
            fn write_item_value_onto(
469
74
                &self,
470
74
                #[allow(unused)]
471
74
                mut out: ItemEncoder,
472
74
            ) -> Result<(), Bug> {
473
                $(
474
48
                    <[<T$i>] as ItemArgument>::write_arg_onto(&self.$i, &mut out)?;
475
                )*
476
74
                Ok(())
477
74
            }
478
        }
479
    } }
480
}
481

            
482
item_value_encodable_for_tuple! {}
483
item_value_encodable_for_tuple! { 0 }
484
item_value_encodable_for_tuple! { 0 1 }
485
item_value_encodable_for_tuple! { 0 1 2 }
486
item_value_encodable_for_tuple! { 0 1 2 3 }
487
item_value_encodable_for_tuple! { 0 1 2 3 4 }
488
item_value_encodable_for_tuple! { 0 1 2 3 4 5 }
489
item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 }
490
item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 }
491
item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 8 }
492
item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 8 9 }
493

            
494
#[cfg(test)]
495
mod test {
496
    // @@ begin test lint list maintained by maint/add_warning @@
497
    #![allow(clippy::bool_assert_comparison)]
498
    #![allow(clippy::clone_on_copy)]
499
    #![allow(clippy::dbg_macro)]
500
    #![allow(clippy::mixed_attributes_style)]
501
    #![allow(clippy::print_stderr)]
502
    #![allow(clippy::print_stdout)]
503
    #![allow(clippy::single_char_pattern)]
504
    #![allow(clippy::unwrap_used)]
505
    #![allow(clippy::unchecked_time_subtraction)]
506
    #![allow(clippy::useless_vec)]
507
    #![allow(clippy::needless_pass_by_value)]
508
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
509
    use super::*;
510
    use std::str::FromStr;
511

            
512
    use crate::types::misc::Iso8601TimeNoSp;
513
    use base64ct::{Base64Unpadded, Encoding};
514

            
515
    #[test]
516
    fn time_formats_as_args() {
517
        use crate::doc::authcert::AuthCertKwd as ACK;
518
        use crate::doc::netstatus::NetstatusKwd as NK;
519

            
520
        let t_sp = Iso8601TimeSp::from_str("2020-04-18 08:36:57").unwrap();
521
        let t_no_sp = Iso8601TimeNoSp::from_str("2021-04-18T08:36:57").unwrap();
522

            
523
        let mut encode = NetdocEncoder::new();
524
        encode.item(ACK::DIR_KEY_EXPIRES).arg(&t_sp);
525
        encode
526
            .item(NK::SHARED_RAND_PREVIOUS_VALUE)
527
            .arg(&"3")
528
            .arg(&"bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs=")
529
            .arg(&t_no_sp);
530

            
531
        let doc = encode.finish().unwrap();
532
        println!("{}", doc);
533
        assert_eq!(
534
            doc,
535
            r"dir-key-expires 2020-04-18 08:36:57
536
shared-rand-previous-value 3 bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs= 2021-04-18T08:36:57
537
"
538
        );
539
    }
540

            
541
    #[test]
542
    fn authcert() {
543
        use crate::doc::authcert::AuthCertKwd as ACK;
544
        use crate::doc::authcert::{AuthCert, UncheckedAuthCert};
545

            
546
        // c&p from crates/tor-llcrypto/tests/testvec.rs
547
        let pk_rsa = {
548
            let pem = "
549
MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
550
PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
551
qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE";
552
            Base64Unpadded::decode_vec(&pem.replace('\n', "")).unwrap()
553
        };
554

            
555
        let mut encode = NetdocEncoder::new();
556
        encode.item(ACK::DIR_KEY_CERTIFICATE_VERSION).arg(&3);
557
        encode
558
            .item(ACK::FINGERPRINT)
559
            .arg(&"9367f9781da8eabbf96b691175f0e701b43c602e");
560
        encode
561
            .item(ACK::DIR_KEY_PUBLISHED)
562
            .arg(&Iso8601TimeSp::from_str("2020-04-18 08:36:57").unwrap());
563
        encode
564
            .item(ACK::DIR_KEY_EXPIRES)
565
            .arg(&Iso8601TimeSp::from_str("2021-04-18 08:36:57").unwrap());
566
        encode
567
            .item(ACK::DIR_IDENTITY_KEY)
568
            .object("RSA PUBLIC KEY", &*pk_rsa);
569
        encode
570
            .item(ACK::DIR_SIGNING_KEY)
571
            .object("RSA PUBLIC KEY", &*pk_rsa);
572
        encode
573
            .item(ACK::DIR_KEY_CROSSCERT)
574
            .object("ID SIGNATURE", []);
575
        encode
576
            .item(ACK::DIR_KEY_CERTIFICATION)
577
            .object("SIGNATURE", []);
578

            
579
        let doc = encode.finish().unwrap();
580
        eprintln!("{}", doc);
581
        assert_eq!(
582
            doc,
583
            r"dir-key-certificate-version 3
584
fingerprint 9367f9781da8eabbf96b691175f0e701b43c602e
585
dir-key-published 2020-04-18 08:36:57
586
dir-key-expires 2021-04-18 08:36:57
587
dir-identity-key
588
-----BEGIN RSA PUBLIC KEY-----
589
MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
590
PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
591
qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
592
-----END RSA PUBLIC KEY-----
593
dir-signing-key
594
-----BEGIN RSA PUBLIC KEY-----
595
MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
596
PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
597
qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
598
-----END RSA PUBLIC KEY-----
599
dir-key-crosscert
600
-----BEGIN ID SIGNATURE-----
601
-----END ID SIGNATURE-----
602
dir-key-certification
603
-----BEGIN SIGNATURE-----
604
-----END SIGNATURE-----
605
"
606
        );
607

            
608
        let _: UncheckedAuthCert = AuthCert::parse(&doc).unwrap();
609
    }
610
}