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
mod multiplicity;
17
#[macro_use]
18
mod derive;
19

            
20
use std::cmp;
21
use std::collections::BTreeSet;
22
use std::fmt::Write;
23
use std::iter;
24
use std::marker::PhantomData;
25

            
26
use base64ct::{Base64, Base64Unpadded, Encoding};
27
use educe::Educe;
28
use itertools::Itertools;
29
use paste::paste;
30
use rand::{CryptoRng, RngCore};
31
use tor_bytes::EncodeError;
32
use tor_error::internal;
33
use void::Void;
34

            
35
use crate::KeywordEncodable;
36
use crate::parse::tokenize::tag_keywords_ok;
37
use crate::types::misc::Iso8601TimeSp;
38

            
39
// Exports used by macros, which treat this module as a prelude
40
#[doc(hidden)]
41
pub use {
42
    derive::{DisplayHelper, RestMustComeLastMarker},
43
    multiplicity::{
44
        MultiplicityMethods, MultiplicitySelector, OptionalityMethods,
45
        SingletonMultiplicitySelector,
46
    },
47
    std::fmt::{self, Display},
48
    std::result::Result,
49
    tor_error::{Bug, into_internal},
50
};
51

            
52
/// Encoder, representing a partially-built document.
53
///
54
/// For example usage, see the tests in this module, or a descriptor building
55
/// function in tor-netdoc (such as `hsdesc::build::inner::HsDescInner::build_sign`).
56
#[derive(Debug, Clone)]
57
pub struct NetdocEncoder {
58
    /// The being-built document, with everything accumulated so far
59
    ///
60
    /// If an [`ItemEncoder`] exists, it will add a newline when it's dropped.
61
    ///
62
    /// `Err` means bad values passed to some builder function.
63
    /// Such errors are accumulated here for the benefit of handwritten document encoders.
64
    built: Result<String, Bug>,
65
}
66

            
67
/// Encoder for an individual item within a being-built document
68
///
69
/// Returned by [`NetdocEncoder::item()`].
70
#[derive(Debug)]
71
pub struct ItemEncoder<'n> {
72
    /// The document including the partial item that we're building
73
    ///
74
    /// We will always add a newline when we're dropped
75
    doc: &'n mut NetdocEncoder,
