Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace Kyber with ML-KEM (FIPS 203) for quantum-resistant tunnel secrets #6915

Merged
merged 6 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .github/workflows/cargo-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ jobs:
denyWarnings: true
# Ignored audit issues. This list should be kept short, and effort should be
# put into removing items from the list.
# RUSTSEC-2023-0079 - KyberSlash in `pqc_kyber`.
ignore: RUSTSEC-2023-0079

- uses: actions-rust-lang/audit@v1.1.11
name: Audit testrunner Rust Dependencies
Expand All @@ -38,5 +36,3 @@ jobs:
denyWarnings: true
# Ignored audit issues. This list should be kept short, and effort should be
# put into removing items from the list.
# RUSTSEC-2023-0079 - KyberSlash in `pqc_kyber`.
ignore: RUSTSEC-2023-0079
2 changes: 1 addition & 1 deletion .github/workflows/rust-unused-dependencies.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
workflow_dispatch:
env:
# Pinning nightly just to avoid random breakage. It's fine to bump this at any time
RUST_NIGHTLY_TOOLCHAIN: nightly-2024-06-06
RUST_NIGHTLY_TOOLCHAIN: nightly-2024-10-02

permissions: {}

Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ Line wrap the file at 100 chars. Th
- Add experimental support for Windows ARM64.

### Changed
- Replace the draft key encapsulation mechanism Kyber (round 3) with the standardized
ML-KEM (FIPS 203) dito in the handshake for Quantum-resistant tunnels.

#### Windows
- Enable quantum-resistant tunnels by default (when set to `auto`).

Expand Down
44 changes: 33 additions & 11 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions android/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ Line wrap the file at 100 chars. Th

### Changed
- Update colors in the app to be more in line with material design.
- Replace the draft key encapsulation mechanism Kyber (round 3) with the standardized
ML-KEM (FIPS 203) dito in the handshake for Quantum-resistant tunnels.

### Fixed
- Fix VPN service being recreated multiple times when toggling certain options.
Expand Down
2 changes: 0 additions & 2 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ yanked = "deny"
ignore = [
# Ignored audit issues. This list should be kept short, and effort should be
# put into removing items from the list.
# RUSTSEC-2023-0079 - KyberSlash in `pqc_kyber`.
"RUSTSEC-2023-0079",
]


Expand Down
3 changes: 3 additions & 0 deletions ios/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ Line wrap the file at 100 chars. Th
* **Security**: in case of vulnerabilities.

## Unreleased
### Changed
- Replace the draft key encapsulation mechanism Kyber (round 3) with the standardized
ML-KEM (FIPS 203) dito in the handshake for Quantum-resistant tunnels.

## [2024.8 - 2024-10-14]
### Added
Expand Down
10 changes: 0 additions & 10 deletions osv-scanner.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,3 @@
# `renderer-helper` currently depend on this version of libbaz, preventing us from upgrading to a fixed version.
# """
# ```

# KyberSlash timing attack against Kyber PQ KEM
[[IgnoredVulns]]
id = "RUSTSEC-2023-0079"
ignoreUntil = 2024-12-05 # Ignored for six months at a time. This class of timing based attacks are not exploitable in our protocol design
reason = """
KyberSlash is not exploitable in our usage of it:
https://mullvad.net/en/blog/mullvads-usage-of-kyber-is-not-affected-by-kyberslash
And no patched version is available.
"""
3 changes: 2 additions & 1 deletion talpid-tunnel-config-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ classic-mceliece-rust = { version = "2.0.0", features = [
"mceliece460896f",
"zeroize",
] }
pqc_kyber = { version = "0.4.0", features = ["std", "kyber1024", "zeroize"] }

ml-kem = { version = "0.2.1", features = ["zeroize"] }
zeroize = "1.5.7"

[target.'cfg(unix)'.dependencies]
Expand Down
51 changes: 37 additions & 14 deletions talpid-tunnel-config-client/examples/tuncfg-server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
mod proto {
tonic::include_proto!("ephemeralpeer");
}
use classic_mceliece_rust::{PublicKey, CRYPTO_PUBLICKEYBYTES};
use proto::{
ephemeral_peer_server::{EphemeralPeer, EphemeralPeerServer},
EphemeralPeerRequestV1, EphemeralPeerResponseV1, PostQuantumResponseV1,
};
use rand::{CryptoRng, RngCore};
use talpid_types::net::wireguard::PresharedKey;

