1
//! network status documents - types that vary by flavour
2
//!
3
//! **This file is reincluded multiple times**,
4
//! once for each consensus flavour, and once for votes.
5
//!
6
//! Each time, with different behaviour for the macros `ns_***`.
7
//!
8
//! Thus, this file generates (for example) all three of:
9
//! `ns::NetworkStatus` aka `NetworkStatusNs`,
10
//! `NetworkStatusMd` and `NetworkStatusVote`.
11
//!
12
//! (We treat votes as a "flavour".)
13

            
14
use super::super::*;
15

            
16
/// Toplevel document string for error reporting
17
const TOPLEVEL_DOCTYPE_FOR_ERROR: &str =
18
    ns_expr!("NetworkStatusVote", "NetworkStatusNs", "NetworkStatusMd",);
19

            
20
/// The real router status entry type.
21
pub type Router = ns_type!(
22
    crate::doc::netstatus::VoteRouterStatus,
23
    crate::doc::netstatus::PlainRouterStatus,
24
    crate::doc::netstatus::MdRouterStatus,
25
);
26

            
27
/// Network status document (vote, consensus, or microdescriptor consensus) - body
28
///
29
/// The preamble items are members of this struct.
30
/// The rest are handled as sub-documents.
31
#[derive(Deftly, Clone, Debug)]
32
#[derive_deftly(NetdocParseable, NetdocSigned)]
33
#[deftly(netdoc(doctype_for_error = "TOPLEVEL_DOCTYPE_FOR_ERROR"))]
34
#[non_exhaustive]
35
pub struct NetworkStatus {
36
    /// `network-status-version`
37
    pub network_status_version: (NdaNetworkStatusVersion, NdaNetworkStatusVersionFlavour),
38

            
39
    /// `vote-status`
40
    pub vote_status: NdiVoteStatus,
41

            
42
    /// `published`
43
    pub published: ns_type!((NdaSystemTimeDeprecatedSyntax,), Option<Void>,),
44

            
45
    /// `valid-after`
46
    pub valid_after: (NdaSystemTimeDeprecatedSyntax,),
47

            
48
    /// `valid-until`
49
    pub valid_until: (NdaSystemTimeDeprecatedSyntax,),
50

            
51
    /// `voting-delay`
52
    pub voting_delay: NdiVotingDelay,
53

            
54
    /// `params`
55
    #[deftly(netdoc(default))]
56
    pub params: NdiParams,
57

            
58
    /// Authority section
59
    #[deftly(netdoc(subdoc))]
60
    pub authority: NddAuthoritySection,
61

            
62
    /// `r` subdocuments
63
    #[deftly(netdoc(subdoc))]
64
    pub r: Vec<Router>,
65

            
66
    /// `directory-footer` section (which we handle as a sub-document)
67
    #[deftly(netdoc(subdoc))]
68
    pub directory_footer: Option<NddDirectoryFooter>,
69
}
70

            
71
/// Signatures on a network status document
72
#[derive(Deftly, Clone, Debug)]
73
#[derive_deftly(NetdocParseable)]
74
#[deftly(netdoc(signatures))]
75
#[non_exhaustive]
76
pub struct NetworkStatusSignatures {
77
    /// `directory-signature`s
78
    pub directory_signature: ns_type!(NdiDirectorySignature, Vec<NdiDirectorySignature>),
79
}
80

            
81
/// `vote-status` value
82
///
83
/// In a non-demo we'd probably abolish this,
84
/// using `NdaStatus` directly in `NddNetworkStatus`
85
/// impl of `ItemValueParseable` for tuples.
86
#[derive(Deftly, Clone, Debug, Hash, Eq, PartialEq)]
87
#[derive_deftly(ItemValueParseable)]
88
#[non_exhaustive]
89
pub struct NdiVoteStatus {
90
    /// status
91
    pub status: NdaVoteStatus,
92
}
93

            
94
/// `vote-status` status argument (for a specific flavour)
95
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
96
#[non_exhaustive]
97
pub struct NdaVoteStatus {}
98

            
99
/// `network-status-version` _flavour_ value
100
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
101
#[non_exhaustive]
102
pub struct NdaNetworkStatusVersionFlavour {}
103

            
104
/// The argument in `network-status-version` that is there iff it's a microdesc consensus.
105
const NDA_NETWORK_STATUS_VERSION_FLAVOUR: Option<&str> = ns_expr!(None, None, Some("microdesc"));
106

            
107
impl ItemArgumentParseable for NdaNetworkStatusVersionFlavour {
108
6
    fn from_args<'s>(args: &mut ArgumentStream<'s>) -> Result<Self, AE> {
109
6
        let exp: Option<&str> = NDA_NETWORK_STATUS_VERSION_FLAVOUR;
110
6
        if let Some(exp) = exp {
111
2
            let got = args.next().ok_or(AE::Missing)?;
112
2
            if got != exp {
113
                return Err(AE::Invalid);
114
2
            };
115
        } else {
116
            // NS consensus, or vote.  Reject additional arguments, since they
117
            // might be an unknown flavour.  See
118
            //   https://gitlab.torproject.org/tpo/core/torspec/-/issues/359
119
4
            args.reject_extra_args()?;
120
        }
121
6
        Ok(Self {})
122
6
    }
