From df06aa8ad85ae23b78ddafbb887af932a15ee8f9 Mon Sep 17 00:00:00 2001 From: Dane Date: Thu, 3 Aug 2017 14:56:28 -0400 Subject: [PATCH] Add engine support for sign operation This change adds the sign operation functionality to the cryptography engine. --- kmip/services/server/crypto/api.py | 30 ++ kmip/services/server/crypto/engine.py | 141 ++++++++- .../services/server/crypto/test_engine.py | 267 +++++++++++++++++- 3 files changed, 436 insertions(+), 2 deletions(-) diff --git a/kmip/services/server/crypto/api.py b/kmip/services/server/crypto/api.py index 333b8e5..b06e2bd 100644 --- a/kmip/services/server/crypto/api.py +++ b/kmip/services/server/crypto/api.py @@ -154,3 +154,33 @@ class CryptographicEngine(object): Returns: bytes: the bytes of the decrypted data """ + + @abstractmethod + def sign(self, + digital_signature_algorithm, + cryptographic_algorithm, + hash_algorithm, + signing_key, + data): + """ + Generate a signature for the provided data. + + Args: + digital_signature_algorithm (DigitalSignatureAlgorithm): An + enumeration specifying the asymmetric cryptographic algorithm + and hashing algorithm to use for the signature operation. Can + be None if cryptographic_algorithm and hash_algorithm are set. + cryptographic_algorithm (CryptographicAlgorithm): An enumeration + specifying the asymmetric cryptographic algorithm to use for + the signature operation. Can be None if + digital_signature_algorithm is set. + hash_algorithm (HashingAlgorithm): An enumeration specifying the + hash algorithm to use for the signature operation. Can be None + if digital_signature_algorithm is set. + signing_key (bytes): The bytes of the private key to use for the + signature operation. + data (bytes): The data to be signed. + + Returns: + bytes: the bytes of the signature data + """ diff --git a/kmip/services/server/crypto/engine.py b/kmip/services/server/crypto/engine.py index 8d2877a..9fc53b3 100644 --- a/kmip/services/server/crypto/engine.py +++ b/kmip/services/server/crypto/engine.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +import binascii import logging import os @@ -89,7 +90,8 @@ class CryptographyEngine(api.CryptographicEngine): } self._asymmetric_padding_methods = { enums.PaddingMethod.OAEP: asymmetric_padding.OAEP, - enums.PaddingMethod.PKCS1v15: asymmetric_padding.PKCS1v15 + enums.PaddingMethod.PKCS1v15: asymmetric_padding.PKCS1v15, + enums.PaddingMethod.PSS: asymmetric_padding.PSS } self._symmetric_padding_methods = { enums.PaddingMethod.ANSI_X923: symmetric_padding.ANSIX923, @@ -105,6 +107,21 @@ class CryptographyEngine(api.CryptographicEngine): enums.BlockCipherMode.GCM ] + self._digital_signature_algorithms = { + enums.DigitalSignatureAlgorithm.MD5_WITH_RSA_ENCRYPTION: + (hashes.MD5, enums.CryptographicAlgorithm.RSA), + enums.DigitalSignatureAlgorithm.SHA1_WITH_RSA_ENCRYPTION: + (hashes.SHA1, enums.CryptographicAlgorithm.RSA), + enums.DigitalSignatureAlgorithm.SHA224_WITH_RSA_ENCRYPTION: + (hashes.SHA224, enums.CryptographicAlgorithm.RSA), + enums.DigitalSignatureAlgorithm.SHA256_WITH_RSA_ENCRYPTION: + (hashes.SHA256, enums.CryptographicAlgorithm.RSA), + enums.DigitalSignatureAlgorithm.SHA384_WITH_RSA_ENCRYPTION: + (hashes.SHA384, enums.CryptographicAlgorithm.RSA), + enums.DigitalSignatureAlgorithm.SHA512_WITH_RSA_ENCRYPTION: + (hashes.SHA512, enums.CryptographicAlgorithm.RSA) + } + def create_symmetric_key(self, algorithm, length): """ Create a symmetric key. @@ -1170,3 +1187,125 @@ class CryptographyEngine(api.CryptographicEngine): "Wrapping method '{0}' is not a supported key wrapping " "method.".format(wrapping_method) ) + + def _create_RSA_private_key(self, + bytes): + """ + Instantiates an RSA key from bytes. + + Args: + bytes (byte string): Bytes of RSA private key. + Returns: + private_key + (cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey): + RSA private key created from key bytes. + """ + + try: + private_key = serialization.load_pem_private_key( + binascii.unhexlify(bytes), + password=None, + backend=default_backend() + ) + return private_key + except: + private_key = serialization.load_der_private_key( + binascii.unhexlify(bytes), + password=None, + backend=default_backend() + ) + return private_key + + def sign(self, + digital_signature_algorithm, + crypto_alg, + hash_algorithm, + padding, + signing_key, + data): + """ + Args: + digital_signature_algorithm (DigitalSignatureAlgorithm): An + enumeration specifying the asymmetric cryptographic algorithm + and hashing algorithm to use for the signature operation. Can + be None if cryptographic_algorithm and hash_algorithm are set. + crypto_alg (CryptographicAlgorithm): An enumeration + specifying the asymmetric cryptographic algorithm to use for + the signature operation. Can be None if + digital_signature_algorithm is set. + hash_algorithm (HashingAlgorithm): An enumeration specifying the + hash algorithm to use for the signature operation. Can be None + if digital_signature_algorithm is set. + padding (PaddingMethod): An enumeration specifying the asymmetric + padding method to use for the signature operation. + signing_key (bytes): The bytes of the private key to use for the + signature operation. + data (bytes): The data to be signed. + + Returns: + signature (bytes): the bytes of the signature data + + Raises: + CryptographicFailure: Raised when an error occurs during signature + creation. + InvalidField: Raised when an unsupported hashing or cryptographic + algorithm is specified. + """ + + if digital_signature_algorithm: + (hash_alg, crypto_alg) = self._digital_signature_algorithms.get( + digital_signature_algorithm, + (None, None) + ) + + elif crypto_alg and hash_algorithm: + hash_alg = self._encryption_hash_algorithms.get( + hash_algorithm, None + ) + else: + raise exceptions.InvalidField( + 'For signing, either a digital signature algorithm or a hash' + ' algorithm and a cryptographic algorithm must be specified.' + ) + + if crypto_alg == enums.CryptographicAlgorithm.RSA: + try: + key = self._create_RSA_private_key(signing_key) + except: + raise exceptions.InvalidField('Unable to deserialize key ' + 'bytes, unknown format.') + else: + raise exceptions.InvalidField( + 'For signing, an RSA key must be used.' + ) + + if padding: + padding_method = self._asymmetric_padding_methods.get( + padding, None + ) + else: + raise exceptions.InvalidField( + 'For signing, a padding method must be specified.' + ) + + if padding == enums.PaddingMethod.PSS: + signature = key.sign( + data, + asymmetric_padding.PSS( + mgf=asymmetric_padding.MGF1(hash_alg()), + salt_length=asymmetric_padding.PSS.MAX_LENGTH + ), + hash_alg() + ) + elif padding == enums.PaddingMethod.PKCS1v15: + signature = key.sign( + data, + padding_method(), + hash_alg() + ) + else: + raise exceptions.InvalidField( + "Padding method '{0}' is not a supported signature " + "padding method.".format(padding) + ) + return signature diff --git a/kmip/tests/unit/services/server/crypto/test_engine.py b/kmip/tests/unit/services/server/crypto/test_engine.py index 2ad9cb7..17f64d3 100644 --- a/kmip/tests/unit/services/server/crypto/test_engine.py +++ b/kmip/tests/unit/services/server/crypto/test_engine.py @@ -13,14 +13,18 @@ # License for the specific language governing permissions and limitations # under the License. +import binascii import mock import pytest import testtools from cryptography.hazmat import backends -from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.ciphers import algorithms +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import serialization from kmip.core import enums from kmip.core import exceptions @@ -904,6 +908,113 @@ class TestCryptographyEngine(testtools.TestCase): *args ) + def test_sign_no_alg(self): + """ + Test that an InvalidField exception is raised when sign is + called without sufficient crypto parameters. + """ + engine = crypto.CryptographyEngine() + + args = (None, None, None, None, None, None) + self.assertRaisesRegexp( + exceptions.InvalidField, + 'For signing, either a digital signature algorithm or a hash' + ' algorithm and a cryptographic algorithm must be specified.', + engine.sign, + *args + ) + + def test_sign_non_RSA(self): + """ + Test that an InvalidField exception is raised when sign is + called with a crypto algorithm other than RSA. + """ + engine = crypto.CryptographyEngine() + + args = ( + None, + enums.CryptographicAlgorithm.TRIPLE_DES, + enums.HashingAlgorithm.MD5, + None, + None, + None + ) + self.assertRaisesRegexp( + exceptions.InvalidField, + 'For signing, an RSA key must be used.', + engine.sign, + *args + ) + + def test_sign_invalid_padding(self): + """ + Test that an InvalidField exception is raised when sign is + called with an unsupported padding algorithm. + """ + engine = crypto.CryptographyEngine() + + args = ( + None, + enums.CryptographicAlgorithm.RSA, + enums.HashingAlgorithm.MD5, + enums.PaddingMethod.OAEP, + DER_RSA_KEY, + None + ) + self.assertRaisesRegexp( + exceptions.InvalidField, + "Padding method 'PaddingMethod.OAEP' is not a supported" + " signature padding method.", + engine.sign, + *args + ) + + def test_sign_no_padding(self): + """ + Test that an InvalidField exception is raised when sign is + called without a padding algorithm. + """ + engine = crypto.CryptographyEngine() + + args = ( + None, + enums.CryptographicAlgorithm.RSA, + enums.HashingAlgorithm.MD5, + None, + DER_RSA_KEY, + None + ) + self.assertRaisesRegexp( + exceptions.InvalidField, + 'For signing, a padding method must be specified.', + engine.sign, + *args + ) + + def test_sign_invalid_key_bytes(self): + """ + Test that an InvalidField exception is raised when + sign is called with invalid key bytes. + """ + engine = crypto.CryptographyEngine() + + args = ( + None, + enums.CryptographicAlgorithm.RSA, + enums.HashingAlgorithm.MD5, + enums.PaddingMethod.PKCS1v15, + 'thisisnotavalidkey', + None + ) + + self.assertRaisesRegexp( + exceptions.InvalidField, + 'Unable to deserialize key ' + 'bytes, unknown format.', + engine.sign, + *args + ) + # TODO(peter-hamilton): Replace this with actual fixture files from NIST CAPV. # Most of these test vectors were obtained from the pyca/cryptography test @@ -2140,3 +2251,157 @@ def test_wrap_key(wrapping_parameters): ) assert wrapping_parameters.get('wrapped_data') == result + +# Test vectors obtained from pyca/cryptography +# https://cryptography.io/en/latest/ + + +DER_RSA_KEY = ('3082025e02010002818100aebac1b9a174315d27cc3c201e215789' + '4372d6450d4cf80ce0ebcf5169519b9e8550036f4abe0fe4f94fbf' + '9cca606f39743365499611ba3f25a9a47158ba05214b655f4258a4' + 'c29516becaa583f2d26650696ad6fc03d5b47d3aba9c5479fdb047' + '7d29513399cb19283ccdc28dbb23b7c7eee4b35dc940daca0055dc' + 'd28f503b02030100010281810092890942d6c68d47a4c2c181e602' + 'ec58af7a357c7fa5173a25bf5d84d7209bb41bf5788bf350e61f8f' + '7e7421d80f7bf7e11de14a0f531ab12eb2d0b84642eb5d181170c2' + 'c58aabbd6754842fafee57fef2f545d09fdc664902e55baced5a3c' + '6d26f3465859d33a33a555537daf2263aaef28354c8b53513145a7' + 'e228824dabb1024100d3aa237e8942b93d56a681254c27be1f4a49' + '6ca4a87fc0604b0cff8f980e742d2bbb91b88a247b6ebbed01458c' + '4afdb68c0f8c6d4a37e028c5fcb3a6a39ca64f024100d354168c61' + '9c836e8597fef50193a6f42607952a1c87ebae91db5043b8855072' + 'b4e92af5dcedb2148773dfbd217bafc8dc9da8ae8e757e7248c1e5' + '13a144685502410090fda214c2b7b726825dca679f3436333ef2ee' + 'fe180272e84360e30b1d11019a13b4080d0e6c1135787bd07c30af' + '09feeb10979421dc06ac477b6420c940bc570240164de8b7565213' + '9925a67e3553be46bfbc07ced98bfb5887ab434f7c664c43ca6787' + 'b88e0c8c55e04ecf8f0cc22cf0c7ad69427571f9baa7cb4013b277' + 'b1e5a5024100cae150f5fa559b2e2c39444e0f5c651034092ac97b' + 'ac10d528dd15dfda254cb06bef41e39881f7e7496910b4655659dc' + '842d30b9ae2759f3c2cd41c79a3684ec') + +PEM_RSA_KEY = ('2d2d2d2d2d424547494e205253412050524956415445204b45592d' + '2d2d2d2d0a4d4949435867494241414b4267514375757347356f58' + '51785853664d504341654956654a51334c575251314d2b417a6736' + '383952615647626e6f56514132394b0a76672f6b2b552b2f6e4d70' + '67627a6c304d32564a6c6847365079577070484659756755685332' + '5666516c696b777055577673716c672f4c535a6c4270617462380a' + '41395730665471366e4652352f62424866536c524d356e4c475367' + '387a634b4e75794f33782b376b7331334a514e724b41465863306f' + '39514f774944415141420a416f4742414a4b4a43554c57786f3148' + '704d4c426765594337466976656a5638663655584f69572f585954' + '58494a7530472f5634692f4e5135682b50666e51680a3241393739' + '2b456434556f50557871784c724c5175455a433631305945584443' + '785971727657645568432b76376c662b38765646304a2f635a6b6b' + '43355675730a37566f386253627a526c685a307a6f7a7056565466' + '613869593672764b44564d69314e524d55576e3469694354617578' + '416b454130366f6a666f6c43755431570a706f456c5443652b4830' + '704a624b536f6638426753777a2f6a35674f644330727535473469' + '695237627276744155574d537632326a412b4d62556f3334436a46' + '0a2f4c4f6d6f35796d54774a42414e4e55466f78686e494e75685a' + '662b395147547076516d42355571484966727270486255454f3468' + '564279744f6b7139647a740a7368534863392b3949587576794e79' + '64714b364f64583579534d486c4536464561465543515143512f61' + '4955777265334a6f4a64796d65664e44597a50764c750a2f686743' + '63756844594f4d4c485245426d684f304341304f62424531654876' + '516644437643663772454a655549647747724564375a43444a514c' + '7858416b41570a54656933566c49546d53576d666a5654766b612f' + '7641664f3259763757496572513039385a6b7844796d6548754934' + '4d6a46586754732b50444d4973384d65740a61554a3163666d3670' + '38744145374a337365576c416b454179754651396670566d793473' + '4f55524f4431786c4544514a4b736c37724244564b4e305633396f' + '6c0a544c42723730486a6d49483335306c70454c526c566c6e6368' + '4330777561346e576650437a5548486d6a614537413d3d0a2d2d2d' + '2d2d454e44205253412050524956415445204b45592d2d2d2d2d0a') + +RSA_private_key = serialization.load_der_private_key( + binascii.unhexlify(DER_RSA_KEY), + password=None, + backend=default_backend() +) + +RSA_public_key = RSA_private_key.public_key() + +SIGN_TEST_DATA = (b'\x01\x02\x03\x04\x05\x06\x07\x08' + b'\x09\x10\x11\x12\x13\x14\x15\x16') + + +@pytest.fixture( + scope='function', + params=[ + {'digital_signature_algorithm': + enums.DigitalSignatureAlgorithm.MD5_WITH_RSA_ENCRYPTION, + 'crypto_alg': None, + 'hash_algorithm': None, + 'padding': enums.PaddingMethod.PSS, + 'key': DER_RSA_KEY, + 'verify_args': (padding.PSS( + mgf=padding.MGF1(hashes.MD5()), + salt_length=padding.PSS.MAX_LENGTH + ), + hashes.MD5())}, + {'digital_signature_algorithm': + enums.DigitalSignatureAlgorithm.SHA1_WITH_RSA_ENCRYPTION, + 'crypto_alg': None, + 'hash_algorithm': None, + 'padding': enums.PaddingMethod.PKCS1v15, + 'key': PEM_RSA_KEY, + 'verify_args': (padding.PKCS1v15(), hashes.SHA1())}, + {'digital_signature_algorithm': + enums.DigitalSignatureAlgorithm.SHA224_WITH_RSA_ENCRYPTION, + 'crypto_alg': None, + 'hash_algorithm': None, + 'padding': enums.PaddingMethod.PSS, + 'key': DER_RSA_KEY, + 'verify_args': (padding.PSS( + mgf=padding.MGF1(hashes.SHA224()), + salt_length=padding.PSS.MAX_LENGTH + ), + hashes.SHA224())}, + {'digital_signature_algorithm': + enums.DigitalSignatureAlgorithm.SHA256_WITH_RSA_ENCRYPTION, + 'crypto_alg': None, + 'hash_algorithm': None, + 'padding': enums.PaddingMethod.PKCS1v15, + 'key': PEM_RSA_KEY, + 'verify_args': (padding.PKCS1v15(), hashes.SHA256())}, + {'digital_signature_algorithm': + enums.DigitalSignatureAlgorithm.SHA384_WITH_RSA_ENCRYPTION, + 'crypto_alg': None, + 'hash_algorithm': None, + 'padding': enums.PaddingMethod.PSS, + 'key': DER_RSA_KEY, + 'verify_args': (padding.PSS( + mgf=padding.MGF1(hashes.SHA384()), + salt_length=padding.PSS.MAX_LENGTH + ), + hashes.SHA384())}, + {'digital_signature_algorithm': None, + 'crypto_alg': enums.CryptographicAlgorithm.RSA, + 'hash_algorithm': enums.HashingAlgorithm.SHA_512, + 'padding': enums.PaddingMethod.PKCS1v15, + 'key': PEM_RSA_KEY, + 'verify_args': (padding.PKCS1v15(), hashes.SHA512())} + ] +) +def signing_parameters(request): + return request.param + + +def test_sign(signing_parameters): + engine = crypto.CryptographyEngine() + result = engine.sign( + signing_parameters.get('digital_signature_algorithm'), + signing_parameters.get('crypto_alg'), + signing_parameters.get('hash_algorithm'), + signing_parameters.get('padding'), + signing_parameters.get('key'), + SIGN_TEST_DATA + ) + + RSA_public_key.verify( + result, + SIGN_TEST_DATA, + signing_parameters.get('verify_args')[0], + signing_parameters.get('verify_args')[1] + )