mirror of
https://github.com/OpenKMIP/PyKMIP.git
synced 2025-06-30 10:44:23 +02:00
Adding the CryptographyEngine
This changes adds the CryptographyEngine, which uses pyca/cryptography to create symmetric and asymmetric keys. A test suite is included.
This commit is contained in:
parent
9d02201178
commit
53d6b1776e
@ -13,6 +13,65 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# 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):
|
class InvalidKmipEncoding(Exception):
|
||||||
"""
|
"""
|
||||||
|
@ -12,3 +12,11 @@
|
|||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from kmip.services.server.crypto.api import CryptographicEngine
|
||||||
|
from kmip.services.server.crypto.engine import CryptographyEngine
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'CryptographicEngine',
|
||||||
|
'CryptographyEngine',
|
||||||
|
]
|
||||||
|
212
kmip/services/server/crypto/engine.py
Normal file
212
kmip/services/server/crypto/engine.py
Normal file
@ -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
|
0
kmip/tests/unit/services/server/__init__.py
Normal file
0
kmip/tests/unit/services/server/__init__.py
Normal file
0
kmip/tests/unit/services/server/crypto/__init__.py
Normal file
0
kmip/tests/unit/services/server/crypto/__init__.py
Normal file
148
kmip/tests/unit/services/server/crypto/test_engine.py
Normal file
148
kmip/tests/unit/services/server/crypto/test_engine.py
Normal file
@ -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
|
||||||
|
)
|
@ -1,3 +1,4 @@
|
|||||||
|
cryptography>=1.1
|
||||||
enum34
|
enum34
|
||||||
six>=1.9.0
|
six>=1.9.0
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user