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

            
3
mod keys;
4
mod views;
5

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

            
15
use tor_chanmgr::ChanMgr;
16
use tor_keymgr::KeyMgr;
17
use tor_netdir::{DirEvent, NetDirProvider};
18
use tor_proto::RelayChannelAuthMaterial;
19
use tor_proto::relay::CreateRequestHandler;
20
use tor_relay_crypto::pk::{RelayIdentityKeypair, RelayIdentityRsaKeypair, RelayNtorKeys};
21
use tor_rtcompat::{Runtime, SleepProviderExt};
22

            
23
use crate::{
24
    keys::{RelayIdentityKeypairSpecifier, RelayIdentityRsaKeypairSpecifier},
25
    tasks::crypto::views::FullKeyView,
26
};
27

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

            
35
/// Key rotation parameters derived from the consensus.
36
#[derive(Copy, Clone, Debug)]
37
struct KeyRotationParams {
38
    /// How long a newly generated ntor key is valid.
39
    ntor_lifetime: Duration,
40
    /// How long after expiry the ntor key is still accepted for incoming circuits.
41
    ntor_grace_period: Duration,
42
}
43

            
44
impl From<&tor_netdir::params::NetParameters> for KeyRotationParams {
45
60
    fn from(params: &tor_netdir::params::NetParameters) -> Self {
46
60
        let rotation_days = params.onion_key_rotation_days.get() as u64;
47
        // Grace period is clamped to [1, rotation_days] per the spec.
48
60
        let grace_days = (params.onion_key_grace_period_days.get() as u64).min(rotation_days);
49
60
        Self {
50
60
            ntor_lifetime: Duration::from_secs(rotation_days * 24 * 60 * 60),
51
60
            ntor_grace_period: Duration::from_secs(grace_days * 24 * 60 * 60),
52
60
        }
53
60
    }
54
}
55

            
56
/// Key material generated/loaded at init.
57
///
58
/// This is specific to be at the relay startup and only returned by `try_generate_keys()` that is
59
/// only called before the relay starts.
60
pub(crate) struct InitKeyMaterial {
61
    /// Channel authentication key material.
62
    pub(crate) chan_auth_keys: RelayChannelAuthMaterial,
63
    /// Ntor keys.
64
    pub(crate) ntor_keys: RelayNtorKeys,
65
}
66

            
67
/// Attempt to initialize the key material needed for a relay to function. This function will
68
/// generate any missing keys or load them from the given [`KeyMgr`]. The keys are:
69
///
70
/// * Identity Ed25519 keypair.
71
/// * Identity RSA.
72
/// * Relay signing keypair.
73
/// * Relay link signing keypair.
74
/// * Relay ntor keypair.
75
///
76
/// This function is only called when our relay initializes in order to attempt to generate any
77
/// missing keys or/and rotate expired keys.
78
///
79
/// Returned the initialization key material.
80
4
pub(crate) fn init_keys<R: Runtime>(
81
4
    runtime: &R,
82
4
    keymgr: Arc<KeyMgr>,
83
4
) -> anyhow::Result<InitKeyMaterial> {
84
4
    let now = runtime.wallclock();
85

            
86
    // Attempt to generate our identity keys (ed and RSA). Those keys DO NOT rotate. It won't be
87
    // replaced if they already exists.
88
4
    keys::generate_key::<RelayIdentityKeypair>(&keymgr, &RelayIdentityKeypairSpecifier::new())?;
89
4
    keys::generate_key::<RelayIdentityRsaKeypair>(
90
4
        &keymgr,
91
4
        &RelayIdentityRsaKeypairSpecifier::new(),
92
    )?;
93

            
94
    // Attempt to rotate the keys. Any missing keys (and cert) will be generated. At bootstrap
95
    // there is no consensus yet, so we have to use the default parameters.
96
4
    let _ = keys::try_rotate_keys(
97
4
        now,
98
4
        &keymgr,
99
4
        KeyRotationParams::from(&tor_netdir::params::NetParameters::default()),
100
    )?;
101

            
102
    // Throwaway full key view only for this purpose.
103
4
    let key_view = FullKeyView::new(keymgr)?;
104

            
105
    Ok(InitKeyMaterial {
106
4
        chan_auth_keys: keys::build_proto_relay_auth_material(now, &key_view)?,
107
4
        ntor_keys: key_view.ks_ntor_keys()?,
108
    })
109
4
}
110

            
111
/// Reactor object handling the rotation of relay crypto keys.
112
pub(crate) struct Reactor<R: Runtime> {
113
    /// Underlying runtime for a time provider.
114
    runtime: R,
115
    /// Reference to the arti-relay channel manager [`ChanMgr`]
116
    chanmgr: Arc<ChanMgr<R>>,
117
    /// Reference to the create request handler so we can update it.
118
    create_request_handler: Arc<CreateRequestHandler>,
119
    /// Full key view.
120
    view: FullKeyView,
121
    /// Net directory provider used to watch for consensus changes.
122
    netdir: Arc<dyn NetDirProvider>,
123
}
124

            
125
impl<R: Runtime> Reactor<R> {
126
    /// Constructor.
127
    pub(crate) fn new(
128
        runtime: R,
129
        chanmgr: Arc<ChanMgr<R>>,
130
        create_request_handler: Arc<CreateRequestHandler>,
131
        keymgr: Arc<KeyMgr>,
132
        netdir: Arc<dyn NetDirProvider>,
133
    ) -> anyhow::Result<Self> {
134
        Ok(Self {
135
            runtime,
136
            chanmgr,
137
            create_request_handler,
138
            view: FullKeyView::new(keymgr)?,
139
            netdir,
140
        })
141
    }
142

            
143
    /// Log the relay's identities and public ntor key.
144
    fn log_public_keys(&self) -> anyhow::Result<()> {
145
        let rsa_id = self.view.ks_relayid_rsa()?.to_rsa_identity();
146
        let ed_id = self.view.ks_relayid_ed()?.to_ed25519_id();
147

            
148
        let ntor_keys = self.view.ks_ntor_keys()?;
149
        // Base64-encode the public ntor key.
150
        let ntor = Base64Unpadded::encode_string(ntor_keys.latest().public().inner().as_bytes());
151

            
152
        // Log the relay's identities.
153
        // TODO: We should also log this after a key rotation:
154
        // https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/3773#note_3367789
155
        // TODO: This is useful at info level while we're developing,
156
        // but the level should probably be lowered in the future.
157
        tracing::info!("RSA identity: {rsa_id}");
158
        tracing::info!("Ed25519 identity: {ed_id}");
159
        tracing::info!("Ntor public key: {ntor}");
160

            
161
        Ok(())
162
    }
163

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

            
168
        // Subscribe before the first run_once() so we don't miss any events that arrive
169
        // between startup and entering the select loop.
170
        let mut consensus_events = self
171
            .netdir
172
            .events()
173
            .filter(|ev| std::future::ready(matches!(ev, DirEvent::NewConsensus)));
174

            
175
        // TODO: This is mostly useful for debugging.
176
        // We might want to remove this in the future, or move this somewhere else.
177
        self.log_public_keys()
178
            .context("Failed to log public keys")?;
179

            
180
        loop {
181
            let next_wake = self.run_once()?;
182
            futures::select! {
183
                // Sleep until next wake up.
184
                _ = self.runtime.sleep_until_wallclock(next_wake).fuse() => {}
185
                // New consensus arrived, might be new parameters. Run the loop, it will pickup the
186
                // latest.
187
                ev = consensus_events.next().fuse() => {
188
                    ev.context("NetDir event stream ended unexpectedly")?;
189
                }
190
            }
191
        }
192
    }
