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