123
}
124

            
125
/// The document type argumnet in `vote-status`
126
const NDA_VOTE_STATUS: &str = ns_expr!("vote", "consensus", "consensus");
127

            
128
impl FromStr for NdaVoteStatus {
129
    type Err = InvalidNetworkStatusVoteStatus;
130
6
    fn from_str(s: &str) -> Result<Self, InvalidNetworkStatusVoteStatus> {
131
6
        if s == NDA_VOTE_STATUS {
132
6
            Ok(Self {})
133
        } else {
134
            Err(InvalidNetworkStatusVoteStatus {})
135
        }
136
6
    }
137
}
138

            
139
impl Display for NdaVoteStatus {
140
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
141
        Display::fmt(NDA_VOTE_STATUS, f)
142
    }
143
}
144

            
145
impl NormalItemArgument for NdaVoteStatus {}
146

            
147
/// `voting-delay` value
148
#[derive(Deftly, Clone, Debug, Hash, Eq, PartialEq)]
149
#[derive_deftly(ItemValueParseable)]
150
#[non_exhaustive]
151
pub struct NdiVotingDelay {
152
    /// VoteSeconds
153
    pub vote_seconds: u32,
154
    /// DistSeconds
155
    pub dist_seconds: u32,
156
}
157

            
158
/// `directory-footer` section
159
#[derive(Deftly, Clone, Debug)]
160
#[derive_deftly(NetdocParseable)]
161
#[non_exhaustive]
162
pub struct NddDirectoryFooter {
163
    /// `directory-footer`
164
    pub directory_footer: (),
165
}
166

            
167
/// Authority Key Entry (in a network status document)
168
#[derive(Deftly, Clone, Debug)]
169
#[derive_deftly(NetdocParseable)]
170
#[non_exhaustive]
171
pub struct NddAuthorityEntry {
172
    /// `dir-source`
173
    pub dir_source: NdiAuthorityDirSource,
174
}
175

            
176
/// `dir-source`
177
#[derive(Deftly, Clone, Debug)]
178
#[derive_deftly(ItemValueParseable)]
179
#[non_exhaustive]
180
pub struct NdiAuthorityDirSource {
181
    /// nickname
182
    pub nickname: types::Nickname,
183
    /// fingerprint
184
    pub h_p_auth_id_rsa: types::Fingerprint,
185
}
186

            
187
ns_choose! { (
188
    define_derive_deftly! {
189
        NddAuthoritySection:
190

            
191
        impl NetdocParseable for NddAuthoritySection {
192
2
            fn doctype_for_error() -> &'static str {
193
                "vote.authority.section"
194
            }
195
304
            fn is_intro_item_keyword(kw: KeywordRef<'_>) -> bool {
196
                NddAuthorityEntry::is_intro_item_keyword(kw)
197
            }
198
68
            fn is_structural_keyword(kw: KeywordRef<'_>) -> Option<IsStructural> {
199
                NddAuthorityEntry::is_structural_keyword(kw)
200
60
                    .or_else(|| authcert::DirAuthKeyCertSigned::is_structural_keyword(kw))
201
            }
202
2
            fn from_items<'s>(
203
2
                input: &mut ItemStream<'s>,
204
2
                stop_outer: stop_at!(),
205
2
            ) -> Result<Self, ErrorProblem> {
206
                let stop_inner = stop_outer
207
                  $(
208
                    | StopAt($ftype::is_intro_item_keyword)
209
                  )
210
                ;
211
                Ok(NddAuthoritySection { $(
212
                    $fname: NetdocParseable::from_items(input, stop_inner)?,
213
                ) })
214
            }
215
        }
216
    }
