diff options
author | Ilya Etingof <etingof@gmail.com> | 2018-02-16 08:33:28 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-02-16 08:33:28 +0100 |
commit | cd5db32c6c4e33b02b74f650aeadeaba9d31130d (patch) | |
tree | a67c5aa5eb6f3a81edeec41fc4c9678b7fc69ae3 | |
parent | b42880e3ead36d586638b4aeb0d8725a30dc865b (diff) | |
parent | eed17d7cf8f24acce069725a0ee2ff740cf4e946 (diff) | |
download | pysnmp-git-cd5db32c6c4e33b02b74f650aeadeaba9d31130d.tar.gz |
Merge pull request #133 from mattsb42-aws/hybrid-crypto
Move to a hybrid crypto backend, using pyca/cryptography when available but failing back to PyCryptodomex for Python versions that pyca/cryptography does not support
-rw-r--r-- | .gitignore | 6 | ||||
-rw-r--r-- | .travis.yml | 2 | ||||
-rw-r--r-- | CHANGES.txt | 6 | ||||
-rw-r--r-- | README.md | 4 | ||||
-rw-r--r-- | pysnmp/crypto/__init__.py | 131 | ||||
-rw-r--r-- | pysnmp/crypto/aes.py | 67 | ||||
-rw-r--r-- | pysnmp/crypto/des.py | 74 | ||||
-rw-r--r-- | pysnmp/crypto/des3.py | 67 | ||||
-rw-r--r-- | pysnmp/proto/secmod/eso/priv/des3.py | 24 | ||||
-rw-r--r-- | pysnmp/proto/secmod/rfc3414/priv/des.py | 22 | ||||
-rw-r--r-- | pysnmp/proto/secmod/rfc3826/priv/aes.py | 23 | ||||
-rw-r--r-- | requirements.txt | 7 | ||||
-rw-r--r-- | setup.py | 20 | ||||
-rw-r--r-- | tox.ini | 5 |
14 files changed, 391 insertions, 67 deletions
@@ -23,3 +23,9 @@ docs/source/.templates/layout.html # Virtual envs venv* + +# Tox +.tox/ + +# Pyenv +.python-version diff --git a/.travis.yml b/.travis.yml index eaf5f481..56f9a710 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,4 +16,4 @@ install: - pip install -e . - pip install pysnmp-mibs script: - - sh runtests.sh + - travis_wait 20 sh runtests.sh diff --git a/CHANGES.txt b/CHANGES.txt index d81161a6..c55ddc16 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,10 @@ +Revision 4.?.?, released 2018-??-?? +----------------------------------- + +- Crypto abstraction layer added to allow use of pyca/cryptography instead of Pycryptodome +- Dependencies modified to use pyca/cryptography for supported Python versions + Revision 4.4.4, released 2018-01-03 ----------------------------------- @@ -56,8 +56,10 @@ $ pip install pysnmp to download and install PySNMP along with its dependencies: * [PyASN1](http://snmplabs.com/pyasn1/) -* [PyCryptodomex](https://pycryptodome.readthedocs.io) (required only if SNMPv3 encryption is in use) * [PySMI](http://snmplabs.com/pysmi/) (required for MIB services only) +* A supported cryptography backend (required only if SNMPv3 encryption is in use) + * [pyca/cryptography](http://cryptography.io/) for Python 2.7 and 3.4+ + * [PyCryptodomex](https://pycryptodome.readthedocs.io) for Python 2.4-2.6 and 3.2-3.3 Besides the library, command-line [SNMP utilities](https://github.com/etingof/pysnmp-apps) written in pure-Python could be installed via: diff --git a/pysnmp/crypto/__init__.py b/pysnmp/crypto/__init__.py new file mode 100644 index 00000000..2431cd91 --- /dev/null +++ b/pysnmp/crypto/__init__.py @@ -0,0 +1,131 @@ +"""Backend-selecting cryptographic logic to allow migration to pyca/cryptography +without immediately dropping support for legacy minor Python versions. + +On installation, the correct backend dependency is selected based on the Python +version. Versions that are supported by pyca/cryptography use that backend; all +other versions (currently 2.4, 2.5, 2.6, 3.2, and 3.3) fall back to Pycryptodome. +""" +from pysnmp.proto import errind, error +CRYPTOGRAPHY = 'cryptography' +CRYPTODOME = 'Cryptodome' + +# Determine the available backend. Always prefer cryptography if it is available. +try: + import cryptography + backend = CRYPTOGRAPHY +except ImportError: + try: + import Cryptodome + backend = CRYPTODOME + except ImportError: + backend = None + + +def _cryptodome_encrypt(cipher_factory, plaintext, key, iv): + """Use a Pycryptodome cipher factory to encrypt data. + + :param cipher_factory: Factory callable that builds a Pycryptodome Cipher instance based + on the key and IV + :type cipher_factory: callable + :param bytes plaintext: Plaintext data to encrypt + :param bytes key: Encryption key + :param bytes IV: Initialization vector + :returns: Encrypted ciphertext + :rtype: bytes + """ + encryptor = cipher_factory(key, iv) + return encryptor.encrypt(plaintext) + + +def _cryptodome_decrypt(cipher_factory, ciphertext, key, iv): + """Use a Pycryptodome cipher factory to decrypt data. + + :param cipher_factory: Factory callable that builds a Pycryptodome Cipher instance based + on the key and IV + :type cipher_factory: callable + :param bytes ciphertext: Ciphertext data to decrypt + :param bytes key: Encryption key + :param bytes IV: Initialization vector + :returns: Decrypted plaintext + :rtype: bytes + """ + decryptor = cipher_factory(key, iv) + return decryptor.decrypt(ciphertext) + + +def _cryptography_encrypt(cipher_factory, plaintext, key, iv): + """Use a cryptography cipher factory to encrypt data. + + :param cipher_factory: Factory callable that builds a cryptography Cipher instance based + on the key and IV + :type cipher_factory: callable + :param bytes plaintext: Plaintext data to encrypt + :param bytes key: Encryption key + :param bytes IV: Initialization vector + :returns: Encrypted ciphertext + :rtype: bytes + """ + encryptor = cipher_factory(key, iv).encryptor() + return encryptor.update(plaintext) + encryptor.finalize() + + +def _cryptography_decrypt(cipher_factory, ciphertext, key, iv): + """Use a cryptography cipher factory to decrypt data. + + :param cipher_factory: Factory callable that builds a cryptography Cipher instance based + on the key and IV + :type cipher_factory: callable + :param bytes ciphertext: Ciphertext data to decrypt + :param bytes key: Encryption key + :param bytes IV: Initialization vector + :returns: Decrypted plaintext + :rtype: bytes + """ + decryptor = cipher_factory(key, iv).decryptor() + return decryptor.update(ciphertext) + decryptor.finalize() + + +_DECRYPT_MAP = { + CRYPTOGRAPHY: _cryptography_decrypt, + CRYPTODOME: _cryptodome_decrypt +} +_ENCRYPT_MAP = { + CRYPTOGRAPHY: _cryptography_encrypt, + CRYPTODOME: _cryptodome_encrypt +} + + +def generic_encrypt(cipher_factory_map, plaintext, key, iv): + """Encrypt data using the available backend. + + :param dict cipher_factory_map: Dictionary that maps the backend name to a cipher factory + callable for that backend + :param bytes plaintext: Plaintext data to encrypt + :param bytes key: Encryption key + :param bytes IV: Initialization vector + :returns: Encrypted ciphertext + :rtype: bytes + """ + if backend is None: + raise error.StatusInformation( + errorIndication=errind.encryptionError + ) + return _ENCRYPT_MAP[backend](cipher_factory_map[backend], plaintext, key, iv) + + +def generic_decrypt(cipher_factory_map, ciphertext, key, iv): + """Decrypt data using the available backend. + + :param dict cipher_factory_map: Dictionary that maps the backend name to a cipher factory + callable for that backend + :param bytes ciphertext: Ciphertext data to decrypt + :param bytes key: Encryption key + :param bytes IV: Initialization vector + :returns: Decrypted plaintext + :rtype: bytes + """ + if backend is None: + raise error.StatusInformation( + errorIndication=errind.decryptionError + ) + return _DECRYPT_MAP[backend](cipher_factory_map[backend], ciphertext, key, iv) diff --git a/pysnmp/crypto/aes.py b/pysnmp/crypto/aes.py new file mode 100644 index 00000000..20e91e7f --- /dev/null +++ b/pysnmp/crypto/aes.py @@ -0,0 +1,67 @@ +""" +Crypto logic for RFC3826. + +https://tools.ietf.org/html/rfc3826 +""" +from pysnmp.crypto import backend, CRYPTODOME, CRYPTOGRAPHY, generic_decrypt, generic_encrypt + +if backend == CRYPTOGRAPHY: + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives.ciphers import algorithms, Cipher, modes +elif backend == CRYPTODOME: + from Cryptodome.Cipher import AES + + +def _cryptodome_cipher(key, iv): + """Build a Pycryptodome AES Cipher object. + + :param bytes key: Encryption key + :param bytes IV: Initialization vector + :returns: AES Cipher instance + """ + return AES.new(key, AES.MODE_CFB, iv, segment_size=128) + + +def _cryptography_cipher(key, iv): + """Build a cryptography AES Cipher object. + + :param bytes key: Encryption key + :param bytes IV: Initialization vector + :returns: AES Cipher instance + :rtype: cryptography.hazmat.primitives.ciphers.Cipher + """ + return Cipher( + algorithm=algorithms.AES(key), + mode=modes.CFB(iv), + backend=default_backend() + ) + + +_CIPHER_FACTORY_MAP = { + CRYPTOGRAPHY: _cryptography_cipher, + CRYPTODOME: _cryptodome_cipher +} + + +def encrypt(plaintext, key, iv): + """Encrypt data using AES on the available backend. + + :param bytes plaintext: Plaintext data to encrypt + :param bytes key: Encryption key + :param bytes IV: Initialization vector + :returns: Encrypted ciphertext + :rtype: bytes + """ + return generic_encrypt(_CIPHER_FACTORY_MAP, plaintext, key, iv) + + +def decrypt(ciphertext, key, iv): + """Decrypt data using AES on the available backend. + + :param bytes ciphertext: Ciphertext data to decrypt + :param bytes key: Encryption key + :param bytes IV: Initialization vector + :returns: Decrypted plaintext + :rtype: bytes + """ + return generic_decrypt(_CIPHER_FACTORY_MAP, ciphertext, key, iv) diff --git a/pysnmp/crypto/des.py b/pysnmp/crypto/des.py new file mode 100644 index 00000000..12ce611b --- /dev/null +++ b/pysnmp/crypto/des.py @@ -0,0 +1,74 @@ +""" +Crypto logic for RFC3414. + +https://tools.ietf.org/html/rfc3414 +""" +from pysnmp.crypto import backend, CRYPTODOME, CRYPTOGRAPHY, generic_decrypt, generic_encrypt + +if backend == CRYPTOGRAPHY: + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives.ciphers import algorithms, Cipher, modes +elif backend == CRYPTODOME: + from Cryptodome.Cipher import DES + + +def _cryptodome_cipher(key, iv): + """Build a Pycryptodome DES Cipher object. + + :param bytes key: Encryption key + :param bytes IV: Initialization vector + :returns: DES Cipher instance + """ + return DES.new(key, DES.MODE_CBC, iv) + + +def _cryptography_cipher(key, iv): + """Build a cryptography DES(-like) Cipher object. + + .. note:: + + pyca/cryptography does not support DES directly because it is a seriously old, insecure, + and deprecated algorithm. However, triple DES is just three rounds of DES (encrypt, + decrypt, encrypt) done by taking a key three times the size of a DES key and breaking + it into three pieces. So triple DES with des_key * 3 is equivalent to DES. + + :param bytes key: Encryption key + :param bytes IV: Initialization vector + :returns: TripleDES Cipher instance providing DES behavior by using provided DES key + :rtype: cryptography.hazmat.primitives.ciphers.Cipher + """ + return Cipher( + algorithm=algorithms.TripleDES(key * 3), + mode=modes.CBC(iv), + backend=default_backend() + ) + + +_CIPHER_FACTORY_MAP = { + CRYPTOGRAPHY: _cryptography_cipher, + CRYPTODOME: _cryptodome_cipher +} + + +def encrypt(plaintext, key, iv): + """Encrypt data using DES on the available backend. + + :param bytes plaintext: Plaintext data to encrypt + :param bytes key: Encryption key + :param bytes IV: Initialization vector + :returns: Encrypted ciphertext + :rtype: bytes + """ + return generic_encrypt(_CIPHER_FACTORY_MAP, plaintext, key, iv) + + +def decrypt(ciphertext, key, iv): + """Decrypt data using DES on the available backend. + + :param bytes ciphertext: Ciphertext data to decrypt + :param bytes key: Encryption key + :param bytes IV: Initialization vector + :returns: Decrypted plaintext + :rtype: bytes + """ + return generic_decrypt(_CIPHER_FACTORY_MAP, ciphertext, key, iv) diff --git a/pysnmp/crypto/des3.py b/pysnmp/crypto/des3.py new file mode 100644 index 00000000..1ccb9aeb --- /dev/null +++ b/pysnmp/crypto/des3.py @@ -0,0 +1,67 @@ +""" +Crypto logic for Reeder 3DES-EDE for USM (Internet draft). + +https://tools.ietf.org/html/draft-reeder-snmpv3-usm-3desede-00 +""" +from pysnmp.crypto import backend, CRYPTODOME, CRYPTOGRAPHY, generic_decrypt, generic_encrypt + +if backend == CRYPTOGRAPHY: + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives.ciphers import algorithms, Cipher, modes +elif backend == CRYPTODOME: + from Cryptodome.Cipher import DES3 + + +def _cryptodome_cipher(key, iv): + """Build a Pycryptodome DES3 Cipher object. + + :param bytes key: Encryption key + :param bytes IV: Initialization vector + :returns: DES3 Cipher instance + """ + return DES3.new(key, DES3.MODE_CBC, iv) + + +def _cryptography_cipher(key, iv): + """Build a cryptography TripleDES Cipher object. + + :param bytes key: Encryption key + :param bytes IV: Initialization vector + :returns: TripleDES Cipher instance + :rtype: cryptography.hazmat.primitives.ciphers.Cipher + """ + return Cipher( + algorithm=algorithms.TripleDES(key), + mode=modes.CBC(iv), + backend=default_backend() + ) + + +_CIPHER_FACTORY_MAP = { + CRYPTOGRAPHY: _cryptography_cipher, + CRYPTODOME: _cryptodome_cipher +} + + +def encrypt(plaintext, key, iv): + """Encrypt data using triple DES on the available backend. + + :param bytes plaintext: Plaintext data to encrypt + :param bytes key: Encryption key + :param bytes IV: Initialization vector + :returns: Encrypted ciphertext + :rtype: bytes + """ + return generic_encrypt(_CIPHER_FACTORY_MAP, plaintext, key, iv) + + +def decrypt(ciphertext, key, iv): + """Decrypt data using triple DES on the available backend. + + :param bytes ciphertext: Ciphertext data to decrypt + :param bytes key: Encryption key + :param bytes IV: Initialization vector + :returns: Decrypted plaintext + :rtype: bytes + """ + return generic_decrypt(_CIPHER_FACTORY_MAP, ciphertext, key, iv) diff --git a/pysnmp/proto/secmod/eso/priv/des3.py b/pysnmp/proto/secmod/eso/priv/des3.py index 426df633..bf39a8ff 100644 --- a/pysnmp/proto/secmod/eso/priv/des3.py +++ b/pysnmp/proto/secmod/eso/priv/des3.py @@ -5,6 +5,7 @@ # License: http://snmplabs.com/pysnmp/license.html # import random +from pysnmp.crypto import des3 from pysnmp.proto.secmod.rfc3414.priv import base from pysnmp.proto.secmod.rfc3414.auth import hmacmd5, hmacsha from pysnmp.proto.secmod.rfc3414 import localkey @@ -12,7 +13,6 @@ from pysnmp.proto.secmod.rfc7860.auth import hmacsha2 from pysnmp.proto import errind, error from pyasn1.type import univ from pyasn1.compat.octets import null -from math import ceil try: from hashlib import md5, sha1 @@ -23,11 +23,6 @@ except ImportError: md5 = md5.new sha1 = sha.new -try: - from Cryptodome.Cipher import DES3 -except ImportError: - DES3 = None - random.seed() @@ -113,32 +108,21 @@ class Des3(base.AbstractEncryptionService): # 5.1.1.2 def encryptData(self, encryptKey, privParameters, dataToEncrypt): - if DES3 is None: - raise error.StatusInformation( - errorIndication=errind.encryptionError - ) - snmpEngineBoots, snmpEngineTime, salt = privParameters des3Key, salt, iv = self.__getEncryptionKey( encryptKey, snmpEngineBoots ) - des3Obj = DES3.new(des3Key, DES3.MODE_CBC, iv) - privParameters = univ.OctetString(salt) plaintext = dataToEncrypt + univ.OctetString((0,) * (8 - len(dataToEncrypt) % 8)).asOctets() - ciphertext = des3Obj.encrypt(plaintext) + ciphertext = des3.encrypt(plaintext, des3Key, iv) return univ.OctetString(ciphertext), privParameters # 5.1.1.3 def decryptData(self, decryptKey, privParameters, encryptedData): - if DES3 is None: - raise error.StatusInformation( - errorIndication=errind.decryptionError - ) snmpEngineBoots, snmpEngineTime, salt = privParameters if len(salt) != 8: @@ -153,9 +137,7 @@ class Des3(base.AbstractEncryptionService): errorIndication=errind.decryptionError ) - des3Obj = DES3.new(des3Key, DES3.MODE_CBC, iv) - ciphertext = encryptedData.asOctets() - plaintext = des3Obj.decrypt(ciphertext) + plaintext = des3.decrypt(ciphertext, des3Key, iv) return plaintext diff --git a/pysnmp/proto/secmod/rfc3414/priv/des.py b/pysnmp/proto/secmod/rfc3414/priv/des.py index b66889e2..b874162a 100644 --- a/pysnmp/proto/secmod/rfc3414/priv/des.py +++ b/pysnmp/proto/secmod/rfc3414/priv/des.py @@ -5,6 +5,7 @@ # License: http://snmplabs.com/pysnmp/license.html # import random +from pysnmp.crypto import des from pysnmp.proto.secmod.rfc3414.priv import base from pysnmp.proto.secmod.rfc3414.auth import hmacmd5, hmacsha from pysnmp.proto.secmod.rfc3414 import localkey @@ -14,10 +15,6 @@ from pyasn1.type import univ from sys import version_info try: - from Cryptodome.Cipher import DES -except ImportError: - DES = None -try: from hashlib import md5, sha1 except ImportError: import md5 @@ -98,11 +95,6 @@ class Des(base.AbstractEncryptionService): # 8.2.4.1 def encryptData(self, encryptKey, privParameters, dataToEncrypt): - if DES is None: - raise error.StatusInformation( - errorIndication=errind.encryptionError - ) - snmpEngineBoots, snmpEngineTime, salt = privParameters # 8.3.1.1 @@ -114,20 +106,14 @@ class Des(base.AbstractEncryptionService): privParameters = univ.OctetString(salt) # 8.1.1.2 - desObj = DES.new(desKey, DES.MODE_CBC, iv) plaintext = dataToEncrypt + univ.OctetString((0,) * (8 - len(dataToEncrypt) % 8)).asOctets() - ciphertext = desObj.encrypt(plaintext) + ciphertext = des.encrypt(plaintext, desKey, iv) # 8.3.1.3 & 4 return univ.OctetString(ciphertext), privParameters # 8.2.4.2 def decryptData(self, decryptKey, privParameters, encryptedData): - if DES is None: - raise error.StatusInformation( - errorIndication=errind.decryptionError - ) - snmpEngineBoots, snmpEngineTime, salt = privParameters # 8.3.2.1 @@ -147,7 +133,5 @@ class Des(base.AbstractEncryptionService): errorIndication=errind.decryptionError ) - desObj = DES.new(desKey, DES.MODE_CBC, iv) - # 8.3.2.6 - return desObj.decrypt(encryptedData.asOctets()) + return des.decrypt(encryptedData.asOctets(), desKey, iv) diff --git a/pysnmp/proto/secmod/rfc3826/priv/aes.py b/pysnmp/proto/secmod/rfc3826/priv/aes.py index c702a418..82fa0da5 100644 --- a/pysnmp/proto/secmod/rfc3826/priv/aes.py +++ b/pysnmp/proto/secmod/rfc3826/priv/aes.py @@ -6,6 +6,7 @@ # import random from pyasn1.type import univ +from pysnmp.crypto import aes from pysnmp.proto.secmod.rfc3414.priv import base from pysnmp.proto.secmod.rfc3414.auth import hmacmd5, hmacsha from pysnmp.proto.secmod.rfc7860.auth import hmacsha2 @@ -13,10 +14,6 @@ from pysnmp.proto.secmod.rfc3414 import localkey from pysnmp.proto import errind, error try: - from Cryptodome.Cipher import AES -except ImportError: - AES = None -try: from hashlib import md5, sha1 except ImportError: import md5 @@ -102,11 +99,6 @@ class Aes(base.AbstractEncryptionService): # 3.2.4.1 def encryptData(self, encryptKey, privParameters, dataToEncrypt): - if AES is None: - raise error.StatusInformation( - errorIndication=errind.encryptionError - ) - snmpEngineBoots, snmpEngineTime, salt = privParameters # 3.3.1.1 @@ -115,23 +107,16 @@ class Aes(base.AbstractEncryptionService): ) # 3.3.1.3 - aesObj = AES.new(aesKey, AES.MODE_CFB, iv, segment_size=128) - # PyCrypto seems to require padding dataToEncrypt = dataToEncrypt + univ.OctetString((0,) * (16 - len(dataToEncrypt) % 16)).asOctets() - ciphertext = aesObj.encrypt(dataToEncrypt) + ciphertext = aes.encrypt(dataToEncrypt, aesKey, iv) # 3.3.1.4 return univ.OctetString(ciphertext), univ.OctetString(salt) # 3.2.4.2 def decryptData(self, decryptKey, privParameters, encryptedData): - if AES is None: - raise error.StatusInformation( - errorIndication=errind.decryptionError - ) - snmpEngineBoots, snmpEngineTime, salt = privParameters # 3.3.2.1 @@ -145,10 +130,8 @@ class Aes(base.AbstractEncryptionService): decryptKey, snmpEngineBoots, snmpEngineTime, salt ) - aesObj = AES.new(aesKey, AES.MODE_CFB, iv, segment_size=128) - # PyCrypto seems to require padding encryptedData = encryptedData + univ.OctetString((0,) * (16 - len(encryptedData) % 16)).asOctets() # 3.3.2.4-6 - return aesObj.decrypt(encryptedData.asOctets()) + return aes.decrypt(encryptedData.asOctets(), aesKey, iv) diff --git a/requirements.txt b/requirements.txt index d90ebe40..76ab7775 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,8 @@ pysmi -pycryptodomex +pycryptodomex; python_version < '2.7' +cryptography; python_version == '2.7' +pycryptodomex; python_version == '3.2' +pycryptodomex; python_version == '3.3' +cryptography; python_version >= '3.4' pyasn1>=0.2.3 +ordereddict; python_version < '2.7' @@ -50,15 +50,26 @@ def howto_install_setuptools(): """) -if sys.version_info[:2] < (2, 4): +py_version = sys.version_info[:2] +if py_version < (2, 4): print("ERROR: this package requires Python 2.4 or later!") sys.exit(1) +if py_version < (2, 7) or (py_version >= (3, 0) and py_version < (3, 4)): + crypto_lib = 'pycryptodomex' +else: + crypto_lib = 'cryptography' + +requires = ['pyasn1>=0.2.3', 'pysmi', crypto_lib] + +if py_version < (2, 7): + requires.append('ordereddict') + try: from setuptools import setup params = { - 'install_requires': ['pyasn1>=0.2.3', 'pysmi', 'pycryptodomex'], + 'install_requires': requires, 'zip_safe': True } @@ -71,8 +82,8 @@ except ImportError: from distutils.core import setup params = {} - if sys.version_info[:2] > (2, 4): - params['requires'] = ['pyasn1(>=0.2.3)', 'pysmi', 'pycryptodomex'] + if py_version > (2, 4): + params['requires'] = requires doclines = [x.strip() for x in (__doc__ or '').split('\n') if x] @@ -101,6 +112,7 @@ params.update({ 'pysnmp.carrier.twisted.dgram', 'pysnmp.carrier.asyncio', 'pysnmp.carrier.asyncio.dgram', + 'pysnmp.crypto', 'pysnmp.entity', 'pysnmp.entity.rfc3413', 'pysnmp.entity.rfc3413.oneliner', diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..af983437 --- /dev/null +++ b/tox.ini @@ -0,0 +1,5 @@ +[envlist] +envlist = py{26,27,33,34,35,36} + +[testenv] +commands = {toxinidir}/runtests.sh |