76
}
77

            
78
/// Position within a (perhaps partially-) built document
79
///
80
/// This is provided mainly to allow the caller to perform signature operations
81
/// on the part of the document that is to be signed.
82
/// (Sometimes this is only part of it.)
83
///
84
/// There is no enforced linkage between this and the document it refers to.
85
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
86
pub struct Cursor {
87
    /// The offset (in bytes, as for `&str`)
88
    ///
89
    /// Can be out of range if the corresponding `NetdocEncoder` is contains an `Err`.
90
    offset: usize,
91
}
92

            
93
/// Types that can be added as argument(s) to item keyword lines
94
///
95
/// Implemented for strings, and various other types.
96
///
97
/// This is a separate trait so we can control the formatting of (eg) [`Iso8601TimeSp`],
98
/// without having a method on `ItemEncoder` for each argument type.
99
//
100
// TODO consider renaming this to ItemArgumentEncodable to mirror all the other related traits.
101
pub trait ItemArgument {
102
    /// Format as a string suitable for including as a netdoc keyword line argument
103
    ///
104
    /// The implementation is responsible for checking that the syntax is legal.
105
    /// For example, if `self` is a string, it must check that the string is
106
    /// in legal as a single argument.
107
    ///
108
    /// Some netdoc values (eg times) turn into several arguments; in that case,
109
    /// one `ItemArgument` may format into multiple arguments, and this method
110
    /// is responsible for writing them all, with the necessary spaces.
111
    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug>;
112
}
113

            
114
impl NetdocEncoder {
115
    /// Start encoding a document
116
10648
    pub fn new() -> Self {
117
10648
        NetdocEncoder {
118
10648
            built: Ok(String::new()),
119
10648
        }
120
10648
    }
121

            
122
    /// Adds an item to the being-built document
123
    ///
124
    /// The item can be further extended with arguments or an object,
125
    /// using the returned `ItemEncoder`.
126
58308
    pub fn item(&mut self, keyword: impl KeywordEncodable) -> ItemEncoder {
127
58308
        self.raw(&keyword.to_str());
128
58308
        ItemEncoder { doc: self }
129
58308
    }
130

            
131
    /// Internal name for `push_raw_string()`
132
272888
    fn raw(&mut self, s: &dyn Display) {
133
279048
        self.write_with(|b| {
134
272888
            write!(b, "{}", s).expect("write! failed on String");
135
272888
            Ok(())
136
272888
        });
137
272888
    }
138

            
139
    /// Extend the being-built document with a fallible function `f`
140
    ///
141
    /// Doesn't call `f` if the building has already failed,
142
    /// and handles the error if `f` fails.
143
274268
    fn write_with(&mut self, f: impl FnOnce(&mut String) -> Result<(), Bug>) {
144
274268
        let Ok(build) = &mut self.built else {
145
            return;
146
        };
147
274268
        match f(build) {
148
274268
            Ok(()) => (),
149
            Err(e) => {
150
                self.built = Err(e);
151
            }
152
        }
153
274268
    }
154

            
155
    /// Adds raw text to the being-built document
156
    ///
157
    /// `s` is added as raw text, after the newline ending the previous item.
158
    /// If `item` is subsequently called, the start of that item
159
    /// will immediately follow `s`.
160
    ///
161
    /// It is the responsibility of the caller to obey the metadocument syntax.
162
    /// In particular, `s` should end with a newline.
163
    /// No checks are performed.
164
    /// Incorrect use might lead to malformed documents, or later errors.
165
    pub fn push_raw_string(&mut self, s: &dyn Display) {
166
        self.raw(s);
167
    }
168

            
169
    /// Return a cursor, pointing to just after the last item (if any)
170
7076
    pub fn cursor(&self) -> Cursor {
171
7076
        let offset = match &self.built {
172
7076
            Ok(b) => b.len(),
173
            Err(_) => usize::MAX,
174
        };
175
7076
        Cursor { offset }
176
7076
    }
177

            
178
    /// Obtain the text of a section of the document
179
    ///
180
    /// Useful for making a signature.
181
3538
    pub fn slice(&self, begin: Cursor, end: Cursor) -> Result<&str, Bug> {
182
3538
        self.built
183
3538
            .as_ref()
184
3538
            .map_err(Clone::clone)?
185
3538
            .get(begin.offset..end.offset)
186
3538
            .ok_or_else(|| internal!("NetdocEncoder::slice out of bounds, Cursor mismanaged"))
187
3538
    }
188

            
189
    /// Build the document into textual form
190
10644
    pub fn finish(self) -> Result<String, Bug> {
191
10644
        self.built
192
10644
    }
193
}
194

            
195
impl Default for NetdocEncoder {
196
16
    fn default() -> Self {
197
        // We must open-code this because the actual encoder contains Result, which isn't Default
198
16
        NetdocEncoder::new()
199
16
    }
200
}
201

            
202
impl ItemArgument for str {
203
88576
    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
204
        // Implements this
205
        // https://gitlab.torproject.org/tpo/core/torspec/-/merge_requests/106
206
2689900
        if self.is_empty() || self.chars().any(|c| !c.is_ascii_graphic()) {
207
            return Err(internal!(
208
                "invalid netdoc keyword line argument syntax {:?}",
209
                self
210
            ));
211
88576
        }
212
88576
        out.args_raw_nonempty(&self);
213
88576
        Ok(())
214
88576
    }
215
}
216

            
217
impl ItemArgument for &str {
218
28318
    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
219
28318
        <str as ItemArgument>::write_arg_onto(self, out)
220
28318
    }
221
}
222

            
223
impl<T: crate::NormalItemArgument> ItemArgument for T {
224
53490
    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
225
53490
        (*self.to_string()).write_arg_onto(out)
226
53490
    }
227
}
228

            
229
impl ItemArgument for Iso8601TimeSp {
230
    // Unlike the macro'd formats, contains a space while still being one argument
231
6
    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
232
6
        let arg = self.to_string();
233
6
        out.args_raw_nonempty(&arg.as_str());
234
6
        Ok(())
235
6
    }
236
}
237

            
238
#[cfg(feature = "hs-pow-full")]
239
impl ItemArgument for tor_hscrypto::pow::v1::Seed {
240
2
    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
241
2
        let mut seed_bytes = vec![];
242
2
        tor_bytes::Writer::write(&mut seed_bytes, &self)?;
243
2
        out.add_arg(&Base64Unpadded::encode_string(&seed_bytes));
244
2
        Ok(())
245
2
    }
246
}
247

            
248
#[cfg(feature = "hs-pow-full")]
249
impl ItemArgument for tor_hscrypto::pow::v1::Effort {
250
2
    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
251
2
        out.add_arg(&<Self as Into<u32>>::into(*self));
252
2
        Ok(())
253
2
    }
