tor_netdoc/doc/hsdesc/
middle.rs

1//! Handle the middle document of an onion service descriptor.
2
3use std::sync::LazyLock;
4use subtle::ConstantTimeEq;
5use tor_hscrypto::pk::{HsBlindId, HsClientDescEncSecretKey, HsSvcDescEncKey};
6use tor_hscrypto::{RevisionCounter, Subcredential};
7use tor_llcrypto::pk::curve25519;
8use tor_llcrypto::util::ct::CtByteArray;
9
10use crate::doc::hsdesc::desc_enc::build_descriptor_cookie_key;
11use crate::parse::tokenize::{Item, NetDocReader};
12use crate::parse::{keyword::Keyword, parser::SectionRules};
13use crate::types::misc::B64;
14use crate::{Pos, Result};
15
16use super::HsDescError;
17use super::desc_enc::{
18    HS_DESC_CLIENT_ID_LEN, HS_DESC_ENC_NONCE_LEN, HS_DESC_IV_LEN, HsDescEncNonce, HsDescEncryption,
19};
20
21/// The only currently recognized `desc-auth-type`.
22//
23// TODO: In theory this should be an enum, if we ever add a second value here.
24pub(super) const HS_DESC_AUTH_TYPE: &str = "x25519";
25
26/// A more-or-less verbatim representation of the middle document of an onion
27/// service descriptor.
28#[derive(Debug, Clone)]
29#[cfg_attr(feature = "hsdesc-inner-docs", visibility::make(pub))]
30pub(super) struct HsDescMiddle {
31    /// A public key used by authorized clients to decrypt the key used to
32    /// decrypt the encryption layer and decode the inner document.  This is
33    /// ignored if restricted discovery is not in use.
34    ///
35    /// This is `KP_hss_desc_enc`, and appears as `desc-auth-ephemeral-key` in
36    /// the document format; It is used along with `KS_hsc_desc_enc` to perform
37    /// a diffie-hellman operation and decrypt the encryption layer.
38    svc_desc_enc_key: HsSvcDescEncKey,
39    /// One or more authorized clients, and the key exchange information that
40    /// they use to compute shared keys for decrypting the encryption layer.
41    ///
42    /// Each of these is parsed from a `auth-client` line.
43    auth_clients: Vec<AuthClient>,
44    /// The (encrypted) inner document of the onion service descriptor.
45    encrypted: Vec<u8>,
46}
47
48impl HsDescMiddle {
49    /// Decrypt the encrypted inner document contained within this middle
50    /// document.
51    ///
52    /// If present, `key` is an authorization key, and we assume that the
53    /// decryption is nontrivial.
54    ///
55    /// A failure may mean either that the encryption was corrupted, or that we
56    /// didn't have the right key.
57    #[cfg_attr(feature = "hsdesc-inner-docs", visibility::make(pub))]
58    pub(super) fn decrypt_inner(
59        &self,
60        blinded_id: &HsBlindId,
61        revision: RevisionCounter,
62        subcredential: &Subcredential,
63        key: Option<&HsClientDescEncSecretKey>,
64    ) -> std::result::Result<Vec<u8>, super::HsDescError> {
65        let desc_enc_nonce = key.and_then(|k| self.find_cookie(subcredential, k));
66        let decrypt = HsDescEncryption {
67            blinded_id,
68            desc_enc_nonce: desc_enc_nonce.as_ref(),
69            subcredential,
70            revision,
71            string_const: b"hsdir-encrypted-data",
72        };
73
74        match decrypt.decrypt(&self.encrypted) {
75            Ok(mut v) => {
76                // Work around a bug in an implementation we presume to be
77                // OnionBalance: it doesn't NL-terminate the final line of the
78                // inner document.
79                if !v.ends_with(b"\n") {
80                    v.push(b'\n');
81                }
82                Ok(v)
83            }
84            Err(_) => match (key, desc_enc_nonce) {
85                (Some(_), None) => Err(HsDescError::WrongDecryptionKey),
86                (Some(_), Some(_)) => Err(HsDescError::DecryptionFailed),
87                (None, _) => Err(HsDescError::MissingDecryptionKey),
88            },
89        }
90    }
91
92    /// Use a `ClientDescAuthSecretKey` (`KS_hsc_desc_enc`) to see if there is any `auth-client`
93    /// entry for us (a client who holds that secret key) in this descriptor.
94    /// If so, decrypt it and return its
95    /// corresponding "Descriptor Cookie" (`N_hs_desc_enc`)
96    ///
97    /// If no such `N_hs_desc_enc` is found, then either we do not have
98    /// permission to decrypt the encryption layer, OR no permission is required.
99    ///
100    /// (The protocol makes it intentionally impossible to distinguish any error
101    /// conditions here other than "no cookie for you.")
102    fn find_cookie(
103        &self,
104        subcredential: &Subcredential,
105        ks_hsc_desc_enc: &HsClientDescEncSecretKey,
106    ) -> Option<HsDescEncNonce> {
107        use cipher::{KeyIvInit, StreamCipher};
108        use tor_llcrypto::cipher::aes::Aes256Ctr as Cipher;
109        use tor_llcrypto::util::ct::ct_lookup;
110
111        let (client_id, cookie_key) = build_descriptor_cookie_key(
112            ks_hsc_desc_enc.as_ref(),
113            &self.svc_desc_enc_key,
114            subcredential,
115        );
116        // See whether there is any matching client_id in self.auth_ids.
117        let auth_client = ct_lookup(&self.auth_clients, |c| c.client_id.ct_eq(&client_id))?;
118
119        // We found an auth client entry: Take and decrypt the cookie `N_hs_desc_enc` at last.
120        let mut cookie = auth_client.encrypted_cookie;
121        let mut cipher = Cipher::new(&cookie_key.into(), &auth_client.iv.into());
122        cipher.apply_keystream(&mut cookie);
123        Some(cookie.into())
124    }
125}
126
127/// Information that a single authorized client can use to decrypt the onion
128/// service descriptor.
129#[derive(Debug, Clone)]
130pub(super) struct AuthClient {
131    /// A check field that clients can use to see if this [`AuthClient`] entry corresponds
132    /// to a key they hold.
133    ///
134    /// This is the first part of the `auth-client` line.
135    pub(super) client_id: CtByteArray<HS_DESC_CLIENT_ID_LEN>,
136    /// An IV used to decrypt `encrypted_cookie`.
137    ///
138    /// This is the second item on the `auth-client` line.
139    pub(super) iv: [u8; HS_DESC_IV_LEN],
140    /// An encrypted value used to find the descriptor cookie `N_hs_desc_enc`,
141    /// which in turn is
142    /// needed to decrypt the [HsDescMiddle]'s `encrypted_body`.
143    ///
144    /// This is the third item on the `auth-client` line.  When decrypted, it
145    /// reveals a `DescEncEncryptionCookie` (`N_hs_desc_enc`, not yet so named
146    /// in the spec).
147    pub(super) encrypted_cookie: [u8; HS_DESC_ENC_NONCE_LEN],
148}
149
150impl AuthClient {
151    /// Try to extract an AuthClient from a single AuthClient item.
152    fn from_item(item: &Item<'_, HsMiddleKwd>) -> Result<Self> {
153        use crate::NetdocErrorKind as EK;
154
155        if item.kwd() != HsMiddleKwd::AUTH_CLIENT {
156            return Err(EK::Internal.with_msg("called with invalid argument."));
157        }
158        let client_id = item.parse_arg::<B64>(0)?.into_array()?.into();
159        let iv = item.parse_arg::<B64>(1)?.into_array()?;
160        let encrypted_cookie = item.parse_arg::<B64>(2)?.into_array()?;
161        Ok(AuthClient {
162            client_id,
163            iv,
164            encrypted_cookie,
165        })
166    }
167}
168
169decl_keyword! {
170    pub(crate) HsMiddleKwd {
171        "desc-auth-type" => DESC_AUTH_TYPE,
172        "desc-auth-ephemeral-key" => DESC_AUTH_EPHEMERAL_KEY,
173        "auth-client" => AUTH_CLIENT,
174        "encrypted" => ENCRYPTED,
175    }
176}
177
178/// Rules about how keywords appear in the middle document of an onion service
179/// descriptor.
180static HS_MIDDLE_RULES: LazyLock<SectionRules<HsMiddleKwd>> = LazyLock::new(|| {
181    use HsMiddleKwd::*;
182
183    let mut rules = SectionRules::builder();
184    rules.add(DESC_AUTH_TYPE.rule().required().args(1..));
185    rules.add(DESC_AUTH_EPHEMERAL_KEY.rule().required().args(1..));
186    rules.add(AUTH_CLIENT.rule().required().may_repeat().args(3..));
187    rules.add(ENCRYPTED.rule().required().obj_required());
188    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
189
190    rules.build()
191});
192
193impl HsDescMiddle {
194    /// Try to parse the middle document of an onion service descriptor from a provided
195    /// string.
196    #[cfg_attr(feature = "hsdesc-inner-docs", visibility::make(pub))]
197    pub(super) fn parse(s: &str) -> Result<HsDescMiddle> {
198        let mut reader = NetDocReader::new(s)?;
199        let result = HsDescMiddle::take_from_reader(&mut reader).map_err(|e| e.within(s))?;
200        Ok(result)
201    }
202
203    /// Extract an HsDescMiddle from a reader.
204    ///
205    /// The reader must contain a single HsDescOuter; we return an error if not.
206    fn take_from_reader(reader: &mut NetDocReader<'_, HsMiddleKwd>) -> Result<HsDescMiddle> {
207        use crate::NetdocErrorKind as EK;
208        use HsMiddleKwd::*;
209
210        let body = HS_MIDDLE_RULES.parse(reader)?;
211
212        // Check for the only currently recognized `desc-auth-type`
213        {
214            let auth_type = body.required(DESC_AUTH_TYPE)?.required_arg(0)?;
215            if auth_type != HS_DESC_AUTH_TYPE {
216                return Err(EK::BadDocumentVersion
217                    .at_pos(Pos::at(auth_type))
218                    .with_msg(format!("Unrecognized desc-auth-type {auth_type:?}")));
219            }
220        }
221
222        // Extract `KP_hss_desc_enc` from DESC_AUTH_EPHEMERAL_KEY
223        let ephemeral_key: HsSvcDescEncKey = {
224            let token = body.required(DESC_AUTH_EPHEMERAL_KEY)?;
225            let key = curve25519::PublicKey::from(token.parse_arg::<B64>(0)?.into_array()?);
226            key.into()
227        };
228
229        // Parse all the auth-client lines.
230        let auth_clients: Vec<AuthClient> = body
231            .slice(AUTH_CLIENT)
232            .iter()
233            .map(AuthClient::from_item)
234            .collect::<Result<Vec<_>>>()?;
235
236        // The encrypted body is taken verbatim.
237        let encrypted_body: Vec<u8> = body.required(ENCRYPTED)?.obj("MESSAGE")?;
238
239        Ok(HsDescMiddle {
240            svc_desc_enc_key: ephemeral_key,
241            auth_clients,
242            encrypted: encrypted_body,
243        })
244    }
245}
246
247#[cfg(test)]
248mod test {
249    // @@ begin test lint list maintained by maint/add_warning @@
250    #![allow(clippy::bool_assert_comparison)]
251    #![allow(clippy::clone_on_copy)]
252    #![allow(clippy::dbg_macro)]
253    #![allow(clippy::mixed_attributes_style)]
254    #![allow(clippy::print_stderr)]
255    #![allow(clippy::print_stdout)]
256    #![allow(clippy::single_char_pattern)]
257    #![allow(clippy::unwrap_used)]
258    #![allow(clippy::unchecked_duration_subtraction)]
259    #![allow(clippy::useless_vec)]
260    #![allow(clippy::needless_pass_by_value)]
261    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
262
263    use hex_literal::hex;
264    use tor_checkable::{SelfSigned, Timebound};
265
266    use super::*;
267    use crate::doc::hsdesc::{
268        outer::HsDescOuter,
269        test_data::{TEST_DATA, TEST_SUBCREDENTIAL},
270    };
271
272    #[test]
273    fn parse_good() -> Result<()> {
274        let desc = HsDescOuter::parse(TEST_DATA)?
275            .dangerously_assume_wellsigned()
276            .dangerously_assume_timely();
277        let subcred = TEST_SUBCREDENTIAL.into();
278        let body = desc.decrypt_body(&subcred).unwrap();
279        let body = std::str::from_utf8(&body[..]).unwrap();
280
281        let middle = HsDescMiddle::parse(body)?;
282        assert_eq!(
283            middle.svc_desc_enc_key.as_bytes(),
284            &hex!("161090571E6DB517C0C8591CE524A56DF17BAE3FF8DCD50735F9AEB89634073E")
285        );
286        assert_eq!(middle.auth_clients.len(), 16);
287
288        // Here we make sure that decryption "works" minimally and returns some
289        // bytes for a descriptor with no HsClientDescEncSecretKey.
290        //
291        // We make sure that the actual decrypted value is reasonable elsewhere,
292        // in the tests in inner.rs.
293        //
294        // We test the case where a HsClientDescEncSecretKey is needed
295        // elsewhere, in `hsdesc::test::parse_desc_auth_good`.
296        let _inner_body = middle
297            .decrypt_inner(&desc.blinded_id(), desc.revision_counter(), &subcred, None)
298            .unwrap();
299
300        Ok(())
301    }
302}