Lp/encrypted kkt (#6331)

* enable encryption - kkt

* integrate encrypted kkt into nym-lp

* chore: remove unused imports

* chore: remove magic constants from KKTContext

* fixed KKT exchange

* use more strict typing for KKTFrame fields

* removed recursive error conversion

* removed needless borrow

* restored kkt tests

* fixed KKT benchmarks compilation

---------

Co-authored-by: Georgio Nicolas <me@georgio.xyz>
This commit is contained in:
Jędrzej Stuczyński
2026-01-16 10:11:49 +00:00
committed by GitHub
parent 3b75af34e8
commit 7d8d1e9d6d
15 changed files with 1345 additions and 800 deletions
Generated
+6 -3
View File
@@ -6656,18 +6656,21 @@ dependencies = [
name = "nym-kkt"
version = "0.1.0"
dependencies = [
"anyhow",
"blake3",
"classic-mceliece-rust",
"criterion",
"libcrux-chacha20poly1305",
"libcrux-ecdh",
"libcrux-kem",
"libcrux-ml-kem",
"libcrux-psq",
"libcrux-sha3",
"libcrux-traits",
"num_enum",
"nym-crypto",
"nym-sphinx",
"rand 0.9.2",
"rand_chacha 0.9.0",
"thiserror 2.0.12",
"zeroize",
]
[[package]]
+9 -3
View File
@@ -8,24 +8,30 @@ license.workspace = true
[dependencies]
blake3 = { workspace = true }
thiserror = { workspace = true }
num_enum = { workspace = true }
# internal
nym-crypto = { path = "../crypto", features = ["asymmetric", "serde"] }
nym-sphinx = { path = "../nymsphinx" }
libcrux-traits = { git = "https://github.com/cryspen/libcrux" }
libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
libcrux-psq = { git = "https://github.com/cryspen/libcrux", features = ["test-utils"] }
libcrux-sha3 = { git = "https://github.com/cryspen/libcrux" }
libcrux-ml-kem = { git = "https://github.com/cryspen/libcrux" }
libcrux-ecdh = { git = "https://github.com/cryspen/libcrux", features = ["codec"] }
libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux" }
#rand = "0.9.2"
rand = "0.9.2"
zeroize = { workspace = true, features = ["zeroize_derive"] }
classic-mceliece-rust = { git = "https://github.com/georgio/classic-mceliece-rust", features = ["mceliece460896f", "zeroize"] }
[dev-dependencies]
rand_chacha = "0.9.0"
anyhow = { workspace = true }
criterion = { workspace = true }
[[bench]]
name = "benches"
harness = false
+40 -82
View File
@@ -48,6 +48,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
let mut secret_responder: [u8; 32] = [0u8; 32];
rng.fill_bytes(&mut secret_responder);
let responder_ed25519_keypair = ed25519::KeyPair::from_secret(secret_responder, 1);
for kem in [KEM::MlKem768, KEM::XWing, KEM::X25519, KEM::McEliece] {
for hash_function in [
@@ -104,10 +105,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
// Anonymous Initiator, OneWay
{
c.bench_function(
&format!(
"{}, {} | Anonymous Initiator: Generate Request",
kem, hash_function
),
&format!("{kem}, {hash_function} | Anonymous Initiator: Generate Request",),
|b| {
b.iter(|| anonymous_initiator_process(&mut rng, ciphersuite).unwrap());
},
@@ -118,8 +116,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
c.bench_function(
&format!(
"{}, {} | Anonymous Initiator: Encode Frame - Request",
kem, hash_function
"{kem}, {hash_function} | Anonymous Initiator: Encode Frame - Request",
),
|b| b.iter(|| i_frame.to_bytes()),
);
@@ -128,8 +125,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
c.bench_function(
&format!(
"{}, {} | Anonymous Initiator: Decode Frame - Request",
kem, hash_function
"{kem}, {hash_function} | Anonymous Initiator: Decode Frame - Request",
),
|b| b.iter(|| KKTFrame::from_bytes(&i_frame_bytes).unwrap()),
);
@@ -138,8 +134,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
c.bench_function(
&format!(
"{}, {} | Anonymous Initiator: Responder Ingest Frame",
kem, hash_function
"{kem}, {hash_function} | Anonymous Initiator: Responder Ingest Frame",
),
|b| {
b.iter(|| {
@@ -153,14 +148,13 @@ pub fn kkt_benchmark(c: &mut Criterion) {
c.bench_function(
&format!(
"{}, {} | Anonymous Initiator: Responder Generate Response",
kem, hash_function
"{kem}, {hash_function} | Anonymous Initiator: Responder Generate Response",
),
|b| {
b.iter(|| {
responder_process(
&mut r_context,
i_frame_r.session_id_ref(),
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
@@ -170,7 +164,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
);
let r_frame = responder_process(
&mut r_context,
i_frame_r.session_id_ref(),
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
@@ -178,26 +172,23 @@ pub fn kkt_benchmark(c: &mut Criterion) {
c.bench_function(
&format!(
"{}, {} | Anonymous Initiator: Responder Encode Frame",
kem, hash_function
"{kem}, {hash_function} | Anonymous Initiator: Responder Encode Frame",
),
|b| b.iter(|| r_frame.to_bytes()),
);
let r_bytes = r_frame.to_bytes();
c.bench_function(
&format!(
"{}, {} | Anonymous Initiator: Initiator Ingest Response",
kem, hash_function
"{kem}, {hash_function} | Anonymous Initiator: Initiator Ingest Response",
),
|b| {
b.iter(|| {
initiator_ingest_response(
&mut i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
&r_dir_hash,
&r_bytes,
)
.unwrap()
});
@@ -206,9 +197,10 @@ pub fn kkt_benchmark(c: &mut Criterion) {
let obtained_key = initiator_ingest_response(
&mut i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
&r_dir_hash,
&r_bytes,
)
.unwrap();
@@ -226,10 +218,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
.unwrap();
c.bench_function(
&format!(
"{}, {} | Initiator OneWay: Generate Request",
kem, hash_function
),
&format!("{kem}, {hash_function} | Initiator OneWay: Generate Request",),
|b| {
b.iter(|| {
initiator_process(
@@ -245,30 +234,21 @@ pub fn kkt_benchmark(c: &mut Criterion) {
);
c.bench_function(
&format!(
"{}, {} | Initiator OneWay: Encode Frame - Request",
kem, hash_function
),
&format!("{kem}, {hash_function} | Initiator OneWay: Encode Frame - Request",),
|b| b.iter(|| i_frame.to_bytes()),
);
let i_frame_bytes = i_frame.to_bytes();
c.bench_function(
&format!(
"{}, {} | Initiator OneWay: Decode Frame - Request",
kem, hash_function
),
&format!("{kem}, {hash_function} | Initiator OneWay: Decode Frame - Request",),
|b| b.iter(|| KKTFrame::from_bytes(&i_frame_bytes).unwrap()),
);
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
c.bench_function(
&format!(
"{}, {} | Initiator OneWay: Responder Ingest Frame",
kem, hash_function
),
&format!("{kem}, {hash_function} | Initiator OneWay: Responder Ingest Frame",),
|b| {
b.iter(|| {
responder_ingest_message(
@@ -294,14 +274,13 @@ pub fn kkt_benchmark(c: &mut Criterion) {
c.bench_function(
&format!(
"{}, {} | Initiator OneWay: Responder Generate Response",
kem, hash_function
"{kem}, {hash_function} | Initiator OneWay: Responder Generate Response",
),
|b| {
b.iter(|| {
responder_process(
&mut r_context,
i_frame_r.session_id_ref(),
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
@@ -312,36 +291,31 @@ pub fn kkt_benchmark(c: &mut Criterion) {
let r_frame = responder_process(
&mut r_context,
i_frame_r.session_id_ref(),
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap();
c.bench_function(
&format!(
"{}, {} | Initiator OneWay: Responder Encode Frame",
kem, hash_function
),
&format!("{kem}, {hash_function} | Initiator OneWay: Responder Encode Frame",),
|b| {
b.iter(|| r_frame.to_bytes());
},
);
let r_bytes = r_frame.to_bytes();
c.bench_function(
&format!(
"{}, {} | Initiator OneWay: Initiator Ingest Response",
kem, hash_function
"{kem}, {hash_function} | Initiator OneWay: Initiator Ingest Response",
),
|b| {
b.iter(|| {
initiator_ingest_response(
&mut i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
&r_dir_hash,
&r_bytes,
)
.unwrap()
});
@@ -350,9 +324,10 @@ pub fn kkt_benchmark(c: &mut Criterion) {
let i_obtained_key = initiator_ingest_response(
&mut i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
&r_dir_hash,
&r_bytes,
)
.unwrap();
@@ -362,10 +337,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
// Initiator, Mutual
{
c.bench_function(
&format!(
"{}, {} | Initiator Mutual: Generate Request",
kem, hash_function
),
&format!("{kem}, {hash_function} | Initiator Mutual: Generate Request",),
|b| {
b.iter(|| {
initiator_process(
@@ -390,10 +362,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
.unwrap();
c.bench_function(
&format!(
"{}, {} | Initiator Mutual: Encode Frame - Request",
kem, hash_function
),
&format!("{kem}, {hash_function} | Initiator Mutual: Encode Frame - Request",),
|b| {
b.iter(|| i_frame.to_bytes());
},
@@ -402,10 +371,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
let i_frame_bytes = i_frame.to_bytes();
c.bench_function(
&format!(
"{}, {} | Initiator Mutual: Decode Frame - Request",
kem, hash_function
),
&format!("{kem}, {hash_function} | Initiator Mutual: Decode Frame - Request",),
|b| {
b.iter(|| KKTFrame::from_bytes(&i_frame_bytes).unwrap());
},
@@ -414,10 +380,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
c.bench_function(
&format!(
"{}, {} | Initiator Mutual: Responder Ingest Frame",
kem, hash_function
),
&format!("{kem}, {hash_function} | Initiator Mutual: Responder Ingest Frame",),
|b| {
b.iter(|| {
responder_ingest_message(
@@ -443,14 +406,13 @@ pub fn kkt_benchmark(c: &mut Criterion) {
c.bench_function(
&format!(
"{}, {} | Initiator Mutual: Responder Generate Response",
kem, hash_function
"{kem}, {hash_function} | Initiator Mutual: Responder Generate Response",
),
|b| {
b.iter(|| {
responder_process(
&mut r_context,
i_frame_r.session_id_ref(),
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
@@ -461,17 +423,14 @@ pub fn kkt_benchmark(c: &mut Criterion) {
let r_frame = responder_process(
&mut r_context,
i_frame_r.session_id_ref(),
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap();
c.bench_function(
&format!(
"{}, {} | Initiator Mutual: Responder Encode Frame",
kem, hash_function
),
&format!("{kem}, {hash_function} | Initiator Mutual: Responder Encode Frame",),
|b| {
b.iter(|| {
r_frame.to_bytes();
@@ -479,20 +438,18 @@ pub fn kkt_benchmark(c: &mut Criterion) {
},
);
let r_bytes = r_frame.to_bytes();
c.bench_function(
&format!(
"{}, {} | Initiator Mutual: Initiator Ingest Response",
kem, hash_function
"{kem}, {hash_function} | Initiator Mutual: Initiator Ingest Response",
),
|b| {
b.iter(|| {
initiator_ingest_response(
&mut i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
&r_dir_hash,
&r_bytes,
)
.unwrap()
});
@@ -501,9 +458,10 @@ pub fn kkt_benchmark(c: &mut Criterion) {
let obtained_key = initiator_ingest_response(
&mut i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
&r_dir_hash,
&r_bytes,
)
.unwrap();
+45 -55
View File
@@ -8,12 +8,12 @@ use nym_crypto::asymmetric::ed25519;
use crate::error::KKTError;
pub const HASH_LEN_256: u8 = 32;
pub const HASH_LEN_256: usize = 32;
pub const CIPHERSUITE_ENCODING_LEN: usize = 4;
pub const CURVE25519_KEY_LEN: usize = 32;
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum HashFunction {
Blake3,
SHAKE128,
@@ -87,7 +87,7 @@ impl<'a> EncapsulationKey<'a> {
}
}
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum SignatureScheme {
Ed25519,
}
@@ -99,7 +99,7 @@ impl Display for SignatureScheme {
}
}
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum KEM {
MlKem768,
XWing,
@@ -118,7 +118,7 @@ impl Display for KEM {
}
}
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Ciphersuite {
hash_function: HashFunction,
signature_scheme: SignatureScheme,
@@ -172,7 +172,7 @@ impl Ciphersuite {
l
}
}
None => HASH_LEN_256,
None => HASH_LEN_256 as u8,
};
Ok(Self {
hash_function,
@@ -203,7 +203,7 @@ impl Ciphersuite {
},
})
}
pub fn encode(&self) -> [u8; 4] {
pub fn encode(&self) -> [u8; CIPHERSUITE_ENCODING_LEN] {
// [kem, hash, hashlen, sig]
[
match self.kem {
@@ -218,8 +218,8 @@ impl Ciphersuite {
HashFunction::SHAKE128 => 2,
HashFunction::SHA256 => 3,
},
match self.hash_length {
HASH_LEN_256 => 0,
match self.hash_length as usize {
HASH_LEN_256 => 0u8,
_ => self.hash_length,
},
match self.signature_scheme {
@@ -227,55 +227,45 @@ impl Ciphersuite {
},
]
}
pub fn decode(encoding: &[u8]) -> Result<Self, KKTError> {
if encoding.len() == 4 {
let kem = match encoding[0] {
0 => KEM::XWing,
1 => KEM::MlKem768,
2 => KEM::McEliece,
255 => KEM::X25519,
_ => {
return Err(KKTError::CiphersuiteDecodingError {
info: format!("Undefined KEM: {}", encoding[0]),
});
}
};
let hash_function = match encoding[1] {
0 => HashFunction::Blake3,
1 => HashFunction::SHAKE256,
2 => HashFunction::SHAKE128,
3 => HashFunction::SHA256,
_ => {
return Err(KKTError::CiphersuiteDecodingError {
info: format!("Undefined Hash Function: {}", encoding[1]),
});
}
};
pub fn decode(encoding: [u8; CIPHERSUITE_ENCODING_LEN]) -> Result<Self, KKTError> {
let kem = match encoding[0] {
0 => KEM::XWing,
1 => KEM::MlKem768,
2 => KEM::McEliece,
255 => KEM::X25519,
_ => {
return Err(KKTError::CiphersuiteDecodingError {
info: format!("Undefined KEM: {}", encoding[0]),
});
}
};
let hash_function = match encoding[1] {
0 => HashFunction::Blake3,
1 => HashFunction::SHAKE256,
2 => HashFunction::SHAKE128,
3 => HashFunction::SHA256,
_ => {
return Err(KKTError::CiphersuiteDecodingError {
info: format!("Undefined Hash Function: {}", encoding[1]),
});
}
};
let custom_hash_length = match encoding[2] {
0 => None,
_ => Some(encoding[2]),
};
let custom_hash_length = match encoding[2] {
0 => None,
_ => Some(encoding[2]),
};
let signature_scheme = match encoding[3] {
0 => SignatureScheme::Ed25519,
_ => {
return Err(KKTError::CiphersuiteDecodingError {
info: format!("Undefined Signature Scheme: {}", encoding[3]),
});
}
};
let signature_scheme = match encoding[3] {
0 => SignatureScheme::Ed25519,
_ => {
return Err(KKTError::CiphersuiteDecodingError {
info: format!("Undefined Signature Scheme: {}", encoding[3]),
});
}
};
Self::resolve_ciphersuite(kem, hash_function, signature_scheme, custom_hash_length)
} else {
Err(KKTError::CiphersuiteDecodingError {
info: format!(
"Incorrect Encoding Length: actual: {} != expected: {}",
encoding.len(),
CIPHERSUITE_ENCODING_LEN
),
})
}
Self::resolve_ciphersuite(kem, hash_function, signature_scheme, custom_hash_length)
}
}
+102 -119
View File
@@ -1,22 +1,25 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::fmt::Display;
use crate::ciphersuite::CIPHERSUITE_ENCODING_LEN;
use crate::{KKT_VERSION, ciphersuite::Ciphersuite, error::KKTError, frame::KKT_SESSION_ID_LEN};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use std::fmt::Display;
pub const KKT_CONTEXT_LEN: usize = 7;
#[derive(Clone, Copy, PartialEq, Debug)]
// bitmask used: 0b1110_0000
#[derive(Clone, Copy, PartialEq, Debug, IntoPrimitive, TryFromPrimitive)]
#[repr(u8)]
pub enum KKTStatus {
Ok,
InvalidRequestFormat,
InvalidResponseFormat,
InvalidSignature,
UnsupportedCiphersuite,
UnsupportedKKTVersion,
InvalidKey,
Timeout,
Ok = 0b0000_0000,
InvalidRequestFormat = 0b0010_0000,
InvalidResponseFormat = 0b0100_0000,
InvalidSignature = 0b0110_0000,
UnsupportedCiphersuite = 0b1000_0000,
UnsupportedKKTVersion = 0b1010_0000,
InvalidKey = 0b1100_0000,
Timeout = 0b1110_0000,
}
impl Display for KKTStatus {
@@ -33,20 +36,25 @@ impl Display for KKTStatus {
})
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
// bitmask used: 0b0000_0011
#[derive(Clone, Copy, PartialEq, Debug, IntoPrimitive, TryFromPrimitive)]
#[repr(u8)]
pub enum KKTRole {
Initiator,
AnonymousInitiator,
Responder,
Initiator = 0b0000_0000,
Responder = 0b0000_0001,
AnonymousInitiator = 0b0000_0010,
}
#[derive(Clone, Copy, PartialEq, Debug)]
// bitmask used: 0b0001_1100
#[derive(Clone, Copy, PartialEq, Debug, IntoPrimitive, TryFromPrimitive)]
#[repr(u8)]
pub enum KKTMode {
OneWay,
Mutual,
OneWay = 0b0000_0000,
Mutual = 0b0000_0100,
}
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct KKTContext {
version: u8,
message_sequence: u8,
@@ -127,11 +135,14 @@ impl KKTContext {
}
}
pub fn header_len(&self) -> usize {
pub const fn header_len(&self) -> usize {
KKT_CONTEXT_LEN
}
pub fn session_id_len(&self) -> usize {
pub const fn session_id_len(&self) -> usize {
// note: if anyone decides to update this function and changes the constant value,
// you will have to adjust encoding/decoding functions
// match self.role {
// KKTRole::Initiator | KKTRole::Responder => SESSION_ID_LENGTH,
// It doesn't make sense to send a session_id if we send messages in the clear
@@ -144,115 +155,87 @@ impl KKTContext {
self.body_len() + self.signature_len() + self.header_len() + self.session_id_len()
}
pub fn encode(&self) -> Result<Vec<u8>, KKTError> {
let mut header_bytes: Vec<u8> = Vec::with_capacity(KKT_CONTEXT_LEN);
pub fn encode(&self) -> Result<[u8; KKT_CONTEXT_LEN], KKTError> {
let mut header_bytes = [0u8; KKT_CONTEXT_LEN];
if self.message_sequence >= 1 << 4 {
return Err(KKTError::MessageCountLimitReached);
}
header_bytes.push((KKT_VERSION << 4) + self.message_sequence);
let ciphersuite_bytes = self.ciphersuite.encode();
header_bytes.push(
match self.status {
KKTStatus::Ok => 0,
KKTStatus::InvalidRequestFormat => 0b0010_0000,
KKTStatus::InvalidResponseFormat => 0b0100_0000,
KKTStatus::InvalidSignature => 0b0110_0000,
KKTStatus::UnsupportedCiphersuite => 0b1000_0000,
KKTStatus::UnsupportedKKTVersion => 0b1010_0000,
KKTStatus::InvalidKey => 0b1100_0000,
KKTStatus::Timeout => 0b1110_0000,
} + match self.mode {
KKTMode::OneWay => 0,
KKTMode::Mutual => 0b0000_0100,
} + match self.role {
KKTRole::Initiator => 0,
KKTRole::Responder => 1,
KKTRole::AnonymousInitiator => 2,
},
);
header_bytes[0] = (KKT_VERSION << 4) + self.message_sequence;
header_bytes[1] = u8::from(self.status) + u8::from(self.mode) + u8::from(self.role);
header_bytes.extend_from_slice(&self.ciphersuite.encode());
header_bytes.push(0);
let mut i = 2;
for b in ciphersuite_bytes.into_iter() {
header_bytes[i] = b;
i += 1;
}
header_bytes[i] = 0;
Ok(header_bytes)
}
pub fn try_decode(header_bytes: &[u8]) -> Result<Self, KKTError> {
if header_bytes.len() == KKT_CONTEXT_LEN {
let kkt_version = header_bytes[0] & 0b1111_0000;
pub fn try_decode(header_bytes: [u8; KKT_CONTEXT_LEN]) -> Result<Self, KKTError> {
let kkt_version = (header_bytes[0] & 0b1111_0000) >> 4;
let message_sequence_counter = header_bytes[0] & 0b0000_1111;
let message_sequence_counter = header_bytes[0] & 0b0000_1111;
// We only check if stuff is valid here, not necessarily if it's compatible
// We only check if stuff is valid here, not necessarily if it's compatible
if (kkt_version >> 4) > KKT_VERSION {
return Err(KKTError::FrameDecodingError {
info: format!("Header - Invalid KKT Version: {}", kkt_version >> 4),
});
}
let status = match header_bytes[1] & 0b1110_0000 {
0 => KKTStatus::Ok,
0b0010_0000 => KKTStatus::InvalidRequestFormat,
0b0100_0000 => KKTStatus::InvalidResponseFormat,
0b0110_0000 => KKTStatus::InvalidSignature,
0b1000_0000 => KKTStatus::UnsupportedCiphersuite,
0b1010_0000 => KKTStatus::UnsupportedKKTVersion,
0b1100_0000 => KKTStatus::InvalidKey,
0b1110_0000 => KKTStatus::Timeout,
_ => {
return Err(KKTError::FrameDecodingError {
info: format!(
"Header - Invalid KKT Status: {}",
header_bytes[1] & 0b1110_0000
),
});
}
};
let role = match header_bytes[1] & 0b0000_0011 {
0 => KKTRole::Initiator,
1 => KKTRole::Responder,
2 => KKTRole::AnonymousInitiator,
_ => {
return Err(KKTError::FrameDecodingError {
info: format!(
"Header - Invalid KKT Role: {}",
header_bytes[1] & 0b0000_0011
),
});
}
};
let mode = match (header_bytes[1] & 0b0001_1100) >> 2 {
0 => KKTMode::OneWay,
1 => KKTMode::Mutual,
_ => {
return Err(KKTError::FrameDecodingError {
info: format!(
"Header - Invalid KKT Mode: {}",
(header_bytes[1] & 0b0001_1100) >> 2
),
});
}
};
Ok(KKTContext {
version: kkt_version,
status,
mode,
role,
ciphersuite: Ciphersuite::decode(&header_bytes[2..6])?,
message_sequence: message_sequence_counter,
})
} else {
Err(KKTError::FrameDecodingError {
info: format!(
"Header - Invalid Header Length: actual: {} != expected: {}",
header_bytes.len(),
KKT_CONTEXT_LEN
),
})
if kkt_version > KKT_VERSION {
return Err(KKTError::FrameDecodingError {
info: format!("Header - Invalid KKT Version: {kkt_version}"),
});
}
let raw_kkt_status = header_bytes[1] & 0b1110_0000;
let raw_kkt_role = header_bytes[1] & 0b0000_0011;
let raw_kkt_mode = header_bytes[1] & 0b0001_1100;
let status =
KKTStatus::try_from(raw_kkt_status).map_err(|_| KKTError::FrameDecodingError {
info: format!("Header - Invalid KKT Status: {raw_kkt_status}"),
})?;
let role = KKTRole::try_from(raw_kkt_role).map_err(|_| KKTError::FrameDecodingError {
info: format!("Header - Invalid KKT Role: {raw_kkt_role}"),
})?;
let mode = KKTMode::try_from(raw_kkt_mode).map_err(|_| KKTError::FrameDecodingError {
info: format!("Header - Invalid KKT Mode: {raw_kkt_mode}"),
})?;
let ciphersuite_bytes = header_bytes[2..6].try_into().map_err(|_| {
KKTError::CiphersuiteDecodingError {
info: format!(
"Incorrect Encoding Length: actual: 4 != expected: {CIPHERSUITE_ENCODING_LEN}",
),
}
})?;
Ok(KKTContext {
version: kkt_version,
status,
mode,
role,
ciphersuite: Ciphersuite::decode(ciphersuite_bytes)?,
message_sequence: message_sequence_counter,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kkt_context_encoding() {
let valid_context = KKTContext::new(
KKTRole::Initiator,
KKTMode::Mutual,
Ciphersuite::decode([255, 1, 0, 0]).unwrap(),
)
.unwrap();
let encoded = valid_context.encode().unwrap();
let decoded = KKTContext::try_decode(encoded).unwrap();
assert_eq!(decoded, valid_context);
}
}
+230 -74
View File
@@ -1,95 +1,251 @@
use core::hash;
use blake3::Hasher;
use blake3::{Hash, Hasher};
use curve25519_dalek::digest::DynDigest;
use libcrux_psq::traits::Ciphertext;
use nym_crypto::symmetric::aead::{AeadKey, Nonce};
use nym_crypto::{
aes::Aes256,
asymmetric::x25519::{self, PrivateKey, PublicKey},
generic_array::GenericArray,
Aes256GcmSiv,
};
// use rand::{CryptoRng, RngCore};
use libcrux_chacha20poly1305::{NONCE_LEN, TAG_LEN};
use nym_sphinx::{PrivateKey, PublicKey};
use rand::{CryptoRng, RngCore};
use zeroize::Zeroize;
use nym_crypto::aes::cipher::crypto_common::rand_core::{CryptoRng, RngCore};
use crate::kkt::KKT_INITIAL_FRAME_AAD;
use crate::{
ciphersuite::CURVE25519_KEY_LEN, context::KKTContext, error::KKTError, frame::KKTFrame,
};
use crate::error::KKTError;
#[derive(Clone, Copy, Zeroize)]
pub struct KKTSessionSecret([u8; 32]);
fn generate_round_trip_symmetric_key<R>(
rng: &mut R,
remote_public_key: &PublicKey,
) -> ([u8; 64], [u8; 32])
where
R: CryptoRng + RngCore,
{
let mut s = x25519::PrivateKey::new(rng);
let gs = s.public_key();
impl KKTSessionSecret {
pub fn new(remote_public_key: &PublicKey) -> (Self, PublicKey) {
// this doesn't use the newer rand crate
let ephemeral_private_key = PrivateKey::random();
let ephemeral_public_key = PublicKey::from(&ephemeral_private_key);
let mut gbs = s.diffie_hellman(remote_public_key);
s.zeroize();
(
Self::derive(&ephemeral_private_key, remote_public_key),
ephemeral_public_key,
)
}
pub fn from_bytes(secret: [u8; 32]) -> Self {
Self(secret)
}
pub fn try_derive(private_key: &PrivateKey, public_key: &[u8]) -> Result<Self, KKTError> {
let mut pub_key: [u8; 32] = [0u8; 32];
pub_key.copy_from_slice(&public_key[0..CURVE25519_KEY_LEN]);
let mut message: [u8; 64] = [0u8; 64];
message[0..32].clone_from_slice(gs.as_bytes());
// Todo: check validity of pk...
let pk = PublicKey::from(pub_key);
Ok(Self::derive(private_key, &pk))
}
let mut hasher = Hasher::new();
pub fn derive(private_key: &PrivateKey, public_key: &PublicKey) -> Self {
let mut shared_secret = private_key.diffie_hellman(public_key);
hasher.update(&gbs);
gbs.zeroize();
let key: [u8; 32] = hasher.finalize().as_bytes().to_owned();
let mut hasher = Hasher::new();
hasher.update(remote_public_key.as_bytes());
hasher.update(gs.as_bytes());
hasher.update(shared_secret.as_bytes());
shared_secret.zeroize();
hasher.finalize_into_reset(&mut message[32..64]);
(message, key)
}
fn extract_shared_secret(b: &PrivateKey, message: &[u8; 64]) -> Result<[u8; 32], KKTError> {
let gs = PublicKey::from_bytes(&message[0..32])?;
let mut gsb = b.diffie_hellman(&gs);
let mut hasher = Hasher::new();
hasher.update(&gsb);
gsb.zeroize();
let key: [u8; 32] = hasher.finalize().as_bytes().to_owned();
hasher.update(b.public_key().as_bytes());
hasher.update(gs.as_bytes());
// This runs in constant time
if hasher.finalize() == message[32..64] {
Ok(key)
} else {
Err(KKTError::X25519Error {
info: format!("Symmetric Key Hash Validation Error"),
})
Self(hasher.finalize().as_bytes().to_owned())
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
fn encrypt(mut key: [u8; 32], message: &[u8]) -> Result<Vec<u8>, KKTError> {
// The empty nonce is fine since we use the key once.
let nonce = Nonce::<Aes256GcmSiv>::from_slice(&[]);
pub fn encrypt_initial_kkt_frame<R>(
rng: &mut R,
remote_public_key: &PublicKey,
kkt_frame: &KKTFrame,
) -> Result<(KKTSessionSecret, Vec<u8>), KKTError>
where
R: CryptoRng + RngCore,
{
let (session_secret_key, ephemeral_public_key) = KKTSessionSecret::new(remote_public_key);
let ciphertext =
nym_crypto::symmetric::aead::encrypt::<Aes256GcmSiv>(&key.into(), nonce, message)?;
let mut encrypted_frame =
encrypt_kkt_frame(rng, &session_secret_key, kkt_frame, KKT_INITIAL_FRAME_AAD)?;
key.zeroize();
let mut output_buffer = Vec::with_capacity(encrypted_frame.len() + CURVE25519_KEY_LEN);
output_buffer.extend_from_slice(ephemeral_public_key.as_bytes());
output_buffer.append(&mut encrypted_frame);
Ok(ciphertext)
// [ 32 | 12 | ciphertext | 16];
// [eph_pub_key | nonce | ciphertext | tag];
Ok((session_secret_key, output_buffer))
}
fn decrypt(key: [u8; 32], ciphertext: Vec<u8>) -> Vec<u8> {
// The empty nonce is fine since we use the key once.
let nonce = Nonce::<Aes256>::from_slice(&[]);
pub fn decrypt_initial_kkt_frame(
responder_private_key: &PrivateKey,
encrypted_frame_bytes: &[u8],
) -> Result<(KKTSessionSecret, KKTFrame, KKTContext), KKTError> {
if encrypted_frame_bytes.len() < CURVE25519_KEY_LEN + TAG_LEN + NONCE_LEN {
Err(KKTError::AEADError {
info: "Encrypted KKT Frame is too short.",
})
} else {
let shared_secret = KKTSessionSecret::try_derive(
responder_private_key,
&encrypted_frame_bytes[0..CURVE25519_KEY_LEN],
)?;
let ciphertext =
nym_crypto::symmetric::aead::encrypt::<Aes256GcmSiv>(&key.into(), nonce, message)?;
key.zeroize();
Ok(ciphertext)
let (kkt_frame, kkt_context) = decrypt_kkt_frame(
&shared_secret,
&encrypted_frame_bytes[CURVE25519_KEY_LEN..],
KKT_INITIAL_FRAME_AAD,
)?;
Ok((shared_secret, kkt_frame, kkt_context))
}
}
pub fn encrypt_kkt_frame<R>(
rng: &mut R,
secret_key: &KKTSessionSecret,
kkt_frame: &KKTFrame,
aad: &[u8],
) -> Result<Vec<u8>, KKTError>
where
R: CryptoRng + RngCore,
{
let kkt_frame_bytes = kkt_frame.to_bytes();
// generate nonce
let mut nonce: [u8; NONCE_LEN] = [0u8; NONCE_LEN];
rng.fill_bytes(&mut nonce);
let mut ciphertext = encrypt(secret_key.as_bytes(), &kkt_frame_bytes, aad, &nonce)?;
// [ 12 | ciphertext | 16];
// [nonce | ciphertext | tag];
let mut output_buffer: Vec<u8> =
Vec::with_capacity(NONCE_LEN + kkt_frame_bytes.len() + TAG_LEN);
output_buffer.extend_from_slice(&nonce);
output_buffer.append(&mut ciphertext);
Ok(output_buffer)
}
// kkt_frame_bytes should look like this
// [ 12 | ciphertext | 16];
// [nonce | ciphertext | tag];
pub fn decrypt_kkt_frame(
secret_key: &KKTSessionSecret,
kkt_frame_bytes: &[u8],
aad: &[u8],
) -> Result<(KKTFrame, KKTContext), KKTError> {
let mut nonce: [u8; NONCE_LEN] = [0u8; NONCE_LEN];
nonce.copy_from_slice(&kkt_frame_bytes[0..NONCE_LEN]);
let plaintext = decrypt(
secret_key.as_bytes(),
&kkt_frame_bytes[NONCE_LEN..],
aad,
&nonce,
)?;
KKTFrame::from_bytes(&plaintext)
}
fn encrypt(
secret_key: &[u8; 32],
plaintext: &[u8],
aad: &[u8],
nonce: &[u8; NONCE_LEN],
) -> Result<Vec<u8>, KKTError> {
let mut output_buffer = vec![0; plaintext.len() + TAG_LEN];
libcrux_chacha20poly1305::encrypt(secret_key, plaintext, &mut output_buffer, aad, nonce)?;
Ok(output_buffer)
}
fn decrypt(
secret_key: &[u8; 32],
ciphertext: &[u8],
aad: &[u8],
nonce: &[u8; NONCE_LEN],
) -> Result<Vec<u8>, KKTError> {
let mut output_buffer = vec![0; ciphertext.len() - TAG_LEN];
libcrux_chacha20poly1305::decrypt(secret_key, &mut output_buffer, ciphertext, aad, nonce)?;
Ok(output_buffer)
}
#[cfg(test)]
mod test {
use crate::ciphersuite::Ciphersuite;
use crate::context::{KKTContext, KKTMode, KKTRole};
use crate::encryption::{decrypt_kkt_frame, encrypt_kkt_frame};
use crate::frame::{KKT_SESSION_ID_LEN, KKTFrame};
use crate::{
ciphersuite::HASH_LEN_256,
encryption::{KKTSessionSecret, decrypt, encrypt},
key_utils::generate_keypair_x25519,
};
use rand::{RngCore, SeedableRng, rng};
use rand_chacha::ChaCha20Rng;
#[test]
fn test_keygen() {
let responder_x25519_keypair = generate_keypair_x25519();
let (session_secret_key, ephemeral_public_key) =
KKTSessionSecret::new(&responder_x25519_keypair.1);
let shared_secret = KKTSessionSecret::try_derive(
&responder_x25519_keypair.0,
ephemeral_public_key.as_bytes().as_slice(),
)
.unwrap();
assert_eq!(shared_secret.as_bytes(), session_secret_key.as_bytes())
}
#[test]
fn test_encryption() {
let mut rng = rng();
let mut secret_key = [0u8; HASH_LEN_256];
rng.fill_bytes(&mut secret_key);
let mut plaintext = vec![0; 100];
rng.fill_bytes(&mut plaintext);
let mut nonce = [0; 12];
rng.fill_bytes(&mut nonce);
let mut aad = vec![0; 124];
rng.fill_bytes(&mut aad);
let ciphertext = encrypt(&secret_key, &plaintext, &aad, &nonce).unwrap();
let o_plaintext = decrypt(&secret_key, &ciphertext, &aad, &nonce).unwrap();
assert_eq!(o_plaintext, plaintext)
}
#[test]
fn kkt_frame_encryption() -> anyhow::Result<()> {
let mut rng = ChaCha20Rng::seed_from_u64(42);
let session_key = KKTSessionSecret::from_bytes([42u8; 32]);
let aad = b"my-amazing-aad";
let valid_context = KKTContext::new(
KKTRole::Initiator,
KKTMode::Mutual,
Ciphersuite::decode([255, 1, 0, 0])?,
)?;
let dummy_frame = KKTFrame::new(
valid_context.encode()?,
&[2u8; 32],
[3u8; KKT_SESSION_ID_LEN],
&[4u8; 64],
);
let ciphertext = encrypt_kkt_frame(&mut rng, &session_key, &dummy_frame, aad.as_slice())?;
let (frame, context) = decrypt_kkt_frame(&session_key, &ciphertext, aad.as_slice())?;
assert_eq!(dummy_frame, frame);
assert_eq!(context, valid_context);
Ok(())
}
}
+28
View File
@@ -1,6 +1,7 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::fmt::Debug;
use thiserror::Error;
use crate::context::KKTStatus;
@@ -44,6 +45,9 @@ pub enum KKTError {
#[error("{}", info)]
X25519Error { info: &'static str },
#[error("{}", info)]
AEADError { info: &'static str },
#[error("Generic libcrux error")]
LibcruxError,
}
@@ -87,3 +91,27 @@ impl From<libcrux_ecdh::Error> for KKTError {
}
}
}
impl From<libcrux_chacha20poly1305::AeadError> for KKTError {
fn from(err: libcrux_chacha20poly1305::AeadError) -> Self {
KKTError::KEMError {
info: match err {
libcrux_chacha20poly1305::AeadError::PlaintextTooLarge => {
"Plaintext is longer than u32::MAX"
}
libcrux_chacha20poly1305::AeadError::CiphertextTooLarge => {
"Ciphertext is longer than u32::MAX"
}
libcrux_chacha20poly1305::AeadError::AadTooLarge => "Aad is longer than u32::MAX",
libcrux_chacha20poly1305::AeadError::CiphertextTooShort => {
"The provided destination ciphertext does not fit the ciphertext and tag"
}
libcrux_chacha20poly1305::AeadError::PlaintextTooShort => {
"The provided destination plaintext is too short to fit the decrypted plaintext"
}
libcrux_chacha20poly1305::AeadError::InvalidCiphertext => {
"The ciphertext is not a valid encryption under the given key and nonce."
}
},
}
}
}
+79 -53
View File
@@ -14,9 +14,12 @@ use crate::{
pub const KKT_SESSION_ID_LEN: usize = 16;
pub type KKTSessionId = [u8; KKT_SESSION_ID_LEN];
#[derive(Debug, PartialEq, Clone)]
pub struct KKTFrame {
context: Vec<u8>,
session_id: Vec<u8>,
context: [u8; KKT_CONTEXT_LEN],
session_id: KKTSessionId,
body: Vec<u8>,
signature: Vec<u8>,
}
@@ -27,20 +30,31 @@ pub struct KKTFrame {
// if coming from responder => body has the responder's kem public key and the signature is over the context + body + session_id.
impl KKTFrame {
pub fn new(context: &[u8], body: &[u8], session_id: &[u8], signature: &[u8]) -> Self {
pub fn new(
context: [u8; KKT_CONTEXT_LEN],
body: &[u8],
session_id: [u8; KKT_SESSION_ID_LEN],
signature: &[u8],
) -> Self {
Self {
context: Vec::from(context),
context,
body: Vec::from(body),
session_id: Vec::from(session_id),
session_id,
signature: Vec::from(signature),
}
}
pub fn context_ref(&self) -> &[u8] {
&self.context
}
pub fn context(&self) -> Result<KKTContext, KKTError> {
KKTContext::try_decode(self.context)
}
pub fn signature_ref(&self) -> &[u8] {
&self.signature
}
pub fn body_ref(&self) -> &[u8] {
&self.body
}
@@ -48,6 +62,10 @@ impl KKTFrame {
pub fn session_id_ref(&self) -> &[u8] {
&self.session_id
}
pub fn session_id(&self) -> [u8; KKT_SESSION_ID_LEN] {
self.session_id
}
pub fn signature_mut(&mut self) -> &mut [u8] {
&mut self.signature
}
@@ -73,57 +91,65 @@ impl KKTFrame {
}
pub fn from_bytes(bytes: &[u8]) -> Result<(Self, KKTContext), KKTError> {
let len = bytes.len();
if bytes.len() < KKT_CONTEXT_LEN {
Err(KKTError::FrameDecodingError {
return Err(KKTError::FrameDecodingError {
info: format!(
"Frame is shorter than expected context length: actual {} != expected {}",
bytes.len(),
KKT_CONTEXT_LEN
"Frame is shorter than expected context length: actual {len} != expected {KKT_CONTEXT_LEN}",
),
})
} else {
let context_bytes = Vec::from(&bytes[0..KKT_CONTEXT_LEN]);
let context = KKTContext::try_decode(&context_bytes)?;
let (mut session_id, mut body, mut signature): (Vec<u8>, Vec<u8>, Vec<u8>) =
(vec![], vec![], vec![]);
if bytes.len() == context.full_message_len() {
if context.body_len() > 0 {
body.extend_from_slice(
&bytes[KKT_CONTEXT_LEN..KKT_CONTEXT_LEN + context.body_len()],
);
}
if context.session_id_len() > 0 {
session_id.extend_from_slice(
&bytes[KKT_CONTEXT_LEN + context.body_len()
..KKT_CONTEXT_LEN + context.body_len() + context.session_id_len()],
);
}
if context.signature_len() > 0 {
signature.extend_from_slice(
&bytes[KKT_CONTEXT_LEN + context.body_len() + context.session_id_len()
..KKT_CONTEXT_LEN
+ context.body_len()
+ context.session_id_len()
+ context.signature_len()],
);
}
Ok((
KKTFrame::new(&context_bytes, &body, &session_id, &signature),
context,
))
} else {
Err(KKTError::FrameDecodingError {
info: format!(
"Frame is shorter than expected: actual {} != expected {}",
bytes.len(),
context.full_message_len()
),
})
}
});
}
// SAFETY: we're using exactly KKT_CONTEXT_LEN bytes
#[allow(clippy::unwrap_used)]
let context_bytes = bytes[0..KKT_CONTEXT_LEN].try_into().unwrap();
let context = KKTContext::try_decode(context_bytes)?;
if bytes.len() != context.full_message_len() {
return Err(KKTError::FrameDecodingError {
info: format!(
"Frame is shorter than expected: actual {len} != expected {}",
context.full_message_len()
),
});
}
let mut body = Vec::new();
let mut signature = Vec::new();
// decode body
if context.body_len() > 0 {
let body_bytes = &bytes[KKT_CONTEXT_LEN..KKT_CONTEXT_LEN + context.body_len()];
body.extend_from_slice(body_bytes);
}
let session_bytes = &bytes[KKT_CONTEXT_LEN + context.body_len()
..KKT_CONTEXT_LEN + context.body_len() + KKT_SESSION_ID_LEN];
// SAFETY: we're using exactly KKT_SESSION_ID_LEN bytes and we checked for sufficient bytes
#[allow(clippy::unwrap_used)]
let session_id = session_bytes.try_into().unwrap();
// // old code left for reference if session id becomes variable in length:
// if context.session_id_len() > 0 {
// session_id.extend_from_slice(
// &bytes[KKT_CONTEXT_LEN + context.body_len()
// ..KKT_CONTEXT_LEN + context.body_len() + context.session_id_len()],
// );
// }
// decode signature
if context.signature_len() > 0 {
let signature_bytes = &bytes[KKT_CONTEXT_LEN + context.body_len() + KKT_SESSION_ID_LEN
..KKT_CONTEXT_LEN
+ context.body_len()
+ KKT_SESSION_ID_LEN
+ context.signature_len()];
signature.extend_from_slice(signature_bytes);
}
Ok((
KKTFrame::new(context_bytes, &body, session_id, &signature),
context,
))
}
}
+15
View File
@@ -7,7 +7,22 @@ use classic_mceliece_rust::keypair_boxed;
use libcrux_kem::{Algorithm, key_gen};
use libcrux_sha3;
use nym_crypto::asymmetric::ed25519;
use rand::{CryptoRng, RngCore};
pub fn generate_keypair_ed25519<R>(rng: &mut R, index: Option<u32>) -> ed25519::KeyPair
where
R: RngCore + CryptoRng,
{
let mut secret_initiator: [u8; 32] = [0u8; 32];
rng.fill_bytes(&mut secret_initiator);
ed25519::KeyPair::from_secret(secret_initiator, index.unwrap_or(0))
}
pub fn generate_keypair_x25519() -> (nym_sphinx::PrivateKey, nym_sphinx::PublicKey) {
let private_key = nym_sphinx::PrivateKey::random();
let public_key = nym_sphinx::PublicKey::from(&private_key);
(private_key, public_key)
}
// (decapsulation_key, encapsulation_key)
pub fn generate_keypair_libcrux<R>(
+132 -41
View File
@@ -14,8 +14,8 @@ use rand::{CryptoRng, RngCore};
use crate::{
ciphersuite::{Ciphersuite, EncapsulationKey},
context::{KKTContext, KKTMode},
encryption::{decrypt_initial_kkt_frame, decrypt_kkt_frame, encrypt_kkt_frame},
error::KKTError,
frame::KKTFrame,
};
// Re-export core session functions for advanced use cases
@@ -24,7 +24,13 @@ pub use crate::session::{
responder_ingest_message, responder_process,
};
/// Request a KEM public key from a responder (OneWay mode).
use crate::encryption::{KKTSessionSecret, encrypt_initial_kkt_frame};
use crate::frame::KKTFrame;
pub(crate) const KKT_RESPONSE_AAD: &[u8] = b"KKT_Response";
pub(crate) const KKT_INITIAL_FRAME_AAD: &[u8] = b"KKT_INITIAL_FRAME";
/// Perform an *Encrypted* request for a KEM public key from a responder (OneWay mode).
///
/// This is the client-side operation that initiates a KKT exchange.
/// The request will be signed with the provided signing key.
@@ -33,17 +39,20 @@ pub use crate::session::{
/// * `rng` - Random number generator
/// * `ciphersuite` - Negotiated ciphersuite (KEM, hash, signature algorithms)
/// * `signing_key` - Client's Ed25519 signing key for authentication
/// * `responder_dh_public_key` - Responder's long-term x25519 Diffie-Hellman public key
///
/// # Returns
/// * `KKTSessionSecret` - Session Secret Key to use when decrypting responses
/// * `KKTContext` - Context to use when validating the response
/// * `KKTFrame` - Signed request frame to send to responder
/// * `Vec<u8>` - Contains the client's ephemeral public key and encrypted and signed bytes to send to responder
///
/// # Example
/// ```ignore
/// let (context, request_frame) = request_kem_key(
/// let (session_secret, context, request_frame) = request_kem_key(
/// &mut rng,
/// ciphersuite,
/// client_signing_key,
/// responder_dh_public_key,
/// )?;
/// // Send request_frame to gateway
/// ```
@@ -51,13 +60,21 @@ pub fn request_kem_key<R: CryptoRng + RngCore>(
rng: &mut R,
ciphersuite: Ciphersuite,
signing_key: &ed25519::PrivateKey,
) -> Result<(KKTContext, KKTFrame), KKTError> {
responder_dh_public_key: &nym_sphinx::PublicKey,
) -> Result<(KKTSessionSecret, KKTContext, Vec<u8>), KKTError> {
// OneWay mode: client only wants responder's KEM key
// None: client doesn't send their own KEM key
initiator_process(rng, KKTMode::OneWay, ciphersuite, signing_key, None)
let (initiator_context, initiator_frame) =
initiator_process(rng, KKTMode::OneWay, ciphersuite, signing_key, None)?;
// Generate the session's shared secret and encrypt the Initiator's request
let (session_secret, encrypted_request_bytes) =
encrypt_initial_kkt_frame(rng, responder_dh_public_key, &initiator_frame)?;
Ok((session_secret, initiator_context, encrypted_request_bytes))
}
/// Validate a KKT response and extract the responder's KEM public key.
/// Decrypt, validate an *Encrypted* KKT response and extract the responder's KEM public key.
///
/// This is the client-side operation that processes the gateway's response.
/// It verifies the signature and validates the key hash against the expected value
@@ -65,6 +82,7 @@ pub fn request_kem_key<R: CryptoRng + RngCore>(
///
/// # Arguments
/// * `context` - Context from the initial request
/// * `session_secret` - Session Secret Key (generated with request)
/// * `responder_vk` - Responder's Ed25519 verification key (from directory)
/// * `expected_key_hash` - Expected hash of responder's KEM key (from directory)
/// * `response_bytes` - Serialized response frame from responder
@@ -76,7 +94,8 @@ pub fn request_kem_key<R: CryptoRng + RngCore>(
/// ```ignore
/// let gateway_kem_key = validate_kem_response(
/// &mut context,
/// gateway_verification_key,
/// &session_secret,
/// &gateway_verification_key,
/// &expected_hash_from_directory,
/// &response_bytes,
/// )?;
@@ -84,23 +103,44 @@ pub fn request_kem_key<R: CryptoRng + RngCore>(
/// ```
pub fn validate_kem_response<'a>(
context: &mut KKTContext,
session_secret: &KKTSessionSecret,
responder_vk: &ed25519::PublicKey,
expected_key_hash: &[u8],
response_bytes: &[u8],
encrypted_response_bytes: &[u8],
) -> Result<EncapsulationKey<'a>, KKTError> {
initiator_ingest_response(context, responder_vk, expected_key_hash, response_bytes)
let (responder_frame, responder_context) =
decrypt_kkt_response_frame(session_secret, encrypted_response_bytes)?;
initiator_ingest_response(
context,
&responder_frame,
&responder_context,
responder_vk,
expected_key_hash,
)
}
/// Handle a KKT request and generate a signed response with the responder's KEM key.
/// Decrypts and validates an *Encrypted* KKT response
///
/// This is the client-side operation that processes the gateway's response.
pub fn decrypt_kkt_response_frame(
session_secret: &KKTSessionSecret,
frame_ciphertext: &[u8],
) -> Result<(KKTFrame, KKTContext), KKTError> {
decrypt_kkt_frame(session_secret, frame_ciphertext, KKT_RESPONSE_AAD)
}
/// Handle an *Encrypted* KKT request and generate a signed response with the responder's KEM key.
///
/// This is the gateway-side operation that processes a client's KKT request.
/// It validates the request signature (if authenticated) and responds with
/// the gateway's KEM public key, signed for authenticity.
///
/// # Arguments
/// * `request_frame` - Request frame received from initiator
/// * `encrypted_request_bytes` - encrypted KEM request
/// * `initiator_vk` - Initiator's Ed25519 verification key (None for anonymous)
/// * `responder_signing_key` - Gateway's Ed25519 signing key
/// * `responder_dh_public_key` - Gateway's long-term x25519 Diffie-Hellman private key
/// * `responder_kem_key` - Gateway's KEM public key to send
///
/// # Returns
@@ -116,31 +156,40 @@ pub fn validate_kem_response<'a>(
/// )?;
/// // Send response_frame back to client
/// ```
pub fn handle_kem_request<'a>(
request_frame: &KKTFrame,
pub fn handle_kem_request<'a, R>(
rng: &mut R,
encrypted_request_bytes: &[u8],
initiator_vk: Option<&ed25519::PublicKey>,
responder_signing_key: &ed25519::PrivateKey,
responder_dh_private_key: &nym_sphinx::PrivateKey,
responder_kem_key: &EncapsulationKey<'a>,
) -> Result<KKTFrame, KKTError> {
// Parse context from the request frame
let request_bytes = request_frame.to_bytes();
let (_, request_context) = KKTFrame::from_bytes(&request_bytes)?;
) -> Result<Vec<u8>, KKTError>
where
R: RngCore + CryptoRng,
{
// Compute the session's shared secret, decrypt and parse context from the request frame
let (session_secret, request_frame, initiator_context) =
decrypt_initial_kkt_frame(responder_dh_private_key, encrypted_request_bytes)?;
// Validate the request (verifies signature if initiator_vk provided)
let (mut response_context, _) = responder_ingest_message(
&request_context,
&initiator_context,
initiator_vk,
None, // Not checking initiator's KEM key in OneWay mode
request_frame,
&request_frame,
)?;
// Generate signed response with our KEM public key
responder_process(
let responder_frame = responder_process(
&mut response_context,
request_frame.session_id_ref(),
request_frame.session_id(),
responder_signing_key,
responder_kem_key,
)
)?;
// Encrypt the responder's response with the session's shared secret
encrypt_kkt_frame(rng, &session_secret, &responder_frame, KKT_RESPONSE_AAD)
}
#[cfg(test)]
@@ -158,11 +207,14 @@ mod tests {
// Generate Ed25519 keypairs for both parties
let mut initiator_secret = [0u8; 32];
rng.fill_bytes(&mut initiator_secret);
let initiator_keypair = ed25519::KeyPair::from_secret(initiator_secret, 0);
let ed25519_init = ed25519::KeyPair::from_secret(initiator_secret, 0);
let mut responder_secret = [0u8; 32];
rng.fill_bytes(&mut responder_secret);
let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1);
let ed25519_resp = ed25519::KeyPair::from_secret(responder_secret, 1);
let x25519_resp_priv = nym_sphinx::PrivateKey::random();
let x25519_resp_pub = nym_sphinx::PublicKey::from(&x25519_resp_priv);
// Generate responder's KEM keypair (X25519 for testing)
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
@@ -185,14 +237,21 @@ mod tests {
);
// Client: Request KEM key
let (mut context, request_frame) =
request_kem_key(&mut rng, ciphersuite, initiator_keypair.private_key()).unwrap();
let (session_key, mut context, request_frame_ciphertext) = request_kem_key(
&mut rng,
ciphersuite,
ed25519_init.private_key(),
&x25519_resp_pub,
)
.unwrap();
// Gateway: Handle request
let response_frame = handle_kem_request(
&request_frame,
Some(initiator_keypair.public_key()), // Authenticated
responder_keypair.private_key(),
let response_frame_ciphertext = handle_kem_request(
&mut rng,
&request_frame_ciphertext,
Some(ed25519_init.public_key()), // Authenticated
ed25519_resp.private_key(),
&x25519_resp_priv,
&responder_kem_key,
)
.unwrap();
@@ -200,9 +259,10 @@ mod tests {
// Client: Validate response
let obtained_key = validate_kem_response(
&mut context,
responder_keypair.public_key(),
&session_key,
ed25519_resp.public_key(),
&key_hash,
&response_frame.to_bytes(),
&response_frame_ciphertext,
)
.unwrap();
@@ -222,6 +282,9 @@ mod tests {
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
let x25519_resp_priv = nym_sphinx::PrivateKey::random();
let x25519_resp_pub = nym_sphinx::PublicKey::from(&x25519_resp_priv);
let ciphersuite = Ciphersuite::resolve_ciphersuite(
KEM::X25519,
HashFunction::Blake3,
@@ -240,11 +303,17 @@ mod tests {
let (mut context, request_frame) =
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
// Generate the session's shared secret and encrypt the Initiator's request
let (session_secret, encrypted_request_bytes) =
encrypt_initial_kkt_frame(&mut rng, &x25519_resp_pub, &request_frame).unwrap();
// Gateway: Handle anonymous request
let response_frame = handle_kem_request(
&request_frame,
&mut rng,
&encrypted_request_bytes,
None, // Anonymous - no verification key
responder_keypair.private_key(),
&x25519_resp_priv,
&responder_kem_key,
)
.unwrap();
@@ -252,9 +321,10 @@ mod tests {
// Initiator: Validate response
let obtained_key = validate_kem_response(
&mut context,
&session_secret,
responder_keypair.public_key(),
&key_hash,
&response_frame.to_bytes(),
&response_frame,
)
.unwrap();
@@ -273,6 +343,9 @@ mod tests {
rng.fill_bytes(&mut responder_secret);
let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1);
let x25519_resp_priv = nym_sphinx::PrivateKey::random();
let x25519_resp_pub = nym_sphinx::PublicKey::from(&x25519_resp_priv);
// Different keypair for wrong signature
let mut wrong_secret = [0u8; 32];
rng.fill_bytes(&mut wrong_secret);
@@ -289,14 +362,21 @@ mod tests {
)
.unwrap();
let (_context, request_frame) =
request_kem_key(&mut rng, ciphersuite, initiator_keypair.private_key()).unwrap();
let (_session_key, _context, request_frame_ciphertext) = request_kem_key(
&mut rng,
ciphersuite,
initiator_keypair.private_key(),
&x25519_resp_pub,
)
.unwrap();
// Gateway handles request but we provide WRONG verification key
let result = handle_kem_request(
&request_frame,
&mut rng,
&request_frame_ciphertext,
Some(wrong_keypair.public_key()), // Wrong key!
responder_keypair.private_key(),
&x25519_resp_priv,
&responder_kem_key,
);
@@ -316,6 +396,9 @@ mod tests {
rng.fill_bytes(&mut responder_secret);
let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1);
let x25519_resp_priv = nym_sphinx::PrivateKey::random();
let x25519_resp_pub = nym_sphinx::PublicKey::from(&x25519_resp_priv);
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
@@ -330,13 +413,20 @@ mod tests {
// Use WRONG hash
let wrong_hash = [0u8; 32];
let (mut context, request_frame) =
request_kem_key(&mut rng, ciphersuite, initiator_keypair.private_key()).unwrap();
let (session_key, mut context, request_frame) = request_kem_key(
&mut rng,
ciphersuite,
initiator_keypair.private_key(),
&x25519_resp_pub,
)
.unwrap();
let response_frame = handle_kem_request(
&mut rng,
&request_frame,
Some(initiator_keypair.public_key()),
responder_keypair.private_key(),
&x25519_resp_priv,
&responder_kem_key,
)
.unwrap();
@@ -344,9 +434,10 @@ mod tests {
// Client validates with WRONG hash
let result = validate_kem_response(
&mut context,
&session_key,
responder_keypair.public_key(),
&wrong_hash, // Wrong!
&response_frame.to_bytes(),
&response_frame,
);
// Should fail hash validation
+276 -23
View File
@@ -3,28 +3,31 @@
pub mod ciphersuite;
pub mod context;
// pub mod encryption;
pub mod encryption;
pub mod error;
pub mod frame;
pub mod key_utils;
pub mod kkt;
pub mod session;
// pub mod psq;
// This must be less than 4 bits
pub const KKT_VERSION: u8 = 1;
const _: () = assert!(KKT_VERSION < 1 << 4);
#[cfg(test)]
mod test {
use nym_crypto::asymmetric::ed25519;
use rand::prelude::*;
use crate::kkt::KKT_RESPONSE_AAD;
use crate::{
ciphersuite::{Ciphersuite, EncapsulationKey, HashFunction, KEM},
encryption::{
decrypt_initial_kkt_frame, decrypt_kkt_frame, encrypt_initial_kkt_frame,
encrypt_kkt_frame,
},
frame::KKTFrame,
key_utils::{generate_keypair_libcrux, generate_keypair_mceliece, hash_encapsulation_key},
key_utils::{
generate_keypair_ed25519, generate_keypair_libcrux, generate_keypair_mceliece,
generate_keypair_x25519, hash_encapsulation_key,
},
session::{
anonymous_initiator_process, initiator_ingest_response, initiator_process,
responder_ingest_message, responder_process,
@@ -36,13 +39,9 @@ mod test {
let mut rng = rand::rng();
// generate ed25519 keys
let mut secret_initiator: [u8; 32] = [0u8; 32];
rng.fill_bytes(&mut secret_initiator);
let initiator_ed25519_keypair = ed25519::KeyPair::from_secret(secret_initiator, 0);
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
let mut secret_responder: [u8; 32] = [0u8; 32];
rng.fill_bytes(&mut secret_responder);
let responder_ed25519_keypair = ed25519::KeyPair::from_secret(secret_responder, 1);
for kem in [KEM::MlKem768, KEM::XWing, KEM::X25519, KEM::McEliece] {
for hash_function in [
HashFunction::Blake3,
@@ -117,7 +116,7 @@ mod test {
let r_frame = responder_process(
&mut r_context,
i_frame_r.session_id_ref(),
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
@@ -125,15 +124,18 @@ mod test {
let r_bytes = r_frame.to_bytes();
let obtained_key = initiator_ingest_response(
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
let i_obtained_key = initiator_ingest_response(
&mut i_context,
&i_frame_r,
&i_context_r,
responder_ed25519_keypair.public_key(),
&r_dir_hash,
&r_bytes,
)
.unwrap();
assert_eq!(obtained_key.encode(), r_kem_key_bytes)
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
}
// Initiator, OneWay
{
@@ -162,7 +164,7 @@ mod test {
let r_frame = responder_process(
&mut r_context,
i_frame_r.session_id_ref(),
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
@@ -170,11 +172,14 @@ mod test {
let r_bytes = r_frame.to_bytes();
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
let i_obtained_key = initiator_ingest_response(
&mut i_context,
&i_frame_r,
&i_context_r,
responder_ed25519_keypair.public_key(),
&r_dir_hash,
&r_bytes,
)
.unwrap();
@@ -208,7 +213,7 @@ mod test {
let r_frame = responder_process(
&mut r_context,
i_frame_r.session_id_ref(),
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
@@ -216,15 +221,263 @@ mod test {
let r_bytes = r_frame.to_bytes();
let obtained_key = initiator_ingest_response(
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
let i_obtained_key = initiator_ingest_response(
&mut i_context,
&i_frame_r,
&i_context_r,
responder_ed25519_keypair.public_key(),
&r_dir_hash,
&r_bytes,
)
.unwrap();
assert_eq!(obtained_key.encode(), r_kem_key_bytes)
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
}
}
}
}
#[test]
fn test_kkt_psq_e2e_encrypted() {
let mut rng = rand::rng();
// generate ed25519 keys
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
// generate responder x25519 keys
let responder_x25519_keypair = generate_keypair_x25519();
for kem in [KEM::MlKem768, KEM::XWing, KEM::X25519, KEM::McEliece] {
for hash_function in [
HashFunction::Blake3,
HashFunction::SHA256,
HashFunction::SHAKE128,
HashFunction::SHAKE256,
] {
let ciphersuite = Ciphersuite::resolve_ciphersuite(
kem,
hash_function,
crate::ciphersuite::SignatureScheme::Ed25519,
None,
)
.unwrap();
// generate kem public keys
let (responder_kem_public_key, initiator_kem_public_key) = match kem {
KEM::MlKem768 => (
EncapsulationKey::MlKem768(
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
),
EncapsulationKey::MlKem768(
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
),
),
KEM::XWing => (
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
),
KEM::X25519 => (
EncapsulationKey::X25519(
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
),
EncapsulationKey::X25519(
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
),
),
KEM::McEliece => (
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
),
};
let i_kem_key_bytes = initiator_kem_public_key.encode();
let r_kem_key_bytes = responder_kem_public_key.encode();
let i_dir_hash = hash_encapsulation_key(
&ciphersuite.hash_function(),
ciphersuite.hash_len(),
&i_kem_key_bytes,
);
let r_dir_hash = hash_encapsulation_key(
&ciphersuite.hash_function(),
ciphersuite.hash_len(),
&r_kem_key_bytes,
);
// Anonymous Initiator, OneWay
{
let (mut i_context, i_frame) =
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
// encryption - initiator frame
let (i_session_secret, i_bytes) =
encrypt_initial_kkt_frame(&mut rng, &responder_x25519_keypair.1, &i_frame)
.unwrap();
// decryption - initiator frame
let (r_session_secret, i_frame_r, i_context_r) =
decrypt_initial_kkt_frame(&responder_x25519_keypair.0, &i_bytes).unwrap();
let (mut r_context, _) =
responder_ingest_message(&i_context_r, None, None, &i_frame_r).unwrap();
let r_frame = responder_process(
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap();
// encryption - responder frame
let r_bytes =
encrypt_kkt_frame(&mut rng, &r_session_secret, &r_frame, KKT_RESPONSE_AAD)
.unwrap();
// decryption - responder frame
let (i_frame_r, i_context_r) =
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
let i_obtained_key = initiator_ingest_response(
&mut i_context,
&i_frame_r,
&i_context_r,
responder_ed25519_keypair.public_key(),
&r_dir_hash,
)
.unwrap();
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
}
// Initiator, OneWay
{
let (mut i_context, i_frame) = initiator_process(
&mut rng,
crate::context::KKTMode::OneWay,
ciphersuite,
initiator_ed25519_keypair.private_key(),
None,
)
.unwrap();
// encryption - initiator frame
let (i_session_secret, i_bytes) =
encrypt_initial_kkt_frame(&mut rng, &responder_x25519_keypair.1, &i_frame)
.unwrap();
// decryption - initiator frame
let (r_session_secret, i_frame_r, r_context) =
decrypt_initial_kkt_frame(&responder_x25519_keypair.0, &i_bytes).unwrap();
let (mut r_context, r_obtained_key) = responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
None,
&i_frame_r,
)
.unwrap();
assert!(r_obtained_key.is_none());
let r_frame = responder_process(
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap();
// encryption - responder frame
let r_bytes =
encrypt_kkt_frame(&mut rng, &r_session_secret, &r_frame, KKT_RESPONSE_AAD)
.unwrap();
// decryption - responder frame
let (i_frame_r, i_context_r) =
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
let i_obtained_key = initiator_ingest_response(
&mut i_context,
&i_frame_r,
&i_context_r,
responder_ed25519_keypair.public_key(),
&r_dir_hash,
)
.unwrap();
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
}
// Initiator, Mutual
{
let (mut i_context, i_frame) = initiator_process(
&mut rng,
crate::context::KKTMode::Mutual,
ciphersuite,
initiator_ed25519_keypair.private_key(),
Some(&initiator_kem_public_key),
)
.unwrap();
// encryption - initiator frame
let (i_session_secret, i_bytes) =
encrypt_initial_kkt_frame(&mut rng, &responder_x25519_keypair.1, &i_frame)
.unwrap();
// decryption - initiator frame
let (r_session_secret, i_frame_r, i_context_r) =
decrypt_initial_kkt_frame(&responder_x25519_keypair.0, &i_bytes).unwrap();
let (mut r_context, r_obtained_key) = responder_ingest_message(
&i_context_r,
Some(initiator_ed25519_keypair.public_key()),
Some(&i_dir_hash),
&i_frame_r,
)
.unwrap();
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
let r_frame = responder_process(
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap();
// encryption - responder frame
let r_bytes =
encrypt_kkt_frame(&mut rng, &r_session_secret, &r_frame, KKT_RESPONSE_AAD)
.unwrap();
// decryption - responder frame
let (i_frame_r, i_context_r) =
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
let i_obtained_key = initiator_ingest_response(
&mut i_context,
&i_frame_r,
&i_context_r,
responder_ed25519_keypair.public_key(),
&r_dir_hash,
)
.unwrap();
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
}
}
}
+14 -18
View File
@@ -1,6 +1,7 @@
use nym_crypto::asymmetric::ed25519::{self, Signature};
use rand::{CryptoRng, RngCore};
use crate::frame::KKTSessionId;
use crate::{
ciphersuite::{Ciphersuite, EncapsulationKey},
context::{KKTContext, KKTMode, KKTRole, KKTStatus},
@@ -51,7 +52,7 @@ where
Ok((
context,
KKTFrame::new(&context_bytes, body, &session_id, &signature),
KKTFrame::new(context_bytes, body, session_id, &signature),
))
}
@@ -68,43 +69,38 @@ where
let mut session_id = [0u8; KKT_SESSION_ID_LEN];
rng.fill_bytes(&mut session_id);
Ok((
context,
KKTFrame::new(&context_bytes, &[], &session_id, &[]),
))
Ok((context, KKTFrame::new(context_bytes, &[], session_id, &[])))
}
pub fn initiator_ingest_response<'a>(
own_context: &mut KKTContext,
remote_frame: &KKTFrame,
remote_context: &KKTContext,
remote_verification_key: &ed25519::PublicKey,
expected_hash: &[u8],
message_bytes: &[u8],
) -> Result<EncapsulationKey<'a>, KKTError> {
// sizes have to be correct
let (frame, remote_context) = KKTFrame::from_bytes(message_bytes)?;
check_compatibility(own_context, &remote_context)?;
check_compatibility(own_context, remote_context)?;
match remote_context.status() {
KKTStatus::Ok => {
let mut bytes_to_verify: Vec<u8> = Vec::with_capacity(
remote_context.full_message_len() - remote_context.signature_len(),
);
bytes_to_verify.extend_from_slice(&remote_context.encode()?);
bytes_to_verify.extend_from_slice(frame.body_ref());
bytes_to_verify.extend_from_slice(frame.session_id_ref());
bytes_to_verify.extend_from_slice(remote_frame.body_ref());
bytes_to_verify.extend_from_slice(remote_frame.session_id_ref());
match Signature::from_bytes(frame.signature_ref()) {
match Signature::from_bytes(remote_frame.signature_ref()) {
Ok(sig) => match remote_verification_key.verify(bytes_to_verify, &sig) {
Ok(()) => {
let received_encapsulation_key = EncapsulationKey::decode(
own_context.ciphersuite().kem(),
frame.body_ref(),
remote_frame.body_ref(),
)?;
match validate_encapsulation_key(
&own_context.ciphersuite().hash_function(),
own_context.ciphersuite().hash_len(),
frame.body_ref(),
remote_frame.body_ref(),
expected_hash,
) {
true => Ok(received_encapsulation_key),
@@ -206,7 +202,7 @@ pub fn responder_ingest_message<'a>(
pub fn responder_process<'a>(
own_context: &mut KKTContext,
session_id: &[u8],
session_id: KKTSessionId,
signing_key: &ed25519::PrivateKey,
encapsulation_key: &EncapsulationKey<'a>,
) -> Result<KKTFrame, KKTError> {
@@ -218,11 +214,11 @@ pub fn responder_process<'a>(
Vec::with_capacity(own_context.full_message_len() - own_context.signature_len());
bytes_to_sign.extend_from_slice(&own_context.encode()?);
bytes_to_sign.extend_from_slice(&body);
bytes_to_sign.extend_from_slice(session_id);
bytes_to_sign.extend_from_slice(&session_id);
let signature = signing_key.sign(bytes_to_sign).to_bytes();
Ok(KKTFrame::new(&context_bytes, &body, session_id, &signature))
Ok(KKTFrame::new(context_bytes, &body, session_id, &signature))
}
fn check_compatibility(
+132 -101
View File
@@ -37,9 +37,10 @@
//! ).unwrap();
//!
//! // Client: Create request
//! let (client_context, request_data) = create_request(
//! let (session_secret, client_context, request_data) = create_request(
//! ciphersuite,
//! &client_signing_key,
//! &responder_dh_public_key
//! ).unwrap();
//!
//! // Gateway: Handle request
@@ -47,12 +48,14 @@
//! &request_data,
//! Some(&client_verification_key),
//! &gateway_signing_key,
//! &gateway_dh_private_key,
//! &gateway_kem_public_key,
//! ).unwrap();
//!
//! // Client: Process response
//! let gateway_kem_key = process_response(
//! client_context,
//! &session_secret,
//! &gateway_verification_key,
//! &expected_key_hash,
//! &response_data,
@@ -64,7 +67,7 @@ use crate::message::{KKTRequestData, KKTResponseData};
use nym_crypto::asymmetric::ed25519;
use nym_kkt::ciphersuite::{Ciphersuite, EncapsulationKey};
use nym_kkt::context::KKTContext;
use nym_kkt::frame::KKTFrame;
use nym_kkt::encryption::KKTSessionSecret;
use nym_kkt::kkt::{handle_kem_request, request_kem_key, validate_kem_response};
/// Creates a KKT request to obtain the responder's KEM public key.
@@ -75,8 +78,10 @@ use nym_kkt::kkt::{handle_kem_request, request_kem_key, validate_kem_response};
/// # Arguments
/// * `ciphersuite` - Negotiated ciphersuite (KEM, hash, signature algorithms)
/// * `signing_key` - Client's Ed25519 signing key for authentication
/// * `responder_dh_public_key` - Gateway's x25519 public key (from directory)
///
/// # Returns
/// * `KKTSessionSecret` - Session secret key to encrypt/decrypt KKT messages for this session
/// * `KKTContext` - Context to use when validating the response
/// * `KKTRequestData` - Serialized KKT request frame to send to gateway
///
@@ -85,14 +90,15 @@ use nym_kkt::kkt::{handle_kem_request, request_kem_key, validate_kem_response};
pub fn create_request(
ciphersuite: Ciphersuite,
signing_key: &ed25519::PrivateKey,
) -> Result<(KKTContext, KKTRequestData), LpError> {
responder_dh_public_key: &nym_sphinx::PublicKey,
) -> Result<(KKTSessionSecret, KKTContext, KKTRequestData), LpError> {
// Note: Uses rand 0.9's thread_rng() to match nym-kkt's rand version
let mut rng = rand09::rng();
let (context, frame) = request_kem_key(&mut rng, ciphersuite, signing_key)
.map_err(|e| LpError::KKTError(e.to_string()))?;
let (session_secret, context, request_bytes) =
request_kem_key(&mut rng, ciphersuite, signing_key, responder_dh_public_key)
.map_err(|e| LpError::KKTError(e.to_string()))?;
let request_bytes = frame.to_bytes();
Ok((context, KKTRequestData(request_bytes)))
Ok((session_secret, context, KKTRequestData(request_bytes)))
}
/// Processes a KKT response and extracts the responder's KEM public key.
@@ -102,6 +108,7 @@ pub fn create_request(
///
/// # Arguments
/// * `context` - Context from the initial `create_request()` call
/// * `session_secret` - The KKT session secret key from the initial `create_request()` call
/// * `responder_vk` - Responder's Ed25519 verification key (from directory)
/// * `expected_key_hash` - Expected hash of responder's KEM key (from directory)
/// * `response_data` - Serialized KKT response frame from responder
@@ -116,12 +123,14 @@ pub fn create_request(
/// - Key hash doesn't match expected value
pub fn process_response<'a>(
mut context: KKTContext,
session_secret: &KKTSessionSecret,
responder_vk: &ed25519::PublicKey,
expected_key_hash: &[u8],
response_data: &KKTResponseData,
) -> Result<EncapsulationKey<'a>, LpError> {
validate_kem_response(
&mut context,
session_secret,
responder_vk,
expected_key_hash,
&response_data.0,
@@ -139,6 +148,7 @@ pub fn process_response<'a>(
/// * `request_data` - Serialized KKT request frame from initiator
/// * `initiator_vk` - Initiator's Ed25519 verification key (None for anonymous)
/// * `responder_signing_key` - Gateway's Ed25519 signing key
/// * `responder_dh_private_key` - Gateway's x25519 private key
/// * `responder_kem_key` - Gateway's KEM public key to send
///
/// # Returns
@@ -153,22 +163,21 @@ pub fn handle_request<'a>(
request_data: &KKTRequestData,
initiator_vk: Option<&ed25519::PublicKey>,
responder_signing_key: &ed25519::PrivateKey,
responder_dh_private_key: &nym_sphinx::PrivateKey,
responder_kem_key: &EncapsulationKey<'a>,
) -> Result<KKTResponseData, LpError> {
// Deserialize request frame
let (request_frame, _) = KKTFrame::from_bytes(&request_data.0)
.map_err(|e| LpError::KKTError(format!("Failed to parse KKT request: {}", e)))?;
let mut rng = rand09::rng();
// Handle the request and generate response
let response_frame = handle_kem_request(
&request_frame,
let response_bytes = handle_kem_request(
&mut rng,
&request_data.0,
initiator_vk,
responder_signing_key,
responder_dh_private_key,
responder_kem_key,
)
.map_err(|e| LpError::KKTError(e.to_string()))?;
let response_bytes = response_frame.to_bytes();
Ok(KKTResponseData(response_bytes))
}
@@ -176,7 +185,10 @@ pub fn handle_request<'a>(
mod tests {
use super::*;
use nym_kkt::ciphersuite::{HashFunction, KEM, SignatureScheme};
use nym_kkt::key_utils::{generate_keypair_libcrux, hash_encapsulation_key};
use nym_kkt::key_utils::{
generate_keypair_ed25519, generate_keypair_libcrux, generate_keypair_x25519,
hash_encapsulation_key,
};
use rand09::RngCore;
#[test]
@@ -184,13 +196,10 @@ mod tests {
let mut rng = rand09::rng();
// Generate Ed25519 keypairs for both parties
let mut initiator_secret = [0u8; 32];
rng.fill_bytes(&mut initiator_secret);
let initiator_keypair = ed25519::KeyPair::from_secret(initiator_secret, 0);
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
let mut responder_secret = [0u8; 32];
rng.fill_bytes(&mut responder_secret);
let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1);
let (responder_x25519_sk, responder_x25519_pk) = generate_keypair_x25519();
// Generate responder's KEM keypair (X25519 for testing)
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
@@ -213,14 +222,19 @@ mod tests {
);
// Client: Create request
let (context, request_data) =
create_request(ciphersuite, initiator_keypair.private_key()).unwrap();
let (session_secret, context, request_data) = create_request(
ciphersuite,
initiator_ed25519_keypair.private_key(),
&responder_x25519_pk,
)
.unwrap();
// Gateway: Handle request
let response_data = handle_request(
&request_data,
Some(initiator_keypair.public_key()),
responder_keypair.private_key(),
Some(initiator_ed25519_keypair.public_key()),
responder_ed25519_keypair.private_key(),
&responder_x25519_sk,
&responder_kem_key,
)
.unwrap();
@@ -228,7 +242,8 @@ mod tests {
// Client: Process response
let obtained_key = process_response(
context,
responder_keypair.public_key(),
&session_secret,
responder_ed25519_keypair.public_key(),
&key_hash,
&response_data,
)
@@ -238,70 +253,71 @@ mod tests {
assert_eq!(obtained_key.encode(), responder_kem_key.encode());
}
#[test]
fn test_kkt_roundtrip_anonymous() {
let mut rng = rand09::rng();
// #[test]
// fn test_kkt_roundtrip_anonymous() {
// let mut rng = rand09::rng();
// Only responder has keys (anonymous initiator)
let mut responder_secret = [0u8; 32];
rng.fill_bytes(&mut responder_secret);
let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1);
// // Only responder has keys (anonymous initiator)
// // Generate Ed25519 keypairs for both parties
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
// let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
let ciphersuite = Ciphersuite::resolve_ciphersuite(
KEM::X25519,
HashFunction::Blake3,
SignatureScheme::Ed25519,
None,
)
.unwrap();
// let (responder_x25519_sk, responder_x25519_pk) = generate_keypair_x25519();
let key_hash = hash_encapsulation_key(
&ciphersuite.hash_function(),
ciphersuite.hash_len(),
&responder_kem_key.encode(),
);
// let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
// let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
// Anonymous initiator - use anonymous_initiator_process directly
use nym_kkt::kkt::anonymous_initiator_process;
let (mut context, request_frame) =
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
let request_data = KKTRequestData(request_frame.to_bytes());
// let ciphersuite = Ciphersuite::resolve_ciphersuite(
// KEM::X25519,
// HashFunction::Blake3,
// SignatureScheme::Ed25519,
// None,
// )
// .unwrap();
// Gateway: Handle anonymous request
let response_data = handle_request(
&request_data,
None, // Anonymous - no verification key
responder_keypair.private_key(),
&responder_kem_key,
)
.unwrap();
// let key_hash = hash_encapsulation_key(
// &ciphersuite.hash_function(),
// ciphersuite.hash_len(),
// &responder_kem_key.encode(),
// );
// Initiator: Validate response
let obtained_key = validate_kem_response(
&mut context,
responder_keypair.public_key(),
&key_hash,
&response_data.0,
)
.unwrap();
// // Anonymous initiator - use anonymous_initiator_process directly
// use nym_kkt::kkt::anonymous_initiator_process;
// let (mut context, request_frame) =
// anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
// let request_data = KKTRequestData(request_frame.to_bytes());
assert_eq!(obtained_key.encode(), responder_kem_key.encode());
}
// // Gateway: Handle anonymous request
// let response_data = handle_request(
// &request_data,
// None,
// responder_ed25519_keypair.private_key(),
// &responder_x25519_sk,
// &responder_kem_key,
// )
// .unwrap();
// // Initiator: Validate response
// let obtained_key = initiator_ingest_response(
// &mut context,
// responder_ed25519_keypair.public_key(),
// &key_hash,
// &response_data.0,
// )
// .unwrap();
// assert_eq!(obtained_key.encode(), responder_kem_key.encode());
// }
#[test]
fn test_invalid_signature_rejected() {
let mut rng = rand09::rng();
let mut initiator_secret = [0u8; 32];
rng.fill_bytes(&mut initiator_secret);
let initiator_keypair = ed25519::KeyPair::from_secret(initiator_secret, 0);
// Generate Ed25519 keypairs for both parties
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
let mut responder_secret = [0u8; 32];
rng.fill_bytes(&mut responder_secret);
let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1);
let (responder_x25519_sk, responder_x25519_pk) = generate_keypair_x25519();
// Different keypair for wrong signature
let mut wrong_secret = [0u8; 32];
@@ -319,14 +335,19 @@ mod tests {
)
.unwrap();
let (_context, request_data) =
create_request(ciphersuite, initiator_keypair.private_key()).unwrap();
let (_session_secret, _context, request_data) = create_request(
ciphersuite,
initiator_ed25519_keypair.private_key(),
&responder_x25519_pk,
)
.unwrap();
// Gateway handles request but we provide WRONG verification key
let result = handle_request(
&request_data,
Some(wrong_keypair.public_key()), // Wrong key!
responder_keypair.private_key(),
responder_ed25519_keypair.private_key(),
&responder_x25519_sk,
&responder_kem_key,
);
@@ -343,13 +364,11 @@ mod tests {
fn test_hash_mismatch_rejected() {
let mut rng = rand09::rng();
let mut initiator_secret = [0u8; 32];
rng.fill_bytes(&mut initiator_secret);
let initiator_keypair = ed25519::KeyPair::from_secret(initiator_secret, 0);
// Generate Ed25519 keypairs for both parties
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
let mut responder_secret = [0u8; 32];
rng.fill_bytes(&mut responder_secret);
let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1);
let (responder_x25519_sk, responder_x25519_pk) = generate_keypair_x25519();
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
@@ -365,13 +384,18 @@ mod tests {
// Use WRONG hash
let wrong_hash = [0u8; 32];
let (context, request_data) =
create_request(ciphersuite, initiator_keypair.private_key()).unwrap();
let (session_secret, context, request_data) = create_request(
ciphersuite,
initiator_ed25519_keypair.private_key(),
&responder_x25519_pk,
)
.unwrap();
let response_data = handle_request(
&request_data,
Some(initiator_keypair.public_key()),
responder_keypair.private_key(),
Some(initiator_ed25519_keypair.public_key()),
responder_ed25519_keypair.private_key(),
&responder_x25519_sk,
&responder_kem_key,
)
.unwrap();
@@ -379,7 +403,8 @@ mod tests {
// Client validates with WRONG hash
let result = process_response(
context,
responder_keypair.public_key(),
&session_secret,
responder_ed25519_keypair.public_key(),
&wrong_hash, // Wrong!
&response_data,
);
@@ -399,7 +424,9 @@ mod tests {
let mut responder_secret = [0u8; 32];
rng.fill_bytes(&mut responder_secret);
let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1);
let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
let (responder_x25519_sk, _responder_x25519_pk) = generate_keypair_x25519();
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
@@ -410,7 +437,8 @@ mod tests {
let result = handle_request(
&malformed_request,
None,
responder_keypair.private_key(),
responder_ed25519_keypair.private_key(),
&responder_x25519_sk,
&responder_kem_key,
);
@@ -427,13 +455,11 @@ mod tests {
fn test_malformed_response_rejected() {
let mut rng = rand09::rng();
let mut initiator_secret = [0u8; 32];
rng.fill_bytes(&mut initiator_secret);
let initiator_keypair = ed25519::KeyPair::from_secret(initiator_secret, 0);
// Generate Ed25519 keypairs for both parties
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
let mut responder_secret = [0u8; 32];
rng.fill_bytes(&mut responder_secret);
let responder_keypair = ed25519::KeyPair::from_secret(responder_secret, 1);
let (_responder_x25519_sk, responder_x25519_pk) = generate_keypair_x25519();
let ciphersuite = Ciphersuite::resolve_ciphersuite(
KEM::X25519,
@@ -443,8 +469,12 @@ mod tests {
)
.unwrap();
let (context, _request_data) =
create_request(ciphersuite, initiator_keypair.private_key()).unwrap();
let (session_secret, context, _request_data) = create_request(
ciphersuite,
initiator_ed25519_keypair.private_key(),
&responder_x25519_pk,
)
.unwrap();
// Create malformed response data
let malformed_response = KKTResponseData(vec![0xFF; 100]);
@@ -452,7 +482,8 @@ mod tests {
let result = process_response(
context,
responder_keypair.public_key(),
&session_secret,
responder_ed25519_keypair.public_key(),
&key_hash,
&malformed_response,
);
+32 -23
View File
@@ -18,6 +18,8 @@ use crate::replay::ReceivingKeyCounterValidator;
use crate::{LpError, LpMessage, LpPacket};
use nym_crypto::asymmetric::ed25519;
use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey};
use nym_kkt::encryption::KKTSessionSecret;
use nym_kkt::kkt::decrypt_kkt_response_frame;
use parking_lot::Mutex;
use snow::Builder;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
@@ -71,6 +73,7 @@ pub enum KKTState {
InitiatorWaiting {
/// KKT context for verifying the response
context: nym_kkt::context::KKTContext,
session_secret: KKTSessionSecret,
},
/// KKT exchange completed (initiator received and validated KEM key).
@@ -88,7 +91,7 @@ impl std::fmt::Debug for KKTState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotStarted => write!(f, "KKTState::NotStarted"),
Self::InitiatorWaiting { context } => f
Self::InitiatorWaiting { context, .. } => f
.debug_struct("KKTState::InitiatorWaiting")
.field("context", context)
.finish(),
@@ -561,13 +564,20 @@ impl LpSession {
};
let mut rng = rand09::rng();
match request_kem_key(&mut rng, ciphersuite, &self.local_ed25519_private) {
Ok((context, request_frame)) => {
match request_kem_key(
&mut rng,
ciphersuite,
&self.local_ed25519_private,
&self.remote_x25519_public,
) {
Ok((session_secret, context, request_bytes)) => {
// Store context for response validation
*kkt_state = KKTState::InitiatorWaiting { context };
*kkt_state = KKTState::InitiatorWaiting {
context,
session_secret,
};
// Serialize KKT frame to bytes
let request_bytes = request_frame.to_bytes();
Some(Ok(LpMessage::KKTRequest(crate::message::KKTRequestData(
request_bytes,
))))
@@ -613,8 +623,11 @@ impl LpSession {
let mut kkt_state = self.kkt_state.lock();
// Extract context from waiting state
let mut context = match &*kkt_state {
KKTState::InitiatorWaiting { context } => *context,
let (mut context, session_secret) = match &*kkt_state {
KKTState::InitiatorWaiting {
context,
session_secret,
} => (*context, *session_secret),
_ => {
return Err(LpError::Internal(
"KKT response received in invalid state".to_string(),
@@ -629,11 +642,10 @@ impl LpSession {
None => {
// Signature-only mode: extract key from response and compute its hash
// This effectively bypasses hash validation while keeping signature validation
use nym_kkt::frame::KKTFrame;
let (frame, _) = KKTFrame::from_bytes(response_bytes).map_err(|e| {
LpError::Internal(format!("Failed to parse KKT response: {:?}", e))
})?;
let (frame, _) = decrypt_kkt_response_frame(&session_secret, response_bytes)
.map_err(|e| {
LpError::Internal(format!("Failed to decrypt KKT response: {:?}", e))
})?;
hash_for_validation = hash_encapsulation_key(
&context.ciphersuite().hash_function(),
@@ -647,6 +659,7 @@ impl LpSession {
// Validate response and extract KEM key
let kem_pk = validate_kem_response(
&mut context,
&session_secret,
&self.remote_ed25519_public,
hash_ref,
response_bytes,
@@ -680,20 +693,19 @@ impl LpSession {
request_bytes: &[u8],
responder_kem_pk: &EncapsulationKey,
) -> Result<LpMessage, LpError> {
use nym_kkt::{frame::KKTFrame, kkt::handle_kem_request};
use nym_kkt::kkt::handle_kem_request;
let mut rng = rand09::rng();
let mut kkt_state = self.kkt_state.lock();
// Deserialize request frame
let (request_frame, _) = KKTFrame::from_bytes(request_bytes).map_err(|e| {
LpError::Internal(format!("KKT request deserialization failed: {:?}", e))
})?;
// Handle request and create signed response
let response_frame = handle_kem_request(
&request_frame,
let response_bytes = handle_kem_request(
&mut rng,
request_bytes,
Some(&self.remote_ed25519_public), // Verify initiator signature
&self.local_ed25519_private, // Sign response
&self.local_x25519_private,
responder_kem_pk,
)
.map_err(|e| LpError::Internal(format!("KKT request handling failed: {:?}", e)))?;
@@ -702,9 +714,6 @@ impl LpSession {
// Responder doesn't store the kem_pk since they already have their own KEM keypair
*kkt_state = KKTState::ResponderProcessed;
// Serialize response frame
let response_bytes = response_frame.to_bytes();
Ok(LpMessage::KKTResponse(crate::message::KKTResponseData(
response_bytes,
)))
+205 -205
View File
@@ -439,75 +439,75 @@ impl LpStateMachine {
// --- Inline handle_handshake_packet logic ---
// 1. Check replay protection *before* processing
if let Err(e) = session.receiving_counter_quick_check(packet.header.counter) {
let _reason = e.to_string();
result_action = Some(Err(e));
LpState::Handshaking { session }
let _reason = e.to_string();
result_action = Some(Err(e));
LpState::Handshaking { session }
// LpState::Closed { reason }
} else {
// 2. Process the handshake message
match session.process_handshake_message(&packet.message) {
Ok(_) => {
// 3. Mark counter as received *after* successful processing
if let Err(e) = session.receiving_counter_mark(packet.header.counter) {
let _reason = e.to_string();
result_action = Some(Err(e));
// 2. Process the handshake message
match session.process_handshake_message(&packet.message) {
Ok(_) => {
// 3. Mark counter as received *after* successful processing
if let Err(e) = session.receiving_counter_mark(packet.header.counter) {
let _reason = e.to_string();
result_action = Some(Err(e));
// LpState::Closed { reason }
LpState::Handshaking { session }
} else {
// 4. First check if we need to send a handshake message (before checking completion)
match session.prepare_handshake_message() {
Some(Ok(message)) => {
match session.next_packet(message) {
Ok(response_packet) => {
result_action = Some(Ok(LpAction::SendPacket(response_packet)));
// Check if handshake became complete after preparing message
if session.is_handshake_complete() {
LpState::Transport { session } // Transition to Transport
} else {
LpState::Handshaking { session } // Remain Handshaking
}
}
Err(e) => {
let reason = e.to_string();
result_action = Some(Err(e));
LpState::Closed { reason }
}
}
}
Some(Err(e)) => {
let reason = e.to_string();
result_action = Some(Err(e));
LpState::Closed { reason }
}
None => {
// 5. No message to send - check if handshake is complete
if session.is_handshake_complete() {
result_action = Some(Ok(LpAction::HandshakeComplete));
LpState::Transport { session } // Transition to Transport
} else {
// Handshake stalled unexpectedly
let err = LpError::NoiseError(NoiseError::Other(
"Handshake stalled unexpectedly".to_string(),
));
let reason = err.to_string();
result_action = Some(Err(err));
LpState::Closed { reason }
}
}
}
}
}
Err(e) => { // Error from process_handshake_message
let reason = e.to_string();
result_action = Some(Err(e));
LpState::Closed { reason }
}
}
} else {
// 4. First check if we need to send a handshake message (before checking completion)
match session.prepare_handshake_message() {
Some(Ok(message)) => {
match session.next_packet(message) {
Ok(response_packet) => {
result_action = Some(Ok(LpAction::SendPacket(response_packet)));
// Check if handshake became complete after preparing message
if session.is_handshake_complete() {
LpState::Transport { session } // Transition to Transport
} else {
LpState::Handshaking { session } // Remain Handshaking
}
}
Err(e) => {
let reason = e.to_string();
result_action = Some(Err(e));
LpState::Closed { reason }
}
}
}
Some(Err(e)) => {
let reason = e.to_string();
result_action = Some(Err(e));
LpState::Closed { reason }
}
None => {
// 5. No message to send - check if handshake is complete
if session.is_handshake_complete() {
result_action = Some(Ok(LpAction::HandshakeComplete));
LpState::Transport { session } // Transition to Transport
} else {
// Handshake stalled unexpectedly
let err = LpError::NoiseError(NoiseError::Other(
"Handshake stalled unexpectedly".to_string(),
));
let reason = err.to_string();
result_action = Some(Err(err));
LpState::Closed { reason }
}
}
}
}
}
Err(e) => { // Error from process_handshake_message
let reason = e.to_string();
result_action = Some(Err(e));
LpState::Closed { reason }
}
}
}
// --- End inline handle_handshake_packet logic ---
}
}
// Reject SendData during handshake
// Reject SendData during handshake
(LpState::Handshaking { session }, LpInput::SendData(_)) => { // Keep session if returning to this state
result_action = Some(Err(LpError::InvalidStateTransition {
state: "Handshaking".to_string(),
@@ -522,114 +522,114 @@ impl LpStateMachine {
state: "Handshaking".to_string(),
input: "StartHandshake".to_string(),
}));
// Invalid input, remain in Handshaking state
LpState::Handshaking { session }
// Invalid input, remain in Handshaking state
LpState::Handshaking { session }
}
// --- Transport State ---
(LpState::Transport { session }, LpInput::ReceivePacket(packet)) => {
// Check if packet lp_id matches our session
if packet.header.receiver_idx() != session.id() {
// Check if packet lp_id matches our session
if packet.header.receiver_idx() != session.id() {
result_action = Some(Err(LpError::UnknownSessionId(packet.header.receiver_idx())));
LpState::Transport { session }
} else {
// Check message type - handle subsession initiation from peer
match &packet.message {
// Peer initiated subsession - we become responder
LpMessage::SubsessionKK1(kk1_data) => {
// Create subsession as responder
let subsession_index = session.next_subsession_index();
match session.create_subsession(subsession_index, false) {
Ok(subsession) => {
// Process KK1
match subsession.process_message(&kk1_data.payload) {
Ok(_) => {
// Prepare KK2 response
match subsession.prepare_message() {
Ok(kk2_payload) => {
let kk2_msg = LpMessage::SubsessionKK2(SubsessionKK2Data { payload: kk2_payload });
match session.next_packet(kk2_msg) {
Ok(response_packet) => {
result_action = Some(Ok(LpAction::SendPacket(response_packet)));
// Stay in SubsessionHandshaking, wait for SubsessionReady
LpState::SubsessionHandshaking { session, subsession: Box::new(subsession) }
}
Err(e) => {
let reason = e.to_string();
result_action = Some(Err(e));
LpState::Closed { reason }
}
}
}
Err(e) => {
let reason = e.to_string();
result_action = Some(Err(e));
LpState::Closed { reason }
}
}
}
Err(e) => {
let reason = e.to_string();
result_action = Some(Err(e));
LpState::Closed { reason }
}
}
}
Err(e) => {
let reason = e.to_string();
result_action = Some(Err(e));
LpState::Closed { reason }
}
}
}
// Normal encrypted data
LpMessage::EncryptedData(_) => {
// 1. Check replay protection
if let Err(e) = session.receiving_counter_quick_check(packet.header.counter) {
result_action = Some(Err(e));
LpState::Transport { session }
} else {
// 2. Decrypt data
match session.decrypt_data(&packet.message) {
Ok(plaintext) => {
// 3. Mark counter as received
if let Err(e) = session.receiving_counter_mark(packet.header.counter) {
result_action = Some(Err(e));
LpState::Transport { session }
} else {
// 4. Deliver data
result_action = Some(Ok(LpAction::DeliverData(BytesMut::from(plaintext.as_slice()))));
LpState::Transport { session }
}
}
Err(e) => {
let reason = e.to_string();
result_action = Some(Err(e.into()));
LpState::Closed { reason }
}
}
}
}
// Stale abort in Transport state - race already resolved.
// This can happen if abort arrives after loser already returned to Transport
// via KK1 processing (loser detected local < remote and became responder).
// The winner's abort message arrived late. Silently ignore.
LpMessage::SubsessionAbort => {
debug!("Ignoring stale SubsessionAbort in Transport state");
result_action = None;
LpState::Transport { session }
}
_ => {
// Unexpected message type in Transport state
let err = LpError::InvalidStateTransition {
state: "Transport".to_string(),
input: format!("Unexpected message type: {}", packet.message),
};
result_action = Some(Err(err));
LpState::Transport { session }
}
}
}
} else {
// Check message type - handle subsession initiation from peer
match &packet.message {
// Peer initiated subsession - we become responder
LpMessage::SubsessionKK1(kk1_data) => {
// Create subsession as responder
let subsession_index = session.next_subsession_index();
match session.create_subsession(subsession_index, false) {
Ok(subsession) => {
// Process KK1
match subsession.process_message(&kk1_data.payload) {
Ok(_) => {
// Prepare KK2 response
match subsession.prepare_message() {
Ok(kk2_payload) => {
let kk2_msg = LpMessage::SubsessionKK2(SubsessionKK2Data { payload: kk2_payload });
match session.next_packet(kk2_msg) {
Ok(response_packet) => {
result_action = Some(Ok(LpAction::SendPacket(response_packet)));
// Stay in SubsessionHandshaking, wait for SubsessionReady
LpState::SubsessionHandshaking { session, subsession: Box::new(subsession) }
}
Err(e) => {
let reason = e.to_string();
result_action = Some(Err(e));
LpState::Closed { reason }
}
}
}
Err(e) => {
let reason = e.to_string();
result_action = Some(Err(e));
LpState::Closed { reason }
}
}
}
Err(e) => {
let reason = e.to_string();
result_action = Some(Err(e));
LpState::Closed { reason }
}
}
}
Err(e) => {
let reason = e.to_string();
result_action = Some(Err(e));
LpState::Closed { reason }
}
}
}
// Normal encrypted data
LpMessage::EncryptedData(_) => {
// 1. Check replay protection
if let Err(e) = session.receiving_counter_quick_check(packet.header.counter) {
result_action = Some(Err(e));
LpState::Transport { session }
} else {
// 2. Decrypt data
match session.decrypt_data(&packet.message) {
Ok(plaintext) => {
// 3. Mark counter as received
if let Err(e) = session.receiving_counter_mark(packet.header.counter) {
result_action = Some(Err(e));
LpState::Transport { session }
} else {
// 4. Deliver data
result_action = Some(Ok(LpAction::DeliverData(BytesMut::from(plaintext.as_slice()))));
LpState::Transport { session }
}
}
Err(e) => {
let reason = e.to_string();
result_action = Some(Err(e.into()));
LpState::Closed { reason }
}
}
}
}
// Stale abort in Transport state - race already resolved.
// This can happen if abort arrives after loser already returned to Transport
// via KK1 processing (loser detected local < remote and became responder).
// The winner's abort message arrived late. Silently ignore.
LpMessage::SubsessionAbort => {
debug!("Ignoring stale SubsessionAbort in Transport state");
result_action = None;
LpState::Transport { session }
}
_ => {
// Unexpected message type in Transport state
let err = LpError::InvalidStateTransition {
state: "Transport".to_string(),
input: format!("Unexpected message type: {}", packet.message),
};
result_action = Some(Err(err));
LpState::Transport { session }
}
}
}
}
(LpState::Transport { session }, LpInput::SendData(data)) => {
// Encrypt and send application data
@@ -641,17 +641,17 @@ impl LpStateMachine {
result_action = Some(Err(e.into()));
}
}
// Remain in transport state
LpState::Transport { session }
// Remain in transport state
LpState::Transport { session }
}
// Reject StartHandshake if already in transport
// Reject StartHandshake if already in transport
(LpState::Transport { session }, LpInput::StartHandshake) => { // Keep session
result_action = Some(Err(LpError::InvalidStateTransition {
state: "Transport".to_string(),
input: "StartHandshake".to_string(),
}));
// Invalid input, remain in Transport state
LpState::Transport { session }
// Invalid input, remain in Transport state
LpState::Transport { session }
}
// --- Transport + InitiateSubsession → SubsessionHandshaking ---
@@ -970,25 +970,25 @@ impl LpStateMachine {
result_action = Some(Err(LpError::UnknownSessionId(packet.header.receiver_idx())));
LpState::ReadOnlyTransport { session }
} else if let Err(e) = session.receiving_counter_quick_check(packet.header.counter) {
result_action = Some(Err(e));
LpState::ReadOnlyTransport { session }
} else {
match session.decrypt_data(&packet.message) {
Ok(plaintext) => {
if let Err(e) = session.receiving_counter_mark(packet.header.counter) {
result_action = Some(Err(e));
LpState::ReadOnlyTransport { session }
} else {
result_action = Some(Ok(LpAction::DeliverData(BytesMut::from(plaintext.as_slice()))));
LpState::ReadOnlyTransport { session }
}
}
Err(e) => {
let reason = e.to_string();
result_action = Some(Err(e.into()));
LpState::Closed { reason }
result_action = Some(Err(e));
LpState::ReadOnlyTransport { session }
} else {
match session.decrypt_data(&packet.message) {
Ok(plaintext) => {
if let Err(e) = session.receiving_counter_mark(packet.header.counter) {
result_action = Some(Err(e));
LpState::ReadOnlyTransport { session }
} else {
result_action = Some(Ok(LpAction::DeliverData(BytesMut::from(plaintext.as_slice()))));
LpState::ReadOnlyTransport { session }
}
}
Err(e) => {
let reason = e.to_string();
result_action = Some(Err(e.into()));
LpState::Closed { reason }
}
}
}
}
@@ -1026,8 +1026,8 @@ impl LpStateMachine {
LpInput::Close,
) => {
result_action = Some(Ok(LpAction::ConnectionClosed));
// Transition to Closed state
LpState::Closed { reason: "Closed by user".to_string() }
// Transition to Closed state
LpState::Closed { reason: "Closed by user".to_string() }
}
// Ignore Close if already Closed
(closed_state @ LpState::Closed { .. }, LpInput::Close) => {
@@ -1040,36 +1040,36 @@ impl LpStateMachine {
// result_action = Some(Err(LpError::LpSessionClosed));
// closed_state
// }
// Ignore ReceivePacket if Closed
// Ignore ReceivePacket if Closed
(closed_state @ LpState::Closed { .. }, LpInput::ReceivePacket(_)) => {
result_action = Some(Err(LpError::LpSessionClosed));
closed_state
result_action = Some(Err(LpError::LpSessionClosed));
closed_state
}
// Ignore SendData if Closed
// Ignore SendData if Closed
(closed_state @ LpState::Closed { .. }, LpInput::SendData(_)) => {
result_action = Some(Err(LpError::LpSessionClosed));
closed_state
result_action = Some(Err(LpError::LpSessionClosed));
closed_state
}
// Processing state should not be matched directly if using replace
(LpState::Processing, _) => {
// This case should ideally be unreachable if placeholder logic is correct
let err = LpError::Internal("Reached Processing state unexpectedly".to_string());
let reason = err.to_string();
result_action = Some(Err(err));
LpState::Closed { reason }
// This case should ideally be unreachable if placeholder logic is correct
let err = LpError::Internal("Reached Processing state unexpectedly".to_string());
let reason = err.to_string();
result_action = Some(Err(err));
LpState::Closed { reason }
}
// --- Default: Invalid input for current state (if any combinations missed) ---
// Consider if this should transition to Closed state. For now, just report error
// and transition to Closed as a safety measure.
(invalid_state, input) => {
let err = LpError::InvalidStateTransition {
state: format!("{:?}", invalid_state), // Use owned state for debug info
input: format!("{:?}", input),
};
let reason = err.to_string();
result_action = Some(Err(err));
LpState::Closed { reason }
let err = LpError::InvalidStateTransition {
state: format!("{:?}", invalid_state), // Use owned state for debug info
input: format!("{:?}", input),
};
let reason = err.to_string();
result_action = Some(Err(err));
LpState::Closed { reason }
}
};