254
}
255

            
256
impl<'n> ItemEncoder<'n> {
257
    /// Add a single argument.
258
    ///
259
    /// Convenience method that defers error handling, for use in infallible contexts.
260
    /// Consider whether to use `ItemArgument::write_arg_onto` directly, instead.
261
    ///
262
    /// If the argument is not in the correct syntax, a `Bug`
263
    /// error will be reported (later).
264
    //
265
    // This is not a hot path.  `dyn` for smaller code size.
266
88476
    pub fn arg(mut self, arg: &dyn ItemArgument) -> Self {
267
88476
        self.add_arg(arg);
268
88476
        self
269
88476
    }
270

            
271
    /// Add a single argument, to a borrowed `ItemEncoder`
272
    ///
273
    /// If the argument is not in the correct syntax, a `Bug`
274
    /// error will be reported (later).
275
    //
276
    // Needed for implementing `ItemArgument`
277
88482
    pub(crate) fn add_arg(&mut self, arg: &dyn ItemArgument) {
278
88482
        let () = arg
279
88482
            .write_arg_onto(self)
280
88482
            .unwrap_or_else(|err| self.doc.built = Err(err));
281
88482
    }
282

            
283
    /// Add zero or more arguments, supplied as a single string.
284
    ///
285
    /// `args` should zero or more valid argument strings,
286
    /// separated by (single) spaces.
287
    /// This is not (properly) checked.
288
    /// Incorrect use might lead to malformed documents, or later errors.
289
10
    pub fn args_raw_string(&mut self, args: &dyn Display) {
290
10
        let args = args.to_string();
291
10
        if !args.is_empty() {
292
10
            self.args_raw_nonempty(&args);
293
10
        }
294
10
    }
295

            
296
    /// Add one or more arguments, supplied as a single string, without any checking
297
88592
    fn args_raw_nonempty(&mut self, args: &dyn Display) {
298
88592
        self.doc.raw(&format_args!(" {}", args));
299
88592
    }
300

            
301
    /// Add an object to the item
302
    ///
303
    /// Checks that `keywords` is in the correct syntax.
304
    /// Doesn't check that it makes semantic sense for the position of the document.
305
    /// `data` will be PEM (base64) encoded.
306
    //
307
    // If keyword is not in the correct syntax, a `Bug` is stored in self.doc.
308
1380
    pub fn object(
309
1380
        self,
310
1380
        keywords: &str,
311
1380
        // Writeable isn't dyn-compatible
312
1380
        data: impl tor_bytes::WriteableOnce,
313
1380
    ) {
314
        use crate::parse::tokenize::object::*;
315

            
316
1380
        self.doc.write_with(|out| {
317
1380
            if keywords.is_empty() || !tag_keywords_ok(keywords) {
318
                return Err(internal!("bad object keywords string {:?}", keywords));
319
1380
            }
320
1380
            let data = {
321
1380
                let mut bytes = vec![];
322
1380
                data.write_into(&mut bytes)?;
323
1380
                Base64::encode_string(&bytes)
324
            };
325
1380
            let mut data = &data[..];
326
1380
            writeln!(out, "\n{BEGIN_STR}{keywords}{TAG_END}").expect("write!");
327
44288
            while !data.is_empty() {
328
42908
                let (l, r) = if data.len() > BASE64_PEM_MAX_LINE {
329
41532
                    data.split_at(BASE64_PEM_MAX_LINE)
330
                } else {
331
1376
                    (data, "")
332
                };
333
42908
                writeln!(out, "{l}").expect("write!");
334
42908
                data = r;
335
            }
336
            // final newline will be written by Drop impl
337
1380
            write!(out, "{END_STR}{keywords}{TAG_END}").expect("write!");
338
1380
            Ok(())
339
1380
        });
340
1380
    }
341

            
342
    /// Finish encoding this item
343
    ///
344
    /// The item will also automatically be finished if the `ItemEncoder` is dropped.
345
36
    pub fn finish(self) {}
346
}
347

            
348
impl Drop for ItemEncoder<'_> {
349
92148
    fn drop(&mut self) {
350
92148
        self.doc.raw(&'\n');
351
92148
    }
