tor_netdoc/
encode.rs

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
16mod multiplicity;
17#[macro_use]
18mod derive;
19
20use std::cmp;
21use std::collections::BTreeSet;
22use std::fmt::Write;
23use std::iter;
24use std::marker::PhantomData;
25
26use base64ct::{Base64, Base64Unpadded, Encoding};
27use educe::Educe;
28use itertools::Itertools;
29use paste::paste;
30use rand::{CryptoRng, RngCore};
31use tor_bytes::EncodeError;
32use tor_error::internal;
33use void::Void;
34
35use crate::KeywordEncodable;
36use crate::parse::tokenize::tag_keywords_ok;
37use crate::types::misc::Iso8601TimeSp;
38
39// Exports used by macros, which treat this module as a prelude
40#[doc(hidden)]
41pub 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)]
57pub 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)]
71pub 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)]
86pub 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.
101pub 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
114impl NetdocEncoder {
115    /// Start encoding a document
116    pub fn new() -> Self {
117        NetdocEncoder {
118            built: Ok(String::new()),
119        }
120    }
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    pub fn item(&mut self, keyword: impl KeywordEncodable) -> ItemEncoder {
127        self.raw(&keyword.to_str());
128        ItemEncoder { doc: self }
129    }
130
131    /// Internal name for `push_raw_string()`
132    fn raw(&mut self, s: &dyn Display) {
133        self.write_with(|b| {
134            write!(b, "{}", s).expect("write! failed on String");
135            Ok(())
136        });
137    }
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    fn write_with(&mut self, f: impl FnOnce(&mut String) -> Result<(), Bug>) {
144        let Ok(build) = &mut self.built else {
145            return;
146        };
147        match f(build) {
148            Ok(()) => (),
149            Err(e) => {
150                self.built = Err(e);
151            }
152        }
153    }
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    pub fn cursor(&self) -> Cursor {
171        let offset = match &self.built {
172            Ok(b) => b.len(),
173            Err(_) => usize::MAX,
174        };
175        Cursor { offset }
176    }
177
178    /// Obtain the text of a section of the document
179    ///
180    /// Useful for making a signature.
181    pub fn slice(&self, begin: Cursor, end: Cursor) -> Result<&str, Bug> {
182        self.built
183            .as_ref()
184            .map_err(Clone::clone)?
185            .get(begin.offset..end.offset)
186            .ok_or_else(|| internal!("NetdocEncoder::slice out of bounds, Cursor mismanaged"))
187    }
188
189    /// Build the document into textual form
190    pub fn finish(self) -> Result<String, Bug> {
191        self.built
192    }
193}
194
195impl Default for NetdocEncoder {
196    fn default() -> Self {
197        // We must open-code this because the actual encoder contains Result, which isn't Default
198        NetdocEncoder::new()
199    }
200}
201
202impl ItemArgument for str {
203    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        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        }
212        out.args_raw_nonempty(&self);
213        Ok(())
214    }
215}
216
217impl ItemArgument for &str {
218    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
219        <str as ItemArgument>::write_arg_onto(self, out)
220    }
221}
222
223impl<T: crate::NormalItemArgument> ItemArgument for T {
224    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
225        (*self.to_string()).write_arg_onto(out)
226    }
227}
228
229impl ItemArgument for Iso8601TimeSp {
230    // Unlike the macro'd formats, contains a space while still being one argument
231    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
232        let arg = self.to_string();
233        out.args_raw_nonempty(&arg.as_str());
234        Ok(())
235    }
236}
237
238#[cfg(feature = "hs-pow-full")]
239impl ItemArgument for tor_hscrypto::pow::v1::Seed {
240    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
241        let mut seed_bytes = vec![];
242        tor_bytes::Writer::write(&mut seed_bytes, &self)?;
243        out.add_arg(&Base64Unpadded::encode_string(&seed_bytes));
244        Ok(())
245    }
246}
247
248#[cfg(feature = "hs-pow-full")]
249impl ItemArgument for tor_hscrypto::pow::v1::Effort {
250    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
251        out.add_arg(&<Self as Into<u32>>::into(*self));
252        Ok(())
253    }
254}
255
256impl<'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    pub fn arg(mut self, arg: &dyn ItemArgument) -> Self {
267        self.add_arg(arg);
268        self
269    }
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    pub(crate) fn add_arg(&mut self, arg: &dyn ItemArgument) {
278        let () = arg
279            .write_arg_onto(self)
280            .unwrap_or_else(|err| self.doc.built = Err(err));
281    }
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    pub fn args_raw_string(&mut self, args: &dyn Display) {
290        let args = args.to_string();
291        if !args.is_empty() {
292            self.args_raw_nonempty(&args);
293        }
294    }
295
296    /// Add one or more arguments, supplied as a single string, without any checking
297    fn args_raw_nonempty(&mut self, args: &dyn Display) {
298        self.doc.raw(&format_args!(" {}", args));
299    }
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    pub fn object(
309        self,
310        keywords: &str,
311        // Writeable isn't dyn-compatible
312        data: impl tor_bytes::WriteableOnce,
313    ) {
314        use crate::parse::tokenize::object::*;
315
316        self.doc.write_with(|out| {
317            if keywords.is_empty() || !tag_keywords_ok(keywords) {
318                return Err(internal!("bad object keywords string {:?}", keywords));
319            }
320            let data = {
321                let mut bytes = vec![];
322                data.write_into(&mut bytes)?;
323                Base64::encode_string(&bytes)
324            };
325            let mut data = &data[..];
326            writeln!(out, "\n{BEGIN_STR}{keywords}{TAG_END}").expect("write!");
327            while !data.is_empty() {
328                let (l, r) = if data.len() > BASE64_PEM_MAX_LINE {
329                    data.split_at(BASE64_PEM_MAX_LINE)
330                } else {
331                    (data, "")
332                };
333                writeln!(out, "{l}").expect("write!");
334                data = r;
335            }
336            // final newline will be written by Drop impl
337            write!(out, "{END_STR}{keywords}{TAG_END}").expect("write!");
338            Ok(())
339        });
340    }
341
342    /// Finish encoding this item
343    ///
344    /// The item will also automatically be finished if the `ItemEncoder` is dropped.
345    pub fn finish(self) {}
346}
347
348impl Drop for ItemEncoder<'_> {
349    fn drop(&mut self) {
350        self.doc.raw(&'\n');
351    }
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).
360pub trait EncodeOrd {
361    /// Compare `self` and `other`
362    ///
363    /// As `Ord::cmp`.
364    fn encode_cmp(&self, other: &Self) -> cmp::Ordering;
365}
366impl<T: Ord> EncodeOrd for T {
367    fn encode_cmp(&self, other: &Self) -> cmp::Ordering {
368        self.cmp(other)
369    }
370}
371
372/// Documents (or sub-documents) that can be encoded in the netdoc metaformat
373pub 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.
381pub 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
387pub 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
395pub 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.
421pub 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
426impl ItemValueEncodable for Void {
427    fn write_item_value_onto(&self, _out: ItemEncoder) -> Result<(), Bug> {
428        void::unreachable(*self)
429    }
430}
431
432impl 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
442macro_rules! item_value_encodable_for_tuple {
443    { $($i:literal)* } => { paste! {
444        impl< $( [<T$i>]: ItemArgument, )* > ItemValueEncodable for ( $( [<T$i>], )* ) {
445            fn write_item_value_onto(
446                &self,
447                #[allow(unused)]
448                mut out: ItemEncoder,
449            ) -> Result<(), Bug> {
450                $(
451                    <[<T$i>] as ItemArgument>::write_arg_onto(&self.$i, &mut out)?;
452                )*
453                Ok(())
454            }
455        }
456    } }
457}
458
459item_value_encodable_for_tuple! {}
460item_value_encodable_for_tuple! { 0 }
461item_value_encodable_for_tuple! { 0 1 }
462item_value_encodable_for_tuple! { 0 1 2 }
463item_value_encodable_for_tuple! { 0 1 2 3 }
464item_value_encodable_for_tuple! { 0 1 2 3 4 }
465item_value_encodable_for_tuple! { 0 1 2 3 4 5 }
466item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 }
467item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 }
468item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 8 }
469item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 8 9 }
470
471#[cfg(test)]
472mod 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
513shared-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 = "
526MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
527PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
528qiBHRBGbtkF/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
561fingerprint 9367f9781da8eabbf96b691175f0e701b43c602e
562dir-key-published 2020-04-18 08:36:57
563dir-key-expires 2021-04-18 08:36:57
564dir-identity-key
565-----BEGIN RSA PUBLIC KEY-----
566MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
567PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
568qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
569-----END RSA PUBLIC KEY-----
570dir-signing-key
571-----BEGIN RSA PUBLIC KEY-----
572MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
573PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
574qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
575-----END RSA PUBLIC KEY-----
576dir-key-crosscert
577-----BEGIN ID SIGNATURE-----
578-----END ID SIGNATURE-----
579dir-key-certification
580-----BEGIN SIGNATURE-----
581-----END SIGNATURE-----
582"
583        );
584
585        let _: UncheckedAuthCert = AuthCert::parse(&doc).unwrap();
586    }
587}