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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
214
    /// Obtain the document so far in textual form
215
2
    pub fn text_sofar(&self) -> Result<&str, Bug> {
216
2
        self.built.as_deref().map_err(Clone::clone)
217
2
    }
218

            
219
    /// Build the document into textual form
220
11516
    pub fn finish(self) -> Result<String, Bug> {
221
11516
        self.built
222
11516
    }
223
}
224

            
225
impl Default for NetdocEncoder {
226
16
    fn default() -> Self {
227
        // We must open-code this because the actual encoder contains Result, which isn't Default
228
16
        NetdocEncoder::new()
229
16
    }
230
}
231

            
232
impl<T: crate::NormalItemArgument> ItemArgument for T {
233
57816
    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
234
57816
        (*self.to_string()).write_arg_onto(out)
235
57816
    }
236
}
237

            
238
impl<'n> ItemEncoder<'n> {
239
    /// Add a single argument.
240
    ///
241
    /// Convenience method that defers error handling, for use in infallible contexts.
242
    /// Consider whether to use `ItemArgument::write_arg_onto` directly, instead.
243
    ///
244
    /// If the argument is not in the correct syntax, a `Bug`
245
    /// error will be reported (later).
246
    //
247
    // This is not a hot path.  `dyn` for smaller code size.
248
95676
    pub fn arg(mut self, arg: &dyn ItemArgument) -> Self {
249
95676
        self.add_arg(arg);
250
95676
        self
251
95676
    }
252

            
253
    /// Add a single argument, to a borrowed `ItemEncoder`
254
    ///
255
    /// If the argument is not in the correct syntax, a `Bug`
256
    /// error will be reported (later).
257
    //
258
    // Needed for implementing `ItemArgument`
259
95682
    pub(crate) fn add_arg(&mut self, arg: &dyn ItemArgument) {
260
95682
        let () = arg
261
95682
            .write_arg_onto(self)
262
95682
            .unwrap_or_else(|err| self.doc.built = Err(err));
263
95682
    }
264

            
265
    /// Add zero or more arguments, supplied as a single string.
266
    ///
267
    /// `args` should zero or more valid argument strings,
268
    /// separated by (single) spaces.
269
    /// This is not (properly) checked.
270
    /// Incorrect use might lead to malformed documents, or later errors.
271
16
    pub fn args_raw_string(&mut self, args: &dyn Display) {
272
16
        let args = args.to_string();
273
16
        if !args.is_empty() {
274
16
            self.args_raw_nonempty(&args);
275
16
        }
276
16
    }
277

            
278
    /// Add one or more arguments, supplied as a single string, without any checking
279
95808
    fn args_raw_nonempty(&mut self, args: &dyn Display) {
280
95808
        self.doc.raw(&format_args!(" {}", args));
281
95808
    }
282

            
283
    /// Add an `ItemObjectEncodable` to the item
284
    //
285
    // Note that the `ItemValueEncodable` derive macro (in `derive.rs`)
286
    // also implements this functionality.
287
4
    pub fn object(self, object: &dyn ItemObjectEncodable) {
288
4
        let label = object.label();
289
4
        let mut buf = vec![];
290
4
        object
291
4
            .write_object_onto(&mut buf)
292
4
            .unwrap_or_else(|err| self.doc.built = Err(err));
293
4
        self.object_bytes(label, buf);
294
4
    }
295

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

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

            
337
    /// Finish encoding this item
338
    ///
339
    /// The item will also automatically be finished if the `ItemEncoder` is dropped.
340
46
    pub fn finish(self) {}
341
}
342

            
343
impl Drop for ItemEncoder<'_> {
344
99666
    fn drop(&mut self) {
345
99666
        self.doc.raw(&'\n');
346
99666
    }