352
}
353

            
354
/// Ordering, to be used when encoding network documents
355
///
356
/// Implemented for anything `Ord`.
357
///
358
/// Can also be implemented manually, for if a type cannot be `Ord`
359
/// (perhaps for trait coherence reasons).
360
pub trait EncodeOrd {
361
    /// Compare `self` and `other`
362
    ///
363
    /// As `Ord::cmp`.
364
    fn encode_cmp(&self, other: &Self) -> cmp::Ordering;
365
}
366
impl<T: Ord> EncodeOrd for T {
367
20
    fn encode_cmp(&self, other: &Self) -> cmp::Ordering {
368
20
        self.cmp(other)
369
20
    }
370
}
371

            
372
/// Documents (or sub-documents) that can be encoded in the netdoc metaformat
373
pub trait NetdocEncodable {
374
    /// Append the document onto `out`
375
    fn encode_unsigned(&self, out: &mut NetdocEncoder) -> Result<(), Bug>;
376
}
377

            
378
/// Collections of fields that can be encoded in the netdoc metaformat
379
///
380
/// Whole documents have structure; a `NetdocEncodableFields` does not.
381
pub trait NetdocEncodableFields {
382
    /// Append the document onto `out`
383
    fn encode_fields(&self, out: &mut NetdocEncoder) -> Result<(), Bug>;
384
}
385

            
386
/// Items that can be encoded in network documents
387
pub trait ItemValueEncodable {
388
    /// Write the item's arguments, and any object, onto `out`
389
    ///
390
    /// `out` will have been freshly returned from [`NetdocEncoder::item`].
391
    fn write_item_value_onto(&self, out: ItemEncoder) -> Result<(), Bug>;
392
}
393

            
394
/// An Object value that be encoded into a netdoc
395
pub trait ItemObjectEncodable {
396
    /// The label (keyword(s) in `BEGIN` and `END`)
397
    fn label(&self) -> &str;
398

            
399
    /// Represent the actual value as bytes.
400
    ///
401
    /// The caller, not the object, is responsible for base64 encoding.
402
    //
403
    // This is not a tor_bytes::Writeable supertrait because tor_bytes's writer argument
404
    // is generic, which prevents many deisrable manipulations of an `impl Writeable`.
405
    fn write_object_onto(&self, b: &mut Vec<u8>) -> Result<(), Bug>;
406
}
407

            
408
/// Builders for network documents.
409
///
410
/// This trait is a bit weird, because its `Self` type must contain the *private* keys
411
/// necessary to sign the document!
412
///
413
/// So it is implemented for "builders", not for documents themselves.
414
/// Some existing documents can be constructed only via these builders.
415
/// The newer approach is for documents to be transparent data, at the Rust level,
416
/// and to derive an encoder.
417
/// TODO this derive approach is not yet implemented!
418
///
419
/// Actual document types, which only contain the information in the document,
420
/// don't implement this trait.
421
pub trait NetdocBuilder {
422
    /// Build the document into textual form.
423
    fn build_sign<R: RngCore + CryptoRng>(self, rng: &mut R) -> Result<String, EncodeError>;
424
}
425

            
426
impl ItemValueEncodable for Void {
427
    fn write_item_value_onto(&self, _out: ItemEncoder) -> Result<(), Bug> {
428
        void::unreachable(*self)
429
    }
430
}
431

            
432
impl ItemObjectEncodable for Void {
433
    fn label(&self) -> &str {
434
        void::unreachable(*self)
435
    }
436
    fn write_object_onto(&self, _: &mut Vec<u8>) -> Result<(), Bug> {
437
        void::unreachable(*self)
438
    }
439
}
440

            
441
/// implement [`ItemValueEncodable`] for a particular tuple size
442
macro_rules! item_value_encodable_for_tuple {
443
    { $($i:literal)* } => { paste! {
444
        impl< $( [<T$i>]: ItemArgument, )* > ItemValueEncodable for ( $( [<T$i>], )* ) {
445
74
            fn write_item_value_onto(
446
74
                &self,
447
74
                #[allow(unused)]
448
74
                mut out: ItemEncoder,
449
74
            ) -> Result<(), Bug> {
450
                $(
451
48
                    <[<T$i>] as ItemArgument>::write_arg_onto(&self.$i, &mut out)?;
452
                )*
453
74
                Ok(())
454
74
            }
455
        }
456
    } }
457
}
458

            
459
item_value_encodable_for_tuple! {}
460
item_value_encodable_for_tuple! { 0 }
461
item_value_encodable_for_tuple! { 0 1 }
462
item_value_encodable_for_tuple! { 0 1 2 }
463
item_value_encodable_for_tuple! { 0 1 2 3 }
464
item_value_encodable_for_tuple! { 0 1 2 3 4 }
465
item_value_encodable_for_tuple! { 0 1 2 3 4 5 }
466
item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 }
467
item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 }
468
item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 8 }
469
item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 8 9 }
470

            
471
#[cfg(test)]
472
mod test {
473
    // @@ begin test lint list maintained by maint/add_warning @@
474
    #![allow(clippy::bool_assert_comparison)]
475
    #![allow(clippy::clone_on_copy)]
476
    #![allow(clippy::dbg_macro)]
477
    #![allow(clippy::mixed_attributes_style)]
478
    #![allow(clippy::print_stderr)]
479
    #![allow(clippy::print_stdout)]
480
    #![allow(clippy::single_char_pattern)]
481
    #![allow(clippy::unwrap_used)]
482
    #![allow(clippy::unchecked_time_subtraction)]
483
    #![allow(clippy::useless_vec)]
484
    #![allow(clippy::needless_pass_by_value)]
485
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
486
    use super::*;
487
    use std::str::FromStr;
488

            
489
    use crate::types::misc::Iso8601TimeNoSp;
490
    use base64ct::{Base64Unpadded, Encoding};
491

            
492
    #[test]
493
    fn time_formats_as_args() {
494
        use crate::doc::authcert::AuthCertKwd as ACK;
495
        use crate::doc::netstatus::NetstatusKwd as NK;
496

            
497
        let t_sp = Iso8601TimeSp::from_str("2020-04-18 08:36:57").unwrap();
498
        let t_no_sp = Iso8601TimeNoSp::from_str("2021-04-18T08:36:57").unwrap();
499

            
500
        let mut encode = NetdocEncoder::new();
501
        encode.item(ACK::DIR_KEY_EXPIRES).arg(&t_sp);
502
        encode
503
            .item(NK::SHARED_RAND_PREVIOUS_VALUE)
504
            .arg(&"3")
505
            .arg(&"bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs=")
506
            .arg(&t_no_sp);
507

            
508
        let doc = encode.finish().unwrap();
509
        println!("{}", doc);
510
        assert_eq!(
511
            doc,
512
            r"dir-key-expires 2020-04-18 08:36:57
513
shared-rand-previous-value 3 bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs= 2021-04-18T08:36:57
514
"
515
        );
516
    }
517

            
518
    #[test]
519
    fn authcert() {
520
        use crate::doc::authcert::AuthCertKwd as ACK;
521
        use crate::doc::authcert::{AuthCert, UncheckedAuthCert};
522

            
523
        // c&p from crates/tor-llcrypto/tests/testvec.rs
524
        let pk_rsa = {
525
            let pem = "
526
MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
527
PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
528
qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE";
529
            Base64Unpadded::decode_vec(&pem.replace('\n', "")).unwrap()
530
        };
531

            
532
        let mut encode = NetdocEncoder::new();
533
        encode.item(ACK::DIR_KEY_CERTIFICATE_VERSION).arg(&3);
534
        encode
535
            .item(ACK::FINGERPRINT)
536
            .arg(&"9367f9781da8eabbf96b691175f0e701b43c602e");
537
        encode
538
            .item(ACK::DIR_KEY_PUBLISHED)
539
            .arg(&Iso8601TimeSp::from_str("2020-04-18 08:36:57").unwrap());
540
        encode
541
            .item(ACK::DIR_KEY_EXPIRES)
542
            .arg(&Iso8601TimeSp::from_str("2021-04-18 08:36:57").unwrap());
543
        encode
544
            .item(ACK::DIR_IDENTITY_KEY)
545
            .object("RSA PUBLIC KEY", &*pk_rsa);
546
        encode
547
            .item(ACK::DIR_SIGNING_KEY)
548
            .object("RSA PUBLIC KEY", &*pk_rsa);
549
        encode
550
            .item(ACK::DIR_KEY_CROSSCERT)
551
            .object("ID SIGNATURE", []);
552
        encode
553
            .item(ACK::DIR_KEY_CERTIFICATION)
554
            .object("SIGNATURE", []);
555

            
556
        let doc = encode.finish().unwrap();
557
        eprintln!("{}", doc);
558
        assert_eq!(
559
            doc,
560
            r"dir-key-certificate-version 3
561
fingerprint 9367f9781da8eabbf96b691175f0e701b43c602e
562
dir-key-published 2020-04-18 08:36:57
563
dir-key-expires 2021-04-18 08:36:57
564
dir-identity-key
565
-----BEGIN RSA PUBLIC KEY-----
566
MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
567
PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
568
qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
569
-----END RSA PUBLIC KEY-----
570
dir-signing-key
571
-----BEGIN RSA PUBLIC KEY-----
572
MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
573
PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
574
qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
575
-----END RSA PUBLIC KEY-----
576
dir-key-crosscert
577
-----BEGIN ID SIGNATURE-----
578
-----END ID SIGNATURE-----
579
dir-key-certification
580
-----BEGIN SIGNATURE-----
581
-----END SIGNATURE-----
582
"
583
        );
584

            
585
        let _: UncheckedAuthCert = AuthCert::parse(&doc).unwrap();
586
    }
587
}