1
//! Building support for the network document meta-format
2
//!
3
//! Implements building 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
use std::fmt::{Display, Write};
17

            
18
use base64ct::{Base64, Base64Unpadded, Encoding};
19
use rand::{CryptoRng, RngCore};
20
use tor_bytes::EncodeError;
21
use tor_error::{internal, Bug};
22

            
23
use crate::parse::keyword::Keyword;
24
use crate::parse::tokenize::tag_keywords_ok;
25
use crate::types::misc::{Iso8601TimeNoSp, Iso8601TimeSp};
26

            
27
/// Encoder, representing a partially-built document.
28
///
29
/// For example usage, see the tests in this module, or a descriptor building
30
/// function in tor-netdoc (such as `hsdesc::build::inner::HsDescInner::build_sign`).
31
#[derive(Debug, Clone)]
32
pub(crate) struct NetdocEncoder {
33
    /// The being-built document, with everything accumulated so far
34
    ///
35
    /// If an [`ItemEncoder`] exists, it will add a newline when it's dropped.
36
    ///
37
    /// `Err` means bad values passed to some builder function
38
    built: Result<String, Bug>,
39
}
40

            
41
/// Encoder for an individual item within a being-built document
42
///
43
/// Returned by [`NetdocEncoder::item()`].
44
#[derive(Debug)]
45
pub(crate) struct ItemEncoder<'n> {
46
    /// The document including the partial item that we're building
47
    ///
48
    /// We will always add a newline when we're dropped
49
    doc: &'n mut NetdocEncoder,
50
}
51

            
52
/// Position within a (perhaps partially-) built document
53
///
54
/// This is provided mainly to allow the caller to perform signature operations
55
/// on the part of the document that is to be signed.
56
/// (Sometimes this is only part of it.)
57
///
58
/// There is no enforced linkage between this and the document it refers to.
59
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
60
pub(crate) struct Cursor {
61
    /// The offset (in bytes, as for `&str`)
62
    ///
63
    /// Can be out of range if the corresponding `NetdocEncoder` is contains an `Err`.
64
    offset: usize,
65
}
66

            
67
/// Types that can be added as argument(s) to item keyword lines
68
///
69
/// Implemented for strings, and various other types.
70
///
71
/// This is a separate trait so we can control the formatting of (eg) [`Iso8601TimeSp`],
72
/// without having a method on `ItemEncoder` for each argument type.
73
pub(crate) trait ItemArgument {
74
    /// Format as a string suitable for including as a netdoc keyword line argument
75
    ///
76
    /// The implementation is responsible for checking that the syntax is legal.
77
    /// For example, if `self` is a string, it must check that the string is
78
    /// in legal as a single argument.
79
    ///
80
    /// Some netdoc values (eg times) turn into several arguments; in that case,
81
    /// one `ItemArgument` may format into multiple arguments, and this method
82
    /// is responsible for writing them all, with the necessary spaces.
83
    fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug>;
84
}
85

            
86
impl NetdocEncoder {
87
    /// Start encoding a document
88
11328
    pub(crate) fn new() -> Self {
89
11328
        NetdocEncoder {
90
11328
            built: Ok(String::new()),
91
11328
        }
92
11328
    }
93

            
94
    /// Adds an item to the being-built document
95
    ///
96
    /// The item can be further extended with arguments or an object,
97
    /// using the returned `ItemEncoder`.
98
62014
    pub(crate) fn item(&mut self, keyword: impl Keyword) -> ItemEncoder {
99
62014
        self.raw(&keyword.to_str());
100
62014
        ItemEncoder { doc: self }
101
62014
    }
102

            
103
    /// Internal name for `push_raw_string()`
104
290284
    fn raw(&mut self, s: &dyn Display) {
105
296826
        self.write_with(|b| {
106
290284
            write!(b, "{}", s).expect("write! failed on String");
107
290284
            Ok(())
108
296826
        });
109
290284
    }
110

            
111
    /// Extend the being-built document with a fallible function `f`
112
    ///
113
    /// Doesn't call `f` if the building has already failed,
114
    /// and handles the error if `f` fails.
115
291800
    fn write_with(&mut self, f: impl FnOnce(&mut String) -> Result<(), Bug>) {
116
291800
        let Ok(build) = &mut self.built else {
117
            return;
118
        };
119
291800
        match f(build) {
120
291800
            Ok(()) => (),
121
            Err(e) => {
122
                self.built = Err(e);
123
            }
124
        }
125
291800
    }
126

            
127
    /// Adds raw text to the being-built document
128
    ///
129
    /// `s` is added as raw text, after the newline ending the previous item.
130
    /// If `item` is subsequently called, the start of that item
131
    /// will immediately follow `s`.
132
    ///
133
    /// It is the responsibility of the caller to obey the metadocument syntax.
134
    /// In particular, `s` should end with a newline.
135
    /// No checks are performed.
136
    /// Incorrect use might lead to malformed documents, or later errors.
137
    #[allow(dead_code)] // TODO: We should remove this if it never used.
138
    pub(crate) fn push_raw_string(&mut self, s: &dyn Display) {
139
        self.raw(s);
140
    }
141

            
142
    /// Return a cursor, pointing to just after the last item (if any)
143
7540
    pub(crate) fn cursor(&self) -> Cursor {
144
7540
        let offset = match &self.built {
145
7540
            Ok(b) => b.len(),
146
            Err(_) => usize::MAX,
147
        };
148
7540
        Cursor { offset }
149
7540
    }
150

            
151
    /// Obtain the text of a section of the document
152
    ///
153
    /// Useful for making a signature.
154
3770
    pub(crate) fn slice(&self, begin: Cursor, end: Cursor) -> Result<&str, Bug> {
155
3770
        self.built
156
3770
            .as_ref()
157
3770
            .map_err(Clone::clone)?
158
3770
            .get(begin.offset..end.offset)
159
3770
            .ok_or_else(|| internal!("NetdocEncoder::slice out of bounds, Cursor mismanaged"))
160
3770
    }
161

            
162
    /// Build the document into textual form
163
11324
    pub(crate) fn finish(self) -> Result<String, Bug> {
164
11324
        self.built
165
11324
    }
166
}
167

            
168
impl ItemArgument for str {
169
94250
    fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
170
94250
        // Implements this
171
94250
        // https://gitlab.torproject.org/tpo/core/torspec/-/merge_requests/106
172
2865929
        if self.is_empty() || self.chars().any(|c| !c.is_ascii_graphic()) {
173
            return Err(internal!("invalid keyword argument syntax {:?}", self));
174
94250
        }
175
94250
        out.args_raw_nonempty(&self);
176
94250
        Ok(())
177
94250
    }
