From 67297dc73acc0ddd3553a65dc25f6d7871ec3191 Mon Sep 17 00:00:00 2001 From: Kevin Lewi Date: Sat, 8 Nov 2025 13:25:57 -0800 Subject: [PATCH] Moving shared functionality out of TripleDh key exchange (#405) --- src/key_exchange/shared.rs | 138 ++++++++++++++++++++++++++++++++++- src/key_exchange/tripledh.rs | 108 ++++++++++----------------- 2 files changed, 176 insertions(+), 70 deletions(-) diff --git a/src/key_exchange/shared.rs b/src/key_exchange/shared.rs index b3289aa..0541d9c 100644 --- a/src/key_exchange/shared.rs +++ b/src/key_exchange/shared.rs @@ -10,11 +10,12 @@ use core::ops::Add; use derive_where::derive_where; use digest::core_api::BlockSizeUser; -use digest::{Digest, Output, OutputSizeUser, Update}; +use digest::{Digest, Mac, Output, OutputSizeUser, Update}; use generic_array::sequence::Concat; use generic_array::typenum::{IsLess, Le, NonZero, Sum, U1, U2, U32, U256, Unsigned}; use generic_array::{ArrayLength, GenericArray}; use hkdf::{Hkdf, HkdfExtract}; +use hmac::Hmac; use rand::{CryptoRng, RngCore}; use super::{ @@ -100,6 +101,23 @@ pub(super) struct DerivedKeys { pub(super) handshake_secret: Output, } +/// Helper bundle containing the common `TripleDH` server state that both +/// `TripleDh` and `TripleDhKem` builders need. +pub(super) struct Ke2BuilderCommon +where + H::Core: ProxyHash, + ::BlockSize: IsLess, + Le<::BlockSize, U256>: NonZero, + G::Sk: DiffieHellman, +{ + pub(super) server_nonce: GenericArray, + pub(super) transcript_hasher: H, + pub(super) client_e_pk: PublicKey, + pub(super) server_e_pk: PublicKey, + pub(super) shared_secret_1: GenericArray, + pub(super) shared_secret_3: GenericArray, +} + //////////////////////////////////////////////// // Helper functions and Trait Implementations // // ========================================== // @@ -158,6 +176,60 @@ pub(super) fn transcript( .chain(server_e_pk) } +/// Generates the server-side `TripleDH` transcript state shared by multiple +/// key-exchange variants. +pub(super) fn ke2_builder_common<'a, G, H, CS, R>( + rng: &mut R, + credential_request: SerializedCredentialRequest, + ke1_message: Ke1Message, + credential_response: SerializedCredentialResponse, + client_s_pk: PublicKey, + identifiers: SerializedIdentifiers<'a, KeGroup>, + context: SerializedContext<'a>, +) -> Result, ProtocolError> +where + G: Group, + H: Hash, + R: RngCore + CryptoRng, + CS: CipherSuite, + H::Core: ProxyHash, + ::BlockSize: IsLess, + Le<::BlockSize, U256>: NonZero, + G::Sk: DiffieHellman, + CS::KeyExchange: KeyExchange, +{ + let server_ephemeral = KeyPair::::derive_random(rng); + let server_nonce = generate_nonce::(rng); + let server_e_pk_bytes = server_ephemeral.public().serialize(); + + let ke1_iter = ke1_message.to_iter(); + let client_e_pk = ke1_message.client_e_pk.clone(); + + let transcript_hasher = transcript( + &context, + &identifiers, + &credential_request, + &ke1_iter, + &credential_response, + server_nonce, + &server_e_pk_bytes, + ); + + let shared_secret_1 = server_ephemeral + .private() + .ke_diffie_hellman(&ke1_message.client_e_pk); + let shared_secret_3 = server_ephemeral.private().ke_diffie_hellman(&client_s_pk); + + Ok(Ke2BuilderCommon { + server_nonce, + transcript_hasher, + client_e_pk, + server_e_pk: server_ephemeral.public().clone(), + shared_secret_1, + shared_secret_3, + }) +} + // Internal function which takes computed shared secrets, along with some // auxiliary metadata, to produce the session key and two MAC keys pub(super) fn derive_keys<'a, H: Hash>( @@ -199,6 +271,70 @@ where }) } +/// Helper function for shared functionality in KE2 MAC computation +/// for both `TripleDH` and TripleDH-KEM +pub(super) fn compute_ke2_macs( + transcript_hasher: &mut H, + derived_keys: &DerivedKeys, + transcript_digest: &[u8], +) -> Result<(Output, Output), ProtocolError> +where + H::Core: ProxyHash, + ::BlockSize: IsLess, + Le<::BlockSize, U256>: NonZero, +{ + let mut mac_hasher = + Hmac::::new_from_slice(&derived_keys.km2).map_err(|_| InternalError::HmacError)?; + Mac::update(&mut mac_hasher, transcript_digest); + let mac = mac_hasher.finalize().into_bytes(); + + transcript_hasher.update(&mac); + let finalized_transcript = transcript_hasher.clone().finalize(); + + let mut expected_mac_hasher = + Hmac::::new_from_slice(&derived_keys.km3).map_err(|_| InternalError::HmacError)?; + Mac::update(&mut expected_mac_hasher, &finalized_transcript); + let expected_mac = expected_mac_hasher.finalize().into_bytes(); + + Ok((mac, expected_mac)) +} + +/// Finalizes the KE3 transcript by deriving session material from the provided +/// shared secrets and verifying the server's MAC, returning both the derived +/// keys and the client's MAC response. Callers are expected to supply any +/// protocol-specific shared secrets (e.g. classic Diffie-Hellman results or +/// KEM outputs) as byte slices. +pub(super) fn finalize_ke3_transcript<'a, H: Hash>( + transcript_hasher: &mut H, + shared_secrets: impl Iterator, + server_mac: &Output, +) -> Result<(DerivedKeys, Output), ProtocolError> +where + H::Core: ProxyHash, + ::BlockSize: IsLess, + Le<::BlockSize, U256>: NonZero, +{ + let transcript_digest = transcript_hasher.clone().finalize(); + let derived_keys = derive_keys::(shared_secrets, &transcript_digest)?; + let mut server_mac_hasher = + Hmac::::new_from_slice(&derived_keys.km2).map_err(|_| InternalError::HmacError)?; + Mac::update(&mut server_mac_hasher, &transcript_digest); + server_mac_hasher + .verify(server_mac) + .map_err(|_| ProtocolError::InvalidLoginError)?; + + transcript_hasher.update(server_mac.as_slice()); + let finalized_transcript = transcript_hasher.clone().finalize(); + + let mut client_mac_hasher = + Hmac::::new_from_slice(&derived_keys.km3).map_err(|_| InternalError::HmacError)?; + Mac::update(&mut client_mac_hasher, &finalized_transcript); + + let client_mac = client_mac_hasher.finalize().into_bytes(); + + Ok((derived_keys, client_mac)) +} + fn hkdf_expand_label( secret: &[u8], label: &[u8], diff --git a/src/key_exchange/tripledh.rs b/src/key_exchange/tripledh.rs index 17eef64..2d1f8b5 100644 --- a/src/key_exchange/tripledh.rs +++ b/src/key_exchange/tripledh.rs @@ -13,11 +13,10 @@ use core::ops::Add; use derive_where::derive_where; use digest::core_api::BlockSizeUser; -use digest::{Digest, Mac, Output, OutputSizeUser}; +use digest::{Digest, Output, OutputSizeUser}; use generic_array::sequence::Concat; use generic_array::typenum::{IsLess, Le, NonZero, Sum, U256}; use generic_array::{ArrayLength, GenericArray}; -use hmac::Hmac; use rand::{CryptoRng, RngCore}; use subtle::{ConstantTimeEq, CtOption}; use zeroize::{Zeroize, ZeroizeOnDrop}; @@ -28,12 +27,12 @@ use super::{ SerializedIdentifiers, }; use crate::ciphersuite::{CipherSuite, KeGroup}; -use crate::errors::{InternalError, ProtocolError}; +use crate::errors::ProtocolError; use crate::hash::{Hash, OutputSize, ProxyHash}; use crate::key_exchange::group::Group; use crate::key_exchange::shared::{self, NonceLen}; pub use crate::key_exchange::shared::{DiffieHellman, Ke1Message, Ke1State}; -use crate::keypair::{KeyPair, PrivateKey, PublicKey}; +use crate::keypair::{PrivateKey, PublicKey}; use crate::opaque::Identifiers; use crate::serialization::SliceExt; @@ -62,8 +61,8 @@ pub struct TripleDh(PhantomData<(G, H)>); )] #[derive_where(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, ZeroizeOnDrop)] pub struct Ke2State { - session_key: Output, - expected_mac: Output, + pub(super) session_key: Output, + pub(super) expected_mac: Output, } /// Builder for the second key exchange message @@ -108,10 +107,10 @@ where ::BlockSize: IsLess, Le<::BlockSize, U256>: NonZero, { - server_nonce: GenericArray, + pub(super) server_nonce: GenericArray, #[derive_where(skip(Zeroize))] - server_e_pk: PublicKey, - mac: Output, + pub(super) server_e_pk: PublicKey, + pub(super) mac: Output, } /// The third key exchange message @@ -127,7 +126,7 @@ where ::BlockSize: IsLess, Le<::BlockSize, U256>: NonZero, { - mac: Output, + pub(super) mac: Output, } //////////////////////////////// @@ -169,32 +168,28 @@ where identifiers: SerializedIdentifiers<'_, KeGroup>, context: SerializedContext<'a>, ) -> Result, ProtocolError> { - let server_e = KeyPair::::derive_random(rng); - let server_nonce = shared::generate_nonce::(rng); - - let ke1_message_iter = ke1_message.to_iter(); - let server_e_pk = server_e.public().serialize(); - - let transcript_hasher = shared::transcript( - &context, - &identifiers, - &credential_request, - &ke1_message_iter, - &credential_response, + let shared::Ke2BuilderCommon { server_nonce, - &server_e_pk, - ); - - let shared_secret_1 = server_e - .private() - .ke_diffie_hellman(&ke1_message.client_e_pk); - let shared_secret_3 = server_e.private().ke_diffie_hellman(&client_s_pk); + transcript_hasher, + client_e_pk, + server_e_pk, + shared_secret_1, + shared_secret_3, + } = shared::ke2_builder_common::( + rng, + credential_request, + ke1_message, + credential_response, + client_s_pk, + identifiers, + context, + )?; Ok(Ke2Builder { server_nonce, transcript_hasher, - client_e_pk: ke1_message.client_e_pk.clone(), - server_e_pk: server_e.public().clone(), + client_e_pk, + server_e_pk, shared_secret_1, shared_secret_3, }) @@ -218,6 +213,7 @@ where mut builder: Self::KE2Builder<'_, CS>, shared_secret_2: Self::KE2BuilderInput, ) -> Result, ProtocolError> { + let transcript_digest = builder.transcript_hasher.clone().finalize(); let derived_keys = shared::derive_keys::( [ builder.shared_secret_1.as_slice(), @@ -225,25 +221,14 @@ where &builder.shared_secret_3, ] .into_iter(), - &builder.transcript_hasher.clone().finalize(), + &transcript_digest, )?; - let mut mac_hasher = - Hmac::::new_from_slice(&derived_keys.km2).map_err(|_| InternalError::HmacError)?; - Mac::update( - &mut mac_hasher, - &builder.transcript_hasher.clone().finalize(), - ); - let mac = mac_hasher.finalize().into_bytes(); - - builder.transcript_hasher.update(&mac); - let mut mac_hasher = - Hmac::::new_from_slice(&derived_keys.km3).map_err(|_| InternalError::HmacError)?; - Mac::update( - &mut mac_hasher, - &builder.transcript_hasher.clone().finalize(), - ); - let expected_mac = mac_hasher.finalize().into_bytes(); + let (mac, expected_mac) = shared::compute_ke2_macs( + &mut builder.transcript_hasher, + &derived_keys, + &transcript_digest, + )?; Ok(GenerateKe2Result { state: Ke2State { @@ -290,35 +275,20 @@ where let shared_secret_2 = ke1_state.client_e_sk.ke_diffie_hellman(&server_s_pk); let shared_secret_3 = client_s_sk.ke_diffie_hellman(&ke2_message.server_e_pk); - let derived_keys = shared::derive_keys::( + let (derived_keys, client_mac) = shared::finalize_ke3_transcript( + &mut transcript_hasher, [ shared_secret_1.as_slice(), - &shared_secret_2, - &shared_secret_3, + shared_secret_2.as_slice(), + shared_secret_3.as_slice(), ] .into_iter(), - &transcript_hasher.clone().finalize(), + &ke2_message.mac, )?; - let mut server_mac = - Hmac::::new_from_slice(&derived_keys.km2).map_err(|_| InternalError::HmacError)?; - Mac::update(&mut server_mac, &transcript_hasher.clone().finalize()); - - server_mac - .verify(&ke2_message.mac) - .map_err(|_| ProtocolError::InvalidLoginError)?; - - transcript_hasher.update(&ke2_message.mac); - - let mut client_mac = - Hmac::::new_from_slice(&derived_keys.km3).map_err(|_| InternalError::HmacError)?; - Mac::update(&mut client_mac, &transcript_hasher.finalize()); - Ok(GenerateKe3Result { session_key: derived_keys.session_key, - message: Ke3Message { - mac: client_mac.finalize().into_bytes(), - }, + message: Ke3Message { mac: client_mac }, #[cfg(test)] handshake_secret: derived_keys.handshake_secret, #[cfg(test)]