217

            
218
    /// An authority section in a vote
219
    ///
220
    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#section:authority>
221
    //
222
    // We can't derive the parsing here with the normal macro, because it's not a document,
223
    // just a kind of ad-hoc thing which we've made into its own type
224
    // to avoid the NetworkStatus becoming very odd.
225
    #[derive(Deftly, Clone, Debug)]
226
    #[derive_deftly(NddAuthoritySection)]
227
    #[non_exhaustive]
228
    pub struct NddAuthoritySection {
229
        /// Authority entry
230
        pub authority: NddAuthorityEntry,
231
        /// Authority key certificate
232
        pub cert: crate::doc::authcert::EncodedAuthCert,
233
    }
234
)(
235
    /// An authority section in a consensus
236
    ///
237
    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#section:authority>
238
    //
239
    // We can't derive the parsing here, because it's not a document,
240
    // just a kind of ad-hoc thing - and one which is quite weird.
241
    // https://gitlab.torproject.org/tpo/core/torspec/-/issues/361
242
    #[derive(Deftly, Clone, Debug)]
243
    #[non_exhaustive]
244
    pub struct NddAuthoritySection {
245
        /// The authority entries.
246
        ///
247
        /// Proper entries precede superseded ones.
248
        pub authorities: Vec<NddAuthorityEntryOrSuperseded>,
249
    }
250

            
251
    /// An element of an authority section in a consensus
252
    #[derive(Clone, Debug)]
253
    #[non_exhaustive]
254
    pub enum NddAuthorityEntryOrSuperseded {
255
        /// Proper Authority Entry
256
        Entry(NddAuthorityEntry),
257
        /// Superseded Key Authority
258
        ///
259
        /// `nickname` contains the value *with* `-legacy`
260
        Superseded(NdiAuthorityDirSource),
261
    }
262

            
263
    impl NetdocParseable for NddAuthoritySection {
264
4
        fn doctype_for_error() -> &'static str {
265
4
            "consensus.authority.section"
266
4
        }
267
436
        fn is_intro_item_keyword(kw: KeywordRef<'_>) -> bool {
268
436
            NddAuthorityEntry::is_intro_item_keyword(kw)
269
436
        }
270
        fn is_structural_keyword(kw: KeywordRef<'_>) -> Option<IsStructural> {
271
            NddAuthorityEntry::is_structural_keyword(kw)
272
        }
273
4
        fn from_items(
274
4
            input: &mut ItemStream<'_>,
275
4
            stop_outer: stop_at!(),
276
4
        ) -> Result<Self, ErrorProblem> {
277
4
            let is_our_keyword = NddAuthorityEntry::is_intro_item_keyword;
278
4
            let stop_inner = stop_outer | StopAt(is_our_keyword);
279
4
            let mut authorities = vec![];
280
20
            while let Some(peek) = input.peek_keyword()? {
281
20
                if !is_our_keyword(peek) { break };
282

            
283
                // But is it a superseded entry or not?
284
16
                let mut lookahead = input.clone();
285
16
                let _: UnparsedItem<'_> = lookahead.next().expect("peeked")?;
286

            
287
16
                let entry = match lookahead.next().transpose()? {
288
16
                    Some(item) if !stop_inner.stop_at(item.keyword()) => {
289
                        // Non-structural item.  Non-superseded entry.
290
16
                        let entry = NddAuthorityEntry::from_items(input, stop_inner)?;
291
16
                        NddAuthorityEntryOrSuperseded::Entry(entry)
292
                    }
293
                    None | Some(_) => {
294
                        // EOF, or the item is another dir-source, or the item
295
                        // is the start of the next document at the next outer level
296
                        // (eg a router status entry)
297
                        let item = input.next().expect("just peeked")?;
298
                        let entry = NdiAuthorityDirSource::from_unparsed(item)?;
299
                        if !entry.nickname.as_str().ends_with("-legacy") {
300
                            return Err(EP::OtherBadDocument(
301
 "authority entry lacks mandatory fields (eg `contact`) so is not a proper (non-superseded) entry, but nickname lacks `-legacy` suffix so is not a superseded entry"
302
                            ))
303
                        }
304
                        NddAuthorityEntryOrSuperseded::Superseded(entry)
305
                    }
306
                };
307
16
                authorities.push(entry);
308
            }
309
4
            if !authorities.is_sorted_by_key(
310
16
                |entry| matches!(entry, NddAuthorityEntryOrSuperseded::Superseded(_))
311
            ) {
312
                return Err(EP::OtherBadDocument(
313
 "normal (non-superseded) authority entry follows superseded authority key entry"
314
                ))
315
4
            }
316

            
317
4
            Ok(NddAuthoritySection { authorities })
318
4
        }
319
    }