178
}
179

            
180
impl ItemArgument for String {
181
56534
    fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
182
56534
        ItemArgument::write_onto(&self.as_str(), out)
183
56534
    }
184
}
185

            
186
impl<T: ItemArgument + ?Sized> ItemArgument for &'_ T {
187
94250
    fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
188
94250
        <T as ItemArgument>::write_onto(self, out)
189
94250
    }
190
}
191

            
192
/// Implement [`ItemArgument`] for `$ty` in terms of `<$ty as Display>`
193
///
194
/// Checks that the syntax is acceptable.
195
macro_rules! impl_item_argument_as_display { { $( $ty:ty $(,)? )* } => { $(
196
    impl ItemArgument for $ty {
197
7570
        fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
198
7570
            let arg = self.to_string();
199
7570
            out.add_arg(&arg.as_str());
200
7570
            Ok(())
201
7570
        }
202
    }
203
)* } }
204

            
205
impl_item_argument_as_display! { usize, u8, u16, u32, u64, u128 }
206
impl_item_argument_as_display! { isize, i8, i16, i32, i64, i128 }
207
// TODO: should we implement ItemArgument for, say, tor_llcrypto::pk::rsa::RsaIdentity ?
208
// It's not clear whether it's always formatted the same way in all parts of the spec.
209
// The Display impl of RsaIdentity adds a `$` which is not supposed to be present
210
// in (for example) an authority certificate (authcert)'s "fingerprint" line.
211

            
212
impl_item_argument_as_display! {Iso8601TimeNoSp}
213
impl ItemArgument for Iso8601TimeSp {
214
    // Unlike the macro'd formats, contains a space while still being one argument
215
6
    fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
216
6
        let arg = self.to_string();
217
6
        out.args_raw_nonempty(&arg.as_str());
218
6
        Ok(())
219
6
    }
220
}
221

            
222
#[cfg(feature = "hs-pow-full")]
223
impl ItemArgument for tor_hscrypto::pow::v1::Seed {
224
2
    fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
225
2
        let mut seed_bytes = vec![];
226
2
        tor_bytes::Writer::write(&mut seed_bytes, &self)?;
227
2
        out.add_arg(&Base64Unpadded::encode_string(&seed_bytes));
228
2
        Ok(())
229
2
    }
