aboutsummaryrefslogtreecommitdiff
path: root/crates/tor-hsservice/src/svc/publish/descriptor.rs
blob: 4e40fcc4ebe416d412f4829b8db3d63750c441e8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
//! Helpers for building and representing hidden service descriptors.

use std::sync::Arc;
use std::time::{Duration, SystemTime};

use rand_core::{CryptoRng, RngCore};

use tor_cell::chancell::msg::HandshakeType;
use tor_error::{internal, into_bad_api_usage, into_internal};
use tor_hscrypto::pk::{HsBlindIdKey, HsDescSigningKeypair, HsIdKey, HsIdKeypair};
use tor_hscrypto::time::TimePeriod;
use tor_hscrypto::RevisionCounter;
use tor_keymgr::KeyMgr;
use tor_llcrypto::pk::curve25519;
use tor_netdoc::doc::hsdesc::{create_desc_sign_key_cert, HsDescBuilder};
use tor_netdoc::NetdocBuilder;

use crate::config::DescEncryptionConfig;
use crate::ipt_set::IptSet;
use crate::svc::publish::reactor::{read_blind_id_keypair, AuthorizedClientConfigError};
use crate::{
    BlindIdKeypairSpecifier, DescSigningKeypairSpecifier, FatalError, HsIdKeypairSpecifier,
    OnionServiceConfig,
};

/// Build the descriptor.
///
/// The `now` argument is used for computing the expiry of the `intro_{auth, enc}_key_cert`
/// certificates included in the descriptor. The expiry will be set to 54 hours from `now`.
///
/// Note: `blind_id_kp` is the blinded hidden service signing keypair used to sign descriptor
/// signing keys (KP_hs_blind_id, KS_hs_blind_id).
pub(super) fn build_sign<Rng: RngCore + CryptoRng>(
    keymgr: &Arc<KeyMgr>,
    config: &Arc<OnionServiceConfig>,
    ipt_set: &IptSet,
    period: TimePeriod,
    revision_counter: RevisionCounter,
    rng: &mut Rng,
    now: SystemTime,
) -> Result<VersionedDescriptor, FatalError> {
    // TODO: should this be configurable? If so, we should read it from the svc config.
    //
    /// The CREATE handshake type we support.
    const CREATE2_FORMATS: &[HandshakeType] = &[HandshakeType::NTOR];

    /// Lifetime of the intro_{auth, enc}_key_cert certificates in the descriptor.
    ///
    /// From C-Tor src/feature/hs/hs_descriptor.h:
    ///
    /// "This defines the lifetime of the descriptor signing key and the cross certification cert of
    /// that key. It is set to 54 hours because a descriptor can be around for 48 hours and because
    /// consensuses are used after the hour, add an extra 6 hours to give some time for the service
    /// to stop using it."
    const HS_DESC_CERT_LIFETIME_SEC: Duration = Duration::from_secs(54 * 60 * 60);

    let intro_points = ipt_set
        .ipts
        .iter()
        .map(|ipt_in_set| ipt_in_set.ipt.clone())
        .collect::<Vec<_>>();

    let nickname = &config.nickname;

    let svc_key_spec = HsIdKeypairSpecifier::new(nickname.clone());
    let hsid_kp = keymgr
        .get::<HsIdKeypair>(&svc_key_spec)?
        .ok_or_else(|| FatalError::MissingHsIdKeypair(nickname.clone()))?;
    let hsid = HsIdKey::from(&hsid_kp);

    let blind_id_key_spec = BlindIdKeypairSpecifier::new(nickname.clone(), period);

    // TODO: make the keystore selector configurable
    let keystore_selector = Default::default();
    let blind_id_kp = read_blind_id_keypair(keymgr, nickname, period)?
        .ok_or_else(|| internal!("hidden service offline mode not supported"))?;

    let blind_id_key = HsBlindIdKey::from(&blind_id_kp);
    let subcredential = hsid.compute_subcredential(&blind_id_key, period);

    let hs_desc_sign_key_spec = DescSigningKeypairSpecifier::new(nickname.clone(), period);
    let hs_desc_sign = keymgr.get_or_generate::<HsDescSigningKeypair>(
        &hs_desc_sign_key_spec,
        keystore_selector,
        rng,
    )?;

    // TODO #1028: support introduction-layer authentication.
    let auth_required = None;

    let is_single_onion_service =
        matches!(config.anonymity, crate::Anonymity::DangerouslyNonAnonymous);

    // TODO (#955): perhaps the certificates should be read from the keystore, rather than created
    // when building the descriptor. See #1048
    let intro_auth_key_cert_expiry = now + HS_DESC_CERT_LIFETIME_SEC;
    let intro_enc_key_cert_expiry = now + HS_DESC_CERT_LIFETIME_SEC;
    let hs_desc_sign_cert_expiry = now + HS_DESC_CERT_LIFETIME_SEC;

    // TODO (#1206): Temporarily disabled while we figure out how we want the client auth config to
    // work; see #1028
    /*
    let auth_clients: Option<Vec<curve25519::PublicKey>> = config.encrypt_descriptor
        .map(|auth_clients| build_auth_clients(&auth_clients));
    */

    let auth_clients: Option<Vec<curve25519::PublicKey>> = None;

    let desc_signing_key_cert = create_desc_sign_key_cert(
        &hs_desc_sign.as_ref().verifying_key(),
        &blind_id_kp,
        hs_desc_sign_cert_expiry,
    )
    .map_err(into_bad_api_usage!(
        "failed to sign the descriptor signing key"
    ))?;

    let desc = HsDescBuilder::default()
        .blinded_id(&(&blind_id_kp).into())
        .hs_desc_sign(hs_desc_sign.as_ref())
        .hs_desc_sign_cert(desc_signing_key_cert)
        .create2_formats(CREATE2_FORMATS)
        .auth_required(auth_required)
        .is_single_onion_service(is_single_onion_service)
        .intro_points(&intro_points[..])
        .intro_auth_key_cert_expiry(intro_auth_key_cert_expiry)
        .intro_enc_key_cert_expiry(intro_enc_key_cert_expiry)
        .lifetime(((ipt_set.lifetime.as_secs() / 60) as u16).into())
        .revision_counter(revision_counter)
        .subcredential(subcredential)
        .auth_clients(auth_clients.as_deref())
        .build_sign(rng)
        .map_err(|e| into_internal!("failed to build descriptor")(e))?;

    Ok(VersionedDescriptor {
        desc,
        revision_counter,
    })
}