320
)}
321

            
322
ns_choose! { (
323
    impl NetworkStatusSigned {
324
        /// Verify this vote's signatures using the embedded certificate
325
        ///
326
        /// # Security considerations
327
        ///
328
        /// The caller should use `NetworkStatus::h_kp_auth_id_rsa`
329
        /// to find out which voters vote this is.
330
        pub fn verify_selfcert(
331
            self,
332
            now: SystemTime,
333
        ) -> Result<(NetworkStatus, NetworkStatusSignatures), VF> {
334
            let validity = *self.body.published.0 ..= *self.body.valid_until.0;
335
            check_validity_time(now, validity)?;
336

            
337
            let cert = self.body.parse_authcert()?.verify_selfcert(now)?;
338

            
339
            netstatus::verify_general_timeless(
340
                slice::from_ref(&self.signatures.directory_signature),
341
                &[*cert.fingerprint],
342
                &[&cert],
343
                1,
344
            )?;
345

            
346
            Ok(self.unwrap_unverified())
347
        }
348
    }
349

            
350
    impl NetworkStatus {
351
        /// Parse the embedded authcert
352
        fn parse_authcert(&self) -> Result<crate::doc::authcert::AuthCertSigned, EP> {
353
            let cert_input = ParseInput::new(
354
                self.authority.cert.as_str(),
355
                "<embedded auth cert>",
356
            );
357
            parse_netdoc(&cert_input).map_err(|e| e.problem)
358
        }
359

            
360
        /// Voter identity
361
        ///
362
        /// # Security considerations
363
        ///
364
        /// The returned identity has been confirmed to have properly certified
365
        /// this vote at this time.
366
        ///
367
        /// It is up to the caller to decide whether this identity is actually
368
        /// a voter, count up votes, etc.
369
        pub fn h_kp_auth_id_rsa(&self) -> pk::rsa::RsaIdentity {
370
            *self.parse_authcert()
371
                // SECURITY: if the user calls this function, they have a bare
372
                // NetworkStatus, not a NetworkStatusSigned, so parsing
373
                // and verification has already been done in verify_selfcert above.
374
                .expect("was verified already!")
375
                .inspect_unverified()
376
                .0
377
                .fingerprint
378
        }
379
    }
380
) (
381
    impl NetworkStatusSigned {
382
        /// Verify this consensus document
383
        ///
384
        /// # Security considerations
385
        ///
386
        /// The timeliness verification is relaxed, and incorporates the `DistSeconds` skew.
387
        /// The caller **must not use** the returned consensus before its `valid_after`,
388
        /// and must handle `fresh_until`.
389
        ///
390
        /// `authorities` should be a list of the authorities
391
        /// that the caller trusts.
392
        ///
393
        /// `certs` is a list of dir auth key certificates to use to try to link
394
        /// the signed consensus to those authorities.
395
        /// Extra certificates in `certs`, that don't come from anyone in `authorities`,
396
        /// are ignored.
397
2
        pub fn verify(
398
2
            self,
399
2
            now: SystemTime,
400
2
            authorities: &[pk::rsa::RsaIdentity],
401
2
            certs: &[&DirAuthKeyCert],
402
2
        ) -> Result<(NetworkStatus, NetworkStatusSignatures), VF> {
403
2
            let threshold = authorities.len() / 2 + 1; // strict majority
404
2
            let validity_start = self.body.valid_after.0
405
2
                .checked_sub(Duration::from_secs(self.body.voting_delay.dist_seconds.into()))
406
2
                .ok_or(VF::Other)?;
407
2
            check_validity_time(now, validity_start..= *self.body.valid_until.0)?;
408

            
409
2
            netstatus::verify_general_timeless(
410
2
                &self.signatures.directory_signature,
411
2
                authorities,
412
2
                certs,
413
2
                threshold,
414
            )?;
415

            
416
2
            Ok(self.unwrap_unverified())
417
2
        }
418
    }
419
)}