230
}
231

            
232
#[cfg(feature = "hs-pow-full")]
233
impl ItemArgument for tor_hscrypto::pow::v1::Effort {
234
2
    fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
235
2
        out.add_arg(&<Self as Into<u32>>::into(*self));
236
2
        Ok(())
237
2
    }
238
}
239

            
240
impl<'n> ItemEncoder<'n> {
241
    /// Add a single argument.
242
    ///
243
    /// If the argument is not in the correct syntax, a `Bug`
244
    /// error will be reported (later).
245
    //
246
    // This is not a hot path.  `dyn` for smaller code size.
247
94248
    pub(crate) fn arg(mut self, arg: &dyn ItemArgument) -> Self {
248
94248
        self.add_arg(arg);
249
94248
        self
250
94248
    }
251

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

            
264
    /// Add zero or more arguments, supplied as a single string.
265
    ///
266
    /// `args` should zero or more valid argument strings,
267
    /// separated by (single) spaces.
268
    /// This is not (properly) checked.
269
    /// Incorrect use might lead to malformed documents, or later errors.
270
    #[allow(unused)] // TODO: We should eventually remove this if nothing starts to use it.
271
    pub(crate) fn args_raw_string(mut self, args: &dyn Display) -> Self {
272
        let args = args.to_string();
273
        if !args.is_empty() {
274
            self.args_raw_nonempty(&args);
275
        }
276
        self
277
    }
278

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

            
284
    /// Add an object to the item
285
    ///
286
    /// Checks that `keywords` is in the correct syntax.
287
    /// Doesn't check that it makes semantic sense for the position of the document.
288
    /// `data` will be PEM (base64) encoded.
289
    //
290
    // If keyword is not in the correct syntax, a `Bug` is stored in self.doc.
291
1516
    pub(crate) fn object(
292
1516
        self,
293
1516
        keywords: &str,
294
1516
        // Writeable isn't dyn-compatible
295
1516
        data: impl tor_bytes::WriteableOnce,
296
1516
    ) {
297
        use crate::parse::tokenize::object::*;
298

            
299
1516
        self.doc.write_with(|out| {
300
1516
            if keywords.is_empty() || !tag_keywords_ok(keywords) {
301
                return Err(internal!("bad object keywords string {:?}", keywords));
302
1516
            }
303
1516
            let data = {
304
1516
                let mut bytes = vec![];
305
1516
                data.write_into(&mut bytes)?;
306
1516
                Base64::encode_string(&bytes)
307
1516
            };
308
1516
            let mut data = &data[..];
309
1516
            writeln!(out, "\n{BEGIN_STR}{keywords}{TAG_END}").expect("write!");
310
48960
            while !data.is_empty() {
311
47444
                let (l, r) = if data.len() > BASE64_PEM_MAX_LINE {
312
45932
                    data.split_at(BASE64_PEM_MAX_LINE)
313
                } else {
314
1512
                    (data, "")
315
                };
316
47444
                writeln!(out, "{l}").expect("write!");
317
47444
                data = r;
318
            }
319
            // final newline will be written by Drop impl
320
1516
            write!(out, "{END_STR}{keywords}{TAG_END}").expect("write!");
321
1516
            Ok(())
322
1516
        });
323
1516
    }
324
}
325

            
326
impl Drop for ItemEncoder<'_> {
327
98014
    fn drop(&mut self) {
328
98014
        self.doc.raw(&'\n');
329
98014
    }
