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
mod impls;
43

            
44
use std::cmp;
45
use std::collections::BTreeSet;
46
use std::fmt::Write;
47
use std::iter;
48
use std::marker::PhantomData;
49
use std::sync::Arc;
50

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

            
60
use crate::KeywordEncodable;
61
use crate::parse::tokenize::tag_keywords_ok;
62
use crate::types::misc::Iso8601TimeSp;
63

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

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

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

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

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

            
140
/// Encode one or more whole (unsigned) network documents into a `String`
141
///
142
/// To encode just one document, write `encode_netdoc_unsigned([&doc])`.
143
74
pub fn encode_netdoc_unsigned<'d, 'i, D, I>(docs: I) -> Result<String, Bug>
144
74
where
145
74
    D: NetdocEncodable + 'd,
146
74
    I: IntoIterator<Item = &'d D> + 'i,
147
{
148
74
    let mut encoder = NetdocEncoder::new();
149
86
    for doc in docs {
150
86
        doc.encode_unsigned(&mut encoder)?;
151
    }
152
74
    encoder.finish()
153
74
}
154

            
155
/// Encode a collection of fields (a document without an intro item) into a `String`
156
///
157
/// Does not support multiple document inputs, because unlike [`NetdocEncodable`],
158
/// document texts for [`NetdocEncodableFields`] can't be concatenated
159
/// to make multiple documents, because there aren't any intro items to use as boundaries.
160
pub fn encode_netdoc_fields<D: NetdocEncodableFields>(doc: &D) -> Result<String, Bug> {
161
    let mut encoder = NetdocEncoder::new();
162
    doc.encode_fields(&mut encoder)?;
163
    encoder.finish()
164
}
165

            
166
impl NetdocEncoder {
167
    /// Start encoding a document
168
13188
    pub fn new() -> Self {
169
13188
        NetdocEncoder {
170
13188
            built: Ok(String::new()),
171
13188
        }
172
13188
    }
173

            
174
    /// Adds an item to the being-built document
175
    ///
176
    /// The item can be further extended with arguments or an object,
177
    /// using the returned `ItemEncoder`.
178
80442
    pub fn item(&mut self, keyword: impl KeywordEncodable) -> ItemEncoder {
179
80442
        self.raw(&keyword.to_str());
180
80442
        ItemEncoder { doc: self }
181
80442
    }
182

            
183
    /// Internal name for `push_raw_string()`
184
354824
    fn raw(&mut self, s: &dyn Display) {
185
366384
        self.write_with(|b| {
186
354824
            write!(b, "{}", s).expect("write! failed on String");
187
354824
            Ok(())
188
354824
        });
189
354824
    }
190

            
191
    /// Extend the being-built document with a fallible function `f`
192
    ///
193
    /// Doesn't call `f` if the building has already failed,
194
    /// and handles the error if `f` fails.
195
363896
    fn write_with(&mut self, f: impl FnOnce(&mut String) -> Result<(), Bug>) {
196
363896
        let Ok(build) = &mut self.built else {
197
            return;
198
        };
199
363896
        match f(build) {
200
363896
            Ok(()) => (),
201
            Err(e) => {
202
                self.built = Err(e);
203
            }
204
        }
205
363896
    }
206

            
207
    /// Adds raw text to the being-built document
208
    ///
209
    /// `s` is added as raw text, after the newline ending the previous item.
210
    /// If `item` is subsequently called, the start of that item
211
    /// will immediately follow `s`.
212
    ///
213
    /// It is the responsibility of the caller to obey the metadocument syntax.
214
    /// In particular, `s` should end with a newline.
215
    /// No checks are performed.
216
    /// Incorrect use might lead to malformed documents, or later errors.
217
92
    pub fn push_raw_string(&mut self, s: &dyn Display) {
218
92
        self.raw(s);
219
92
    }
220

            
221
    /// Return a cursor, pointing to just after the last item (if any)
222
7652
    pub fn cursor(&self) -> Cursor {
223
7652
        let offset = match &self.built {
224
7652
            Ok(b) => b.len(),
225
            Err(_) => usize::MAX,
226
        };
227
7652
        Cursor { offset }
228
7652
    }
229

            
230
    /// Obtain the text of a section of the document
231
    ///
232
    /// Useful for making a signature.
233
3826
    pub fn slice(&self, begin: Cursor, end: Cursor) -> Result<&str, Bug> {
234
3826
        self.built
235
3826
            .as_ref()
236
3826
            .map_err(Clone::clone)?
237
3826
            .get(begin.offset..end.offset)
238
3826
            .ok_or_else(|| internal!("NetdocEncoder::slice out of bounds, Cursor mismanaged"))
239
3826
    }
240

            
241
    /// Obtain the document so far in textual form
242
56
    pub fn text_sofar(&self) -> Result<&str, Bug> {
243
56
        self.built.as_deref().map_err(Clone::clone)
244
56
    }
245

            
246
    /// Build the document into textual form
247
13176
    pub fn finish(self) -> Result<String, Bug> {
248
13176
        self.built
249
13176
    }
250
}
251

            
252
impl Default for NetdocEncoder {
253
34
    fn default() -> Self {
254
        // We must open-code this because the actual encoder contains Result, which isn't Default
255
34
        NetdocEncoder::new()
256
34
    }
257
}
258

            
259
impl<T: crate::NormalItemArgument + Display> ItemArgument for T {
260
83248
    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
261
83248
        (*self.to_string()).write_arg_onto(out)
262
83248
    }
