tor_netdoc/doc/hsdesc/build/
middle.rs

1//! Functionality for encoding the middle document of an onion service descriptor.
2//!
3//! NOTE: `HsDescMiddle` is a private helper for building hidden service descriptors, and is
4//! not meant to be used directly. Hidden services will use `HsDescBuilder` to build and encode
5//! hidden service descriptors.
6
7use crate::build::NetdocEncoder;
8use crate::doc::hsdesc::build::ClientAuth;
9use crate::doc::hsdesc::desc_enc::{
10    build_descriptor_cookie_key, HS_DESC_CLIENT_ID_LEN, HS_DESC_ENC_NONCE_LEN, HS_DESC_IV_LEN,
11};
12use crate::doc::hsdesc::middle::{AuthClient, HsMiddleKwd, HS_DESC_AUTH_TYPE};
13use crate::NetdocBuilder;
14
15use tor_bytes::EncodeError;
16use tor_hscrypto::Subcredential;
17use tor_llcrypto::pk::curve25519::{EphemeralSecret, PublicKey};
18use tor_llcrypto::util::ct::CtByteArray;
19
20use base64ct::{Base64, Encoding};
21use rand::{CryptoRng, Rng, RngCore};
22
23/// The representation of the middle document of an onion service descriptor.
24///
25/// The plaintext format of this document is described in section 2.5.1.2. of rend-spec-v3.
26#[derive(Debug)]
27pub(super) struct HsDescMiddle<'a> {
28    /// Restricted discovery parameters, if restricted discovery is enabled. If set to `None`,
29    /// restricted discovery is disabled.
30    pub(super) client_auth: Option<&'a ClientAuth<'a>>,
31    /// The "subcredential" of the onion service.
32    pub(super) subcredential: Subcredential,
33    /// The (encrypted) inner document of the onion service descriptor.
34    ///
35    /// The `encrypted` field is created by encrypting a
36    /// [`build::inner::HsDescInner`](super::inner::HsDescInner)
37    /// inner document as described in sections
38    /// 2.5.2.1. and 2.5.2.2. of rend-spec-v3.
39    pub(super) encrypted: Vec<u8>,
40}
41
42impl<'a> NetdocBuilder for HsDescMiddle<'a> {
43    fn build_sign<R: RngCore + CryptoRng>(self, rng: &mut R) -> Result<String, EncodeError> {
44        use cipher::{KeyIvInit, StreamCipher};
45        use tor_llcrypto::cipher::aes::Aes256Ctr as Cipher;
46        use HsMiddleKwd::*;
47
48        let HsDescMiddle {
49            client_auth,
50            subcredential,
51            encrypted,
52        } = self;
53
54        let mut encoder = NetdocEncoder::new();
55
56        let (ephemeral_key, auth_clients): (_, Box<dyn std::iter::Iterator<Item = AuthClient>>) =
57            match client_auth {
58                Some(client_auth) if client_auth.auth_clients.is_empty() => {
59                    return Err(tor_error::bad_api_usage!(
60                        "restricted discovery is enabled, but there are no authorized clients"
61                    )
62                    .into());
63                }
64                Some(client_auth) => {
65                    // Restricted discovery is enabled.
66                    let auth_clients = client_auth.auth_clients.iter().map(|client| {
67                        let (client_id, cookie_key) = build_descriptor_cookie_key(
68                            client_auth.ephemeral_key.secret.as_ref(),
69                            client,
70                            &subcredential,
71                        );
72
73                        // Encrypt the descriptor cookie with the public key of the client.
74                        let mut encrypted_cookie = client_auth.descriptor_cookie;
75                        let iv = rng.random::<[u8; HS_DESC_IV_LEN]>();
76                        let mut cipher = Cipher::new(&cookie_key.into(), &iv.into());
77                        cipher.apply_keystream(&mut encrypted_cookie);
78
79                        AuthClient {
80                            client_id,
81                            iv,
82                            encrypted_cookie,
83                        }
84                    });
85
86                    (*client_auth.ephemeral_key.public, Box::new(auth_clients))
87                }
88                None => {
89                    // Generate a single client-auth line filled with random values for client-id,
90                    // iv, and encrypted-cookie.
91                    let dummy_auth_client = AuthClient {
92                        client_id: CtByteArray::from(rng.random::<[u8; HS_DESC_CLIENT_ID_LEN]>()),
93                        iv: rng.random::<[u8; HS_DESC_IV_LEN]>(),
94                        encrypted_cookie: rng.random::<[u8; HS_DESC_ENC_NONCE_LEN]>(),
95                    };
96
97                    // As per section 2.5.1.2. of rend-spec-v3, if restricted discovery is disabled,
98                    // we need to generate some fake data for the desc-auth-ephemeral-key
99                    // and auth-client fields.
100                    let secret = EphemeralSecret::random_from_rng(rng);
101                    let dummy_ephemeral_key = PublicKey::from(&secret);
102
103                    (
104                        dummy_ephemeral_key,
105                        Box::new(std::iter::once(dummy_auth_client)),
106                    )
107                }
108            };
109
110        encoder.item(DESC_AUTH_TYPE).arg(&HS_DESC_AUTH_TYPE);
111        encoder
112            .item(DESC_AUTH_EPHEMERAL_KEY)
113            .arg(&Base64::encode_string(ephemeral_key.as_bytes()));
114
115        for auth_client in auth_clients {
116            encoder
117                .item(AUTH_CLIENT)
118                .arg(&Base64::encode_string(&*auth_client.client_id))
119                .arg(&Base64::encode_string(&auth_client.iv))
120                .arg(&Base64::encode_string(&auth_client.encrypted_cookie));
121        }
122
123        encoder.item(ENCRYPTED).object("MESSAGE", encrypted);
124        encoder.finish().map_err(|e| e.into())
125    }
126}
127
128#[cfg(test)]
129mod test {
130    // @@ begin test lint list maintained by maint/add_warning @@
131    #![allow(clippy::bool_assert_comparison)]
132    #![allow(clippy::clone_on_copy)]
133    #![allow(clippy::dbg_macro)]
134    #![allow(clippy::mixed_attributes_style)]
135    #![allow(clippy::print_stderr)]
136    #![allow(clippy::print_stdout)]
137    #![allow(clippy::single_char_pattern)]
138    #![allow(clippy::unwrap_used)]
139    #![allow(clippy::unchecked_duration_subtraction)]
140    #![allow(clippy::useless_vec)]
141    #![allow(clippy::needless_pass_by_value)]
142    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
143
144    use super::*;
145    use crate::doc::hsdesc::build::test::{create_curve25519_pk, expect_bug};
146    use crate::doc::hsdesc::build::ClientAuth;
147    use crate::doc::hsdesc::test_data::TEST_SUBCREDENTIAL;
148    use tor_basic_utils::test_rng::Config;
149    use tor_hscrypto::pk::HsSvcDescEncKeypair;
150    use tor_llcrypto::pk::curve25519;
151
152    // Some dummy bytes, not actually encrypted.
153    const TEST_ENCRYPTED_VALUE: &[u8] = &[1, 2, 3, 4];
154
155    #[test]
156    fn middle_hsdesc_encoding_no_client_auth() {
157        let hs_desc = HsDescMiddle {
158            client_auth: None,
159            subcredential: TEST_SUBCREDENTIAL.into(),
160            encrypted: TEST_ENCRYPTED_VALUE.into(),
161        }
162        .build_sign(&mut Config::Deterministic.into_rng())
163        .unwrap();
164
165        assert_eq!(
166            hs_desc,
167            r#"desc-auth-type x25519
168desc-auth-ephemeral-key XI/a9NGh/7ClaFcKqtdI9DoP8da5ovwPDdgCHUr3xX0=
169auth-client F+Z6EDfG7oc= 7EIXRtlSozVtGAs6+mNujQ== pNtSIyiCahSvUVg+7s71Ow==
170encrypted
171-----BEGIN MESSAGE-----
172AQIDBA==
173-----END MESSAGE-----
174"#
175        );
176    }
177
178    #[test]
179    fn middle_hsdesc_encoding_with_bad_client_auth() {
180        let mut rng = Config::Deterministic.into_rng();
181        let secret = curve25519::StaticSecret::random_from_rng(&mut rng);
182        let public = curve25519::PublicKey::from(&secret).into();
183
184        let client_auth = ClientAuth {
185            ephemeral_key: HsSvcDescEncKeypair {
186                public,
187                secret: secret.into(),
188            },
189            auth_clients: &[],
190            descriptor_cookie: rand::Rng::random::<[u8; HS_DESC_ENC_NONCE_LEN]>(&mut rng),
191        };
192
193        let err = HsDescMiddle {
194            client_auth: Some(&client_auth),
195            subcredential: TEST_SUBCREDENTIAL.into(),
196            encrypted: TEST_ENCRYPTED_VALUE.into(),
197        }
198        .build_sign(&mut rng)
199        .unwrap_err();
200
201        assert!(expect_bug(err)
202            .contains("restricted discovery is enabled, but there are no authorized clients"));
203    }
204
205    #[test]
206    fn middle_hsdesc_encoding_client_auth() {
207        let mut rng = Config::Deterministic.into_rng();
208        // 2 authorized clients
209        let auth_clients = vec![
210            create_curve25519_pk(&mut rng),
211            create_curve25519_pk(&mut rng),
212        ];
213
214        let secret = curve25519::StaticSecret::random_from_rng(&mut rng);
215        let public = curve25519::PublicKey::from(&secret).into();
216
217        let client_auth = ClientAuth {
218            ephemeral_key: HsSvcDescEncKeypair {
219                public,
220                secret: secret.into(),
221            },
222            auth_clients: &auth_clients,
223            descriptor_cookie: rand::Rng::random::<[u8; HS_DESC_ENC_NONCE_LEN]>(&mut rng),
224        };
225
226        let hs_desc = HsDescMiddle {
227            client_auth: Some(&client_auth),
228            subcredential: TEST_SUBCREDENTIAL.into(),
229            encrypted: TEST_ENCRYPTED_VALUE.into(),
230        }
231        .build_sign(&mut Config::Deterministic.into_rng())
232        .unwrap();
233
234        assert_eq!(
235            hs_desc,
236            r#"desc-auth-type x25519
237desc-auth-ephemeral-key 9Upi9XNWyqx3ZwHeQ5r3+Dh116k+C4yHeE9BcM68HDc=
238auth-client pxfSbhBMPw0= F+Z6EDfG7ofsQhdG2VKjNQ== fEursUD9Bj5Q9mFP8sIddA==
239auth-client DV7nt+CDOno= bRgLOvpjbo2k21IjKIJqFA== 2yVT+Lpm/WL4JAU64zlGpQ==
240encrypted
241-----BEGIN MESSAGE-----
242AQIDBA==
243-----END MESSAGE-----
244"#
245        );
246    }
247}