tor_netdoc/
build.rs

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
16use std::fmt::{Display, Write};
17
18use base64ct::{Base64, Base64Unpadded, Encoding};
19use rand::{CryptoRng, RngCore};
20use tor_bytes::EncodeError;
21use tor_error::{internal, Bug};
22
23use crate::parse::keyword::Keyword;
24use crate::parse::tokenize::tag_keywords_ok;
25use 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)]
32pub(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)]
45pub(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)]
60pub(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.
73pub(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
86impl NetdocEncoder {
87    /// Start encoding a document
88    pub(crate) fn new() -> Self {
89        NetdocEncoder {
90            built: Ok(String::new()),
91        }
92    }
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    pub(crate) fn item(&mut self, keyword: impl Keyword) -> ItemEncoder {
99        self.raw(&keyword.to_str());
100        ItemEncoder { doc: self }
101    }
102
103    /// Internal name for `push_raw_string()`
104    fn raw(&mut self, s: &dyn Display) {
105        self.write_with(|b| {
106            write!(b, "{}", s).expect("write! failed on String");
107            Ok(())
108        });
109    }
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    fn write_with(&mut self, f: impl FnOnce(&mut String) -> Result<(), Bug>) {
116        let Ok(build) = &mut self.built else {
117            return;
118        };
119        match f(build) {
120            Ok(()) => (),
121            Err(e) => {
122                self.built = Err(e);
123            }
124        }
125    }
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    pub(crate) fn cursor(&self) -> Cursor {
144        let offset = match &self.built {
145            Ok(b) => b.len(),
146            Err(_) => usize::MAX,
147        };
148        Cursor { offset }
149    }
150
151    /// Obtain the text of a section of the document
152    ///
153    /// Useful for making a signature.
154    pub(crate) fn slice(&self, begin: Cursor, end: Cursor) -> Result<&str, Bug> {
155        self.built
156            .as_ref()
157            .map_err(Clone::clone)?
158            .get(begin.offset..end.offset)
159            .ok_or_else(|| internal!("NetdocEncoder::slice out of bounds, Cursor mismanaged"))
160    }
161
162    /// Build the document into textual form
163    pub(crate) fn finish(self) -> Result<String, Bug> {
164        self.built
165    }
166}
167
168impl ItemArgument for str {
169    fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
170        // Implements this
171        // https://gitlab.torproject.org/tpo/core/torspec/-/merge_requests/106
172        if self.is_empty() || self.chars().any(|c| !c.is_ascii_graphic()) {
173            return Err(internal!("invalid keyword argument syntax {:?}", self));
174        }
175        out.args_raw_nonempty(&self);
176        Ok(())
177    }
178}
179
180impl ItemArgument for String {
181    fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
182        ItemArgument::write_onto(&self.as_str(), out)
183    }
184}
185
186impl<T: ItemArgument + ?Sized> ItemArgument for &'_ T {
187    fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
188        <T as ItemArgument>::write_onto(self, out)
189    }
190}
191
192/// Implement [`ItemArgument`] for `$ty` in terms of `<$ty as Display>`
193///
194/// Checks that the syntax is acceptable.
195macro_rules! impl_item_argument_as_display { { $( $ty:ty $(,)? )* } => { $(
196    impl ItemArgument for $ty {
197        fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
198            let arg = self.to_string();
199            out.add_arg(&arg.as_str());
200            Ok(())
201        }
202    }
203)* } }
204
205impl_item_argument_as_display! { usize, u8, u16, u32, u64, u128 }
206impl_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
212impl_item_argument_as_display! {Iso8601TimeNoSp}
213impl ItemArgument for Iso8601TimeSp {
214    // Unlike the macro'd formats, contains a space while still being one argument
215    fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
216        let arg = self.to_string();
217        out.args_raw_nonempty(&arg.as_str());
218        Ok(())
219    }
220}
221
222#[cfg(feature = "hs-pow-full")]
223impl ItemArgument for tor_hscrypto::pow::v1::Seed {
224    fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
225        let mut seed_bytes = vec![];
226        tor_bytes::Writer::write(&mut seed_bytes, &self)?;
227        out.add_arg(&Base64Unpadded::encode_string(&seed_bytes));
228        Ok(())
229    }
230}
231
232#[cfg(feature = "hs-pow-full")]
233impl ItemArgument for tor_hscrypto::pow::v1::Effort {
234    fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
235        out.add_arg(&<Self as Into<u32>>::into(*self));
236        Ok(())
237    }
238}
239
240impl<'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    pub(crate) fn arg(mut self, arg: &dyn ItemArgument) -> Self {
248        self.add_arg(arg);
249        self
250    }
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    pub(crate) fn add_arg(&mut self, arg: &dyn ItemArgument) {
259        let () = arg
260            .write_onto(self)
261            .unwrap_or_else(|err| self.doc.built = Err(err));
262    }
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    fn args_raw_nonempty(&mut self, args: &dyn Display) {
281        self.doc.raw(&format_args!(" {}", args));
282    }
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    pub(crate) fn object(
292        self,
293        keywords: &str,
294        // Writeable isn't dyn-compatible
295        data: impl tor_bytes::WriteableOnce,
296    ) {
297        use crate::parse::tokenize::object::*;
298
299        self.doc.write_with(|out| {
300            if keywords.is_empty() || !tag_keywords_ok(keywords) {
301                return Err(internal!("bad object keywords string {:?}", keywords));
302            }
303            let data = {
304                let mut bytes = vec![];
305                data.write_into(&mut bytes)?;
306                Base64::encode_string(&bytes)
307            };
308            let mut data = &data[..];
309            writeln!(out, "\n{BEGIN_STR}{keywords}{TAG_END}").expect("write!");
310            while !data.is_empty() {
311                let (l, r) = if data.len() > BASE64_PEM_MAX_LINE {
312                    data.split_at(BASE64_PEM_MAX_LINE)
313                } else {
314                    (data, "")
315                };
316                writeln!(out, "{l}").expect("write!");
317                data = r;
318            }
319            // final newline will be written by Drop impl
320            write!(out, "{END_STR}{keywords}{TAG_END}").expect("write!");
321            Ok(())
322        });
323    }
324}
325
326impl Drop for ItemEncoder<'_> {
327    fn drop(&mut self) {
328        self.doc.raw(&'\n');
329    }
330}
331
332/// A trait for building and signing netdocs.
333pub 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)]
339mod 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
379shared-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 = "
392MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
393PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
394qiBHRBGbtkF/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
427fingerprint 9367f9781da8eabbf96b691175f0e701b43c602e
428dir-key-published 2020-04-18 08:36:57
429dir-key-expires 2021-04-18 08:36:57
430dir-identity-key
431-----BEGIN RSA PUBLIC KEY-----
432MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
433PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
434qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
435-----END RSA PUBLIC KEY-----
436dir-signing-key
437-----BEGIN RSA PUBLIC KEY-----
438MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
439PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
440qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
441-----END RSA PUBLIC KEY-----
442dir-key-crosscert
443-----BEGIN ID SIGNATURE-----
444-----END ID SIGNATURE-----
445dir-key-certification
446-----BEGIN SIGNATURE-----
447-----END SIGNATURE-----
448"
449        );
450
451        let _: UncheckedAuthCert = AuthCert::parse(&doc).unwrap();
452    }
453}