1
//! Key rotation tasks of the relay.
2

            
3
mod keys;
4
mod views;
5

            
6
use anyhow::{Context, anyhow};
7
use base64ct::{Base64Unpadded, Encoding};
8
use futures::{FutureExt as _, StreamExt as _, channel::mpsc};
9
use std::{
10
    sync::Arc,
11
    time::{Duration, SystemTime},
12
};
13
use tracing::trace;
14

            
15
use tor_async_utils::{mpsc_channel_no_memquota, oneshot};
16
use tor_chanmgr::ChanMgr;
17
use tor_error::warn_report;
18
use tor_keymgr::KeyMgr;
19
use tor_netdir::{DirEvent, NetDirProvider};
20
use tor_proto::RelayChannelAuthMaterial;
21
use tor_proto::relay::CreateRequestHandler;
22
use tor_relay_crypto::pk::{
23
    RelayIdentityKeypair, RelayIdentityRsaKeypair, RelayNtorKeys, RelayNtorPublicKey,
24
    RelaySigningKeypair,
25
};
26
use tor_rtcompat::{Runtime, SleepProviderExt};
27

            
28
use crate::{
29
    keys::{RelayIdentityKeypairSpecifier, RelayIdentityRsaKeypairSpecifier},
30
    tasks::{
31
        crypto::views::FullKeyView,
32
        descriptor::{DescriptorCommand, DescriptorCommandSender},
33
    },
34
};
35

            
36
/// Buffer time before key expiry to trigger rotation. This ensures we rotate slightly before the
37
/// key actually expires rather than right at or after expiry.
38
///
39
/// C-tor uses 3 hours for the link/auth key and 1 day for the signing key. Let's use 3 hours here,
40
/// it should be plenty to make it happen even if hiccups happen.
41
const KEY_ROTATION_EXPIRE_BUFFER: Duration = Duration::from_secs(3 * 60 * 60);
42

            
43
/// A command sent handled by the [`Reactor`] over a command channel.
44
#[derive(Debug)]
45
#[non_exhaustive]
46
pub(crate) enum CryptoCommand {
47
    /// Request to get the latest ntor key.
48
    GetLatestNtorKey {
49
        /// Reply channel for the key.
50
        tx: oneshot::Sender<RelayNtorPublicKey>,
51
    },
52
    /// Request to get the relay signing key.
53
    GetSignKey {
54
        /// Reply channel for the key.
55
        tx: oneshot::Sender<RelaySigningKeypair>,
56
    },
57
}
58

            
59
/// The sending side of the [`DescriptorCommand`] channel.
60
pub(crate) type CryptoCommandSender = mpsc::Sender<CryptoCommand>;
61
/// The receiving side of the [`DescriptorCommand`] channel.
62
pub(crate) type CryptoCommandReceiver = mpsc::Receiver<CryptoCommand>;
63

            
64
/// Returns a new [`CryptoCommand`] channel.
65
///
66
/// This is a bounded to limit key request spamming (in case of a bug).
67
pub(crate) fn new_command_channel() -> (CryptoCommandSender, CryptoCommandReceiver) {
68
    mpsc_channel_no_memquota(128)
69
}
70

            
71
/// Key rotation parameters derived from the consensus.
72
#[derive(Copy, Clone, Debug)]
73
struct KeyRotationParams {
74
    /// How long a newly generated ntor key is valid.
75
    ntor_lifetime: Duration,
76
    /// How long after expiry the ntor key is still accepted for incoming circuits.
77
    ntor_grace_period: Duration,
78
}
79

            
80
impl From<&tor_netdir::params::NetParameters> for KeyRotationParams {
81
60
    fn from(params: &tor_netdir::params::NetParameters) -> Self {
82
60
        let rotation_days = params.onion_key_rotation_days.get() as u64;
83
        // Grace period is clamped to [1, rotation_days] per the spec.
84
60
        let grace_days = (params.onion_key_grace_period_days.get() as u64).min(rotation_days);
85
60
        Self {
86
60
            ntor_lifetime: Duration::from_secs(rotation_days * 24 * 60 * 60),
87
60
            ntor_grace_period: Duration::from_secs(grace_days * 24 * 60 * 60),
88
60
        }
89
60
    }
90
}
91

            
92
/// Key material generated/loaded at init.
93
///
94
/// This is specific to be at the relay startup and only returned by `try_generate_keys()` that is
95
/// only called before the relay starts.
96
pub(crate) struct InitKeyMaterial {
97
    /// Channel authentication key material.
98
    pub(crate) chan_auth_keys: RelayChannelAuthMaterial,
99
    /// Ntor keys.
100
    pub(crate) ntor_keys: RelayNtorKeys,
101
}
102

            
103
/// Attempt to initialize the key material needed for a relay to function. This function will
104
/// generate any missing keys or load them from the given [`KeyMgr`]. The keys are:
105
///
106
/// * Identity Ed25519 keypair.
107
/// * Identity RSA.
108
/// * Relay signing keypair.
109
/// * Relay link signing keypair.
110
/// * Relay ntor keypair.
111
///
112
/// This function is only called when our relay initializes in order to attempt to generate any
113
/// missing keys or/and rotate expired keys.
114
///
115
/// Returned the initialization key material.
116
4
pub(crate) fn init_keys<R: Runtime>(
117
4
    runtime: &R,
118
4
    keymgr: &KeyMgr,
119
4
) -> anyhow::Result<InitKeyMaterial> {
120
4
    let now = runtime.wallclock();
121

            
122
    // Attempt to generate our identity keys (ed and RSA). Those keys DO NOT rotate. It won't be
123
    // replaced if they already exists.
124
4
    keys::generate_key::<RelayIdentityKeypair>(keymgr, &RelayIdentityKeypairSpecifier::new())?;
125
4
    keys::generate_key::<RelayIdentityRsaKeypair>(
126
4
        keymgr,
127
4
        &RelayIdentityRsaKeypairSpecifier::new(),
128
    )?;
129

            
130
    // Attempt to rotate the keys. Any missing keys (and cert) will be generated. At bootstrap
131
    // there is no consensus yet, so we have to use the default parameters.
132
4
    let _ = keys::try_rotate_keys(
133
4
        now,
134
4
        keymgr,
135
4
        KeyRotationParams::from(&tor_netdir::params::NetParameters::default()),
136
    )?;
137

            
138
    // Throwaway full key view only for this purpose.
139
4
    let key_view = FullKeyView::new(keymgr)?;
140

            
141
    Ok(InitKeyMaterial {
142
4
        chan_auth_keys: keys::build_proto_relay_auth_material(now, &key_view)?,
143
4
        ntor_keys: key_view.ks_ntor_keys()?,
144
    })
145
4
}
146

            
147
/// Reactor object handling the rotation of relay crypto keys.
148
pub(crate) struct Reactor<R: Runtime> {
149
    /// Underlying runtime for a time provider.
150
    runtime: R,
151
    /// Reference to the arti-relay channel manager [`ChanMgr`]
152
    chanmgr: Arc<ChanMgr<R>>,
153
    /// Reference to the create request handler so we can update it.
154
    create_request_handler: Arc<CreateRequestHandler>,
155
    /// Full key view.
156
    view: FullKeyView<KeyMgr>,
157
    /// Net directory provider used to watch for consensus changes.
158
    netdir: Arc<dyn NetDirProvider>,
159
    /// Descriptor task TX channel.
160
    desc_tx: DescriptorCommandSender,
161
    /// Our crypto command RX channel.
162
    our_rx: CryptoCommandReceiver,
163
}
164

            
165
impl<R: Runtime> Reactor<R> {
166
    /// Constructor.
167
    pub(crate) fn new(
168
        runtime: R,
169
        chanmgr: Arc<ChanMgr<R>>,
170
        create_request_handler: Arc<CreateRequestHandler>,
171
        keymgr: KeyMgr,
172
        netdir: Arc<dyn NetDirProvider>,
173
        desc_tx: DescriptorCommandSender,
174
        our_rx: CryptoCommandReceiver,
175
    ) -> anyhow::Result<Self> {
176
        Ok(Self {
177
            runtime,
178
            chanmgr,
179
            create_request_handler,
180
            view: FullKeyView::new(keymgr)?,
181
            netdir,
182
            desc_tx,
183
            our_rx,
184
        })
185
    }
186

            
187
    /// Log the relay's identities and public ntor key.
188
    fn log_public_keys(&self) -> anyhow::Result<()> {
189
        let rsa_id = self.view.ks_relayid_rsa()?.to_rsa_identity();
190
        let ed_id = self.view.ks_relayid_ed()?.to_ed25519_id();
191

            
192
        let ntor_keys = self.view.ks_ntor_keys()?;
193
        // Base64-encode the public ntor key.
194
        let ntor = Base64Unpadded::encode_string(ntor_keys.latest().public().inner().as_bytes());
195

            
196
        // Log the relay's identities.
197
        // TODO: We should also log this after a key rotation:
198
        // https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/3773#note_3367789
199
        // TODO: This is useful at info level while we're developing,
200
        // but the level should probably be lowered in the future.
201
        tracing::info!("RSA identity: {rsa_id}");
202
        tracing::info!("Ed25519 identity: {ed_id}");
203
        tracing::info!("Ntor public key: {ntor}");
204

            
205
        Ok(())
206
    }
207

            
208
    /// Handle a [`CryptoCommand`] received by the reactor.
209
    fn handle_command(&mut self, cmd: CryptoCommand) -> anyhow::Result<()> {
210
        match cmd {
211
            CryptoCommand::GetLatestNtorKey { tx } => {
212
                let pubkey = self.view.ks_ntor_keys()?.latest().public();
213
                tx.send(pubkey)
214
                    .map_err(|_| anyhow!("GetLatestNtorKey replay tx failed"))?;
215
            }
216
            CryptoCommand::GetSignKey { tx } => {
217
                let keypair = self.view.ks_relaysign_ed()?;
218
                tx.send(keypair)
219
                    .map_err(|_| anyhow!("GetSignKey replay tx failed"))?;
220
            }
221
        }
222

            
223
        Ok(())
224
    }
225

            
226
    /// Launch the reactor, and run until an error is encountered.
227
    pub(crate) async fn run(mut self) -> anyhow::Result<void::Void> {
228
        trace!("Starting crypto reactor task");
229

            
230
        // Subscribe before the first run_once() so we don't miss any events that arrive
231
        // between startup and entering the select loop.
232
        let mut consensus_events = self
233
            .netdir
234
            .events()
235
            .filter(|ev| std::future::ready(matches!(ev, DirEvent::NewConsensus)));
236

            
237
        // TODO: This is mostly useful for debugging.
238
        // We might want to remove this in the future, or move this somewhere else.
239
        self.log_public_keys()
240
            .context("Failed to log public keys")?;
241

            
242
        loop {
243
            let next_wake = self.run_once()?;
244
            futures::select! {
245
                // Sleep until next wake up.
246
                _ = self.runtime.sleep_until_wallclock(next_wake).fuse() => {}
247
                // New consensus arrived, might be new parameters. Run the loop, it will pickup the
248
                // latest.
249
                ev = consensus_events.next().fuse() => {
250
                    ev.context("NetDir event stream ended unexpectedly")?;
251
                }
252
                // Crypto command channel.
253
                cmd = self.our_rx.next().fuse() => {
254
                    let cmd = cmd.context("Crypto command channel closed")?;
255
                    if let Err(e) = self.handle_command(cmd) {
256
                        warn_report!(e, "Crypto task command failure");
257
                    }
258
                }
259
            }
260
        }
261
    }
262

            
263
    /// Helper: run once to handle a single rotation tick.
264
    fn run_once(&mut self) -> anyhow::Result<SystemTime> {
265
        let now = self.runtime.wallclock();
266
        // Attempt a rotation of all keys.
267
        let (changed, next_expiry) = self.try_rotate_keys(now)?;
268

            
269
        if changed.link_ed || changed.relaysign_ed {
270
            let auth_material = keys::build_proto_relay_auth_material(now, &self.view)?;
271
            self.chanmgr
272
                .set_relay_auth_material(Arc::new(auth_material))
273
                .context("Failed to set relay auth material on ChanMgr")?;
274
        }
275

            
276
        if changed.ntor_latest || changed.ntor_previous {
277
            let ntor_keys = self.view.ks_ntor_keys()?;
278
            self.create_request_handler.update_ntor_keys(ntor_keys);
279
        }
280

            
281
        // Notify the descriptor task that its keys have changed.
282
        if changed.relay_desc_keys_changed() {
283
            self.desc_tx
284
                .try_send(DescriptorCommand::Publish)
285
                .context("Desc task channel try_send failed")?;
286
            // Note, we can never wait for the publish to finish here because the
287
            // descriptor task upon receiving the command will send us commands to get
288
            // the keys it needs. Waiting here would lead to a deadlock.
289
        }
290

            
291
        // Sleep until the earliest key expiry minus buffer so we rotate before it expires.
292
        // If the subtraction would underflow, wake up immediately to rotate the expired key.
293
        Ok(next_expiry
294
            .checked_sub(KEY_ROTATION_EXPIRE_BUFFER)
295
            .unwrap_or(now))
296
    }
297

            
298
    /// Attempt to rotate all keys except identity keys.
299
    ///
300
    /// Holds the write lock for the entire rotate + reconcile to prevent the race where another
301
    /// task reads a key between the keymgr update and the cache update.
302
    ///
303
    /// Returns which key types changed and the earliest expiry time across all keys.
304
    fn try_rotate_keys(
305
        &mut self,
306
        now: SystemTime,
307
    ) -> anyhow::Result<(views::ValidUntilChanged, SystemTime)> {
308
        let rotation_params = KeyRotationParams::from(self.netdir.params().as_ref().as_ref());
309
        let next_expiry = keys::try_rotate_keys(now, self.view.keymgr(), rotation_params)?;
310
        let changed = self.view.recompute_valid_until()?;
311
        Ok((changed, next_expiry))
312
    }
313
}
314

            
315
#[cfg(test)]
316
mod test {
317
    // @@ begin test lint list maintained by maint/add_warning @@
318
    #![allow(clippy::bool_assert_comparison)]
319
    #![allow(clippy::clone_on_copy)]
320
    #![allow(clippy::dbg_macro)]
321
    #![allow(clippy::mixed_attributes_style)]
322
    #![allow(clippy::print_stderr)]
323
    #![allow(clippy::print_stdout)]
324
    #![allow(clippy::single_char_pattern)]
325
    #![allow(clippy::unwrap_used)]
326
    #![allow(clippy::unchecked_time_subtraction)]
327
    #![allow(clippy::useless_vec)]
328
    #![allow(clippy::needless_pass_by_value)]
329
    #![allow(clippy::string_slice)] // See arti#2571
330
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
331

            
332
    use super::*;
333

            
334
    use tor_keymgr::{ArtiEphemeralKeystore, KeyMgrBuilder};
335
    use tor_rtmock::MockRuntime;
336

            
337
    /// Initialize test basics that is runtime and a KeyMgr.
338
    pub(super) fn new_keymgr() -> KeyMgr {
339
        let store = Box::new(ArtiEphemeralKeystore::new("test".to_string()));
340
        KeyMgrBuilder::default()
341
            .primary_store(store)
342
            .build()
343
            .unwrap()
344
    }
345

            
346
    /// Test the actual bootstrap function, `try_generate_keys()` which is in charge of
347
    /// initializing the auth material.
348
    #[test]
349
    fn test_bootstrap() {
350
        MockRuntime::test_with_various(|runtime| async move {
351
            let _auth_material = match init_keys(&runtime, &new_keymgr()) {
352
                Ok(a) => a,
353
                Err(e) => {
354
                    panic!("Unable to bootstrap keys and generate RelayChannelAuthMaterial: {e}");
355
                }
356
            };
357
        });
358
    }
359
}