From 1eb3bd2f4537b974a406b5539b8461a0bc038752 Mon Sep 17 00:00:00 2001 From: iafaneh Date: Wed, 28 Jan 2026 14:46:10 +0200 Subject: [PATCH 1/2] ipsec: support truncated ICV sizes (8, 12) for AES-GCM When icv_size is set to 8 or 12, the encrypted packet should contain a truncated ICV of that length. Previously, the implementation always defaulted to the full 16-byte ICV for AES-GCM, ignoring the configured truncation. This change ensures: 1. Encryption truncates the generated tag to the requested icv_size using the Cipher API. 2. Decryption correctly verifies packets with truncated tags using the Cipher API. Signed-off-by: Iman Afaneh --- scapy/layers/ipsec.py | 71 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/scapy/layers/ipsec.py b/scapy/layers/ipsec.py index 8cff919102a..bf2b777ef56 100644 --- a/scapy/layers/ipsec.py +++ b/scapy/layers/ipsec.py @@ -420,15 +420,33 @@ def encrypt(self, sa, esp, key, icv_size=None, esn_en=False, esn=0): aad = struct.pack('!LL', esp.spi, esp.seq) if self.ciphers_aead_api: # New API - if self.cipher == aead.AESCCM: + # For ciphers that don't support custom tag_length (e.g., AES-GCM), + # use the lower-level API when icv_size differs from default + use_lower_level_api = (self.cipher == aead.AESGCM and + icv_size != self.icv_size) + + if use_lower_level_api: + # Use lower-level API for truncated ICV support + # For AES-GCM, we need to use algorithms.AES with modes.GCM + cipher = Cipher( + algorithms.AES(key), + modes.GCM(mode_iv), + default_backend(), + ) + encryptor = cipher.encryptor() + encryptor.authenticate_additional_data(aad) + data = encryptor.update(data) + encryptor.finalize() + data += encryptor.tag[:icv_size] + elif self.cipher == aead.AESCCM: cipher = self.cipher(key, tag_length=icv_size) + data = cipher.encrypt(mode_iv, data, aad) else: cipher = self.cipher(key) - if self.name == 'AES-NULL-GMAC': - # Special case for GMAC (rfc 4543 sect 3) - data = data + cipher.encrypt(mode_iv, b"", aad + esp.iv + data) - else: - data = cipher.encrypt(mode_iv, data, aad) + if self.name == 'AES-NULL-GMAC': + # Special case for GMAC (rfc 4543 sect 3) + data = data + cipher.encrypt(mode_iv, b"", aad + esp.iv + data) + else: + data = cipher.encrypt(mode_iv, data, aad) else: cipher = self.new_cipher(key, mode_iv) encryptor = cipher.encryptor() @@ -475,18 +493,41 @@ def decrypt(self, sa, esp, key, icv_size=None, esn_en=False, esn=0): aad = struct.pack('!LL', esp.spi, esp.seq) if self.ciphers_aead_api: # New API - if self.cipher == aead.AESCCM: + # For ciphers that don't support custom tag_length (e.g., AES-GCM), + # use the lower-level API when icv_size differs from default + use_lower_level_api = (self.cipher == aead.AESGCM and + icv_size != self.icv_size) + + if use_lower_level_api: + # Use lower-level API for truncated ICV support + # For AES-GCM, we need to use algorithms.AES with modes.GCM + cipher = Cipher( + algorithms.AES(key), + modes.GCM(mode_iv, icv, len(icv)), + default_backend(), + ) + decryptor = cipher.decryptor() + decryptor.authenticate_additional_data(aad) + try: + data = decryptor.update(data) + decryptor.finalize() + except InvalidTag as err: + raise IPSecIntegrityError(err) + elif self.cipher == aead.AESCCM: cipher = self.cipher(key, tag_length=icv_size) + try: + data = cipher.decrypt(mode_iv, data + icv, aad) + except InvalidTag as err: + raise IPSecIntegrityError(err) else: cipher = self.cipher(key) - try: - if self.name == 'AES-NULL-GMAC': - # Special case for GMAC (rfc 4543 sect 3) - data = data + cipher.decrypt(mode_iv, icv, aad + iv + data) - else: - data = cipher.decrypt(mode_iv, data + icv, aad) - except InvalidTag as err: - raise IPSecIntegrityError(err) + try: + if self.name == 'AES-NULL-GMAC': + # Special case for GMAC (rfc 4543 sect 3) + data = data + cipher.decrypt(mode_iv, icv, aad + iv + data) + else: + data = cipher.decrypt(mode_iv, data + icv, aad) + except InvalidTag as err: + raise IPSecIntegrityError(err) else: cipher = self.new_cipher(key, mode_iv, icv) decryptor = cipher.decryptor() From ec22ff5e3f215a2319abc7192a6bba0233199078 Mon Sep 17 00:00:00 2001 From: iafaneh Date: Wed, 28 Jan 2026 14:46:54 +0200 Subject: [PATCH 2/2] ipsec.uts: add unit test for ipsec crypt_icv_size 12 Signed-off-by: Iman Afaneh --- test/scapy/layers/ipsec.uts | 43 +++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/test/scapy/layers/ipsec.uts b/test/scapy/layers/ipsec.uts index 0af1eefdc0b..f5e27bc4e5e 100644 --- a/test/scapy/layers/ipsec.uts +++ b/test/scapy/layers/ipsec.uts @@ -1885,6 +1885,49 @@ try: except IPSecIntegrityError as err: err +####################################### += IPv4 / ESP - Transport - AES-GCM - Truncated ICV (12 bytes) +~ crypto_advanced + +p = IP(src='1.1.1.1', dst='2.2.2.2') +p /= TCP(sport=45012, dport=80) +p /= Raw('testdata') +p = IP(raw(p)) +p + +sa = SecurityAssociation(ESP, spi=0x222, + crypt_algo='AES-GCM', crypt_key=b'16bytekey+4bytenonce', + crypt_icv_size=12, + auth_algo='NULL', auth_key=None) + +e = sa.encrypt(p) +e + +assert isinstance(e, IP) +assert e.src == '1.1.1.1' and e.dst == '2.2.2.2' +assert e.chksum != p.chksum +assert e.proto == socket.IPPROTO_ESP +assert e.haslayer(ESP) +assert not e.haslayer(TCP) +assert e[ESP].spi == sa.spi +* after encryption the original packet payload should NOT be readable +assert b'testdata' not in e[ESP].data + +d = sa.decrypt(e) +d + +* after decryption original packet should be preserved +assert d[TCP] == p[TCP] + +* test with altered packet - integrity verification should fail +e2 = sa.encrypt(p) +e2[ESP].data = e2[ESP].data[:-1] + b'\xff' +try: + d = sa.decrypt(e2) + assert False, "Integrity check should have failed" +except IPSecIntegrityError as err: + err + ####################################### = IPv4 / ESP - Transport - AES-CCM - NULL ~ crypto_advanced