pub(crate) mod decode;
pub mod header;
pub(crate) mod icv;
use crate::field::Field;
use crate::packet::{Layer, LayerContext};
use crate::protocols::icmp::{Icmpv4, Icmpv6};
use crate::protocols::ip::shared::protocol_numbers::{
IPPROTO_ICMP, IPPROTO_ICMPV6, IPPROTO_IPV6, IPPROTO_NO_NEXT, IPPROTO_TCP, IPPROTO_UDP,
};
use crate::protocols::ipsec::sa::{
EncryptionAlgorithm, IntegrityAlgorithm, IpsecMode, SecurityAssociation,
};
use crate::protocols::ipv4::Ipv4;
use crate::protocols::ipv6::Ipv6;
use crate::protocols::transport::common::{
hex_bytes, impl_layer_div, impl_layer_object, payload_bytes_after,
};
use crate::protocols::{Tcp, Udp};
use crate::{CrafterError, Result};
use self::icv::{
canonical_ipv4_for_ah, canonical_ipv6_for_ah, preceding_ipv4_header_bytes,
preceding_ipv6_header_bytes,
};
const IPPROTO_IPV4: u8 = 4;
const DEFAULT_AH_SPI: u32 = 0x0000_0001;
const DEFAULT_AH_SEQUENCE: u32 = 1;
const DEFAULT_AH_RESERVED: u16 = 0;
const DEFAULT_AH_HIGH_SEQUENCE: u32 = 0;
#[derive(Debug, Clone)]
pub struct Ah {
next_header: Field<u8>,
payload_len: Field<u8>,
reserved: Field<u16>,
spi: Field<u32>,
sequence: Field<u32>,
icv: Field<Vec<u8>>,
sa: Option<SecurityAssociation>,
high_sequence: Field<u32>,
verified: Option<bool>,
}
#[derive(Clone, Copy)]
struct ResolvedAhFields {
next_header: u8,
payload_len: u8,
reserved: u16,
spi: u32,
sequence: u32,
}
impl Ah {
pub fn new() -> Self {
Self {
next_header: Field::unset(),
payload_len: Field::unset(),
reserved: Field::defaulted(DEFAULT_AH_RESERVED),
spi: Field::defaulted(DEFAULT_AH_SPI),
sequence: Field::defaulted(DEFAULT_AH_SEQUENCE),
icv: Field::unset(),
sa: None,
high_sequence: Field::defaulted(DEFAULT_AH_HIGH_SEQUENCE),
verified: None,
}
}
pub fn secured(sa: SecurityAssociation) -> Self {
let mut ah = Self::new();
ah.sa = Some(sa);
ah
}
pub fn next_header_value(&self) -> Option<u8> {
self.next_header.value().copied()
}
pub fn payload_len_value(&self) -> Option<u8> {
self.payload_len.value().copied()
}
pub fn reserved_value(&self) -> Option<u16> {
self.reserved.value().copied()
}
pub fn spi_value(&self) -> Option<u32> {
self.spi.value().copied()
}
pub fn sequence_value(&self) -> Option<u32> {
self.sequence.value().copied()
}
pub fn high_sequence_value(&self) -> Option<u32> {
self.high_sequence.value().copied()
}
pub fn icv_value(&self) -> Option<&[u8]> {
self.icv.value().map(Vec::as_slice)
}
pub fn verification_status(&self) -> Option<bool> {
self.verified
}
pub(crate) fn set_verification_status(&mut self, verified: bool) {
self.verified = Some(verified);
}
pub fn attached_security_association(&self) -> Option<&SecurityAssociation> {
self.sa.as_ref()
}
pub fn spi(mut self, spi: u32) -> Self {
self.spi.set_user(spi);
self
}
pub fn sequence(mut self, sequence: u32) -> Self {
self.sequence.set_user(sequence);
self
}
pub fn seq(self, sequence: u32) -> Self {
self.sequence(sequence)
}
pub fn next_header(mut self, next_header: u8) -> Self {
self.next_header.set_user(next_header);
self
}
pub fn payload_len(mut self, payload_len: u8) -> Self {
self.payload_len.set_user(payload_len);
self
}
pub fn reserved(mut self, reserved: u16) -> Self {
self.reserved.set_user(reserved);
self
}
pub fn icv(mut self, icv: impl Into<Vec<u8>>) -> Self {
self.icv.set_user(icv.into());
self
}
pub fn security_association(mut self, sa: SecurityAssociation) -> Self {
self.sa = Some(sa);
self
}
pub fn high_sequence(mut self, high_sequence: u32) -> Self {
self.high_sequence.set_user(high_sequence);
self
}
pub(crate) fn effective_icv_len(&self, ip_version: u8) -> usize {
let unpadded = self.unpadded_icv_len();
let alignment = icv_alignment(ip_version);
let remainder = unpadded % alignment;
if remainder == 0 {
unpadded
} else {
unpadded + (alignment - remainder)
}
}
pub(crate) fn effective_payload_len(&self, ip_version: u8) -> u8 {
if let Some(payload_len) = self.payload_len.value().copied() {
return payload_len;
}
let total = header::AH_FIXED_LEN + self.effective_icv_len(ip_version);
let words = (total / header::AH_LENGTH_UNIT) as u8;
words.saturating_sub(header::AH_PAYLOAD_LEN_OFFSET)
}
fn unpadded_icv_len(&self) -> usize {
if let Some(icv) = self.icv.value() {
return icv.len();
}
let Some(sa) = self.sa.as_ref() else {
return 0;
};
let resolved = if sa.enc.is_aead() {
sa.enc.icv_len()
} else {
sa.integ.icv_len()
};
resolved.unwrap_or(0)
}
fn preceding_ip_version(ctx: &LayerContext<'_>) -> Result<u8> {
let previous = ctx.previous().ok_or_else(|| {
CrafterError::invalid_field_value(
"ah.compile.previous",
"AH requires a preceding IP header to authenticate",
)
})?;
let any = previous.as_any();
if any.is::<Ipv4>() {
Ok(4)
} else if any.is::<Ipv6>() {
Ok(6)
} else {
Err(CrafterError::invalid_field_value(
"ah.compile.previous",
"preceding layer is not an IPv4 or IPv6 header",
))
}
}
fn effective_next_header(&self, ctx: &LayerContext<'_>, mode: IpsecMode) -> u8 {
if let Some(next_header) = self.next_header.value().copied() {
return next_header;
}
ctx.next()
.and_then(|layer| layer_ah_next_header(layer, mode))
.unwrap_or(IPPROTO_NO_NEXT)
}
fn canonical_preceding_ip(ctx: &LayerContext<'_>, ip_version: u8) -> Result<Vec<u8>> {
if ip_version == 6 {
let (base, ext) = preceding_ipv6_header_bytes(ctx)?;
canonical_ipv6_for_ah(&base, &ext)
} else {
let header = preceding_ipv4_header_bytes(ctx)?;
canonical_ipv4_for_ah(&header)
}
}
fn assemble_header(fields: &ResolvedAhFields, icv: &[u8]) -> Vec<u8> {
let mut bytes = Vec::with_capacity(header::AH_FIXED_LEN + icv.len());
bytes.push(fields.next_header);
bytes.push(fields.payload_len);
bytes.extend_from_slice(&fields.reserved.to_be_bytes());
bytes.extend_from_slice(&fields.spi.to_be_bytes());
bytes.extend_from_slice(&fields.sequence.to_be_bytes());
bytes.extend_from_slice(icv);
bytes
}
fn ah_icv_input(
&self,
ctx: &LayerContext<'_>,
sa: &SecurityAssociation,
ip_version: u8,
fields: &ResolvedAhFields,
padded_icv_len: usize,
) -> Result<Vec<u8>> {
let canonical_ip = Self::canonical_preceding_ip(ctx, ip_version)?;
let zeroed = vec![0u8; padded_icv_len];
let ah_zeroed = Self::assemble_header(fields, &zeroed);
let upper = payload_bytes_after(*ctx)?;
let mut input = Vec::with_capacity(canonical_ip.len() + ah_zeroed.len() + upper.len() + 4);
input.extend_from_slice(&canonical_ip);
input.extend_from_slice(&ah_zeroed);
input.extend_from_slice(&upper);
if sa.esn {
let high = self
.high_sequence
.value()
.copied()
.unwrap_or(DEFAULT_AH_HIGH_SEQUENCE);
input.extend_from_slice(&high.to_be_bytes());
}
Ok(input)
}
fn resolved_icv_fields(&self, ctx: &LayerContext<'_>, ip_version: u8) -> ResolvedAhFields {
let mode = self.sa.as_ref().map(|sa| sa.mode).unwrap_or_default();
let next_header = self.effective_next_header(ctx, mode);
let payload_len = self.effective_payload_len(ip_version);
let reserved = self
.reserved
.value()
.copied()
.unwrap_or(DEFAULT_AH_RESERVED);
let spi = self.spi.value().copied().unwrap_or(DEFAULT_AH_SPI);
let sequence = self
.sequence
.value()
.copied()
.unwrap_or(DEFAULT_AH_SEQUENCE);
ResolvedAhFields {
next_header,
payload_len,
reserved,
spi,
sequence,
}
}
fn compute_icv(
&self,
ctx: &LayerContext<'_>,
sa: &SecurityAssociation,
ip_version: u8,
fields: &ResolvedAhFields,
) -> Result<Vec<u8>> {
let padded_icv_len = self.effective_icv_len(ip_version);
let input = self.ah_icv_input(ctx, sa, ip_version, fields, padded_icv_len)?;
let transform = sa.integ.integrity_transform()?;
let mut icv = transform.compute(&sa.integ_key, &input)?;
if icv.len() < padded_icv_len {
icv.resize(padded_icv_len, 0);
}
Ok(icv)
}
fn compile_ah(&self, ctx: &LayerContext<'_>, out: &mut Vec<u8>) -> Result<()> {
let ip_version = Self::preceding_ip_version(ctx)?;
let fields = self.resolved_icv_fields(ctx, ip_version);
let padded_icv_len = self.effective_icv_len(ip_version);
let icv = if let Some(icv) = self.icv.value() {
let mut icv = icv.clone();
if icv.len() < padded_icv_len {
icv.resize(padded_icv_len, 0);
}
icv
} else if let Some(sa) = self.sa.as_ref() {
self.compute_icv(ctx, sa, ip_version, &fields)?
} else {
Vec::new()
};
let header = Self::assemble_header(&fields, &icv);
out.extend_from_slice(&header);
Ok(())
}
fn display_spi(&self) -> u32 {
self.spi.value().copied().unwrap_or(DEFAULT_AH_SPI)
}
fn display_sequence(&self) -> u32 {
self.sequence
.value()
.copied()
.unwrap_or(DEFAULT_AH_SEQUENCE)
}
fn display_integ_label(&self) -> String {
match self.sa.as_ref() {
Some(sa) if sa.enc.is_aead() => ah_encryption_label(sa.enc),
Some(sa) => ah_integrity_label(sa.integ),
None => "opaque".to_string(),
}
}
}
fn layer_ah_next_header(layer: &dyn Layer, _mode: IpsecMode) -> Option<u8> {
let any = layer.as_any();
if any.is::<Ipv4>() {
return Some(IPPROTO_IPV4);
}
if any.is::<Ipv6>() {
return Some(IPPROTO_IPV6);
}
if any.is::<Tcp>() {
return Some(IPPROTO_TCP);
}
if any.is::<Udp>() {
return Some(IPPROTO_UDP);
}
if any.is::<Icmpv4>() {
return Some(IPPROTO_ICMP);
}
if any.is::<Icmpv6>() {
return Some(IPPROTO_ICMPV6);
}
None
}
fn ah_integrity_label(alg: IntegrityAlgorithm) -> String {
match alg {
IntegrityAlgorithm::None => "NONE".to_string(),
IntegrityAlgorithm::HmacSha1_96 => "HMAC_SHA1_96".to_string(),
IntegrityAlgorithm::AesXcbc96 => "AES_XCBC_96".to_string(),
IntegrityAlgorithm::AesGmac => "AES_128_GMAC".to_string(),
IntegrityAlgorithm::HmacSha2_256_128 => "HMAC_SHA2_256_128".to_string(),
IntegrityAlgorithm::HmacSha2_384_192 => "HMAC_SHA2_384_192".to_string(),
IntegrityAlgorithm::HmacSha2_512_256 => "HMAC_SHA2_512_256".to_string(),
IntegrityAlgorithm::Unknown(id) => format!("UNKNOWN({id})"),
}
}
fn ah_encryption_label(alg: EncryptionAlgorithm) -> String {
match alg {
EncryptionAlgorithm::Null => "NULL".to_string(),
EncryptionAlgorithm::AesCbc => "AES_CBC".to_string(),
EncryptionAlgorithm::AesCtr => "AES_CTR".to_string(),
EncryptionAlgorithm::AesCcm8 => "AES_CCM_8".to_string(),
EncryptionAlgorithm::AesGcm16 => "AES_GCM_16".to_string(),
EncryptionAlgorithm::ChaCha20Poly1305 => "CHACHA20_POLY1305".to_string(),
EncryptionAlgorithm::Unknown(id) => format!("UNKNOWN({id})"),
}
}
impl Layer for Ah {
fn name(&self) -> &'static str {
"Ah"
}
fn summary(&self) -> String {
format!(
"Ah(spi=0x{:08x}, seq={}, integ={}, nh={})",
self.display_spi(),
self.display_sequence(),
self.display_integ_label(),
self.next_header
.value()
.map(|nh| nh.to_string())
.unwrap_or_else(|| "auto".to_string()),
)
}
fn inspection_fields(&self) -> Vec<(&'static str, String)> {
let mut fields = vec![
(
"next_header",
self.next_header
.value()
.map(|nh| nh.to_string())
.unwrap_or_else(|| "auto".to_string()),
),
(
"payload_len",
self.payload_len
.value()
.map(|len| len.to_string())
.unwrap_or_else(|| "auto".to_string()),
),
(
"reserved",
format!(
"0x{:04x}",
self.reserved
.value()
.copied()
.unwrap_or(DEFAULT_AH_RESERVED)
),
),
("spi", format!("0x{:08x}", self.display_spi())),
("sequence", self.display_sequence().to_string()),
("integrity", self.display_integ_label()),
];
match self.icv.value() {
Some(icv) => fields.push(("icv", hex_bytes(icv))),
None => fields.push(("icv_len", self.effective_icv_len(4).to_string())),
}
fields
}
fn encoded_len(&self) -> usize {
header::AH_FIXED_LEN + self.effective_icv_len(4)
}
fn encoded_len_with_context(&self, ctx: &LayerContext<'_>) -> usize {
let ip_version = Self::preceding_ip_version(ctx).unwrap_or(4);
header::AH_FIXED_LEN + self.effective_icv_len(ip_version)
}
fn compile(&self, ctx: &LayerContext<'_>, out: &mut Vec<u8>) -> Result<()> {
self.compile_ah(ctx, out)
}
impl_layer_object!(Ah);
}
impl_layer_div!(Ah);
const fn icv_alignment(ip_version: u8) -> usize {
if ip_version == 6 {
8
} else {
4
}
}
impl Default for Ah {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::field::FieldState;
use crate::protocols::ipsec::sa::{EncryptionAlgorithm, SecurityAssociation};
#[test]
fn new_defaults_spi_sequence_reserved_and_high_sequence() {
let ah = Ah::new();
assert_eq!(ah.spi.state(), FieldState::Defaulted);
assert_eq!(ah.spi_value(), Some(DEFAULT_AH_SPI));
assert_eq!(ah.sequence.state(), FieldState::Defaulted);
assert_eq!(ah.sequence_value(), Some(DEFAULT_AH_SEQUENCE));
assert_eq!(ah.reserved.state(), FieldState::Defaulted);
assert_eq!(ah.reserved_value(), Some(DEFAULT_AH_RESERVED));
assert_eq!(ah.high_sequence.state(), FieldState::Defaulted);
assert_eq!(ah.high_sequence_value(), Some(DEFAULT_AH_HIGH_SEQUENCE));
assert_eq!(ah.next_header.state(), FieldState::Unset);
assert_eq!(ah.payload_len.state(), FieldState::Unset);
assert_eq!(ah.icv.state(), FieldState::Unset);
assert!(ah.next_header_value().is_none());
assert!(ah.payload_len_value().is_none());
assert!(ah.icv_value().is_none());
assert!(ah.attached_security_association().is_none());
}
#[test]
fn default_matches_new() {
let ah = Ah::default();
assert_eq!(ah.spi_value(), Some(DEFAULT_AH_SPI));
assert_eq!(ah.sequence_value(), Some(DEFAULT_AH_SEQUENCE));
assert!(ah.attached_security_association().is_none());
}
#[test]
fn secured_attaches_the_sa() {
let sa = SecurityAssociation::new(0x0000_2000).integrity(
crate::protocols::ipsec::sa::IntegrityAlgorithm::HmacSha2_256_128,
vec![0x33u8; 32],
);
let ah = Ah::secured(sa.clone());
assert_eq!(ah.attached_security_association(), Some(&sa));
assert_eq!(ah.spi.state(), FieldState::Defaulted);
assert_eq!(ah.sequence.state(), FieldState::Defaulted);
}
#[test]
fn fixed_header_length_constant_is_twelve() {
assert_eq!(header::AH_FIXED_LEN, 12);
assert_eq!(
header::AH_NEXT_HEADER_LEN
+ header::AH_PAYLOAD_LEN_FIELD_LEN
+ header::AH_RESERVED_LEN
+ header::AH_SPI_LEN
+ header::AH_SEQUENCE_LEN,
header::AH_FIXED_LEN
);
let _ = EncryptionAlgorithm::AesGcm16;
}
use crate::protocols::ipsec::sa::IntegrityAlgorithm;
#[test]
fn setters_mark_fields_as_user() {
let ah = Ah::new()
.spi(0x0000_2000)
.sequence(42)
.high_sequence(0x0000_0001)
.next_header(6)
.payload_len(7)
.reserved(0xFFFF)
.icv(vec![0xAAu8; 12]);
assert_eq!(ah.spi.state(), FieldState::User);
assert_eq!(ah.spi_value(), Some(0x0000_2000));
assert_eq!(ah.sequence.state(), FieldState::User);
assert_eq!(ah.sequence_value(), Some(42));
assert_eq!(ah.high_sequence.state(), FieldState::User);
assert_eq!(ah.high_sequence_value(), Some(0x0000_0001));
assert_eq!(ah.next_header.state(), FieldState::User);
assert_eq!(ah.next_header_value(), Some(6));
assert_eq!(ah.payload_len.state(), FieldState::User);
assert_eq!(ah.payload_len_value(), Some(7));
assert_eq!(ah.reserved.state(), FieldState::User);
assert_eq!(ah.reserved_value(), Some(0xFFFF));
assert_eq!(ah.icv.state(), FieldState::User);
assert_eq!(ah.icv_value(), Some(&[0xAAu8; 12][..]));
}
#[test]
fn seq_alias_matches_sequence() {
let ah = Ah::new().seq(9);
assert_eq!(ah.sequence.state(), FieldState::User);
assert_eq!(ah.sequence_value(), Some(9));
}
#[test]
fn security_association_setter_attaches_the_sa() {
let sa = SecurityAssociation::new(0x10)
.integrity(IntegrityAlgorithm::HmacSha2_256_128, vec![0x33u8; 32]);
let ah = Ah::new().security_association(sa.clone());
assert_eq!(ah.attached_security_association(), Some(&sa));
}
#[test]
fn payload_len_for_twelve_byte_icv_over_ipv4_is_rfc_value() {
let sa = SecurityAssociation::new(0x0000_2000)
.integrity(IntegrityAlgorithm::HmacSha1_96, vec![0x44u8; 20]);
let ah = Ah::secured(sa);
assert_eq!(ah.effective_icv_len(4), 12);
assert_eq!(ah.effective_payload_len(4), 4);
}
#[test]
fn payload_len_override_is_kept_verbatim() {
let sa = SecurityAssociation::new(0x0000_2000)
.integrity(IntegrityAlgorithm::HmacSha1_96, vec![0x44u8; 20]);
let ah = Ah::secured(sa).payload_len(0x7F);
assert_eq!(ah.effective_payload_len(4), 0x7F);
}
#[test]
fn sixteen_byte_icv_pads_to_sixty_four_bits_on_ipv6() {
let sa = SecurityAssociation::new(0x0000_2000)
.integrity(IntegrityAlgorithm::HmacSha2_256_128, vec![0x33u8; 32]);
let ah = Ah::secured(sa);
assert_eq!(ah.effective_icv_len(4), 16);
assert_eq!(ah.effective_payload_len(4), 5);
assert_eq!(ah.effective_icv_len(6), 16);
assert_eq!(ah.effective_payload_len(6), 5);
}
#[test]
fn icv_padded_to_sixty_four_bits_on_ipv6_when_unaligned() {
let sha384 = SecurityAssociation::new(0x0000_2000)
.integrity(IntegrityAlgorithm::HmacSha2_384_192, vec![0x55u8; 48]);
let ah384 = Ah::secured(sha384);
assert_eq!(ah384.effective_icv_len(4), 24);
assert_eq!(ah384.effective_payload_len(4), 7);
assert_eq!(ah384.effective_icv_len(6), 24);
assert_eq!(ah384.effective_payload_len(6), 7);
let sha1 = SecurityAssociation::new(0x0000_2000)
.integrity(IntegrityAlgorithm::HmacSha1_96, vec![0x44u8; 20]);
let ah1 = Ah::secured(sha1);
assert_eq!(ah1.effective_icv_len(4), 12);
assert_eq!(ah1.effective_icv_len(6), 16);
assert_eq!(ah1.effective_payload_len(6), 5);
}
#[test]
fn icv_override_drives_the_padded_length() {
let ah = Ah::new().icv(vec![0xABu8; 10]);
assert_eq!(ah.effective_icv_len(4), 12);
assert_eq!(ah.effective_icv_len(6), 16);
assert_eq!(ah.effective_payload_len(4), 4);
}
use crate::packet::{LayerContext, Packet, Raw};
use crate::protocols::ipsec::ah::icv::{canonical_ipv4_for_ah, preceding_ipv4_header_bytes};
use crate::protocols::ipsec::crypto::IntegrityTransform;
use crate::protocols::ipv4::{Ipv4, IPPROTO_AH};
use crate::protocols::transport::common::payload_bytes_after;
use crate::protocols::Tcp;
fn hmac_key() -> Vec<u8> {
vec![0x77u8; 32]
}
fn expected_ah_bytes(
ip_ctx: &LayerContext<'_>,
integ_key: &[u8],
fields: &ResolvedAhFields,
icv_len: usize,
) -> Vec<u8> {
let ip_header = preceding_ipv4_header_bytes(ip_ctx).unwrap();
let canonical_ip = canonical_ipv4_for_ah(&ip_header).unwrap();
let mut ah_zeroed = Vec::new();
ah_zeroed.push(fields.next_header);
ah_zeroed.push(fields.payload_len);
ah_zeroed.extend_from_slice(&fields.reserved.to_be_bytes());
ah_zeroed.extend_from_slice(&fields.spi.to_be_bytes());
ah_zeroed.extend_from_slice(&fields.sequence.to_be_bytes());
ah_zeroed.extend_from_slice(&vec![0u8; icv_len]);
let upper = payload_bytes_after(*ip_ctx).unwrap();
let mut input = Vec::new();
input.extend_from_slice(&canonical_ip);
input.extend_from_slice(&ah_zeroed);
input.extend_from_slice(&upper);
let mut icv = IntegrityTransform::HmacSha2_256_128
.compute(integ_key, &input)
.unwrap();
icv.resize(icv_len, 0);
let mut out = Vec::new();
out.push(fields.next_header);
out.push(fields.payload_len);
out.extend_from_slice(&fields.reserved.to_be_bytes());
out.extend_from_slice(&fields.spi.to_be_bytes());
out.extend_from_slice(&fields.sequence.to_be_bytes());
out.extend_from_slice(&icv);
out
}
#[test]
fn compile_ipv4_ah_hmac_sha256_golden() {
let sa = SecurityAssociation::new(0x0000_2000)
.integrity(IntegrityAlgorithm::HmacSha2_256_128, hmac_key());
assert!(sa.validate().is_ok());
let ipv4 = Ipv4::new()
.protocol(IPPROTO_AH)
.src("192.0.2.1".parse().unwrap())
.dst("192.0.2.2".parse().unwrap())
.ttl(64);
let ah = Ah::secured(sa.clone()).spi(0x0000_2000).sequence(1);
let packet: Packet = ipv4 / ah / Tcp::new() / Raw::from_bytes([0xDE, 0xAD, 0xBE, 0xEF]);
let whole = packet.compile().unwrap();
let ah_ctx = LayerContext::new(&packet, 1);
let mut ah_bytes = Vec::new();
packet
.get(1)
.unwrap()
.compile(&ah_ctx, &mut ah_bytes)
.unwrap();
let icv_len = 16usize;
let fields = ResolvedAhFields {
next_header: IPPROTO_TCP,
payload_len: 5,
reserved: 0,
spi: 0x0000_2000,
sequence: 1,
};
let expected = expected_ah_bytes(&ah_ctx, &hmac_key(), &fields, icv_len);
assert_eq!(ah_bytes, expected);
assert_eq!(
ah_bytes[0], IPPROTO_TCP,
"Next Header is the inner protocol"
);
assert_eq!(ah_bytes[1], 5, "Payload Len = (12 + 16)/4 − 2");
assert_eq!(&ah_bytes[2..4], &[0, 0], "Reserved is zero");
assert_eq!(&ah_bytes[4..8], &0x0000_2000u32.to_be_bytes(), "SPI");
assert_eq!(&ah_bytes[8..12], &1u32.to_be_bytes(), "Sequence");
assert_eq!(ah_bytes.len(), header::AH_FIXED_LEN + icv_len);
let ip_header = preceding_ipv4_header_bytes(&ah_ctx).unwrap();
let ah_start = ip_header.len();
assert_eq!(&whole[ah_start..ah_start + ah_bytes.len()], &ah_bytes[..]);
assert_eq!(&whole[whole.len() - 4..], &[0xDE, 0xAD, 0xBE, 0xEF]);
}
#[test]
fn ipv4_ah_tcp_composes_and_compiles() {
let sa = SecurityAssociation::new(0x0000_2000)
.integrity(IntegrityAlgorithm::HmacSha2_256_128, vec![0xABu8; 32]);
let ipv4 = Ipv4::new()
.protocol(IPPROTO_AH)
.src("192.0.2.1".parse().unwrap())
.dst("192.0.2.2".parse().unwrap());
let packet: Packet = ipv4 / Ah::secured(sa) / Tcp::new();
assert_eq!(packet.len(), 3);
let bytes = packet.compile().unwrap();
assert_eq!(bytes.len(), 20 + 12 + 16 + 20);
}
#[test]
fn summary_carries_spi_and_algorithm_without_key_bytes() {
let sa = SecurityAssociation::new(0x0000_2000)
.integrity(IntegrityAlgorithm::HmacSha2_256_128, vec![0xABu8; 32]);
let ah = Ah::secured(sa).spi(0x0000_2000).sequence(1);
let summary = ah.summary();
assert!(summary.contains("spi=0x00002000"), "summary: {summary}");
assert!(
summary.contains("integ=HMAC_SHA2_256_128"),
"summary: {summary}"
);
assert!(summary.contains("seq=1"), "summary: {summary}");
assert!(!summary.contains("ab ab ab ab"), "summary leaked key bytes");
}
#[test]
fn show_carries_spi_and_algorithm_without_key_bytes() {
let sa = SecurityAssociation::new(0x0000_2000)
.integrity(IntegrityAlgorithm::HmacSha2_256_128, vec![0xABu8; 32]);
let ipv4 = Ipv4::new()
.protocol(IPPROTO_AH)
.src("192.0.2.1".parse().unwrap())
.dst("192.0.2.2".parse().unwrap());
let packet: Packet = ipv4 / Ah::secured(sa).spi(0x0000_2000) / Tcp::new();
let show = packet.show();
assert!(show.contains("Ah"), "show: {show}");
assert!(show.contains("spi: 0x00002000"), "show: {show}");
assert!(
show.contains("integrity: HMAC_SHA2_256_128"),
"show: {show}"
);
assert!(show.contains("icv_len: 16"), "show: {show}");
assert!(!show.contains("ab ab ab ab"), "show leaked key bytes");
}
#[test]
fn inspection_fields_show_explicit_icv_and_auto_placeholders() {
let ah = Ah::new().icv(vec![0xDE, 0xAD, 0xBE, 0xEF]);
let fields = ah.inspection_fields();
let lookup = |name: &str| {
fields
.iter()
.find(|(field, _)| *field == name)
.map(|(_, value)| value.clone())
};
assert_eq!(lookup("next_header").as_deref(), Some("auto"));
assert_eq!(lookup("payload_len").as_deref(), Some("auto"));
assert_eq!(lookup("integrity").as_deref(), Some("opaque"));
assert_eq!(lookup("icv").as_deref(), Some("de ad be ef"));
}
}