diff options
| author | Linus Färnstrand <linus@mullvad.net> | 2023-01-30 14:25:10 +0100 |
|---|---|---|
| committer | Linus Färnstrand <linus@mullvad.net> | 2023-01-30 14:25:10 +0100 |
| commit | 23d4e88dbddc6c8640da053c32fc67b2adb3d8d9 (patch) | |
| tree | dbafaaa221aedb2aeda04e030596f6a5434b0d39 | |
| parent | 5d52ebc582d42fe51a28e3476b48b97059d2b8d2 (diff) | |
| parent | 9ffc820b579e2f948c5faaf2982ab33ffe00b4ae (diff) | |
| download | mullvadvpn-23d4e88dbddc6c8640da053c32fc67b2adb3d8d9.tar.xz mullvadvpn-23d4e88dbddc6c8640da053c32fc67b2adb3d8d9.zip | |
Merge branch 'post-quantum-add-kyber1024'
| -rw-r--r-- | CHANGELOG.md | 8 | ||||
| -rw-r--r-- | Cargo.lock | 32 | ||||
| -rw-r--r-- | mullvad-cli/src/cmds/tunnel.rs | 2 | ||||
| -rw-r--r-- | talpid-tunnel-config-client/Cargo.toml | 4 | ||||
| -rw-r--r-- | talpid-tunnel-config-client/examples/tuncfg-server.rs | 28 | ||||
| -rw-r--r-- | talpid-tunnel-config-client/proto/tunnel_config.proto | 30 | ||||
| -rw-r--r-- | talpid-tunnel-config-client/src/classic_mceliece.rs | 27 | ||||
| -rw-r--r-- | talpid-tunnel-config-client/src/kyber.rs | 28 | ||||
| -rw-r--r-- | talpid-tunnel-config-client/src/lib.rs | 79 |
9 files changed, 156 insertions, 82 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index ede4526fb9..0c739113eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,14 @@ Line wrap the file at 100 chars. Th ## [Unreleased] +### Added +- Add Kyber1024 KEM algorithm into the Post-Quantum secure key exchange algorithm. This means the + Quantum-resistant-tunnels feature now mixes both Classic McEliece and Kyber for added protection. + +### Changed +- Update the Post-Quantum secure key exchange gRPC client to use the stabilized + `PskExchangeV1` endpoint + ### Fixed #### Android - Fix adaptive app icon which previously had a displaced nose and some other oddities. diff --git a/Cargo.lock b/Cargo.lock index 6bef7c336c..e4a510edc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -290,9 +290,9 @@ checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "cc" -version = "1.0.71" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" +checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" [[package]] name = "cesu8" @@ -457,7 +457,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" dependencies = [ "generic-array 0.14.4", - "rand_core 0.6.3", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -712,7 +712,7 @@ dependencies = [ "crypto-bigint", "der", "generic-array 0.14.4", - "rand_core 0.6.3", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -2302,6 +2302,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" [[package]] +name = "pqc_kyber" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b79004a05337e54e8ffc0ec7470e40fa26eca6fe182968ec2b803247f2283c" +dependencies = [ + "rand_core 0.6.4", + "zeroize", +] + +[[package]] name = "prettyplease" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2471,7 +2481,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -2491,7 +2501,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -2505,9 +2515,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom 0.2.3", ] @@ -2952,7 +2962,7 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02658e48d89f2bec991f9a78e69cfa4c316f8d6a6c4ec12fae1aeb263d486788" dependencies = [ - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -3287,6 +3297,7 @@ version = "0.0.0" dependencies = [ "classic-mceliece-rust", "log", + "pqc_kyber", "prost", "rand 0.8.5", "talpid-types", @@ -3294,6 +3305,7 @@ dependencies = [ "tonic", "tonic-build", "tower", + "zeroize", ] [[package]] @@ -4223,7 +4235,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5da623d8af10a62342bcbbb230e33e58a63255a58012f8653c578e54bab48df" dependencies = [ "curve25519-dalek", - "rand_core 0.6.3", + "rand_core 0.6.4", "zeroize", ] diff --git a/mullvad-cli/src/cmds/tunnel.rs b/mullvad-cli/src/cmds/tunnel.rs index e47ac5afe5..a40aa985b9 100644 --- a/mullvad-cli/src/cmds/tunnel.rs +++ b/mullvad-cli/src/cmds/tunnel.rs @@ -60,7 +60,7 @@ fn create_wireguard_mtu_subcommand() -> clap::App<'static> { fn create_wireguard_quantum_resistant_tunnel_subcommand() -> clap::App<'static> { clap::App::new("quantum-resistant-tunnel") - .about("EXPERIMENTAL: Enables quantum-resistant PSK exchange in the tunnel") + .about("Controls the quantum-resistant PSK exchange in the tunnel") .setting(clap::AppSettings::SubcommandRequiredElseHelp) .subcommand(clap::App::new("get")) .subcommand(clap::App::new("set").arg(clap::Arg::new("policy").required(true))) diff --git a/talpid-tunnel-config-client/Cargo.toml b/talpid-tunnel-config-client/Cargo.toml index c6b2d1d655..62542da683 100644 --- a/talpid-tunnel-config-client/Cargo.toml +++ b/talpid-tunnel-config-client/Cargo.toml @@ -15,7 +15,9 @@ tonic = "0.8" prost = "0.11" tower = "0.4" tokio = "1" -classic-mceliece-rust = { version = "2.0.0", features = ["mceliece460896f"] } +classic-mceliece-rust = { version = "2.0.0", features = ["mceliece460896f", "zeroize"] } +pqc_kyber = { version = "0.4.0", features = ["std", "kyber1024", "zeroize"] } +zeroize = "1.5.7" [dev-dependencies] tokio = { version = "1", features = ["rt-multi-thread"] } diff --git a/talpid-tunnel-config-client/examples/tuncfg-server.rs b/talpid-tunnel-config-client/examples/tuncfg-server.rs index 31a9fcff8b..4d29d764a2 100644 --- a/talpid-tunnel-config-client/examples/tuncfg-server.rs +++ b/talpid-tunnel-config-client/examples/tuncfg-server.rs @@ -1,4 +1,4 @@ -//! A server implementation of the tuncfg PskExchangeExperimentalV1 RPC to test +//! A server implementation of the tuncfg PskExchangeV1 RPC to test //! the client side implementation. #[allow(clippy::derive_partial_eq_without_eq)] @@ -8,8 +8,7 @@ mod proto { use classic_mceliece_rust::{PublicKey, CRYPTO_PUBLICKEYBYTES}; use proto::{ post_quantum_secure_server::{PostQuantumSecure, PostQuantumSecureServer}, - PskRequestExperimentalV0, PskRequestExperimentalV1, PskResponseExperimentalV0, - PskResponseExperimentalV1, + PskRequestV1, PskResponseV1, }; use talpid_types::net::wireguard::PresharedKey; @@ -20,17 +19,10 @@ pub struct PostQuantumSecureImpl {} #[tonic::async_trait] impl PostQuantumSecure for PostQuantumSecureImpl { - async fn psk_exchange_experimental_v0( + async fn psk_exchange_v1( &self, - _request: Request<PskRequestExperimentalV0>, - ) -> Result<Response<PskResponseExperimentalV0>, Status> { - unimplemented!("Use V1 instead"); - } - - async fn psk_exchange_experimental_v1( - &self, - request: Request<PskRequestExperimentalV1>, - ) -> Result<Response<PskResponseExperimentalV1>, Status> { + request: Request<PskRequestV1>, + ) -> Result<Response<PskResponseV1>, Status> { let mut rng = rand::thread_rng(); let request = request.into_inner(); @@ -45,7 +37,7 @@ impl PostQuantumSecure for PostQuantumSecureImpl { for kem_pubkey in request.kem_pubkeys { println!("\tKEM algorithm: {}", kem_pubkey.algorithm_name); let (ciphertext, shared_secret) = match kem_pubkey.algorithm_name.as_str() { - "Classic-McEliece-460896f" => { + "Classic-McEliece-460896f-round3" => { let key_data: [u8; CRYPTO_PUBLICKEYBYTES] = kem_pubkey.key_data.as_slice().try_into().unwrap(); let public_key = PublicKey::from(&key_data); @@ -53,6 +45,12 @@ impl PostQuantumSecure for PostQuantumSecureImpl { classic_mceliece_rust::encapsulate_boxed(&public_key, &mut rng); (ciphertext.as_array().to_vec(), *shared_secret.as_array()) } + "Kyber1024" => { + let public_key = kem_pubkey.key_data.as_slice(); + let (ciphertext, shared_secret) = + pqc_kyber::encapsulate(public_key, &mut rng).unwrap(); + (ciphertext.to_vec(), shared_secret) + } name => panic!("Unsupported KEM algorithm: {name}"), }; @@ -66,7 +64,7 @@ impl PostQuantumSecure for PostQuantumSecureImpl { let psk = PresharedKey::from(psk_data); println!("psk: {psk:?}"); println!("=============================================="); - Ok(Response::new(PskResponseExperimentalV1 { ciphertexts })) + Ok(Response::new(PskResponseV1 { ciphertexts })) } } diff --git a/talpid-tunnel-config-client/proto/tunnel_config.proto b/talpid-tunnel-config-client/proto/tunnel_config.proto index 215aa941c8..af7f7f158e 100644 --- a/talpid-tunnel-config-client/proto/tunnel_config.proto +++ b/talpid-tunnel-config-client/proto/tunnel_config.proto @@ -5,11 +5,6 @@ option go_package = "github.com/mullvad/wg-manager/server/tuncfg"; package tunnel_config; service PostQuantumSecure { - // PskExchangeExperimentalV0 uses the common API defined by LibOQS. See: - // https://github.com/open-quantum-safe/liboqs - // This endpoint is deprecated in favor for `PskExchangeExperimentalV1`. Please use that instead. - rpc PskExchangeExperimentalV0(PskRequestExperimentalV0) returns (PskResponseExperimentalV0) {} - // Allows deriving a preshared key (PSK) using one or multiple PQ-secure key-encapsulation // mechanisms (KEM). The preshared key is added to WireGuard's preshared-key field in a new // ephemeral peer (PQ-peer). This makes the tunnel resistant towards attacks using @@ -71,35 +66,20 @@ service PostQuantumSecure { // Mixing with XOR (A = B ^ C) is fine since nothing about A is revealed even if one of B or C // is known. Both B *and* C must be known to compute any bit in A. This means all involved // KEM algorithms must be broken before the PSK can be computed by an attacker. - rpc PskExchangeExperimentalV1(PskRequestExperimentalV1) returns (PskResponseExperimentalV1) {} -} - -message PskRequestExperimentalV0 { - bytes wg_pubkey = 1; - bytes wg_psk_pubkey = 2; - KemPubkeyExperimentalV0 kem_pubkey = 3; -} - -message KemPubkeyExperimentalV0 { - string algorithm_name = 1; - bytes key_data = 2; -} - -message PskResponseExperimentalV0 { - bytes ciphertext = 1; + rpc PskExchangeV1(PskRequestV1) returns (PskResponseV1) {} } -message PskRequestExperimentalV1 { +message PskRequestV1 { bytes wg_pubkey = 1; bytes wg_psk_pubkey = 2; - repeated KemPubkeyExperimentalV1 kem_pubkeys = 3; + repeated KemPubkeyV1 kem_pubkeys = 3; } -message KemPubkeyExperimentalV1 { +message KemPubkeyV1 { string algorithm_name = 1; bytes key_data = 2; } -message PskResponseExperimentalV1 { +message PskResponseV1 { repeated bytes ciphertexts = 1; } diff --git a/talpid-tunnel-config-client/src/classic_mceliece.rs b/talpid-tunnel-config-client/src/classic_mceliece.rs index dbba95f067..780566958d 100644 --- a/talpid-tunnel-config-client/src/classic_mceliece.rs +++ b/talpid-tunnel-config-client/src/classic_mceliece.rs @@ -1,4 +1,6 @@ -use classic_mceliece_rust::{keypair_boxed, SharedSecret}; +use classic_mceliece_rust::{ + keypair_boxed, Ciphertext, PublicKey, SecretKey, SharedSecret, CRYPTO_CIPHERTEXTBYTES, +}; /// The `keypair_boxed` function needs just under 1 MiB of stack in debug /// builds. Even though it probably works to run it directly on the main @@ -6,7 +8,9 @@ use classic_mceliece_rust::{keypair_boxed, SharedSecret}; /// keys on a separate thread with a large enough stack. const STACK_SIZE: usize = 2 * 1024 * 1024; -pub use classic_mceliece_rust::{Ciphertext, PublicKey, SecretKey, CRYPTO_CIPHERTEXTBYTES}; +/// Use the smallest CME variant with NIST security level 3. This variant has significantly smaller +/// keys than the larger variants, and is considered safe. +pub const ALGORITHM_NAME: &str = "Classic-McEliece-460896f-round3"; pub async fn generate_keys() -> (PublicKey<'static>, SecretKey<'static>) { let (tx, rx) = tokio::sync::oneshot::channel(); @@ -22,6 +26,21 @@ pub async fn generate_keys() -> (PublicKey<'static>, SecretKey<'static>) { rx.await.unwrap() } -pub fn decapsulate(secret: &SecretKey, ciphertext: &Ciphertext) -> SharedSecret<'static> { - classic_mceliece_rust::decapsulate_boxed(ciphertext, secret) +pub fn decapsulate( + secret: &SecretKey, + ciphertext_slice: &[u8], +) -> Result<SharedSecret<'static>, super::Error> { + let ciphertext_array = + <[u8; CRYPTO_CIPHERTEXTBYTES]>::try_from(ciphertext_slice).map_err(|_| { + super::Error::InvalidCiphertextLength { + algorithm: ALGORITHM_NAME, + actual: ciphertext_slice.len(), + expected: CRYPTO_CIPHERTEXTBYTES, + } + })?; + let ciphertext = Ciphertext::from(ciphertext_array); + Ok(classic_mceliece_rust::decapsulate_boxed( + &ciphertext, + secret, + )) } diff --git a/talpid-tunnel-config-client/src/kyber.rs b/talpid-tunnel-config-client/src/kyber.rs new file mode 100644 index 0000000000..0946e7f5c2 --- /dev/null +++ b/talpid-tunnel-config-client/src/kyber.rs @@ -0,0 +1,28 @@ +use pqc_kyber::{SecretKey, KYBER_CIPHERTEXTBYTES}; + +pub use pqc_kyber::{keypair, KyberError}; + +/// Use the strongest variant of Kyber. It is fast and the keys are small, so there is no practical +/// benefit of going with anything lower. +pub const ALGORITHM_NAME: &str = "Kyber1024"; + +// Always inline in order to try to avoid potential copies of `shared_secret` to multiple places on the stack. +#[inline(always)] +pub fn decapsulate( + secret_key: SecretKey, + ciphertext_slice: &[u8], +) -> Result<[u8; 32], super::Error> { + // The `pqc_kyber` library takes a byte slice. But we convert it into an array + // in order to catch the length mismatch error and report it better than `pqc_kyber` would. + let ciphertext_array = + <[u8; KYBER_CIPHERTEXTBYTES]>::try_from(ciphertext_slice).map_err(|_| { + super::Error::InvalidCiphertextLength { + algorithm: ALGORITHM_NAME, + actual: ciphertext_slice.len(), + expected: KYBER_CIPHERTEXTBYTES, + } + })?; + let shared_secret = pqc_kyber::decapsulate(ciphertext_array.as_slice(), secret_key.as_slice()) + .map_err(super::Error::FailedDecapsulateKyber)?; + Ok(shared_secret) +} diff --git a/talpid-tunnel-config-client/src/lib.rs b/talpid-tunnel-config-client/src/lib.rs index 4cb7cd2c91..a4beee9be3 100644 --- a/talpid-tunnel-config-client/src/lib.rs +++ b/talpid-tunnel-config-client/src/lib.rs @@ -1,8 +1,10 @@ use std::{fmt, net::IpAddr}; use talpid_types::net::wireguard::{PresharedKey, PrivateKey, PublicKey}; use tonic::transport::Channel; +use zeroize::Zeroize; mod classic_mceliece; +mod kyber; #[allow(clippy::derive_partial_eq_without_eq)] mod proto { @@ -13,8 +15,15 @@ mod proto { pub enum Error { GrpcConnectError(tonic::transport::Error), GrpcError(tonic::Status), - InvalidCiphertextLength { actual: usize, expected: usize }, - InvalidCiphertextCount { actual: usize }, + InvalidCiphertextLength { + algorithm: &'static str, + actual: usize, + expected: usize, + }, + InvalidCiphertextCount { + actual: usize, + }, + FailedDecapsulateKyber(kyber::KyberError), } impl std::fmt::Display for Error { @@ -23,13 +32,18 @@ impl std::fmt::Display for Error { match self { GrpcConnectError(_) => "Failed to connect to config service".fmt(f), GrpcError(status) => write!(f, "RPC failed: {status}"), - InvalidCiphertextLength { actual, expected } => write!( + InvalidCiphertextLength { + algorithm, + actual, + expected, + } => write!( f, - "Expected a ciphertext of length {expected}, got {actual} bytes" + "Expected a {expected} bytes ciphertext for {algorithm}, got {actual} bytes" ), InvalidCiphertextCount { actual } => { - write!(f, "Expected 1 ciphertext in the response, got {actual}") + write!(f, "Expected 2 ciphertext in the response, got {actual}") } + FailedDecapsulateKyber(_) => "Failed to decapsulate Kyber1024 ciphertext".fmt(f), } } } @@ -38,6 +52,7 @@ impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Self::GrpcConnectError(error) => Some(error), + Self::FailedDecapsulateKyber(error) => Some(error), _ => None, } } @@ -48,10 +63,6 @@ type RelayConfigService = proto::post_quantum_secure_client::PostQuantumSecureCl /// Port used by the tunnel config service. pub const CONFIG_SERVICE_PORT: u16 = 1337; -/// Use the smallest CME variant with NIST security level 3. This variant has significantly smaller -/// keys than the larger variants, and is considered safe. -const CLASSIC_MCELIECE_VARIANT: &str = "Classic-McEliece-460896f"; - /// Generates a new WireGuard key pair and negotiates a PSK with the relay in a PQ-safe /// manner. This creates a peer on the relay with the new WireGuard pubkey and PSK, /// which can then be used to establish a PQ-safe tunnel to the relay. @@ -62,42 +73,58 @@ pub async fn push_pq_key( ) -> Result<(PrivateKey, PresharedKey), Error> { let wg_psk_privkey = PrivateKey::new_from_random(); let (cme_kem_pubkey, cme_kem_secret) = classic_mceliece::generate_keys().await; + let kyber_keypair = kyber::keypair(&mut rand::thread_rng()); let mut client = new_client(service_address).await?; let response = client - .psk_exchange_experimental_v1(proto::PskRequestExperimentalV1 { + .psk_exchange_v1(proto::PskRequestV1 { wg_pubkey: wg_pubkey.as_bytes().to_vec(), wg_psk_pubkey: wg_psk_privkey.public_key().as_bytes().to_vec(), - kem_pubkeys: vec![proto::KemPubkeyExperimentalV1 { - algorithm_name: CLASSIC_MCELIECE_VARIANT.to_owned(), - key_data: cme_kem_pubkey.as_array().to_vec(), - }], + kem_pubkeys: vec![ + proto::KemPubkeyV1 { + algorithm_name: classic_mceliece::ALGORITHM_NAME.to_owned(), + key_data: cme_kem_pubkey.as_array().to_vec(), + }, + proto::KemPubkeyV1 { + algorithm_name: kyber::ALGORITHM_NAME.to_owned(), + key_data: kyber_keypair.public.to_vec(), + }, + ], }) .await .map_err(Error::GrpcError)?; let ciphertexts = response.into_inner().ciphertexts; + // Unpack the ciphertexts into one per KEM without needing to access them by index. - let [cme_ciphertext] = <&[Vec<u8>; 1]>::try_from(ciphertexts.as_slice()).map_err(|_| { - Error::InvalidCiphertextCount { + let [cme_ciphertext, kyber_ciphertext] = <&[Vec<u8>; 2]>::try_from(ciphertexts.as_slice()) + .map_err(|_| Error::InvalidCiphertextCount { actual: ciphertexts.len(), - } - })?; + })?; // Store the PSK data on the heap. So it can be passed around and then zeroized on drop without // being stored in a bunch of places on the stack. let mut psk_data = Box::new([0u8; 32]); + // Decapsulate Classic McEliece and mix into PSK { - let ciphertext_array = - <[u8; classic_mceliece::CRYPTO_CIPHERTEXTBYTES]>::try_from(cme_ciphertext.as_slice()) - .map_err(|_| Error::InvalidCiphertextLength { - actual: cme_ciphertext.len(), - expected: classic_mceliece::CRYPTO_CIPHERTEXTBYTES, - })?; - let ciphertext = classic_mceliece::Ciphertext::from(ciphertext_array); - let shared_secret = classic_mceliece::decapsulate(&cme_kem_secret, &ciphertext); + let mut shared_secret = classic_mceliece::decapsulate(&cme_kem_secret, cme_ciphertext)?; xor_assign(&mut psk_data, shared_secret.as_array()); + + // This should happen automatically due to `SharedSecret` implementing ZeroizeOnDrop. But doing it explicitly + // provides a stronger guarantee that it's not accidentally removed. + shared_secret.zeroize(); + } + // Decapsulate Kyber and mix into PSK + { + let mut shared_secret = kyber::decapsulate(kyber_keypair.secret, kyber_ciphertext)?; + xor_assign(&mut psk_data, &shared_secret); + + // The shared secret is sadly stored in an array on the stack. So we can't get any + // guarantees that it's not copied around on the stack. The best we can do here + // is to zero out the version we have and hope the compiler optimizes out copies. + // https://github.com/Argyle-Software/kyber/issues/59 + shared_secret.zeroize(); } Ok((wg_psk_privkey, PresharedKey::from(psk_data))) |
