1
//! Computing microdescriptors
2

            
3
use super::*;
4

            
5
/// Error creating a microdescriptor
6
#[derive(Debug, Clone, thiserror::Error)]
7
#[non_exhaustive]
8
pub enum MicrodescError {
9
    /// Internal error
10
    #[error("internal error")]
11
    Internal(#[from] Bug),
12
}
13

            
14
/// Compute a microdescriptor from a routerdesc
15
///
16
/// <https://spec.torproject.org/dir-spec/computing-microdescriptors.html>
17
32
pub fn compute_microdesc(
18
32
    rd: &RouterDesc,
19
32
    meth: &TrackedConsensusMethod,
20
32
) -> Result<Microdesc, MicrodescError> {
21
32
    let mut family = RelayFamily::clone(&rd.family); // avoids Arc::Clone
22
32
    if !family.is_empty() {
23
26
        family.push(rd.signing_key.to_rsa_identity());
24
26
    }
25
32
    let family = family.intern(); // also normalises
26

            
27
32
    let family_ids: RelayFamilyIds = rd
28
32
        .family_cert
29
32
        .iter()
30
42
        .map(|cert| Ok::<_, Bug>(cert.get()?.family_ed25519.into()))
31
32
        .try_collect()?;
32

            
33
30
    let ipv4_policy =
34
30
        ip_summary::summarise_policy_v4_approximate(&rd.ipv4_policy, meth)?.into_intern();
35

            
36
30
    let m = Microdesc {
37
30
        family,
38
30
        family_ids,
39
30
        ipv4_policy,
40
30
        ipv6_policy: rd.ipv6_policy.clone(),
41
        ..MicrodescConstructor {
42
30
            ntor_onion_key: rd.ntor_onion_key.clone(),
43
30
            ed25519_id: rd.identity_ed25519.get()?.id_ed25519.into(),
44
        }
45
30
        .construct()
46
    };
47
30
    Ok(m)
48
32
}
49

            
50
/// Microdescriptors computed from one routerdesc, for various supported consensus methods
51
pub type MicrodescsForRouterDesc = BTreeMap<tor_netdoc::doc::netstatus::ConsensusMethods, String>;
52

            
53
/// Errors calculating one or more microdescriptors
54
pub type MicrodescsErrors = Vec<(SupportedConsensusMethod, MicrodescError)>;
55

            
56
/// Computes the microdescriptor for this descriptor, for each supported consensus method
57
///
58
/// If all goes well returns `MicrodescsForRouterDesc`, a map from methods to strings.
59
/// The map is guaranteed not to have the same *value* for different keys:
60
/// if different emthods yield the same microdesc, thy will be represented as
61
/// a single `ConsensusMethods` set mapping to a single `String`.
62
///
63
/// Hashing (calculating and collating document digests) is done by the caller of this function.
64
///
65
/// If any microdesc couldn't be computed (currently, only possible due to internal errors)
66
/// returns an `Err` containing both the failed consensus method(s) and the corresponding errors,
67
/// and the successful results (if any).
68
///
69
/// This unusual error handling is so that a handful of strange routerdescs,
70
/// that trigger bugs in our code, do not cause the whole algorithm to collapse.
71
/// See torspec!522  TODO DIRAUTH turn this into a proper spec ref when that's merged.
72
22
pub fn compute_supported_microdescs(
73
22
    rd: &RouterDesc,
74
22
) -> Result<MicrodescsForRouterDesc, (MicrodescsErrors, MicrodescsForRouterDesc)> {
75
22
    compute_supported_generic(
76
        //
77
22
        SupportedConsensusMethod::iter_all(),
78
22
        |tracker| {
79
22
            let md = compute_microdesc(rd, tracker)?;
80
20
            let md = encode_netdoc_unsigned([&md])?;
81
20
            Ok(md)
82
22
        },
83
    )
84
22
}
85

            
86
/// "Good" output from [`compute_supported_generic`]
87
type Outputs<O> = BTreeMap<tor_netdoc::doc::netstatus::ConsensusMethods, O>;
88

            
89
/// Compute `O` for all supported consensus methods
90
///
91
/// This is the core of `compute_supported_microdescs`,
92
/// but made generic so that we can pass an interesting calculation function, for testing.
93
#[allow(clippy::type_complexity)] // return type is sadly rather complex
94
24
fn compute_supported_generic<O: Clone + Eq + Hash, E: From<Bug>>(
95
24
    all_methods: impl Iterator<Item = SupportedConsensusMethod>,
96
24
    computor: impl Fn(&TrackedConsensusMethod) -> Result<O, E>,
97
24
) -> Result<Outputs<O>, (Vec<(SupportedConsensusMethod, E)>, Outputs<O>)> {
98
24
    let mut results = HashMap::<O, tor_netdoc::doc::netstatus::ConsensusMethods>::new();
99
24
    let mut errors = vec![];
100

            
101
24
    let mut last = None::<(ConsensusMethodRange, O)>;
102
222
    for method in all_methods {
103
222
        (|| {
104
222
            let entry = if let Some((range, prev)) = &last
105
198
                && range.contains(&method)
106
            {
107
178
                results
108
178
                    .get_mut(prev)
109
178
                    .ok_or_else(|| internal!("prev not inserted!"))?
110
            } else {
111
44
                let tracker = TrackedConsensusMethod::new(method);
112
44
                let output = computor(&tracker)?;
113
40
                last = Some((tracker.finish_get_equivalent(), output.clone()));
114
40
                results.entry(output).or_default()
115
            };
116
218
            entry.methods.insert(method.into());
117
218
            Ok(())
118
        })()
119
222
        .unwrap_or_else(|e| {
120
4
            errors.push((method, e));
121
4
        });
122
    }
123

            
124
24
    let results = results
125
24
        .into_iter()
126
34
        .map(|(output, meths)| (meths, output))
127
24
        .collect();
128
24
    if errors.is_empty() {
129
20
        Ok(results)
130
    } else {
131
4
        Err((errors, results))
132
    }
133
24
}
134

            
135
impl tor_error::HasKind for MicrodescError {
136
    fn kind(&self) -> tor_error::ErrorKind {
137
        use MicrodescError as ME;
138
        use tor_error::ErrorKind as EK;
139
        match self {
140
            ME::Internal { .. } => EK::Internal,
141
        }
142
    }
143
}
144

            
145
#[cfg(test)]
146
mod test {
147
    // @@ begin test lint list maintained by maint/add_warning @@
148
    #![allow(clippy::bool_assert_comparison)]
149
    #![allow(clippy::clone_on_copy)]
150
    #![allow(clippy::dbg_macro)]
151
    #![allow(clippy::mixed_attributes_style)]
152
    #![allow(clippy::print_stderr)]
153
    #![allow(clippy::print_stdout)]
154
    #![allow(clippy::single_char_pattern)]
155
    #![allow(clippy::unwrap_used)]
156
    #![allow(clippy::unchecked_time_subtraction)]
157
    #![allow(clippy::useless_vec)]
158
    #![allow(clippy::needless_pass_by_value)]
159
    #![allow(clippy::string_slice)] // See arti#2571
160
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
161
    use super::*;
162
    use std::rc::Rc;
163
    use tor_checkable::TimeBound as _;
164
    use tor_error::ErrorReport as _;
165
    use tor_netdoc::{
166
        assert_eq_or_diff,
167
        doc::routerdesc::RouterDescUnverified,
168
        parse2::{NetdocParseableUnverified as _, ParseInput, parse_netdoc},
169
        test_support::regsub,
170
        testdata_live,
171
    };
172

            
173
    fn get_router_desc(relay: &testdata_live::PerRelay) -> anyhow::Result<RouterDesc> {
174
        let rd_txt = relay.data.plain;
175
        let rd: RouterDescUnverified = parse_netdoc(&ParseInput::new(rd_txt, relay.nick))?;
176
        let rd = rd.verify()?.dangerously_assume_timely();
177
        Ok(rd)
178
    }
179

            
180
    #[test]
181
    fn microdescs() -> anyhow::Result<()> {
182
        let method = TrackedConsensusMethod::new(
183
            // Test with method 100; will need to bump this occasionally,
184
            // and/or add more regsub fixups, if this test fails.
185
            ConsensusMethod(100).try_into()?,
186
        );
187

            
188
        for relay in testdata_live::RELAY_DESCRIPTORS {
189
            let rd = get_router_desc(relay)?;
190
            let md = compute_microdesc(&rd, &method)?;
191
            let md_txt = encode_netdoc_unsigned([&md])?;
192

            
193
            let mut exp_md = relay.data.md.to_owned();
194
            regsub(
195
                &mut exp_md,
196
                r#"(?x) ^ onion-key \n
197
                          -----BEGIN\ .*----- \n
198
                          [^-]+
199
                          -----END\ .*----- \n
200
                "#,
201
                "onion-key\n",
202
            );
203

            
204
            assert_eq_or_diff!(md_txt, exp_md, "for relay {}", relay.nick);
205
        }
206

            
207
        Ok(())
208
    }
209

            
210
    #[test]
211
    fn all_microdesc() -> anyhow::Result<()> {
212
        for relay in testdata_live::RELAY_DESCRIPTORS {
213
            let rd = get_router_desc(relay)?;
214
            let descs = compute_supported_microdescs(&rd).expect("no bugs");
215
            for md in descs.values() {
216
                let _md: Microdesc = parse_netdoc(&ParseInput::new(md, relay.nick))?;
217
            }
218
            itertools::assert_equal(
219
                descs
220
                    .keys()
221
                    .flat_map(|m| m.methods.iter())
222
                    .sorted()
223
                    .copied(),
224
                SupportedConsensusMethod::iter_all().map(|m| *m),
225
            );
226
        }
227
        Ok(())
228
    }
229

            
230
    #[test]
231
    #[allow(clippy::len_zero)] // assert!(!foo.is_empty()) is just terrible with all the subtle !
232
    fn buggy_microdesc() -> anyhow::Result<()> {
233
        // Test error handling of microdescriptor construction.
234
        // It turns out we can sabotage microdescriptor construction if we bypass
235
        // RouterDesc::verify, because that fails to populate family info that we need.
236
        let relay = &testdata_live::RELAY_DESCRIPTORS[0];
237
        let rd_txt = relay.data.plain;
238
        let rd: RouterDescUnverified = parse_netdoc(&ParseInput::new(rd_txt, relay.nick))?;
239
        let rd = rd.unwrap_unverified().0;
240
        let (errors, descs) = compute_supported_microdescs(&rd)
241
            .expect_err("should access unverified EmbeddedCert, throwing Bug");
242
        assert!(descs.len() == 0);
243
        assert!(errors.len() > 0);
244
        for (method, error) in errors {
245
            let _: SupportedConsensusMethod = method;
246
            let msg = error.report().to_string();
247
            let exp = "attempted to access verified data of unverified EmbeddedCert";
248
            assert!(msg.contains(exp), "{msg:?}");
249
        }
250
        Ok(())
251
    }
252

            
253
    #[test]
254
    fn all_supported_track_coalesce() {
255
        use crate::consensus::tracked_method::test::interesting_function;
256

            
257
        #[derive(Debug, thiserror::Error)]
258
        enum TestCaseError {
259
            #[error("expected")]
260
            Expected,
261
            #[error("{0:?}")]
262
            Internal(#[from] Bug),
263
        }
264

            
265
        let n_calls = Cell::new(0);
266

            
267
        let (errors, good) = compute_supported_generic(
268
            (1..=100)
269
                .map(ConsensusMethod)
270
                .map(SupportedConsensusMethod::new_unchecked),
271
            |tracker| {
272
                n_calls.update(|n_calls| n_calls + 1);
273
                eprintln!("called with {tracker:?}");
274

            
275
                if tracker == 80 {
276
                    Err(TestCaseError::Expected)
277
                } else {
278
                    let v = interesting_function(tracker);
279
                    Ok(Rc::new(v))
280
                }
281
            },
282
        )
283
        .expect_err("80 fails");
284

            
285
        // Check that we got the expected error
286
        match &*errors {
287
            [(m, TestCaseError::Expected)] if *m == 80 => {}
288
            other => panic!("{other:?}"),
289
        }
290

            
291
        // Check that results are consistent with calculating every value
292
        for (ms, o) in good {
293
            for &m in &ms.methods {
294
                let m = SupportedConsensusMethod::new_unchecked(m);
295
                let exp = interesting_function(&TrackedConsensusMethod::new(m));
296
                assert_eq!(*o, exp);
297
            }
298
        }
299

            
300
        // Check how many calculations were needed:
301
        assert_eq!(n_calls.get(), 11);
302
    }
303
}