#[cfg(not(feature = "std"))]
extern crate alloc;
use crate::crypto::profiles::SecurityProfileDesc;
use crate::der::Sequence;
use crate::Beamable;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[cfg(feature = "std")]
use std::vec::Vec;
#[cfg(feature = "derive")]
use crate::Errorizable;
pub const MAX_OFFER_PROFILES: usize = 32;
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "derive", derive(Beamable, Sequence))]
pub struct SecurityOffer {
pub profiles: Vec<SecurityProfileDesc>,
}
impl SecurityOffer {
pub fn new(profiles: Vec<SecurityProfileDesc>) -> Self {
Self { profiles }
}
pub fn single(profile: SecurityProfileDesc) -> Self {
Self { profiles: Vec::from([profile]) }
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "derive", derive(Beamable, Sequence))]
pub struct SecurityAccept {
pub profile: SecurityProfileDesc,
}
impl SecurityAccept {
pub fn new(profile: SecurityProfileDesc) -> Self {
Self { profile }
}
}
#[cfg_attr(feature = "derive", derive(Errorizable))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NegotiationError {
#[cfg_attr(feature = "derive", error("No mutually supported security profile"))]
NoMutualProfile,
#[cfg_attr(feature = "derive", error("Security offer is empty"))]
EmptyOffer,
#[cfg_attr(feature = "derive", error("No profile meets the minimum-strength policy"))]
BelowStrengthFloor,
#[cfg_attr(
feature = "derive",
error("Security offer too large: {count} profiles exceeds cap of {max}")
)]
OfferTooLarge { count: usize, max: usize },
#[cfg_attr(feature = "derive", error("DER encoding error: {0}"))]
DerError(crate::der::Error),
}
crate::impl_error_display!(NegotiationError {
NoMutualProfile => "No mutually supported security profile",
EmptyOffer => "Security offer is empty",
BelowStrengthFloor => "No profile meets the minimum-strength policy",
OfferTooLarge { count, max } => "Security offer too large: {count} profiles exceeds cap of {max}",
DerError(e) => "DER encoding error: {e}",
});
impl From<crate::der::Error> for NegotiationError {
fn from(e: crate::der::Error) -> Self {
Self::DerError(e)
}
}
pub trait ProfileStrengthPolicy {
fn meets_floor(&self, profile: &SecurityProfileDesc) -> bool;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct DefaultStrengthFloor;
fn digest_bits(oid: &crate::der::asn1::ObjectIdentifier) -> u16 {
use crate::oids::{HASH_SHA256, HASH_SHA3_256, HASH_SHA3_384, HASH_SHA3_512};
if *oid == HASH_SHA256 || *oid == HASH_SHA3_256 {
return 256;
}
if *oid == HASH_SHA3_384 {
return 384;
}
if *oid == HASH_SHA3_512 {
return 512;
}
0
}
impl ProfileStrengthPolicy for DefaultStrengthFloor {
fn meets_floor(&self, profile: &SecurityProfileDesc) -> bool {
let aead_ok = matches!(profile.aead_key_size, Some(size) if size >= 32);
let digest_ok = matches!(profile.digest.as_ref(), Some(oid) if digest_bits(oid) >= 256);
aead_ok && digest_ok
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NoStrengthFloor;
impl ProfileStrengthPolicy for NoStrengthFloor {
fn meets_floor(&self, _profile: &SecurityProfileDesc) -> bool {
true
}
}
pub fn select_profile(
offer: &SecurityOffer,
supported: &[SecurityProfileDesc],
) -> Result<SecurityProfileDesc, NegotiationError> {
if offer.profiles.is_empty() {
return Err(NegotiationError::EmptyOffer);
}
if offer.profiles.len() > MAX_OFFER_PROFILES {
return Err(NegotiationError::OfferTooLarge { count: offer.profiles.len(), max: MAX_OFFER_PROFILES });
}
for candidate in supported {
if offer.profiles.contains(candidate) {
return Ok(*candidate);
}
}
Err(NegotiationError::NoMutualProfile)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::oids::{
AES_128_WRAP, AES_192_WRAP, AES_256_GCM, AES_256_WRAP, HASH_SHA3_256, SIGNER_ECDSA_WITH_SHA3_512,
};
fn mock_profile(id: u8) -> SecurityProfileDesc {
SecurityProfileDesc {
digest: Some(HASH_SHA3_256),
aead: Some(AES_256_GCM),
aead_key_size: Some(32),
signature: Some(SIGNER_ECDSA_WITH_SHA3_512),
kdf: Some(HASH_SHA3_256),
curve: Some(crate::oids::CURVE_SECP256K1),
key_wrap: match id {
1 => Some(AES_128_WRAP),
2 => Some(AES_256_WRAP),
3 => Some(AES_192_WRAP),
_ => None,
},
kem: None,
}
}
#[test]
fn test_offer_single() {
let profile = mock_profile(1);
let offer = SecurityOffer::single(profile);
assert_eq!(offer.profiles.len(), 1);
assert_eq!(offer.profiles[0], profile);
}
#[test]
fn test_select_first_mutual() -> Result<(), Box<dyn core::error::Error>> {
let p1 = mock_profile(1);
let p2 = mock_profile(2);
let p3 = mock_profile(3);
let offer = SecurityOffer::new(Vec::from([p1, p2, p3]));
let supported = [p2, p3];
let selected = select_profile(&offer, &supported)?;
assert_eq!(selected, p2);
Ok(())
}
#[test]
fn test_select_follows_server_preference_not_client_order() -> Result<(), Box<dyn core::error::Error>> {
let p1 = mock_profile(1);
let p2 = mock_profile(2);
let offer = SecurityOffer::new(Vec::from([p1, p2]));
let supported = [p2, p1];
let selected = select_profile(&offer, &supported)?;
assert_eq!(selected, p2);
Ok(())
}
#[test]
fn test_no_mutual_profile() {
let p1 = mock_profile(1);
let p2 = mock_profile(2);
let p3 = mock_profile(3);
let offer = SecurityOffer::new(Vec::from([p1, p2]));
let supported = [p3];
let result = select_profile(&offer, &supported);
assert!(matches!(result, Err(NegotiationError::NoMutualProfile)));
}
#[test]
fn test_oversized_offer_rejected() {
let profile = mock_profile(1);
let offer = SecurityOffer::new(vec![profile; MAX_OFFER_PROFILES + 1]);
let supported = [profile];
let result = select_profile(&offer, &supported);
assert!(matches!(result, Err(NegotiationError::OfferTooLarge { count: 33, max: 32 })));
}
#[test]
fn test_empty_offer() {
let offer = SecurityOffer::new(Vec::new());
let supported = [mock_profile(1)];
let result = select_profile(&offer, &supported);
assert!(matches!(result, Err(NegotiationError::EmptyOffer)));
}
#[cfg(feature = "aead")]
#[test]
fn test_select_profile_multiple_aead_ciphers() -> Result<(), Box<dyn core::error::Error>> {
use crate::oids::{
AES_128_GCM, AES_128_WRAP, AES_256_GCM, AES_256_WRAP, CURVE_SECP256K1, HASH_SHA256,
SIGNER_ECDSA_WITH_SHA256,
};
let aes128_gcm = SecurityProfileDesc {
digest: Some(HASH_SHA256),
aead: Some(AES_128_GCM),
aead_key_size: Some(16),
signature: Some(SIGNER_ECDSA_WITH_SHA256),
kdf: Some(HASH_SHA256),
curve: Some(CURVE_SECP256K1),
key_wrap: Some(AES_128_WRAP),
kem: None,
};
let aes256_gcm = SecurityProfileDesc {
digest: Some(HASH_SHA256),
aead: Some(AES_256_GCM),
aead_key_size: Some(32),
signature: Some(SIGNER_ECDSA_WITH_SHA256),
kdf: Some(HASH_SHA256),
curve: Some(CURVE_SECP256K1),
key_wrap: Some(AES_256_WRAP),
kem: None,
};
let client_offer = SecurityOffer::new(Vec::from([aes128_gcm, aes256_gcm]));
let server_supported = [aes256_gcm, aes128_gcm];
let selected = select_profile(&client_offer, &server_supported)?;
assert_eq!(selected.aead, Some(AES_256_GCM));
assert_eq!(selected.aead_key_size, Some(32));
let client_offer_256 = SecurityOffer::new(Vec::from([aes256_gcm, aes128_gcm]));
let selected_256 = select_profile(&client_offer_256, &server_supported)?;
assert_eq!(selected_256.aead, Some(AES_256_GCM));
assert_eq!(selected_256.aead_key_size, Some(32));
let server_128_first = [aes128_gcm, aes256_gcm];
let selected_128 = select_profile(&client_offer_256, &server_128_first)?;
assert_eq!(selected_128.aead, Some(AES_128_GCM));
assert_eq!(selected_128.aead_key_size, Some(16));
let server_256_only = [aes256_gcm];
let selected_fallback = select_profile(&client_offer, &server_256_only)?;
assert_eq!(selected_fallback.aead, Some(AES_256_GCM));
assert_eq!(selected_fallback.aead_key_size, Some(32));
Ok(())
}
}