use crate::derec_message::{DeRecMessageBuilder, current_timestamp};
use crate::primitives::pairing::PairingError;
use crate::protocol_version::ProtocolVersion;
use crate::types::ChannelId;
use crate::utils::verify_timestamps;
use derec_cryptography::pairing::{
self as cryptography_pairing, PairingSecretKeyMaterial, PairingSharedKey,
};
use derec_proto::{
CommunicationInfo, ContactMessage, ContactMode, DeRecMessage, DeRecResult, MessageBody,
PairRequestMessage, PairResponseMessage, PrePairRequestMessage, PrePairResponseMessage,
StatusEnum, TransportProtocol,
};
use prost::Message;
use sha2::{Digest, Sha384};
use subtle::ConstantTimeEq;
pub struct ProduceResult {
pub envelope: Vec<u8>,
pub peer_transport_protocol: TransportProtocol,
pub shared_key: PairingSharedKey,
pub channel_id: ChannelId,
}
pub struct ExtractResult {
pub response: PairResponseMessage,
}
pub struct ProcessResult {
pub shared_key: PairingSharedKey,
pub channel_id: ChannelId,
}
pub struct ProducePrePairResult {
pub envelope: Vec<u8>,
}
pub struct ProducePrePairNoKeysResult {
pub envelope: Vec<u8>,
pub pairing_secret_key_material: PairingSecretKeyMaterial,
}
pub struct PrePairExtractResult {
pub response: PrePairResponseMessage,
}
pub struct ProcessPrePairResult {
pub mlkem_encapsulation_key: Vec<u8>,
pub ecies_public_key: Vec<u8>,
pub nonce: u64,
}
#[cfg_attr(
feature = "logging",
tracing::instrument(skip_all, fields(channel_id = channel_id.0))
)]
pub fn produce(
channel_id: ChannelId,
request: &PairRequestMessage,
pairing_secret_key_material: &PairingSecretKeyMaterial,
communication_info: Option<CommunicationInfo>,
parameter_range: Option<derec_proto::ParameterRange>,
) -> Result<ProduceResult, crate::Error> {
validate_produce_inputs(request)?;
let peer_transport_protocol = extract_peer_transport_protocol(request)?;
let pairing_request = cryptography_pairing::PairingRequestMessageMaterial {
mlkem_ciphertext: request.mlkem_ciphertext.clone(),
ecies_public_key: request.ecies_public_key.clone(),
};
let initiator_material = match pairing_secret_key_material {
cryptography_pairing::PairingSecretKeyMaterial::Initiator(m) => m,
_ => {
return Err(PairingError::Invariant(
"expected Initiator key material for pairing response",
)
.into());
}
};
let shared_key =
cryptography_pairing::finish_pairing_initiator(initiator_material, &pairing_request)
.map_err(|e| PairingError::FinishPairingInitiator { source: e })?;
let rekeyed_channel_id = derive_rekeyed_channel_id(channel_id, &shared_key);
let timestamp = current_timestamp();
let response = PairResponseMessage {
result: Some(DeRecResult {
status: StatusEnum::Ok as i32,
memo: String::new(),
}),
nonce: request.nonce,
communication_info,
parameter_range,
timestamp: Some(timestamp),
channel_id: rekeyed_channel_id.into(),
};
let envelope = DeRecMessageBuilder::pairing()
.channel_id(channel_id)
.timestamp(timestamp)
.message_body(MessageBody::PairResponse(response))
.encrypt_pairing(&request.ecies_public_key)?
.build()?
.encode_to_vec();
#[cfg(feature = "logging")]
tracing::info!("pairing response envelope produced; initiator shared key derived");
Ok(ProduceResult {
envelope,
shared_key,
peer_transport_protocol,
channel_id: rekeyed_channel_id,
})
}
#[cfg_attr(
feature = "logging",
tracing::instrument(skip_all, fields(channel_id = channel_id.0))
)]
pub fn produce_pre_pair(
channel_id: ChannelId,
request: &PrePairRequestMessage,
pairing_secret_key_material: &PairingSecretKeyMaterial,
) -> Result<ProducePrePairResult, crate::Error> {
let initiator_material = match pairing_secret_key_material {
PairingSecretKeyMaterial::Initiator(m) => m,
_ => {
#[cfg(feature = "logging")]
tracing::warn!("expected Initiator key material for PrePair response");
return Err(PairingError::Invariant(
"expected Initiator key material for PrePair response",
)
.into());
}
};
let timestamp = current_timestamp();
let response = PrePairResponseMessage {
result: Some(DeRecResult {
status: StatusEnum::Ok as i32,
memo: String::new(),
}),
mlkem_encapsulation_key: Some(initiator_material.mlkem_encapsulation_key.clone()),
ecies_public_key: Some(initiator_material.ecies_public_key.clone()),
nonce: request.nonce,
timestamp: Some(timestamp),
};
let protocol_version = ProtocolVersion::current();
let envelope = DeRecMessage {
protocol_version_major: protocol_version.major,
protocol_version_minor: protocol_version.minor,
sequence: 0,
channel_id: channel_id.into(),
timestamp: Some(timestamp),
message: MessageBody::PrePairResponse(response).encode_to_vec(),
trace_id: 0,
}
.encode_to_vec();
#[cfg(feature = "logging")]
tracing::info!("PrePair response envelope produced");
Ok(ProducePrePairResult { envelope })
}
#[cfg_attr(
feature = "logging",
tracing::instrument(skip_all, fields(channel_id = channel_id.0))
)]
pub fn produce_pre_pair_no_keys(
channel_id: ChannelId,
request: &PrePairRequestMessage,
) -> Result<ProducePrePairNoKeysResult, crate::Error> {
let seed = crate::utils::generate_seed::<32>();
let (pk, secret_key) = cryptography_pairing::contact_message(*seed)
.map_err(|e| PairingError::ContactMessageKeygen { source: e })?;
let timestamp = current_timestamp();
let response = PrePairResponseMessage {
result: Some(DeRecResult {
status: StatusEnum::Ok as i32,
memo: String::new(),
}),
mlkem_encapsulation_key: Some(pk.mlkem_encapsulation_key.clone()),
ecies_public_key: Some(pk.ecies_public_key.clone()),
nonce: request.nonce,
timestamp: Some(timestamp),
};
let protocol_version = ProtocolVersion::current();
let envelope = DeRecMessage {
protocol_version_major: protocol_version.major,
protocol_version_minor: protocol_version.minor,
sequence: 0,
channel_id: channel_id.into(),
timestamp: Some(timestamp),
message: MessageBody::PrePairResponse(response).encode_to_vec(),
trace_id: 0,
}
.encode_to_vec();
#[cfg(feature = "logging")]
tracing::info!("NoKeys PrePair response envelope produced with fresh key material");
Ok(ProducePrePairNoKeysResult {
envelope,
pairing_secret_key_material: PairingSecretKeyMaterial::Initiator(secret_key),
})
}
#[cfg_attr(
feature = "logging",
tracing::instrument(skip_all, fields(envelope_len = envelope_bytes.len()))
)]
pub fn extract(
envelope_bytes: &[u8],
ecies_secret_key: &[u8],
) -> Result<ExtractResult, crate::Error> {
let envelope = DeRecMessage::decode(envelope_bytes).map_err(crate::Error::ProtobufDecode)?;
let plaintext =
derec_cryptography::pairing::envelope::decrypt(&envelope.message, ecies_secret_key)
.map_err(PairingError::PairingEncryption)?;
let response = match MessageBody::decode_from_vec(plaintext.as_slice())
.map_err(crate::Error::ProtobufDecode)?
{
MessageBody::PairResponse(r) => r,
_ => {
#[cfg(feature = "logging")]
tracing::warn!("unexpected message type; expected PairResponseMessage");
return Err(crate::Error::Invariant(
"Invalid message. Expected: PairResponseMessage",
));
}
};
verify_timestamps(envelope.timestamp, response.timestamp)?;
#[cfg(feature = "logging")]
tracing::info!("pairing response extracted and validated");
Ok(ExtractResult { response })
}
#[cfg_attr(
feature = "logging",
tracing::instrument(skip_all, fields(envelope_len = envelope_bytes.len()))
)]
pub fn extract_pre_pair(envelope_bytes: &[u8]) -> Result<PrePairExtractResult, crate::Error> {
let envelope = DeRecMessage::decode(envelope_bytes).map_err(crate::Error::ProtobufDecode)?;
let response = match crate::derec_message::extract_inner_plaintext_message(&envelope.message)? {
MessageBody::PrePairResponse(r) => r,
_ => {
#[cfg(feature = "logging")]
tracing::warn!("unexpected message type; expected PrePairResponseMessage");
return Err(crate::Error::Invariant(
"Invalid message. Expected: PrePairResponseMessage",
));
}
};
verify_timestamps(envelope.timestamp, response.timestamp)?;
#[cfg(feature = "logging")]
tracing::info!("PrePair response envelope decoded and validated");
Ok(PrePairExtractResult { response })
}
#[cfg_attr(
feature = "logging",
tracing::instrument(skip_all, fields(channel_id = contact_message.channel_id))
)]
pub fn process(
contact_message: &ContactMessage,
response: &PairResponseMessage,
pairing_secret_key_material: &PairingSecretKeyMaterial,
) -> Result<ProcessResult, crate::Error> {
let responder_material =
validate_process_inputs(contact_message, response, pairing_secret_key_material)?;
let pairing_contact_key_material = build_pairing_contact_material(contact_message);
let shared_key = cryptography_pairing::finish_pairing_responder(
responder_material,
&pairing_contact_key_material,
)
.map_err(|e| PairingError::FinishPairingResponder { source: e })?;
let expected_channel_id =
validate_rekeyed_channel_id(response, ChannelId(contact_message.channel_id), &shared_key)?;
#[cfg(feature = "logging")]
tracing::info!("pairing complete; responder shared key derived");
Ok(ProcessResult {
shared_key,
channel_id: expected_channel_id,
})
}
#[cfg_attr(
feature = "logging",
tracing::instrument(skip_all, fields(channel_id = contact_message.channel_id))
)]
pub fn process_pre_pair(
contact_message: &ContactMessage,
response: &PrePairResponseMessage,
) -> Result<ProcessPrePairResult, crate::Error> {
validate_process_pre_pair_inputs(contact_message, response, ContactMode::HashedKeys)?;
let (mlkem_encapsulation_key, ecies_public_key) =
validate_contact_binding_hash(contact_message, response)?;
#[cfg(feature = "logging")]
tracing::info!("PrePair response validated against contact binding hash");
Ok(ProcessPrePairResult {
mlkem_encapsulation_key,
ecies_public_key,
nonce: response.nonce,
})
}
#[cfg_attr(
feature = "logging",
tracing::instrument(skip_all, fields(channel_id = contact_message.channel_id))
)]
pub fn process_pre_pair_no_keys(
contact_message: &ContactMessage,
response: &PrePairResponseMessage,
) -> Result<ProcessPrePairResult, crate::Error> {
validate_process_pre_pair_inputs(contact_message, response, ContactMode::NoKeys)?;
let mlkem_encapsulation_key = response
.mlkem_encapsulation_key
.clone()
.expect("validate_process_pre_pair_inputs guarantees mlkem_encapsulation_key is Some");
let ecies_public_key = response
.ecies_public_key
.clone()
.expect("validate_process_pre_pair_inputs guarantees ecies_public_key is Some");
#[cfg(feature = "logging")]
tracing::info!("NoKeys PrePair response accepted without hash verification");
Ok(ProcessPrePairResult {
mlkem_encapsulation_key,
ecies_public_key,
nonce: response.nonce,
})
}
fn validate_produce_inputs(request: &PairRequestMessage) -> Result<(), crate::Error> {
if request.mlkem_ciphertext.is_empty() {
#[cfg(feature = "logging")]
tracing::warn!("pair request missing mlkem_ciphertext");
return Err(PairingError::InvalidPairRequestMessage("mlkem_ciphertext is empty").into());
}
if request.ecies_public_key.is_empty() {
#[cfg(feature = "logging")]
tracing::warn!("pair request missing ecies_public_key");
return Err(PairingError::InvalidPairRequestMessage("ecies_public_key is empty").into());
}
Ok(())
}
fn extract_peer_transport_protocol(
request: &PairRequestMessage,
) -> Result<TransportProtocol, crate::Error> {
let peer_transport_protocol = request
.transport_protocol
.clone()
.ok_or(PairingError::EmptyTransportUri)?;
if peer_transport_protocol.uri.trim().is_empty() {
#[cfg(feature = "logging")]
tracing::warn!("peer transport URI is empty");
return Err(PairingError::EmptyTransportUri.into());
}
let _ = crate::transport::TransportProtocol::try_from(&peer_transport_protocol)?;
Ok(peer_transport_protocol)
}
fn validate_process_inputs<'a>(
contact_message: &ContactMessage,
response: &PairResponseMessage,
pairing_secret_key_material: &'a PairingSecretKeyMaterial,
) -> Result<&'a cryptography_pairing::ResponderSecretKeyMaterial, crate::Error> {
let res = response
.result
.as_ref()
.ok_or(PairingError::InvalidPairResponseMessage("missing result"))?;
if res.status != StatusEnum::Ok as i32 {
#[cfg(feature = "logging")]
tracing::warn!(status = res.status, memo = %res.memo, "pair response status is not Ok");
return Err(PairingError::NonOkStatus {
status: res.status,
memo: res.memo.to_owned(),
}
.into());
}
let mlkem_present = contact_message
.mlkem_encapsulation_key
.as_ref()
.is_some_and(|v| !v.is_empty());
if !mlkem_present {
#[cfg(feature = "logging")]
tracing::warn!("contact message missing mlkem_encapsulation_key");
return Err(PairingError::InvalidContactMessage("mlkem_encapsulation_key is empty").into());
}
let ecies_present = contact_message
.ecies_public_key
.as_ref()
.is_some_and(|v| !v.is_empty());
if !ecies_present {
#[cfg(feature = "logging")]
tracing::warn!("contact message missing ecies_public_key");
return Err(PairingError::InvalidContactMessage("ecies_public_key is empty").into());
}
if response.nonce != contact_message.nonce {
#[cfg(feature = "logging")]
tracing::warn!("nonce mismatch; possible replay or wrong pairing session");
return Err(PairingError::ProtocolViolation("nonce mismatch").into());
}
match pairing_secret_key_material {
PairingSecretKeyMaterial::Responder(m) => Ok(m),
PairingSecretKeyMaterial::Initiator(_) => Err(PairingError::Invariant(
"expected Responder key material for pairing process",
)
.into()),
}
}
fn build_pairing_contact_material(
contact_message: &ContactMessage,
) -> cryptography_pairing::PairingContactMessageMaterial {
let mlkem_encapsulation_key = contact_message
.mlkem_encapsulation_key
.as_ref()
.expect("validate_process_inputs guarantees mlkem_encapsulation_key is Some")
.clone();
let ecies_public_key = contact_message
.ecies_public_key
.as_ref()
.expect("validate_process_inputs guarantees ecies_public_key is Some")
.clone();
cryptography_pairing::PairingContactMessageMaterial {
mlkem_encapsulation_key,
ecies_public_key,
}
}
fn validate_rekeyed_channel_id(
response: &PairResponseMessage,
original_channel_id: ChannelId,
shared_key: &PairingSharedKey,
) -> Result<ChannelId, crate::Error> {
let expected_channel_id = derive_rekeyed_channel_id(original_channel_id, shared_key);
if response.channel_id != u64::from(expected_channel_id) {
#[cfg(feature = "logging")]
tracing::warn!(
advertised = response.channel_id,
expected = u64::from(expected_channel_id),
"channel id rekey mismatch"
);
return Err(PairingError::ProtocolViolation("channel_id rekey mismatch").into());
}
Ok(expected_channel_id)
}
fn validate_process_pre_pair_inputs(
contact_message: &ContactMessage,
response: &PrePairResponseMessage,
expected_mode: ContactMode,
) -> Result<(), crate::Error> {
let res = response
.result
.as_ref()
.ok_or(PairingError::InvalidPairResponseMessage("missing result"))?;
if res.status != StatusEnum::Ok as i32 {
#[cfg(feature = "logging")]
tracing::warn!(
status = res.status,
memo = %res.memo,
"PrePair response status is not Ok"
);
return Err(PairingError::NonOkStatus {
status: res.status,
memo: res.memo.to_owned(),
}
.into());
}
super::validate_contact_for_mode(contact_message, expected_mode)?;
let mlkem_present = response
.mlkem_encapsulation_key
.as_ref()
.is_some_and(|v| !v.is_empty());
if !mlkem_present {
#[cfg(feature = "logging")]
tracing::warn!("PrePair response missing mlkem_encapsulation_key");
return Err(
PairingError::InvalidPairResponseMessage("mlkem_encapsulation_key is empty").into(),
);
}
let ecies_present = response
.ecies_public_key
.as_ref()
.is_some_and(|v| !v.is_empty());
if !ecies_present {
#[cfg(feature = "logging")]
tracing::warn!("PrePair response missing ecies_public_key");
return Err(PairingError::InvalidPairResponseMessage("ecies_public_key is empty").into());
}
if response.nonce != contact_message.nonce {
#[cfg(feature = "logging")]
tracing::warn!("nonce mismatch; possible replay or wrong pairing session");
return Err(PairingError::ProtocolViolation("nonce mismatch").into());
}
Ok(())
}
fn derive_rekeyed_channel_id(
original_channel_id: ChannelId,
shared_key: &PairingSharedKey,
) -> ChannelId {
let mut hasher = Sha384::new();
hasher.update(u64::from(original_channel_id).to_be_bytes());
hasher.update(shared_key.as_slice());
let digest = hasher.finalize();
let prefix: [u8; 8] = digest[..8]
.try_into()
.expect("SHA-384 digest has at least 8 bytes");
ChannelId(u64::from_be_bytes(prefix))
}
fn validate_contact_binding_hash(
contact_message: &ContactMessage,
response: &PrePairResponseMessage,
) -> Result<(Vec<u8>, Vec<u8>), crate::Error> {
let mlkem_encapsulation_key = response
.mlkem_encapsulation_key
.clone()
.expect("validate_process_pre_pair_inputs guarantees mlkem_encapsulation_key is Some");
let ecies_public_key = response
.ecies_public_key
.clone()
.expect("validate_process_pre_pair_inputs guarantees ecies_public_key is Some");
let expected_hash = contact_message
.contact_binding_hash
.as_ref()
.expect("validate_process_pre_pair_inputs guarantees contact_binding_hash is Some");
let recomputed = derec_cryptography::pairing::contact_binding_hash(
&mlkem_encapsulation_key,
&ecies_public_key,
contact_message.nonce,
contact_message.channel_id,
);
let matched: bool = recomputed.as_slice().ct_eq(expected_hash.as_slice()).into();
if !matched {
#[cfg(feature = "logging")]
tracing::warn!(
"contact binding hash mismatch; published keys do not match the contact commitment"
);
return Err(PairingError::PrePairHashMismatch.into());
}
Ok((mlkem_encapsulation_key, ecies_public_key))
}