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;
42mod impls;
43
44use std::cmp;
45use std::collections::BTreeSet;
46use std::fmt::Write;
47use std::iter;
48use std::marker::PhantomData;
49use std::sync::Arc;
50
51use base64ct::{Base64, Base64Unpadded, Encoding};
52use educe::Educe;
53use itertools::Itertools;
54use paste::paste;
55use rand::{CryptoRng, Rng};
56use tor_bytes::EncodeError;
57use tor_error::internal;
58use void::Void;
59
60use crate::KeywordEncodable;
61use crate::parse::tokenize::tag_keywords_ok;
62use crate::types::misc::Iso8601TimeSp;
63
64// Exports used by macros, which treat this module as a prelude
65#[doc(hidden)]
66pub 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)]
83pub 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)]
97pub 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)]
112pub 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.
127pub 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])`.
143pub fn encode_netdoc_unsigned<'d, 'i, D, I>(docs: I) -> Result<String, Bug>
144where
145    D: NetdocEncodable + 'd,
146    I: IntoIterator<Item = &'d D> + 'i,
147{
148    let mut encoder = NetdocEncoder::new();
149    for doc in docs {
150        doc.encode_unsigned(&mut encoder)?;
151    }
152    encoder.finish()
153}
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.
160pub 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
166impl NetdocEncoder {
167    /// Start encoding a document
168    pub fn new() -> Self {
169        NetdocEncoder {
170            built: Ok(String::new()),
171        }
172    }
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    pub fn item(&mut self, keyword: impl KeywordEncodable) -> ItemEncoder {
179        self.raw(&keyword.to_str());
180        ItemEncoder { doc: self }
181    }
182
183    /// Internal name for `push_raw_string()`
184    fn raw(&mut self, s: &dyn Display) {
185        self.write_with(|b| {
186            write!(b, "{}", s).expect("write! failed on String");
187            Ok(())
188        });
189    }
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    fn write_with(&mut self, f: impl FnOnce(&mut String) -> Result<(), Bug>) {
196        let Ok(build) = &mut self.built else {
197            return;
198        };
199        match f(build) {
200            Ok(()) => (),
201            Err(e) => {
202                self.built = Err(e);
203            }
204        }
205    }
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    pub fn push_raw_string(&mut self, s: &dyn Display) {
218        self.raw(s);
219    }
220
221    /// Return a cursor, pointing to just after the last item (if any)
222    pub fn cursor(&self) -> Cursor {
223        let offset = match &self.built {
224            Ok(b) => b.len(),
225            Err(_) => usize::MAX,
226        };
227        Cursor { offset }
228    }
229
230    /// Obtain the text of a section of the document
231    ///
232    /// Useful for making a signature.
233    pub fn slice(&self, begin: Cursor, end: Cursor) -> Result<&str, Bug> {
234        self.built
235            .as_ref()
236            .map_err(Clone::clone)?
237            .get(begin.offset..end.offset)
238            .ok_or_else(|| internal!("NetdocEncoder::slice out of bounds, Cursor mismanaged"))
239    }
240
241    /// Obtain the document so far in textual form
242    pub fn text_sofar(&self) -> Result<&str, Bug> {
243        self.built.as_deref().map_err(Clone::clone)
244    }
245
246    /// Build the document into textual form
247    pub fn finish(self) -> Result<String, Bug> {
248        self.built
249    }
250}
251
252impl Default for NetdocEncoder {
253    fn default() -> Self {
254        // We must open-code this because the actual encoder contains Result, which isn't Default
255        NetdocEncoder::new()
256    }
257}
258
259impl<T: crate::NormalItemArgument + Display> ItemArgument for T {
260    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
261        (*self.to_string()).write_arg_onto(out)
262    }
263}
264
265impl<'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    pub fn arg(mut self, arg: &dyn ItemArgument) -> Self {
276        self.add_arg(arg);
277        self
278    }
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    pub(crate) fn add_arg(&mut self, arg: &dyn ItemArgument) {
287        let () = arg
288            .write_arg_onto(self)
289            .unwrap_or_else(|err| self.doc.built = Err(err));
290    }
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    pub fn args_raw_string(&mut self, args: &dyn Display) {
299        let args = args.to_string();
300        if !args.is_empty() {
301            self.args_raw_nonempty(&args);
302        }
303    }
304
305    /// Add one or more arguments, supplied as a single string, without any checking
306    fn args_raw_nonempty(&mut self, args: &dyn Display) {
307        self.doc.raw(&format_args!(" {}", args));
308    }
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    pub fn object(self, object: &dyn ItemObjectEncodable) {
315        let label = object.label();
316        let mut buf = vec![];
317        object
318            .write_object_onto(&mut buf)
319            .unwrap_or_else(|err| self.doc.built = Err(err));
320        self.object_bytes(label, buf);
321    }
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    pub fn object_bytes(
331        self,
332        keywords: &str,
333        // Writeable isn't dyn-compatible
334        data: impl tor_bytes::WriteableOnce,
335    ) {
336        use crate::parse::tokenize::object::*;
337
338        self.doc.write_with(|out| {
339            if keywords.is_empty() || !tag_keywords_ok(keywords) {
340                return Err(internal!("bad object keywords string {:?}", keywords));
341            }
342            let data = {
343                let mut bytes = vec![];
344                data.write_into(&mut bytes)?;
345                Base64::encode_string(&bytes)
346            };
347            let mut data = data.as_str();
348            writeln!(out, "\n{BEGIN_STR}{keywords}{TAG_END}").expect("write!");
349            while !data.is_empty() {
350                let (l, r) = if data.len() > BASE64_PEM_MAX_LINE {
351                    data.split_at(BASE64_PEM_MAX_LINE)
352                } else {
353                    (data, "")
354                };
355                writeln!(out, "{l}").expect("write!");
356                data = r;
357            }
358            // final newline will be written by Drop impl
359            write!(out, "{END_STR}{keywords}{TAG_END}").expect("write!");
360            Ok(())
361        });
362    }
363
364    /// Finish encoding this item
365    ///
366    /// The item will also automatically be finished if the `ItemEncoder` is dropped.
367    pub fn finish(self) {}
368}
369
370impl Drop for ItemEncoder<'_> {
371    fn drop(&mut self) {
372        self.doc.raw(&'\n');
373    }
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).
382pub trait EncodeOrd {
383    /// Compare `self` and `other`
384    ///
385    /// As `Ord::cmp`.
386    fn encode_cmp(&self, other: &Self) -> cmp::Ordering;
387}
388impl<T: Ord> EncodeOrd for T {
389    fn encode_cmp(&self, other: &Self) -> cmp::Ordering {
390        self.cmp(other)
391    }
392}
393
394/// Documents (or sub-documents) that can be encoded in the netdoc metaformat
395pub 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.
403pub 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
409pub 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
417pub 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.
443pub 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
449macro_rules! item_value_encodable_for_tuple {
450    { $($i:literal)* } => { paste! {
451        impl< $( [<T$i>]: ItemArgument, )* > ItemValueEncodable for ( $( [<T$i>], )* ) {
452            fn write_item_value_onto(
453                &self,
454                #[allow(unused)]
455                mut out: ItemEncoder,
456            ) -> Result<(), Bug> {
457                $(
458                    <[<T$i>] as ItemArgument>::write_arg_onto(&self.$i, &mut out)?;
459                )*
460                Ok(())
461            }
462        }
463    } }
464}
465
466item_value_encodable_for_tuple! {}
467item_value_encodable_for_tuple! { 0 }
468item_value_encodable_for_tuple! { 0 1 }
469item_value_encodable_for_tuple! { 0 1 2 }
470item_value_encodable_for_tuple! { 0 1 2 3 }
471item_value_encodable_for_tuple! { 0 1 2 3 4 }
472item_value_encodable_for_tuple! { 0 1 2 3 4 5 }
473item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 }
474item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 }
475item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 8 }
476item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 8 9 }
477
478#[cfg(test)]
479mod 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
520shared-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 = "
533MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
534PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
535qiBHRBGbtkF/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
567fingerprint 9367f9781da8eabbf96b691175f0e701b43c602e
568dir-key-published 2020-04-18 08:36:57
569dir-key-expires 2021-04-18 08:36:57
570dir-identity-key
571-----BEGIN RSA PUBLIC KEY-----
572MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
573PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
574qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
575-----END RSA PUBLIC KEY-----
576dir-signing-key
577-----BEGIN RSA PUBLIC KEY-----
578MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
579PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
580qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
581-----END RSA PUBLIC KEY-----
582dir-key-crosscert
583-----BEGIN ID SIGNATURE-----
584-----END ID SIGNATURE-----
585dir-key-certification
586-----BEGIN SIGNATURE-----
587-----END SIGNATURE-----
588"
589        );
590
591        let _: UncheckedAuthCert = AuthCert::parse(&doc).unwrap();
592    }
593}