use tonic::{transport::Server, Request, Response, Status};
Expand Down Expand Up @@ -44,20 +44,9 @@ impl EphemeralPeer for EphemeralPeerImpl {
println!("\tKEM algorithm: {}", kem_pubkey.algorithm_name);
let (ciphertext, shared_secret) = match kem_pubkey.algorithm_name.as_str() {
"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);
let (ciphertext, shared_secret) =
classic_mceliece_rust::encapsulate_boxed(&public_key, &mut rng);
(ciphertext.as_array().to_vec(), *shared_secret.as_array())
}
// Kyber round3
"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)
encapsulate_classic_mceliece(kem_pubkey.key_data.as_slice(), &mut rng)
}
"ML-KEM-1024" => encapsulate_ml_kem(kem_pubkey.key_data.as_slice(), &mut rng),
name => panic!("Unsupported KEM algorithm: {name}"),
};

Expand All @@ -82,6 +71,40 @@ impl EphemeralPeer for EphemeralPeerImpl {
}
}

/// Generate a random shared secret and encapsulate it with the given
/// public key/encapsulation key. Returns the ciphertext to return
/// to the owner of the public key, along with the shared secret.
fn encapsulate_classic_mceliece<R: RngCore + CryptoRng>(
public_key: &[u8],
rng: &mut R,
) -> (Vec<u8>, [u8; 32]) {
use classic_mceliece_rust::{PublicKey, CRYPTO_PUBLICKEYBYTES};

let public_key_array = <[u8; CRYPTO_PUBLICKEYBYTES]>::try_from(public_key).unwrap();
let public_key = PublicKey::from(&public_key_array);
let (ciphertext, shared_secret) = classic_mceliece_rust::encapsulate_boxed(&public_key, rng);
(ciphertext.as_array().to_vec(), *shared_secret.as_array())
}

