Repository: secdev/scapy

Issue:
Add ESN support for IPSec's AH
#### Brief description
According to [RFC 4302](https://tools.ietf.org/html/rfc4302#section-3.3.3.2.2), for Authentication Header (AH) with Extended Sequence Number (ESN) enabled, the computation of the ICV field needs to implicitly append the 32 most significant bits of the ESN to the packet. This is currently not implemented. 

#### Environment

-   Scapy version: `2.4.3`
-   Python version: `3.7`
-   Operating System: `Windows 10`

The feature seems simple to implement. I'll give it a try, following the current pattern of ESN handling in the ESP header
Add ESN support for IPSec's AH
#### Brief description
According to [RFC 4302](https://tools.ietf.org/html/rfc4302#section-3.3.3.2.2), for Authentication Header (AH) with Extended Sequence Number (ESN) enabled, the computation of the ICV field needs to implicitly append the 32 most significant bits of the ESN to the packet. This is currently not implemented. 

#### Environment

-   Scapy version: `2.4.3`
-   Python version: `3.7`
-   Operating System: `Windows 10`

The feature seems simple to implement. I'll give it a try, following the current pattern of ESN handling in the ESP header


Discussion:

Resolving patch:
diff --git a/scapy/layers/ipsec.py b/scapy/layers/ipsec.py
--- a/scapy/layers/ipsec.py
+++ b/scapy/layers/ipsec.py
@@ -546,12 +546,15 @@ def new_mac(self, key):
         else:
             return self.mac(key, self.digestmod(), default_backend())
 
-    def sign(self, pkt, key):
+    def sign(self, pkt, key, esn_en=False, esn=0):
         """
         Sign an IPsec (ESP or AH) packet with this algo.
 
         :param pkt:    a packet that contains a valid encrypted ESP or AH layer
         :param key:    the authentication key, a byte string
+        :param esn_en: extended sequence number enable which allows to use
+                       64-bit sequence number instead of 32-bit
+        :param esn: extended sequence number (32 MSB)
 
         :returns: the signed packet
         """
@@ -566,17 +569,24 @@ def sign(self, pkt, key):
 
         elif pkt.haslayer(AH):
             clone = zero_mutable_fields(pkt.copy(), sending=True)
-            mac.update(raw(clone))
+            if esn_en:
+                temp = raw(clone) + struct.pack('!L', esn)
+            else:
+                temp = raw(clone)
+            mac.update(temp)
             pkt[AH].icv = mac.finalize()[:self.icv_size]
 
         return pkt
 
-    def verify(self, pkt, key):
+    def verify(self, pkt, key, esn_en=False, esn=0):
         """
         Check that the integrity check value (icv) of a packet is valid.
 
         :param pkt:    a packet that contains a valid encrypted ESP or AH layer
         :param key:    the authentication key, a byte string
+        :param esn_en: extended sequence number enable which allows to use
+                       64-bit sequence number instead of 32-bit
+        :param esn: extended sequence number (32 MSB)
 
         :raise scapy.layers.ipsec.IPSecIntegrityError: if the integrity check
             fails
@@ -592,6 +602,7 @@ def verify(self, pkt, key):
             pkt_icv = pkt.data[len(pkt.data) - self.icv_size:]
             clone = pkt.copy()
             clone.data = clone.data[:len(clone.data) - self.icv_size]
+            temp = raw(clone)
 
         elif pkt.haslayer(AH):
             if len(pkt[AH].icv) != self.icv_size:
@@ -600,8 +611,12 @@ def verify(self, pkt, key):
                 pkt[AH].icv = pkt[AH].icv[:self.icv_size]
             pkt_icv = pkt[AH].icv
             clone = zero_mutable_fields(pkt.copy(), sending=False)
+            if esn_en:
+                temp = raw(clone) + struct.pack('!L', esn)
+            else:
+                temp = raw(clone)
 
-        mac.update(raw(clone))
+        mac.update(temp)
         computed_icv = mac.finalize()[:self.icv_size]
 
         # XXX: Cannot use mac.verify because the ICV can be truncated
@@ -926,7 +941,7 @@ def _encrypt_esp(self, pkt, seq_num=None, iv=None, esn_en=None, esn=None):
 
         return ip_header / esp
 
-    def _encrypt_ah(self, pkt, seq_num=None):
+    def _encrypt_ah(self, pkt, seq_num=None, esn_en=False, esn=0):
 
         ah = AH(spi=self.spi, seq=seq_num or self.seq_num,
                 icv=b"\x00" * self.auth_algo.icv_size)
@@ -968,7 +983,10 @@ def _encrypt_ah(self, pkt, seq_num=None):
         else:
             ip_header.plen = len(ip_header.payload) + len(ah) + len(payload)
 
-        signed_pkt = self.auth_algo.sign(ip_header / ah / payload, self.auth_key)  # noqa: E501
+        signed_pkt = self.auth_algo.sign(ip_header / ah / payload,
+                                         self.auth_key,
+                                         esn_en=esn_en or self.esn_en,
+                                         esn=esn or self.esn)
 
         # sequence number must always change, unless specified by the user
         if seq_num is None:
@@ -1001,7 +1019,8 @@ def encrypt(self, pkt, seq_num=None, iv=None, esn_en=None, esn=None):
                                      iv=iv, esn_en=esn_en,
                                      esn=esn)
         else:
-            return self._encrypt_ah(pkt, seq_num=seq_num)
+            return self._encrypt_ah(pkt, seq_num=seq_num,
+                                    esn_en=esn_en, esn=esn)
 
     def _decrypt_esp(self, pkt, verify=True, esn_en=None, esn=None):
 
@@ -1048,11 +1067,13 @@ def _decrypt_esp(self, pkt, verify=True, esn_en=None, esn=None):
             # reassemble the ip_header with the ESP payload
             return ip_header / cls(esp.data)
 
-    def _decrypt_ah(self, pkt, verify=True):
+    def _decrypt_ah(self, pkt, verify=True, esn_en=None, esn=None):
 
         if verify:
             self.check_spi(pkt)
-            self.auth_algo.verify(pkt, self.auth_key)
+            self.auth_algo.verify(pkt, self.auth_key,
+                                  esn_en=esn_en or self.esn_en,
+                                  esn=esn or self.esn)
 
         ah = pkt[AH]
         payload = ah.payload
@@ -1100,6 +1121,6 @@ def decrypt(self, pkt, verify=True, esn_en=None, esn=None):
             return self._decrypt_esp(pkt, verify=verify,
                                      esn_en=esn_en, esn=esn)
         elif self.proto is AH and pkt.haslayer(AH):
-            return self._decrypt_ah(pkt, verify=verify)
+            return self._decrypt_ah(pkt, verify=verify, esn_en=esn_en, esn=esn)
         else:
             raise TypeError('%s has no %s layer' % (pkt, self.proto.name))


Test patch:
diff --git a/test/ipsec.uts b/test/ipsec.uts
--- a/test/ipsec.uts
+++ b/test/ipsec.uts
@@ -2469,6 +2469,76 @@ assert(e[AH].spi == sa.spi)
 * simulate the alteration of the packet before verification
 e[TCP].dport = 46
 
+* integrity verification should fail
+try:
+    d = sa.decrypt(e)
+    assert(False)
+except IPSecIntegrityError as err:
+    err
+
+#######################################
+= IPv4 / AH - Transport - AES-CMAC-96 -- ESN
+
+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(AH, spi=0x222,
+                         auth_algo='AES-CMAC-96', auth_key=b'sixteenbytes key',
+                         esn_en=True, esn=0x200)
+
+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)
+* the encrypted packet should have an AH layer
+assert(e.proto == socket.IPPROTO_AH)
+assert(e.haslayer(AH))
+assert(e.haslayer(TCP))
+assert(e[AH].spi == sa.spi)
+
+* alter mutable fields in the packet
+e.ttl = 2
+
+* integrity verification should pass
+d = sa.decrypt(e)
+d
+
+* after decryption the original packet should be unaltered
+assert(d[TCP] == p[TCP])
+
+#######################################
+= IPv4 / AH - Transport - AES-CMAC-96 - altered packet -- ESN
+
+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(AH, spi=0x222,
+                         auth_algo='AES-CMAC-96', auth_key=b'sixteenbytes key',
+                         esn_en=True, esn=0x200)
+
+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)
+* the encrypted packet should have an AH layer
+assert(e.proto == socket.IPPROTO_AH)
+assert(e.haslayer(AH))
+assert(e.haslayer(TCP))
+assert(e[AH].spi == sa.spi)
+
+* simulate the alteration of the packet before verification
+e[TCP].dport = 46
+
 * integrity verification should fail
 try:
     d = sa.decrypt(e)
@@ -2880,6 +2950,76 @@ assert(e[AH].spi == sa.spi)
 * simulate the alteration of the packet before verification
 e.dst = '4.4.4.4'
 
+* integrity verification should fail
+try:
+    d = sa.decrypt(e)
+    assert(False)
+except IPSecIntegrityError as err:
+    err
+
+#######################################
+= IPv4 / AH - Tunnel - AES-CMAC-96 -- ESN
+
+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(AH, spi=0x222,
+                         auth_algo='AES-CMAC-96', auth_key=b'sixteenbytes key',
+                         tunnel_header=IP(src='11.11.11.11', dst='22.22.22.22'),
+                         esn_en=True, esn=0x200)
+
+e = sa.encrypt(p)
+e
+
+assert(isinstance(e, IP))
+assert(e.src == '11.11.11.11' and e.dst == '22.22.22.22')
+assert(e.chksum != p.chksum)
+assert(e.proto == socket.IPPROTO_AH)
+assert(e.haslayer(AH))
+assert(e.haslayer(TCP))
+assert(e[AH].spi == sa.spi)
+
+* alter mutable fields in the packet
+e.ttl = 2
+
+* integrity verification should pass
+d = sa.decrypt(e)
+d
+
+* after decryption the original packet should be unaltered
+assert(d == p)
+
+#######################################
+= IPv4 / AH - Tunnel - AES-CMAC-96 - altered packet -- ESN
+
+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(AH, spi=0x222,
+                         auth_algo='AES-CMAC-96', auth_key=b'sixteenbytes key',
+                         tunnel_header=IP(src='11.11.11.11', dst='22.22.22.22'),
+                         esn_en=True, esn=0x200)
+
+e = sa.encrypt(p)
+e
+
+assert(isinstance(e, IP))
+assert(e.src == '11.11.11.11' and e.dst == '22.22.22.22')
+assert(e.chksum != p.chksum)
+assert(e.proto == socket.IPPROTO_AH)
+assert(e.haslayer(AH))
+assert(e.haslayer(TCP))
+assert(e[AH].spi == sa.spi)
+
+* simulate the alteration of the packet before verification
+e.dst = '4.4.4.4'
+
 * integrity verification should fail
 try:
     d = sa.decrypt(e)