/// Decode an encoded curve25519 key.
fn decode_curve25519_str(key: &str) -> Result<curve25519::PublicKey, AuthorizedClientConfigError> {
    use base64ct::{Base64, Encoding};
    let Some(enc_key) = key.strip_prefix("curve25519:") else {
        return Err(AuthorizedClientConfigError::MalformedKey);
    };
    let key = Base64::decode_vec(enc_key.trim_end())
        .map_err(AuthorizedClientConfigError::Base64Decode)?;
    let bytes: [u8; 32] = key
        .try_into()
        .map_err(|_| AuthorizedClientConfigError::MalformedKey)?;
    Ok(curve25519::PublicKey::from(bytes))
}

/// Return the keys in a directory or an error if the directory is malformed
fn read_key_dir(
    dir: &std::path::Path,
) -> Result<Vec<curve25519::PublicKey>, AuthorizedClientConfigError> {
    // TODO (#1206): We will eventually need to validate the key file names and
    // extensions.
    std::fs::read_dir(dir)
        .map_err(|e| AuthorizedClientConfigError::KeyDir {
            action: "traversing a directory",
            path: dir.into(),
            error: e.into(),
        })?
        .map(|entry| {
            let file = entry.map_err(|e| AuthorizedClientConfigError::KeyDir {
                action: "invalid key dir entry",
                path: dir.into(),
                error: e.into(),
            })?;

            let meta = file
                .metadata()
                .map_err(|e| AuthorizedClientConfigError::KeyDir {
                    action: "read metadata",
                    path: file.path(),
                    error: e.into(),
                })?;

            if !meta.is_file() {
                return Err(AuthorizedClientConfigError::MalformedFile { path: file.path() });
            }

            let buffer = std::fs::read_to_string(file.path()).map_err(|e| {
                AuthorizedClientConfigError::KeyDir {
                    action: "read",
                    path: file.path(),
                    error: e.into(),
                }
            })?;

            decode_curve25519_str(buffer.as_str())
        })
        .collect::<Result<Vec<_>, _>>()
}

/// Return the list of authorized public keys from the specified [`DescEncryptionConfig`].
fn build_auth_clients(
    auth_clients: &DescEncryptionConfig,
) -> Result<Vec<curve25519::PublicKey>, AuthorizedClientConfigError> {
    use crate::config::AuthorizedClientConfig::{Curve25519Key, DirectoryOfKeys};

    Ok(auth_clients
        .authorized_client
        .iter()
        .map(|client| match client {
            Curve25519Key(key) => Ok(vec![**key]),
            DirectoryOfKeys(dir) => read_key_dir(dir.as_ref()),
        })
        .collect::<Result<Vec<_>, _>>()?
        .into_iter()
        .flatten()
        .collect::<Vec<_>>())
}

/// The freshness status of a descriptor at a particular HsDir.
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub(super) enum DescriptorStatus {
    #[default]
    /// Dirty, needs to be (re)uploaded.
    Dirty,
    /// Clean, does not need to be reuploaded.
    Clean,
}

/// A descriptor and its revision.
#[derive(Clone)]
pub(super) struct VersionedDescriptor {
    /// The serialized descriptor.
    pub(super) desc: String,
    /// The revision counter.
    pub(super) revision_counter: RevisionCounter,
}

#[cfg(test)]
mod test {
    // @@ begin test lint