/// Generate a random shared secret and encapsulate it with the given
/// public key/encapsulation key. Returns the ciphertext to return
/// to the owner of the public key, along with the shared secret.
fn encapsulate_ml_kem<R: RngCore + CryptoRng>(
public_key: &[u8],
rng: &mut R,
) -> (Vec<u8>, [u8; 32]) {
use ml_kem::{kem::Encapsulate, Encoded, EncodedSizeUser, KemCore, MlKem1024};

type EncapsulationKey = <MlKem1024 as KemCore>::EncapsulationKey;

let encapsulation_key_array = <Encoded<EncapsulationKey>>::try_from(public_key).unwrap();
let encapsulation_key = EncapsulationKey::from_bytes(&encapsulation_key_array);

let (ciphertext, shared_secret) = encapsulation_key.encapsulate(rng).unwrap();

(ciphertext.to_vec(), shared_secret.into())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "127.0.0.1:1337".parse()?;
Expand Down
8 changes: 4 additions & 4 deletions talpid-tunnel-config-client/proto/ephemeralpeer.proto
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@ message EphemeralPeerRequestV1 {
DaitaRequestV1 daita = 4;
}

// The v1 request supports exactly two algorithms.
// The algorithms can appear soletary or in mixed order:
// The v1 request supports these three algorithms.
// The algorithms can appear soletary or mixed. Kyber1024 and ML-KEM-1024 cannot be used in the
// same request as they are just different versions of the same kem.
// - "Classic-McEliece-460896f", but explicitly identified as "Classic-McEliece-460896f-round3"
// - "Kyber1024", this is round3 of the Kyber KEM
// - "ML-KEM-1024". This is the standardized version of ML-KEM (FIPS 203) at the highest strength
message PostQuantumRequestV1 { repeated KemPubkeyV1 kem_pubkeys = 1; }

message KemPubkeyV1 {
Expand All @@ -70,8 +72,6 @@ message EphemeralPeerResponseV1 {
// Since the PSK provided to WireGuard is directly fed into a HKDF, it is not important that
// the entropy in the PSK is uniformly distributed. The actual keys used for encrypting the
// data channel will have uniformly distributed entropy anyway, thanks to the HKDF.
// But even if that was not true, since both CME and Kyber run SHAKE256 as the last step
// of their internal key derivation, the output they produce are uniformly distributed.
//
// If we later want to support another type of KEM that produce longer or shorter output,
// we can hash that secret into a 32 byte hash before proceeding to the XOR step.
Expand Down
31 changes: 0 additions & 31 deletions talpid-tunnel-config-client/src/kyber.rs

This file was deleted.

23 changes: 10 additions & 13 deletions talpid-tunnel-config-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use tower::service_fn;
use zeroize::Zeroize;

mod classic_mceliece;
mod kyber;
mod ml_kem;
#[cfg(not(target_os = "ios"))]
mod socket;

Expand All @@ -35,7 +35,6 @@ pub enum Error {
InvalidCiphertextCount {
actual: usize,
},
FailedDecapsulateKyber(kyber::KyberError),
#[cfg(target_os = "ios")]
TcpConnectionExpired,
#[cfg(target_os = "ios")]
Expand All @@ -60,7 +59,6 @@ impl std::fmt::Display for Error {
InvalidCiphertextCount { actual } => {
write!(f, "Expected 2 ciphertext in the response, got {actual}")
}
FailedDecapsulateKyber(_) => "Failed to decapsulate Kyber1024 ciphertext".fmt(f),
#[cfg(target_os = "ios")]
TcpConnectionExpired => "TCP connection is already shut down".fmt(f),
#[cfg(target_os = "ios")]
Expand All @@ -73,7 +71,6 @@ 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,
}
}
Expand Down Expand Up @@ -110,15 +107,15 @@ pub async fn request_ephemeral_peer_with(
.await
.map_err(Error::GrpcError)?;

let psk = if let Some((cme_kem_secret, kyber_secret)) = kem_secrets {
let psk = if let Some((cme_kem_secret, ml_kem_secret)) = kem_secrets {
let ciphertexts = response
.into_inner()
.post_quantum
.ok_or(Error::MissingCiphertexts)?
.ciphertexts;

// Unpack the ciphertexts into one per KEM without needing to access them by index.
let [cme_ciphertext, kyber_ciphertext] = <&[Vec<u8>; 2]>::try_from(ciphertexts.as_slice())
let [cme_ciphertext, ml_kem_ciphertext] = <&[Vec<u8>; 2]>::try_from(ciphertexts.as_slice())
.map_err(|_| Error::InvalidCiphertextCount {
actual: ciphertexts.len(),
})?;
Expand All @@ -137,9 +134,9 @@ pub async fn request_ephemeral_peer_with(
// accidentally removed.
shared_secret.zeroize();
}
// Decapsulate Kyber and mix into PSK
// Decapsulate ML-KEM and mix into PSK
{
let mut shared_secret = kyber::decapsulate(kyber_secret, kyber_ciphertext)?;
let mut shared_secret = ml_kem_secret.decapsulate(ml_kem_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
Expand Down Expand Up @@ -182,11 +179,11 @@ async fn post_quantum_secrets(
enable_post_quantum: bool,
) -> (
Option<PostQuantumRequestV1>,
Option<(classic_mceliece_rust::SecretKey<'static>, [u8; 3168])>,
Option<(classic_mceliece_rust::SecretKey<'static>, ml_kem::Keypair)>,
) {
if enable_post_quantum {
let (cme_kem_pubkey, cme_kem_secret) = classic_mceliece::generate_keys().await;
let kyber_keypair = kyber::keypair(&mut rand::thread_rng());
let ml_kem_keypair = ml_kem::keypair();

(
Some(proto::PostQuantumRequestV1 {
Expand All @@ -196,12 +193,12 @@ async fn post_quantum_secrets(
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(),
algorithm_name: ml_kem::ALGORITHM_NAME.to_owned(),
key_data: ml_kem_keypair.encapsulation_key(),
},
],
}),
Some((cme_kem_secret, kyber_keypair.secret)),
Some((cme_kem_secret, ml_kem_keypair)),
)
} else {
(None, None)
Expand Down
Loading
Loading