Skip to main content

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
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
39mod multiplicity;
40#[macro_use]
41mod derive;
42
43use std::cmp;
44use std::collections::BTreeSet;
45use std::fmt::Write;
46use std::iter;
47use std::marker::PhantomData;
48
49use base64ct::{Base64, Base64Unpadded, Encoding};
50use educe::Educe;
51use itertools::Itertools;
52use paste::paste;
53use rand::{CryptoRng, RngCore};
54use tor_bytes::EncodeError;
55use tor_error::internal;
56use void::Void;
57
58use crate::KeywordEncodable;
59use crate::parse::tokenize::tag_keywords_ok;
60use crate::types::misc::Iso8601TimeSp;
61
62// Exports used by macros, which treat this module as a prelude
63#[doc(hidden)]
64pub use {
65    derive::{DisplayHelper, RestMustComeLastMarker},
66    multiplicity::{
67        MultiplicityMethods, MultiplicitySelector, OptionalityMethods,
68        SingletonMultiplicitySelector,
69    },
70    std::fmt::{self, Display},
71    std::result::Result,
72    tor_error::{Bug, into_internal},
73};
74
75/// Encoder, representing a partially-built document.
76///
77/// For example usage, see the tests in this module, or a descriptor building
78/// function in tor-netdoc (such as `hsdesc::build::inner::HsDescInner::build_sign`).
79#[derive(Debug, Clone)]
80pub struct NetdocEncoder {
81    /// The being-built document, with everything accumulated so far
82    ///
83    /// If an [`ItemEncoder`] exists, it will add a newline when it's dropped.
84    ///
85    /// `Err` means bad values passed to some builder function.
86    /// Such errors are accumulated here for the benefit of handwritten document encoders.
87    built: Result<String, Bug>,
88}
89
90/// Encoder for an individual item within a being-built document
91///
92/// Returned by [`NetdocEncoder::item()`].
93#[derive(Debug)]
94pub struct ItemEncoder<'n> {
95    /// The document including the partial item that we're building
96    ///
97    /// We will always add a newline when we're dropped
98    doc: &'n mut NetdocEncoder,
99}
100
101/// Position within a (perhaps partially-) built document
102///
103/// This is provided mainly to allow the caller to perform signature operations
104/// on the part of the document that is to be signed.
105/// (Sometimes this is only part of it.)
106///
107/// There is no enforced linkage between this and the document it refers to.
108#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
109pub struct Cursor {
110    /// The offset (in bytes, as for `&str`)
111    ///
112    /// Can be out of range if the corresponding `NetdocEncoder` is contains an `Err`.
113    offset: usize,
114}
115
116/// Types that can be added as argument(s) to item keyword lines
117///
118/// Implemented for strings, and various other types.
119///
120/// This is a separate trait so we can control the formatting of (eg) [`Iso8601TimeSp`],
121/// without having a method on `ItemEncoder` for each argument type.
122//
123// TODO consider renaming this to ItemArgumentEncodable to mirror all the other related traits.
124pub trait ItemArgument {
125    /// Format as a string suitable for including as a netdoc keyword line argument
126    ///
127    /// The implementation is responsible for checking that the syntax is legal.
128    /// For example, if `self` is a string, it must check that the string is
129    /// in legal as a single argument.
130    ///
131    /// Some netdoc values (eg times) turn into several arguments; in that case,
132    /// one `ItemArgument` may format into multiple arguments, and this method
133    /// is responsible for writing them all, with the necessary spaces.
134    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug>;
135}
136
137impl NetdocEncoder {
138    /// Start encoding a document
139    pub fn new() -> Self {
140        NetdocEncoder {
141            built: Ok(String::new()),
142        }
143    }
144
145    /// Adds an item to the being-built document
146    ///
147    /// The item can be further extended with arguments or an object,
148    /// using the returned `ItemEncoder`.
149    pub fn item(&mut self, keyword: impl KeywordEncodable) -> ItemEncoder {
150        self.raw(&keyword.to_str());
151        ItemEncoder { doc: self }
152    }
153
154    /// Internal name for `push_raw_string()`
155    fn raw(&mut self, s: &dyn Display) {
156        self.write_with(|b| {
157            write!(b, "{}", s).expect("write! failed on String");
158            Ok(())
159        });
160    }
161
162    /// Extend the being-built document with a fallible function `f`
163    ///
164    /// Doesn't call `f` if the building has already failed,
165    /// and handles the error if `f` fails.
166    fn write_with(&mut self, f: impl FnOnce(&mut String) -> Result<(), Bug>) {
167        let Ok(build) = &mut self.built else {
168            return;
169        };
170        match f(build) {
171            Ok(()) => (),
172            Err(e) => {
173                self.built = Err(e);
174            }
175        }
176    }
177
178    /// Adds raw text to the being-built document
179    ///
180    /// `s` is added as raw text, after the newline ending the previous item.
181    /// If `item` is subsequently called, the start of that item
182    /// will immediately follow `s`.
183    ///
184    /// It is the responsibility of the caller to obey the metadocument syntax.
185    /// In particular, `s` should end with a newline.
186    /// No checks are performed.
187    /// Incorrect use might lead to malformed documents, or later errors.
188    pub fn push_raw_string(&mut self, s: &dyn Display) {
189        self.raw(s);
190    }
191
192    /// Return a cursor, pointing to just after the last item (if any)
193    pub fn cursor(&self) -> Cursor {
194        let offset = match &self.built {
195            Ok(b) => b.len(),
196            Err(_) => usize::MAX,
197        };
198        Cursor { offset }
199    }
200
201    /// Obtain the text of a section of the document
202    ///
203    /// Useful for making a signature.
204    pub fn slice(&self, begin: Cursor, end: Cursor) -> Result<&str, Bug> {
205        self.built
206            .as_ref()
207            .map_err(Clone::clone)?
208            .get(begin.offset..end.offset)
209            .ok_or_else(|| internal!("NetdocEncoder::slice out of bounds, Cursor mismanaged"))
210    }
211
212    /// Build the document into textual form
213    pub fn finish(self) -> Result<String, Bug> {
214        self.built
215    }
216}
217
218impl Default for NetdocEncoder {
219    fn default() -> Self {
220        // We must open-code this because the actual encoder contains Result, which isn't Default
221        NetdocEncoder::new()
222    }
223}
224
225impl ItemArgument for str {
226    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
227        // Implements this
228        // https://gitlab.torproject.org/tpo/core/torspec/-/merge_requests/106
229        if self.is_empty() || self.chars().any(|c| !c.is_ascii_graphic()) {
230            return Err(internal!(
231                "invalid netdoc keyword line argument syntax {:?}",
232                self
233            ));
234        }
235        out.args_raw_nonempty(&self);
236        Ok(())
237    }
238}
239
240impl ItemArgument for &str {
241    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
242        <str as ItemArgument>::write_arg_onto(self, out)
243    }
244}
245
246impl<T: crate::NormalItemArgument> ItemArgument for T {
247    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
248        (*self.to_string()).write_arg_onto(out)
249    }
250}
251
252impl ItemArgument for Iso8601TimeSp {
253    // Unlike the macro'd formats, contains a space while still being one argument
254    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
255        let arg = self.to_string();
256        out.args_raw_nonempty(&arg.as_str());
257        Ok(())
258    }
259}
260
261#[cfg(feature = "hs-pow-full")]
262impl ItemArgument for tor_hscrypto::pow::v1::Seed {
263    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
264        let mut seed_bytes = vec![];
265        tor_bytes::Writer::write(&mut seed_bytes, &self)?;
266        out.add_arg(&Base64Unpadded::encode_string(&seed_bytes));
267        Ok(())
268    }
269}
270
271#[cfg(feature = "hs-pow-full")]
272impl ItemArgument for tor_hscrypto::pow::v1::Effort {
273    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
274        out.add_arg(&<Self as Into<u32>>::into(*self));
275        Ok(())
276    }
277}
278
279impl<'n> ItemEncoder<'n> {
280    /// Add a single argument.
281    ///
282    /// Convenience method that defers error handling, for use in infallible contexts.
283    /// Consider whether to use `ItemArgument::write_arg_onto` directly, instead.
284    ///
285    /// If the argument is not in the correct syntax, a `Bug`
286    /// error will be reported (later).
287    //
288    // This is not a hot path.  `dyn` for smaller code size.
289    pub fn arg(mut self, arg: &dyn ItemArgument) -> Self {
290        self.add_arg(arg);
291        self
292    }
293
294    /// Add a single argument, to a borrowed `ItemEncoder`
295    ///
296    /// If the argument is not in the correct syntax, a `Bug`
297    /// error will be reported (later).
298    //
299    // Needed for implementing `ItemArgument`
300    pub(crate) fn add_arg(&mut self, arg: &dyn ItemArgument) {
301        let () = arg
302            .write_arg_onto(self)
303            .unwrap_or_else(|err| self.doc.built = Err(err));
304    }
305
306    /// Add zero or more arguments, supplied as a single string.
307    ///
308    /// `args` should zero or more valid argument strings,
309    /// separated by (single) spaces.
310    /// This is not (properly) checked.
311    /// Incorrect use might lead to malformed documents, or later errors.
312    pub fn args_raw_string(&mut self, args: &dyn Display) {
313        let args = args.to_string();
314        if !args.is_empty() {
315            self.args_raw_nonempty(&args);
316        }
317    }
318
319    /// Add one or more arguments, supplied as a single string, without any checking
320    fn args_raw_nonempty(&mut self, args: &dyn Display) {
321        self.doc.raw(&format_args!(" {}", args));
322    }
323
324    /// Add an object to the item
325    ///
326    /// Checks that `keywords` is in the correct syntax.
327    /// Doesn't check that it makes semantic sense for the position of the document.
328    /// `data` will be PEM (base64) encoded.
329    //
330    // If keyword is not in the correct syntax, a `Bug` is stored in self.doc.
331    pub fn object(
332        self,
333        keywords: &str,
334        // Writeable isn't dyn-compatible
335        data: impl tor_bytes::WriteableOnce,
336    ) {
337        use crate::parse::tokenize::object::*;
338
339        self.doc.write_with(|out| {
340            if keywords.is_empty() || !tag_keywords_ok(keywords) {
341                return Err(internal!("bad object keywords string {:?}", keywords));
342            }
343            let data = {
344                let mut bytes = vec![];
345                data.write_into(&mut bytes)?;
346                Base64::encode_string(&bytes)
347            };
348            let mut data = &data[..];
349            writeln!(out, "\n{BEGIN_STR}{keywords}{TAG_END}").expect("write!");
350            while !data.is_empty() {
351                let (l, r) = if data.len() > BASE64_PEM_MAX_LINE {
352                    data.split_at(BASE64_PEM_MAX_LINE)
353                } else {
354                    (data, "")
355                };
356                writeln!(out, "{l}").expect("write!");
357                data = r;
358            }
359            // final newline will be written by Drop impl
360            write!(out, "{END_STR}{keywords}{TAG_END}").expect("write!");
361            Ok(())
362        });
363    }
364
365    /// Finish encoding this item
366    ///
367    /// The item will also automatically be finished if the `ItemEncoder` is dropped.
368    pub fn finish(self) {}
369}
370
371impl Drop for ItemEncoder<'_> {
372    fn drop(&mut self) {
373        self.doc.raw(&'\n');
374    }
375}
376
377/// Ordering, to be used when encoding network documents
378///
379/// Implemented for anything `Ord`.
380///
381/// Can also be implemented manually, for if a type cannot be `Ord`
382/// (perhaps for trait coherence reasons).
383pub trait EncodeOrd {
384    /// Compare `self` and `other`
385    ///
386    /// As `Ord::cmp`.
387    fn encode_cmp(&self, other: &Self) -> cmp::Ordering;
388}
389impl<T: Ord> EncodeOrd for T {
390    fn encode_cmp(&self, other: &Self) -> cmp::Ordering {
391        self.cmp(other)
392    }
393}
394
395/// Documents (or sub-documents) that can be encoded in the netdoc metaformat
396pub trait NetdocEncodable {
397    /// Append the document onto `out`
398    fn encode_unsigned(&self, out: &mut NetdocEncoder) -> Result<(), Bug>;
399}
400
401/// Collections of fields that can be encoded in the netdoc metaformat
402///
403/// Whole documents have structure; a `NetdocEncodableFields` does not.
404pub trait NetdocEncodableFields {
405    /// Append the document onto `out`
406    fn encode_fields(&self, out: &mut NetdocEncoder) -> Result<(), Bug>;
407}
408
409/// Items that can be encoded in network documents
410pub trait ItemValueEncodable {
411    /// Write the item's arguments, and any object, onto `out`
412    ///
413    /// `out` will have been freshly returned from [`NetdocEncoder::item`].
414    fn write_item_value_onto(&self, out: ItemEncoder) -> Result<(), Bug>;
415}
416
417/// An Object value that be encoded into a netdoc
418pub trait ItemObjectEncodable {
419    /// The label (keyword(s) in `BEGIN` and `END`)
420    fn label(&self) -> &str;
421
422    /// Represent the actual value as bytes.
423    ///
424    /// The caller, not the object, is responsible for base64 encoding.
425    //
426    // This is not a tor_bytes::Writeable supertrait because tor_bytes's writer argument
427    // is generic, which prevents many deisrable manipulations of an `impl Writeable`.
428    fn write_object_onto(&self, b: &mut Vec<u8>) -> Result<(), Bug>;
429}
430
431/// Builders for network documents.
432///
433/// This trait is a bit weird, because its `Self` type must contain the *private* keys
434/// necessary to sign the document!
435///
436/// So it is implemented for "builders", not for documents themselves.
437/// Some existing documents can be constructed only via these builders.
438/// The newer approach is for documents to be transparent data, at the Rust level,
439/// and to derive an encoder.
440/// TODO this derive approach is not yet implemented!
441///
442/// Actual document types, which only contain the information in the document,
443/// don't implement this trait.
444pub trait NetdocBuilder {
445    /// Build the document into textual form.
446    fn build_sign<R: RngCore + CryptoRng>(self, rng: &mut R) -> Result<String, EncodeError>;
447}
448
449impl ItemValueEncodable for Void {
450    fn write_item_value_onto(&self, _out: ItemEncoder) -> Result<(), Bug> {
451        void::unreachable(*self)
452    }
453}
454
455impl ItemObjectEncodable for Void {
456    fn label(&self) -> &str {
457        void::unreachable(*self)
458    }
459    fn write_object_onto(&self, _: &mut Vec<u8>) -> Result<(), Bug> {
460        void::unreachable(*self)
461    }
462}
463
464/// implement [`ItemValueEncodable`] for a particular tuple size
465macro_rules! item_value_encodable_for_tuple {
466    { $($i:literal)* } => { paste! {
467        impl< $( [<T$i>]: ItemArgument, )* > ItemValueEncodable for ( $( [<T$i>], )* ) {
468            fn write_item_value_onto(
469                &self,
470                #[allow(unused)]
471                mut out: ItemEncoder,
472            ) -> Result<(), Bug> {
473                $(
474                    <[<T$i>] as ItemArgument>::write_arg_onto(&self.$i, &mut out)?;
475                )*
476                Ok(())
477            }
478        }
479    } }
480}
481
482item_value_encodable_for_tuple! {}
483item_value_encodable_for_tuple! { 0 }
484item_value_encodable_for_tuple! { 0 1 }
485item_value_encodable_for_tuple! { 0 1 2 }
486item_value_encodable_for_tuple! { 0 1 2 3 }
487item_value_encodable_for_tuple! { 0 1 2 3 4 }
488item_value_encodable_for_tuple! { 0 1 2 3 4 5 }
489item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 }
490item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 }
491item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 8 }
492item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 8 9 }
493
494#[cfg(test)]
495mod test {
496    // @@ begin test lint list maintained by maint/add_warning @@
497    #![allow(clippy::bool_assert_comparison)]
498    #![allow(clippy::clone_on_copy)]
499    #![allow(clippy::dbg_macro)]
500    #![allow(clippy::mixed_attributes_style)]
501    #![allow(clippy::print_stderr)]
502    #![allow(clippy::print_stdout)]
503    #![allow(clippy::single_char_pattern)]
504    #![allow(clippy::unwrap_used)]
505    #![allow(clippy::unchecked_time_subtraction)]
506    #![allow(clippy::useless_vec)]
507    #![allow(clippy::needless_pass_by_value)]
508    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
509    use super::*;
510    use std::str::FromStr;
511
512    use crate::types::misc::Iso8601TimeNoSp;
513    use base64ct::{Base64Unpadded, Encoding};
514
515    #[test]
516    fn time_formats_as_args() {
517        use crate::doc::authcert::AuthCertKwd as ACK;
518        use crate::doc::netstatus::NetstatusKwd as NK;
519
520        let t_sp = Iso8601TimeSp::from_str("2020-04-18 08:36:57").unwrap();
521        let t_no_sp = Iso8601TimeNoSp::from_str("2021-04-18T08:36:57").unwrap();
522
523        let mut encode = NetdocEncoder::new();
524        encode.item(ACK::DIR_KEY_EXPIRES).arg(&t_sp);
525        encode
526            .item(NK::SHARED_RAND_PREVIOUS_VALUE)
527            .arg(&"3")
528            .arg(&"bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs=")
529            .arg(&t_no_sp);
530
531        let doc = encode.finish().unwrap();
532        println!("{}", doc);
533        assert_eq!(
534            doc,
535            r"dir-key-expires 2020-04-18 08:36:57
536shared-rand-previous-value 3 bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs= 2021-04-18T08:36:57
537"
538        );
539    }
540
541    #[test]
542    fn authcert() {
543        use crate::doc::authcert::AuthCertKwd as ACK;
544        use crate::doc::authcert::{AuthCert, UncheckedAuthCert};
545
546        // c&p from crates/tor-llcrypto/tests/testvec.rs
547        let pk_rsa = {
548            let pem = "
549MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
550PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
551qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE";
552            Base64Unpadded::decode_vec(&pem.replace('\n', "")).unwrap()
553        };
554
555        let mut encode = NetdocEncoder::new();
556        encode.item(ACK::DIR_KEY_CERTIFICATE_VERSION).arg(&3);
557        encode
558            .item(ACK::FINGERPRINT)
559            .arg(&"9367f9781da8eabbf96b691175f0e701b43c602e");
560        encode
561            .item(ACK::DIR_KEY_PUBLISHED)
562            .arg(&Iso8601TimeSp::from_str("2020-04-18 08:36:57").unwrap());
563        encode
564            .item(ACK::DIR_KEY_EXPIRES)
565            .arg(&Iso8601TimeSp::from_str("2021-04-18 08:36:57").unwrap());
566        encode
567            .item(ACK::DIR_IDENTITY_KEY)
568            .object("RSA PUBLIC KEY", &*pk_rsa);
569        encode
570            .item(ACK::DIR_SIGNING_KEY)
571            .object("RSA PUBLIC KEY", &*pk_rsa);
572        encode
573            .item(ACK::DIR_KEY_CROSSCERT)
574            .object("ID SIGNATURE", []);
575        encode
576            .item(ACK::DIR_KEY_CERTIFICATION)
577            .object("SIGNATURE", []);
578
579        let doc = encode.finish().unwrap();
580        eprintln!("{}", doc);
581        assert_eq!(
582            doc,
583            r"dir-key-certificate-version 3
584fingerprint 9367f9781da8eabbf96b691175f0e701b43c602e
585dir-key-published 2020-04-18 08:36:57
586dir-key-expires 2021-04-18 08:36:57
587dir-identity-key
588-----BEGIN RSA PUBLIC KEY-----
589MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
590PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
591qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
592-----END RSA PUBLIC KEY-----
593dir-signing-key
594-----BEGIN RSA PUBLIC KEY-----
595MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
596PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
597qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
598-----END RSA PUBLIC KEY-----
599dir-key-crosscert
600-----BEGIN ID SIGNATURE-----
601-----END ID SIGNATURE-----
602dir-key-certification
603-----BEGIN SIGNATURE-----
604-----END SIGNATURE-----
605"
606        );
607
608        let _: UncheckedAuthCert = AuthCert::parse(&doc).unwrap();
609    }
610}