use super::algorithms::IntegrityAlgorithm;
use super::SecurityAssociation;
use crate::{CrafterError, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SealOutput {
pub ciphertext: Vec<u8>,
pub icv: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IvRequirement {
pub iv_len: usize,
pub iv_required: bool,
}
pub fn iv_requirement(sa: &SecurityAssociation) -> IvRequirement {
let iv_len = sa.enc.iv_len();
IvRequirement {
iv_len,
iv_required: iv_len != 0,
}
}
pub fn seal(
sa: &SecurityAssociation,
iv: &[u8],
aad: &[u8],
plaintext: &[u8],
) -> Result<SealOutput> {
if sa.enc.is_aead() {
let transform = sa.enc.aead_transform()?;
let nonce = aead_nonce(sa, iv);
let (ciphertext, icv) = transform.seal(&sa.enc_key, &nonce, aad, plaintext)?;
Ok(SealOutput { ciphertext, icv })
} else {
let cipher = sa.enc.cipher_transform()?;
let cipher_key = cipher_key(sa);
let ciphertext = cipher.encrypt(&cipher_key, iv, plaintext)?;
let icv = compute_integrity(sa, iv, aad, &ciphertext)?;
Ok(SealOutput { ciphertext, icv })
}
}
pub fn open(
sa: &SecurityAssociation,
iv: &[u8],
aad: &[u8],
ciphertext: &[u8],
icv: &[u8],
) -> Result<Vec<u8>> {
if sa.enc.is_aead() {
let transform = sa.enc.aead_transform()?;
let nonce = aead_nonce(sa, iv);
transform.open(&sa.enc_key, &nonce, aad, ciphertext, icv)
} else {
verify_integrity(sa, iv, aad, ciphertext, icv)?;
let cipher = sa.enc.cipher_transform()?;
let cipher_key = cipher_key(sa);
cipher.decrypt(&cipher_key, iv, ciphertext)
}
}
fn aead_nonce(sa: &SecurityAssociation, iv: &[u8]) -> Vec<u8> {
let mut nonce = Vec::with_capacity(sa.salt.len() + iv.len());
nonce.extend_from_slice(&sa.salt);
nonce.extend_from_slice(iv);
nonce
}
fn cipher_key(sa: &SecurityAssociation) -> Vec<u8> {
if matches!(sa.enc, super::EncryptionAlgorithm::AesCtr) {
let mut key = Vec::with_capacity(sa.enc_key.len() + sa.salt.len());
key.extend_from_slice(&sa.enc_key);
key.extend_from_slice(&sa.salt);
key
} else {
sa.enc_key.clone()
}
}
fn integrity_input(iv: &[u8], aad: &[u8], ciphertext: &[u8]) -> Vec<u8> {
let mut input = Vec::with_capacity(aad.len() + iv.len() + ciphertext.len());
input.extend_from_slice(aad);
input.extend_from_slice(iv);
input.extend_from_slice(ciphertext);
input
}
fn integrity_key(sa: &SecurityAssociation, iv: &[u8]) -> Vec<u8> {
if matches!(sa.integ, IntegrityAlgorithm::AesGmac) {
let mut key = Vec::with_capacity(sa.integ_key.len() + sa.salt.len() + iv.len());
key.extend_from_slice(&sa.integ_key);
key.extend_from_slice(&sa.salt);
key.extend_from_slice(iv);
key
} else {
sa.integ_key.clone()
}
}
fn compute_integrity(
sa: &SecurityAssociation,
iv: &[u8],
aad: &[u8],
ciphertext: &[u8],
) -> Result<Vec<u8>> {
match sa.integ {
IntegrityAlgorithm::None => Ok(Vec::new()),
_ => {
let transform = sa.integ.integrity_transform()?;
let key = integrity_key(sa, iv);
let input = integrity_input(iv, aad, ciphertext);
transform.compute(&key, &input)
}
}
}
fn verify_integrity(
sa: &SecurityAssociation,
iv: &[u8],
aad: &[u8],
ciphertext: &[u8],
icv: &[u8],
) -> Result<()> {
match sa.integ {
IntegrityAlgorithm::None => {
if icv.is_empty() {
Ok(())
} else {
Err(icv_mismatch())
}
}
_ => {
let transform = sa.integ.integrity_transform()?;
let key = integrity_key(sa, iv);
let input = integrity_input(iv, aad, ciphertext);
if transform.verify(&key, &input, icv)? {
Ok(())
} else {
Err(icv_mismatch())
}
}
}
}
const fn icv_mismatch() -> CrafterError {
CrafterError::invalid_field_value("ipsec.sa.icv", "integrity check failed: ICV did not verify")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocols::ipsec::sa::{EncryptionAlgorithm, IntegrityAlgorithm};
fn aes_key() -> Vec<u8> {
vec![0x11u8; 16]
}
fn chacha_key() -> Vec<u8> {
vec![0x22u8; 32]
}
fn iv8() -> Vec<u8> {
vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]
}
fn iv16() -> Vec<u8> {
(0u8..16).collect()
}
fn aad() -> Vec<u8> {
vec![0x01, 0x02, 0x03, 0x04, 0x00, 0x00, 0x00, 0x07]
}
fn round_trip_and_tamper(sa: &SecurityAssociation, iv: &[u8], plaintext: &[u8]) {
let sealed = seal(sa, iv, &aad(), plaintext).unwrap();
let opened = open(sa, iv, &aad(), &sealed.ciphertext, &sealed.icv).unwrap();
assert_eq!(opened, plaintext, "seal/open must round-trip the plaintext");
if !sealed.icv.is_empty() {
let mut bad_icv = sealed.icv.clone();
bad_icv[0] ^= 0x01;
assert!(
open(sa, iv, &aad(), &sealed.ciphertext, &bad_icv).is_err(),
"a tampered ICV must make open() error"
);
}
if !sealed.ciphertext.is_empty() {
let mut bad_ct = sealed.ciphertext.clone();
bad_ct[0] ^= 0x01;
assert!(
open(sa, iv, &aad(), &bad_ct, &sealed.icv).is_err(),
"a tampered ciphertext must make open() error"
);
}
}
#[test]
fn aes_gcm_round_trip_and_tamper() {
let sa = SecurityAssociation::new(0x0102_0304)
.encryption(EncryptionAlgorithm::AesGcm16, aes_key())
.salt(vec![0xAA, 0xBB, 0xCC, 0xDD]);
assert!(sa.validate().is_ok());
round_trip_and_tamper(&sa, &iv8(), b"AES-GCM-16 ESP plaintext payload!");
}
#[test]
fn chacha20_poly1305_round_trip_and_tamper() {
let sa = SecurityAssociation::new(0x0102_0304)
.encryption(EncryptionAlgorithm::ChaCha20Poly1305, chacha_key())
.salt(vec![0xA0, 0xA1, 0xA2, 0xA3]);
assert!(sa.validate().is_ok());
round_trip_and_tamper(&sa, &iv8(), b"ChaCha20-Poly1305 ESP plaintext!");
}
#[test]
fn aes_ccm_round_trip_and_tamper() {
let sa = SecurityAssociation::new(0x0102_0304)
.encryption(EncryptionAlgorithm::AesCcm8, aes_key())
.salt(vec![0xA0, 0xA1, 0xA2]); assert!(sa.validate().is_ok());
round_trip_and_tamper(&sa, &iv8(), b"AES-CCM-8 ESP plaintext payload!");
}
#[test]
fn aes_cbc_hmac_sha256_round_trip_and_tamper() {
let sa = SecurityAssociation::new(0x0102_0304)
.encryption(EncryptionAlgorithm::AesCbc, aes_key())
.integrity(IntegrityAlgorithm::HmacSha2_256_128, vec![0x33u8; 32]);
assert!(sa.validate().is_ok());
let plaintext = vec![0x5Au8; 32];
round_trip_and_tamper(&sa, &iv16(), &plaintext);
}
#[test]
fn aes_ctr_hmac_sha256_round_trip_and_tamper() {
let sa = SecurityAssociation::new(0x0102_0304)
.encryption(EncryptionAlgorithm::AesCtr, aes_key())
.salt(vec![0x00, 0x00, 0x00, 0x30]) .integrity(IntegrityAlgorithm::HmacSha2_256_128, vec![0x44u8; 32]);
assert!(sa.validate().is_ok());
round_trip_and_tamper(&sa, &iv8(), b"AES-CTR ESP plaintext, any length");
}
#[test]
fn null_hmac_sha256_round_trip_and_tamper() {
let sa = SecurityAssociation::new(0x0102_0304)
.encryption(EncryptionAlgorithm::Null, Vec::new())
.integrity(IntegrityAlgorithm::HmacSha2_256_128, vec![0x55u8; 32]);
assert!(sa.validate().is_ok());
round_trip_and_tamper(&sa, &[], b"NULL cipher with HMAC integrity!");
}
#[test]
fn aes_gmac_integrity_round_trip_and_tamper() {
let sa = SecurityAssociation::new(0x0102_0304)
.encryption(EncryptionAlgorithm::AesCtr, aes_key())
.salt(vec![0x00, 0x00, 0x00, 0x30])
.integrity(IntegrityAlgorithm::AesGmac, vec![0x66u8; 16]);
assert!(sa.validate().is_ok());
round_trip_and_tamper(&sa, &iv8(), b"AES-CTR + AES-GMAC integrity!!!!");
}
#[test]
fn iv_requirement_reports_lengths() {
let gcm = SecurityAssociation::new(1)
.encryption(EncryptionAlgorithm::AesGcm16, aes_key())
.salt(vec![0u8; 4]);
assert_eq!(
iv_requirement(&gcm),
IvRequirement {
iv_len: 8,
iv_required: true
}
);
let cbc = SecurityAssociation::new(1)
.encryption(EncryptionAlgorithm::AesCbc, aes_key())
.integrity(IntegrityAlgorithm::HmacSha2_256_128, vec![0u8; 32]);
assert_eq!(
iv_requirement(&cbc),
IvRequirement {
iv_len: 16,
iv_required: true
}
);
let null = SecurityAssociation::new(1);
assert_eq!(
iv_requirement(&null),
IvRequirement {
iv_len: 0,
iv_required: false
}
);
}
#[test]
fn icv_mismatch_is_structured_error() {
let sa = SecurityAssociation::new(0x10)
.encryption(EncryptionAlgorithm::AesCbc, aes_key())
.integrity(IntegrityAlgorithm::HmacSha2_256_128, vec![0x33u8; 32]);
let plaintext = vec![0u8; 16];
let sealed = seal(&sa, &iv16(), &aad(), &plaintext).unwrap();
let mut bad_icv = sealed.icv.clone();
bad_icv[0] ^= 0x01;
let err = open(&sa, &iv16(), &aad(), &sealed.ciphertext, &bad_icv).unwrap_err();
match err {
CrafterError::InvalidFieldValue { field, .. } => {
assert_eq!(field, "ipsec.sa.icv");
}
other => panic!("expected structured ICV error, got {other:?}"),
}
}
#[test]
fn tampered_aad_fails_open() {
let gcm = SecurityAssociation::new(0x0102_0304)
.encryption(EncryptionAlgorithm::AesGcm16, aes_key())
.salt(vec![0xAA, 0xBB, 0xCC, 0xDD]);
let sealed = seal(&gcm, &iv8(), &aad(), b"payload").unwrap();
let mut bad_aad = aad();
bad_aad[0] ^= 0x01;
assert!(open(&gcm, &iv8(), &bad_aad, &sealed.ciphertext, &sealed.icv).is_err());
let cbc = SecurityAssociation::new(0x0102_0304)
.encryption(EncryptionAlgorithm::AesCbc, aes_key())
.integrity(IntegrityAlgorithm::HmacSha2_256_128, vec![0x33u8; 32]);
let sealed = seal(&cbc, &iv16(), &aad(), &[0u8; 16]).unwrap();
let mut bad_aad = aad();
bad_aad[0] ^= 0x01;
assert!(open(&cbc, &iv16(), &bad_aad, &sealed.ciphertext, &sealed.icv).is_err());
}
}