use subtle::ConstantTimeEq;
use crate::endian::{read_u16_be, read_u32_be};
use crate::error::{CrafterError, Result};
use crate::packet::{LayerContext, Packet};
use crate::protocols::ip::shared::protocol_numbers::IPPROTO_IPV6;
use crate::protocols::ipsec::sa::SecurityAssociation;
use crate::registry::ProtocolRegistry;
use super::header::{AH_FIXED_LEN, AH_LENGTH_UNIT, AH_PAYLOAD_LEN_OFFSET};
use super::Ah;
const IPPROTO_IPV4: u8 = 4;
pub(crate) fn decode_ah_parts(bytes: &[u8]) -> Result<(Ah, usize, u8)> {
if bytes.len() < AH_FIXED_LEN {
return Err(CrafterError::buffer_too_short(
"ah header",
AH_FIXED_LEN,
bytes.len(),
));
}
let next_header = bytes[0];
let payload_len = bytes[1];
let reserved = read_u16_be(&bytes[2..4])?;
let spi = read_u32_be(&bytes[4..8])?;
let sequence = read_u32_be(&bytes[8..12])?;
let total_len =
(usize::from(payload_len) + usize::from(AH_PAYLOAD_LEN_OFFSET)) * AH_LENGTH_UNIT;
let icv_len = total_len.saturating_sub(AH_FIXED_LEN);
let inner_offset = AH_FIXED_LEN + icv_len;
if bytes.len() < inner_offset {
return Err(CrafterError::buffer_too_short(
"ah icv",
inner_offset,
bytes.len(),
));
}
let icv = &bytes[AH_FIXED_LEN..inner_offset];
let ah = Ah::new()
.next_header(next_header)
.payload_len(payload_len)
.reserved(reserved)
.spi(spi)
.sequence(sequence)
.icv(icv.to_vec());
Ok((ah, inner_offset, next_header))
}
fn dispatch_ah_inner(
registry: &ProtocolRegistry,
packet: Packet,
next_header: u8,
inner: &[u8],
) -> Result<Packet> {
match next_header {
IPPROTO_IPV4 => {
let decoded = registry.decode_ipv4(inner)?;
Ok(packet.concat(decoded))
}
IPPROTO_IPV6 => {
let decoded = registry.decode_ipv6(inner)?;
Ok(packet.concat(decoded))
}
protocol => registry.decode_ipv4_protocol(packet, protocol, inner),
}
}
pub(crate) fn append_ah_packet_with_registry(
registry: &ProtocolRegistry,
packet: Packet,
bytes: &[u8],
) -> Result<Packet> {
let (ah, inner_offset, next_header) = decode_ah_parts(bytes)?;
let packet = packet.push(ah);
dispatch_ah_inner(registry, packet, next_header, &bytes[inner_offset..])
}
pub(crate) fn verify_ah(packet: &Packet, ah_index: usize, sa: &SecurityAssociation) -> Result<()> {
let ctx = LayerContext::new(packet, ah_index);
let ah = packet
.get(ah_index)
.and_then(|layer| layer.as_any().downcast_ref::<Ah>())
.ok_or_else(|| {
CrafterError::invalid_field_value(
"ipsec.ah.verify",
"layer at the given index is not an AH header",
)
})?;
let transmitted_icv = ah.icv_value().ok_or_else(|| {
CrafterError::invalid_field_value("ipsec.ah.icv", "AH layer carries no ICV to verify")
})?;
let ip_version = Ah::preceding_ip_version(&ctx)?;
let padded_icv_len = ah.effective_icv_len(ip_version);
let fields = ah.resolved_icv_fields(&ctx, ip_version);
let input = ah.ah_icv_input(&ctx, sa, ip_version, &fields, padded_icv_len)?;
let transform = sa.integ.integrity_transform()?;
let matched = if transform.icv_len() == transmitted_icv.len() {
transform.verify(&sa.integ_key, &input, transmitted_icv)?
} else {
let mut expected = transform.compute(&sa.integ_key, &input)?;
if expected.len() < transmitted_icv.len() {
expected.resize(transmitted_icv.len(), 0);
}
expected.as_slice().ct_eq(transmitted_icv).into()
};
if !matched {
return Err(CrafterError::invalid_field_value(
"ipsec.ah.icv",
"AH ICV verification failed: recomputed integrity check value does not match",
));
}
Ok(())
}
pub(crate) fn append_ah_packet_with_registry_sa(
registry: &ProtocolRegistry,
packet: Packet,
bytes: &[u8],
sa: Option<&SecurityAssociation>,
) -> Result<Packet> {
let Some(sa) = sa else {
return append_ah_packet_with_registry(registry, packet, bytes);
};
let ah_index = packet.len();
let mut decoded = append_ah_packet_with_registry(registry, packet, bytes)?;
verify_ah(&decoded, ah_index, sa)?;
if let Some(ah) = decoded
.get_mut(ah_index)
.and_then(|layer| layer.as_any_mut().downcast_mut::<Ah>())
{
ah.set_verification_status(true);
}
Ok(decoded)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::packet::{LayerContext, Packet, Raw};
use crate::protocols::ipsec::sa::{IntegrityAlgorithm, SecurityAssociation};
use crate::protocols::ipv4::{Ipv4, IPPROTO_AH, IPPROTO_TCP};
use crate::protocols::Tcp;
fn compile_ah_packet() -> Vec<u8> {
let sa = SecurityAssociation::new(0x0000_2000)
.integrity(IntegrityAlgorithm::HmacSha2_256_128, vec![0x77u8; 32]);
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).spi(0x0000_2000).sequence(1);
let tcp = Tcp::new().sport(1234).dport(443);
let raw = Raw::from_bytes(vec![0xDE, 0xAD, 0xBE, 0xEF]);
let packet: Packet = Packet::from_layer(ipv4) / ah / tcp / raw;
let compiled = packet.compile().expect("compile AH packet").into_bytes();
let ip_header_len = usize::from(compiled[0] & 0x0f) * 4;
assert_eq!(compiled[9], IPPROTO_AH, "enclosing IPv4 advertises AH (51)");
compiled[ip_header_len..].to_vec()
}
fn compile_ah_layer(packet: &Packet, index: usize) -> Vec<u8> {
let mut out = Vec::new();
let ctx = LayerContext::new(packet, index);
packet.get(index).unwrap().compile(&ctx, &mut out).unwrap();
out
}
#[test]
fn decode_without_sa_exposes_all_fields_and_inner_tcp() {
let ah_bytes = compile_ah_packet();
let (ah, inner_offset, next_header) = decode_ah_parts(&ah_bytes).expect("decode AH parts");
assert_eq!(
next_header, IPPROTO_TCP,
"Next Header is the inner protocol"
);
assert_eq!(ah.next_header_value(), Some(IPPROTO_TCP));
assert_eq!(ah.payload_len_value(), Some(5));
assert_eq!(ah.reserved_value(), Some(0));
assert_eq!(ah.spi_value(), Some(0x0000_2000));
assert_eq!(ah.sequence_value(), Some(1));
let icv_len = 16usize;
assert_eq!(
ah.icv_value(),
Some(&ah_bytes[AH_FIXED_LEN..AH_FIXED_LEN + icv_len])
);
assert_eq!(inner_offset, AH_FIXED_LEN + icv_len);
assert_eq!(
&ah_bytes[inner_offset..inner_offset + 4],
&[0x04, 0xD2, 0x01, 0xBB]
);
assert_eq!(&ah_bytes[ah_bytes.len() - 4..], &[0xDE, 0xAD, 0xBE, 0xEF]);
let registry = ProtocolRegistry::with_builtin_bindings();
let decoded = dispatch_ah_inner(
®istry,
Packet::new(),
next_header,
&ah_bytes[inner_offset..],
)
.expect("dispatch AH inner");
let tcp = decoded.layer::<Tcp>().expect("inner Tcp decoded");
assert_eq!(tcp.source_port_value(), 1234);
assert_eq!(tcp.destination_port_value(), 443);
}
#[test]
fn decoded_ah_recompiles_byte_exact() {
let ah_bytes = compile_ah_packet();
let (ah, inner_offset, _) = decode_ah_parts(&ah_bytes).expect("decode AH parts");
let ipv4 = Ipv4::new()
.protocol(IPPROTO_AH)
.src("192.0.2.1".parse().unwrap())
.dst("192.0.2.2".parse().unwrap())
.ttl(64);
let packet: Packet = Packet::from_layer(ipv4) / ah;
let recompiled = compile_ah_layer(&packet, 1);
assert_eq!(recompiled, ah_bytes[..inner_offset]);
}
#[test]
fn append_with_registry_pushes_ah_and_inner_tcp() {
let ah_bytes = compile_ah_packet();
let registry = ProtocolRegistry::with_builtin_bindings();
let packet = append_ah_packet_with_registry(®istry, Packet::new(), &ah_bytes)
.expect("append AH with registry");
let ah = packet
.get(0)
.unwrap()
.as_any()
.downcast_ref::<Ah>()
.expect("first layer is Ah");
assert_eq!(ah.spi_value(), Some(0x0000_2000));
assert_eq!(ah.next_header_value(), Some(IPPROTO_TCP));
let tcp = packet.layer::<Tcp>().expect("inner Tcp decoded");
assert_eq!(tcp.source_port_value(), 1234);
assert_eq!(tcp.destination_port_value(), 443);
assert_eq!(
packet.layer::<Raw>().expect("inner Raw decoded").as_bytes(),
&[0xDE, 0xAD, 0xBE, 0xEF]
);
}
#[test]
fn truncated_before_fixed_header_is_structured_error() {
let truncated = vec![0x06, 0x05, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00];
let err = decode_ah_parts(&truncated).expect_err("must reject short fixed header");
match err {
CrafterError::BufferTooShort {
context,
required,
available,
} => {
assert_eq!(context, "ah header");
assert_eq!(required, AH_FIXED_LEN);
assert_eq!(available, truncated.len());
}
other => panic!("expected buffer_too_short, got {other:?}"),
}
}
#[test]
fn one_byte_short_of_icv_is_structured_error() {
let ah_bytes = compile_ah_packet();
let inner_offset = AH_FIXED_LEN + 16; let truncated = ah_bytes[..inner_offset - 1].to_vec();
let err = decode_ah_parts(&truncated).expect_err("must reject short ICV");
match err {
CrafterError::BufferTooShort {
context,
required,
available,
} => {
assert_eq!(context, "ah icv");
assert_eq!(required, inner_offset);
assert_eq!(available, truncated.len());
}
other => panic!("expected buffer_too_short, got {other:?}"),
}
}
fn verify_sa() -> SecurityAssociation {
SecurityAssociation::new(0x0000_2000)
.integrity(IntegrityAlgorithm::HmacSha2_256_128, vec![0x77u8; 32])
}
fn verify_ipv4() -> Ipv4 {
Ipv4::new()
.protocol(IPPROTO_AH)
.src("192.0.2.1".parse().unwrap())
.dst("192.0.2.2".parse().unwrap())
.ttl(64)
}
fn decoded_verify_packet() -> Packet {
let ah_bytes = compile_ah_packet();
let (decoded_ah, _, _) = decode_ah_parts(&ah_bytes).expect("decode AH parts");
Packet::from_layer(verify_ipv4())
/ decoded_ah
/ Tcp::new().sport(1234).dport(443)
/ Raw::from_bytes(vec![0xDE, 0xAD, 0xBE, 0xEF])
}
#[test]
fn verify_ah_with_matching_sa_passes() {
let packet = decoded_verify_packet();
verify_ah(&packet, 1, &verify_sa()).expect("ICV verifies with the matching SA");
}
#[test]
fn verify_ah_detects_tampered_payload_byte() {
let ah_bytes = compile_ah_packet();
let (decoded_ah, _, _) = decode_ah_parts(&ah_bytes).expect("decode AH parts");
let tampered: Packet = Packet::from_layer(verify_ipv4())
/ decoded_ah
/ Tcp::new().sport(1234).dport(443)
/ Raw::from_bytes(vec![0xDF, 0xAD, 0xBE, 0xEF]);
let err = verify_ah(&tampered, 1, &verify_sa())
.expect_err("a flipped payload byte must fail verification");
match err {
CrafterError::InvalidFieldValue { field, .. } => {
assert_eq!(field, "ipsec.ah.icv", "tamper fails on the ICV field");
}
other => panic!("expected an ICV mismatch error, got {other:?}"),
}
}
#[test]
fn verify_ah_detects_tampered_immutable_ipv4_field() {
let ah_bytes = compile_ah_packet();
let (decoded_ah, _, _) = decode_ah_parts(&ah_bytes).expect("decode AH parts");
let tampered_ip = Ipv4::new()
.protocol(IPPROTO_AH)
.src("192.0.2.9".parse().unwrap()) .dst("192.0.2.2".parse().unwrap())
.ttl(64);
let tampered: Packet = Packet::from_layer(tampered_ip)
/ decoded_ah
/ Tcp::new().sport(1234).dport(443)
/ Raw::from_bytes(vec![0xDE, 0xAD, 0xBE, 0xEF]);
let err = verify_ah(&tampered, 1, &verify_sa())
.expect_err("a flipped immutable IPv4 field must fail verification");
assert!(
matches!(err, CrafterError::InvalidFieldValue { field, .. } if field == "ipsec.ah.icv"),
"expected an ICV mismatch error, got {err:?}"
);
}
#[test]
fn verify_ah_ignores_mutable_ipv4_field_changes() {
let ah_bytes = compile_ah_packet();
let (decoded_ah, _, _) = decode_ah_parts(&ah_bytes).expect("decode AH parts");
let retimed_ip = Ipv4::new()
.protocol(IPPROTO_AH)
.src("192.0.2.1".parse().unwrap())
.dst("192.0.2.2".parse().unwrap())
.ttl(7); let packet: Packet = Packet::from_layer(retimed_ip)
/ decoded_ah
/ Tcp::new().sport(1234).dport(443)
/ Raw::from_bytes(vec![0xDE, 0xAD, 0xBE, 0xEF]);
verify_ah(&packet, 1, &verify_sa())
.expect("a mutable-field (TTL) change is canonicalized away and still verifies");
}
#[test]
fn decode_with_sa_records_verified_status() {
let ah_bytes = compile_ah_packet();
let registry = ProtocolRegistry::with_builtin_bindings();
let sa = verify_sa();
let seeded = Packet::from_layer(verify_ipv4());
let decoded = append_ah_packet_with_registry_sa(®istry, seeded, &ah_bytes, Some(&sa))
.expect("SA-aware decode + verify succeeds");
let ah = decoded
.get(1)
.and_then(|layer| layer.as_any().downcast_ref::<Ah>())
.expect("AH layer at index 1");
assert_eq!(
ah.verification_status(),
Some(true),
"verified status recorded on the AH layer"
);
let tcp = decoded
.layer::<Tcp>()
.expect("inner Tcp decoded in the clear");
assert_eq!(tcp.source_port_value(), 1234);
assert_eq!(tcp.destination_port_value(), 443);
}
#[test]
fn decode_with_sa_fails_closed_on_tampered_icv() {
let mut ah_bytes = compile_ah_packet();
ah_bytes[AH_FIXED_LEN] ^= 0x01;
let registry = ProtocolRegistry::with_builtin_bindings();
let sa = verify_sa();
let seeded = Packet::from_layer(verify_ipv4());
let err = append_ah_packet_with_registry_sa(®istry, seeded, &ah_bytes, Some(&sa))
.expect_err("a tampered ICV must fail the SA-aware decode");
assert!(
matches!(err, CrafterError::InvalidFieldValue { field, .. } if field == "ipsec.ah.icv"),
"expected an ICV mismatch error, got {err:?}"
);
}
#[test]
fn decode_without_sa_leaves_verification_status_unset() {
let ah_bytes = compile_ah_packet();
let registry = ProtocolRegistry::with_builtin_bindings();
let seeded = Packet::from_layer(verify_ipv4());
let decoded = append_ah_packet_with_registry_sa(®istry, seeded, &ah_bytes, None)
.expect("opaque decode without an SA succeeds");
let ah = decoded
.get(1)
.and_then(|layer| layer.as_any().downcast_ref::<Ah>())
.expect("AH layer at index 1");
assert_eq!(
ah.verification_status(),
None,
"no verification attempted without an SA"
);
}
}