diff --git a/kmip/core/exceptions.py b/kmip/core/exceptions.py index 8308d93..00ae0b9 100644 --- a/kmip/core/exceptions.py +++ b/kmip/core/exceptions.py @@ -13,6 +13,65 @@ # License for the specific language governing permissions and limitations # under the License. +from kmip.core import enums + + +class KmipError(Exception): + """ + A generic KMIP error that is the base for the KMIP error hierarchy. + """ + + def __init__(self, + status=enums.ResultStatus.OPERATION_FAILED, + reason=enums.ResultReason.GENERAL_FAILURE, + message='A general failure occurred.'): + """ + Create a KmipError exception. + + Args: + status (ResultStatus): An enumeration detailing the result outcome. + reason (ResultReason): An enumeration giving the status rationale. + message (string): A string containing more information about the + error. + """ + super(KmipError, self).__init__(message) + self.status = status + self.reason = reason + + +class CryptographicFailure(KmipError): + """ + An error generated when problems occur with cryptographic operations. + """ + + def __init__(self, message): + """ + Create a CryptographicFailure exception. + + Args: + message (string): A string containing information about the error. + """ + super(CryptographicFailure, self).__init__( + reason=enums.ResultReason.CRYPTOGRAPHIC_FAILURE, + message=message) + + +class InvalidField(KmipError): + """ + An error generated when an invalid field value is processed. + """ + + def __init__(self, message): + """ + Create an InvalidField exception. + + Args: + message (string): A string containing information about the error. + """ + super(InvalidField, self).__init__( + reason=enums.ResultReason.INVALID_FIELD, + message=message) + class InvalidKmipEncoding(Exception): """ diff --git a/kmip/services/server/crypto/__init__.py b/kmip/services/server/crypto/__init__.py index c258e8d..0162ca7 100644 --- a/kmip/services/server/crypto/__init__.py +++ b/kmip/services/server/crypto/__init__.py @@ -12,3 +12,11 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. + +from kmip.services.server.crypto.api import CryptographicEngine +from kmip.services.server.crypto.engine import CryptographyEngine + +__all__ = [ + 'CryptographicEngine', + 'CryptographyEngine', +] diff --git a/kmip/services/server/crypto/engine.py b/kmip/services/server/crypto/engine.py new file mode 100644 index 0000000..5d349af --- /dev/null +++ b/kmip/services/server/crypto/engine.py @@ -0,0 +1,212 @@ +# Copyright (c) 2016 The Johns Hopkins University/Applied Physics Laboratory +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging +import os + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.ciphers import algorithms + +from kmip.core import enums +from kmip.core import exceptions +from kmip.services.server.crypto import api + + +class CryptographyEngine(api.CryptographicEngine): + """ + A cryptographic engine that uses pyca/cryptography to generate + cryptographic objects and conduct cryptographic operations. + """ + + def __init__(self): + """ + Construct a CryptographyEngine. + """ + self.logger = logging.getLogger(__name__) + self._symmetric_key_algorithms = { + enums.CryptographicAlgorithm.TRIPLE_DES: algorithms.TripleDES, + enums.CryptographicAlgorithm.AES: algorithms.AES, + enums.CryptographicAlgorithm.BLOWFISH: algorithms.Blowfish, + enums.CryptographicAlgorithm.CAMELLIA: algorithms.Camellia, + enums.CryptographicAlgorithm.CAST5: algorithms.CAST5, + enums.CryptographicAlgorithm.IDEA: algorithms.IDEA, + enums.CryptographicAlgorithm.RC4: algorithms.ARC4 + } + self._asymetric_key_algorithms = { + enums.CryptographicAlgorithm.RSA: self._create_rsa_key_pair + } + + def create_symmetric_key(self, algorithm, length): + """ + Create a symmetric key. + + Args: + algorithm(CryptographicAlgorithm): An enumeration specifying the + algorithm for which the created key will be compliant. + length(int): The length of the key to be created. This value must + be compliant with the constraints of the provided algorithm. + + Returns: + dict: A dictionary containing the key data, with the following + key/value fields: + * value - the bytes of the key + * format - a KeyFormatType enumeration for the bytes format + + Raises: + InvalidField: Raised when the algorithm is unsupported or the + length is incompatible with the algorithm. + CryptographicFailure: Raised when the key generation process + fails. + + Example: + >>> engine = CryptographyEngine() + >>> key = engine.create_symmetric_key( + ... CryptographicAlgorithm.AES, 256) + """ + if algorithm not in self._symmetric_key_algorithms.keys(): + raise exceptions.InvalidField( + "The cryptographic algorithm {0} is not a supported symmetric " + "key algorithm.".format(algorithm) + ) + + cryptography_algorithm = self._symmetric_key_algorithms.get(algorithm) + + if length not in cryptography_algorithm.key_sizes: + raise exceptions.InvalidField( + "The cryptographic length ({0}) is not valid for " + "the cryptographic algorithm ({1}).".format( + length, algorithm.name + ) + ) + + self.logger.info( + "Generating a {0} symmetric key with length: {1}".format( + algorithm.name, length + ) + ) + + key_bytes = os.urandom(length // 8) + try: + cryptography_algorithm(key_bytes) + except Exception as e: + self.logger.exception(e) + raise exceptions.CryptographicFailure( + "Invalid bytes for the provided cryptographic algorithm.") + + return {'value': key_bytes, 'format': enums.KeyFormatType.RAW} + + def create_asymmetric_key_pair(self, algorithm, length): + """ + Create an asymmetric key pair. + + Args: + algorithm(CryptographicAlgorithm): An enumeration specifying the + algorithm for which the created keys will be compliant. + length(int): The length of the keys to be created. This value must + be compliant with the constraints of the provided algorithm. + + Returns: + dict: A dictionary containing the public key data, with at least + the following key/value fields: + * value - the bytes of the key + * format - a KeyFormatType enumeration for the bytes format + dict: A dictionary containing the private key data, identical in + structure to the one above. + + Raises: + InvalidField: Raised when the algorithm is unsupported or the + length is incompatible with the algorithm. + CryptographicFailure: Raised when the key generation process + fails. + + Example: + >>> engine = CryptographyEngine() + >>> key = engine.create_asymmetric_key( + ... CryptographicAlgorithm.RSA, 2048) + """ + if algorithm not in self._asymetric_key_algorithms.keys(): + raise exceptions.InvalidField( + "The cryptographic algorithm ({0}) is not a supported " + "asymmetric key algorithm.".format(algorithm) + ) + + engine_method = self._asymetric_key_algorithms.get(algorithm) + return engine_method(length) + + def _create_rsa_key_pair(self, length, public_exponent=65537): + """ + Create an RSA key pair. + + Args: + length(int): The length of the keys to be created. This value must + be compliant with the constraints of the provided algorithm. + public_exponent(int): The value of the public exponent needed to + generate the keys. Usually a small Fermat prime number. + Optional, defaults to 65537. + + Returns: + dict: A dictionary containing the public key data, with the + following key/value fields: + * value - the bytes of the key + * format - a KeyFormatType enumeration for the bytes format + * public_exponent - the public exponent integer + dict: A dictionary containing the private key data, identical in + structure to the one above. + + Raises: + CryptographicFailure: Raised when the key generation process + fails. + """ + self.logger.info( + "Generating an RSA key pair with length: {0}, and " + "public_exponent: {1}".format( + length, public_exponent + ) + ) + try: + private_key = rsa.generate_private_key( + public_exponent=public_exponent, + key_size=length, + backend=default_backend()) + public_key = private_key.public_key() + + private_bytes = private_key.private_bytes( + serialization.Encoding.DER, + serialization.PrivateFormat.PKCS8, + serialization.NoEncryption()) + public_bytes = public_key.public_bytes( + serialization.Encoding.DER, + serialization.PublicFormat.PKCS1) + except Exception as e: + self.logger.exception(e) + raise exceptions.CryptographicFailure( + "An error occurred while generating the RSA key pair. " + "See the server log for more information." + ) + + public_key = { + 'value': public_bytes, + 'format': enums.KeyFormatType.PKCS_1, + 'public_exponent': public_exponent + } + private_key = { + 'value': private_bytes, + 'format': enums.KeyFormatType.PKCS_8, + 'public_exponent': public_exponent + } + + return public_key, private_key diff --git a/kmip/tests/unit/services/server/__init__.py b/kmip/tests/unit/services/server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kmip/tests/unit/services/server/crypto/__init__.py b/kmip/tests/unit/services/server/crypto/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kmip/tests/unit/services/server/crypto/test_engine.py b/kmip/tests/unit/services/server/crypto/test_engine.py new file mode 100644 index 0000000..e1b9c5f --- /dev/null +++ b/kmip/tests/unit/services/server/crypto/test_engine.py @@ -0,0 +1,148 @@ +# Copyright (c) 2016 The Johns Hopkins University/Applied Physics Laboratory +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import testtools + +from kmip.core import enums +from kmip.core import exceptions +from kmip.services.server import crypto + + +class TestCryptographyEngine(testtools.TestCase): + """ + Test suite for the CryptographyEngine. + """ + + def setUp(self): + super(TestCryptographyEngine, self).setUp() + + def tearDown(self): + super(TestCryptographyEngine, self).tearDown() + + def test_init(self): + """ + Test that a CryptographyEngine can be constructed. + """ + crypto.CryptographyEngine() + + def test_create_symmetric_key(self): + """ + Test that a symmetric key can be created with valid arguments. + """ + engine = crypto.CryptographyEngine() + key = engine.create_symmetric_key( + enums.CryptographicAlgorithm.AES, + 256 + ) + + self.assertIn('value', key) + self.assertIn('format', key) + self.assertEqual(enums.KeyFormatType.RAW, key.get('format')) + + def test_create_symmetric_key_with_invalid_algorithm(self): + """ + Test that an InvalidField error is raised when creating a symmetric + key with an invalid algorithm. + """ + engine = crypto.CryptographyEngine() + + args = ['invalid', 256] + self.assertRaises( + exceptions.InvalidField, + engine.create_symmetric_key, + *args + ) + + def test_create_symmetric_key_with_invalid_length(self): + """ + Test that an InvalidField error is raised when creating a symmetric + key with an invalid length. + """ + engine = crypto.CryptographyEngine() + + args = [enums.CryptographicAlgorithm.AES, 'invalid'] + self.assertRaises( + exceptions.InvalidField, + engine.create_symmetric_key, + *args + ) + + def test_create_symmetric_key_with_cryptographic_failure(self): + """ + Test that a CryptographicFailure error is raised when the symmetric + key generation process fails. + """ + # Create a dummy algorithm that always fails on instantiation. + class DummyAlgorithm(object): + key_sizes = [0] + + def __init__(self, key_bytes): + raise Exception() + + engine = crypto.CryptographyEngine() + engine._symmetric_key_algorithms.update([( + enums.CryptographicAlgorithm.AES, + DummyAlgorithm + )]) + + args = [enums.CryptographicAlgorithm.AES, 0] + self.assertRaises( + exceptions.CryptographicFailure, + engine.create_symmetric_key, + *args + ) + + def test_create_asymmetric_key(self): + """ + Test that an asymmetric key pair can be created with valid arguments. + """ + engine = crypto.CryptographyEngine() + public_key, private_key = engine.create_asymmetric_key_pair( + enums.CryptographicAlgorithm.RSA, + 2048 + ) + + self.assertIn('value', public_key) + self.assertIn('format', public_key) + self.assertIn('value', private_key) + self.assertIn('format', private_key) + + def test_create_asymmetric_key_with_invalid_algorithm(self): + """ + Test that an InvalidField error is raised when creating an asymmetric + key pair with an invalid algorithm. + """ + engine = crypto.CryptographyEngine() + + args = ['invalid', 2048] + self.assertRaises( + exceptions.InvalidField, + engine.create_asymmetric_key_pair, + *args + ) + + def test_create_asymmetric_key_with_invalid_length(self): + """ + Test that an CryptographicFailure error is raised when creating an + asymmetric key pair with an invalid length. + """ + engine = crypto.CryptographyEngine() + + args = [enums.CryptographicAlgorithm.RSA, 0] + self.assertRaises( + exceptions.CryptographicFailure, + engine.create_asymmetric_key_pair, + *args + ) diff --git a/requirements.txt b/requirements.txt index abd5627..7f49a1d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +cryptography>=1.1 enum34 six>=1.9.0