tor_netdoc/parse2/poc/netstatus/
flavoured.rs

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
14use super::super::*;
15
16/// Toplevel document string for error reporting
17const TOPLEVEL_DOCTYPE_FOR_ERROR: &str = ns_expr!(
18    "NetworkStatusVote",
19    "NetworkStatusNs",
20    "NetworkStatusMd",
21);
22
23/// Network status document (vote, consensus, or microdescriptor consensus) - body
24///
25/// The preamble items are members of this struct.
26/// The rest are handled as sub-documents.
27#[derive(Deftly, Clone, Debug)]
28#[derive_deftly(NetdocParseable, NetdocSigned)]
29#[deftly(netdoc(doctype_for_error = "TOPLEVEL_DOCTYPE_FOR_ERROR"))]
30#[non_exhaustive]
31pub struct NetworkStatus {
32    /// `network-status-version`
33    pub network_status_version: (NdaNetworkStatusVersion, NdaNetworkStatusVersionFlavour),
34
35    /// `vote-status`
36    pub vote_status: NdiVoteStatus,
37
38    /// `published`
39    pub published: ns_type!(
40        (NdaSystemTimeDeprecatedSyntax,),
41        Option<Void>,
42    ),
43
44    /// `valid-after`
45    pub valid_after: (NdaSystemTimeDeprecatedSyntax,),
46
47    /// `valid-until`
48    pub valid_until: (NdaSystemTimeDeprecatedSyntax,),
49
50    /// `voting-delay`
51    pub voting_delay: NdiVotingDelay,
52
53    /// `params`
54    #[deftly(netdoc(default))]
55    pub params: NdiParams,
56
57    /// Authority section
58    #[deftly(netdoc(subdoc))]
59    pub authority: NddAuthoritySection,
60
61    /// `r` subdocuments
62    #[deftly(netdoc(subdoc))]
63    pub r: Vec<Router>,
64
65    /// `directory-footer` section (which we handle as a sub-document)
66    #[deftly(netdoc(subdoc))]
67    pub directory_footer: Option<NddDirectoryFooter>,
68}
69
70/// Signatures on a network status document
71#[derive(Deftly, Clone, Debug)]
72#[derive_deftly(NetdocParseable)]
73#[deftly(netdoc(signatures))]
74#[non_exhaustive]
75pub struct NetworkStatusSignatures {
76    /// `directory-signature`s
77    pub directory_signature: ns_type!(NdiDirectorySignature, Vec<NdiDirectorySignature>),
78}
79
80/// `vote-status` value
81///
82/// In a non-demo we'd probably abolish this,
83/// using `NdaStatus` directly in `NddNetworkStatus`
84/// impl of `ItemValueParseable` for tuples.
85#[derive(Deftly, Clone, Debug, Hash, Eq, PartialEq)]
86#[derive_deftly(ItemValueParseable)]
87#[non_exhaustive]
88pub struct NdiVoteStatus {
89    /// status
90    pub status: NdaVoteStatus,
91}
92
93/// `vote-status` status argument (for a specific flavour)
94#[derive(Clone, Debug, Hash, Eq, PartialEq)]
95#[non_exhaustive]
96pub struct NdaVoteStatus {}
97
98/// `network-status-version` _flavour_ value
99#[derive(Clone, Debug, Hash, Eq, PartialEq)]
100#[non_exhaustive]
101pub struct NdaNetworkStatusVersionFlavour {}
102
103impl ItemArgumentParseable for NdaNetworkStatusVersionFlavour {
104    fn from_args<'s>(args: &mut ArgumentStream<'s>, field: &'static str)
105                     -> Result<Self, ErrorProblem>
106    {
107        let exp: Option<&str> = ns_expr!(None, None, Some("microdesc"));
108        if let Some(exp) = exp {
109            let got = args.next().ok_or(EP::MissingArgument { field })?;
110            if got != exp { return Err(EP::InvalidArgument { field } ) };
111        } else {
112            // NS consensus, or vote.  Reject additional arguments, since they
113            // might be an unknown flavour.  See
114            //   https://gitlab.torproject.org/tpo/core/torspec/-/issues/359
115            args.reject_extra_args()?;
116        }
117        Ok(Self {})
118    }
119}
120
121impl FromStr for NdaVoteStatus {
122    type Err = InvalidNetworkStatusVoteStatus;
123    fn from_str(s: &str) -> Result<Self, InvalidNetworkStatusVoteStatus> {
124        let exp = ns_expr!("vote", "consensus", "consensus");
125        if s == exp {
126            Ok(Self {})
127        } else {
128            Err(InvalidNetworkStatusVoteStatus { })
129        }
130    }
131}
132
133/// `voting-delay` value
134#[derive(Deftly, Clone, Debug, Hash, Eq, PartialEq)]
135#[derive_deftly(ItemValueParseable)]
136#[non_exhaustive]
137pub struct NdiVotingDelay {
138    /// VoteSeconds
139    pub vote_seconds: u32,
140    /// DistSeconds
141    pub dist_seconds: u32,
142}
143
144/// Router entry in a network status
145#[derive(Deftly, Clone, Debug)]
146#[derive_deftly(NetdocParseable)]
147#[non_exhaustive]
148pub struct Router {
149    /// `r`
150    pub r: NdiR,
151}
152
153/// `directory-footer` section
154#[derive(Deftly, Clone, Debug)]
155#[derive_deftly(NetdocParseable)]
156#[non_exhaustive]
157pub struct NddDirectoryFooter {
158    /// `directory-footer`
159    pub directory_footer: (),
160}
161
162/// Authority Key Entry (in a network status document)
163#[derive(Deftly, Clone, Debug)]
164#[derive_deftly(NetdocParseable)]
165#[non_exhaustive]
166pub struct NddAuthorityEntry {
167    /// `dir-source`
168    pub dir_source: NdiAuthorityDirSource,
169}
170
171/// `dir-source`
172#[derive(Deftly, Clone, Debug)]
173#[derive_deftly(ItemValueParseable)]
174#[non_exhaustive]
175pub struct NdiAuthorityDirSource {
176    /// nickname
177    pub nickname: types::Nickname,
178    /// fingerprint
179    pub h_p_auth_id_rsa: types::Fingerprint,
180}
181
182ns_choose! { (
183    define_derive_deftly! {
184        NddAuthoritySection:
185
186        impl NetdocParseable for NddAuthoritySection {
187            fn doctype_for_error() -> &'static str {
188                "vote.authority.section"
189            }
190            fn is_intro_item_keyword(kw: KeywordRef<'_>) -> bool {
191                NddAuthorityEntry::is_intro_item_keyword(kw)
192            }
193            fn from_items<'s>(
194                input: &mut ItemStream<'s>,
195                stop_outer: stop_at!(),
196            ) -> Result<Self, ErrorProblem> {
197                let stop_inner = stop_outer
198                  $(
199                    | StopAt($ftype::is_intro_item_keyword)
200                  )
201                ;
202                Ok(NddAuthoritySection { $(
203                    $fname: NetdocParseable::from_items(input, stop_inner)?,
204                ) })
205            }
206        }
207    }
208
209    /// An authority section in a vote
210    ///
211    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#section:authority>
212    //
213    // We can't derive the parsing here with the normal macro, because it's not a document,
214    // just a kind of ad-hoc thing which we've made into its own type
215    // to avoid the NetworkStatus becoming very odd.
216    #[derive(Deftly, Clone, Debug)]
217    #[derive_deftly(NddAuthoritySection)]
218    #[non_exhaustive]
219    pub struct NddAuthoritySection {
220        /// Authority entry
221        pub authority: NddAuthorityEntry,
222        /// Authority key certificate
223        pub cert: authcert::DirAuthKeyCertSigned,
224    }
225)(
226    /// An authority section in a consensus
227    ///
228    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#section:authority>
229    //
230    // We can't derive the parsing here, because it's not a document,
231    // just a kind of ad-hoc thing - and one which is quite weird.
232    // https://gitlab.torproject.org/tpo/core/torspec/-/issues/361
233    #[derive(Deftly, Clone, Debug)]
234    #[non_exhaustive]
235    pub struct NddAuthoritySection {
236        /// The authority entries.
237        ///
238        /// Proper entries precede superseded ones.
239        pub authorities: Vec<NddAuthorityEntryOrSuperseded>,
240    }
241
242    /// An element of an authority section in a consensus
243    #[derive(Clone, Debug)]
244    #[non_exhaustive]
245    pub enum NddAuthorityEntryOrSuperseded {
246        /// Proper Authority Entry
247        Entry(NddAuthorityEntry),
248        /// Superseded Key Authority
249        ///
250        /// `nickname` contains the value *with* `-legacy`
251        Superseded(NdiAuthorityDirSource),
252    }
253
254    impl NetdocParseable for NddAuthoritySection {
255        fn doctype_for_error() -> &'static str {
256            "consensus.authority.section"
257        }
258        fn is_intro_item_keyword(kw: KeywordRef<'_>) -> bool {
259            NddAuthorityEntry::is_intro_item_keyword(kw)
260        }
261        fn from_items(
262            input: &mut ItemStream<'_>,
263            stop_outer: stop_at!(),
264        ) -> Result<Self, ErrorProblem> {
265            let is_our_keyword = NddAuthorityEntry::is_intro_item_keyword;
266            let stop_inner = stop_outer | StopAt(is_our_keyword);
267            let mut authorities = vec![];
268            loop {
269                let Some(peek) = input.peek_keyword()? else { break };
270                if !is_our_keyword(peek) { break };
271
272                // But is it a superseded entry or not?
273                let mut lookahead = input.clone();
274                let _: UnparsedItem<'_> = lookahead.next().expect("peeked")?;
275
276                let entry = match lookahead.next().transpose()? {
277                    Some(item) if !stop_inner.stop_at(item.keyword()) => {
278                        // Non-structural item.  Non-superseded entry.
279                        let entry = NddAuthorityEntry::from_items(input, stop_inner)?;
280                        NddAuthorityEntryOrSuperseded::Entry(entry)
281                    }
282                    None | Some(_) => {
283                        // EOF, or the item is another dir-source, or the item
284                        // is the start of the next document at the next outer level
285                        // (eg a router status entry)
286                        let item = input.next().expect("just peeked")?;
287                        let entry = NdiAuthorityDirSource::from_unparsed(item)?;
288                        if !entry.nickname.as_str().ends_with("-legacy") {
289                            return Err(EP::Other(
290 "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"
291                            ))
292                        }
293                        NddAuthorityEntryOrSuperseded::Superseded(entry)
294                    }
295                };
296                authorities.push(entry);
297            }
298            if !authorities.is_sorted_by_key(
299                |entry| matches!(entry, NddAuthorityEntryOrSuperseded::Superseded(_))
300            ) {
301                return Err(EP::Other(
302 "normal (non-superseded) authority entry follows superseded authority key entry"
303                ))
304            }
305
306            Ok(NddAuthoritySection { authorities })
307        }
308    }
309)}
310
311ns_choose! { (
312    impl NetworkStatusSigned {
313        /// Verify this vote's signatures using the embedded certificate
314        ///
315        /// # Security considerations
316        ///
317        /// The caller should use `NetworkStatus::h_kp_auth_id_rsa`
318        /// to find out which voters vote this is.
319        pub fn verify_selfcert(
320            self,
321            now: SystemTime,
322        ) -> Result<(NetworkStatus, NetworkStatusSignatures), VF> {
323            let validity = *self.body.published.0 ..= *self.body.valid_until.0;
324            check_validity_time(now, validity)?;
325
326            let cert = self.body.authority.cert.clone();
327            let cert = cert.verify_selfcert(now)?;
328
329            netstatus::verify_general_timeless(
330                slice::from_ref(&self.signatures.directory_signature),
331                &[*cert.h_kp_auth_id_rsa.0],
332                &[&cert],
333                1,
334            )?;
335
336            Ok(self.unwrap_unverified())
337        }
338    }
339
340    impl NetworkStatus {
341        /// Voter identity
342        ///
343        /// # Security considerations
344        ///
345        /// The returned identity has been confirmed to have properly certified
346        /// this vote at this time.
347        ///
348        /// It is up to the caller to decide whether this identity is actually
349        /// a voter, count up votes, etc.
350        pub fn h_kp_auth_id_rsa(&self) -> pk::rsa::RsaIdentity {
351            *self.authority.cert
352                // SECURITY: if the user calls this function, they have a bare
353                // NetworkStatus, not a NetworkStatusSigned,
354                // so verification has already been done in verify_selfcert above.
355                .inspect_unverified()
356                .0
357                .h_kp_auth_id_rsa.0
358        }
359    }
360) (
361    impl NetworkStatusSigned {
362        /// Verify this consensus document
363        ///
364        /// # Security considerations
365        ///
366        /// The timeliness verification is relaxed, and incorporates the `DistSeconds` skew.
367        /// The caller **must not use** the returned consensus before its `valid_after`,
368        /// and must handle `fresh_until`.
369        ///
370        /// `authorities` should be a list of the authorities
371        /// that the caller trusts.
372        ///
373        /// `certs` is a list of dir auth key certificates to use to try to link
374        /// the signed consensus to those authorities.
375        /// Extra certificates in `certs`, that don't come from anyone in `authorities`,
376        /// are ignored.
377        pub fn verify(
378            self,
379            now: SystemTime,
380            authorities: &[pk::rsa::RsaIdentity],
381            certs: &[&DirAuthKeyCert],
382        ) -> Result<(NetworkStatus, NetworkStatusSignatures), VF> {
383            let threshold = authorities.len() / 2 + 1; // strict majority
384            let validity_start = self.body.valid_after.0
385                .checked_sub(Duration::from_secs(self.body.voting_delay.dist_seconds.into()))
386                .ok_or(VF::Other)?;
387            check_validity_time(now, validity_start..= *self.body.valid_until.0)?;
388
389            netstatus::verify_general_timeless(
390                &self.signatures.directory_signature,
391                authorities,
392                certs,
393                threshold,
394            )?;
395
396            Ok(self.unwrap_unverified())
397        }
398    }
399)}