Skip to content

Commit

Permalink
Merge branch 'upgrade-kyber-ml-kem-for-quantum-resistance-des-1267'
Browse files Browse the repository at this point in the history
  • Loading branch information
faern committed Oct 15, 2024
2 parents a21b702 + 6e64e75 commit d89d329
Show file tree
Hide file tree
Showing 15 changed files with 163 additions and 94 deletions.
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/[email protected]
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

0 comments on commit d89d329

Please sign in to comment.