diff options
| author | David Lönnhager <david.l@mullvad.net> | 2019-12-17 12:31:11 +0100 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2019-12-17 12:31:11 +0100 |
| commit | a85e9db8d1689ac292daa60f674067f21cec1f33 (patch) | |
| tree | 70f42303da155de47edbf1689cf2b8958eeea671 | |
| parent | a5f588de6cc711ea3b5a790056f1a50242e98dcc (diff) | |
| parent | 964d5211746a3e2841e6a86c22d1c5bd757707bb (diff) | |
| download | mullvadvpn-a85e9db8d1689ac292daa60f674067f21cec1f33.tar.xz mullvadvpn-a85e9db8d1689ac292daa60f674067f21cec1f33.zip | |
Merge branch 'rotate-wireguard-pubkey'
| -rw-r--r-- | CHANGELOG.md | 4 | ||||
| -rw-r--r-- | mullvad-cli/src/cmds/tunnel.rs | 50 | ||||
| -rw-r--r-- | mullvad-daemon/src/lib.rs | 64 | ||||
| -rw-r--r-- | mullvad-daemon/src/management_interface.rs | 22 | ||||
| -rw-r--r-- | mullvad-daemon/src/wireguard.rs | 245 | ||||
| -rw-r--r-- | mullvad-ipc-client/src/lib.rs | 4 | ||||
| -rw-r--r-- | mullvad-types/src/settings/mod.rs | 17 | ||||
| -rw-r--r-- | talpid-types/src/net/wireguard.rs | 2 |
8 files changed, 398 insertions, 10 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c70a86bac..4aad5e6eaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,10 @@ Line wrap the file at 100 chars. Th improve battery life in some cases. ### Security +- Add automatic key rotation for WireGuard (every 7 days by default). This limits the potential + for an attacker to correlate traffic with a public key and identity, and reduces the harm of + software that might leak the private tunnel IP (since it is no longer fixed). + #### Linux - Stop [CVE-2019-14899](https://seclists.org/oss-sec/2019/q4/122) by dropping all packets destined for the tunnel IP coming in on some other interface than the tunnel. diff --git a/mullvad-cli/src/cmds/tunnel.rs b/mullvad-cli/src/cmds/tunnel.rs index 75119ac87d..2d0da4bf32 100644 --- a/mullvad-cli/src/cmds/tunnel.rs +++ b/mullvad-cli/src/cmds/tunnel.rs @@ -56,6 +56,18 @@ fn create_wireguard_keys_subcommand() -> clap::App<'static, 'static> { .setting(clap::AppSettings::SubcommandRequiredElseHelp) .subcommand(clap::SubCommand::with_name("check")) .subcommand(clap::SubCommand::with_name("generate")) + .subcommand(create_wireguard_keys_rotation_interval_subcommand()) +} + +fn create_wireguard_keys_rotation_interval_subcommand() -> clap::App<'static, 'static> { + clap::SubCommand::with_name("rotation-interval") + .about("Manage automatic key rotation (specified in hours; 0 = disabled)") + .setting(clap::AppSettings::SubcommandRequiredElseHelp) + .subcommand(clap::SubCommand::with_name("get")) + .subcommand(clap::SubCommand::with_name("reset").about("Use the default rotation interval")) + .subcommand( + clap::SubCommand::with_name("set").arg(clap::Arg::with_name("interval").required(true)), + ) } @@ -120,8 +132,17 @@ impl Tunnel { ("key", Some(matches)) => match matches.subcommand() { ("check", _) => Self::process_wireguard_key_check(), ("generate", _) => Self::process_wireguard_key_generate(), + ("rotation-interval", Some(matches)) => match matches.subcommand() { + ("get", _) => Self::process_wireguard_rotation_interval_get(), + ("set", Some(matches)) => { + Self::process_wireguard_rotation_interval_set(matches) + } + ("reset", _) => Self::process_wireguard_rotation_interval_reset(), + _ => unreachable!("unhandled command"), + }, _ => unreachable!("unhandled command"), }, + _ => unreachable!("unhandled command"), } } @@ -184,6 +205,35 @@ impl Tunnel { Ok(()) } + fn process_wireguard_rotation_interval_get() -> Result<()> { + let tunnel_options = Self::get_tunnel_options()?; + println!( + "Rotation interval: {} hour(s)", + tunnel_options + .wireguard + .automatic_rotation + .map(|interval| interval.to_string()) + .unwrap_or_else(|| "default".to_owned()) + ); + Ok(()) + } + + fn process_wireguard_rotation_interval_set(matches: &clap::ArgMatches<'_>) -> Result<()> { + let rotate_interval = + value_t!(matches.value_of("interval"), u32).unwrap_or_else(|e| e.exit()); + let mut rpc = new_rpc_client()?; + rpc.set_wireguard_rotation_interval(Some(rotate_interval))?; + println!("Set key rotation interval: {} hour(s)", rotate_interval); + Ok(()) + } + + fn process_wireguard_rotation_interval_reset() -> Result<()> { + let mut rpc = new_rpc_client()?; + rpc.set_wireguard_rotation_interval(None)?; + println!("Set key rotation interval: default"); + Ok(()) + } + fn handle_ipv6_cmd(matches: &clap::ArgMatches<'_>) -> Result<()> { if matches.subcommand_matches("get").is_some() { Self::process_ipv6_get() diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index aabcbb6952..4d01e802f5 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -448,7 +448,6 @@ where ) .map_err(Error::TunnelError)?; - let wireguard_key_manager = wireguard::KeyManager::new( internal_event_tx.clone(), rpc_handle.clone(), @@ -483,6 +482,19 @@ where daemon.ensure_wireguard_keys_for_current_account(); + if let Some(token) = daemon.settings.get_account_token() { + daemon.wireguard_key_manager.set_rotation_interval( + &mut daemon.account_history, + token, + daemon + .settings + .get_tunnel_options() + .wireguard + .automatic_rotation + .map(|hours| 60 * hours), + ); + } + Ok(daemon) } @@ -824,6 +836,9 @@ where SetBridgeState(tx, bridge_state) => self.on_set_bridge_state(tx, bridge_state), SetEnableIpv6(tx, enable_ipv6) => self.on_set_enable_ipv6(tx, enable_ipv6), SetWireguardMtu(tx, mtu) => self.on_set_wireguard_mtu(tx, mtu), + SetWireguardRotationInterval(tx, interval) => { + self.on_set_wireguard_rotation_interval(tx, interval) + } GetSettings(tx) => self.on_get_settings(tx), GenerateWireguardKey(tx) => self.on_generate_wireguard_key(tx), GetWireguardKey(tx) => self.on_get_wireguard_key(tx), @@ -1104,13 +1119,19 @@ where self.event_listener.notify_settings(self.settings.clone()); // Bump account history if a token was set - if let Some(token) = account_token { + if let Some(token) = account_token.clone() { if let Err(e) = self.account_history.bump_history(&token) { log::error!("Failed to bump account history: {}", e); } } self.ensure_wireguard_keys_for_current_account(); + + if let Some(token) = account_token { + // update automatic rotation + self.wireguard_key_manager + .reset_rotation(&mut self.account_history, token); + } } Ok(account_changed) } @@ -1348,6 +1369,33 @@ where } } + fn on_set_wireguard_rotation_interval( + &mut self, + tx: oneshot::Sender<()>, + interval: Option<u32>, + ) { + let save_result = self.settings.set_wireguard_rotation_interval(interval); + match save_result { + Ok(settings_changed) => { + Self::oneshot_send(tx, (), "set_wireguard_rotation_interval response"); + if settings_changed { + let account_token = self.settings.get_account_token(); + + if let Some(token) = account_token { + self.wireguard_key_manager.set_rotation_interval( + &mut self.account_history, + token, + interval.map(|hours| 60 * hours), + ); + } + + self.event_listener.notify_settings(self.settings.clone()); + } + } + Err(e) => error!("{}", e.display_chain_with_msg("Unable to save settings")), + } + } + fn ensure_wireguard_keys_for_current_account(&mut self) { if let Some(account) = self.settings.get_account_token() { if self @@ -1411,6 +1459,18 @@ where })?; let keygen_event = KeygenEvent::NewKey(public_key); self.event_listener.notify_key_event(keygen_event.clone()); + + // update automatic rotation + self.wireguard_key_manager.set_rotation_interval( + &mut self.account_history, + account_token.clone(), + self.settings + .get_tunnel_options() + .wireguard + .automatic_rotation + .map(|hours| 60 * hours), + ); + Ok(keygen_event) } Err(wireguard::Error::TooManyKeys) => Ok(KeygenEvent::TooManyKeys), diff --git a/mullvad-daemon/src/management_interface.rs b/mullvad-daemon/src/management_interface.rs index 4ddeda473c..0e4547b42d 100644 --- a/mullvad-daemon/src/management_interface.rs +++ b/mullvad-daemon/src/management_interface.rs @@ -142,6 +142,10 @@ build_rpc_trait! { #[rpc(meta, name = "set_wireguard_mtu")] fn set_wireguard_mtu(&self, Self::Metadata, Option<u16>) -> BoxFuture<(), Error>; + /// Set automatic key rotation interval for wireguard tunnels + #[rpc(meta, name = "set_wireguard_rotation_interval")] + fn set_wireguard_rotation_interval(&self, Self::Metadata, Option<u32>) -> BoxFuture<(), Error>; + /// Returns the current daemon settings #[rpc(meta, name = "get_settings")] fn get_settings(&self, Self::Metadata) -> BoxFuture<Settings, Error>; @@ -239,6 +243,8 @@ pub enum ManagementCommand { SetEnableIpv6(OneshotSender<()>, bool), /// Set MTU for wireguard tunnels SetWireguardMtu(OneshotSender<()>, Option<u16>), + /// Set automatic key rotation interval for wireguard tunnels + SetWireguardRotationInterval(OneshotSender<()>, Option<u32>), /// Get the daemon settings GetSettings(OneshotSender<Settings>), /// Generate new wireguard key @@ -695,6 +701,22 @@ impl<T: From<ManagementCommand> + 'static + Send> ManagementInterfaceApi Box::new(future) } + /// Set automatic key rotation interval for wireguard tunnels + fn set_wireguard_rotation_interval( + &self, + _: Self::Metadata, + interval: Option<u32>, + ) -> BoxFuture<(), Error> { + log::debug!("set_wireguard_rotation_interval({:?})", interval); + let (tx, rx) = sync::oneshot::channel(); + let future = self + .send_command_to_daemon(ManagementCommand::SetWireguardRotationInterval( + tx, interval, + )) + .and_then(|_| rx.map_err(|_| Error::internal_error())); + Box::new(future) + } + fn get_settings(&self, _: Self::Metadata) -> BoxFuture<Settings, Error> { log::debug!("get_settings"); let (tx, rx) = sync::oneshot::channel(); diff --git a/mullvad-daemon/src/wireguard.rs b/mullvad-daemon/src/wireguard.rs index 0290d7a750..1e1b8f72a5 100644 --- a/mullvad-daemon/src/wireguard.rs +++ b/mullvad-daemon/src/wireguard.rs @@ -1,10 +1,14 @@ -use crate::InternalDaemonEvent; +use crate::{account_history::AccountHistory, InternalDaemonEvent}; use chrono::offset::Utc; -use futures::{future::Executor, sync::oneshot, Async, Future, Poll}; +use futures::{ + future::{Executor, IntoFuture}, + sync::oneshot, + Async, Future, Poll, +}; use jsonrpc_client_core::Error as JsonRpcError; use mullvad_types::account::AccountToken; pub use mullvad_types::wireguard::*; -use std::{sync::mpsc, time::Duration}; +use std::{cmp, sync::mpsc, time::Duration}; pub use talpid_types::net::wireguard::{ ConnectionConfig, PrivateKey, TunnelConfig, TunnelParameters, }; @@ -14,9 +18,17 @@ use tokio_retry::{ strategy::{jitter, ExponentialBackoff}, RetryIf, }; +use tokio_timer; const TOO_MANY_KEYS_ERROR_CODE: i64 = -703; +/// Default automatic key rotation (in minutes) +const DEFAULT_AUTOMATIC_KEY_ROTATION: u32 = 7 * 24 * 60; +/// How long to wait before reattempting to rotate keys on failure (secs) +const AUTOMATIC_ROTATION_RETRY_DELAY: u64 = 5; +/// Minimum interval used by automatic rotation (secs) +const MINIMUM_ROTATION_INTERVAL: u64 = 5; + #[derive(err_derive::Error, Debug)] pub enum Error { @@ -28,6 +40,8 @@ pub enum Error { RpcError(#[error(source)] jsonrpc_client_core::Error), #[error(display = "Account already has maximum number of keys")] TooManyKeys, + #[error(display = "Failed to create rotation timer")] + RotationScheduleError(#[error(source)] tokio_timer::TimerError), } pub type Result<T> = std::result::Result<T, Error>; @@ -37,6 +51,10 @@ pub struct KeyManager { http_handle: mullvad_rpc::HttpHandle, tokio_remote: Remote, current_job: Option<CancelHandle>, + + abort_scheduler_tx: Option<CancelHandle>, + // unit: minutes + auto_rotation_interval: u32, } impl KeyManager { @@ -50,9 +68,51 @@ impl KeyManager { http_handle, tokio_remote, current_job: None, + abort_scheduler_tx: None, + auto_rotation_interval: 0, } } + /// Update automatic key rotation interval (given in minutes) + /// Passing `None` for the interval will use the default value. + /// A value of `0` disables automatic key rotation. + pub fn reset_rotation( + &mut self, + account_history: &mut AccountHistory, + account_token: AccountToken, + ) { + log::debug!("reset_rotation"); + + match account_history + .get(&account_token) + .map(|entry| entry.map(|entry| entry.wireguard.map(|wg| wg.get_public_key()))) + { + Ok(Some(Some(public_key))) => self.run_automatic_rotation(account_token, public_key), + Ok(Some(None)) => { + log::error!("reset_rotation: failed to obtain public key for account entry.") + } + Ok(None) => log::error!("reset_rotation: account entry not found."), + Err(e) => log::error!("reset_rotation: failed to obtain account entry. {}", e), + }; + } + + /// Update automatic key rotation interval (given in minutes) + /// Passing `None` for the interval will use the default value. + /// A value of `0` disables automatic key rotation. + pub fn set_rotation_interval( + &mut self, + account_history: &mut AccountHistory, + account_token: AccountToken, + auto_rotation_interval_mins: Option<u32>, + ) { + log::debug!("set_rotation_interval"); + + self.auto_rotation_interval = + auto_rotation_interval_mins.unwrap_or(DEFAULT_AUTOMATIC_KEY_ROTATION); + + self.reset_rotation(account_history, account_token); + } + /// Stop current key generation pub fn reset(&mut self) { if let Some(job) = self.current_job.take() { @@ -90,8 +150,13 @@ impl KeyManager { ) -> Result<WireguardData> { self.reset(); let new_key = PrivateKey::new_from_random().map_err(Error::GenerationError)?; - self.run_future_sync(self.replace_key_rpc(account, old_key, new_key)) - .map_err(Self::map_rpc_error) + self.run_future_sync(Self::replace_key_rpc( + self.http_handle.clone(), + account, + old_key, + new_key, + )) + .map_err(Self::map_rpc_error) } @@ -193,12 +258,12 @@ impl KeyManager { } fn replace_key_rpc( - &self, + http_handle: mullvad_rpc::HttpHandle, account: AccountToken, old_key: PublicKey, new_key: PrivateKey, ) -> impl Future<Item = WireguardData, Error = JsonRpcError> + Send { - let mut rpc = mullvad_rpc::WireguardKeyProxy::new(self.http_handle.clone()); + let mut rpc = mullvad_rpc::WireguardKeyProxy::new(http_handle.clone()); let new_public_key = new_key.public_key(); rpc.replace_wg_key(account.clone(), old_key.key, new_public_key) .map(move |addresses| WireguardData { @@ -217,6 +282,172 @@ impl KeyManager { _ => Error::RpcError(err), } } + fn create_key_expiration_timer( + public_key: PublicKey, + rotation_interval_secs: u64, + ) -> impl Future<Item = (), Error = Error> + Send { + let last_update = public_key.created.clone(); + let key_age = Duration::from_secs( + (Utc::now().signed_duration_since(last_update)).num_seconds() as u64, + ); + + let interval_duration = Duration::from_secs(rotation_interval_secs); + let remaining_time = interval_duration + .checked_sub(key_age) + .unwrap_or(Duration::from_secs(0)); + let key_expiry = cmp::max( + Duration::from_secs(MINIMUM_ROTATION_INTERVAL), + remaining_time, + ); + + log::info!("Next key rotation (time left): {:?}", key_expiry); + + tokio_timer::wheel() + .max_timeout(Duration::new(std::u64::MAX, 0)) + .build() + .sleep(key_expiry) + .map_err(|e| Error::RotationScheduleError(e)) + } + + fn next_automatic_rotation( + daemon_tx: mpsc::Sender<InternalDaemonEvent>, + http_handle: mullvad_rpc::HttpHandle, + public_key: PublicKey, + rotation_interval_secs: u64, + account_token: AccountToken, + ) -> impl Future<Item = WireguardData, Error = Error> + Send { + let expiration_timer = + Self::create_key_expiration_timer(public_key.clone(), rotation_interval_secs); + + let account_token_copy = account_token.clone(); + + expiration_timer + .and_then(move |_| { + log::info!("Replacing WireGuard key"); + + let private_key = PrivateKey::new_from_random() + .map_err(Error::GenerationError) + .into_future(); + private_key.and_then(move |private_key| { + Self::replace_key_rpc( + http_handle.clone(), + account_token.clone(), + public_key.clone(), + private_key, + ) + .map_err(|err| Error::RpcError(err)) + }) + }) + .map(move |wireguard_data| { + // Update account data + let _ = daemon_tx.send(InternalDaemonEvent::WgKeyEvent(( + account_token_copy, + Ok(wireguard_data.clone()), + ))); + + wireguard_data + }) + } + + fn create_automatic_rotation( + daemon_tx: mpsc::Sender<InternalDaemonEvent>, + http_handle: mullvad_rpc::HttpHandle, + public_key: PublicKey, + rotation_interval_secs: u64, + account_token: AccountToken, + ) -> Box<dyn Future<Item = (), Error = Error> + Send> { + log::debug!("create_automatic_rotation"); + + let fut = Self::next_automatic_rotation( + daemon_tx.clone(), + http_handle.clone(), + public_key.clone(), + rotation_interval_secs, + account_token.clone(), + ); + + let create_repeat_future = move |result: Result<WireguardData>| { + let next_public_key; + let next_interval: u64; + + match result { + Ok(wg_data) => { + next_interval = rotation_interval_secs; + next_public_key = wg_data.get_public_key(); + + Self::create_automatic_rotation( + daemon_tx.clone(), + http_handle.clone(), + next_public_key, + next_interval, + account_token.clone(), + ) + } + Err(e) => { + log::error!( + "Key rotation failed: {}. Retrying in {} seconds", + e, + AUTOMATIC_ROTATION_RETRY_DELAY, + ); + + next_interval = rotation_interval_secs; + next_public_key = public_key.clone(); + + let daemon_tx = daemon_tx.clone(); + let http_handle = http_handle.clone(); + let account_token = account_token.clone(); + + Box::new( + tokio_timer::wheel() + .build() + .sleep(Duration::from_secs(AUTOMATIC_ROTATION_RETRY_DELAY)) + .then(move |_| { + Self::create_automatic_rotation( + daemon_tx, + http_handle, + next_public_key, + next_interval, + account_token, + ) + }), + ) + } + } + }; + + Box::new(fut.then(create_repeat_future).map(|_| ())) + } + + fn run_automatic_rotation(&mut self, account_token: AccountToken, public_key: PublicKey) { + self.stop_automatic_rotation(); + + if let 0 = self.auto_rotation_interval { + // disabled + return; + } + + // Schedule cancellable series of repeating rotation tasks + let fut = Self::create_automatic_rotation( + self.daemon_tx.clone(), + self.http_handle.clone(), + public_key, + 60u64 * (self.auto_rotation_interval as u64), + account_token, + ); + let (fut, cancel_handle) = Cancellable::new(fut); + + if let Err(e) = self.tokio_remote.execute(fut.map_err(|_| ())) { + log::error!("Failed to execute auto key rotation: {:?}", e.kind()); + } + self.abort_scheduler_tx = Some(cancel_handle); + } + + fn stop_automatic_rotation(&mut self) { + if let Some(cancel_handle) = self.abort_scheduler_tx.take() { + log::info!("Stopping automatic key rotation"); + cancel_handle.cancel(); + } + } } pub enum CancelErr<E> { diff --git a/mullvad-ipc-client/src/lib.rs b/mullvad-ipc-client/src/lib.rs index dad3be309d..ed55bc15d7 100644 --- a/mullvad-ipc-client/src/lib.rs +++ b/mullvad-ipc-client/src/lib.rs @@ -199,6 +199,10 @@ impl DaemonRpcClient { self.call("set_wireguard_mtu", &[mtu]) } + pub fn set_wireguard_rotation_interval(&mut self, interval: Option<u32>) -> Result<()> { + self.call("set_wireguard_rotation_interval", &[interval]) + } + pub fn set_openvpn_mssfix(&mut self, mssfix: Option<u16>) -> Result<()> { self.call("set_openvpn_mssfix", &[mssfix]) } diff --git a/mullvad-types/src/settings/mod.rs b/mullvad-types/src/settings/mod.rs index 0c3aa2f6fe..89b0fa2ad7 100644 --- a/mullvad-types/src/settings/mod.rs +++ b/mullvad-types/src/settings/mod.rs @@ -284,6 +284,18 @@ impl Settings { } } + pub fn set_wireguard_rotation_interval( + &mut self, + automatic_rotation: Option<u32>, + ) -> Result<bool> { + if self.tunnel_options.wireguard.automatic_rotation != automatic_rotation { + self.tunnel_options.wireguard.automatic_rotation = automatic_rotation; + self.save().map(|_| true) + } else { + Ok(false) + } + } + pub fn get_tunnel_options(&self) -> &TunnelOptions { &self.tunnel_options } @@ -334,7 +346,10 @@ impl Default for TunnelOptions { fn default() -> Self { TunnelOptions { openvpn: openvpn::TunnelOptions::default(), - wireguard: wireguard::TunnelOptions { mtu: None }, + wireguard: wireguard::TunnelOptions { + mtu: None, + automatic_rotation: None, + }, generic: GenericTunnelOptions { enable_ipv6: false }, } } diff --git a/talpid-types/src/net/wireguard.rs b/talpid-types/src/net/wireguard.rs index 68beef9aa0..805d1884ba 100644 --- a/talpid-types/src/net/wireguard.rs +++ b/talpid-types/src/net/wireguard.rs @@ -51,6 +51,8 @@ pub struct TunnelConfig { pub struct TunnelOptions { /// MTU for the wireguard tunnel pub mtu: Option<u16>, + /// Interval used for automatic key rotation, in hours + pub automatic_rotation: Option<u32>, } /// Wireguard x25519 private key |