330
}
331

            
332
/// A trait for building and signing netdocs.
333
pub trait NetdocBuilder {
334
    /// Build the document into textual form.
335
    fn build_sign<R: RngCore + CryptoRng>(self, rng: &mut R) -> Result<String, EncodeError>;
336
}
337

            
338
#[cfg(test)]
339
mod test {
340
    // @@ begin test lint list maintained by maint/add_warning @@
341
    #![allow(clippy::bool_assert_comparison)]
342
    #![allow(clippy::clone_on_copy)]
343
    #![allow(clippy::dbg_macro)]
344
    #![allow(clippy::mixed_attributes_style)]
345
    #![allow(clippy::print_stderr)]
346
    #![allow(clippy::print_stdout)]
347
    #![allow(clippy::single_char_pattern)]
348
    #![allow(clippy::unwrap_used)]
349
    #![allow(clippy::unchecked_duration_subtraction)]
350
    #![allow(clippy::useless_vec)]
351
    #![allow(clippy::needless_pass_by_value)]
352
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
353
    use super::*;
354
    use std::str::FromStr;
355

            
356
    use base64ct::{Base64Unpadded, Encoding};
357

            
358
    #[test]
359
    fn time_formats_as_args() {
360
        use crate::doc::authcert::AuthCertKwd as ACK;
361
        use crate::doc::netstatus::NetstatusKwd as NK;
362

            
363
        let t_sp = Iso8601TimeSp::from_str("2020-04-18 08:36:57").unwrap();
364
        let t_no_sp = Iso8601TimeNoSp::from_str("2021-04-18T08:36:57").unwrap();
365

            
366
        let mut encode = NetdocEncoder::new();
367
        encode.item(ACK::DIR_KEY_EXPIRES).arg(&t_sp);
368
        encode
369
            .item(NK::SHARED_RAND_PREVIOUS_VALUE)
370
            .arg(&"3")
371
            .arg(&"bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs=")
372
            .arg(&t_no_sp);
373

            
374
        let doc = encode.finish().unwrap();
375
        println!("{}", doc);
376
        assert_eq!(
377
            doc,
378
            r"dir-key-expires 2020-04-18 08:36:57
379
shared-rand-previous-value 3 bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs= 2021-04-18T08:36:57
380
"
381
        );
382
    }
383

            
384
    #[test]
385
    fn authcert() {
386
        use crate::doc::authcert::AuthCertKwd as ACK;
387
        use crate::doc::authcert::{AuthCert, UncheckedAuthCert};
388

            
389
        // c&p from crates/tor-llcrypto/tests/testvec.rs
390
        let pk_rsa = {
391
            let pem = "
392
MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
393
PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
394
qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE";
395
            Base64Unpadded::decode_vec(&pem.replace('\n', "")).unwrap()
396
        };
397

            
398
        let mut encode = NetdocEncoder::new();
399
        encode.item(ACK::DIR_KEY_CERTIFICATE_VERSION).arg(&3);
400
        encode
401
            .item(ACK::FINGERPRINT)
402
            .arg(&"9367f9781da8eabbf96b691175f0e701b43c602e");
403
        encode
404
            .item(ACK::DIR_KEY_PUBLISHED)
405
            .arg(&Iso8601TimeSp::from_str("2020-04-18 08:36:57").unwrap());
406
        encode
407
            .item(ACK::DIR_KEY_EXPIRES)
408
            .arg(&Iso8601TimeSp::from_str("2021-04-18 08:36:57").unwrap());
409
        encode
410
            .item(ACK::DIR_IDENTITY_KEY)
411
            .object("RSA PUBLIC KEY", &*pk_rsa);
412
        encode
413
            .item(ACK::DIR_SIGNING_KEY)
414
            .object("RSA PUBLIC KEY", &*pk_rsa);
415
        encode
416
            .item(ACK::DIR_KEY_CROSSCERT)
417
            .object("ID SIGNATURE", []);
418
        encode
419
            .item(ACK::DIR_KEY_CERTIFICATION)
420
            .object("SIGNATURE", []);
421

            
422
        let doc = encode.finish().unwrap();
423
        eprintln!("{}", doc);
424
        assert_eq!(
425
            doc,
426
            r"dir-key-certificate-version 3
427
fingerprint 9367f9781da8eabbf96b691175f0e701b43c602e
428
dir-key-published 2020-04-18 08:36:57
429
dir-key-expires 2021-04-18 08:36:57
430
dir-identity-key
431
-----BEGIN RSA PUBLIC KEY-----
432
MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
433
PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
434
qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
435
-----END RSA PUBLIC KEY-----
436
dir-signing-key
437
-----BEGIN RSA PUBLIC KEY-----
438
MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
439
PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
440
qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
441
-----END RSA PUBLIC KEY-----
442
dir-key-crosscert
443
-----BEGIN ID SIGNATURE-----
444
-----END ID SIGNATURE-----
445
dir-key-certification
446
-----BEGIN SIGNATURE-----
447
-----END SIGNATURE-----
448
"
449
        );
450

            
451
        let _: UncheckedAuthCert = AuthCert::parse(&doc).unwrap();
452
    }
453
}