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, RngCore};
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
140impl NetdocEncoder {
141 /// Start encoding a document
142 pub fn new() -> Self {
143 NetdocEncoder {
144 built: Ok(String::new()),
145 }
146 }
147
148 /// Adds an item to the being-built document
149 ///
150 /// The item can be further extended with arguments or an object,
151 /// using the returned `ItemEncoder`.
152 pub fn item(&mut self, keyword: impl KeywordEncodable) -> ItemEncoder {
153 self.raw(&keyword.to_str());
154 ItemEncoder { doc: self }
155 }
156
157 /// Internal name for `push_raw_string()`
158 fn raw(&mut self, s: &dyn Display) {
159 self.write_with(|b| {
160 write!(b, "{}", s).expect("write! failed on String");
161 Ok(())
162 });
163 }
164
165 /// Extend the being-built document with a fallible function `f`
166 ///
167 /// Doesn't call `f` if the building has already failed,
168 /// and handles the error if `f` fails.
169 fn write_with(&mut self, f: impl FnOnce(&mut String) -> Result<(), Bug>) {
170 let Ok(build) = &mut self.built else {
171 return;
172 };
173 match f(build) {
174 Ok(()) => (),
175 Err(e) => {
176 self.built = Err(e);
177 }
178 }
179 }
180
181 /// Adds raw text to the being-built document
182 ///
183 /// `s` is added as raw text, after the newline ending the previous item.
184 /// If `item` is subsequently called, the start of that item
185 /// will immediately follow `s`.
186 ///
187 /// It is the responsibility of the caller to obey the metadocument syntax.
188 /// In particular, `s` should end with a newline.
189 /// No checks are performed.
190 /// Incorrect use might lead to malformed documents, or later errors.
191 pub fn push_raw_string(&mut self, s: &dyn Display) {
192 self.raw(s);
193 }
194
195 /// Return a cursor, pointing to just after the last item (if any)
196 pub fn cursor(&self) -> Cursor {
197 let offset = match &self.built {
198 Ok(b) => b.len(),
199 Err(_) => usize::MAX,
200 };
201 Cursor { offset }
202 }
203
204 /// Obtain the text of a section of the document
205 ///
206 /// Useful for making a signature.
207 pub fn slice(&self, begin: Cursor, end: Cursor) -> Result<&str, Bug> {
208 self.built
209 .as_ref()
210 .map_err(Clone::clone)?
211 .get(begin.offset..end.offset)
212 .ok_or_else(|| internal!("NetdocEncoder::slice out of bounds, Cursor mismanaged"))
213 }
214
215 /// Obtain the document so far in textual form
216 pub fn text_sofar(&self) -> Result<&str, Bug> {
217 self.built.as_deref().map_err(Clone::clone)
218 }
219
220 /// Build the document into textual form
221 pub fn finish(self) -> Result<String, Bug> {
222 self.built
223 }
224}
225
226impl Default for NetdocEncoder {
227 fn default() -> Self {
228 // We must open-code this because the actual encoder contains Result, which isn't Default
229 NetdocEncoder::new()
230 }
231}
232
233impl<T: crate::NormalItemArgument + Display> ItemArgument for T {
234 fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
235 (*self.to_string()).write_arg_onto(out)
236 }
237}
238
239impl<'n> ItemEncoder<'n> {
240 /// Add a single argument.
241 ///
242 /// Convenience method that defers error handling, for use in infallible contexts.
243 /// Consider whether to use `ItemArgument::write_arg_onto` directly, instead.
244 ///
245 /// If the argument is not in the correct syntax, a `Bug`
246 /// error will be reported (later).
247 //
248 // This is not a hot path. `dyn` for smaller code size.
249 pub fn arg(mut self, arg: &dyn ItemArgument) -> Self {
250 self.add_arg(arg);
251 self
252 }
253
254 /// Add a single argument, to a borrowed `ItemEncoder`
255 ///
256 /// If the argument is not in the correct syntax, a `Bug`
257 /// error will be reported (later).
258 //
259 // Needed for implementing `ItemArgument`
260 pub(crate) fn add_arg(&mut self, arg: &dyn ItemArgument) {
261 let () = arg
262 .write_arg_onto(self)
263 .unwrap_or_else(|err| self.doc.built = Err(err));
264 }
265
266 /// Add zero or more arguments, supplied as a single string.
267 ///
268 /// `args` should zero or more valid argument strings,
269 /// separated by (single) spaces.
270 /// This is not (properly) checked.
271 /// Incorrect use might lead to malformed documents, or later errors.
272 pub fn args_raw_string(&mut self, args: &dyn Display) {
273 let args = args.to_string();
274 if !args.is_empty() {
275 self.args_raw_nonempty(&args);
276 }
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 `ItemObjectEncodable` to the item
285 //
286 // Note that the `ItemValueEncodable` derive macro (in `derive.rs`)
287 // also implements this functionality.
288 pub fn object(self, object: &dyn ItemObjectEncodable) {
289 let label = object.label();
290 let mut buf = vec![];
291 object
292 .write_object_onto(&mut buf)
293 .unwrap_or_else(|err| self.doc.built = Err(err));
294 self.object_bytes(label, buf);
295 }
296
297 /// Add an object to the item, given the keyword and a `tor_bytes::WriteableOnce`
298 ///
299 /// Checks that `keywords` is in the correct syntax.
300 /// Doesn't check that it makes semantic sense for the position of the document.
301 /// `data` will be PEM (base64) encoded.
302 //
303 // If keyword is not in the correct syntax, a `Bug` is stored in self.doc.
304 pub fn object_bytes(
305 self,
306 keywords: &str,
307 // Writeable isn't dyn-compatible
308 data: impl tor_bytes::WriteableOnce,
309 ) {
310 use crate::parse::tokenize::object::*;
311
312 self.doc.write_with(|out| {
313 if keywords.is_empty() || !tag_keywords_ok(keywords) {
314 return Err(internal!("bad object keywords string {:?}", keywords));
315 }
316 let data = {
317 let mut bytes = vec![];
318 data.write_into(&mut bytes)?;
319 Base64::encode_string(&bytes)
320 };
321 let mut data = &data[..];
322 writeln!(out, "\n{BEGIN_STR}{keywords}{TAG_END}").expect("write!");
323 while !data.is_empty() {
324 let (l, r) = if data.len() > BASE64_PEM_MAX_LINE {
325 data.split_at(BASE64_PEM_MAX_LINE)
326 } else {
327 (data, "")
328 };
329 writeln!(out, "{l}").expect("write!");
330 data = r;
331 }
332 // final newline will be written by Drop impl
333 write!(out, "{END_STR}{keywords}{TAG_END}").expect("write!");
334 Ok(())
335 });
336 }
337
338 /// Finish encoding this item
339 ///
340 /// The item will also automatically be finished if the `ItemEncoder` is dropped.
341 pub fn finish(self) {}
342}
343
344impl Drop for ItemEncoder<'_> {
345 fn drop(&mut self) {
346 self.doc.raw(&'\n');
347 }
348}
349
350/// Ordering, to be used when encoding network documents
351///
352/// Implemented for anything `Ord`.
353///
354/// Can also be implemented manually, for if a type cannot be `Ord`
355/// (perhaps for trait coherence reasons).
356pub trait EncodeOrd {
357 /// Compare `self` and `other`
358 ///
359 /// As `Ord::cmp`.
360 fn encode_cmp(&self, other: &Self) -> cmp::Ordering;
361}
362impl<T: Ord> EncodeOrd for T {
363 fn encode_cmp(&self, other: &Self) -> cmp::Ordering {
364 self.cmp(other)
365 }
366}
367
368/// Documents (or sub-documents) that can be encoded in the netdoc metaformat
369pub trait NetdocEncodable {
370 /// Append the document onto `out`
371 fn encode_unsigned(&self, out: &mut NetdocEncoder) -> Result<(), Bug>;
372}
373
374/// Collections of fields that can be encoded in the netdoc metaformat
375///
376/// Whole documents have structure; a `NetdocEncodableFields` does not.
377pub trait NetdocEncodableFields {
378 /// Append the document onto `out`
379 fn encode_fields(&self, out: &mut NetdocEncoder) -> Result<(), Bug>;
380}
381
382/// Items that can be encoded in network documents
383pub trait ItemValueEncodable {
384 /// Write the item's arguments, and any object, onto `out`
385 ///
386 /// `out` will have been freshly returned from [`NetdocEncoder::item`].
387 fn write_item_value_onto(&self, out: ItemEncoder) -> Result<(), Bug>;
388}
389
390/// An Object value that be encoded into a netdoc
391pub trait ItemObjectEncodable {
392 /// The label (keyword(s) in `BEGIN` and `END`)
393 fn label(&self) -> &str;
394
395 /// Represent the actual value as bytes.
396 ///
397 /// The caller, not the object, is responsible for base64 encoding.
398 //
399 // This is not a tor_bytes::Writeable supertrait because tor_bytes's writer argument
400 // is generic, which prevents many deisrable manipulations of an `impl Writeable`.
401 fn write_object_onto(&self, b: &mut Vec<u8>) -> Result<(), Bug>;
402}
403
404/// Builders for network documents.
405///
406/// This trait is a bit weird, because its `Self` type must contain the *private* keys
407/// necessary to sign the document!
408///
409/// So it is implemented for "builders", not for documents themselves.
410/// Some existing documents can be constructed only via these builders.
411/// The newer approach is for documents to be transparent data, at the Rust level,
412/// and to derive an encoder.
413/// TODO this derive approach is not yet implemented!
414///
415/// Actual document types, which only contain the information in the document,
416/// don't implement this trait.
417pub trait NetdocBuilder {
418 /// Build the document into textual form.
419 fn build_sign<R: RngCore + CryptoRng>(self, rng: &mut R) -> Result<String, EncodeError>;
420}
421
422/// implement [`ItemValueEncodable`] for a particular tuple size
423macro_rules! item_value_encodable_for_tuple {
424 { $($i:literal)* } => { paste! {
425 impl< $( [<T$i>]: ItemArgument, )* > ItemValueEncodable for ( $( [<T$i>], )* ) {
426 fn write_item_value_onto(
427 &self,
428 #[allow(unused)]
429 mut out: ItemEncoder,
430 ) -> Result<(), Bug> {
431 $(
432 <[<T$i>] as ItemArgument>::write_arg_onto(&self.$i, &mut out)?;
433 )*
434 Ok(())
435 }
436 }
437 } }
438}
439
440item_value_encodable_for_tuple! {}
441item_value_encodable_for_tuple! { 0 }
442item_value_encodable_for_tuple! { 0 1 }
443item_value_encodable_for_tuple! { 0 1 2 }
444item_value_encodable_for_tuple! { 0 1 2 3 }
445item_value_encodable_for_tuple! { 0 1 2 3 4 }
446item_value_encodable_for_tuple! { 0 1 2 3 4 5 }
447item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 }
448item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 }
449item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 8 }
450item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 8 9 }
451
452#[cfg(test)]
453mod test {
454 // @@ begin test lint list maintained by maint/add_warning @@
455 #![allow(clippy::bool_assert_comparison)]
456 #![allow(clippy::clone_on_copy)]
457 #![allow(clippy::dbg_macro)]
458 #![allow(clippy::mixed_attributes_style)]
459 #![allow(clippy::print_stderr)]
460 #![allow(clippy::print_stdout)]
461 #![allow(clippy::single_char_pattern)]
462 #![allow(clippy::unwrap_used)]
463 #![allow(clippy::unchecked_time_subtraction)]
464 #![allow(clippy::useless_vec)]
465 #![allow(clippy::needless_pass_by_value)]
466 //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
467 use super::*;
468 use std::str::FromStr;
469
470 use crate::types::misc::Iso8601TimeNoSp;
471 use base64ct::{Base64Unpadded, Encoding};
472
473 #[test]
474 fn time_formats_as_args() {
475 use crate::doc::authcert::AuthCertKwd as ACK;
476 use crate::doc::netstatus::NetstatusKwd as NK;
477
478 let t_sp = Iso8601TimeSp::from_str("2020-04-18 08:36:57").unwrap();
479 let t_no_sp = Iso8601TimeNoSp::from_str("2021-04-18T08:36:57").unwrap();
480
481 let mut encode = NetdocEncoder::new();
482 encode.item(ACK::DIR_KEY_EXPIRES).arg(&t_sp);
483 encode
484 .item(NK::SHARED_RAND_PREVIOUS_VALUE)
485 .arg(&"3")
486 .arg(&"bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs=")
487 .arg(&t_no_sp);
488
489 let doc = encode.finish().unwrap();
490 println!("{}", doc);
491 assert_eq!(
492 doc,
493 r"dir-key-expires 2020-04-18 08:36:57
494shared-rand-previous-value 3 bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs= 2021-04-18T08:36:57
495"
496 );
497 }
498
499 #[test]
500 fn authcert() {
501 use crate::doc::authcert::AuthCertKwd as ACK;
502 use crate::doc::authcert::{AuthCert, UncheckedAuthCert};
503
504 // c&p from crates/tor-llcrypto/tests/testvec.rs
505 let pk_rsa = {
506 let pem = "
507MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
508PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
509qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE";
510 Base64Unpadded::decode_vec(&pem.replace('\n', "")).unwrap()
511 };
512
513 let mut encode = NetdocEncoder::new();
514 encode.item(ACK::DIR_KEY_CERTIFICATE_VERSION).arg(&3);
515 encode
516 .item(ACK::FINGERPRINT)
517 .arg(&"9367f9781da8eabbf96b691175f0e701b43c602e");
518 encode
519 .item(ACK::DIR_KEY_PUBLISHED)
520 .arg(&Iso8601TimeSp::from_str("2020-04-18 08:36:57").unwrap());
521 encode
522 .item(ACK::DIR_KEY_EXPIRES)
523 .arg(&Iso8601TimeSp::from_str("2021-04-18 08:36:57").unwrap());
524 encode
525 .item(ACK::DIR_IDENTITY_KEY)
526 .object_bytes("RSA PUBLIC KEY", &*pk_rsa);
527 encode
528 .item(ACK::DIR_SIGNING_KEY)
529 .object_bytes("RSA PUBLIC KEY", &*pk_rsa);
530 encode
531 .item(ACK::DIR_KEY_CROSSCERT)
532 .object_bytes("ID SIGNATURE", []);
533 encode
534 .item(ACK::DIR_KEY_CERTIFICATION)
535 .object_bytes("SIGNATURE", []);
536
537 let doc = encode.finish().unwrap();
538 eprintln!("{}", doc);
539 assert_eq!(
540 doc,
541 r"dir-key-certificate-version 3
542fingerprint 9367f9781da8eabbf96b691175f0e701b43c602e
543dir-key-published 2020-04-18 08:36:57
544dir-key-expires 2021-04-18 08:36:57
545dir-identity-key
546-----BEGIN RSA PUBLIC KEY-----
547MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
548PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
549qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
550-----END RSA PUBLIC KEY-----
551dir-signing-key
552-----BEGIN RSA PUBLIC KEY-----
553MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
554PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
555qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
556-----END RSA PUBLIC KEY-----
557dir-key-crosscert
558-----BEGIN ID SIGNATURE-----
559-----END ID SIGNATURE-----
560dir-key-certification
561-----BEGIN SIGNATURE-----
562-----END SIGNATURE-----
563"
564 );
565
566 let _: UncheckedAuthCert = AuthCert::parse(&doc).unwrap();
567 }
568}