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            loop {
280                let Some(peek) = input.peek_keyword()? else { break };
281                if !is_our_keyword(peek) { break };
282
283                // But is it a superseded entry or not?
284                let mut lookahead = input.clone();
285                let _: UnparsedItem<'_> = lookahead.next().expect("peeked")?;
286
287                let entry = match lookahead.next().transpose()? {
288                    Some(item) if !stop_inner.stop_at(item.keyword()) => {
289                        // Non-structural item.  Non-superseded entry.
290                        let entry = NddAuthorityEntry::from_items(input, stop_inner)?;
291                        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::Other(
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                authorities.push(entry);
308            }
309            if !authorities.is_sorted_by_key(
310                |entry| matches!(entry, NddAuthorityEntryOrSuperseded::Superseded(_))
311            ) {
312                return Err(EP::Other(
313 "normal (non-superseded) authority entry follows superseded authority key entry"
314                ))
315            }
316
317            Ok(NddAuthoritySection { authorities })
318        }
319    }
320)}
321
322ns_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.authority.cert.clone();
338            let cert = cert.verify_selfcert(now)?;
339
340            netstatus::verify_general_timeless(
341                slice::from_ref(&self.signatures.directory_signature),
342                &[*cert.h_kp_auth_id_rsa.0],
343                &[&cert],
344                1,
345            )?;
346
347            Ok(self.unwrap_unverified())
348        }
349    }
350
351    impl NetworkStatus {
352        /// Voter identity
353        ///
354        /// # Security considerations
355        ///
356        /// The returned identity has been confirmed to have properly certified
357        /// this vote at this time.
358        ///
359        /// It is up to the caller to decide whether this identity is actually
360        /// a voter, count up votes, etc.
361        pub fn h_kp_auth_id_rsa(&self) -> pk::rsa::RsaIdentity {
362            *self.authority.cert
363                // SECURITY: if the user calls this function, they have a bare
364                // NetworkStatus, not a NetworkStatusSigned,
365                // so verification has already been done in verify_selfcert above.
366                .inspect_unverified()
367                .0
368                .h_kp_auth_id_rsa.0
369        }
370    }
371) (
372    impl NetworkStatusSigned {
373        /// Verify this consensus document
374        ///
375        /// # Security considerations
376        ///
377        /// The timeliness verification is relaxed, and incorporates the `DistSeconds` skew.
378        /// The caller **must not use** the returned consensus before its `valid_after`,
379        /// and must handle `fresh_until`.
380        ///
381        /// `authorities` should be a list of the authorities
382        /// that the caller trusts.
383        ///
384        /// `certs` is a list of dir auth key certificates to use to try to link
385        /// the signed consensus to those authorities.
386        /// Extra certificates in `certs`, that don't come from anyone in `authorities`,
387        /// are ignored.
388        pub fn verify(
389            self,
390            now: SystemTime,
391            authorities: &[pk::rsa::RsaIdentity],
392            certs: &[&DirAuthKeyCert],
393        ) -> Result<(NetworkStatus, NetworkStatusSignatures), VF> {
394            let threshold = authorities.len() / 2 + 1; // strict majority
395            let validity_start = self.body.valid_after.0
396                .checked_sub(Duration::from_secs(self.body.voting_delay.dist_seconds.into()))
397                .ok_or(VF::Other)?;
398            check_validity_time(now, validity_start..= *self.body.valid_until.0)?;
399
400            netstatus::verify_general_timeless(
401                &self.signatures.directory_signature,
402                authorities,
403                certs,
404                threshold,
405            )?;
406
407            Ok(self.unwrap_unverified())
408        }
409    }
410)}