diff --git a/appveyor.yml b/appveyor.yml index c8300abacf9d4d2ecb7da78ec35e0d0a542d32e5..351cf4ef960119081e10177f54cd114f2891a219 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -29,7 +29,7 @@ test_script: # Secondary unit tests - 'del test\bpf.uts' # Don't bother with BPF regression tests - - "for %%t in (test\\*.uts) do (%PYTHON%\\python -m coverage run -a bin\\UTscapy -f text -t %%t -F -K combined_modes || exit /b 42)" + - "for %%t in (test\\*.uts) do (%PYTHON%\\python -m coverage run -a bin\\UTscapy -f text -t %%t -F -K combined_modes_ccm || exit /b 42)" # Contrib unit tests - "for %%t in (scapy\\contrib\\*.uts) do (%PYTHON%\\python -m coverage run -a bin\\UTscapy -f text -t %%t -F -P \"load_contrib(\'%%~nt\')\" || exit /b 42)" diff --git a/doc/scapy/installation.rst b/doc/scapy/installation.rst index 9272bd4703ab57937265c01d3906cd116f2650f0..ac5ab68bf76f8fed5cae380346e6f12d78da1e71 100644 --- a/doc/scapy/installation.rst +++ b/doc/scapy/installation.rst @@ -166,11 +166,13 @@ Here are the topics involved and some examples that you can use to try if your i >>> enc=rdpcap("weplab-64bit-AA-managed.pcap") >>> enc.show() >>> enc[0] - >>> conf.wepkey="AA\x00\x00\x00" - >>> dec=Dot11PacketList(enc).toEthernet() - >>> dec.show() - >>> dec[0] + >>> conf.wepkey="AA\x00\x00\x00" + >>> dec=Dot11PacketList(enc).toEthernet() + >>> dec.show() + >>> dec[0] +* PKI operations and TLS decryption. `cryptography <https://cryptography.io>` is also needed. + * Fingerprinting. ``nmap_fp()`` needs `Nmap <http://nmap.org>`_. You need an `old version <http://nmap.org/dist-old/>`_ (before v4.23) that still supports first generation fingerprinting. .. code-block:: python @@ -187,8 +189,6 @@ Here are the topics involved and some examples that you can use to try if your i * VOIP. ``voip_play()`` needs `SoX <http://sox.sourceforge.net/>`_. -* IPsec Crypto Support. ``SecurityAssociation()`` needs `Pycrypto 2.7a1 <https://github.com/dlitz/pycrypto>`_. Combined AEAD modes such as GCM and CCM are not available yet because of cryptography restrictions. - Platform-specific instructions ============================== @@ -207,7 +207,13 @@ Debian/Ubuntu Just use the standard packages:: -$ sudo apt-get install tcpdump graphviz imagemagick python-gnuplot python-crypto python-pyx +$ sudo apt-get install tcpdump graphviz imagemagick python-gnuplot python-cryptography python-pyx + +Scapy optionally uses python-cryptography v1.7 or later. It has not been packaged for ``apt`` in less recent OS versions (e.g. Debian Jessie). If you need the cryptography-related methods, you may install the library with: + +.. code-block:: text + + # pip install cryptography Fedora ------ @@ -226,7 +232,7 @@ Some optional packages: .. code-block:: text - # yum install graphviz python-crypto sox PyX gnuplot numpy + # yum install graphviz python-cryptography sox PyX gnuplot numpy # cd /tmp # wget http://heanet.dl.sourceforge.net/sourceforge/gnuplot-py/gnuplot-py-1.8.tar.gz # tar xvfz gnuplot-py-1.8.tar.gz @@ -288,11 +294,11 @@ Here's how to install Scapy on OpenBSD 5.9+ Optional packages (OpenBSD only) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -py-crypto +py-cryptography .. code-block:: text - # pkg_add py-crypto + # pkg_add py-cryptography gnuplot and its Python binding: diff --git a/scapy/config.py b/scapy/config.py index fe541ed5705be605d90ea517eead7de7ce2a070d..6f1f9787569350b1842f3d77f3d1f650d0d034da 100755 --- a/scapy/config.py +++ b/scapy/config.py @@ -4,7 +4,7 @@ ## This program is published under a GPLv2 license """ -Implementation for of the configuration object. +Implementation of the configuration object. """ import os,time,socket,sys @@ -279,6 +279,18 @@ class LogLevel(object): obj._logLevel = val +def isCryptographyValid(): + """ + Check if the cryptography library is present, and if it is recent enough + (v1.7 or later). + """ + try: + import cryptography + except ImportError: + return False + from distutils.version import LooseVersion + return LooseVersion(cryptography.__version__) >= LooseVersion("1.7") + def _prompt_changer(attr,val): prompt = conf.prompt @@ -396,6 +408,7 @@ contribs: a dict which can be used by contrib layers to store local configuratio "tftp", "x509", "bluetooth", "dhcp6", "llmnr", "sctp", "vrrp", "ipsec", "lltd", "vxlan"] contribs = dict() + crypto_valid = isCryptographyValid() if not Conf.ipv6_enabled: @@ -404,7 +417,23 @@ if not Conf.ipv6_enabled: if m in Conf.load_layers: Conf.load_layers.remove(m) +if not Conf.crypto_valid: + log_scapy.warning("Crypto-related methods disabled for IPsec, Dot11 " + "and TLS layers (needs python-cryptography v1.7+).") conf=Conf() conf.logLevel=30 # 30=Warning + +def crypto_validator(func): + """ + This a decorator to be used for any method relying on the cryptography library. + Its behaviour depends on the 'crypto_valid' attribute of the global 'conf'. + """ + def func_in(*args, **kwargs): + if not conf.crypto_valid: + raise ImportError("Cannot execute crypto-related method! " + "Please install python-cryptography v1.7 or later.") + return func(*args, **kwargs) + return func_in + diff --git a/scapy/layers/dot11.py b/scapy/layers/dot11.py index f6f92b773d77dbfc90c01374582ecb6601b605c0..8474412e4898af07273d2582557e40dc63672df7 100644 --- a/scapy/layers/dot11.py +++ b/scapy/layers/dot11.py @@ -10,7 +10,7 @@ Wireless LAN according to IEEE 802.11. import re,struct from zlib import crc32 -from scapy.config import conf +from scapy.config import conf, crypto_validator from scapy.data import * from scapy.packet import * from scapy.fields import * @@ -20,14 +20,12 @@ from scapy.layers.l2 import * from scapy.layers.inet import IP, TCP -try: +if conf.crypto_valid: from cryptography.hazmat.backends import default_backend - from cryptography.hazmat.primitives.ciphers import ( - Cipher, - algorithms, - ) -except ImportError: - log_loading.info("Can't import python cryptography lib. Won't be able to decrypt WEP.") + from cryptography.hazmat.primitives.ciphers import Cipher, algorithms +else: + default_backend = Ciphers = algorithms = None + log_loading.info("Can't import python-cryptography v1.7+. Disabled WEP decryption/encryption.") ### Fields @@ -324,6 +322,18 @@ class Dot11WEP(Packet): StrField("wepdata",None,remain=4), IntField("icv",None) ] + @crypto_validator + def decrypt(self, key=None): + if key is None: + key = conf.wepkey + if key: + d = Cipher( + algorithms.ARC4(self.iv + key), + None, + default_backend(), + ).decryptor() + self.add_payload(LLC(d.update(self.wepdata) + d.finalize())) + def post_dissect(self, s): self.decrypt() @@ -332,36 +342,30 @@ class Dot11WEP(Packet): return Packet.build_payload(self) return "" - def post_build(self, p, pay): - if self.wepdata is None: - key = conf.wepkey - if key: - if self.icv is None: - pay += struct.pack("<I",crc32(pay)) - icv = "" - else: - icv = p[4:8] - e = Cipher( - algorithms.ARC4(self.iv+key), - None, - default_backend(), - ).encryptor() - p = p[:4]+e.update(pay)+e.finalize()+icv - else: - warning("No WEP key set (conf.wepkey).. strange results expected..") - return p - - - def decrypt(self,key=None): + @crypto_validator + def encrypt(self, p, pay, key=None): if key is None: key = conf.wepkey if key: - d = Cipher( - algorithms.ARC4(self.iv+key), + if self.icv is None: + pay += struct.pack("<I", crc32(pay)) + icv = "" + else: + icv = p[4:8] + e = Cipher( + algorithms.ARC4(self.iv + key), None, default_backend(), - ).decryptor() - self.add_payload(LLC(d.update(self.wepdata)+d.finalize())) + ).encryptor() + return p[:4] + e.update(pay) + e.finalize() + icv + else: + warning("No WEP key set (conf.wepkey).. strange results expected..") + return None + + def post_build(self, p, pay): + if self.wepdata is None: + p = self.encrypt(p, pay) + return p bind_layers( PrismHeader, Dot11, ) diff --git a/scapy/layers/ipsec.py b/scapy/layers/ipsec.py index dd9f5a9b7f76bbde36fe2e008ec0111ec7cfdef9..9153ac7c4494af93fabf0b313270241afe75a137 100644 --- a/scapy/layers/ipsec.py +++ b/scapy/layers/ipsec.py @@ -44,19 +44,15 @@ import os import socket import struct -from scapy.error import warning - +from scapy.config import conf, crypto_validator from scapy.data import IP_PROTOS from scapy.error import log_loading - -from scapy.fields import ByteEnumField, ByteField, StrField, XIntField, IntField, \ - ShortField, PacketField - +from scapy.fields import (ByteEnumField, ByteField, StrField, XIntField, + IntField, ShortField, PacketField) from scapy.packet import Packet, bind_layers, Raw - from scapy.layers.inet import IP, UDP -from scapy.layers.inet6 import IPv6, IPv6ExtHdrHopByHop, IPv6ExtHdrDestOpt, \ - IPv6ExtHdrRouting +from scapy.layers.inet6 import (IPv6, IPv6ExtHdrHopByHop, IPv6ExtHdrDestOpt, + IPv6ExtHdrRouting) #------------------------------------------------------------------------------ @@ -142,26 +138,20 @@ class _ESPPlain(Packet): return str(self.data) + self.padding + chr(self.padlen) + chr(self.nh) #------------------------------------------------------------------------------ -try: +if conf.crypto_valid: from cryptography.exceptions import InvalidTag from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import interfaces from cryptography.hazmat.primitives.ciphers import ( Cipher, algorithms, modes, ) -except ImportError: - log_loading.info("Can't import python cryptography lib. " +else: + log_loading.info("Can't import python-cryptography v1.7+. " "Disabled IPsec encryption/authentication.") - algorithms = None - Cipher = None - modes = 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).") + InvalidTag = default_backend = interfaces = None + Cipher = algorithms = modes = None #------------------------------------------------------------------------------ def _lcm(a, b): @@ -201,8 +191,9 @@ class CryptAlgo(object): self.mode = mode self.icv_size = icv_size - if self.mode is not None: - self.is_aead = issubclass(self.mode, modes.ModeWithAuthenticationTag) + if modes and self.mode is not None: + self.is_aead = issubclass(self.mode, + modes.ModeWithAuthenticationTag) else: self.is_aead = False @@ -248,6 +239,7 @@ class CryptAlgo(object): # XXX: random bytes for counters, so it is not wrong to do it that way return os.urandom(self.iv_size - self.salt_size) + @crypto_validator def new_cipher(self, key, iv, digest=None): """ @param key: the secret key, a byte string @@ -430,15 +422,13 @@ if algorithms: mode=modes.CBC) #------------------------------------------------------------------------------ -try: +if conf.crypto_valid: from cryptography.hazmat.primitives.hmac import HMAC from cryptography.hazmat.primitives.cmac import CMAC from cryptography.hazmat.primitives import hashes -except ImportError: +else: # no error if cryptography is not available but authentication won't be supported - HMAC = None - CMAC = None - hashes = None + HMAC = CMAC = hashes = None #------------------------------------------------------------------------------ class IPSecIntegrityError(Exception): @@ -478,6 +468,7 @@ class AuthAlgo(object): raise TypeError('invalid key size %s, must be one of %s' % (len(key), self.key_size)) + @crypto_validator def new_mac(self, key): """ @param key: a byte string diff --git a/scapy/layers/tls/__init__.py b/scapy/layers/tls/__init__.py index 98ee163d88ece95c6548dff00d36d276d714d492..2f20630887846cc99eb5259ebc8ed45c5d589353 100644 --- a/scapy/layers/tls/__init__.py +++ b/scapy/layers/tls/__init__.py @@ -7,12 +7,12 @@ Tools for handling TLS sessions and digital certificates. """ -try: - import cryptography -except ImportError: +from scapy.config import conf + +if not conf.crypto_valid: import logging log_loading = logging.getLogger("scapy.loading") - log_loading.info("Can't import python cryptography lib. Disabled certificate manipulation tools") + log_loading.info("Can't import python-cryptography v1.7+. Disabled PKCS #1 signing/verifying.") try: import ecdsa diff --git a/scapy/layers/tls/cert.py b/scapy/layers/tls/cert.py index 56e505fb290287e45829e8ee69a3726d8b324a5c..8d81097e0668b2d7938dfb7e5816061966d65b41 100644 --- a/scapy/layers/tls/cert.py +++ b/scapy/layers/tls/cert.py @@ -29,9 +29,13 @@ Supports both RSA and ECDSA objects. import base64, os, time import ecdsa -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.hazmat.primitives import hashes + +from scapy.config import conf, crypto_validator +if conf.crypto_valid: + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives.asymmetric import rsa +else: + default_backend = rsa = None from scapy.layers.tls.crypto.curves import import_curve from scapy.layers.tls.crypto.pkcs1 import pkcs_os2ip, pkcs_i2osp, mapHashFunc @@ -199,7 +203,7 @@ class _PubKeyFactory(_PKIObjMaker): obj.__class__ = PubKeyECDSA obj.updateWith(spki) else: - raise Exception("Unsupported publicKey type") + raise marker = "PUBLIC KEY" except: try: @@ -241,6 +245,7 @@ class PubKeyRSA(_PKIObj, PubKey, _EncryptAndVerifyRSA): Wrapper for RSA keys based on _EncryptAndVerifyRSA from crypto/pkcs1.py Use the 'key' attribute to access original object. """ + @crypto_validator def updateWith(self, pubkey): self.modulus = pubkey.modulus.val self.modulusLen = len(binrepr(pubkey.modulus.val)) @@ -391,6 +396,7 @@ class PrivKeyRSA(_PKIObj, PrivKey, _EncryptAndVerifyRSA, _DecryptAndSignRSA): Wrapper for RSA keys based on _DecryptAndSignRSA from crypto/pkcs1.py Use the 'key' attribute to access original object. """ + @crypto_validator def updateWith(self, privkey): self.modulus = privkey.modulus.val self.modulusLen = len(binrepr(privkey.modulus.val)) diff --git a/scapy/layers/tls/crypto/pkcs1.py b/scapy/layers/tls/crypto/pkcs1.py index 3ae21e543cc1b70374d00107127c24e77b7158f6..956268e79adaa7078a8f00da58365df2b7e7c015 100644 --- a/scapy/layers/tls/crypto/pkcs1.py +++ b/scapy/layers/tls/crypto/pkcs1.py @@ -10,10 +10,14 @@ PKCS #1 methods as defined in RFC 3447. import os, popen2, tempfile import math, random, struct -from cryptography.exceptions import InvalidSignature -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.asymmetric import padding +from scapy.config import conf, crypto_validator +if conf.crypto_valid: + from cryptography.exceptions import InvalidSignature + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.asymmetric import padding +else: + InvalidSignature = dafault_backend = hashes = padding = None ##################################################################### @@ -108,40 +112,44 @@ def pkcs_ilen(n): # PKCS#1 v2.1, MD4 should not be used. # - 'tls' one is the concatenation of both md5 and sha1 hashes used # by SSL/TLS when signing/verifying things -def _hashWrapper(hash_algo, message, backend=default_backend()): - digest = hashes.Hash(hash_algo, backend).update(message) - return digest.finalize() - -_hashFuncParams = { - "md5" : (16, - hashes.MD5, - lambda x: _hashWrapper(hashes.MD5, x), - '\x30\x20\x30\x0c\x06\x08\x2a\x86\x48\x86\xf7\x0d\x02\x05\x05\x00\x04\x10'), - "sha1" : (20, - hashes.SHA1, - lambda x: _hashWrapper(hashes.SHA1, x), - '\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14'), - "sha224" : (28, - hashes.SHA224, - lambda x: _hashWrapper(hashes.SHA224, x), - '\x30\x2d\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x04\x05\x00\x04\x1c'), - "sha256" : (32, - hashes.SHA256, - lambda x: _hashWrapper(hashes.SHA256, x), - '\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20'), - "sha384" : (48, - hashes.SHA384, - lambda x: _hashWrapper(hashes.SHA384, x), - '\x30\x41\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x02\x05\x00\x04\x30'), - "sha512" : (64, - hashes.SHA512, - lambda x: _hashWrapper(hashes.SHA512, x), - '\x30\x51\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x03\x05\x00\x04\x40'), - "tls" : (36, - None, - lambda x: _hashWrapper(hashes.MD5, x) + _hashWrapper(hashes.SHA1, x), - '') - } + +_hashFuncParams = {} +if conf.crypto_valid: + + def _hashWrapper(hash_algo, message, backend=default_backend()): + digest = hashes.Hash(hash_algo, backend).update(message) + return digest.finalize() + + _hashFuncParams = { + "md5" : (16, + hashes.MD5, + lambda x: _hashWrapper(hashes.MD5, x), + '\x30\x20\x30\x0c\x06\x08\x2a\x86\x48\x86\xf7\x0d\x02\x05\x05\x00\x04\x10'), + "sha1" : (20, + hashes.SHA1, + lambda x: _hashWrapper(hashes.SHA1, x), + '\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14'), + "sha224" : (28, + hashes.SHA224, + lambda x: _hashWrapper(hashes.SHA224, x), + '\x30\x2d\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x04\x05\x00\x04\x1c'), + "sha256" : (32, + hashes.SHA256, + lambda x: _hashWrapper(hashes.SHA256, x), + '\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20'), + "sha384" : (48, + hashes.SHA384, + lambda x: _hashWrapper(hashes.SHA384, x), + '\x30\x41\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x02\x05\x00\x04\x30'), + "sha512" : (64, + hashes.SHA512, + lambda x: _hashWrapper(hashes.SHA512, x), + '\x30\x51\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x03\x05\x00\x04\x40'), + "tls" : (36, + None, + lambda x: _hashWrapper(hashes.MD5, x) + _hashWrapper(hashes.SHA1, x), + '') + } def mapHashFunc(hashStr): try: @@ -424,6 +432,7 @@ def create_temporary_ca_path(anchor_list, folder): ##################################################################### class _EncryptAndVerifyRSA(object): + @crypto_validator def encrypt(self, m, t=None, h=None, mgf=None, L=None): """ Encrypt message 'm' using 't' encryption scheme where 't' can be: @@ -484,6 +493,7 @@ class _EncryptAndVerifyRSA(object): _warning("Key.encrypt(): Unknown encryption type (%s) provided" % t) return None + @crypto_validator def verify(self, M, S, t=None, h=None, mgf=None, sLen=None): """ Verify alleged signature 'S' is indeed the signature of message 'M' diff --git a/test/ipsec.uts b/test/ipsec.uts index 9707dce8b4c09a4d851c0abbbd24e4a92a4feed7..c271cc8ee3ee1a1cb0f4f2d63be4dc8a96bb6c8a 100644 --- a/test/ipsec.uts +++ b/test/ipsec.uts @@ -2491,7 +2491,6 @@ assert(d[TCP] == p[TCP]) ####################################### = 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) @@ -2526,7 +2525,7 @@ assert(d == p) ####################################### = IPv4 / ESP - Tunnel - AES-CCM - NULL -~ combined_modes combined_modes_ccm +~ combined_modes_ccm p = IP(src='1.1.1.1', dst='2.2.2.2') p /= TCP(sport=45012, dport=80)