tor_netdoc/parse2/signatures.rs
1//! Handling of netdoc signatures
2//
3// TODO use tor_checkable to provide a generic .verify function.
4//
5// But the tor_checkable API might need some updates and this seems nontrivial.
6// Each verification function seems to take different inputs.
7
8use saturating_time::SaturatingTime;
9
10use super::*;
11
12/// A network document with (unverified) signatures
13///
14/// Typically implemented automatically, for `FooUnverified` structs, as defined by
15/// [`#[derive_deftly(NetdocParseableUnverified)]`](derive_deftly_template_NetdocParseableUnverified).
16///
17/// Each `FooUnverified` embodies precisely the body `Body`
18/// and the signatures data `SignaturesData` needed to verify it,
19/// This trait is precisely the constructors/accessors/deconstructors.
20pub trait NetdocUnverified: Sized {
21 /// The body, ie not including the signatures
22 type Body: Sized;
23 /// The signatures (the whole signature section)
24 type Signatures: NetdocParseableSignatures;
25
26 /// Inspect the document (and its signatures)
27 ///
28 /// # Security hazard
29 ///
30 /// The signature has not been verified, so the returned data must not be trusted.
31 fn inspect_unverified(&self) -> (&Self::Body, &SignaturesData<Self>);
32
33 /// Obtain the actual document (and signatures), without verifying
34 ///
35 /// # Security hazard
36 ///
37 /// The signature has not been verified, so the returned data must not be trusted.
38 fn unwrap_unverified(self) -> (Self::Body, SignaturesData<Self>);
39
40 /// Construct a new `NetdocUnverified` from a body and signatures
41 ///
42 /// (Called by code generated by `#[derive_deftly(NetdocUnverified)]`.)
43 fn from_parts(body: Self::Body, signatures: SignaturesData<Self>) -> Self;
44}
45
46/// Network document that has an unparsed body type (internal trait)
47///
48/// This is used internally by the
49/// [`NetdocParseableUnverified` derive](derive_deftly_template_NetdocParseableUnverified).
50//
51// This is a separate trait so that we don't complicate `NetdocUnverified`
52// with the additional internal `UnverifiedParsedBody` type.
53// That keeps `NetdocUnverified` as simply the accessors/constructors for `FooUnverified`.
54pub trait HasUnverifiedParsedBody {
55 /// The actual body payload.
56 type UnverifiedParsedBody: NetdocParseable;
57
58 /// Extract the payload
59 ///
60 /// # Security hazard
61 ///
62 /// The signature has not been verified, so the returned data must not be trusted.
63 //
64 // There is one call site, in `ItemStream::parse_signed`.
65 fn unverified_into_inner_unchecked(unverified: Self::UnverifiedParsedBody) -> Self;
66}
67
68/// The signatures information extracted from a signed network document
69///
70/// Each `SomeDocumentUnverified` contains:
71/// * private `SomeDocument`,
72/// * public `SignatureData<SomeDocumentSignatures>`
73///
74/// See [`NetdocUnverified`]
75/// and the [`NetdocParseable`](derive_deftly_template_NetdocParseable) derive.
76#[derive(Debug, Clone)]
77#[non_exhaustive]
78pub struct SignaturesData<U: NetdocUnverified> {
79 /// The signatures themselves, each including the corresponding hash
80 pub sigs: U::Signatures,
81
82 /// The length in bytes of the body, up to the start of the first signature item.
83 pub unsigned_body_len: usize,
84
85 /// The hashes which were computed as part of parsing.
86 ///
87 /// This will include every hash computed by any signature item's
88 /// `SignatureItemParseable` implementation.
89 ///
90 /// See [`NetdocParseableSignatures::HashesAccu`].
91 pub hashes: <U::Signatures as NetdocParseableSignatures>::HashesAccu,
92}
93
94/// A signature item that can appear in a netdoc
95///
96/// This is the type `T` of a field `item: T` in a netdoc signatures section type.
97///
98/// Types that implement this embody both:
99///
100/// * The item, parameters, and signature data, provided in the document.
101///
102/// They do *not* embody:
103///
104/// * The hash of the document body, which will needed during verification.
105///
106/// However, the hash *is* calculated by `from_unparsed_and_body`, during parsing,
107/// and stored in `hash`.
108///
109/// Typically derived with
110/// [`#[derive_deftly(ItemValueParseable)]`](derive_deftly_template_ItemValueParseable).
111///
112/// Normal (non-signature) items implement [`ItemValueParseable`].
113pub trait SignatureItemParseable: Sized {
114 /// The Rust type of the hash value accumulator for this item.
115 ///
116 /// Often this will be `Option<H>` where `H` is the actual hash value.
117 ///
118 /// This specific item's `HashAccu` will be found via the document's signatures'
119 /// `NetdocParseableSignatures::HashesAccu`,
120 /// which must `impl AsMut<SignatureItemParseable::HashAccu>`.
121 type HashAccu;
122
123 /// Parse the item's value, and also calculate the relevant document hash
124 ///
125 /// If the document hash needed for this item is not already present in `hash`,
126 /// this function must store it there.
127 /// An existing hash should not be overwritten:
128 /// this is because multiple signature items of the same type and hash
129 /// are supposed to be as multiple signatures on the same base document,
130 /// not cumulative signatures where each signer signs the previous signatures.
131 ///
132 /// (Parsing is entangled with hashing because some items have the hash algorithm
133 /// as an argument, and we don't want to parse that twice.)
134 //
135 // This API supports both these cases:
136 // - consensuses have multiple signatures that don't cover each other
137 // - routerdescs have multiple signatures from different algorithms where the
138 // later one in the document *does* cover the earlier one
139 //
140 // In principle it could deal with other kinds of anomalies too,
141 // since the signature item parser gets fed the items in sequence, and can
142 // maintain whatever state it needs in NetdocParseableSignatures::HashesAccu.
143 fn from_unparsed_and_body(
144 item: UnparsedItem<'_>,
145 hash_inputs: &SignatureHashInputs<'_>,
146 hash: &mut Self::HashAccu,
147 ) -> Result<Self, ErrorProblem>;
148}
149
150/// The signatures section of a network document, that can be parsed
151//
152// This is separate from `NetdocParseable` because it needs to deal with hashing too.
153//
154// Its keyword classification can be a bit simpler because all signature items
155// are structural and we do not need to impose an ordering on them during parsing.
156// So long as the body data is appropriately hashed and therefore covered
157// by whatever signature(s) we are relying on, we don't care what other irrelevant
158// signatures might be present, and we don't care if they are or are not over-signed.
159pub trait NetdocParseableSignatures: Sized {
160 /// The type used to accumulate document hashes during parsing
161 ///
162 /// Initialised to `Default` at the start of parsing,
163 /// by the [`parse2` core](ItemStream::parse_signed)
164 ///
165 /// Each item in a signatures section is parsed by a `SignatureItemParseable` impl.
166 /// That impl definites an item-specific
167 /// [`HashAccu`](SignatureItemParseable::HashAccu)
168 /// type.
169 ///
170 /// The [derived](derive_deftly_template_NetdocParseableSignatures)
171 /// signatures section parsing code finds
172 /// the item-specific hash accumulator type
173 /// [`<ITEM as SignatureItemParseable>::HashAccu`](SignatureItemParseable::HashAccu)
174 /// via `AsMut`:
175 /// `NetdocParseableSignatures::HashesAccu`
176 /// must impl `AsMut` for each
177 /// `SignatureItemParseable::HashAccu`.
178 ///
179 /// For a signatures section that can contain multiple signatures with different
180 /// hashes, the `AsMut` will normally be derived by [`derive_more::AsMut`].
181 /// For a document with only one hash type,
182 /// `NetdocParseableSignatures::HashesAccu` and `SignatureItemParseable::HashAccu`
183 /// can be the same newtype,
184 /// [deriving `AsMut<Self>`](derive_deftly_template_AsMutSelf).
185 ///
186 /// During signature verification, the document-specific verification could
187 /// should throw [`VerifyFailed::Bug`] if a hash needed for a signature item
188 /// wasn't populated.
189 /// (This isn't possible if each item's `SignatureItemParseable::from_unparsed_and_body`
190 /// always calculates and stores the hash.)
191 type HashesAccu: Default + Debug + Clone;
192
193 /// Is `kw` one of this signature section's keywords
194 fn is_item_keyword(kw: KeywordRef<'_>) -> bool;
195
196 /// Parse the signature section from a stream of items
197 fn from_items<'s>(
198 input: &mut ItemStream<'s>,
199 signed_doc_body: SignedDocumentBody<'s>,
200 sig_hashes: &mut Self::HashesAccu,
201 stop_at: stop_at!(),
202 ) -> Result<Self, ErrorProblem>;
203}
204
205/// Hash(es) for a signature item
206///
207/// Used by the derived implementation of [`SignatureItemParseable`]
208/// generated by
209/// [`ItemValueParseable`](derive_deftly_template_ItemValueParseable)
210/// with `#[deftly(netdoc(signature))]`.
211pub trait SignatureHashesAccumulator: Clone {
212 /// Update `self`, ensuring that this hash is computed
213 ///
214 /// Should perform precisely the hash-related parts specified for
215 /// [`SignatureItemParseable::from_unparsed_and_body`].
216 ///
217 /// So, if this hash is already recorded in `self`, it should not be updated.
218 fn update_from_netdoc_body(
219 &mut self,
220 document_body: &SignatureHashInputs<'_>,
221 ) -> Result<(), EP>;
222}
223
224/// The part of a network document before the first signature item
225///
226/// This is used for both Orderly signatures
227/// where the hash does not contain any part of the signature Item
228/// nor of any further signatures.
229/// and Disorderly signatures
230/// where the hash contains part of the signature Item.
231/// (The Tor protocols currently only have Disorderly signatures.)
232///
233/// See "Signature item ordering, and signatures covering signatures"
234/// in the [`NetdocParseableSignatures` derive](derive_deftly_template_NetdocParseableSignatures)
235/// and <https://gitlab.torproject.org/tpo/core/torspec/-/issues/322>.
236//
237// This type exists as a separate newtype mostly to avoid mistakes inside
238// parser implementations, where lots of different strings are floating about.
239// In particular, the parser must save this value when it starts parsing
240// signatures and must then reuse it for later ones.
241#[derive(Copy, Debug, Clone, Eq, PartialEq, Hash, amplify::Getters)]
242pub struct SignedDocumentBody<'s> {
243 /// The actual body as a string
244 #[getter(as_copy)]
245 pub(crate) body: &'s str,
246}
247
248/// Inputs needed to calculate a specific signature hash for a specific Item
249///
250/// Embodies:
251///
252/// * `&str` for the body, as for `SignedDocumentBody`.
253/// For calculating Orderly signatures.
254/// (That is, ones that do not include any part of the signature Item;
255/// See [`SignedDocumentBody`].)
256///
257/// * Extra information for calculating Disorderly signatures.
258/// Disorderly signature Items can only be implemented within this crate.
259#[derive(Copy, Debug, Clone, Eq, PartialEq, Hash, amplify::Getters)]
260pub struct SignatureHashInputs<'s> {
261 /// The Orderly body (up to the first signature item)
262 #[getter(as_copy)]
263 pub(crate) body: SignedDocumentBody<'s>,
264 /// The part of the document up to just before this signature item.
265 #[getter(skip)]
266 pub(crate) document_sofar: &'s str,
267 /// The signature item keyword and the following space
268 #[getter(skip)]
269 pub(crate) signature_item_kw_spc: &'s str,
270 /// The whole signature item keyword line not including the final newline
271 #[getter(skip)]
272 pub(crate) signature_item_line: &'s str,
273}
274
275impl<'s> SignatureHashInputs<'s> {
276 /// Hash into `h` the body and the whole of the signature item's keyword line
277 pub(crate) fn hash_whole_keyword_line(&self, h: &mut impl Digest) {
278 h.update(self.body().body());
279 h.update(self.signature_item_line);
280 h.update("\n");
281 }
282}
283
284/// Hash types suitable for use as `#[deftly(netdoc(signature(hash_accu = TY))]`
285///
286/// See
287/// [`#[derive_deftly(ItemValueParseable)]`](derive_deftly_template_ItemValueParseable).
288pub mod sig_hashes {
289 use super::*;
290
291 /// SHA-1 including the whole keyword line
292 ///
293 /// <https://spec.torproject.org/dir-spec/netdoc.html#signing>
294 #[derive(Debug, Clone, Default, Deftly)]
295 #[derive_deftly(AsMutSelf)]
296 #[allow(clippy::exhaustive_structs)]
297 pub struct Sha1WholeKeywordLine(pub Option<[u8; 20]>);
298
299 impl SignatureHashesAccumulator for Sha1WholeKeywordLine {
300 fn update_from_netdoc_body(&mut self, body: &SignatureHashInputs<'_>) -> Result<(), EP> {
301 self.0.get_or_insert_with(|| {
302 let mut h = tor_llcrypto::d::Sha1::new();
303 body.hash_whole_keyword_line(&mut h);
304 h.finalize().into()
305 });
306 Ok(())
307 }
308 }
309}
310
311/// Utility function to check that a time is within a validity period
312pub fn check_validity_time(
313 now: SystemTime,
314 validity: std::ops::RangeInclusive<SystemTime>,
315) -> Result<(), VF> {
316 if now < *validity.start() {
317 Err(VF::TooNew)
318 } else if now > *validity.end() {
319 Err(VF::TooOld)
320 } else {
321 Ok(())
322 }
323}
324
325/// Like [`check_validity_time()`] but with a tolerance to support clock skews.
326///
327/// This function does not use the `DirTolerance` struct because we want to be
328/// agnostic of directories in this context.
329pub fn check_validity_time_tolerance(
330 now: SystemTime,
331 validity: std::ops::RangeInclusive<SystemTime>,
332 pre_tolerance: Duration,
333 post_tolerance: Duration,
334) -> Result<(), VF> {
335 let start = *validity.start();
336 let end = *validity.end();
337 let validity = start.saturating_sub(pre_tolerance)..=end.saturating_add(post_tolerance);
338 check_validity_time(now, validity)
339}