1
//! network status documents: shared between votes, consensuses and md consensuses
2

            
3
use super::*;
4

            
5
use crate::types;
6
use authcert::DirAuthKeyCert;
7

            
8
mod ns_per_flavour_macros;
9
pub use ns_per_flavour_macros::*;
10

            
11
ns_per_flavour_macros::ns_export_flavoured_types! {
12
    NetworkStatus, NetworkStatusUnverified, Router,
13
}
14

            
15
/// `network-status-version` version value
16
#[derive(Debug, Clone, Copy, Eq, PartialEq, strum::EnumString, strum::Display)]
17
#[non_exhaustive]
18
pub enum NdaNetworkStatusVersion {
19
    /// The currently supported version, `3`
20
    #[strum(serialize = "3")]
21
    V3,
22
}
23

            
24
impl NormalItemArgument for NdaNetworkStatusVersion {}
25

            
26
/// `params` value
27
#[derive(Clone, Debug, Default, Deftly)]
28
#[derive_deftly(ItemValueParseable)]
29
#[non_exhaustive]
30
pub struct NdiParams {
31
    // Not implemented.
32
}
33

            
34
/// `r` sub-document
35
#[derive(Deftly, Clone, Debug)]
36
#[derive_deftly(ItemValueParseable)]
37
#[non_exhaustive]
38
pub struct NdiR {
39
    /// nickname
40
    pub nickname: types::Nickname,
41
    /// identity
42
    pub identity: String, // In non-demo, use a better type
43
}
44

            
45
/// `directory-signature` value
46
#[derive(Debug, Clone)]
47
#[non_exhaustive]
48
pub enum NdiDirectorySignature {
49
    /// Known "hash function" name
50
    Known {
51
        /// Hash algorithm
52
        hash_algo: DirectorySignatureHashAlgo,
53
        /// H(KP\_auth\_id\_rsa)
54
        h_kp_auth_id_rsa: pk::rsa::RsaIdentity,
55
        /// H(kp\_auth\_sign\_rsa)
56
        h_kp_auth_sign_rsa: pk::rsa::RsaIdentity,
57
        /// RSA signature
58
        rsa_signature: Vec<u8>,
59
    },
60
    /// Unknown "hash function" name
61
    ///
62
    /// TODO torspec#350;
63
    /// might have been an unknown algorithm, or might be invalid hex, or something.
64
    Unknown {},
65
}
66
define_derive_deftly! {
67
    /// Ad-hoc derives for [`DirectorySignatureHash`] impls, avoiding copypasta bugs
68
    ///
69
    /// # Input
70
    ///
71
    ///  * `pub enum DirectorySignatureHashAlgo`
72
    ///  * Unit variants
73
    ///  * Each variant with `#[deftly(hash_len = "N")]`
74
    ///    where `N` is the digest length in bytes.
75
    ///
76
    /// # Generated code
77
    ///
78
    ///  * `pub enum DirectorySignaturesHashesAccu`,
79
    ///    with each variant a 1-tuple containing `Option<[u8; N]>`.
80
    ///    (These are `None` if this hash has not been computed yet.)
81
    ///
82
    ///  * `DirectorySignaturesHashesAccu::parse_keyword_and_hash`
83
    ///
84
    ///  * `DirectorySignaturesHashesAccu::hash_slice_for_verification`
85
    DirectorySignatureHashesAccu expect items, beta_deftly:
86

            
87
    ${define FNAME ${paste ${snake_case $vname}} }
88

            
89
    /// `directory-signature`a hash algorithm argument
90
    #[derive(Clone, Copy, Default, Debug, Eq, PartialEq, Deftly)]
91
    #[derive_deftly(AsMutSelf)]
92
    #[non_exhaustive]
93
    pub struct DirectorySignaturesHashesAccu {
94
      $(
95
        ${vattrs doc}
96
        $FNAME: Option<[u8; ${vmeta(hash_len) as expr}]>,
97
      )
98
    }
99

            
100
    impl DirectorySignaturesHashesAccu {
101
        /// If `algorithm` is an algorithm name, calculate the hash
102
        ///
103
        /// Otherwise, return `None`.
104
3844
        fn parse_keyword_and_hash(
105
3844
            &mut self,
106
3844
            algorithm: &str,
107
3844
            body: &SignatureHashInputs,
108
3844
        ) -> Option<$ttype> {
109
            Some(match algorithm {
110
              $(
111
                ${concat $FNAME} => {
112
                    let mut h = tor_llcrypto::d::$vname::new();
113
                    h.update(body.body().body());
114
                    h.update(body.signature_item_kw_spc);
115
                    self.$FNAME = Some(h.finalize().into());
116
                    $vtype
117
                }
118
              )
119
                _ => return None,
120
            })
121
        }
122

            
123
        /// Return the hash value for this algorithm, as a slice
124
        ///
125
        /// `None` if the value wasn't computed.
126
        /// That shouldn't happen.
127
8
        fn hash_slice_for_verification(&self, algo: $ttype) -> Option<&[u8]> {
128
            match algo { $(
129
                $vtype => Some(self.$FNAME.as_ref()?),
130
            ) }
131
        }
132
    }
133
}
134

            
135
/// `directory-signature` hash algorithm argument
136
#[derive(Clone, Copy, Debug, Eq, PartialEq, strum::EnumString, Deftly)]
137
#[derive_deftly(DirectorySignatureHashesAccu)]
138
#[non_exhaustive]
139
pub enum DirectorySignatureHashAlgo {
140
    /// SHA-1
141
    #[deftly(hash_len = "20")]
142
    Sha1,
143
    /// SHA-256
144
    #[deftly(hash_len = "32")]
145
    Sha256,
146
}
147

            
148
/// Unsupported `vote-status` value
149
///
150
/// This message is not normally actually shown since our `ErrorProblem` doesn't contain it.
151
#[derive(Clone, Debug, Error)]
152
#[non_exhaustive]
153
#[error("invalid value for vote-status in network status document")]
154
pub struct InvalidNetworkStatusVoteStatus {}
155

            
156
impl SignatureItemParseable for NdiDirectorySignature {
157
    type HashAccu = DirectorySignaturesHashesAccu;
158

            
159
    // TODO torspec#350.  That's why this manual impl is needed
160
1926
    fn from_unparsed_and_body<'s>(
161
1926
        mut input: UnparsedItem<'s>,
162
1926
        document_body: &SignatureHashInputs<'_>,
163
1926
        hashes: &mut DirectorySignaturesHashesAccu,
164
1926
    ) -> Result<Self, EP> {
165
1926
        let object = input.object();
166
1926
        let args = input.args_mut();
167
1926
        let maybe_algorithm = args.clone().next().ok_or(EP::MissingArgument {
168
1926
            field: "algorithm/h_kp_auth_id_rsa",
169
1926
        })?;
170

            
171
1926
        let hash_algo =
172
1926
            if let Some(algo) = hashes.parse_keyword_and_hash(maybe_algorithm, document_body) {
173
8
                let _: &str = args.next().expect("we just peeked");
174
8
                algo
175
1918
            } else if maybe_algorithm
176
76761
                .find(|c: char| !c.is_ascii_hexdigit())
177
1918
                .is_some()
178
            {
179
                // Not hex.  Must be some unknown algorithm.
180
                // There might be Object, but don't worry if not.
181
                return Ok(NdiDirectorySignature::Unknown {});
182
            } else {
183
1918
                hashes
184
1918
                    .parse_keyword_and_hash("sha1", document_body)
185
1918
                    .expect("sha1 is not valid?")
186
            };
187

            
188
1926
        let rsa_signature = object.ok_or(EP::MissingObject)?.decode_data()?;
189

            
190
3897
        let mut fingerprint_arg = |field: &'static str| {
191
3852
            (|| {
192
3852
                args.next()
193
3852
                    .ok_or(AE::Missing)?
194
3852
                    .parse::<types::Fingerprint>()
195
3852
                    .map_err(|_e| AE::Invalid)
196
3852
                    .map(pk::rsa::RsaIdentity::from)
197
            })()
198
3852
            .map_err(args.error_handler(field))
199
3852
        };