193

            
194
    /// Helper: run once to handle a single rotation tick.
195
    fn run_once(&mut self) -> anyhow::Result<SystemTime> {
196
        let now = self.runtime.wallclock();
197
        // Attempt a rotation of all keys.
198
        let (changed, next_expiry) = self.try_rotate_keys(now)?;
199

            
200
        if changed.link_ed || changed.relaysign_ed {
201
            let auth_material = keys::build_proto_relay_auth_material(now, &self.view)?;
202
            self.chanmgr
203
                .set_relay_auth_material(Arc::new(auth_material))
204
                .context("Failed to set relay auth material on ChanMgr")?;
205
        }
206

            
207
        if changed.ntor_latest || changed.ntor_previous {
208
            let ntor_keys = self.view.ks_ntor_keys()?;
209
            self.create_request_handler.update_ntor_keys(ntor_keys);
210
        }
211

            
212
        // Sleep until the earliest key expiry minus buffer so we rotate before it expires.
213
        // If the subtraction would underflow, wake up immediately to rotate the expired key.
214
        Ok(next_expiry
215
            .checked_sub(KEY_ROTATION_EXPIRE_BUFFER)
216
            .unwrap_or(now))
217
    }
218

            
219
    /// Attempt to rotate all keys except identity keys.
220
    ///