263
}
264

            
265
impl<'n> ItemEncoder<'n> {
266
    /// Add a single argument.
267
    ///
268
    /// Convenience method that defers error handling, for use in infallible contexts.
269
    /// Consider whether to use `ItemArgument::write_arg_onto` directly, instead.
270
    ///
271
    /// If the argument is not in the correct syntax, a `Bug`
272
    /// error will be reported (later).
273
    //
274
    // This is not a hot path.  `dyn` for smaller code size.
275
104028
    pub fn arg(mut self, arg: &dyn ItemArgument) -> Self {
276
104028
        self.add_arg(arg);
277
104028
        self
278
104028
    }
279

            
280
    /// Add a single argument, to a borrowed `ItemEncoder`
281
    ///
282
    /// If the argument is not in the correct syntax, a `Bug`
283
    /// error will be reported (later).
284
    //
285
    // Needed for implementing `ItemArgument`
286
104038
    pub(crate) fn add_arg(&mut self, arg: &dyn ItemArgument) {
287
104038
        let () = arg
288
104038
            .write_arg_onto(self)
289
104038
            .unwrap_or_else(|err| self.doc.built = Err(err));
290
104038
    }
291

            
292
    /// Add zero or more arguments, supplied as a single string.
293
    ///
294
    /// `args` should zero or more valid argument strings,
295
    /// separated by (single) spaces.
296
    /// This is not (properly) checked.
297
    /// Incorrect use might lead to malformed documents, or later errors.
298
5748
    pub fn args_raw_string(&mut self, args: &dyn Display) {
299
5748
        let args = args.to_string();
300
5748
        if !args.is_empty() {
301
5710
            self.args_raw_nonempty(&args);
302
5710
        }
303
5748
    }
304

            
305
    /// Add one or more arguments, supplied as a single string, without any checking
306
120408
    fn args_raw_nonempty(&mut self, args: &dyn Display) {
307
120408
        self.doc.raw(&format_args!(" {}", args));
308
120408
    }
309

            
310
    /// Add an `ItemObjectEncodable` to the item
311
    //
312
    // Note that the `ItemValueEncodable` derive macro (in `derive.rs`)
313
    // also implements this functionality.
314
138
    pub fn object(self, object: &dyn ItemObjectEncodable) {
315
138
        let label = object.label();
316
138
        let mut buf = vec![];
317
138
        object
318
138
            .write_object_onto(&mut buf)
319
138
            .unwrap_or_else(|err| self.doc.built = Err(err));
320
138
        self.object_bytes(label, buf);
321
138
    }
322

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

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

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

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

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

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

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

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

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

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

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

            
448
/// implement [`ItemValueEncodable`] for a particular tuple size
449
macro_rules! item_value_encodable_for_tuple {
450
    { $($i:literal)* } => { paste! {
451
        impl< $( [<T$i>]: ItemArgument, )* > ItemValueEncodable for ( $( [<T$i>], )* ) {
452
142
            fn write_item_value_onto(
453
142
                &self,
454
142
                #[allow(unused)]
455
142
                mut out: ItemEncoder,
456
142
            ) -> Result<(), Bug> {
457
                $(
458
74
                    <[<T$i>] as ItemArgument>::write_arg_onto(&self.$i, &mut out)?;
459
                )*
460
142
                Ok(())
461
142
            }
462
        }
463
    } }
464
}
465

            
466
item_value_encodable_for_tuple! {}
467
item_value_encodable_for_tuple! { 0 }
468
item_value_encodable_for_tuple! { 0 1 }
469
item_value_encodable_for_tuple! { 0 1 2 }
470
item_value_encodable_for_tuple! { 0 1 2 3 }
471
item_value_encodable_for_tuple! { 0 1 2 3 4 }
472
item_value_encodable_for_tuple! { 0 1 2 3 4 5 }
473
item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 }
474
item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 }
475
item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 8 }
476
item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 8 9 }
477

            
478
#[cfg(test)]
479
mod test {
480
    // @@ begin test lint list maintained by maint/add_warning @@
481
    #![allow(clippy::bool_assert_comparison)]
482
    #![allow(clippy::clone_on_copy)]
483
    #![allow(clippy::dbg_macro)]
484
    #![allow(clippy::mixed_attributes_style)]
485
    #![allow(clippy::print_stderr)]
486
    #![allow(clippy::print_stdout)]
487
    #![allow(clippy::single_char_pattern)]
488
    #![allow(clippy::unwrap_used)]
489
    #![allow(clippy::unchecked_time_subtraction)]
490
    #![allow(clippy::useless_vec)]
491
    #![allow(clippy::needless_pass_by_value)]
492
    #![allow(clippy::string_slice)] // See arti#2571
493
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
494
    use super::*;
495
    use std::str::FromStr;
496

            
497
    use crate::types::misc::Iso8601TimeNoSp;
498
    use base64ct::{Base64Unpadded, Encoding};
499

            
500
    #[test]
501
    fn time_formats_as_args() {
502
        use crate::doc::authcert::AuthCertKwd as ACK;
503
        use crate::doc::netstatus::NetstatusKwd as NK;
504

            
505
        let t_sp = Iso8601TimeSp::from_str("2020-04-18 08:36:57").unwrap();
506
        let t_no_sp = Iso8601TimeNoSp::from_str("2021-04-18T08:36:57").unwrap();
507

            
508
        let mut encode = NetdocEncoder::new();
509
        encode.item(ACK::DIR_KEY_EXPIRES).arg(&t_sp);
510
        encode
511
            .item(NK::SHARED_RAND_PREVIOUS_VALUE)
512
            .arg(&"3")
513
            .arg(&"bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs=")
514
            .arg(&t_no_sp);
515

            
516
        let doc = encode.finish().unwrap();
517
        assert_eq_or_diff!(
518
            doc,
519
            r"dir-key-expires 2020-04-18 08:36:57
520
shared-rand-previous-value 3 bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs= 2021-04-18T08:36:57
521
"
522
        );
523
    }
524

            
525
    #[test]
526
    fn authcert() {
527
        use crate::doc::authcert::AuthCertKwd as ACK;
528
        use crate::doc::authcert::{AuthCert, UncheckedAuthCert};
529

            
530
        // c&p from crates/tor-llcrypto/tests/testvec.rs
531
        let pk_rsa = {
532
            let pem = "
533
MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
534
PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
535
qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE";
536
            Base64Unpadded::decode_vec(&pem.replace('\n', "")).unwrap()
537
        };
538

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

            
563
        let doc = encode.finish().unwrap();
564
        assert_eq_or_diff!(
565
            doc,
566
            r"dir-key-certificate-version 3
567
fingerprint 9367f9781da8eabbf96b691175f0e701b43c602e
568
dir-key-published 2020-04-18 08:36:57
569
dir-key-expires 2021-04-18 08:36:57
570
dir-identity-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-signing-key
577
-----BEGIN RSA PUBLIC KEY-----
578
MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
579
PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
580
qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
581
-----END RSA PUBLIC KEY-----
582
dir-key-crosscert
583
-----BEGIN ID SIGNATURE-----
584
-----END ID SIGNATURE-----
585
dir-key-certification
586
-----BEGIN SIGNATURE-----
587
-----END SIGNATURE-----
588
"
589
        );
590

            
591
        let _: UncheckedAuthCert = AuthCert::parse(&doc).unwrap();
592
    }
593
}