Skip to main content

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 =
18    ns_expr!("NetworkStatusVote", "NetworkStatusNs", "NetworkStatusMd",);
19
20/// The real router status entry type.
21pub 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]
35pub 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]
76pub 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]
89pub 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]
97pub struct NdaVoteStatus {}
98
99/// `network-status-version` _flavour_ value
100#[derive(Clone, Debug, Hash, Eq, PartialEq)]
101#[non_exhaustive]
102pub struct NdaNetworkStatusVersionFlavour {}
103
104/// The argument in `network-status-version` that is there iff it's a microdesc consensus.
105const NDA_NETWORK_STATUS_VERSION_FLAVOUR: Option<&str> = ns_expr!(None, None, Some("microdesc"));
106
107impl ItemArgumentParseable for NdaNetworkStatusVersionFlavour {
108    fn from_args<'s>(args: &mut ArgumentStream<'s>) -> Result<Self, AE> {
109        let exp: Option<&str> = NDA_NETWORK_STATUS_VERSION_FLAVOUR;
110        if let Some(exp) = exp {
111            let got = args.next().ok_or(AE::Missing)?;
112            if got != exp {
113                return Err(AE::Invalid);
114            };
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            args.reject_extra_args()?;
120        }
121        Ok(Self {})
122    }
123}
124
125/// The document type argumnet in `vote-status`
126const NDA_VOTE_STATUS: &str = ns_expr!("vote", "consensus", "consensus");
127
128impl FromStr for NdaVoteStatus {
129    type Err = InvalidNetworkStatusVoteStatus;
130    fn from_str(s: &str) -> Result<Self, InvalidNetworkStatusVoteStatus> {
131        if s == NDA_VOTE_STATUS {
132            Ok(Self {})
133        } else {
134            Err(InvalidNetworkStatusVoteStatus {})
135        }
136    }
137}
138
139impl Display for NdaVoteStatus {
140    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
141        Display::fmt(NDA_VOTE_STATUS, f)
142    }
143}
144
145impl NormalItemArgument for NdaVoteStatus {}
146
147/// `voting-delay` value
148#[derive(Deftly, Clone, Debug, Hash, Eq, PartialEq)]
149#[derive_deftly(ItemValueParseable)]
150#[non_exhaustive]
151pub 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]
162pub 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]
171pub 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]
180pub struct NdiAuthorityDirSource {
181    /// nickname
182    pub nickname: types::Nickname,
183    /// fingerprint
184    pub h_p_auth_id_rsa: types::Fingerprint,
185}
186
187ns_choose! { (
188    define_derive_deftly! {
189        NddAuthoritySection:
190
191        impl NetdocParseable for NddAuthoritySection {
192            fn doctype_for_error() -> &'static str {
193                "vote.authority.section"
194            }
195            fn is_intro_item_keyword(kw: KeywordRef<'_>) -> bool {
196                NddAuthorityEntry::is_intro_item_keyword(kw)
197            }
198            fn is_structural_keyword(kw: KeywordRef<'_>) -> Option<IsStructural> {
199                NddAuthorityEntry::is_structural_keyword(kw)
200                    .or_else(|| authcert::DirAuthKeyCertSigned::is_structural_keyword(kw))
201            }
202            fn from_items<'s>(
203                input: &mut ItemStream<'s>,
204                stop_outer: stop_at!(),
205            ) -> 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        fn doctype_for_error() -> &'static str {
265            "consensus.authority.section"
266        }
267        fn is_intro_item_keyword(kw: KeywordRef<'_>) -> bool {
268            NddAuthorityEntry::is_intro_item_keyword(kw)
269        }
270        fn is_structural_keyword(kw: KeywordRef<'_>) -> Option<IsStructural> {
271            NddAuthorityEntry::is_structural_keyword(kw)
272        }
273        fn from_items(
274            input: &mut ItemStream<'_>,
275            stop_outer: stop_at!(),
276        ) -> Result<Self, ErrorProblem> {
277            let is_our_keyword = NddAuthorityEntry::is_intro_item_keyword;
278            let stop_inner = stop_outer | StopAt(is_our_keyword);
279            let mut authorities = vec![];
280            while let Some(peek) = input.peek_keyword()? {
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::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                authorities.push(entry);
308            }
309            if !authorities.is_sorted_by_key(
310                |entry| matches!(entry, NddAuthorityEntryOrSuperseded::Superseded(_))
311            ) {
312                return Err(EP::OtherBadDocument(
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.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        pub fn verify(
398            self,
399            now: SystemTime,
400            authorities: &[pk::rsa::RsaIdentity],
401            certs: &[&DirAuthKeyCert],
402        ) -> Result<(NetworkStatus, NetworkStatusSignatures), VF> {
403            let threshold = authorities.len() / 2 + 1; // strict majority
404            let validity_start = self.body.valid_after.0
405                .checked_sub(Duration::from_secs(self.body.voting_delay.dist_seconds.into()))
406                .ok_or(VF::Other)?;
407            check_validity_time(now, validity_start..= *self.body.valid_until.0)?;
408
409            netstatus::verify_general_timeless(
410                &self.signatures.directory_signature,
411                authorities,
412                certs,
413                threshold,
414            )?;
415
416            Ok(self.unwrap_unverified())
417        }
418    }
419)}