diff --git a/lower-constraints.txt b/lower-constraints.txt index 2043b090f..d3253c46f 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -1,5 +1,6 @@ appdirs==1.3.0 coverage==4.0 +cryptography==2.1 decorator==3.4.0 deprecation==1.0 dogpile.cache==0.6.2 diff --git a/openstack/image/image_signer.py b/openstack/image/image_signer.py new file mode 100644 index 000000000..19c6ec965 --- /dev/null +++ b/openstack/image/image_signer.py @@ -0,0 +1,69 @@ +# 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. +# + + +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.primitives.asymmetric import utils +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization + +from openstack.image.iterable_chunked_file import IterableChunkedFile + +HASH_METHODS = { + 'SHA-224': hashes.SHA224(), + 'SHA-256': hashes.SHA256(), + 'SHA-384': hashes.SHA384(), + 'SHA-512': hashes.SHA512(), +} + + +class ImageSigner(object): + """Image file signature generator. + + Generates signatures for files using a specified private key file. + """ + + def __init__(self, hash_method='SHA-256', padding_method='RSA-PSS'): + padding_types = { + 'RSA-PSS': padding.PSS( + mgf=padding.MGF1(HASH_METHODS[hash_method]), + salt_length=padding.PSS.MAX_LENGTH + ) + } + # informational attributes + self.hash_method = hash_method + self.padding_method = padding_method + # runtime objects + self.private_key = None + self.hash = HASH_METHODS[hash_method] + self.hasher = hashes.Hash(self.hash, default_backend()) + self.padding = padding_types[padding_method] + + def load_private_key(self, file_path, password=None): + with open(file_path, 'rb') as key_file: + self.private_key = serialization.load_pem_private_key( + key_file.read(), password=password, backend=default_backend() + ) + + def generate_signature(self, file_obj): + file_obj.seek(0) + chunked_file = IterableChunkedFile(file_obj) + for chunk in chunked_file: + self.hasher.update(chunk) + file_obj.seek(0) + digest = self.hasher.finalize() + signature = self.private_key.sign( + digest, self.padding, utils.Prehashed(self.hash) + ) + return signature diff --git a/openstack/image/iterable_chunked_file.py b/openstack/image/iterable_chunked_file.py new file mode 100644 index 000000000..d887ace5b --- /dev/null +++ b/openstack/image/iterable_chunked_file.py @@ -0,0 +1,39 @@ +# 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. +# + + +class IterableChunkedFile(object): + """File object chunk iterator using yield. + + Represents a local file as an iterable object by splitting the file + into chunks. Avoids the file from being completely loaded into memory. + """ + + def __init__(self, file_object, chunk_size=1024 * 1024 * 128, close=False): + self.close_after_read = close + self.file_object = file_object + self.chunk_size = chunk_size + + def __iter__(self): + try: + while True: + data = self.file_object.read(self.chunk_size) + if not data: + break + yield data + finally: + if self.close_after_read: + self.file_object.close() + + def __len__(self): + return len(self.file_object) diff --git a/requirements.txt b/requirements.txt index 5ffab64c5..8eb788aaf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,3 +20,4 @@ iso8601>=0.1.11 # MIT netifaces>=0.10.4 # MIT dogpile.cache>=0.6.2 # BSD +cryptography>=2.1 # BSD/Apache-2.0