200

            
201
        Ok(NdiDirectorySignature::Known {
202
1926
            hash_algo,
203
1926
            rsa_signature,
204
1926
            h_kp_auth_id_rsa: fingerprint_arg("h_kp_auth_id_rsa")?,
205
1926
            h_kp_auth_sign_rsa: fingerprint_arg("h_kp_auth_sign_rsa")?,
206
        })
207
1926
    }
208
}
209

            
210
/// Meat of the verification functions for network documents
211
///
212
/// Checks that at least `threshold` members of `trusted`
213
/// have signed this document (in `signatures`),
214
/// via some cert(s) in `certs`.
215
///
216
/// Does not check validity time.
217
2
fn verify_general_timeless(
218
2
    hashes: &DirectorySignaturesHashesAccu,
219
2
    signatures: &[NdiDirectorySignature],
220
2
    trusted: &[pk::rsa::RsaIdentity],
221
2
    certs: &[&DirAuthKeyCert],
222
2
    threshold: usize,
223
2
) -> Result<(), VF> {
224
2
    let mut ok = HashSet::<pk::rsa::RsaIdentity>::new();
225

            
226
10
    for sig in signatures {
227
8
        match sig {
228
            NdiDirectorySignature::Known {
229
8
                hash_algo,
230
8
                h_kp_auth_id_rsa,
231
8
                h_kp_auth_sign_rsa,
232
8
                rsa_signature,
233
            } => {
234
8
                let Some(authority) = ({
235
8
                    trusted
236
8
                        .iter()
237
24
                        .find(|trusted| **trusted == *h_kp_auth_id_rsa)
238
                }) else {
239
                    // unknown kp_auth_id_rsa, ignore it
240
                    continue;
241
                };
242
8
                let Some(cert) = ({
243
8
                    certs
244
8
                        .iter()
245
24
                        .find(|cert| cert.dir_signing_key.to_rsa_identity() == *h_kp_auth_sign_rsa)
246
                }) else {
247
                    // no cert for this kp_auth_sign_rsa, ignore it
248
                    continue;
249
                };
250

            
251
8
                let h = hashes
252
8
                    .hash_slice_for_verification(*hash_algo)
253
8
                    .ok_or(VF::Bug)?;
254

            
255
8
                let () = cert.dir_signing_key.verify(h, rsa_signature)?;
256

            
257
8
                ok.insert(*authority);
258
            }
259
            NdiDirectorySignature::Unknown { .. } => {}
260
        }
261
    }
262

            
263
2
    if ok.len() < threshold {
264
        return Err(VF::InsufficientTrustedSigners);
265
2
    }
266

            
267
2
    Ok(())
268
2
}