strong crypto moved to pysnmpcrypto

pull/135/head
Ilya Etingof 2018-02-19 00:41:28 +01:00
parent bc2654205b
commit 24a7988766
16 changed files with 115 additions and 406 deletions

View File

@ -1,9 +1,13 @@
Revision 4.?.?, released 2018-??-??
Revision 5.0.0, released 2018-03-??
-----------------------------------
- Crypto abstraction layer added to allow use of pyca/cryptography instead of Pycryptodome
- Dependencies modified to use pyca/cryptography for supported Python versions
- SNMPv3 crypto operations that require external dependencies
made dependent on the optional external
package -- pysnmpcrypto.
- By switching to pysnmpcrypto, pysnmp effectively migrates from
PyCryptodomex to pyca/cryptography whenever available on the
platform.
Revision 4.4.4, released 2018-01-03
-----------------------------------

View File

@ -57,9 +57,8 @@ to download and install PySNMP along with its dependencies:
* [PyASN1](http://snmplabs.com/pyasn1/)
* [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
* Optional [pysnmpcrypto](https://github.com/etingof/pysnmpcrypto) package
whenever strong SNMPv3 encryption is desired
Besides the library, command-line [SNMP utilities](https://github.com/etingof/snmpclitools)
written in pure-Python could be installed via:

View File

@ -30,5 +30,6 @@ Laurelin of Middle Earth
Robert Reese
Olivier Verriest
Eugene M. Kim
Matt Bullock
Thanks to Python Software Foundation for granting financial support
for the project.

View File

@ -9,7 +9,7 @@ We can look at PySNMP's internal structure from the view point of
SNMP protocol evolution. SNMP was evolving for many years from
a relatively simple way to structure and retrieve data (SNMPv1/v2c)
all the way to extensible and modularized framework that supports
strong crypto out-of-the-box (SNMPv3).
strong SNMPv3 crypto (with optional pysnmpcrypto package).
In the order from most ancient SNMP services to the most current ones,
what follows are different layers of PySNMP APIs:

View File

@ -42,19 +42,16 @@ In case you are installing PySNMP on an off-line system, the following
packages need to be downloaded and installed for PySNMP to become
operational:
* `PyASN1 <https://pypi.python.org/pypi/pyasn1>`_,
used for handling ASN.1 objects
* `PySNMP <https://pypi.python.org/pypi/pysnmp/>`_,
* `pysnmp <https://pypi.python.org/pypi/pysnmp/>`_,
SNMP engine implementation
Optional, but recommended:
* `PyCryptodomex <https://pypi.python.org/pypi/pycryptodomex/>`_,
used by SNMPv3 crypto features
* `PySMI <https://pypi.python.org/pypi/pysmi/>`_ for automatic
* `pyasn1 <https://pypi.python.org/pypi/pyasn1>`_,
used for handling ASN.1 objects
* `pysmi <https://pypi.python.org/pypi/pysmi/>`_ for automatic
MIB download and compilation. That helps visualizing more SNMP objects
* `Ply <https://pypi.python.org/pypi/ply/>`_, parser generator
required by PySMI
Optional:
* `pysnmpcrypto <https://pypi.python.org/pypi/pysnmpcrypto/>`_,
for strong SNMPv3 crypto support
The installation procedure for all the above packages is as follows
(on UNIX-based systems):

View File

@ -1,5 +1,5 @@
# http://www.python.org/dev/peps/pep-0396/
__version__ = '4.4.4'
__version__ = '5.0.0'
# backward compatibility
version = tuple([int(x) for x in __version__.split('.')])
majorVersionId = version[0]

View File

@ -1,131 +0,0 @@
"""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)

View File

@ -1,67 +0,0 @@
"""
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)

View File

@ -1,74 +0,0 @@
"""
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)

View File

@ -1,67 +0,0 @@
"""
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)

View File

@ -175,13 +175,13 @@ authenticationFailure = AuthenticationFailure('Authenticator mismatched')
class UnsupportedAuthProtocol(ErrorIndication):
pass
unsupportedAuthProtocol = UnsupportedAuthProtocol('Authentication protocol is not supprted')
unsupportedAuthProtocol = UnsupportedAuthProtocol('Authentication protocol is not supported')
class UnsupportedPrivProtocol(ErrorIndication):
pass
unsupportedPrivProtocol = UnsupportedPrivProtocol('Privacy protocol is not supprted')
unsupportedPrivProtocol = UnsupportedPrivProtocol('Privacy protocol is not supported')
class UnknownSecurityName(ErrorIndication):

View File

@ -5,15 +5,6 @@
# 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
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
try:
from hashlib import md5, sha1
except ImportError:
@ -23,6 +14,21 @@ except ImportError:
md5 = md5.new
sha1 = sha.new
try:
from pysnmpcrypto import des3, PysnmpCryptoError
except ImportError:
PysnmpCryptoError = AttributeError
des3 = None
from pysnmp.proto.secmod.rfc3414.priv import base
from pysnmp.proto.secmod.rfc3414.auth import hmacmd5, hmacsha
from pysnmp.proto.secmod.rfc3414 import localkey
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
random.seed()
@ -117,7 +123,14 @@ class Des3(base.AbstractEncryptionService):
privParameters = univ.OctetString(salt)
plaintext = dataToEncrypt + univ.OctetString((0,) * (8 - len(dataToEncrypt) % 8)).asOctets()
ciphertext = des3.encrypt(plaintext, des3Key, iv)
try:
ciphertext = des3.encrypt(plaintext, des3Key, iv)
except PysnmpCryptoError:
raise error.StatusInformation(
errorIndication=errind.unsupportedPrivProtocol
)
return univ.OctetString(ciphertext), privParameters
@ -138,6 +151,13 @@ class Des3(base.AbstractEncryptionService):
)
ciphertext = encryptedData.asOctets()
plaintext = des3.decrypt(ciphertext, des3Key, iv)
try:
plaintext = des3.decrypt(ciphertext, des3Key, iv)
except PysnmpCryptoError:
raise error.StatusInformation(
errorIndication=errind.unsupportedPrivProtocol
)
return plaintext

View File

@ -5,15 +5,6 @@
# 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
from pysnmp.proto.secmod.rfc7860.auth import hmacsha2
from pysnmp.proto import errind, error
from pyasn1.type import univ
from sys import version_info
try:
from hashlib import md5, sha1
except ImportError:
@ -23,6 +14,22 @@ except ImportError:
md5 = md5.new
sha1 = sha.new
from sys import version_info
try:
from pysnmpcrypto import des, PysnmpCryptoError
except ImportError:
PysnmpCryptoError = AttributeError
des = None
from pysnmp.proto.secmod.rfc3414.priv import base
from pysnmp.proto.secmod.rfc3414.auth import hmacmd5, hmacsha
from pysnmp.proto.secmod.rfc3414 import localkey
from pysnmp.proto.secmod.rfc7860.auth import hmacsha2
from pysnmp.proto import errind, error
from pyasn1.type import univ
random.seed()
@ -107,7 +114,14 @@ class Des(base.AbstractEncryptionService):
# 8.1.1.2
plaintext = dataToEncrypt + univ.OctetString((0,) * (8 - len(dataToEncrypt) % 8)).asOctets()
ciphertext = des.encrypt(plaintext, desKey, iv)
try:
ciphertext = des.encrypt(plaintext, desKey, iv)
except PysnmpCryptoError:
raise error.StatusInformation(
errorIndication=errind.unsupportedPrivProtocol
)
# 8.3.1.3 & 4
return univ.OctetString(ciphertext), privParameters
@ -133,5 +147,11 @@ class Des(base.AbstractEncryptionService):
errorIndication=errind.decryptionError
)
# 8.3.2.6
return des.decrypt(encryptedData.asOctets(), desKey, iv)
try:
# 8.3.2.6
return des.decrypt(encryptedData.asOctets(), desKey, iv)
except PysnmpCryptoError:
raise error.StatusInformation(
errorIndication=errind.unsupportedPrivProtocol
)

