From 620f195c650e869f7fa27a881bc52e482f8d7ce8 Mon Sep 17 00:00:00 2001 From: Daniel Collins Date: Wed, 26 Oct 2016 01:03:37 -0700 Subject: [PATCH] =?UTF-8?q?Corrected=20ICV=20length=20for=20AES-GCM=20and?= =?UTF-8?q?=20AES-CCM=20in=20IPSec=20layer.=20Added=20un=E2=80=A6=20(#269)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Corrected ICV length for AES-GCM and AES-CCM in IPSec layer. Added unit tests to the ipsec campaign. * Added travis support for pycrypto 2.7a1 combined modes. * Updated documentation for pycrypto installation. --- .travis.yml | 7 ++++ .travis/install.sh | 13 ++++++- .travis/test.sh | 8 ++++- doc/scapy/installation.rst | 12 +++++++ scapy/layers/ipsec.py | 23 +++++++++++-- test/ipsec.uts | 70 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 128 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1ed6cb21a..7ce325581 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,6 +28,13 @@ matrix: - TRAVIS_SUDO=sudo - SCAPY_USE_PCAPDNET=yes + - os: linux + sudo: required + python: 2.7 + env: + - TRAVIS_SUDO=sudo + - TEST_COMBINED_MODES=yes + - os: osx language: generic env: diff --git a/.travis/install.sh b/.travis/install.sh index fd0e68763..79f41dd7f 100644 --- a/.travis/install.sh +++ b/.travis/install.sh @@ -3,7 +3,18 @@ if [ -z $TRAVIS_SUDO ] && [ "$TRAVIS_OS_NAME" = "osx" ] then PIP_INSTALL_FLAGS="--user" fi -$TRAVIS_SUDO pip install $PIP_INSTALL_FLAGS pycrypto ecdsa mock +$TRAVIS_SUDO pip install $PIP_INSTALL_FLAGS ecdsa mock + +# Pycrypto 2.7a1 isn't available on PyPi +if [ "$TEST_COMBINED_MODES" = "yes" ] +then + curl -sL https://github.com/dlitz/pycrypto/archive/v2.7a1.tar.gz | tar xz + cd pycrypto-2.7a1 + python setup.py build + $TRAVIS_SUDO python setup.py install +else + $TRAVIS_SUDO pip install $PIP_INSTALL_FLAGS pycrypto +fi # Install pcap & dnet if [ ! -z $SCAPY_USE_PCAPDNET ] diff --git a/.travis/test.sh b/.travis/test.sh index fca107715..870559a87 100644 --- a/.travis/test.sh +++ b/.travis/test.sh @@ -4,7 +4,13 @@ python -c "from scapy.all import *; print conf" # Don't run tests that requires root privileges if [ -z $TRAVIS_SUDO ] then - UT_FLAGS="-K netaccess" + UT_FLAGS="-K netaccess " +fi + +# Test AEAD modes in IPSec if available +if [ "$TEST_COMBINED_MODES" != "yes" ] +then + UT_FLAGS+="-K combined_modes " fi # Run unit tests diff --git a/doc/scapy/installation.rst b/doc/scapy/installation.rst index dde7a71c3..91fa69f0a 100644 --- a/doc/scapy/installation.rst +++ b/doc/scapy/installation.rst @@ -190,6 +190,18 @@ Here are the topics involved and some examples that you can use to try if your i * VOIP. ``voip_play()`` needs `SoX `_. +* IPSec Crypto Support. ``SecurityAssociation()`` needs `Pycrypto `_. Combined AEAD modes such as GCM and CCM require pycrypto2.7a1, which is only available from source (no pip or package). + + .. code-block:: text + + # pycrypto 2.6 install + sudo pip install pycrypto + + # pycrypto 2.7a1 install + curl -sL https://github.com/dlitz/pycrypto/archive/v2.7a1.tar.gz | tar xz + cd pycrypto-2.7a1 + python setup.py build + sudo python setup.py install Platform-specific instructions ============================== diff --git a/scapy/layers/ipsec.py b/scapy/layers/ipsec.py index b61861ec8..431bc20aa 100644 --- a/scapy/layers/ipsec.py +++ b/scapy/layers/ipsec.py @@ -41,6 +41,8 @@ True import socket import struct +from scapy.error import warning + try: from Crypto.Util.number import GCD as gcd except ImportError: @@ -152,6 +154,7 @@ try: from Crypto import Random except ImportError: # no error if pycrypto is not available but encryption won't be supported + warning("IPSec encryption not supported (pycrypto required).") AES = None DES = None DES3 = None @@ -159,6 +162,12 @@ except ImportError: Blowfish = None Random = None +try: + from Crypto.Cipher.AES import MODE_GCM + from Crypto.Cipher.AES import MODE_CCM +except ImportError: + warning("Combined crypto modes not available for IPSec (pycrypto 2.7a1 required).") + #------------------------------------------------------------------------------ def _lcm(a, b): """ @@ -186,16 +195,20 @@ class CryptAlgo(object): @param key_size: an integer or list/tuple of integers. If specified, force the secret keys length to one of the values. Defaults to the `key_size` of the cipher. + @param icv_size: the length of the integrity check value of this algo. + Only used in this class for AEAD algorithms. """ self.name = name self.cipher = cipher self.mode = mode - self.icv_size = icv_size self.is_aead = (hasattr(self.cipher, 'MODE_GCM') and self.mode == self.cipher.MODE_GCM) or \ (hasattr(self.cipher, 'MODE_CCM') and self.mode == self.cipher.MODE_CCM) + if icv_size is not None: + self.icv_size = icv_size + if block_size is not None: self.block_size = block_size elif cipher is not None: @@ -389,19 +402,23 @@ if AES: block_size=1, iv_size=8, key_size=(16 + 4, 24 + 4, 32 + 4)) + + # AEAD algorithms are only supported in pycrypto 2.7a1+ + # they also have an additional field icv_size, which is usually + # populated by an auth algo when signing and verifying signatures. if hasattr(AES, "MODE_GCM"): CRYPT_ALGOS['AES-GCM'] = CryptAlgo('AES-GCM', cipher=AES, mode=AES.MODE_GCM, iv_size=8, - icv_size=8, + icv_size=16, key_size=(16 + 4, 24 + 4, 32 + 4)) if hasattr(AES, "MODE_CCM"): CRYPT_ALGOS['AES-CCM'] = CryptAlgo('AES-CCM', cipher=AES, mode=AES.MODE_CCM, iv_size=8, - icv_size=8, + icv_size=16, key_size=(16 + 4, 24 + 4, 32 + 4)) if DES: CRYPT_ALGOS['DES'] = CryptAlgo('DES', diff --git a/test/ipsec.uts b/test/ipsec.uts index bf13075ae..dfa8ce1c7 100644 --- a/test/ipsec.uts +++ b/test/ipsec.uts @@ -139,6 +139,76 @@ d * after decryption original packet should be preserved assert(d == p) +####################################### += IPv4 / ESP - Tunnel - AES-GCM - NULL +~ combined_modes + +p = IP(src='1.1.1.1', dst='2.2.2.2') +p /= TCP(sport=45012, dport=80) +p /= Raw('testdata') +p = IP(str(p)) +p + +sa = SecurityAssociation(ESP, spi=0x222, + crypt_algo='AES-GCM', crypt_key='16bytekey+4bytenonce', + auth_algo='NULL', auth_key=None, + tunnel_header=IP(src='11.11.11.11', dst='22.22.22.22')) + +e = sa.encrypt(p) +e + +assert(isinstance(e, IP)) +* after encryption packet should be encapsulated with the given ip tunnel header +assert(e.src == '11.11.11.11' and e.dst == '22.22.22.22') +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('testdata' not in e[ESP].data) + +d = sa.decrypt(e) +d + +* after decryption original packet should be preserved +assert(d == p) + +####################################### += IPv4 / ESP - Tunnel - AES-CCM - NULL +~ combined_modes + +p = IP(src='1.1.1.1', dst='2.2.2.2') +p /= TCP(sport=45012, dport=80) +p /= Raw('testdata') +p = IP(str(p)) +p + +sa = SecurityAssociation(ESP, spi=0x222, + crypt_algo='AES-CCM', crypt_key='16bytekey+4bytenonce', + auth_algo='NULL', auth_key=None, + tunnel_header=IP(src='11.11.11.11', dst='22.22.22.22')) + +e = sa.encrypt(p) +e + +assert(isinstance(e, IP)) +* after encryption packet should be encapsulated with the given ip tunnel header +assert(e.src == '11.11.11.11' and e.dst == '22.22.22.22') +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('testdata' not in e[ESP].data) + +d = sa.decrypt(e) +d + +* after decryption original packet should be preserved +assert(d == p) + ####################################### = IPv4 / ESP - Tunnel - NULL - SHA2-256-128