1
//! Implementation of `compute-mds` method
2

            
3
use super::*;
4

            
5
/// routerdesc to microdesc processor
6
#[derive(Deftly)]
7
#[derive_deftly(New)]
8
pub(crate) struct Processor<'o> {
9
    /// Nominal time, at which to check routerdesc validity
10
    #[deftly(new(arg))]
11
    nominal_time: SystemTime,
12

            
13
    /// Where to write generated microdescs
14
    #[deftly(new(arg))]
15
    mds_out: &'o mut Writing,
16

            
17
    /// Where to write the metadata
18
    #[deftly(new(arg))]
19
    meta_out: &'o mut Writing,
20

            
21
    /// Input descriptors that we've already processed
22
    dedup_inputs: HashSet<RdDigest>,
23

            
24
    /// Output descriptors that we've already written
25
    dedup_outputs: HashSet<MdDigest>,
26

            
27
    /// Consensus methods that triggered `Bug` (for logging deduplication)
28
    bugs_occurred: HashSet<ConsensusMethod>,
29
}
30

            
31
/// One entry in the metadata file
32
///
33
/// This is, in fact, a whole network document, which has only an intro item.
34
#[derive(Deftly)]
35
#[derive_deftly(tor_netdoc::NetdocEncodable)]
36
struct MetaFileEntry {
37
    /// The item
38
    m: MetaItem,
39
}
40

            
41
/// Values in a metadata item
42
#[derive(Deftly)]
43
#[derive_deftly(tor_netdoc::ItemValueEncodable)]
44
struct MetaItem {
45
    /// `CONSENSUS_VERSION`
46
    method: ConsensusMethod,
47

            
48
    /// `RSAID
49
    hk_relayid_rsa: Base64Fingerprint,
50

            
51
    /// `DID`
52
    hk_relayid_ed: Ed25519Public,
53

            
54
    /// `DD`
55
    h_routerdesc: RdDigest,
56

            
57
    /// `MD`
58
    h_md: MdDigest,
59
}
60

            
61
/// Router descriptor hash
62
type RdDigest = FixedB64<{ tor_netdoc::doc::routerdesc::DOC_DIGEST_LEN }>;
63
/// Microdescriptor hash
64
type MdDigest = FixedB64<{ tor_netdoc::doc::microdesc::DOC_DIGEST_LEN }>;
65

            
66
/// Routerdesc, parsed, with its location in the file
67
struct InputDescriptor {
68
    /// Start byte position
69
    start: usize,
70

            
71
    /// End byte position
72
    end: usize,
73

            
74
    /// SHA-1 (as required for the metadata file)
75
    h_routerdesc: RdDigest,
76

            
77
    /// The parsed routerdesc
78
    rd: RouterDesc,
79
}
80

            
81
impl<'o> Processor<'o> {
82
    /// Process one input file
83
2
    pub(crate) fn process_input(
84
2
        &mut self,
85
2
        mut input: parse2::ParseInput<'_>,
86
2
    ) -> Result<(), CliError> {
87
2
        input.retain_unknown_values();
88

            
89
2
        let router_descs = parse2::parse_netdoc_multiple_sophisticated::<
90
2
            CTorAnnotated<RouterDescUnverified>,
91
2
        >(&input)?;
92

            
93
10
        for (rd, start, end) in router_descs {
94
10
            let rd = rd.map(|CTorAnnotated(rd)| rd);
95

            
96
10
            let rd = match self.parse_verify_descriptor(rd, start, end) {
97
10
                Ok(y) => y,
98
                Err(e) => {
99
                    eprintln!("bad routerdesc at bytes {start}..{end}: {}", e.report(),);
100
                    continue;
101
                }
102
            };
103

            
104
10
            self.process_descriptor(&rd)?;
105
        }
106
2
        Ok(())
107
2
    }
108

            
109
    /// Resolve a parsed descriptor (or parse error) to a `InputDescriptor`
110
    ///
111
    /// Mostly, this function exists to provide an error handling context:
112
    /// errors which occur here are (always) logged,
113
    /// with the byte positions but not the routerdesc hash.
114
10
    fn parse_verify_descriptor(
115
10
        &self,
116
10
        rd: Result<RouterDescUnverified, parse2::ParseError>,
117
10
        start: usize,
118
10
        end: usize,
119
10
    ) -> Result<InputDescriptor, anyhow::Error> {
120
        // According to the plugin spec, the final routerdesc might be truncated.
121
        // That is be handled here, and so generates the log message, above.
122
        // That seems OK.
123
10
        let rd = rd.context("parse")?;
124

            
125
10
        let h_routerdesc = FixedB64(
126
10
            rd.sigs
127
10
                .hashes
128
10
                .sha1
129
10
                .ok_or_else(|| anyhow!("not hashed with SHA-1, wrong kind of signature?"))?,
130
        );
131

            
132
10
        let rd = rd
133
10
            .verify()
134
10
            .context("verify")?
135
10
            .if_valid_at(&self.nominal_time)
136
10
            .context("check timeliness")?;
137

            
138
10
        Ok(InputDescriptor {
139
10
            h_routerdesc,
140
10
            rd,
141
10
            start,
142
10
            end,
143
10
        })
144
10
    }
145

            
146
    /// Process a descriptor into its microdescriptor(s) and write them out
147
10
    fn process_descriptor(&mut self, rd: &InputDescriptor) -> Result<(), CliError> {
148
        use tor_error::ErrorKind as EK;
149

            
150
10
        if !self.dedup_inputs.insert(rd.h_routerdesc.clone()) {
151
            return Ok(());
152
10
        }
153

            
154
10
        let (errs, mds) = match tor_dirauth::consensus::compute_supported_microdescs(&rd.rd) {
155
10
            Ok(y) => (vec![], y),
156
            Err(r) => r,
157
        };
158

            
159
10
        for (method, err) in errs.into_iter().rev() {
160
            macro_rules! log_error { { $fmt:literal } => {
161
                eprintln!(
162
                    concat!($fmt, ": {}"),
163
                    format_args!(
164
                        "rd at bytes {}..{} with hash {}",
165
                        rd.start,
166
                        rd.end,
167
                        rd.h_routerdesc,
168
                    ),
169
                    err.report(),
170
                    method=method,
171
                )
172
            } }
173

            
174
            match err.kind() {
175
                EK::RemoteProtocolViolation => {
176
                    // ie, bad routerdesc.  ignore it then, presumably.
177
                }
178
                EK::BadApiUsage | EK::Internal => {
179
                    if self.bugs_occurred.insert(method.into()) {
180
                        log_error!("bug in method {method} triggered by {}");
181
                    }
182
                }
183
                _other_kind => {
184
                    log_error!("problem due to {}, method {method}");
185
                    // Report only one error per md.
186
                    break;
187
                }
188
            }
189
        }
190

            
191
10
        for (methods, md) in mds {
192
10
            let h_md = FixedB64(Sha256::digest(&md).into());
193

            
194
10
            if self.dedup_outputs.insert(h_md.clone()) {
195
15
                self.mds_out.append_with(|w| write!(w, "{md}"))?;
196
            }
197

            
198
10
            for method in methods.methods {
199
10
                let meta = encode_netdoc_unsigned(&[MetaFileEntry {
200
                    m: MetaItem {
201
10
                        method,
202
10
                        h_md: h_md.clone(),
203
10
                        h_routerdesc: rd.h_routerdesc.clone(),
204
10
                        hk_relayid_ed: rd.rd.identity_ed25519.get()?.id_ed25519.into(),
205
10
                        hk_relayid_rsa: rd.rd.signing_key.to_rsa_identity().into(),
206
                    },
207
                }])?;
208

            
209
15
                self.meta_out.append_with(|w| write!(w, "{meta}"))?;
210
            }
211
        }
212

            
213
10
        Ok(())
214
10
    }
215
}
216

            
217
#[cfg(test)]
218
mod test {
219
    // @@ begin test lint list maintained by maint/add_warning @@
220
    #![allow(clippy::bool_assert_comparison)]
221
    #![allow(clippy::clone_on_copy)]
222
    #![allow(clippy::dbg_macro)]
223
    #![allow(clippy::mixed_attributes_style)]
224
    #![allow(clippy::print_stderr)]
225
    #![allow(clippy::print_stdout)]
226
    #![allow(clippy::single_char_pattern)]
227
    #![allow(clippy::unwrap_used)]
228
    #![allow(clippy::unchecked_time_subtraction)]
229
    #![allow(clippy::useless_vec)]
230
    #![allow(clippy::needless_pass_by_value)]
231
    #![allow(clippy::string_slice)] // See arti#2571
232
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
233
    use super::*;
234
    use std::fs;
235
    use test_temp_dir::test_temp_dir;
236
    use tor_dirauth::consensus::SupportedConsensusMethod;
237
    use tor_netdoc::{
238
        doc::microdesc::Microdesc,
239
        parse2::{ParseInput, parse_netdoc_multiple_with_offsets},
240
        testdata_live::{self, relay_document_by_nick},
241
        types::B64,
242
    };
243

            
244
    #[test]
245
    fn test_compute_mds() -> anyhow::Result<()> {
246
        let consensus = testdata_live::netstatus_plain();
247
        let nominal_time = consensus.preamble.lifetime.valid_after();
248
        let tmp = test_temp_dir!();
249

            
250
        tmp.used_by(|tmp| {
251
            let tmp: String = tmp
252
                .as_os_str()
253
                .to_str()
254
                .expect("tmp must be utf-8")
255
                .to_owned();
256
            let mds_f = format!("{tmp}/mds");
257
            let meta_f = format!("{tmp}/meta");
258
            let mk_out = |f: &str| FilenameOrStdio::Path(f.to_owned()).start_writing();
259
            let mut mds_out = mk_out(&mds_f)?;
260
            let mut meta_out = mk_out(&meta_f)?;
261

            
262
            let mut processor = Processor::new(nominal_time, &mut mds_out, &mut meta_out);
263

            
264
            let concatenated_descs = testdata_live::RELAY_DESCRIPTORS
265
                .iter()
266
                .map(|desc| format!("@ nick={}\n{}", desc.nick, desc.data.plain))
267
                .collect::<String>();
268

            
269
            processor.process_input(ParseInput::new(&concatenated_descs, "<input descriptors>"))?;
270

            
271
            mds_out.finish()?;
272
            meta_out.finish()?;
273

            
274
            let meta_made = fs::read_to_string(&meta_f)?;
275
            let mds_made = fs::read_to_string(&mds_f)?;
276

            
277
            eprintln!("@@ EMTA_OUT:\n{meta_made}");
278
            eprintln!("@@ MDS_OUT:\n{mds_made}");
279

            
280
            let mds = parse_netdoc_multiple_with_offsets::<Microdesc>(&ParseInput::new(
281
                &mds_made,
282
                "<out mds>",
283
            ))?;
284

            
285
            let md_hashes = mds
286
                .iter()
287
                .map(|&(_, start, end)| Sha256::digest(&mds_made[start..end]).into())
288
                .collect::<HashSet<[u8; 32]>>();
289

            
290
            let n_expected = testdata_live::RELAY_DESCRIPTORS.len();
291
            eprintln!("expecting at least {n_expected} mds");
292

            
293
            assert!(
294
                mds.len() >= n_expected,
295
                "{} is too few mds;\n{mds:?}",
296
                mds.len(),
297
            );
298

            
299
            let meta_made = meta_made.split_terminator('\n');
300

            
301
            for method in SupportedConsensusMethod::iter_all() {
302
                eprintln!("checking method {method}");
303

            
304
                let n_got = meta_made
305
                    .clone()
306
                    .filter(|l| {
307
                        // ad-hoc parsing, so we have an independent Cross-check
308

            
309
                        let mut l = l.split_ascii_whitespace();
310
                        let mut next = || l.next().unwrap();
311
                        macro_rules! next { {} => { next().parse().unwrap() } }
312

            
313
                        assert_eq!(next(), "m");
314

            
315
                        let m: ConsensusMethod = next!();
316
                        let m: SupportedConsensusMethod = m.try_into().unwrap();
317

            
318
                        let rsaid: Base64Fingerprint = next!();
319
                        let edid: B64 = next!();
320
                        let rd_hash: B64 = next!();
321
                        let md_hash: B64 = next!();
322

            
323
                        let rs_plain = consensus
324
                            .routers
325
                            .iter()
326
                            .find(|rs| rs.r.doc_digest[..] == **rd_hash)
327
                            .expect("missing doc digest");
328

            
329
                        let rs_md = relay_document_by_nick(
330
                            rs_plain.r.nickname.as_ref(),
331
                            &testdata_live::relay_microdescs(),
332
                        );
333

            
334
                        assert_eq!(rsaid, rs_plain.r.identity);
335
                        assert_eq!(*edid, rs_md.ed25519_id.pk.as_bytes());
336
                        assert!(md_hashes.contains(&**md_hash));
337

            
338
                        m == method
339
                    })
340
                    .count();
341
                assert!(n_got >= n_expected, "{} is too few", n_got);
342
            }
343

            
344
            Ok(())
345
        })
346
        .into_untracked()
347
    }
348
}