use crate::constants::{MIN_SALT_ENTROPY_BYTES, TIGHTBEAM_SESSION_KDF_INFO};
use crate::crypto::aead::KeyInit;
use crate::crypto::kdf::KdfFunction;
use crate::crypto::profiles::{CryptoProvider, SecurityProfileDesc};
use crate::crypto::x509::attr::{Attribute, Attributes};
use crate::der::asn1::ObjectIdentifier;
use crate::oids::HANDSHAKE_ABORT_ALERT;
use crate::oids::{AES_128_GCM, AES_256_GCM};
use crate::transport::handshake::attributes::{extract_alert_x509, find_x509};
use crate::transport::handshake::error::HandshakeError;
use crate::transport::handshake::negotiation::{
select_profile, DefaultStrengthFloor, NegotiationError, ProfileStrengthPolicy, SecurityOffer,
};
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
pub trait HandshakeNegotiation {
fn supported_profiles(&self) -> &[SecurityProfileDesc];
fn strength_policy(&self) -> &dyn ProfileStrengthPolicy {
&DefaultStrengthFloor
}
fn negotiate_profile(&self, offer: Option<&SecurityOffer>) -> Result<SecurityProfileDesc, HandshakeError> {
let supported = self.supported_profiles();
if supported.is_empty() {
return Err(HandshakeError::NoSupportedProfiles);
}
let policy = self.strength_policy();
let eligible: Vec<SecurityProfileDesc> = supported
.iter()
.filter(|profile| policy.meets_floor(profile))
.copied()
.collect();
if eligible.is_empty() {
return Err(NegotiationError::BelowStrengthFloor.into());
}
match offer {
Some(offer) => Ok(select_profile(offer, &eligible)?),
None => Ok(eligible[0]), }
}
}
fn aead_key_size_from_oid(oid: ObjectIdentifier) -> Result<usize, HandshakeError> {
if oid == AES_128_GCM {
Ok(16)
} else if oid == AES_256_GCM {
Ok(32)
} else {
Err(HandshakeError::UnsupportedAeadAlgorithm)
}
}
pub trait HandshakeFinalization<P>
where
P: CryptoProvider,
{
fn selected_profile(&self) -> Option<SecurityProfileDesc>;
fn derive_session_aead(&self, input_key: &[u8], salt: &[u8]) -> Result<P::AeadCipher, HandshakeError>
where
P::AeadCipher: KeyInit,
{
let profile = self.selected_profile().ok_or(HandshakeError::InvalidState)?;
let aead_oid = profile.aead.ok_or(HandshakeError::InvalidState)?;
let key_size = usize::from(profile.aead_key_size.ok_or(HandshakeError::InvalidState)?);
let expected = aead_key_size_from_oid(aead_oid)?;
if key_size != expected {
return Err(HandshakeError::AeadKeySizeMismatch { declared: key_size, expected });
}
if salt.len() < MIN_SALT_ENTROPY_BYTES {
return Err(HandshakeError::InsufficientSaltEntropy {
actual: salt.len(),
minimum: MIN_SALT_ENTROPY_BYTES,
});
}
let final_key_bytes = P::Kdf::derive_dynamic_key(input_key, TIGHTBEAM_SESSION_KDF_INFO, Some(salt), key_size)?;
Ok(P::AeadCipher::new_from_slice(&final_key_bytes[..])?)
}
}
pub trait HandshakeAlertHandler {
fn check_for_alert(&self, attrs: Option<&Attributes>) -> Result<(), HandshakeError> {
if let Some(attrs) = attrs {
let attr_refs: Vec<&Attribute> = attrs.iter().collect();
if let Ok(alert_attr) = find_x509(&attr_refs, &HANDSHAKE_ABORT_ALERT) {
let alert = extract_alert_x509(alert_attr)?;
return Err(HandshakeError::AbortReceived(alert));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::profiles::DefaultCryptoProvider;
use crate::der::asn1::ObjectIdentifier;
use crate::oids::{AES_128_GCM, AES_256_GCM, CURVE_SECP256K1, HASH_SHA256, SIGNER_ECDSA_WITH_SHA256};
use crate::transport::handshake::negotiation::NegotiationError;
struct MockServer {
profiles: Vec<SecurityProfileDesc>,
}
impl HandshakeNegotiation for MockServer {
fn supported_profiles(&self) -> &[SecurityProfileDesc] {
&self.profiles
}
}
struct MockClient {
profile: Option<SecurityProfileDesc>,
}
impl<P> HandshakeFinalization<P> for MockClient
where
P: CryptoProvider,
{
fn selected_profile(&self) -> Option<SecurityProfileDesc> {
self.profile
}
}
fn create_test_profile(aead_oid: ObjectIdentifier, key_size: u16) -> SecurityProfileDesc {
SecurityProfileDesc {
digest: Some(HASH_SHA256),
aead: Some(aead_oid),
aead_key_size: Some(key_size),
signature: Some(SIGNER_ECDSA_WITH_SHA256),
kdf: Some(HASH_SHA256),
curve: Some(CURVE_SECP256K1),
key_wrap: None,
kem: None,
}
}
#[test]
fn test_negotiate_profile_with_offer_enforces_floor() -> Result<(), Box<dyn std::error::Error>> {
let p_a = create_test_profile(AES_128_GCM, 16);
let p_b = create_test_profile(AES_256_GCM, 32);
let server = MockServer { profiles: vec![p_a, p_b] };
let offer = SecurityOffer::new(vec![p_a, p_b]);
let selected = server.negotiate_profile(Some(&offer))?;
assert_eq!(selected.aead_key_size, Some(32));
Ok(())
}
#[test]
fn test_negotiate_profile_dealers_choice_skips_below_floor() -> Result<(), Box<dyn std::error::Error>> {
let p_a = create_test_profile(AES_128_GCM, 16);
let p_b = create_test_profile(AES_256_GCM, 32);
let server = MockServer { profiles: vec![p_a, p_b] };
let selected = server.negotiate_profile(None)?;
assert_eq!(selected.aead_key_size, Some(32));
Ok(())
}
#[test]
fn test_negotiate_profile_all_below_floor() {
let p_a = create_test_profile(AES_128_GCM, 16);
let server = MockServer { profiles: vec![p_a] };
let offer = SecurityOffer::new(vec![p_a]);
let result = server.negotiate_profile(Some(&offer));
assert!(matches!(
result,
Err(HandshakeError::NegotiationError(NegotiationError::BelowStrengthFloor))
));
}
#[test]
fn test_negotiate_profile_no_supported() {
let server = MockServer { profiles: vec![] };
let result = server.negotiate_profile(None);
assert!(matches!(result, Err(HandshakeError::NoSupportedProfiles)));
}
#[test]
fn test_derive_session_aead_success() {
let profile = create_test_profile(AES_256_GCM, 32);
let client = MockClient { profile: Some(profile) };
let input_key = [0x42u8; 32];
let salt = [0x99u8; 32];
let result = <MockClient as HandshakeFinalization<DefaultCryptoProvider>>::derive_session_aead(
&client, &input_key, &salt,
);
assert!(result.is_ok());
}
#[test]
fn test_derive_session_aead_insufficient_salt() {
let profile = create_test_profile(AES_256_GCM, 32);
let client = MockClient { profile: Some(profile) };
let input_key = [0x42u8; 32];
let salt = [0x99u8; 8];
let result = <MockClient as HandshakeFinalization<DefaultCryptoProvider>>::derive_session_aead(
&client, &input_key, &salt,
);
assert!(matches!(
result,
Err(HandshakeError::InsufficientSaltEntropy { actual: 8, minimum: 16 })
));
}
#[test]
fn test_derive_session_aead_rejects_key_size_oid_mismatch() {
let profile = create_test_profile(AES_256_GCM, 16);
let client = MockClient { profile: Some(profile) };
let input_key = [0x42u8; 32];
let salt = [0x99u8; 32];
let result = <MockClient as HandshakeFinalization<DefaultCryptoProvider>>::derive_session_aead(
&client, &input_key, &salt,
);
assert!(matches!(
result,
Err(HandshakeError::AeadKeySizeMismatch { declared: 16, expected: 32 })
));
}
#[test]
fn test_derive_session_aead_rejects_unknown_aead_oid() {
let profile = create_test_profile(HASH_SHA256, 32);
let client = MockClient { profile: Some(profile) };
let input_key = [0x42u8; 32];
let salt = [0x99u8; 32];
let result = <MockClient as HandshakeFinalization<DefaultCryptoProvider>>::derive_session_aead(
&client, &input_key, &salt,
);
assert!(matches!(result, Err(HandshakeError::UnsupportedAeadAlgorithm)));
}
#[test]
fn test_derive_session_aead_no_profile() {
let client = MockClient { profile: None };
let input_key = [0x42u8; 32];
let salt = [0x99u8; 32];
let result = <MockClient as HandshakeFinalization<DefaultCryptoProvider>>::derive_session_aead(
&client, &input_key, &salt,
);
assert!(matches!(result, Err(HandshakeError::InvalidState)));
}
}