tor_netdoc/doc/extra_info.rs
1//! Extra-Info Document Implementation.
2//!
3//! Historically, there were no microdescriptors and bootstrapping
4//! involved downloading all router descriptors. In order to save
5//! space, various information not required for bootstrapping was
6//! outsourced into an external document called extra-info, which
7//! itself would then be referred to by original router descriptor.
8//!
9//! In times of microdescriptors, this has become meaningless, yet it
10//! still continues to be a part of the protocol.
11//!
12//! <https://spec.torproject.org/dir-spec/extra-info-document-format.html>
13
14use derive_deftly::Deftly;
15use tor_cert::KeyUnknownCert;
16use tor_llcrypto::pk::ed25519;
17
18use crate::{
19 doc::routerdesc::RouterDesc,
20 parse2::VerifyFailed,
21 types::{descriptor::*, *},
22};
23
24/// Additional information about a relay not contained in it's router
25/// descriptor.
26///
27/// See the module documentation for more information.
28///
29/// <https://spec.torproject.org/dir-spec/extra-info-document-format.html>
30#[derive(Debug, Clone, PartialEq, Deftly)]
31#[derive_deftly(NetdocParseableUnverified, NetdocEncodable)]
32#[non_exhaustive]
33pub struct ExtraInfo {
34 /// `extra-info` — Introduce a server's extra-info
35 ///
36 /// <https://spec.torproject.org/dir-spec/extra-info-document-format.html#extra-info>
37 pub extra_info: ExtraInfoIntroItem,
38
39 /// `identity-ed25519` --- Specify the router's ed25519 identity.
40 ///
41 /// <https://spec.torproject.org/dir-spec/server-descriptor-format.html#item:identity-ed25519>
42 pub identity_ed25519: EmbeddedCert<Ed25519IdentityCert, KeyUnknownCert>,
43
44 /// `published` --- Time this descriptor (and extra-info) was generated.
45 ///
46 /// <https://spec.torproject.org/dir-spec/server-descriptor-format.html#item:published>
47 #[deftly(netdoc(single_arg))]
48 pub published: Iso8601TimeSp,
49}
50
51/// Signatures for an [`ExtraInfo`] document.
52///
53/// The signature logic is the same as with router descriptors.
54///
55/// Technically redundant because the hash is referenced by the router
56/// descriptor in a signed fashion already.
57#[derive(Debug, Clone, PartialEq, Deftly)]
58#[derive_deftly(NetdocParseableSignatures, NetdocEncodable)]
59#[deftly(netdoc(signatures(hashes_accu = "RouterHashAccu")))]
60#[non_exhaustive]
61pub struct ExtraInfoSignatures {
62 /// `router-sig-ed25519` --- Ed25519 signature
63 ///
64 /// <https://spec.torproject.org/dir-spec/server-descriptor-format.html#item:router-sig-ed25519>
65 pub router_sig_ed25519: RouterSigEd25519,
66
67 /// `router-signature` --- RSA signature
68 ///
69 /// <https://spec.torproject.org/dir-spec/server-descriptor-format.html#item:router-signature>
70 pub router_signature: RouterSignature,
71}
72
73/// Introduction line of an extra-info document.
74///
75/// <https://spec.torproject.org/dir-spec/extra-info-document-format.html#extra-info--introduce-a-servers-extra-info>
76#[derive(Debug, Clone, PartialEq, Deftly)]
77#[derive_deftly(ItemValueParseable, ItemValueEncodable)]
78#[non_exhaustive]
79pub struct ExtraInfoIntroItem {
80 /// A valid router [`Nickname`].
81 pub nickname: Nickname,
82 /// Fingerprint of the router's RSA identity.
83 pub fingerprint: Fingerprint,
84}
85
86impl ExtraInfoUnverified {
87 /// Verifies an extra-info document.
88 ///
89 /// This verification *requires* an already verified [`RouterDesc`].
90 ///
91 /// We do not work with time bounded values here, because all respective
92 /// time bounded values in extra-info documents match up with the respective
93 /// values in their associated [`RouterDesc`]. Instead, we simply require
94 /// [`RouterDesc`] to be timely for this to be timely as well.
95 ///
96 /// For an extra-info document to be valid, the following constraints apply:
97 /// * [`RouterDesc::router`] implies [`ExtraInfoIntroItem::nickname`]
98 /// * [`RouterDesc::signing_key`] implies [`ExtraInfoIntroItem::fingerprint`]
99 /// * [`RouterDesc::identity_ed25519`] == [`ExtraInfo::identity_ed25519`]
100 /// * [`RouterDesc::published`] == [`ExtraInfo::published`]
101 /// * [`RouterDesc::extra_info_digest`] implies this document.
102 /// * This document has valid signatures.
103 pub fn verify(self, rd: &RouterDesc) -> Result<ExtraInfo, VerifyFailed> {
104 let (mut body, sigs): (ExtraInfo, _) = (self.body, self.sigs);
105
106 // Check whether the nicknames match.
107 if rd.router.nickname != body.extra_info.nickname {
108 return Err(VerifyFailed::Inconsistent);
109 }
110
111 // Check whether the fingerprint is as expected.
112 // Keep in mind, that we cannot use the fingerprint field in the router
113 // descriptor, as it is optional.
114 if rd.signing_key.to_rsa_identity() != body.extra_info.fingerprint.0 {
115 return Err(VerifyFailed::Inconsistent);
116 }
117
118 // Check whether the Ed25519 signing key certificate is the same.
119 if rd.identity_ed25519.raw_unverified() != body.identity_ed25519.raw_unverified() {
120 return Err(VerifyFailed::Inconsistent);
121 }
122 // We do not re-verify the certificate but rather clone it.
123 // This is okay, because we just verified that the unverified form in
124 // this document is equal to the unverified form in the already verified
125 // router descriptor, which implies that verifying it would lead to the
126 // same (verified) result again.
127 body.identity_ed25519 = rd.identity_ed25519.clone();
128 let sign_ed25519 = body
129 .identity_ed25519
130 .get()
131 .expect("rd must contain verified identity_ed25519")
132 .sign_ed25519;
133
134 // Check whether the published timestamp is the same.
135 if body.published != rd.published {
136 return Err(VerifyFailed::Inconsistent);
137 }
138
139 // Already obtain the hashes we got by having hashed the document.
140 // We need to do that now for the next check but need it later on too.
141 // A failure of this accumulation is a full verification failure.
142 let this_sha1 = sigs.hashes.sha1.ok_or(VerifyFailed::VerifyFailed)?;
143 let this_sha256 = sigs.hashes.sha256.ok_or(VerifyFailed::VerifyFailed)?;
144
145 // Check whether the router descriptor implies this document.
146 // If this field is not set, we should have not obtained this document
147 // in the first place ...
148 let Some(expected_digests) = rd.extra_info_digest.as_ref() else {
149 return Err(VerifyFailed::Inconsistent);
150 };
151 // TODO DIRMIRROR: IMPORTANT, we MUST also verify the SHA-256 hash.
152 // However, we cannot use the normal hash accumulator due to a
153 // long-standig bug in the ExtraInfoDigests value, where the indicated
154 // value there refers to the full document (including the signature
155 // bytes) and not up until "router-sig-ed25519 ". The solution would
156 // most likely involve writing a different accumulator for this and/or
157 // extending RouterHashAccu with an additional field?
158 if *expected_digests.sha1 != this_sha1 {
159 return Err(VerifyFailed::Inconsistent);
160 }
161
162 // Verify the actual outer document signatures.
163 ed25519::PublicKey::try_from(sign_ed25519)
164 .map_err(|_| VerifyFailed::Other)?
165 .verify(&this_sha256, &sigs.sigs.router_sig_ed25519.0)?;
166 rd.signing_key
167 .verify(&this_sha1, &sigs.sigs.router_signature.0)?;
168
169 Ok(body)
170 }
171}
172
173#[cfg(test)]
174mod test {
175 // @@ begin test lint list maintained by maint/add_warning @@
176 #![allow(clippy::bool_assert_comparison)]
177 #![allow(clippy::clone_on_copy)]
178 #![allow(clippy::dbg_macro)]
179 #![allow(clippy::mixed_attributes_style)]
180 #![allow(clippy::print_stderr)]
181 #![allow(clippy::print_stdout)]
182 #![allow(clippy::single_char_pattern)]
183 #![allow(clippy::unwrap_used)]
184 #![allow(clippy::unchecked_time_subtraction)]
185 #![allow(clippy::useless_vec)]
186 #![allow(clippy::needless_pass_by_value)]
187 #![allow(clippy::string_slice)] // See arti#2571
188 //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
189
190 use std::collections::HashMap;
191
192 use tor_checkable::TimeBound;
193
194 use super::*;
195 use crate::doc::routerdesc::RouterDescUnverified;
196 use crate::parse2::{self, NetdocParseableUnverified, ParseInput};
197
198 /// Simple test that just validates all extra-infos in testdata2/.
199 #[test]
200 fn simple() {
201 // Obtain the accompanying router descriptors.
202 let routers = parse2::parse_netdoc_multiple::<RouterDescUnverified>(&ParseInput::new(
203 include_str!("../../testdata2/cached-descriptors.new"),
204 "cached-descriptors",
205 ))
206 .unwrap()
207 .into_iter()
208 // We need to verify because we make use of the embedded certificats.
209 // However, we skip the time verification because it is out of scope
210 // for this module.
211 .map(|rd| rd.verify().unwrap().dangerously_assume_timely())
212 .collect::<Vec<RouterDesc>>();
213
214 // Now, parse all extra info documents and store them by their hash.
215 let extras_list = parse2::parse_netdoc_multiple::<ExtraInfoUnverified>(&ParseInput::new(
216 include_str!("../../testdata2/cached-extrainfo.new"),
217 "cached-extrainfo",
218 ))
219 .unwrap();
220 let mut extras = HashMap::new();
221 for extra in extras_list {
222 // We need to use the sha1 due to a long-lived bug in the sha256,
223 // whereas the field contains the signature over the entire document,
224 // rather than just up until router-sig-ed25519 ...
225 let sha1 = extra.inspect_unverified().1.hashes.sha1.unwrap();
226 // Ensure there are no duplicates.
227 assert!(extras.insert(sha1, extra).is_none());
228 }
229
230 // We now have all router descriptors and all extra infos in a
231 // content-addressable store. Now we iterate over every router
232 // descriptor, look up the extra-info and take it from the hash
233 // map, followed by an actual verification. In the end, all
234 // verification calls should have been successful and the HashMap
235 // should be empty.
236 for router in routers {
237 let sha1 = router.extra_info_digest.clone().unwrap().sha1.0;
238 let extra = extras.remove(&sha1).unwrap();
239 extra.verify(&router).unwrap();
240 }
241 assert!(extras.is_empty());
242 }
243}