View File

@ -5,14 +5,6 @@
# License: http://snmplabs.com/pysnmp/license.html
#
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
from pysnmp.proto.secmod.rfc3414 import localkey
from pysnmp.proto import errind, error
try:
from hashlib import md5, sha1
except ImportError:
@ -22,6 +14,20 @@ except ImportError:
md5 = md5.new
sha1 = sha.new
try:
from pysnmpcrypto import aes, PysnmpCryptoError
except ImportError:
PysnmpCryptoError = AttributeError
aes = None
from pyasn1.type import univ
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
from pysnmp.proto.secmod.rfc3414 import localkey
from pysnmp.proto import errind, error
random.seed()
@ -110,7 +116,13 @@ class Aes(base.AbstractEncryptionService):
# PyCrypto seems to require padding
dataToEncrypt = dataToEncrypt + univ.OctetString((0,) * (16 - len(dataToEncrypt) % 16)).asOctets()
ciphertext = aes.encrypt(dataToEncrypt, aesKey, iv)
try:
ciphertext = aes.encrypt(dataToEncrypt, aesKey, iv)
except PysnmpCryptoError:
raise error.StatusInformation(
errorIndication=errind.unsupportedPrivProtocol
)
# 3.3.1.4
return univ.OctetString(ciphertext), univ.OctetString(salt)
@ -133,5 +145,11 @@ class Aes(base.AbstractEncryptionService):
# PyCrypto seems to require padding
encryptedData = encryptedData + univ.OctetString((0,) * (16 - len(encryptedData) % 16)).asOctets()
# 3.3.2.4-6
return aes.decrypt(encryptedData.asOctets(), aesKey, iv)
try:
# 3.3.2.4-6
return aes.decrypt(encryptedData.asOctets(), aesKey, iv)
except PysnmpCryptoError:
raise error.StatusInformation(
errorIndication=errind.unsupportedPrivProtocol
)

View File

@ -1,8 +1,3 @@
pysmi
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'

View File

@ -55,12 +55,7 @@ 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]
requires = ['pyasn1>=0.2.3', 'pysmi']
if py_version < (2, 7):
requires.append('ordereddict')
@ -112,7 +107,6 @@ params.update({
'pysnmp.carrier.twisted.dgram',
'pysnmp.carrier.asyncio',
'pysnmp.carrier.asyncio.dgram',
'pysnmp.crypto',
'pysnmp.entity',
'pysnmp.entity.rfc3413',
'pysnmp.entity.rfc3413.oneliner',