221
    /// Holds the write lock for the entire rotate + reconcile to prevent the race where another
222
    /// task reads a key between the keymgr update and the cache update.
223
    ///
224
    /// Returns which key types changed and the earliest expiry time across all keys.
225
    fn try_rotate_keys(
226
        &mut self,
227
        now: SystemTime,
228
    ) -> anyhow::Result<(views::ValidUntilChanged, SystemTime)> {
229
        let rotation_params = KeyRotationParams::from(self.netdir.params().as_ref().as_ref());
230
        let next_expiry = keys::try_rotate_keys(now, self.view.keymgr(), rotation_params)?;
231
        let changed = self.view.recompute_valid_until()?;
232
        Ok((changed, next_expiry))
233
    }
234
}
235

            
236
#[cfg(test)]
237
mod test {
238
    // @@ begin test lint list maintained by maint/add_warning @@
239
    #![allow(clippy::bool_assert_comparison)]
240
    #![allow(clippy::clone_on_copy)]
241
    #![allow(clippy::dbg_macro)]
242
    #![allow(clippy::mixed_attributes_style)]
243
    #![allow(clippy::print_stderr)]
244
    #![allow(clippy::print_stdout)]
245
    #![allow(clippy::single_char_pattern)]
246
    #![allow(clippy::unwrap_used)]
247
    #![allow(clippy::unchecked_time_subtraction)]
248
    #![allow(clippy::useless_vec)]
249
    #![allow(clippy::needless_pass_by_value)]
250
    #![allow(clippy::string_slice)] // See arti#2571
251
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
252

            
253
    use super::*;
254

            
255
    use tor_keymgr::{ArtiEphemeralKeystore, KeyMgrBuilder};
256
    use tor_rtmock::MockRuntime;
257

            
258
    /// Initialize test basics that is runtime and a KeyMgr.
259
    pub(super) fn new_keymgr() -> Arc<KeyMgr> {
260
        let store = Box::new(ArtiEphemeralKeystore::new("test".to_string()));
261
        Arc::new(
262
            KeyMgrBuilder::default()
263
                .primary_store(store)
264
                .build()
265
                .unwrap(),
266
        )
267
    }
268

            
269
    /// Test the actual bootstrap function, `try_generate_keys()` which is in charge of
270
    /// initializing the auth material.
271
    #[test]
272
    fn test_bootstrap() {
273
        MockRuntime::test_with_various(|runtime| async move {
274
            let _auth_material = match init_keys(&runtime, new_keymgr()) {
275
                Ok(a) => a,
276
                Err(e) => {
277
                    panic!("Unable to bootstrap keys and generate RelayChannelAuthMaterial: {e}");
278
                }
279
            };
280
        });
281
    }
282
}