347
}
348

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

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

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

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

            
389
/// An Object value that be encoded into a netdoc
390
pub trait ItemObjectEncodable {
391
    /// The label (keyword(s) in `BEGIN` and `END`)
392
    fn label(&self) -> &str;
393

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

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

            
421
/// implement [`ItemValueEncodable`] for a particular tuple size
422
macro_rules! item_value_encodable_for_tuple {
423
    { $($i:literal)* } => { paste! {
424
        impl< $( [<T$i>]: ItemArgument, )* > ItemValueEncodable for ( $( [<T$i>], )* ) {
425
80
            fn write_item_value_onto(
426
80
                &self,
427
80
                #[allow(unused)]
428
80
                mut out: ItemEncoder,
429
80
            ) -> Result<(), Bug> {
430
                $(
431
48
                    <[<T$i>] as ItemArgument>::write_arg_onto(&self.$i, &mut out)?;
432
                )*
433
80
                Ok(())
434
80
            }
435
        }
436
    } }
437
}
438

            
439
item_value_encodable_for_tuple! {}
440
item_value_encodable_for_tuple! { 0 }
441
item_value_encodable_for_tuple! { 0 1 }
442
item_value_encodable_for_tuple! { 0 1 2 }
443
item_value_encodable_for_tuple! { 0 1 2 3 }
444
item_value_encodable_for_tuple! { 0 1 2 3 4 }
445
item_value_encodable_for_tuple! { 0 1 2 3 4 5 }
446
item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 }
447
item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 }
448
item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 8 }
449
item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 8 9 }
450

            
451
#[cfg(test)]
452
mod test {
453
    // @@ begin test lint list maintained by maint/add_warning @@
454
    #![allow(clippy::bool_assert_comparison)]
455
    #![allow(clippy::clone_on_copy)]
456
    #![allow(clippy::dbg_macro)]
457
    #![allow(clippy::mixed_attributes_style)]
458
    #![allow(clippy::print_stderr)]
459
    #![allow(clippy::print_stdout)]
460
    #![allow(clippy::single_char_pattern)]
461
    #![allow(clippy::unwrap_used)]
462
    #![allow(clippy::unchecked_time_subtraction)]
463
    #![allow(clippy::useless_vec)]
464
    #![allow(clippy::needless_pass_by_value)]
465
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
466
    use super::*;
467
    use std::str::FromStr;
468

            
469
    use crate::types::misc::Iso8601TimeNoSp;
470
    use base64ct::{Base64Unpadded, Encoding};
471

            
472
    #[test]
473
    fn time_formats_as_args() {
474
        use crate::doc::authcert::AuthCertKwd as ACK;
475
        use crate::doc::netstatus::NetstatusKwd as NK;
476

            
477
        let t_sp = Iso8601TimeSp::from_str("2020-04-18 08:36:57").unwrap();
478
        let t_no_sp = Iso8601TimeNoSp::from_str("2021-04-18T08:36:57").unwrap();
479

            
480
        let mut encode = NetdocEncoder::new();
481
        encode.item(ACK::DIR_KEY_EXPIRES).arg(&t_sp);
482
        encode
483
            .item(NK::SHARED_RAND_PREVIOUS_VALUE)
484
            .arg(&"3")
485
            .arg(&"bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs=")
486
            .arg(&t_no_sp);
487

            
488
        let doc = encode.finish().unwrap();
489
        println!("{}", doc);
490
        assert_eq!(
491
            doc,
492
            r"dir-key-expires 2020-04-18 08:36:57
493
shared-rand-previous-value 3 bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs= 2021-04-18T08:36:57
494
"
495
        );
496
    }
497

            
498
    #[test]
499
    fn authcert() {
500
        use crate::doc::authcert::AuthCertKwd as ACK;
501
        use crate::doc::authcert::{AuthCert, UncheckedAuthCert};
502

            
503
        // c&p from crates/tor-llcrypto/tests/testvec.rs
504
        let pk_rsa = {
505
            let pem = "
506
MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
507
PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
508
qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE";
509
            Base64Unpadded::decode_vec(&pem.replace('\n', "")).unwrap()
510
        };
511

            
512
        let mut encode = NetdocEncoder::new();
513
        encode.item(ACK::DIR_KEY_CERTIFICATE_VERSION).arg(&3);
514
        encode
515
            .item(ACK::FINGERPRINT)
516
            .arg(&"9367f9781da8eabbf96b691175f0e701b43c602e");
517
        encode
518
            .item(ACK::DIR_KEY_PUBLISHED)
519
            .arg(&Iso8601TimeSp::from_str("2020-04-18 08:36:57").unwrap());
520
        encode
521
            .item(ACK::DIR_KEY_EXPIRES)
522
            .arg(&Iso8601TimeSp::from_str("2021-04-18 08:36:57").unwrap());
523
        encode
524
            .item(ACK::DIR_IDENTITY_KEY)
525
            .object_bytes("RSA PUBLIC KEY", &*pk_rsa);
526
        encode
527
            .item(ACK::DIR_SIGNING_KEY)
528
            .object_bytes("RSA PUBLIC KEY", &*pk_rsa);
529
        encode
530
            .item(ACK::DIR_KEY_CROSSCERT)
531
            .object_bytes("ID SIGNATURE", []);
532
        encode
533
            .item(ACK::DIR_KEY_CERTIFICATION)
534
            .object_bytes("SIGNATURE", []);
535

            
536
        let doc = encode.finish().unwrap();
537
        eprintln!("{}", doc);
538
        assert_eq!(
539
            doc,
540
            r"dir-key-certificate-version 3
541
fingerprint 9367f9781da8eabbf96b691175f0e701b43c602e
542
dir-key-published 2020-04-18 08:36:57
543
dir-key-expires 2021-04-18 08:36:57
544
dir-identity-key
545
-----BEGIN RSA PUBLIC KEY-----
546
MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
547
PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
548
qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
549
-----END RSA PUBLIC KEY-----
550
dir-signing-key
551
-----BEGIN RSA PUBLIC KEY-----
552
MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
553
PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
554
qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
555
-----END RSA PUBLIC KEY-----
556
dir-key-crosscert
557
-----BEGIN ID SIGNATURE-----
558
-----END ID SIGNATURE-----
559
dir-key-certification
560
-----BEGIN SIGNATURE-----
561
-----END SIGNATURE-----
562
"
563
        );
564

            
565
        let _: UncheckedAuthCert = AuthCert::parse(&doc).unwrap();
566
    }
567
}