From 8df5fcd1f19e5c024fd7da481ec3e724c530ff3a Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Tue, 9 Apr 2024 15:15:16 +0200 Subject: [PATCH 1/5] Lower coverage requirement to 75% With the upcoming removal of legacy interfaces and related tests, the ratio of tested to untested code changes significantly. This does not mean that the test coverage of the Signer API decreases! Also note that overall test coverage might be higher (see #208). Signed-off-by: Lukas Puehringer --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 18952fbf..23381fcd 100644 --- a/tox.ini +++ b/tox.ini @@ -21,7 +21,7 @@ deps = commands = python -m tests.check_gpg_available coverage run tests/aggregate_tests.py - coverage report -m --fail-under 83 + coverage report -m --fail-under 75 [testenv:purepy311] deps = From 738a4be9101c9d566251a108208172edbd929d56 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Tue, 9 Apr 2024 10:36:12 +0200 Subject: [PATCH 2/5] Remove legacy key modules, tests and references The Signer API provides a more powerful and modern replacement. Signed-off-by: Lukas Puehringer --- securesystemslib/ecdsa_keys.py | 520 -------- securesystemslib/ed25519_keys.py | 363 ------ securesystemslib/hash.py | 6 +- securesystemslib/interface.py | 1091 ---------------- securesystemslib/keys.py | 1862 --------------------------- securesystemslib/rsa_keys.py | 1102 ---------------- tests/check_public_interfaces.py | 232 +--- tests/data/keystore/ecdsa_key | 1 - tests/data/keystore/ecdsa_key.pub | 1 - tests/data/keystore/ed25519_key | 1 - tests/data/keystore/ed25519_key.pub | 1 - tests/data/keystore/no_key | 1 - tests/data/keystore/rsa_key | 42 - tests/data/keystore/rsa_key.pub | 11 - tests/test_ecdsa_keys.py | 247 ---- tests/test_ed25519_keys.py | 200 --- tests/test_interface.py | 1039 --------------- tests/test_keys.py | 933 -------------- tests/test_rsa_keys.py | 458 ------- 19 files changed, 4 insertions(+), 8107 deletions(-) delete mode 100755 securesystemslib/ecdsa_keys.py delete mode 100755 securesystemslib/ed25519_keys.py delete mode 100644 securesystemslib/interface.py delete mode 100755 securesystemslib/keys.py delete mode 100755 securesystemslib/rsa_keys.py delete mode 100644 tests/data/keystore/ecdsa_key delete mode 100644 tests/data/keystore/ecdsa_key.pub delete mode 100644 tests/data/keystore/ed25519_key delete mode 100644 tests/data/keystore/ed25519_key.pub delete mode 100644 tests/data/keystore/no_key delete mode 100644 tests/data/keystore/rsa_key delete mode 100644 tests/data/keystore/rsa_key.pub delete mode 100755 tests/test_ecdsa_keys.py delete mode 100755 tests/test_ed25519_keys.py delete mode 100755 tests/test_interface.py delete mode 100755 tests/test_keys.py delete mode 100755 tests/test_rsa_keys.py diff --git a/securesystemslib/ecdsa_keys.py b/securesystemslib/ecdsa_keys.py deleted file mode 100755 index c3d7c9b8..00000000 --- a/securesystemslib/ecdsa_keys.py +++ /dev/null @@ -1,520 +0,0 @@ -""" - - ecdsa_keys.py - - - Vladimir Diaz - - - November 22, 2016. - - - See LICENSE for licensing information. - - - The goal of this module is to support ECDSA keys and signatures. ECDSA is an - elliptic-curve digital signature algorithm. It grants a similar level of - security as RSA, but uses smaller keys. No subexponential-time algorithm is - known for the elliptic curve discrete logarithm problem. - - https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm - - 'securesystemslib.ecdsa_keys.py' calls the 'cryptography' library to perform - all of the ecdsa-related operations. - - The ecdsa-related functions included here are generate(), create_signature() - and verify_signature(). The 'cryptography' library is used by ecdsa_keys.py - to perform the actual ECDSA computations, and the functions listed above can - be viewed as an easy-to-use public interface. - """ - -import logging - -# Import cryptography modules to support ecdsa keys and signatures. -CRYPTO = True -NO_CRYPTO_MSG = "ECDSA key support requires the cryptography library" -try: - from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm - from cryptography.hazmat.backends import default_backend - from cryptography.hazmat.primitives import hashes, serialization - from cryptography.hazmat.primitives.asymmetric import ec - from cryptography.hazmat.primitives.serialization import ( - load_pem_private_key, - load_pem_public_key, - ) - - _SCHEME_HASHER = { - "ecdsa-sha2-nistp256": ec.ECDSA(hashes.SHA256()), - "ecdsa-sha2-nistp384": ec.ECDSA(hashes.SHA384()), - } - -except ImportError: - CRYPTO = False - -# Perform object format-checking and add ability to handle/raise exceptions. -from securesystemslib import ( # pylint: disable=wrong-import-position - exceptions, - formats, -) - -_SUPPORTED_ECDSA_SCHEMES = ["ecdsa-sha2-nistp256"] - -logger = logging.getLogger(__name__) - - -def generate_public_and_private(scheme="ecdsa-sha2-nistp256"): - """ - - Generate a pair of ECDSA public and private keys with one of the supported, - external cryptography libraries. The public and private keys returned - conform to 'securesystemslib.formats.PEMECDSA_SCHEMA' and - 'securesystemslib.formats.PEMECDSA_SCHEMA', respectively. - - The public ECDSA public key has the PEM format: - TODO: should we encrypt the private keys returned here? Should the - create_signature() accept encrypted keys? - - '-----BEGIN PUBLIC KEY----- - - ... - - '-----END PUBLIC KEY-----' - - - - The private ECDSA private key has the PEM format: - - '-----BEGIN EC PRIVATE KEY----- - - ... - - -----END EC PRIVATE KEY-----' - - >>> public, private = generate_public_and_private() - >>> securesystemslib.formats.PEMECDSA_SCHEMA.matches(public) - True - >>> securesystemslib.formats.PEMECDSA_SCHEMA.matches(private) - True - - - scheme: - A string indicating which algorithm to use for the generation of the - public and private ECDSA keys. 'ecdsa-sha2-nistp256' is the only - currently supported ECDSA algorithm, which is supported by OpenSSH and - specified in RFC 5656 (https://tools.ietf.org/html/rfc5656). - - - securesystemslib.exceptions.FormatError, if 'algorithm' is improperly - formatted. - - securesystemslib.exceptions.UnsupportedAlgorithmError, if 'scheme' is an - unsupported algorithm. - - securesystemslib.exceptions.UnsupportedLibraryError, if the cryptography - module is not available. - - - None. - - - A (public, private) tuple that conform to - 'securesystemslib.formats.PEMECDSA_SCHEMA' and - 'securesystemslib.formats.PEMECDSA_SCHEMA', respectively. - """ - - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - - # Does 'scheme' have the correct format? - # Verify that 'scheme' is of the correct type, and that it's one of the - # supported ECDSA . It must conform to - # 'securesystemslib.formats.ECDSA_SCHEME_SCHEMA'. Raise - # 'securesystemslib.exceptions.FormatError' if the check fails. - formats.ECDSA_SCHEME_SCHEMA.check_match(scheme) - - public_key = None - private_key = None - - # An if-clause is strictly not needed, since 'ecdsa_sha2-nistp256' is the - # only currently supported ECDSA signature scheme. Nevertheness, include the - # conditional statement to accomodate any schemes that might be added. - if scheme == "ecdsa-sha2-nistp256": - private_key = ec.generate_private_key(ec.SECP256R1, default_backend()) - public_key = private_key.public_key() - - # The ECDSA_SCHEME_SCHEMA.check_match() above should have detected any - # invalid 'scheme'. This is a defensive check. - else: # pragma: no cover - raise exceptions.UnsupportedAlgorithmError( - "An unsupported" - " scheme specified: " + repr(scheme) + ".\n Supported" - " algorithms: " + repr(_SUPPORTED_ECDSA_SCHEMES) - ) - - private_pem = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - ).strip() - - public_pem = public_key.public_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PublicFormat.SubjectPublicKeyInfo, - ).strip() - - return public_pem.decode("utf-8"), private_pem.decode("utf-8") - - -def create_signature( - public_key, private_key, data, scheme="ecdsa-sha2-nistp256" -): - """ - - Return a (signature, scheme) tuple. - - >>> requested_scheme = 'ecdsa-sha2-nistp256' - >>> public, private = generate_public_and_private(requested_scheme) - >>> data = b'The quick brown fox jumps over the lazy dog' - >>> signature, scheme = create_signature(public, private, data, requested_scheme) - >>> securesystemslib.formats.ECDSASIGNATURE_SCHEMA.matches(signature) - True - >>> requested_scheme == scheme - True - - - public: - The ECDSA public key in PEM format. - - private: - The ECDSA private key in PEM format. - - data: - Byte data used by create_signature() to generate the signature returned. - - scheme: - The signature scheme used to generate the signature. For example: - 'ecdsa-sha2-nistp256'. - - - securesystemslib.exceptions.FormatError, if the arguments are improperly - formatted. - - securesystemslib.exceptions.CryptoError, if a signature cannot be created. - - securesystemslib.exceptions.UnsupportedAlgorithmError, if 'scheme' is not - one of the supported signature schemes. - - securesystemslib.exceptions.UnsupportedLibraryError, if the cryptography - module is not available. - - - None. - - - A signature dictionary conformat to - 'securesystemslib.format.SIGNATURE_SCHEMA'. ECDSA signatures are XX bytes, - however, the hexlified signature is stored in the dictionary returned. - """ - - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - - # Do 'public_key' and 'private_key' have the correct format? - # This check will ensure that the arguments conform to - # 'securesystemslib.formats.PEMECDSA_SCHEMA'. Raise - # 'securesystemslib.exceptions.FormatError' if the check fails. - formats.PEMECDSA_SCHEMA.check_match(public_key) - - # Is 'private_key' properly formatted? - formats.PEMECDSA_SCHEMA.check_match(private_key) - - # Is 'scheme' properly formatted? - formats.ECDSA_SCHEME_SCHEMA.check_match(scheme) - - # 'ecdsa-sha2-nistp256' is the only currently supported ECDSA scheme, so this - # if-clause isn't strictly needed. Nevertheless, the conditional statement - # is included to accommodate multiple schemes that can potentially be added - # in the future. - if scheme == "ecdsa-sha2-nistp256": - try: - private_key = load_pem_private_key( - private_key.encode("utf-8"), - password=None, - backend=default_backend(), - ) - - signature = private_key.sign(data, ec.ECDSA(hashes.SHA256())) - - except TypeError as e: - raise exceptions.CryptoError( - "Could not create" " signature: " + str(e) - ) - - # A defensive check for an invalid 'scheme'. The - # ECDSA_SCHEME_SCHEMA.check_match() above should have already validated it. - else: # pragma: no cover - raise exceptions.UnsupportedAlgorithmError( - "Unsupported" " signature scheme is specified: " + repr(scheme) - ) - - return signature, scheme - - -def verify_signature(public_key, scheme, signature, data): - """ - - Verify that 'signature' was produced by the private key associated with - 'public_key'. - - >>> scheme = 'ecdsa-sha2-nistp256' - >>> public, private = generate_public_and_private(scheme) - >>> data = b'The quick brown fox jumps over the lazy dog' - >>> signature, scheme = create_signature(public, private, data, scheme) - >>> verify_signature(public, scheme, signature, data) - True - >>> verify_signature(public, scheme, signature, b'bad data') - False - - - public_key: - The ECDSA public key in PEM format. The public key is needed to verify - 'signature'. - - scheme: - The signature scheme used to generate 'signature'. For example: - 'ecdsa-sha2-nistp256'. - - signature: - The signature to be verified, which should have been generated by - the private key associated with 'public_key'. 'data'. - - data: - Byte data that was used by create_signature() to generate 'signature'. - - - securesystemslib.exceptions.FormatError, if any of the arguments are - improperly formatted. - - securesystemslib.exceptions.UnsupportedAlgorithmError, if 'scheme' is - not one of the supported signature schemes. - - securesystemslib.exceptions.UnsupportedLibraryError, if the cryptography - module is not available. - - - None. - - - Boolean, indicating whether the 'signature' of data was generated by - the private key associated with 'public_key'. - """ - - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - - # Are the arguments properly formatted? - # If not, raise 'securesystemslib.exceptions.FormatError'. - formats.PEMECDSA_SCHEMA.check_match(public_key) - formats.ECDSA_SCHEME_SCHEMA.check_match(scheme) - formats.ECDSASIGNATURE_SCHEMA.check_match(signature) - - try: - ecdsa_key = load_pem_public_key( - public_key.encode("utf-8"), backend=default_backend() - ) - except ValueError as e: - raise exceptions.FormatError( - f"Failed to load PEM key {public_key}" - ) from e - - if not isinstance(ecdsa_key, ec.EllipticCurvePublicKey): - raise exceptions.FormatError( - "Invalid ECDSA public" " key: " + repr(public_key) - ) - logger.debug("Loaded a valid ECDSA public key.") - - # verify() raises an 'InvalidSignature' exception if 'signature' - # is invalid. - try: - ecdsa_key.verify(signature, data, _SCHEME_HASHER[scheme]) - return True - - except (TypeError, InvalidSignature): - return False - - -def create_ecdsa_public_and_private_from_pem(pem, password=None): - """ - - Create public and private ECDSA keys from a private 'pem'. The public and - private keys are strings in PEM format: - - public: '-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----', - private: '-----BEGIN EC PRIVATE KEY----- ... -----END EC PRIVATE KEY-----'}} - - >>> junk, private = generate_public_and_private() - >>> public, private = create_ecdsa_public_and_private_from_pem(private) - >>> securesystemslib.formats.PEMECDSA_SCHEMA.matches(public) - True - >>> securesystemslib.formats.PEMECDSA_SCHEMA.matches(private) - True - >>> passphrase = 'secret' - >>> encrypted_pem = create_ecdsa_encrypted_pem(private, passphrase) - >>> public, private = create_ecdsa_public_and_private_from_pem(encrypted_pem, passphrase) - >>> securesystemslib.formats.PEMECDSA_SCHEMA.matches(public) - True - >>> securesystemslib.formats.PEMECDSA_SCHEMA.matches(private) - True - - - pem: - A string in PEM format. The private key is extracted and returned in - an ecdsakey object. - - password: (optional) - The password, or passphrase, to decrypt the private part of the ECDSA key - if it is encrypted. 'password' is not used directly as the encryption - key, a stronger encryption key is derived from it. - - - securesystemslib.exceptions.FormatError, if the arguments are improperly - formatted. - - securesystemslib.exceptions.UnsupportedAlgorithmError, if the ECDSA key - pair could not be extracted, possibly due to an unsupported algorithm. - - securesystemslib.exceptions.UnsupportedLibraryError, if the cryptography - module is not available. - - - None. - - - A dictionary containing the ECDSA keys and other identifying information. - Conforms to 'securesystemslib.formats.ECDSAKEY_SCHEMA'. - """ - - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - - # Does 'pem' have the correct format? - # This check will ensure 'pem' conforms to - # 'securesystemslib.formats.ECDSARSA_SCHEMA'. - formats.PEMECDSA_SCHEMA.check_match(pem) - - if password is not None: - formats.PASSWORD_SCHEMA.check_match(password) - password = password.encode("utf-8") - - else: - logger.debug( - "The password/passphrase is unset. The PEM is expected" - " to be unencrypted." - ) - - public = None - private = None - - # Generate the public and private ECDSA keys. The pyca/cryptography library - # performs the actual import operation. - try: - private = load_pem_private_key( - pem.encode("utf-8"), password=password, backend=default_backend() - ) - - except (ValueError, UnsupportedAlgorithm) as e: - raise exceptions.CryptoError( - "Could not import private" " PEM.\n" + str(e) - ) - - public = private.public_key() - - # Serialize public and private keys to PEM format. - private = private.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - ).strip() - - public = public.public_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PublicFormat.SubjectPublicKeyInfo, - ).strip() - - return public.decode("utf-8"), private.decode("utf-8") - - -def create_ecdsa_encrypted_pem(private_pem, passphrase): - """ - - Return a string in PEM format, where the private part of the ECDSA key is - encrypted. The private part of the ECDSA key is encrypted as done by - pyca/cryptography: "Encrypt using the best available encryption for a given - key's backend. This is a curated encryption choice and the algorithm may - change over time." - - >>> junk, private = generate_public_and_private() - >>> passphrase = 'secret' - >>> encrypted_pem = create_ecdsa_encrypted_pem(private, passphrase) - >>> securesystemslib.formats.PEMECDSA_SCHEMA.matches(encrypted_pem) - True - - - private_pem: - The private ECDSA key string in PEM format. - - passphrase: - The passphrase, or password, to encrypt the private part of the ECDSA - key. 'passphrase' is not used directly as the encryption key, a stronger - encryption key is derived from it. - - - securesystemslib.exceptions.FormatError, if the arguments are improperly - formatted. - - securesystemslib.exceptions.CryptoError, if an ECDSA key in encrypted PEM - format cannot be created. - - securesystemslib.exceptions.UnsupportedLibraryError, if the cryptography - module is not available. - - - None. - - - A string in PEM format, where the private RSA portion is encrypted. - Conforms to 'securesystemslib.formats.PEMECDSA_SCHEMA'. - """ - - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - - # Does 'private_key' have the correct format? - # Raise 'securesystemslib.exceptions.FormatError' if the check fails. - formats.PEMRSA_SCHEMA.check_match(private_pem) - - # Does 'passphrase' have the correct format? - formats.PASSWORD_SCHEMA.check_match(passphrase) - - private = load_pem_private_key( - private_pem.encode("utf-8"), password=None, backend=default_backend() - ) - - encrypted_private_pem = private.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.BestAvailableEncryption( - passphrase.encode("utf-8") - ), - ).strip() - - return encrypted_private_pem - - -if __name__ == "__main__": - # The interactive sessions of the documentation strings can - # be tested by running 'ecdsa_keys.py' as a standalone module. - # python -B ecdsa_keys.py - import doctest - - doctest.testmod() diff --git a/securesystemslib/ed25519_keys.py b/securesystemslib/ed25519_keys.py deleted file mode 100755 index b4ad62e9..00000000 --- a/securesystemslib/ed25519_keys.py +++ /dev/null @@ -1,363 +0,0 @@ -""" - - ed25519_keys.py - - - Vladimir Diaz - - - September 24, 2013. - - - See LICENSE for licensing information. - - - The goal of this module is to support Ed25519 signatures. Ed25519 is an - elliptic-curve public key signature scheme, its main strength being small - signatures (64 bytes) and small public keys (32 bytes). - http://ed25519.cr.yp.to/ - - 'securesystemslib/ed25519_keys.py' calls 'ed25519.py', which is the pure - Python implementation of ed25519 optimized for a faster runtime. The Python - reference implementation is concise, but very slow (verifying signatures - takes ~9 seconds on an Intel core 2 duo @ 2.2 ghz x 2). The optimized - version can verify signatures in ~2 seconds. - - http://ed25519.cr.yp.to/software.html - https://github.com/pyca/ed25519 - - Optionally, ed25519 cryptographic operations may be executed by PyNaCl, which - is a Python binding to the NaCl library and is faster than the pure python - implementation. Verifying signatures can take approximately 0.0009 seconds. - PyNaCl relies on the libsodium C library. PyNaCl is required for key and - signature generation. Verifying signatures may be done in pure Python. - - https://github.com/pyca/pynacl - https://github.com/jedisct1/libsodium - http://nacl.cr.yp.to/ - https://github.com/pyca/ed25519 - - The ed25519-related functions included here are generate(), create_signature() - and verify_signature(). The 'ed25519' and PyNaCl (i.e., 'nacl') modules used - by ed25519_keys.py perform the actual ed25519 computations and the functions - listed above can be viewed as an easy-to-use public interface. - """ - -# 'os' required to generate OS-specific randomness (os.urandom) suitable for -# cryptographic use. -# http://docs.python.org/2/library/os.html#miscellaneous-functions -import os - -# Import the python implementation of the ed25519 algorithm provided by pyca, -# which is an optimized version of the one provided by ed25519's authors. -# Note: The pure Python version does not include protection against side-channel -# attacks. Verifying signatures can take approximately 2 seconds on an intel -# core 2 duo @ 2.2 ghz x 2). Optionally, the PyNaCl module may be used to -# speed up ed25519 cryptographic operations. -# http://ed25519.cr.yp.to/software.html -# https://github.com/pyca/ed25519 -# https://github.com/pyca/pynacl -# -# Import the PyNaCl library, if available. It is recommended this library be -# used over the pure python implementation of Ed25519, due to its speedier -# routines and side-channel protections available in the libsodium library. -NACL = True -NO_NACL_MSG = "ed25519 key support requires the nacl library" -try: - # avoid conflicts with own exceptions of same name - from nacl import exceptions as nacl_exceptions - from nacl.encoding import RawEncoder - from nacl.signing import SigningKey, VerifyKey -except ImportError: - NACL = False - -# pylint: disable=wrong-import-position -from securesystemslib import exceptions, formats - -# The optimized pure Python implementation of Ed25519. If -# PyNaCl cannot be imported and an attempt to use is made in this module, a -# 'securesystemslib.exceptions.UnsupportedLibraryError' exception is raised. -from securesystemslib._vendor.ed25519 import ed25519 as python_ed25519 - -# pylint: enable=wrong-import-position - -# Supported ed25519 signing schemes: 'ed25519'. The pure Python implementation -# (i.e., ed25519') and PyNaCl (i.e., 'nacl', libsodium + Python bindings) -# modules are currently supported in the creation of 'ed25519' signatures. -# Previously, a distinction was made between signatures made by the pure Python -# implementation and PyNaCl. -_SUPPORTED_ED25519_SIGNING_SCHEMES = ["ed25519"] - - -def generate_public_and_private(): - """ - - Generate a pair of ed25519 public and private keys with PyNaCl. The public - and private keys returned conform to - 'securesystemslib.formats.ED25519PUBLIC_SCHEMA' and - 'securesystemslib.formats.ED25519SEED_SCHEMA', respectively. - - An ed25519 seed key is a random 32-byte string. Public keys are also 32 - bytes. - - >>> public, private = generate_public_and_private() - >>> securesystemslib.formats.ED25519PUBLIC_SCHEMA.matches(public) - True - >>> securesystemslib.formats.ED25519SEED_SCHEMA.matches(private) - True - - - None. - - - securesystemslib.exceptions.UnsupportedLibraryError, if the PyNaCl ('nacl') - module is unavailable. - - NotImplementedError, if a randomness source is not found by 'os.urandom'. - - - The ed25519 keys are generated by first creating a random 32-byte seed - with os.urandom() and then calling PyNaCl's nacl.signing.SigningKey(). - - - A (public, private) tuple that conform to - 'securesystemslib.formats.ED25519PUBLIC_SCHEMA' and - 'securesystemslib.formats.ED25519SEED_SCHEMA', respectively. - """ - - if not NACL: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_NACL_MSG) - - # Generate ed25519's seed key by calling os.urandom(). The random bytes - # returned should be suitable for cryptographic use and is OS-specific. - # Raise 'NotImplementedError' if a randomness source is not found. - # ed25519 seed keys are fixed at 32 bytes (256-bit keys). - # http://blog.mozilla.org/warner/2011/11/29/ed25519-keys/ - seed = os.urandom(32) - public = None - - # Generate the public key. PyNaCl (i.e., 'nacl' module) performs the actual - # key generation. - nacl_key = SigningKey(seed) - public = nacl_key.verify_key.encode(encoder=RawEncoder()) - - return public, seed - - -def create_signature(public_key, private_key, data, scheme): - """ - - Return a (signature, scheme) tuple, where the signature scheme is 'ed25519' - and is always generated by PyNaCl (i.e., 'nacl'). The signature returned - conforms to 'securesystemslib.formats.ED25519SIGNATURE_SCHEMA', and has the - form: - - '\xae\xd7\x9f\xaf\x95{bP\x9e\xa8YO Z\x86\x9d...' - - A signature is a 64-byte string. - - >>> public, private = generate_public_and_private() - >>> data = b'The quick brown fox jumps over the lazy dog' - >>> scheme = 'ed25519' - >>> signature, scheme = \ - create_signature(public, private, data, scheme) - >>> securesystemslib.formats.ED25519SIGNATURE_SCHEMA.matches(signature) - True - >>> scheme == 'ed25519' - True - >>> signature, scheme = \ - create_signature(public, private, data, scheme) - >>> securesystemslib.formats.ED25519SIGNATURE_SCHEMA.matches(signature) - True - >>> scheme == 'ed25519' - True - - - public: - The ed25519 public key, which is a 32-byte string. - - private: - The ed25519 private key, which is a 32-byte string. - - data: - Data object used by create_signature() to generate the signature. - - scheme: - The signature scheme used to generate the signature. - - - securesystemslib.exceptions.FormatError, if the arguments are improperly - formatted. - - securesystemslib.exceptions.CryptoError, if a signature cannot be created. - - securesystemslib.exceptions.UnsupportedLibraryError, if the PyNaCl ('nacl') - module is unavailable. - - - nacl.signing.SigningKey.sign() called to generate the actual signature. - - - A signature dictionary conformat to - 'securesystemslib.format.SIGNATURE_SCHEMA'. ed25519 signatures are 64 - bytes, however, the hexlified signature is stored in the dictionary - returned. - """ - - if not NACL: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_NACL_MSG) - - # Does 'public_key' have the correct format? - # This check will ensure 'public_key' conforms to - # 'securesystemslib.formats.ED25519PUBLIC_SCHEMA', which must have length 32 - # bytes. Raise 'securesystemslib.exceptions.FormatError' if the check fails. - formats.ED25519PUBLIC_SCHEMA.check_match(public_key) - - # Is 'private_key' properly formatted? - formats.ED25519SEED_SCHEMA.check_match(private_key) - - # Is 'scheme' properly formatted? - formats.ED25519_SIG_SCHEMA.check_match(scheme) - - # Signing the 'data' object requires a seed and public key. - # nacl.signing.SigningKey.sign() generates the signature. - signature = None - - # An if-clause is not strictly needed here, since 'ed25519' is the only - # currently supported scheme. Nevertheless, include the conditional - # statement to accommodate schemes that might be added in the future. - if scheme == "ed25519": - try: - nacl_key = SigningKey(private_key) - nacl_sig = nacl_key.sign(data) - signature = nacl_sig.signature - - except (ValueError, TypeError, nacl_exceptions.CryptoError) as e: - raise exceptions.CryptoError( - 'An "ed25519" signature' - " could not be created with PyNaCl." + str(e) - ) - - # This is a defensive check for a valid 'scheme', which should have already - # been validated in the check_match() above. - else: # pragma: no cover - raise exceptions.UnsupportedAlgorithmError( - "Unsupported" " signature scheme is specified: " + repr(scheme) - ) - - return signature, scheme - - -def verify_signature(public_key, scheme, signature, data): - """ - - Determine whether the private key corresponding to 'public_key' produced - 'signature'. verify_signature() will use the public key, the 'scheme' and - 'sig', and 'data' arguments to complete the verification. - - >>> public, private = generate_public_and_private() - >>> data = b'The quick brown fox jumps over the lazy dog' - >>> scheme = 'ed25519' - >>> signature, scheme = \ - create_signature(public, private, data, scheme) - >>> verify_signature(public, scheme, signature, data) - True - >>> bad_data = b'The sly brown fox jumps over the lazy dog' - >>> bad_signature, scheme = \ - create_signature(public, private, bad_data, scheme) - >>> verify_signature(public, scheme, bad_signature, data) - False - - - public_key: - The public key is a 32-byte string. - - scheme: - 'ed25519' signature scheme used by either the pure python - implementation (i.e., ed25519.py) or PyNacl (i.e., 'nacl'). - - signature: - The signature is a 64-byte string. - - data: - Data object used by securesystemslib.ed25519_keys.create_signature() to - generate 'signature'. 'data' is needed here to verify the signature. - - - securesystemslib.exceptions.UnsupportedAlgorithmError. Raised if the - signature scheme 'scheme' is not one supported by - securesystemslib.ed25519_keys.create_signature(). - - securesystemslib.exceptions.FormatError. Raised if the arguments are - improperly formatted. - - - nacl.signing.VerifyKey.verify() called if available, otherwise - securesystemslib._vendor.ed25519.ed25519.checkvalid() called to do the - verification. - - - Boolean. True if the signature is valid, False otherwise. - """ - - # Does 'public_key' have the correct format? - # This check will ensure 'public_key' conforms to - # 'securesystemslib.formats.ED25519PUBLIC_SCHEMA', which must have length 32 - # bytes. Raise 'securesystemslib.exceptions.FormatError' if the check fails. - formats.ED25519PUBLIC_SCHEMA.check_match(public_key) - - # Is 'scheme' properly formatted? - formats.ED25519_SIG_SCHEMA.check_match(scheme) - - # Is 'signature' properly formatted? - formats.ED25519SIGNATURE_SCHEMA.check_match(signature) - - # Verify 'signature'. Before returning the Boolean result, ensure 'ed25519' - # was used as the signature scheme. - public = public_key - valid_signature = False - - if scheme in _SUPPORTED_ED25519_SIGNING_SCHEMES: - if NACL: - try: - nacl_verify_key = VerifyKey(public) - nacl_verify_key.verify(data, signature) - valid_signature = True - - except nacl_exceptions.BadSignatureError: - pass - - # Verify 'ed25519' signature with the pure Python implementation. - else: - try: - python_ed25519.checkvalid(signature, data, public) - valid_signature = True - - # The pure Python implementation raises 'Exception' if 'signature' is - # invalid. - except Exception: # pylint: disable=broad-except # nosec - pass - - # This is a defensive check for a valid 'scheme', which should have already - # been validated in the ED25519_SIG_SCHEMA.check_match(scheme) above. - else: # pragma: no cover - message = ( - "Unsupported ed25519 signature scheme: " - + repr(scheme) - + ".\n" - + "Supported schemes: " - + repr(_SUPPORTED_ED25519_SIGNING_SCHEMES) - + "." - ) - raise exceptions.UnsupportedAlgorithmError(message) - - return valid_signature - - -if __name__ == "__main__": - # The interactive sessions of the documentation strings can - # be tested by running 'ed25519_keys.py' as a standalone module. - # python -B ed25519_keys.py - import doctest - - doctest.testmod() diff --git a/securesystemslib/hash.py b/securesystemslib/hash.py index a8ae0f52..59aaace4 100755 --- a/securesystemslib/hash.py +++ b/securesystemslib/hash.py @@ -64,8 +64,7 @@ class PycaDiggestWrapper( algorithm: - Specific for `cryptography.hazmat.primitives.hashes.Hash` object, but - needed for `rsa_keys.py` + Specific for `cryptography.hazmat.primitives.hashes.Hash` object. digest_size: Returns original's object digest size. @@ -394,8 +393,7 @@ def digest_from_rsa_scheme(scheme, hash_library=DEFAULT_HASH_LIBRARY): scheme: A string that indicates the signature scheme used to generate - 'signature'. Currently supported RSA schemes are defined in - `securesystemslib.keys.RSA_SIGNATURE_SCHEMES` + 'signature'. hash_library: The crypto library to use for the given hash algorithm (e.g., 'hashlib'). diff --git a/securesystemslib/interface.py b/securesystemslib/interface.py deleted file mode 100644 index 3afb4fdd..00000000 --- a/securesystemslib/interface.py +++ /dev/null @@ -1,1091 +0,0 @@ -""" - - interface.py - - - Vladimir Diaz - - - January 5, 2017. - - - See LICENSE for licensing information. - - - Provide an interface to the cryptography functions available in - securesystemslib. The interface can be used with the Python interpreter in - interactive mode, or imported directly into a Python module. See - 'securesystemslib/README' for the complete guide to using 'interface.py'. -""" - -import getpass -import json -import logging -import os -import sys -import tempfile - -from securesystemslib import ( - KEY_TYPE_ECDSA, - KEY_TYPE_ED25519, - KEY_TYPE_RSA, - exceptions, - formats, - keys, - settings, - util, -) -from securesystemslib.storage import FilesystemBackend - -logger = logging.getLogger(__name__) - -# Recommended RSA key sizes: -# https://en.wikipedia.org/wiki/Key_size#Asymmetric_algorithm_key_lengths -# Based on the above, RSA keys of size 3072 bits are expected to provide -# security through 2031 and beyond. -DEFAULT_RSA_KEY_BITS = 3072 - - -def get_password(prompt="Password: ", confirm=False): - """Prompts user to enter a password. - - Arguments: - prompt (optional): A text displayed on the prompt (stderr). - confirm (optional): A boolean indicating if the user needs to enter the - same password twice. - - Returns: - The password entered on the prompt. - - """ - formats.TEXT_SCHEMA.check_match(prompt) - formats.BOOLEAN_SCHEMA.check_match(confirm) - - while True: - # getpass() prompts the user for a password without echoing - # the user input. - password = getpass.getpass(prompt, sys.stderr) - - if not confirm: - return password - password2 = getpass.getpass("Confirm: ", sys.stderr) - - if password == password2: # pylint: disable=no-else-return - return password - - else: - print("Mismatch; try again.") - - -def _get_key_file_encryption_password(password, prompt, path): - """Encryption password helper for `_generate_and_write_*_keypair` functions. - - Combinations of 'password' and 'prompt' -> result (explanation) - ---------------------------------------------------------------- - None False -> return None (clear non-encryption desire) - "" False -> return password (clear encryption desire) - False -> raise (bad pw type, unclear encryption desire) - True -> raise (unclear password/prompt precedence) - None True -> prompt and return password if entered and None - otherwise (users on the prompt can only - indicate desire to not encrypt by entering no - password) - """ - formats.BOOLEAN_SCHEMA.check_match(prompt) - - # We don't want to decide which takes precedence so we fail - if password is not None and prompt: - raise ValueError("passing 'password' and 'prompt=True' is not allowed") - - # Prompt user for password and confirmation - if prompt: - password = get_password( - "enter password to encrypt private key file " - "'" + str(path) + "' (leave empty if key " - "should not be encrypted): ", - confirm=True, - ) - - # Treat empty password as no password. A user on the prompt can only - # indicate the desire to not encrypt by entering no password. - if not len(password): # pylint: disable=use-implicit-booleaness-not-len - return None - - if password is not None: - formats.PASSWORD_SCHEMA.check_match(password) - - # Fail on empty passed password. A caller should pass None to indicate the - # desire to not encrypt. - if not len(password): # pylint: disable=use-implicit-booleaness-not-len - raise ValueError( - "encryption password must be 1 or more characters long" - ) - - return password - - -def _get_key_file_decryption_password(password, prompt, path): - """Decryption password helper for `import_*_privatekey_from_file` functions. - - Combinations of 'password' and 'prompt' -> result (explanation) - ---------------------------------------------------------------- - None False -> return None (clear non-decryption desire) - "" False -> return password (clear decryption desire) - False -> raise (bad pw type, unclear decryption desire) - True -> raise (unclear password/prompt precedence) - None True -> prompt and return password if entered and None - otherwise (users on the prompt can only indicate - desire to not decrypt by entering no password) - - """ - formats.BOOLEAN_SCHEMA.check_match(prompt) - - # We don't want to decide which takes precedence so we fail - if password is not None and prompt: - raise ValueError("passing 'password' and 'prompt=True' is not allowed") - - # Prompt user for password - if prompt: - password = get_password( - "enter password to decrypt private key file " - "'" + str(path) + "' " - "(leave empty if key not encrypted): ", - confirm=False, - ) - - # Treat empty password as no password. A user on the prompt can only - # indicate the desire to not decrypt by entering no password. - if not len(password): # pylint: disable=use-implicit-booleaness-not-len - return None - - if password is not None: - formats.PASSWORD_SCHEMA.check_match(password) - # No additional vetting needed. Decryption will show if it was correct. - - return password - - -def _generate_and_write_rsa_keypair( - filepath=None, bits=DEFAULT_RSA_KEY_BITS, password=None, prompt=False -): - """Generates RSA key pair and writes PEM-encoded keys to disk. - - If a password is passed or entered on the prompt, the private key is - encrypted. According to the documentation of the used pyca/cryptography - library, encryption is performed "using the best available encryption for a - given key's backend", which "is a curated encryption choice and the algorithm - may change over time." The private key is written in PKCS#1 and the public - key in X.509 SubjectPublicKeyInfo format. - - NOTE: A signing scheme can be assigned on key import (see import functions). - - Arguments: - filepath (optional): The path to write the private key to. If not passed, - the key is written to CWD using the keyid as filename. The public key - is written to the same path as the private key using the suffix '.pub'. - bits (optional): The number of bits of the generated RSA key. - password (optional): An encryption password. - prompt (optional): A boolean indicating if the user should be prompted - for an encryption password. If the user enters an empty password, the - key is not encrypted. - - Raises: - UnsupportedLibraryError: pyca/cryptography is not available. - FormatError: Arguments are malformed. - ValueError: An empty string is passed as 'password', or both a 'password' - is passed and 'prompt' is true. - StorageError: Key files cannot be written. - - Side Effects: - Prompts user for a password if 'prompt' is True. - Writes key files to disk. - Overwrites files if they already exist. - - Returns: - The private key filepath. - - """ - formats.RSAKEYBITS_SCHEMA.check_match(bits) - - # Generate private RSA key and extract public and private both in PEM - rsa_key = keys.generate_rsa_key(bits) - public = rsa_key["keyval"]["public"] - private = rsa_key["keyval"]["private"] - - # Use passed 'filepath' or keyid as file name - if not filepath: - filepath = os.path.join(os.getcwd(), rsa_key["keyid"]) - - formats.PATH_SCHEMA.check_match(filepath) - - password = _get_key_file_encryption_password(password, prompt, filepath) - - # Encrypt the private key if a 'password' was passed or entered on the prompt - if password is not None: - private = keys.create_rsa_encrypted_pem(private, password) - - # Create intermediate directories as required - util.ensure_parent_dir(filepath) - - # Write PEM-encoded public key to .pub - file_object = tempfile.TemporaryFile() - file_object.write(public.encode("utf-8")) - util.persist_temp_file(file_object, filepath + ".pub") - - # Write PEM-encoded private key to - file_object = tempfile.TemporaryFile() - file_object.write(private.encode("utf-8")) - util.persist_temp_file(file_object, filepath, restrict=True) - - return filepath - - -def generate_and_write_rsa_keypair( - password, filepath=None, bits=DEFAULT_RSA_KEY_BITS -): - """Generates RSA key pair and writes PEM-encoded keys to disk. - - The private key is encrypted using the best available encryption algorithm - chosen by 'pyca/cryptography', which may change over time. The private key is - written in PKCS#1 and the public key in X.509 SubjectPublicKeyInfo format. - - NOTE: A signing scheme can be assigned on key import (see import functions). - - Arguments: - password: An encryption password. - filepath (optional): The path to write the private key to. If not passed, - the key is written to CWD using the keyid as filename. The public key - is written to the same path as the private key using the suffix '.pub'. - bits (optional): The number of bits of the generated RSA key. - - Raises: - UnsupportedLibraryError: pyca/cryptography is not available. - FormatError: Arguments are malformed. - ValueError: An empty string is passed as 'password'. - StorageError: Key files cannot be written. - - Side Effects: - Writes key files to disk. - Overwrites files if they already exist. - - Returns: - The private key filepath. - - """ - formats.PASSWORD_SCHEMA.check_match(password) - return _generate_and_write_rsa_keypair( - filepath=filepath, bits=bits, password=password, prompt=False - ) - - -def generate_and_write_rsa_keypair_with_prompt( - filepath=None, bits=DEFAULT_RSA_KEY_BITS -): - """Generates RSA key pair and writes PEM-encoded keys to disk. - - The private key is encrypted with a password entered on the prompt, using the - best available encryption algorithm chosen by 'pyca/cryptography', which may - change over time. The private key is written in PKCS#1 and the public key in - X.509 SubjectPublicKeyInfo format. - - NOTE: A signing scheme can be assigned on key import (see import functions). - - Arguments: - filepath (optional): The path to write the private key to. If not passed, - the key is written to CWD using the keyid as filename. The public key - is written to the same path as the private key using the suffix '.pub'. - bits (optional): The number of bits of the generated RSA key. - - Raises: - UnsupportedLibraryError: pyca/cryptography is not available. - FormatError: Arguments are malformed. - StorageError: Key files cannot be written. - - Side Effects: - Prompts user for a password. - Writes key files to disk. - Overwrites files if they already exist. - - Returns: - The private key filepath. - - """ - return _generate_and_write_rsa_keypair( - filepath=filepath, bits=bits, password=None, prompt=True - ) - - -def generate_and_write_unencrypted_rsa_keypair( - filepath=None, bits=DEFAULT_RSA_KEY_BITS -): - """Generates RSA key pair and writes PEM-encoded keys to disk. - - The private key is written in PKCS#1 and the public key in X.509 - SubjectPublicKeyInfo format. - - NOTE: A signing scheme can be assigned on key import (see import functions). - - Arguments: - filepath (optional): The path to write the private key to. If not passed, - the key is written to CWD using the keyid as filename. The public key - is written to the same path as the private key using the suffix '.pub'. - bits (optional): The number of bits of the generated RSA key. - - Raises: - UnsupportedLibraryError: pyca/cryptography is not available. - FormatError: Arguments are malformed. - StorageError: Key files cannot be written. - - Side Effects: - Writes unencrypted key files to disk. - Overwrites files if they already exist. - - Returns: - The private key filepath. - - """ - return _generate_and_write_rsa_keypair( - filepath=filepath, bits=bits, password=None, prompt=False - ) - - -def import_rsa_privatekey_from_file( - filepath, - password=None, - scheme="rsassa-pss-sha256", - prompt=False, - storage_backend=None, -): - """Imports PEM-encoded RSA private key from file storage. - - The expected key format is PKCS#1. If a password is passed or entered on the - prompt, the private key is decrypted, otherwise it is treated as unencrypted. - - Arguments: - filepath: The path to read the file from. - password (optional): A password to decrypt the key. - scheme (optional): The signing scheme assigned to the returned key object. - See RSA_SCHEME_SCHEMA for available signing schemes. - prompt (optional): A boolean indicating if the user should be prompted - for a decryption password. If the user enters an empty password, the - key is not decrypted. - storage_backend (optional): An object implementing StorageBackendInterface. - If not passed a default FilesystemBackend will be used. - - Raises: - UnsupportedLibraryError: pyca/cryptography is not available. - FormatError: Arguments are malformed. - ValueError: Both a 'password' is passed and 'prompt' is true. - StorageError: Key file cannot be read. - CryptoError: Key cannot be parsed. - - Returns: - An RSA private key object conformant with 'RSAKEY_SCHEMA'. - - """ - formats.PATH_SCHEMA.check_match(filepath) - formats.RSA_SCHEME_SCHEMA.check_match(scheme) - - password = _get_key_file_decryption_password(password, prompt, filepath) - - if storage_backend is None: - storage_backend = FilesystemBackend() - - with storage_backend.get(filepath) as file_object: - pem_key = file_object.read().decode("utf-8") - - # Optionally decrypt and convert PEM-encoded key to 'RSAKEY_SCHEMA' format - rsa_key = keys.import_rsakey_from_private_pem(pem_key, scheme, password) - - return rsa_key - - -def import_rsa_publickey_from_file( - filepath, scheme="rsassa-pss-sha256", storage_backend=None -): - """Imports PEM-encoded RSA public key from file storage. - - The expected key format is X.509 SubjectPublicKeyInfo. - - Arguments: - filepath: The path to read the file from. - scheme (optional): The signing scheme assigned to the returned key object. - See RSA_SCHEME_SCHEMA for available signing schemes. - storage_backend (optional): An object implementing StorageBackendInterface. - If not passed a default FilesystemBackend will be used. - - Raises: - UnsupportedLibraryError: pyca/cryptography is not available. - FormatError: Arguments are malformed. - StorageError: Key file cannot be read. - Error: Public key is malformed. - - Returns: - An RSA public key object conformant with 'RSAKEY_SCHEMA'. - - """ - formats.PATH_SCHEMA.check_match(filepath) - formats.RSA_SCHEME_SCHEMA.check_match(scheme) - - if storage_backend is None: - storage_backend = FilesystemBackend() - - with storage_backend.get(filepath) as file_object: - rsa_pubkey_pem = file_object.read().decode("utf-8") - - # Convert PEM-encoded key to 'RSAKEY_SCHEMA' format - try: - rsakey_dict = keys.import_rsakey_from_public_pem(rsa_pubkey_pem, scheme) - - except exceptions.FormatError as e: - raise exceptions.Error( - "Cannot import improperly formatted" " PEM file." + repr(str(e)) - ) - - return rsakey_dict - - -def _generate_and_write_ed25519_keypair( - filepath=None, password=None, prompt=False -): - """Generates ed25519 key pair and writes custom JSON-formatted keys to disk. - - If a password is passed or entered on the prompt, the private key is - encrypted using AES-256 in CTR mode, with the password strengthened in - PBKDF2-HMAC-SHA256. - - NOTE: The custom key format includes 'ed25519' as signing scheme. - - Arguments: - filepath (optional): The path to write the private key to. If not passed, - the key is written to CWD using the keyid as filename. The public key - is written to the same path as the private key using the suffix '.pub'. - password (optional): An encryption password. - prompt (optional): A boolean indicating if the user should be prompted - for an encryption password. If the user enters an empty password, the - key is not encrypted. - - Raises: - UnsupportedLibraryError: pyca/pynacl or pyca/cryptography is not available. - FormatError: Arguments are malformed. - ValueError: An empty string is passed as 'password', or both a 'password' - is passed and 'prompt' is true. - StorageError: Key files cannot be written. - - Side Effects: - Prompts user for a password if 'prompt' is True. - Writes key files to disk. - Overwrites files if they already exist. - - Returns: - The private key filepath. - - """ - ed25519_key = keys.generate_ed25519_key() - - # Use passed 'filepath' or keyid as file name - if not filepath: - filepath = os.path.join(os.getcwd(), ed25519_key["keyid"]) - - formats.PATH_SCHEMA.check_match(filepath) - - password = _get_key_file_encryption_password(password, prompt, filepath) - - # Create intermediate directories as required - util.ensure_parent_dir(filepath) - - # Use custom JSON format for ed25519 keys on-disk - keytype = ed25519_key["keytype"] - keyval = ed25519_key["keyval"] - scheme = ed25519_key["scheme"] - ed25519key_metadata_format = keys.format_keyval_to_metadata( - keytype, scheme, keyval, private=False - ) - - # Write public key to .pub - file_object = tempfile.TemporaryFile() - file_object.write(json.dumps(ed25519key_metadata_format).encode("utf-8")) - util.persist_temp_file(file_object, filepath + ".pub") - - # Encrypt private key if we have a password, store as JSON string otherwise - if password is not None: - ed25519_key = keys.encrypt_key(ed25519_key, password) - else: - ed25519_key = json.dumps(ed25519_key) - - # Write private key to - file_object = tempfile.TemporaryFile() - file_object.write(ed25519_key.encode("utf-8")) - util.persist_temp_file(file_object, filepath, restrict=True) - - return filepath - - -def generate_and_write_ed25519_keypair(password, filepath=None): - """Generates ed25519 key pair and writes custom JSON-formatted keys to disk. - - The private key is encrypted using AES-256 in CTR mode, with the passed - password strengthened in PBKDF2-HMAC-SHA256. - - NOTE: The custom key format includes 'ed25519' as signing scheme. - - Arguments: - password: An encryption password. - filepath (optional): The path to write the private key to. If not passed, - the key is written to CWD using the keyid as filename. The public key - is written to the same path as the private key using the suffix '.pub'. - - Raises: - UnsupportedLibraryError: pyca/pynacl or pyca/cryptography is not available. - FormatError: Arguments are malformed. - ValueError: An empty string is passed as 'password'. - StorageError: Key files cannot be written. - - Side Effects: - Writes key files to disk. - Overwrites files if they already exist. - - Returns: - The private key filepath. - - """ - formats.PASSWORD_SCHEMA.check_match(password) - return _generate_and_write_ed25519_keypair( - filepath=filepath, password=password, prompt=False - ) - - -def generate_and_write_ed25519_keypair_with_prompt(filepath=None): - """Generates ed25519 key pair and writes custom JSON-formatted keys to disk. - - The private key is encrypted using AES-256 in CTR mode, with the password - entered on the prompt strengthened in PBKDF2-HMAC-SHA256. - - NOTE: The custom key format includes 'ed25519' as signing scheme. - - Arguments: - filepath (optional): The path to write the private key to. If not passed, - the key is written to CWD using the keyid as filename. The public key - is written to the same path as the private key using the suffix '.pub'. - - Raises: - UnsupportedLibraryError: pyca/pynacl or pyca/cryptography is not available. - FormatError: Arguments are malformed. - StorageError: Key files cannot be written. - - Side Effects: - Prompts user for a password. - Writes key files to disk. - Overwrites files if they already exist. - - Returns: - The private key filepath. - - """ - return _generate_and_write_ed25519_keypair( - filepath=filepath, password=None, prompt=True - ) - - -def generate_and_write_unencrypted_ed25519_keypair(filepath=None): - """Generates ed25519 key pair and writes custom JSON-formatted keys to disk. - - NOTE: The custom key format includes 'ed25519' as signing scheme. - - Arguments: - filepath (optional): The path to write the private key to. If not passed, - the key is written to CWD using the keyid as filename. The public key - is written to the same path as the private key using the suffix '.pub'. - - Raises: - UnsupportedLibraryError: pyca/pynacl or pyca/cryptography is not available. - FormatError: Arguments are malformed. - StorageError: Key files cannot be written. - - Side Effects: - Writes unencrypted key files to disk. - Overwrites files if they already exist. - - Returns: - The private key filepath. - - """ - return _generate_and_write_ed25519_keypair( - filepath=filepath, password=None, prompt=False - ) - - -def import_ed25519_publickey_from_file(filepath): - """Imports custom JSON-formatted ed25519 public key from disk. - - NOTE: The signing scheme is set at key generation (see generate function). - - Arguments: - filepath: The path to read the file from. - - Raises: - FormatError: Argument is malformed. - StorageError: Key file cannot be read. - Error: Public key is malformed. - - Returns: - An ed25519 public key object conformant with 'ED25519KEY_SCHEMA'. - - """ - formats.PATH_SCHEMA.check_match(filepath) - - # Load custom on-disk JSON formatted key and convert to its custom in-memory - # dict key representation - ed25519_key_metadata = util.load_json_file(filepath) - ed25519_key, _ = keys.format_metadata_to_key(ed25519_key_metadata) - - # Check that the generic loading functions indeed loaded an ed25519 key - if ed25519_key["keytype"] != "ed25519": - message = "Invalid key type loaded: " + repr(ed25519_key["keytype"]) - raise exceptions.FormatError(message) - - return ed25519_key - - -def import_ed25519_privatekey_from_file( - filepath, password=None, prompt=False, storage_backend=None -): - """Imports custom JSON-formatted ed25519 private key from file storage. - - If a password is passed or entered on the prompt, the private key is - decrypted, otherwise it is treated as unencrypted. - - NOTE: The signing scheme is set at key generation (see generate function). - - Arguments: - filepath: The path to read the file from. - password (optional): A password to decrypt the key. - prompt (optional): A boolean indicating if the user should be prompted - for a decryption password. If the user enters an empty password, the - key is not decrypted. - storage_backend (optional): An object implementing StorageBackendInterface. - If not passed a default FilesystemBackend will be used. - - - Raises: - UnsupportedLibraryError: pyca/cryptography is not available. - FormatError: Arguments are malformed. - ValueError: Both a 'password' is passed and 'prompt' is true. - StorageError: Key file cannot be read. - Error, CryptoError: Key cannot be parsed. - - - Returns: - An ed25519 private key object conformant with 'ED25519KEY_SCHEMA'. - - """ - formats.PATH_SCHEMA.check_match(filepath) - password = _get_key_file_decryption_password(password, prompt, filepath) - - if storage_backend is None: - storage_backend = FilesystemBackend() - - with storage_backend.get(filepath) as file_object: - json_str = file_object.read() - - # Load custom on-disk JSON formatted key and convert to its custom - # in-memory dict key representation, decrypting it if password is not None - return keys.import_ed25519key_from_private_json( - json_str, password=password - ) - - -def _generate_and_write_ecdsa_keypair( - filepath=None, password=None, prompt=False -): - """Generates ecdsa key pair and writes custom JSON-formatted keys to disk. - - If a password is passed or entered on the prompt, the private key is - encrypted using AES-256 in CTR mode, with the password strengthened in - PBKDF2-HMAC-SHA256. - - NOTE: The custom key format includes 'ecdsa-sha2-nistp256' as signing scheme. - - Arguments: - filepath (optional): The path to write the private key to. If not passed, - the key is written to CWD using the keyid as filename. The public key - is written to the same path as the private key using the suffix '.pub'. - password (optional): An encryption password. - prompt (optional): A boolean indicating if the user should be prompted - for an encryption password. If the user enters an empty password, the - key is not encrypted. - - Raises: - UnsupportedLibraryError: pyca/cryptography is not available. - FormatError: Arguments are malformed. - ValueError: An empty string is passed as 'password', or both a 'password' - is passed and 'prompt' is true. - StorageError: Key files cannot be written. - - Side Effects: - Prompts user for a password if 'prompt' is True. - Writes key files to disk. - Overwrites files if they already exist. - - Returns: - The private key filepath. - - """ - ecdsa_key = keys.generate_ecdsa_key() - - # Use passed 'filepath' or keyid as file name - if not filepath: - filepath = os.path.join(os.getcwd(), ecdsa_key["keyid"]) - - formats.PATH_SCHEMA.check_match(filepath) - - password = _get_key_file_encryption_password(password, prompt, filepath) - - # Create intermediate directories as required - util.ensure_parent_dir(filepath) - - # Use custom JSON format for ecdsa keys on-disk - keytype = ecdsa_key["keytype"] - keyval = ecdsa_key["keyval"] - scheme = ecdsa_key["scheme"] - ecdsakey_metadata_format = keys.format_keyval_to_metadata( - keytype, scheme, keyval, private=False - ) - - # Write public key to .pub - file_object = tempfile.TemporaryFile() - file_object.write(json.dumps(ecdsakey_metadata_format).encode("utf-8")) - util.persist_temp_file(file_object, filepath + ".pub") - - # Encrypt private key if we have a password, store as JSON string otherwise - if password is not None: - ecdsa_key = keys.encrypt_key(ecdsa_key, password) - else: - ecdsa_key = json.dumps(ecdsa_key) - - # Write private key to - file_object = tempfile.TemporaryFile() - file_object.write(ecdsa_key.encode("utf-8")) - util.persist_temp_file(file_object, filepath, restrict=True) - - return filepath - - -def generate_and_write_ecdsa_keypair(password, filepath=None): - """Generates ecdsa key pair and writes custom JSON-formatted keys to disk. - - The private key is encrypted using AES-256 in CTR mode, with the passed - password strengthened in PBKDF2-HMAC-SHA256. - - NOTE: The custom key format includes 'ecdsa-sha2-nistp256' as signing scheme. - - Arguments: - password: An encryption password. - filepath (optional): The path to write the private key to. If not passed, - the key is written to CWD using the keyid as filename. The public key - is written to the same path as the private key using the suffix '.pub'. - - Raises: - UnsupportedLibraryError: pyca/cryptography is not available. - FormatError: Arguments are malformed. - ValueError: An empty string is passed as 'password'. - StorageError: Key files cannot be written. - - Side Effects: - Writes key files to disk. - Overwrites files if they already exist. - - Returns: - The private key filepath. - - """ - formats.PASSWORD_SCHEMA.check_match(password) - return _generate_and_write_ecdsa_keypair( - filepath=filepath, password=password, prompt=False - ) - - -def generate_and_write_ecdsa_keypair_with_prompt(filepath=None): - """Generates ecdsa key pair and writes custom JSON-formatted keys to disk. - - The private key is encrypted using AES-256 in CTR mode, with the password - entered on the prompt strengthened in PBKDF2-HMAC-SHA256. - - NOTE: The custom key format includes 'ecdsa-sha2-nistp256' as signing scheme. - - Arguments: - filepath (optional): The path to write the private key to. If not passed, - the key is written to CWD using the keyid as filename. The public key - is written to the same path as the private key using the suffix '.pub'. - - Raises: - UnsupportedLibraryError: pyca/cryptography is not available. - FormatError: Arguments are malformed. - StorageError: Key files cannot be written. - - Side Effects: - Prompts user for a password. - Writes key files to disk. - Overwrites files if they already exist. - - Returns: - The private key filepath. - - """ - return _generate_and_write_ecdsa_keypair( - filepath=filepath, password=None, prompt=True - ) - - -def generate_and_write_unencrypted_ecdsa_keypair(filepath=None): - """Generates ecdsa key pair and writes custom JSON-formatted keys to disk. - - NOTE: The custom key format includes 'ecdsa-sha2-nistp256' as signing scheme. - - Arguments: - filepath (optional): The path to write the private key to. If not passed, - the key is written to CWD using the keyid as filename. The public key - is written to the same path as the private key using the suffix '.pub'. - - Raises: - UnsupportedLibraryError: pyca/cryptography is not available. - FormatError: Arguments are malformed. - StorageError: Key files cannot be written. - - Side Effects: - Writes unencrypted key files to disk. - Overwrites files if they already exist. - - Returns: - The private key filepath. - - """ - return _generate_and_write_ecdsa_keypair( - filepath=filepath, password=None, prompt=False - ) - - -def import_ecdsa_publickey_from_file(filepath): - """Imports custom JSON-formatted ecdsa public key from disk. - - NOTE: The signing scheme is set at key generation (see generate function). - - Arguments: - filepath: The path to read the file from. - - Raises: - FormatError: Argument is malformed. - StorageError: Key file cannot be read. - Error: Public key is malformed. - - Returns: - An ecdsa public key object conformant with 'ECDSAKEY_SCHEMA'. - - """ - formats.PATH_SCHEMA.check_match(filepath) - - # Load custom on-disk JSON formatted key and convert to its custom in-memory - # dict key representation - ecdsa_key_metadata = util.load_json_file(filepath) - ecdsa_key, _ = keys.format_metadata_to_key(ecdsa_key_metadata) - - return ecdsa_key - - -def import_ecdsa_privatekey_from_file( - filepath, password=None, prompt=False, storage_backend=None -): - """Imports custom JSON-formatted ecdsa private key from file storage. - - If a password is passed or entered on the prompt, the private key is - decrypted, otherwise it is treated as unencrypted. - - NOTE: The signing scheme is set at key generation (see generate function). - - Arguments: - filepath: The path to read the file from. - password (optional): A password to decrypt the key. - prompt (optional): A boolean indicating if the user should be prompted - for a decryption password. If the user enters an empty password, the - key is not decrypted. - storage_backend (optional): An object implementing StorageBackendInterface. - If not passed a default FilesystemBackend will be used. - - Raises: - UnsupportedLibraryError: pyca/cryptography is not available. - FormatError: Arguments are malformed. - ValueError: Both a 'password' is passed and 'prompt' is true. - StorageError: Key file cannot be read. - Error, CryptoError: Key cannot be parsed. - - Returns: - An ecdsa private key object conformant with 'ED25519KEY_SCHEMA'. - - """ - formats.PATH_SCHEMA.check_match(filepath) - - password = _get_key_file_decryption_password(password, prompt, filepath) - - if storage_backend is None: - storage_backend = FilesystemBackend() - - with storage_backend.get(filepath) as file_object: - key_data = file_object.read().decode("utf-8") - - # Decrypt private key if we have a password, directly load JSON otherwise - if password is not None: - key_object = keys.decrypt_key(key_data, password) - else: - key_object = util.load_json_string(key_data) - - # Raise an exception if an unexpected key type is imported. - # NOTE: we support keytype's of ecdsa-sha2-nistp256 and ecdsa-sha2-nistp384 - # in order to support key files generated with older versions of - # securesystemslib. At some point this backwards compatibility should be - # removed. - if key_object["keytype"] not in [ - "ecdsa", - "ecdsa-sha2-nistp256", - "ecdsa-sha2-nistp384", - ]: - message = "Invalid key type loaded: " + repr(key_object["keytype"]) - raise exceptions.FormatError(message) - - # Add "keyid_hash_algorithms" so that equal ecdsa keys with different keyids - # can be associated using supported keyid_hash_algorithms. - key_object["keyid_hash_algorithms"] = settings.HASH_ALGORITHMS - - return key_object - - -def import_publickeys_from_file(filepaths, key_types=None): - """Imports multiple public keys from files. - - NOTE: The default signing scheme 'rsassa-pss-sha256' is assigned to RSA keys. - Use 'import_rsa_publickey_from_file' to specify any other than the default - signing scheme for an RSA key. ed25519 and ecdsa keys have the signing scheme - included in the custom key format (see generate functions). - - Arguments: - filepaths: A list of paths to public key files. - key_types (optional): A list of types of keys to be imported associated - with filepaths by index. Must be one of KEY_TYPE_RSA, KEY_TYPE_ED25519 - or KEY_TYPE_ECDSA. If not specified, all keys are assumed to be - KEY_TYPE_RSA. - - Raises: - TypeError: filepaths or 'key_types' (if passed) is not iterable. - FormatError: Argument are malformed, or 'key_types' is passed and does not - have the same length as 'filepaths' or contains an unsupported type. - UnsupportedLibraryError: pyca/cryptography is not available. - StorageError: Key file cannot be read. - Error: Public key is malformed. - - Returns: - A dict of public keys in KEYDICT_SCHEMA format. - - """ - if key_types is None: - key_types = [KEY_TYPE_RSA] * len(filepaths) - - if len(key_types) != len(filepaths): - raise exceptions.FormatError( - "Pass equal amount of 'filepaths' (got {}) and 'key_types (got {}), " # pylint: disable=consider-using-f-string - "or no 'key_types' at all to default to '{}'.".format( - len(filepaths), len(key_types), KEY_TYPE_RSA - ) - ) - - key_dict = {} - for idx, filepath in enumerate(filepaths): - if key_types[idx] == KEY_TYPE_ED25519: - key = import_ed25519_publickey_from_file(filepath) - - elif key_types[idx] == KEY_TYPE_RSA: - key = import_rsa_publickey_from_file(filepath) - - elif key_types[idx] == KEY_TYPE_ECDSA: - key = import_ecdsa_publickey_from_file(filepath) - - else: - raise exceptions.FormatError( - "Unsupported key type '{}'. Must be '{}', '{}' or '{}'.".format( # pylint: disable=consider-using-f-string - key_types[idx], - KEY_TYPE_RSA, - KEY_TYPE_ED25519, - KEY_TYPE_ECDSA, - ) - ) - - key_dict[key["keyid"]] = key - - return key_dict - - -def import_privatekey_from_file( - filepath, key_type=None, password=None, prompt=False -): - """Imports private key from file. - - If a password is passed or entered on the prompt, the private key is - decrypted, otherwise it is treated as unencrypted. - - NOTE: The default signing scheme 'rsassa-pss-sha256' is assigned to RSA keys. - Use 'import_rsa_privatekey_from_file' to specify any other than the default - signing scheme for an RSA key. ed25519 and ecdsa keys have the signing scheme - included in the custom key format (see generate functions). - - Arguments: - filepath: The path to read the file from. - key_type (optional): One of KEY_TYPE_RSA, KEY_TYPE_ED25519 or - KEY_TYPE_ECDSA. Default is KEY_TYPE_RSA. - password (optional): A password to decrypt the key. - prompt (optional): A boolean indicating if the user should be prompted - for a decryption password. If the user enters an empty password, the - key is not decrypted. - - Raises: - FormatError: Arguments are malformed or 'key_type' is not supported. - ValueError: Both a 'password' is passed and 'prompt' is true. - UnsupportedLibraryError: pyca/cryptography is not available. - StorageError: Key file cannot be read. - Error, CryptoError: Key cannot be parsed. - - Returns: - A private key object conformant with one of 'ED25519KEY_SCHEMA', - 'RSAKEY_SCHEMA' or 'ECDSAKEY_SCHEMA'. - - """ - if key_type is None: - key_type = KEY_TYPE_RSA - - if key_type == KEY_TYPE_ED25519: # pylint: disable=no-else-return - return import_ed25519_privatekey_from_file( - filepath, password=password, prompt=prompt - ) - - elif key_type == KEY_TYPE_RSA: - return import_rsa_privatekey_from_file( - filepath, password=password, prompt=prompt - ) - - elif key_type == KEY_TYPE_ECDSA: - return import_ecdsa_privatekey_from_file( - filepath, password=password, prompt=prompt - ) - - else: - raise exceptions.FormatError( - "Unsupported key type '{}'. Must be '{}', '{}' or '{}'.".format( # pylint: disable=consider-using-f-string - key_type, KEY_TYPE_RSA, KEY_TYPE_ED25519, KEY_TYPE_ECDSA - ) - ) - - -if __name__ == "__main__": - # The interactive sessions of the documentation strings can - # be tested by running interface.py as a standalone module: - # $ python interface.py. - import doctest - - doctest.testmod() diff --git a/securesystemslib/keys.py b/securesystemslib/keys.py deleted file mode 100755 index 1c6f9243..00000000 --- a/securesystemslib/keys.py +++ /dev/null @@ -1,1862 +0,0 @@ -""" - - keys.py - - - Vladimir Diaz - - - October 4, 2013. - - - See LICENSE for licensing information. - - - The goal of this module is to centralize cryptographic key routines and their - supported operations (e.g., creating and verifying signatures). This module - is designed to support multiple public-key algorithms, such as RSA, Ed25519, - and ECDSA, and multiple cryptography libraries. Which cryptography library - to use is determined by the default, or user modified, values set in - 'settings.py' - - https://en.wikipedia.org/wiki/RSA_(algorithm) - http://ed25519.cr.yp.to/ - https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm - - The (RSA, ECDSA and Ed25519)-related functions provided include - generate_rsa_key(), generate_ed25519_key(), generate_ecdsa_key(), - create_signature(), and verify_signature(). The cryptography libraries - called by 'securesystemslib.keys.py' generate the actual keys and the - functions listed above can be viewed as the easy-to-use public interface. - - Additional functions contained here include format_keyval_to_metadata() and - format_metadata_to_key(). These last two functions produce or use keys - compatible with the key structures listed in Metadata files. The key - generation functions return a dictionary containing all the information needed - of keys, such as public & private keys, and a keyID. create_signature() - and verify_signature() are supplemental functions needed for generating - signatures and verifying them. - - Key IDs are used as identifiers for keys (e.g., RSA key). They are the - hexadecimal representation of the hash of the key object (specifically, the - key object containing only the public key). Review the '_get_keyid()' - function of this module to see precisely how keyids are generated. One may - get the keyid of a key object by simply accessing the dictionary's 'keyid' - key (i.e., rsakey['keyid']). - """ - -# Required for hexadecimal conversions. Signatures and public/private keys are -# hexlified. -import binascii -import logging - -from securesystemslib import ( - ecdsa_keys, - ed25519_keys, - exceptions, - formats, - rsa_keys, - settings, - util, -) -from securesystemslib.hash import digest - -# The hash algorithm to use in the generation of keyids. -_KEY_ID_HASH_ALGORITHM = "sha256" - -# Recommended RSA key sizes: -# http://www.emc.com/emc-plus/rsa-labs/historical/twirl-and-rsa-key-size.htm#table1 -# According to the document above, revised May 6, 2003, RSA keys of -# size 3072 provide security through 2031 and beyond. -_DEFAULT_RSA_KEY_BITS = 3072 - - -RSA_SIGNATURE_SCHEMES = [ - "rsassa-pss-sha224", - "rsassa-pss-sha256", - "rsassa-pss-sha384", - "rsassa-pss-sha512", - "rsa-pkcs1v15-sha224", - "rsa-pkcs1v15-sha256", - "rsa-pkcs1v15-sha384", - "rsa-pkcs1v15-sha512", -] - -logger = logging.getLogger(__name__) - - -def generate_rsa_key(bits=_DEFAULT_RSA_KEY_BITS, scheme="rsassa-pss-sha256"): - """ - - Generate public and private RSA keys, with modulus length 'bits'. In - addition, a keyid identifier for the RSA key is generated. The object - returned conforms to 'securesystemslib.formats.RSAKEY_SCHEMA' and has the - form: - - {'keytype': 'rsa', - 'scheme': 'rsassa-pss-sha256', - 'keyid': keyid, - 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', - 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} - - The public and private keys are strings in PEM format. - - Although the PyCA cryptography library and/or its crypto backend might set - a minimum key size, generate() enforces a minimum key size of 2048 bits. - If 'bits' is unspecified, a 3072-bit RSA key is generated, which is the key - size recommended by securesystemslib. These key size restrictions are only - enforced for keys generated within securesystemslib. RSA keys with sizes - lower than what we recommended may still be imported (e.g., with - import_rsakey_from_pem(). - - >>> rsa_key = generate_rsa_key(bits=2048) - >>> securesystemslib.formats.RSAKEY_SCHEMA.matches(rsa_key) - True - - >>> public = rsa_key['keyval']['public'] - >>> private = rsa_key['keyval']['private'] - >>> securesystemslib.formats.PEMRSA_SCHEMA.matches(public) - True - >>> securesystemslib.formats.PEMRSA_SCHEMA.matches(private) - True - - - bits: - The key size, or key length, of the RSA key. 'bits' must be 2048, or - greater, and a multiple of 256. - - scheme: - The signature scheme used by the key. It must be one from the list - `securesystemslib.keys.RSA_SIGNATURE_SCHEMES`. - - - securesystemslib.exceptions.FormatError, if 'bits' is improperly or invalid - (i.e., not an integer and not at least 2048). - - ValueError, if an exception occurs after calling the RSA key generation - routine. The 'ValueError' exception is raised by the key generation - function of the cryptography library called. - - - None. - - - A dictionary containing the RSA keys and other identifying information. - Conforms to 'securesystemslib.formats.RSAKEY_SCHEMA'. - """ - - # Does 'bits' have the correct format? This check will ensure 'bits' - # conforms to 'securesystemslib.formats.RSAKEYBITS_SCHEMA'. 'bits' must be - # an integer object, with a minimum value of 2048. Raise - # 'securesystemslib.exceptions.FormatError' if the check fails. - formats.RSAKEYBITS_SCHEMA.check_match(bits) - formats.RSA_SCHEME_SCHEMA.check_match(scheme) - - # Begin building the RSA key dictionary. - rsakey_dict = {} - keytype = "rsa" - public = None - private = None - - # Generate the public and private RSA keys. The pyca/cryptography module is - # used to generate the actual key. Raise 'ValueError' if 'bits' is less than - # 1024, although a 2048-bit minimum is enforced by - # securesystemslib.formats.RSAKEYBITS_SCHEMA.check_match(). - public, private = rsa_keys.generate_rsa_public_and_private(bits) - - # When loading in PEM keys, extract_pem() is called, which strips any - # leading or trailing new line characters. Do the same here before generating - # the keyid. - public = extract_pem(public, private_pem=False) - private = extract_pem(private, private_pem=True) - - # Generate the keyid of the RSA key. Note: The private key material is not - # included in the generation of the 'keyid' identifier. Convert any '\r\n' - # (e.g., Windows) newline characters to '\n' so that a consistent keyid is - # generated. - key_value = {"public": public.replace("\r\n", "\n"), "private": ""} - keyid = _get_keyid(keytype, scheme, key_value) - - # Build the 'rsakey_dict' dictionary. Update 'key_value' with the RSA - # private key prior to adding 'key_value' to 'rsakey_dict'. - key_value["private"] = private - - rsakey_dict["keytype"] = keytype - rsakey_dict["scheme"] = scheme - rsakey_dict["keyid"] = keyid - rsakey_dict["keyid_hash_algorithms"] = settings.HASH_ALGORITHMS - rsakey_dict["keyval"] = key_value - - return rsakey_dict - - -def generate_ecdsa_key(scheme="ecdsa-sha2-nistp256"): - """ - - Generate public and private ECDSA keys, with NIST P-256 + SHA256 (for - hashing) being the default scheme. In addition, a keyid identifier for the - ECDSA key is generated. The object returned conforms to - 'securesystemslib.formats.ECDSAKEY_SCHEMA' and has the form: - - {'keytype': 'ecdsa', - 'scheme', 'ecdsa-sha2-nistp256', - 'keyid': keyid, - 'keyval': {'public': '', - 'private': ''}} - - The public and private keys are strings in TODO format. - - >>> ecdsa_key = generate_ecdsa_key(scheme='ecdsa-sha2-nistp256') - >>> securesystemslib.formats.ECDSAKEY_SCHEMA.matches(ecdsa_key) - True - - - scheme: - The ECDSA signature scheme. By default, ECDSA NIST P-256 is used, with - SHA256 for hashing. - - - securesystemslib.exceptions.FormatError, if 'scheme' is improperly - formatted or invalid (i.e., not one of the supported ECDSA signature - schemes). - - - None. - - - A dictionary containing the ECDSA keys and other identifying information. - Conforms to 'securesystemslib.formats.ECDSAKEY_SCHEMA'. - """ - - # Does 'scheme' have the correct format? - # This check will ensure 'scheme' is properly formatted and is a supported - # ECDSA signature scheme. Raise 'securesystemslib.exceptions.FormatError' if - # the check fails. - formats.ECDSA_SCHEME_SCHEMA.check_match(scheme) - - # Begin building the ECDSA key dictionary. - ecdsa_key = {} - keytype = "ecdsa" - public = None - private = None - - # Generate the public and private ECDSA keys with one of the supported - # libraries. - public, private = ecdsa_keys.generate_public_and_private(scheme) - - # Generate the keyid of the Ed25519 key. 'key_value' corresponds to the - # 'keyval' entry of the 'Ed25519KEY_SCHEMA' dictionary. The private key - # information is not included in the generation of the 'keyid' identifier. - # Convert any '\r\n' (e.g., Windows) newline characters to '\n' so that a - # consistent keyid is generated. - key_value = {"public": public.replace("\r\n", "\n"), "private": ""} - keyid = _get_keyid(keytype, scheme, key_value) - - # Build the 'ed25519_key' dictionary. Update 'key_value' with the Ed25519 - # private key prior to adding 'key_value' to 'ed25519_key'. - - key_value["private"] = private - - ecdsa_key["keytype"] = keytype - ecdsa_key["scheme"] = scheme - ecdsa_key["keyid"] = keyid - ecdsa_key["keyval"] = key_value - - # Add "keyid_hash_algorithms" so that equal ECDSA keys with different keyids - # can be associated using supported keyid_hash_algorithms. - ecdsa_key["keyid_hash_algorithms"] = settings.HASH_ALGORITHMS - - return ecdsa_key - - -def generate_ed25519_key(scheme="ed25519"): - """ - - Generate public and private ED25519 keys, both of length 32-bytes, although - they are hexlified to 64 bytes. In addition, a keyid identifier generated - for the returned ED25519 object. The object returned conforms to - 'securesystemslib.formats.ED25519KEY_SCHEMA' and has the form: - - {'keytype': 'ed25519', - 'scheme': 'ed25519', - 'keyid': 'f30a0870d026980100c0573bd557394f8c1bbd6...', - 'keyval': {'public': '9ccf3f02b17f82febf5dd3bab878b767d8408...', - 'private': 'ab310eae0e229a0eceee3947b6e0205dfab3...'}} - - >>> ed25519_key = generate_ed25519_key() - >>> securesystemslib.formats.ED25519KEY_SCHEMA.matches(ed25519_key) - True - >>> len(ed25519_key['keyval']['public']) - 64 - >>> len(ed25519_key['keyval']['private']) - 64 - - - scheme: - The signature scheme used by the generated Ed25519 key. - - - None. - - - The ED25519 keys are generated by calling either the optimized pure Python - implementation of ed25519, or the ed25519 routines provided by 'pynacl'. - - - A dictionary containing the ED25519 keys and other identifying information. - Conforms to 'securesystemslib.formats.ED25519KEY_SCHEMA'. - """ - - # Are the arguments properly formatted? If not, raise an - # 'securesystemslib.exceptions.FormatError' exceptions. - formats.ED25519_SIG_SCHEMA.check_match(scheme) - - # Begin building the Ed25519 key dictionary. - ed25519_key = {} - keytype = "ed25519" - public = None - private = None - - # Generate the public and private Ed25519 key with the 'pynacl' library. - # Unlike in the verification of Ed25519 signatures, do not fall back to the - # optimized, pure python implementation provided by PyCA. Ed25519 should - # always be generated with a backend like libsodium to prevent side-channel - # attacks. - public, private = ed25519_keys.generate_public_and_private() - - # Generate the keyid of the Ed25519 key. 'key_value' corresponds to the - # 'keyval' entry of the 'Ed25519KEY_SCHEMA' dictionary. The private key - # information is not included in the generation of the 'keyid' identifier. - key_value = {"public": binascii.hexlify(public).decode(), "private": ""} - keyid = _get_keyid(keytype, scheme, key_value) - - # Build the 'ed25519_key' dictionary. Update 'key_value' with the Ed25519 - # private key prior to adding 'key_value' to 'ed25519_key'. - key_value["private"] = binascii.hexlify(private).decode() - - ed25519_key["keytype"] = keytype - ed25519_key["scheme"] = scheme - ed25519_key["keyid"] = keyid - ed25519_key["keyid_hash_algorithms"] = settings.HASH_ALGORITHMS - ed25519_key["keyval"] = key_value - - return ed25519_key - - -def format_keyval_to_metadata(keytype, scheme, key_value, private=False): - """ - - Return a dictionary conformant to 'securesystemslib.formats.KEY_SCHEMA'. - If 'private' is True, include the private key. The dictionary - returned has the form: - - {'keytype': keytype, - 'scheme' : scheme, - 'keyval': {'public': '...', - 'private': '...'}} - - or if 'private' is False: - - {'keytype': keytype, - 'scheme': scheme, - 'keyval': {'public': '...', - 'private': ''}} - - >>> ed25519_key = generate_ed25519_key() - >>> key_val = ed25519_key['keyval'] - >>> keytype = ed25519_key['keytype'] - >>> scheme = ed25519_key['scheme'] - >>> ed25519_metadata = \ - format_keyval_to_metadata(keytype, scheme, key_val, private=True) - >>> securesystemslib.formats.KEY_SCHEMA.matches(ed25519_metadata) - True - - - key_type: - The 'rsa' or 'ed25519' strings. - - scheme: - The signature scheme used by the key. - - key_value: - A dictionary containing a private and public keys. - 'key_value' is of the form: - - {'public': '...', - 'private': '...'}}, - - conformant to 'securesystemslib.formats.KEYVAL_SCHEMA'. - - private: - Indicates if the private key should be included in the dictionary - returned. - - - securesystemslib.exceptions.FormatError, if 'key_value' does not conform to - 'securesystemslib.formats.KEYVAL_SCHEMA', or if the private key is not - present in 'key_value' if requested by the caller via 'private'. - - - None. - - - A 'securesystemslib.formats.KEY_SCHEMA' dictionary. - """ - - # Does 'keytype' have the correct format? - # This check will ensure 'keytype' has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'securesystemslib.exceptions.FormatError' if the check fails. - formats.KEYTYPE_SCHEMA.check_match(keytype) - - # Does 'scheme' have the correct format? - formats.SCHEME_SCHEMA.check_match(scheme) - - # Does 'key_value' have the correct format? - formats.KEYVAL_SCHEMA.check_match(key_value) - - if private is True: - # If the caller requests (via the 'private' argument) to include a private - # key in the returned dictionary, ensure the private key is actually - # present in 'key_val' (a private key is optional for 'KEYVAL_SCHEMA' - # dicts). - if "private" not in key_value: - raise exceptions.FormatError( - "The required private key is missing from: " + repr(key_value) - ) - - return {"keytype": keytype, "scheme": scheme, "keyval": key_value} - - public_key_value = {"public": key_value["public"]} - - return { - "keytype": keytype, - "scheme": scheme, - "keyid_hash_algorithms": settings.HASH_ALGORITHMS, - "keyval": public_key_value, - } - - -def format_metadata_to_key( - key_metadata, default_keyid=None, keyid_hash_algorithms=None -): - """ - - Construct a key dictionary (e.g., securesystemslib.formats.RSAKEY_SCHEMA) - according to the keytype of 'key_metadata'. The dict returned by this - function has the exact format as the dict returned by one of the key - generations functions, like generate_ed25519_key(). The dict returned - has the form: - - {'keytype': keytype, - 'scheme': scheme, - 'keyid': 'f30a0870d026980100c0573bd557394f8c1bbd6...', - 'keyval': {'public': '...', - 'private': '...'}} - - For example, RSA key dictionaries in RSAKEY_SCHEMA format should be used by - modules storing a collection of keys, such as with keydb.py. RSA keys as - stored in metadata files use a different format, so this function should be - called if an RSA key is extracted from one of these metadata files and need - converting. The key generation functions create an entirely new key and - return it in the format appropriate for 'keydb.py'. - - >>> ed25519_key = generate_ed25519_key() - >>> key_val = ed25519_key['keyval'] - >>> keytype = ed25519_key['keytype'] - >>> scheme = ed25519_key['scheme'] - >>> ed25519_metadata = \ - format_keyval_to_metadata(keytype, scheme, key_val, private=True) - >>> ed25519_key_2, junk = format_metadata_to_key(ed25519_metadata) - >>> securesystemslib.formats.ED25519KEY_SCHEMA.matches(ed25519_key_2) - True - >>> ed25519_key == ed25519_key_2 - True - - - key_metadata: - The key dictionary as stored in Metadata files, conforming to - 'securesystemslib.formats.KEY_SCHEMA'. It has the form: - - {'keytype': '...', - 'scheme': scheme, - 'keyval': {'public': '...', - 'private': '...'}} - default_keyid: - A default keyid associated with the key metadata. If this is not - provided, the keyid will be calculated by _get_keyid using the default - hash algorithm. If provided, the default keyid can be any string. - - keyid_hash_algorithms: - An optional list of hash algorithms to use when generating keyids. - Defaults to securesystemslib.settings.HASH_ALGORITHMS. - - - securesystemslib.exceptions.FormatError, if 'key_metadata' does not conform - to 'securesystemslib.formats.KEY_SCHEMA'. - - - None. - - - In the case of an RSA key, a dictionary conformant to - 'securesystemslib.formats.RSAKEY_SCHEMA'. - """ - - # Does 'key_metadata' have the correct format? - # This check will ensure 'key_metadata' has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'securesystemslib.exceptions.FormatError' if the check fails. - formats.KEY_SCHEMA.check_match(key_metadata) - - # Construct the dictionary to be returned. - key_dict = {} - keytype = key_metadata["keytype"] - scheme = key_metadata["scheme"] - key_value = key_metadata["keyval"] - - # Convert 'key_value' to 'securesystemslib.formats.KEY_SCHEMA' and generate - # its hash The hash is in hexdigest form. - if default_keyid is None: - default_keyid = _get_keyid(keytype, scheme, key_value) - keyids = set() - keyids.add(default_keyid) - - if keyid_hash_algorithms is None: - keyid_hash_algorithms = settings.HASH_ALGORITHMS - - for hash_algorithm in keyid_hash_algorithms: - keyid = _get_keyid(keytype, scheme, key_value, hash_algorithm) - keyids.add(keyid) - - # All the required key values gathered. Build 'key_dict'. - # 'keyid_hash_algorithms' - key_dict["keytype"] = keytype - key_dict["scheme"] = scheme - key_dict["keyid"] = default_keyid - key_dict["keyid_hash_algorithms"] = keyid_hash_algorithms - key_dict["keyval"] = key_value - - return key_dict, keyids - - -def _get_keyid(keytype, scheme, key_value, hash_algorithm="sha256"): - """Return the keyid of 'key_value'.""" - - # 'keyid' will be generated from an object conformant to KEY_SCHEMA, - # which is the format Metadata files (e.g., root.json) store keys. - # 'format_keyval_to_metadata()' returns the object needed by _get_keyid(). - key_meta = format_keyval_to_metadata( - keytype, scheme, key_value, private=False - ) - - # Convert the key to JSON Canonical format, suitable for adding - # to digest objects. - key_update_data = formats.encode_canonical(key_meta) - - # Create a digest object and call update(), using the JSON - # canonical format of 'rskey_meta' as the update data. - digest_object = digest(hash_algorithm) - digest_object.update(key_update_data.encode("utf-8")) - - # 'keyid' becomes the hexadecimal representation of the hash. - keyid = digest_object.hexdigest() - - return keyid - - -def create_signature(key_dict, data): - """ - - Return a signature dictionary of the form: - {'keyid': 'f30a0870d026980100c0573bd557394f8c1bbd6...', - 'sig': '...'}. - - The signing process will use the private key in - key_dict['keyval']['private'] and 'data' to generate the signature. - - The following signature schemes are supported: - - 'RSASSA-PSS' - RFC3447 - RSASSA-PSS - http://www.ietf.org/rfc/rfc3447. - - 'ed25519' - ed25519 - high-speed high security signatures - http://ed25519.cr.yp.to/ - - Which signature to generate is determined by the key type of 'key_dict' - and the available cryptography library specified in 'settings'. - - >>> ed25519_key = generate_ed25519_key() - >>> data = 'The quick brown fox jumps over the lazy dog' - >>> signature = create_signature(ed25519_key, data) - >>> securesystemslib.formats.SIGNATURE_SCHEMA.matches(signature) - True - >>> len(signature['sig']) - 128 - >>> rsa_key = generate_rsa_key(2048) - >>> signature = create_signature(rsa_key, data) - >>> securesystemslib.formats.SIGNATURE_SCHEMA.matches(signature) - True - >>> ecdsa_key = generate_ecdsa_key() - >>> signature = create_signature(ecdsa_key, data) - >>> securesystemslib.formats.SIGNATURE_SCHEMA.matches(signature) - True - - - key_dict: - A dictionary containing the keys. An example RSA key dict has the - form: - - {'keytype': 'rsa', - 'scheme': 'rsassa-pss-sha256', - 'keyid': 'f30a0870d026980100c0573bd557394f8c1bbd6...', - 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', - 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} - - The public and private keys are strings in PEM format. - - data: - Data to be signed. This should be a bytes object; data should be - encoded/serialized before it is passed here. The same value can be be - passed into securesystemslib.verify_signature() (along with the public - key) to later verify the signature. - - - securesystemslib.exceptions.FormatError, if 'key_dict' is improperly - formatted. - - securesystemslib.exceptions.UnsupportedAlgorithmError, if 'key_dict' - specifies an unsupported key type or signing scheme. - - securesystemslib.exceptions.CryptoError, if the signature cannot be - generated. - - TypeError, if 'key_dict' contains an invalid keytype. - - - The cryptography library specified in 'settings' is called to perform the - actual signing routine. - - - A signature dictionary conformant to - 'securesystemslib_format.SIGNATURE_SCHEMA'. - """ - - # Does 'key_dict' have the correct format? - # This check will ensure 'key_dict' has the appropriate number of objects - # and object types, and that all dict keys are properly named. - # Raise 'securesystemslib.exceptions.FormatError' if the check fails. - # The key type of 'key_dict' must be either 'rsa' or 'ed25519'. - formats.ANYKEY_SCHEMA.check_match(key_dict) - - # Signing the 'data' object requires a private key. Signing schemes that are - # currently supported are: 'ed25519', 'ecdsa-sha2-nistp256', - # 'ecdsa-sha2-nistp384' and rsa schemes defined in - # `securesystemslib.keys.RSA_SIGNATURE_SCHEMES`. - # RSASSA-PSS and RSA-PKCS1v15 keys and signatures can be generated and - # verified by rsa_keys.py, and Ed25519 keys by PyNaCl and PyCA's - # optimized, pure python implementation of Ed25519. - signature = {} - keytype = key_dict["keytype"] - scheme = key_dict["scheme"] - public = key_dict["keyval"]["public"] - private = key_dict["keyval"]["private"] - keyid = key_dict["keyid"] - sig = None - - if keytype == "rsa": - if scheme in RSA_SIGNATURE_SCHEMES: - private = private.replace("\r\n", "\n") - sig, scheme = rsa_keys.create_rsa_signature(private, data, scheme) - - else: - raise exceptions.UnsupportedAlgorithmError( - "Unsupported" " RSA signature scheme specified: " + repr(scheme) - ) - - elif keytype == "ed25519": - public = binascii.unhexlify(public.encode("utf-8")) - private = binascii.unhexlify(private.encode("utf-8")) - sig, scheme = ed25519_keys.create_signature( - public, private, data, scheme - ) - - # Continue to support keytypes of ecdsa-sha2-nistp256 and ecdsa-sha2-nistp384 - # for backwards compatibility with older securesystemslib releases - elif keytype in ["ecdsa", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]: - sig, scheme = ecdsa_keys.create_signature(public, private, data, scheme) - - # 'securesystemslib.formats.ANYKEY_SCHEMA' should have detected invalid key - # types. This is a defensive check against an invalid key type. - else: # pragma: no cover - raise TypeError("Invalid key type.") - - # Build the signature dictionary to be returned. - # The hexadecimal representation of 'sig' is stored in the signature. - signature["keyid"] = keyid - signature["sig"] = binascii.hexlify(sig).decode() - - return signature - - -def verify_signature( - key_dict, signature, data -): # pylint: disable=too-many-branches - """ - - Determine whether the private key belonging to 'key_dict' produced - 'signature'. verify_signature() will use the public key found in - 'key_dict', the 'sig' objects contained in 'signature', and 'data' to - complete the verification. - - >>> ed25519_key = generate_ed25519_key() - >>> data = 'The quick brown fox jumps over the lazy dog' - >>> signature = create_signature(ed25519_key, data) - >>> verify_signature(ed25519_key, signature, data) - True - >>> verify_signature(ed25519_key, signature, 'bad_data') - False - >>> rsa_key = generate_rsa_key() - >>> signature = create_signature(rsa_key, data) - >>> verify_signature(rsa_key, signature, data) - True - >>> verify_signature(rsa_key, signature, 'bad_data') - False - >>> ecdsa_key = generate_ecdsa_key() - >>> signature = create_signature(ecdsa_key, data) - >>> verify_signature(ecdsa_key, signature, data) - True - >>> verify_signature(ecdsa_key, signature, 'bad_data') - False - - - key_dict: - A dictionary containing the keys and other identifying information. - If 'key_dict' is an RSA key, it has the form: - - {'keytype': 'rsa', - 'scheme': 'rsassa-pss-sha256', - 'keyid': 'f30a0870d026980100c0573bd557394f8c1bbd6...', - 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', - 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} - - The public and private keys are strings in PEM format. - - signature: - The signature dictionary produced by one of the key generation functions. - 'signature' has the form: - - {'keyid': 'f30a0870d026980100c0573bd557394f8c1bbd6...', - 'sig': sig}. - - Conformant to 'securesystemslib.formats.SIGNATURE_SCHEMA'. - - data: - Data that the signature is expected to be over. This should be a bytes - object; data should be encoded/serialized before it is passed here.) - This is the same value that can be passed into - securesystemslib.create_signature() in order to create the signature. - - - securesystemslib.exceptions.FormatError, raised if either 'key_dict' or - 'signature' are improperly formatted. - - securesystemslib.exceptions.UnsupportedAlgorithmError, if 'key_dict' or - 'signature' specifies an unsupported algorithm. - - securesystemslib.exceptions.CryptoError, if the KEYID in the given - 'key_dict' does not match the KEYID in 'signature'. - - - The cryptography library specified in 'settings' called to do the actual - verification. - - - Boolean. True if the signature is valid, False otherwise. - """ - - # Does 'key_dict' have the correct format? - # This check will ensure 'key_dict' has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'securesystemslib.exceptions.FormatError' if the check fails. - formats.ANYKEY_SCHEMA.check_match(key_dict) - - # Does 'signature' have the correct format? - formats.SIGNATURE_SCHEMA.check_match(signature) - - # Verify that the KEYID in 'key_dict' matches the KEYID listed in the - # 'signature'. - if key_dict["keyid"] != signature["keyid"]: - raise exceptions.CryptoError( - "The KEYID (" - " " + repr(key_dict["keyid"]) + " ) in the given key does not match" - " the KEYID ( " + repr(signature["keyid"]) + " ) in the signature." - ) - - logger.debug("The KEYIDs of key_dict and the signature match.") - - # Using the public key belonging to 'key_dict' - # (i.e., rsakey_dict['keyval']['public']), verify whether 'signature' - # was produced by key_dict's corresponding private key - # key_dict['keyval']['private']. - sig = signature["sig"] - sig = binascii.unhexlify(sig.encode("utf-8")) - public = key_dict["keyval"]["public"] - keytype = key_dict["keytype"] - scheme = key_dict["scheme"] - valid_signature = False - - if keytype == "rsa": - if scheme in RSA_SIGNATURE_SCHEMES: - valid_signature = rsa_keys.verify_rsa_signature( - sig, scheme, public, data - ) - - else: - raise exceptions.UnsupportedAlgorithmError( - "Unsupported" " signature scheme is specified: " + repr(scheme) - ) - - elif keytype == "ed25519": - if scheme == "ed25519": - try: - public = binascii.unhexlify(public.encode("utf-8")) - except binascii.Error as e: - raise exceptions.FormatError( - f"Failed to parse key {public} as hex" - ) from e - valid_signature = ed25519_keys.verify_signature( - public, scheme, sig, data - ) - - else: - raise exceptions.UnsupportedAlgorithmError( - "Unsupported" " signature scheme is specified: " + repr(scheme) - ) - - elif keytype in ["ecdsa", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]: - if scheme in ["ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]: - valid_signature = ecdsa_keys.verify_signature( - public, scheme, sig, data - ) - - else: - raise exceptions.UnsupportedAlgorithmError( - "Unsupported" " signature scheme is specified: " + repr(scheme) - ) - - # 'securesystemslib.formats.ANYKEY_SCHEMA' should have detected invalid key - # types. This is a defensive check against an invalid key type. - else: # pragma: no cover - raise TypeError("Unsupported key type.") - - return valid_signature - - -def import_rsakey_from_private_pem( - pem, scheme="rsassa-pss-sha256", password=None -): - """ - - Import the private RSA key stored in 'pem', and generate its public key - (which will also be included in the returned rsakey object). In addition, - a keyid identifier for the RSA key is generated. The object returned - conforms to 'securesystemslib.formats.RSAKEY_SCHEMA' and has the form: - - {'keytype': 'rsa', - 'scheme': 'rsassa-pss-sha256', - 'keyid': keyid, - 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', - 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} - - The private key is a string in PEM format. - - >>> rsa_key = generate_rsa_key() - >>> scheme = rsa_key['scheme'] - >>> private = rsa_key['keyval']['private'] - >>> passphrase = 'secret' - >>> encrypted_pem = create_rsa_encrypted_pem(private, passphrase) - >>> rsa_key2 = import_rsakey_from_private_pem(encrypted_pem, scheme, passphrase) - >>> securesystemslib.formats.RSAKEY_SCHEMA.matches(rsa_key) - True - >>> securesystemslib.formats.RSAKEY_SCHEMA.matches(rsa_key2) - True - - - pem: - A string in PEM format. The private key is extracted and returned in - an rsakey object. - - scheme: - The signature scheme used by the imported key. - - password: (optional) - The password, or passphrase, to decrypt the private part of the RSA key - if it is encrypted. 'password' is not used directly as the encryption - key, a stronger encryption key is derived from it. - - - securesystemslib.exceptions.FormatError, if the arguments are improperly - formatted. - - securesystemslib.exceptions.UnsupportedAlgorithmError, if 'pem' specifies - an unsupported key type. - - - None. - - - A dictionary containing the RSA keys and other identifying information. - Conforms to 'securesystemslib.formats.RSAKEY_SCHEMA'. - """ - - # Does 'pem' have the correct format? - # This check will ensure 'pem' conforms to - # 'securesystemslib.formats.PEMRSA_SCHEMA'. - formats.PEMRSA_SCHEMA.check_match(pem) - - # Is 'scheme' properly formatted? - formats.RSA_SCHEME_SCHEMA.check_match(scheme) - - if password is not None: - formats.PASSWORD_SCHEMA.check_match(password) - - else: - logger.debug( - "The password/passphrase is unset. The PEM is expected" - " to be unencrypted." - ) - - # Begin building the RSA key dictionary. - rsakey_dict = {} - keytype = "rsa" - public = None - private = None - - # Generate the public and private RSA keys. The pyca/cryptography library - # performs the actual crypto operations. - public, private = rsa_keys.create_rsa_public_and_private_from_pem( - pem, password - ) - - public = extract_pem(public, private_pem=False) - private = extract_pem(private, private_pem=True) - - # Generate the keyid of the RSA key. 'key_value' corresponds to the - # 'keyval' entry of the 'RSAKEY_SCHEMA' dictionary. The private key - # information is not included in the generation of the 'keyid' identifier. - # Convert any '\r\n' (e.g., Windows) newline characters to '\n' so that a - # consistent keyid is generated. - key_value = {"public": public.replace("\r\n", "\n"), "private": ""} - keyid = _get_keyid(keytype, scheme, key_value) - - # Build the 'rsakey_dict' dictionary. Update 'key_value' with the RSA - # private key prior to adding 'key_value' to 'rsakey_dict'. - key_value["private"] = private - - rsakey_dict["keytype"] = keytype - rsakey_dict["scheme"] = scheme - rsakey_dict["keyid"] = keyid - rsakey_dict["keyval"] = key_value - - return rsakey_dict - - -def import_rsakey_from_public_pem(pem, scheme="rsassa-pss-sha256"): - """ - - Generate an RSA key object from 'pem'. In addition, a keyid identifier for - the RSA key is generated. The object returned conforms to - 'securesystemslib.formats.RSAKEY_SCHEMA' and has the form: - - {'keytype': 'rsa', - 'keyid': keyid, - 'keyval': {'public': '-----BEGIN PUBLIC KEY----- ...', - 'private': ''}} - - The public portion of the RSA key is a string in PEM format. - - >>> rsa_key = generate_rsa_key() - >>> public = rsa_key['keyval']['public'] - >>> rsa_key['keyval']['private'] = '' - >>> rsa_key2 = import_rsakey_from_public_pem(public) - >>> securesystemslib.formats.RSAKEY_SCHEMA.matches(rsa_key) - True - >>> securesystemslib.formats.RSAKEY_SCHEMA.matches(rsa_key2) - True - - - pem: - A string in PEM format (it should contain a public RSA key). - - - securesystemslib.exceptions.FormatError, if 'pem' is improperly formatted. - - - Only the public portion of the PEM is extracted. Leading or trailing - whitespace is not included in the PEM string stored in the rsakey object - returned. - - - A dictionary containing the RSA keys and other identifying information. - Conforms to 'securesystemslib.formats.RSAKEY_SCHEMA'. - """ - - # Does 'pem' have the correct format? - # This check will ensure arguments has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'securesystemslib.exceptions.FormatError' if the check fails. - formats.PEMRSA_SCHEMA.check_match(pem) - - # Does 'scheme' have the correct format? - formats.RSA_SCHEME_SCHEMA.check_match(scheme) - - # Ensure the PEM string has a public header and footer. Although a simple - # validation of 'pem' is performed here, a fully valid PEM string is needed - # later to successfully verify signatures. Performing stricter validation of - # PEMs are left to the external libraries that use 'pem'. - - if is_pem_public(pem): - public_pem = extract_pem(pem, private_pem=False) - - else: - raise exceptions.FormatError("Invalid public pem: " + repr(pem)) - - # Begin building the RSA key dictionary. - rsakey_dict = {} - keytype = "rsa" - - # Generate the keyid of the RSA key. 'key_value' corresponds to the - # 'keyval' entry of the 'RSAKEY_SCHEMA' dictionary. The private key - # information is not included in the generation of the 'keyid' identifier. - # Convert any '\r\n' (e.g., Windows) newline characters to '\n' so that a - # consistent keyid is generated. - key_value = {"public": public_pem.replace("\r\n", "\n"), "private": ""} - keyid = _get_keyid(keytype, scheme, key_value) - - rsakey_dict["keytype"] = keytype - rsakey_dict["scheme"] = scheme - rsakey_dict["keyid"] = keyid - rsakey_dict["keyval"] = key_value - - # Add "keyid_hash_algorithms" so that equal RSA keys with different keyids - # can be associated using supported keyid_hash_algorithms. - rsakey_dict["keyid_hash_algorithms"] = settings.HASH_ALGORITHMS - - return rsakey_dict - - -def import_rsakey_from_pem(pem, scheme="rsassa-pss-sha256"): - """ - - Import either a public or private PEM. In contrast to the other explicit - import functions (import_rsakey_from_public_pem and - import_rsakey_from_private_pem), this function is useful for when it is not - known whether 'pem' is private or public. - - - pem: - A string in PEM format. - - scheme: - The signature scheme used by the imported key. - - - securesystemslib.exceptions.FormatError, if 'pem' is improperly formatted. - - - None. - - - A dictionary containing the RSA keys and other identifying information. - Conforms to 'securesystemslib.formats.RSAKEY_SCHEMA'. - """ - - # Does 'pem' have the correct format? - # This check will ensure arguments has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'securesystemslib.exceptions.FormatError' if the check fails. - formats.PEMRSA_SCHEMA.check_match(pem) - - # Is 'scheme' properly formatted? - formats.RSA_SCHEME_SCHEMA.check_match(scheme) - - public_pem = "" - - # Ensure the PEM string has a public or private header and footer. Although - # a simple validation of 'pem' is performed here, a fully valid PEM string is - # needed later to successfully verify signatures. Performing stricter - # validation of PEMs are left to the external libraries that use 'pem'. - if is_pem_public(pem): - public_pem = extract_pem(pem, private_pem=False) - - elif is_pem_private(pem): - # Return an rsakey object (RSAKEY_SCHEMA) with the private key included. - return import_rsakey_from_private_pem(pem, scheme, password=None) - - else: - raise exceptions.FormatError( - "PEM contains neither a" " public nor private key: " + repr(pem) - ) - - # Begin building the RSA key dictionary. - rsakey_dict = {} - keytype = "rsa" - - # Generate the keyid of the RSA key. 'key_value' corresponds to the 'keyval' - # entry of the 'RSAKEY_SCHEMA' dictionary. The private key information is - # not included in the generation of the 'keyid' identifier. If a PEM is - # found to contain a private key, the generated rsakey object should be - # returned above. The following key object is for the case of a PEM with - # only a public key. Convert any '\r\n' (e.g., Windows) newline characters - # to '\n' so that a consistent keyid is generated. - key_value = {"public": public_pem.replace("\r\n", "\n"), "private": ""} - keyid = _get_keyid(keytype, scheme, key_value) - - rsakey_dict["keytype"] = keytype - rsakey_dict["scheme"] = scheme - rsakey_dict["keyid"] = keyid - rsakey_dict["keyval"] = key_value - - # Add "keyid_hash_algorithms" so that equal RSA keys with - # different keyids can be associated using supported keyid_hash_algorithms. - rsakey_dict["keyid_hash_algorithms"] = settings.HASH_ALGORITHMS - - return rsakey_dict - - -def extract_pem(pem, private_pem=False): - """ - - Extract only the portion of the pem that includes the header and footer, - with any leading and trailing characters removed. The string returned has - the following form: - - '-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----' - - or - - '-----BEGIN RSA PRIVATE KEY----- ... -----END RSA PRIVATE KEY-----' - - Note: This function assumes "pem" is a valid pem in the following format: - pem header + key material + key footer. Crypto libraries (e.g., pyca's - cryptography) that parse the pem returned by this function are expected to - fully validate the pem. - - - pem: - A string in PEM format. - - private_pem: - Boolean that indicates whether 'pem' is a private PEM. Private PEMs - are not shown in exception messages. - - - securesystemslib.exceptions.FormatError, if 'pem' is improperly formatted. - - - Only the public and private portion of the PEM is extracted. Leading or - trailing whitespace is not included in the returned PEM string. - - - A PEM string (excluding leading and trailing newline characters). - That is: pem header + key material + pem footer. - - """ - - if private_pem: - pem_header = "-----BEGIN RSA PRIVATE KEY-----" - pem_footer = "-----END RSA PRIVATE KEY-----" - - else: - pem_header = "-----BEGIN PUBLIC KEY-----" - pem_footer = "-----END PUBLIC KEY-----" - - header_start = 0 - footer_start = 0 - - # Raise error message if the expected header or footer is not found in 'pem'. - try: - header_start = pem.index(pem_header) - - except ValueError: - # Be careful not to print private key material in exception message. - if not private_pem: - raise exceptions.FormatError( # pylint: disable=raise-missing-from - "Required PEM header " - + repr(pem_header) - + "\n not found in PEM string: " - + repr(pem) - ) - - raise exceptions.FormatError( # pylint: disable=raise-missing-from - "Required PEM header " - + repr(pem_header) - + "\n not found in private PEM string." - ) - - try: - # Search for 'pem_footer' after the PEM header. - footer_start = pem.index(pem_footer, header_start + len(pem_header)) - - except ValueError: - # Be careful not to print private key material in exception message. - if not private_pem: - raise exceptions.FormatError( # pylint: disable=raise-missing-from - "Required PEM footer " - + repr(pem_footer) - + "\n not found in PEM string " - + repr(pem) - ) - - raise exceptions.FormatError( # pylint: disable=raise-missing-from - "Required PEM footer " - + repr(pem_footer) - + "\n not found in private PEM string." - ) - - # Extract only the public portion of 'pem'. Leading or trailing whitespace - # is excluded. - pem = pem[header_start : footer_start + len(pem_footer)] - - return pem - - -def encrypt_key(key_object, password): - """ - - Return a string containing 'key_object' in encrypted form. Encrypted - strings may be safely saved to a file. The corresponding decrypt_key() - function can be applied to the encrypted string to restore the original key - object. 'key_object' is a key (e.g., RSAKEY_SCHEMA, ED25519KEY_SCHEMA). - This function relies on the rsa_keys.py module to perform the - actual encryption. - - Encrypted keys use AES-256-CTR-Mode, and passwords are strengthened with - PBKDF2-HMAC-SHA256 (100K iterations by default, but may be overriden in - 'securesystemslib.settings.PBKDF2_ITERATIONS' by the user). - - http://en.wikipedia.org/wiki/Advanced_Encryption_Standard - http://en.wikipedia.org/wiki/CTR_mode#Counter_.28CTR.29 - https://en.wikipedia.org/wiki/PBKDF2 - - >>> ed25519_key = generate_ed25519_key() - >>> password = 'secret' - >>> encrypted_key = encrypt_key(ed25519_key, password).encode('utf-8') - >>> securesystemslib.formats.ENCRYPTEDKEY_SCHEMA.matches(encrypted_key) - True - - - key_object: - A key (containing also the private key portion) of the form - 'securesystemslib.formats.ANYKEY_SCHEMA' - - password: - The password, or passphrase, to encrypt the private part of the RSA - key. 'password' is not used directly as the encryption key, a stronger - encryption key is derived from it. - - - securesystemslib.exceptions.FormatError, if the arguments are improperly - formatted. - - securesystemslib.exceptions.CryptoError, if 'key_object' cannot be - encrypted. - - - None. - - - An encrypted string of the form: - 'securesystemslib.formats.ENCRYPTEDKEY_SCHEMA'. - """ - - # Does 'key_object' have the correct format? - # This check will ensure 'key_object' has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'securesystemslib.exceptions.FormatError' if the check fails. - formats.ANYKEY_SCHEMA.check_match(key_object) - - # Does 'password' have the correct format? - formats.PASSWORD_SCHEMA.check_match(password) - - # Encrypted string of 'key_object'. The encrypted string may be safely saved - # to a file and stored offline. - encrypted_key = None - - # Generate an encrypted string of 'key_object' using AES-256-CTR-Mode, where - # 'password' is strengthened with PBKDF2-HMAC-SHA256. - encrypted_key = rsa_keys.encrypt_key(key_object, password) - - return encrypted_key - - -def decrypt_key(encrypted_key, passphrase): - """ - - Return a string containing 'encrypted_key' in non-encrypted form. The - decrypt_key() function can be applied to the encrypted string to restore - the original key object, a key (e.g., RSAKEY_SCHEMA, ED25519KEY_SCHEMA). - This function calls rsa_keys.py to perform the actual decryption. - - Encrypted keys use AES-256-CTR-Mode and passwords are strengthened with - PBKDF2-HMAC-SHA256 (100K iterations be default, but may be overriden in - 'settings.py' by the user). - - http://en.wikipedia.org/wiki/Advanced_Encryption_Standard - http://en.wikipedia.org/wiki/CTR_mode#Counter_.28CTR.29 - https://en.wikipedia.org/wiki/PBKDF2 - - >>> ed25519_key = generate_ed25519_key() - >>> password = 'secret' - >>> encrypted_key = encrypt_key(ed25519_key, password) - >>> decrypted_key = decrypt_key(encrypted_key.encode('utf-8'), password) - >>> securesystemslib.formats.ANYKEY_SCHEMA.matches(decrypted_key) - True - >>> decrypted_key == ed25519_key - True - - - encrypted_key: - An encrypted key (additional data is also included, such as salt, number - of password iterations used for the derived encryption key, etc) of the - form 'securesystemslib.formats.ENCRYPTEDKEY_SCHEMA'. 'encrypted_key' - should have been generated with encrypt_key(). - - password: - The password, or passphrase, to decrypt 'encrypted_key'. 'password' is - not used directly as the encryption key, a stronger encryption key is - derived from it. The supported general-purpose module takes care of - re-deriving the encryption key. - - - securesystemslib.exceptions.FormatError, if the arguments are improperly - formatted. - - securesystemslib.exceptions.CryptoError, if 'encrypted_key' cannot be - decrypted. - - - None. - - - A key object of the form: 'securesystemslib.formats.ANYKEY_SCHEMA' (e.g., - RSAKEY_SCHEMA, ED25519KEY_SCHEMA). - """ - - # Does 'encrypted_key' have the correct format? - # This check ensures 'encrypted_key' has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'securesystemslib.exceptions.FormatError' if the check fails. - formats.ENCRYPTEDKEY_SCHEMA.check_match(encrypted_key) - - # Does 'passphrase' have the correct format? - formats.PASSWORD_SCHEMA.check_match(passphrase) - - # Store and return the decrypted key object. - key_object = None - - # Decrypt 'encrypted_key' so that the original key object is restored. - # encrypt_key() generates an encrypted string of the key object using - # AES-256-CTR-Mode, where 'password' is strengthened with PBKDF2-HMAC-SHA256. - key_object = rsa_keys.decrypt_key(encrypted_key, passphrase) - - # The corresponding encrypt_key() encrypts and stores key objects in - # non-metadata format (i.e., original format of key object argument to - # encrypt_key()) prior to returning. - - return key_object - - -def create_rsa_encrypted_pem(private_key, passphrase): - """ - - Return a string in PEM format (TraditionalOpenSSL), where the private part - of the RSA key is encrypted using the best available encryption for a given - key's backend. This is a curated (by cryptography.io) encryption choice and - the algorithm may change over time. - - c.f. cryptography.io/en/latest/hazmat/primitives/asymmetric/serialization/ - #cryptography.hazmat.primitives.serialization.BestAvailableEncryption - - >>> rsa_key = generate_rsa_key() - >>> private = rsa_key['keyval']['private'] - >>> passphrase = 'secret' - >>> encrypted_pem = create_rsa_encrypted_pem(private, passphrase) - >>> securesystemslib.formats.PEMRSA_SCHEMA.matches(encrypted_pem) - True - - - private_key: - The private key string in PEM format. - - passphrase: - The passphrase, or password, to encrypt the private part of the RSA key. - 'passphrase' is not used directly as the encryption key, a stronger - encryption key is derived from it. - - - securesystemslib.exceptions.FormatError, if the arguments are improperly - formatted. - - securesystemslib.exceptions.CryptoError, if an RSA key in encrypted PEM - format cannot be created. - - TypeError, 'private_key' is unset. - - - None. - - - A string in PEM format, where the private RSA key is encrypted. - Conforms to 'securesystemslib.formats.PEMRSA_SCHEMA'. - """ - - # Does 'private_key' have the correct format? - # This check will ensure 'private_key' has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'securesystemslib.exceptions.FormatError' if the check fails. - formats.PEMRSA_SCHEMA.check_match(private_key) - - # Does 'passphrase' have the correct format? - formats.PASSWORD_SCHEMA.check_match(passphrase) - - encrypted_pem = None - - # Generate the public and private RSA keys. A 2048-bit minimum is enforced by - # create_rsa_encrypted_pem() via a - # securesystemslib.formats.RSAKEYBITS_SCHEMA.check_match(). - encrypted_pem = rsa_keys.create_rsa_encrypted_pem(private_key, passphrase) - - return encrypted_pem - - -def is_pem_public(pem): - """ - - Checks if a passed PEM formatted string is a PUBLIC key, by looking for the - following pattern: - - '-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----' - - >>> rsa_key = generate_rsa_key() - >>> public = rsa_key['keyval']['public'] - >>> private = rsa_key['keyval']['private'] - >>> is_pem_public(public) - True - >>> is_pem_public(private) - False - - - pem: - A string in PEM format. - - - securesystemslib.exceptions.FormatError, if 'pem' is improperly formatted. - - - None - - - True if 'pem' is public and false otherwise. - """ - - # Do the arguments have the correct format? - # This check will ensure arguments have the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'securesystemslib.exceptions.FormatError' if the check fails. - formats.PEMRSA_SCHEMA.check_match(pem) - - pem_header = "-----BEGIN PUBLIC KEY-----" - pem_footer = "-----END PUBLIC KEY-----" - - try: - header_start = pem.index(pem_header) - pem.index(pem_footer, header_start + len(pem_header)) - - except ValueError: - return False - - return True - - -def is_pem_private(pem, keytype="rsa"): - """ - - Checks if a passed PEM formatted string is a PRIVATE key, by looking for - the following patterns: - - '-----BEGIN RSA PRIVATE KEY----- ... -----END RSA PRIVATE KEY-----' - '-----BEGIN EC PRIVATE KEY----- ... -----END EC PRIVATE KEY-----' - - >>> rsa_key = generate_rsa_key() - >>> private = rsa_key['keyval']['private'] - >>> public = rsa_key['keyval']['public'] - >>> is_pem_private(private) - True - >>> is_pem_private(public) - False - - - pem: - A string in PEM format. - - - securesystemslib.exceptions.FormatError, if any of the arguments are - improperly formatted. - - - None - - - True if 'pem' is private and false otherwise. - """ - - # Do the arguments have the correct format? - # This check will ensure arguments have the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'securesystemslib.exceptions.FormatError' if the check fails. - formats.PEMRSA_SCHEMA.check_match(pem) - formats.NAME_SCHEMA.check_match(keytype) - - if keytype == "rsa": - pem_header = "-----BEGIN RSA PRIVATE KEY-----" - pem_footer = "-----END RSA PRIVATE KEY-----" - - elif keytype == "ec": - pem_header = "-----BEGIN EC PRIVATE KEY-----" - pem_footer = "-----END EC PRIVATE KEY-----" - - else: - raise exceptions.FormatError( - "Unsupported key" - " type: " + repr(keytype) + '. Supported keytypes: ["rsa", "ec"]' - ) - - try: - header_start = pem.index(pem_header) - pem.index(pem_footer, header_start + len(pem_header)) - - except ValueError: - return False - - return True - - -def import_ed25519key_from_private_json( - json_str, password=None -): # pylint: disable=missing-function-docstring - if password is not None: - # This check will not fail, because a mal-formatted passed password fails - # above and an entered password will always be a string (see get_password) - # However, we include it in case PASSWORD_SCHEMA or get_password changes. - formats.PASSWORD_SCHEMA.check_match(password) - - # Decrypt the loaded key file, calling the 'cryptography' library to - # generate the derived encryption key from 'password'. Raise - # 'securesystemslib.exceptions.CryptoError' if the decryption fails. - key_object = decrypt_key(json_str.decode("utf-8"), password) - - else: - logger.debug( - "No password was given. Attempting to import an" - " unencrypted file." - ) - try: - key_object = util.load_json_string(json_str.decode("utf-8")) - # If the JSON could not be decoded, it is very likely, but not necessarily, - # due to a non-empty password. - except exceptions.Error: - raise exceptions.CryptoError( # pylint: disable=raise-missing-from - "Malformed Ed25519 key JSON, " - "possibly due to encryption, " - "but no password provided?" - ) - - # Raise an exception if an unexpected key type is imported. - if key_object["keytype"] != "ed25519": - message = "Invalid key type loaded: " + repr(key_object["keytype"]) - raise exceptions.FormatError(message) - - # Add "keyid_hash_algorithms" so that equal ed25519 keys with - # different keyids can be associated using supported keyid_hash_algorithms. - key_object["keyid_hash_algorithms"] = settings.HASH_ALGORITHMS - - return key_object - - -def import_ecdsakey_from_private_pem( - pem, scheme="ecdsa-sha2-nistp256", password=None -): - """ - - Import the private ECDSA key stored in 'pem', and generate its public key - (which will also be included in the returned ECDSA key object). In addition, - a keyid identifier for the ECDSA key is generated. The object returned - conforms to: - - {'keytype': 'ecdsa', - 'scheme': 'ecdsa-sha2-nistp256', - 'keyid': keyid, - 'keyval': {'public': '-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----', - 'private': '-----BEGIN EC PRIVATE KEY----- ... -----END EC PRIVATE KEY-----'}} - - The private key is a string in PEM format. - - >>> ecdsa_key = generate_ecdsa_key() - >>> private_pem = ecdsa_key['keyval']['private'] - >>> ecdsa_key = import_ecdsakey_from_private_pem(private_pem) - >>> securesystemslib.formats.ECDSAKEY_SCHEMA.matches(ecdsa_key) - True - - - pem: - A string in PEM format. The private key is extracted and returned in - an ecdsakey object. - - scheme: - The signature scheme used by the imported key. - - password: (optional) - The password, or passphrase, to decrypt the private part of the ECDSA - key if it is encrypted. 'password' is not used directly as the encryption - key, a stronger encryption key is derived from it. - - - securesystemslib.exceptions.FormatError, if the arguments are improperly - formatted. - - securesystemslib.exceptions.UnsupportedAlgorithmError, if 'pem' specifies - an unsupported key type. - - - None. - - - A dictionary containing the ECDSA keys and other identifying information. - Conforms to 'securesystemslib.formats.ECDSAKEY_SCHEMA'. - """ - - # Does 'pem' have the correct format? - # This check will ensure 'pem' conforms to - # 'securesystemslib.formats.ECDSARSA_SCHEMA'. - formats.PEMECDSA_SCHEMA.check_match(pem) - - # Is 'scheme' properly formatted? - formats.ECDSA_SCHEME_SCHEMA.check_match(scheme) - - if password is not None: - formats.PASSWORD_SCHEMA.check_match(password) - - else: - logger.debug( - "The password/passphrase is unset. The PEM is expected" - " to be unencrypted." - ) - - # Begin building the ECDSA key dictionary. - ecdsakey_dict = {} - keytype = "ecdsa" - public = None - private = None - - public, private = ecdsa_keys.create_ecdsa_public_and_private_from_pem( - pem, password - ) - - # Generate the keyid of the ECDSA key. 'key_value' corresponds to the - # 'keyval' entry of the 'ECDSAKEY_SCHEMA' dictionary. The private key - # information is not included in the generation of the 'keyid' identifier. - # Convert any '\r\n' (e.g., Windows) newline characters to '\n' so that a - # consistent keyid is generated. - key_value = {"public": public.replace("\r\n", "\n"), "private": ""} - keyid = _get_keyid(keytype, scheme, key_value) - - # Build the 'ecdsakey_dict' dictionary. Update 'key_value' with the ECDSA - # private key prior to adding 'key_value' to 'ecdsakey_dict'. - key_value["private"] = private - - ecdsakey_dict["keytype"] = keytype - ecdsakey_dict["scheme"] = scheme - ecdsakey_dict["keyid"] = keyid - ecdsakey_dict["keyval"] = key_value - - # Add "keyid_hash_algorithms" so equal ECDSA keys with - # different keyids can be associated using supported keyid_hash_algorithms - ecdsakey_dict["keyid_hash_algorithms"] = settings.HASH_ALGORITHMS - - return ecdsakey_dict - - -def import_ecdsakey_from_public_pem(pem, scheme="ecdsa-sha2-nistp256"): - """ - - Generate an ECDSA key object from 'pem'. In addition, a keyid identifier - for the ECDSA key is generated. The object returned conforms to - 'securesystemslib.formats.ECDSAKEY_SCHEMA' and has the form: - - {'keytype': 'ecdsa', - 'scheme': 'ecdsa-sha2-nistp256', - 'keyid': keyid, - 'keyval': {'public': '-----BEGIN PUBLIC KEY----- ...', - 'private': ''}} - - The public portion of the ECDSA key is a string in PEM format. - - >>> ecdsa_key = generate_ecdsa_key() - >>> public = ecdsa_key['keyval']['public'] - >>> ecdsa_key['keyval']['private'] = '' - >>> scheme = ecdsa_key['scheme'] - >>> ecdsa_key2 = import_ecdsakey_from_public_pem(public, scheme) - >>> securesystemslib.formats.ECDSAKEY_SCHEMA.matches(ecdsa_key) - True - >>> securesystemslib.formats.ECDSAKEY_SCHEMA.matches(ecdsa_key2) - True - - - pem: - A string in PEM format (it should contain a public ECDSA key). - - scheme: - The signature scheme used by the imported key. - - - securesystemslib.exceptions.FormatError, if 'pem' is improperly formatted. - - - Only the public portion of the PEM is extracted. Leading or trailing - whitespace is not included in the PEM string stored in the rsakey object - returned. - - - A dictionary containing the ECDSA keys and other identifying information. - Conforms to 'securesystemslib.formats.ECDSAKEY_SCHEMA'. - """ - - # Does 'pem' have the correct format? - # This check will ensure arguments has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'securesystemslib.exceptions.FormatError' if the check fails. - formats.PEMECDSA_SCHEMA.check_match(pem) - - # Is 'scheme' properly formatted? - formats.ECDSA_SCHEME_SCHEMA.check_match(scheme) - - # Ensure the PEM string has a public header and footer. Although a simple - # validation of 'pem' is performed here, a fully valid PEM string is needed - # later to successfully verify signatures. Performing stricter validation of - # PEMs are left to the external libraries that use 'pem'. - - if is_pem_public(pem): - public_pem = extract_pem(pem, private_pem=False) - - else: - raise exceptions.FormatError("Invalid public" " pem: " + repr(pem)) - - # Begin building the ECDSA key dictionary. - ecdsakey_dict = {} - keytype = "ecdsa" - - # Generate the keyid of the ECDSA key. 'key_value' corresponds to the - # 'keyval' entry of the 'ECDSAKEY_SCHEMA' dictionary. The private key - # information is not included in the generation of the 'keyid' identifier. - # Convert any '\r\n' (e.g., Windows) newline characters to '\n' so that a - # consistent keyid is generated. - key_value = {"public": public_pem.replace("\r\n", "\n"), "private": ""} - keyid = _get_keyid(keytype, scheme, key_value) - - ecdsakey_dict["keytype"] = keytype - ecdsakey_dict["scheme"] = scheme - ecdsakey_dict["keyid"] = keyid - ecdsakey_dict["keyval"] = key_value - - # Add "keyid_hash_algorithms" so that equal ECDSA keys with different keyids - # can be associated using supported keyid_hash_algorithms. - ecdsakey_dict["keyid_hash_algorithms"] = settings.HASH_ALGORITHMS - - return ecdsakey_dict - - -def import_ecdsakey_from_pem(pem, scheme="ecdsa-sha2-nistp256"): - """ - - Import either a public or private ECDSA PEM. In contrast to the other - explicit import functions (import_ecdsakey_from_public_pem and - import_ecdsakey_from_private_pem), this function is useful for when it is - not known whether 'pem' is private or public. - - - pem: - A string in PEM format. - - scheme: - The signature scheme used by the imported key. - - securesystemslib.exceptions.FormatError, if 'pem' is improperly formatted. - - - None. - - - A dictionary containing the ECDSA keys and other identifying information. - Conforms to 'securesystemslib.formats.ECDSAKEY_SCHEMA'. - """ - - # Does 'pem' have the correct format? - # This check will ensure arguments has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'securesystemslib.exceptions.FormatError' if the check fails. - formats.PEMECDSA_SCHEMA.check_match(pem) - - # Is 'scheme' properly formatted? - formats.ECDSA_SCHEME_SCHEMA.check_match(scheme) - - public_pem = "" - - # Ensure the PEM string has a public or private header and footer. Although - # a simple validation of 'pem' is performed here, a fully valid PEM string is - # needed later to successfully verify signatures. Performing stricter - # validation of PEMs are left to the external libraries that use 'pem'. - if is_pem_public(pem): - public_pem = extract_pem(pem, private_pem=False) - - elif is_pem_private(pem, "ec"): - # Return an ecdsakey object (ECDSAKEY_SCHEMA) with the private key included. - return import_ecdsakey_from_private_pem(pem, password=None) - - else: - raise exceptions.FormatError( - "PEM contains neither a public" " nor private key: " + repr(pem) - ) - - # Begin building the ECDSA key dictionary. - ecdsakey_dict = {} - keytype = "ecdsa" - - # Generate the keyid of the ECDSA key. 'key_value' corresponds to the - # 'keyval' entry of the 'ECDSAKEY_SCHEMA' dictionary. The private key - # information is not included in the generation of the 'keyid' identifier. - # If a PEM is found to contain a private key, the generated rsakey object - # should be returned above. The following key object is for the case of a - # PEM with only a public key. Convert any '\r\n' (e.g., Windows) newline - # characters to '\n' so that a consistent keyid is generated. - key_value = {"public": public_pem.replace("\r\n", "\n"), "private": ""} - keyid = _get_keyid(keytype, scheme, key_value) - - ecdsakey_dict["keytype"] = keytype - ecdsakey_dict["scheme"] = scheme - ecdsakey_dict["keyid"] = keyid - ecdsakey_dict["keyval"] = key_value - - return ecdsakey_dict - - -if __name__ == "__main__": - # The interactive sessions of the documentation strings can - # be tested by running 'keys.py' as a standalone module: - # $ python keys.py - import doctest - - doctest.testmod() diff --git a/securesystemslib/rsa_keys.py b/securesystemslib/rsa_keys.py deleted file mode 100755 index 94c1a678..00000000 --- a/securesystemslib/rsa_keys.py +++ /dev/null @@ -1,1102 +0,0 @@ -""" - - rsa_keys.py - - - Vladimir Diaz - - - June 3, 2015. - - - See LICENSE for licensing information. - - - The goal of this module is to support public-key and general-purpose - cryptography through the pyca/cryptography (available as 'cryptography' on - pypi) library. - - The RSA-related functions provided include: - generate_rsa_public_and_private() - create_rsa_signature() - verify_rsa_signature() - create_rsa_encrypted_pem() - create_rsa_public_and_private_from_pem() - - The general-purpose functions include: - encrypt_key() - decrypt_key() - - pyca/cryptography performs the actual cryptographic operations and the - functions listed above can be viewed as the easy-to-use public interface. - - https://pypi.python.org/pypi/cryptography/ - https://github.com/pyca/cryptography - - https://en.wikipedia.org/wiki/RSA_(algorithm) - https://en.wikipedia.org/wiki/Advanced_Encryption_Standard - https://en.wikipedia.org/wiki/PBKDF - http://en.wikipedia.org/wiki/Scrypt - - securesystemslib key files are encrypted with the AES-256-CTR-Mode symmetric - key algorithm. User passwords are strengthened with PBKDF2, currently set to - 100,000 passphrase iterations. The previous evpy implementation used 1,000 - iterations. - - PEM-encrypted RSA key files use the Triple Data Encryption Algorithm (3DES), - and Cipher-block chaining (CBC) for the mode of operation. Password-Based - Key Derivation Function 1 (PBKF1) + MD5. - """ - -import binascii -import json -import os - -CRYPTO = True -NO_CRYPTO_MSG = "RSA key support requires the cryptography library" -try: - # Import pyca/cryptography routines needed to generate and load cryptographic - # keys in PEM format. - # Import Exception classes need to catch pyca/cryptography exceptions. - from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm - from cryptography.hazmat.backends import default_backend - - # pyca/cryptography requires hash objects to generate PKCS#1 PSS - # signatures (i.e., padding.PSS). The 'hmac' module is needed to verify - # ciphertexts in encrypted key files. - from cryptography.hazmat.primitives import hashes, hmac, serialization - - # RSA's probabilistic signature scheme with appendix (RSASSA-PSS). - # PKCS#1 v1.5 is available for compatibility with existing applications, but - # RSASSA-PSS is encouraged for newer applications. RSASSA-PSS generates - # a random salt to ensure the signature generated is probabilistic rather than - # deterministic (e.g., PKCS#1 v1.5). - # http://en.wikipedia.org/wiki/RSA-PSS#Schemes - # https://tools.ietf.org/html/rfc3447#section-8.1 - # The 'padding' module is needed for PSS signatures. - # 'cryptography.hazmat.primitives.asymmetric' (i.e., pyca/cryptography's - # public-key cryptography modules) supports algorithms like the Digital - # Signature Algorithm (DSA) and the ECDSA (Elliptic Curve Digital Signature - # Algorithm) encryption system. The 'rsa' module module is needed here to - # generate RSA keys and PS - from cryptography.hazmat.primitives.asymmetric import padding, rsa - - # The mode of operation is presently set to CTR (CounTeR Mode) for symmetric - # block encryption (AES-256, where the symmetric key is 256 bits). 'modes' can - # be used as an argument to 'ciphers.Cipher' to specify the mode of operation - # for the block cipher. The initial random block, or initialization vector - # (IV), can be set to begin the process of incrementing the 128-bit blocks and - # allowing the AES algorithm to perform cipher block operations on them. - # pyca/cryptography's AES implementation available in 'ciphers.Cipher. and - # 'ciphers.algorithms'. AES is a symmetric key algorithm that operates on - # fixed block sizes of 128-bits. - # https://en.wikipedia.org/wiki/Advanced_Encryption_Standard - from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - - # Import pyca/cryptography's Key Derivation Function (KDF) module. - # 'securesystemslib.keys.py' needs this module to derive a secret key according - # to the Password-Based Key Derivation Function 2 specification. The derived - # key is used as the symmetric key to encrypt securesystemslib key information. - # PKCS#5 v2.0 PBKDF2 specification: http://tools.ietf.org/html/rfc2898#section-5.2 - from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC - from cryptography.hazmat.primitives.serialization import ( - load_pem_private_key, - ) -except ImportError: - CRYPTO = False - -from securesystemslib import ( # pylint: disable=wrong-import-position - exceptions, - formats, - settings, - util, -) -from securesystemslib.hash import ( # pylint: disable=wrong-import-position - digest_from_rsa_scheme, -) - -# Recommended RSA key sizes: -# http://www.emc.com/emc-plus/rsa-labs/historical/twirl-and-rsa-key-size.htm#table1 -# According to the document above, revised May 6, 2003, RSA keys of size 3072 -# provide security through 2031 and beyond. -_DEFAULT_RSA_KEY_BITS = 3072 - -# The delimiter symbol used to separate the different sections of encrypted -# files (i.e., salt, iterations, hmac, IV, ciphertext). This delimiter is -# arbitrarily chosen and should not occur in the hexadecimal representations of -# the fields it is separating. -_ENCRYPTION_DELIMITER = "@@@@" - -# AES key size. Default key size = 32 bytes = AES-256. -_AES_KEY_SIZE = 32 - -# Default salt size, in bytes. A 128-bit salt (i.e., a random sequence of data -# to protect against attacks that use precomputed rainbow tables to crack -# password hashes) is generated for PBKDF2. -_SALT_SIZE = 16 - -# Default PBKDF2 passphrase iterations. The current "good enough" number -# of passphrase iterations. We recommend that important keys, such as root, -# be kept offline. 'settings.PBKDF2_ITERATIONS' should increase as CPU -# speeds increase, set here at 100,000 iterations by default (in 2013). -# Repository maintainers may opt to modify the default setting according to -# their security needs and computational restrictions. A strong user password -# is still important. Modifying the number of iterations will result in a new -# derived key+PBDKF2 combination if the key is loaded and re-saved, overriding -# any previous iteration setting used by the old '.key'. -# https://en.wikipedia.org/wiki/PBKDF2 -_PBKDF2_ITERATIONS = settings.PBKDF2_ITERATIONS - - -def generate_rsa_public_and_private(bits=_DEFAULT_RSA_KEY_BITS): - """ - - Generate public and private RSA keys with modulus length 'bits'. The - public and private keys returned conform to - 'securesystemslib.formats.PEMRSA_SCHEMA' and have the form: - - '-----BEGIN RSA PUBLIC KEY----- ...' - - or - - '-----BEGIN RSA PRIVATE KEY----- ...' - - The public and private keys are returned as strings in PEM format. - - 'generate_rsa_public_and_private()' enforces a minimum key size of 2048 - bits. If 'bits' is unspecified, a 3072-bit RSA key is generated, which is - the key size recommended by TUF. - - >>> public, private = generate_rsa_public_and_private(2048) - >>> securesystemslib.formats.PEMRSA_SCHEMA.matches(public) - True - >>> securesystemslib.formats.PEMRSA_SCHEMA.matches(private) - True - - - bits: - The key size, or key length, of the RSA key. 'bits' must be 2048, or - greater. 'bits' defaults to 3072 if not specified. - - - securesystemslib.exceptions.FormatError, if 'bits' does not contain the - correct format. - - securesystemslib.exceptions.UnsupportedLibraryError, if the cryptography - module is not available. - - - The RSA keys are generated from pyca/cryptography's - rsa.generate_private_key() function. - - - A (public, private) tuple containing the RSA keys in PEM format. - """ - - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - - # Does 'bits' have the correct format? - # This check will ensure 'bits' conforms to - # 'securesystemslib.formats.RSAKEYBITS_SCHEMA'. 'bits' must be an integer - # object, with a minimum value of 2048. Raise - # 'securesystemslib.exceptions.FormatError' if the check fails. - formats.RSAKEYBITS_SCHEMA.check_match(bits) - - # Generate the public and private RSA keys. The pyca/cryptography 'rsa' - # module performs the actual key generation. The 'bits' argument is used, - # and a 2048-bit minimum is enforced by - # securesystemslib.formats.RSAKEYBITS_SCHEMA.check_match(). - private_key = rsa.generate_private_key( - public_exponent=65537, key_size=bits, backend=default_backend() - ) - - # Extract the public & private halves of the RSA key and generate their - # PEM-formatted representations. Return the key pair as a (public, private) - # tuple, where each RSA is a string in PEM format. - private_pem = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - ).strip() - - # Need to generate the public pem from the private key before serialization - # to PEM. - public_key = private_key.public_key() - public_pem = public_key.public_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PublicFormat.SubjectPublicKeyInfo, - ).strip() - - return public_pem.decode("utf-8"), private_pem.decode("utf-8") - - -def create_rsa_signature(private_key, data, scheme="rsassa-pss-sha256"): - """ - - Generate a 'scheme' signature. The signature, and the signature scheme - used, is returned as a (signature, scheme) tuple. - - The signing process will use 'private_key' to generate the signature of - 'data'. - - RFC3447 - RSASSA-PSS - http://www.ietf.org/rfc/rfc3447.txt - - >>> public, private = generate_rsa_public_and_private(2048) - >>> data = 'The quick brown fox jumps over the lazy dog'.encode('utf-8') - >>> scheme = 'rsassa-pss-sha256' - >>> signature, scheme = create_rsa_signature(private, data, scheme) - >>> securesystemslib.formats.NAME_SCHEMA.matches(scheme) - True - >>> scheme == 'rsassa-pss-sha256' - True - >>> securesystemslib.formats.PYCACRYPTOSIGNATURE_SCHEMA.matches(signature) - True - - - private_key: - The private RSA key, a string in PEM format. - - data: - Data (string) used by create_rsa_signature() to generate the signature. - - scheme: - The signature scheme used to generate the signature. - - - securesystemslib.exceptions.FormatError, if 'private_key' is improperly - formatted. - - ValueError, if 'private_key' is unset. - - securesystemslib.exceptions.CryptoError, if the signature cannot be - generated. - - securesystemslib.exceptions.UnsupportedLibraryError, if the cryptography - module is not available. - - - pyca/cryptography's 'RSAPrivateKey.signer()' called to generate the - signature. - - - A (signature, scheme) tuple, where the signature is a string and the scheme - is one of the supported RSA signature schemes. For example: - 'rsassa-pss-sha256'. - """ - - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - - # Does the arguments have the correct format? - # If not, raise 'securesystemslib.exceptions.FormatError' if any of the - # checks fail. - formats.PEMRSA_SCHEMA.check_match(private_key) - formats.DATA_SCHEMA.check_match(data) - formats.RSA_SCHEME_SCHEMA.check_match(scheme) - - # Signing 'data' requires a private key. Currently supported RSA signature - # schemes are defined in `securesystemslib.keys.RSA_SIGNATURE_SCHEMES`. - signature = None - - # Verify the signature, but only if the private key has been set. The - # private key is a NULL string if unset. Although it may be clearer to - # explicitly check that 'private_key' is not '', we can/should check for a - # value and not compare identities with the 'is' keyword. Up to this point - # 'private_key' has variable size and can be an empty string. - if not len(private_key): # pylint: disable=use-implicit-booleaness-not-len - raise ValueError("The required private key is unset.") - - try: - # 'private_key' (in PEM format) must first be converted to a - # pyca/cryptography private key object before a signature can be - # generated. - private_key_object = load_pem_private_key( - private_key.encode("utf-8"), - password=None, - backend=default_backend(), - ) - - digest_obj = digest_from_rsa_scheme(scheme, "pyca_crypto") - - if scheme.startswith("rsassa-pss"): - # Generate an RSSA-PSS signature. Raise - # 'securesystemslib.exceptions.CryptoError' for any of the expected - # exceptions raised by pyca/cryptography. - signature = private_key_object.sign( - data, - padding.PSS( - mgf=padding.MGF1(digest_obj.algorithm), - salt_length=padding.PSS.DIGEST_LENGTH, - ), - digest_obj.algorithm, - ) - - elif scheme.startswith("rsa-pkcs1v15"): - # Generate an RSA-PKCS1v15 signature. Raise - # 'securesystemslib.exceptions.CryptoError' for any of the expected - # exceptions raised by pyca/cryptography. - signature = private_key_object.sign( - data, padding.PKCS1v15(), digest_obj.algorithm - ) - - # The RSA_SCHEME_SCHEMA.check_match() above should have validated 'scheme'. - # This is a defensive check check.. - else: # pragma: no cover - raise exceptions.UnsupportedAlgorithmError( - "Unsupported" " signature scheme is specified: " + repr(scheme) - ) - - # If the PEM data could not be decrypted, or if its structure could not - # be decoded successfully. - except ValueError: - raise exceptions.CryptoError( # pylint: disable=raise-missing-from - "The private key" - " (in PEM format) could not be deserialized." # pylint: disable=implicit-str-concat - ) - - # 'TypeError' is raised if a password was given and the private key was - # not encrypted, or if the key was encrypted but no password was - # supplied. Note: A passphrase or password is not used when generating - # 'private_key', since it should not be encrypted. - except TypeError: - raise exceptions.CryptoError( # pylint: disable=raise-missing-from - "The private key was" - " unexpectedly encrypted." # pylint: disable=implicit-str-concat - ) - - # 'cryptography.exceptions.UnsupportedAlgorithm' is raised if the - # serialized key is of a type that is not supported by the backend, or if - # the key is encrypted with a symmetric cipher that is not supported by - # the backend. - except UnsupportedAlgorithm: # pragma: no cover - raise exceptions.CryptoError( # pylint: disable=raise-missing-from - "The private key is" - " encrypted with an unsupported algorithm." # pylint: disable=implicit-str-concat - ) - - return signature, scheme - - -def verify_rsa_signature(signature, signature_scheme, public_key, data): - """ - - Determine whether the corresponding private key of 'public_key' produced - 'signature'. verify_signature() will use the public key, signature scheme, - and 'data' to complete the verification. - - >>> public, private = generate_rsa_public_and_private(2048) - >>> data = b'The quick brown fox jumps over the lazy dog' - >>> scheme = 'rsassa-pss-sha256' - >>> signature, scheme = create_rsa_signature(private, data, scheme) - >>> verify_rsa_signature(signature, scheme, public, data) - True - >>> verify_rsa_signature(signature, scheme, public, b'bad_data') - False - - - signature: - A signature, as a string. This is the signature returned - by create_rsa_signature(). - - signature_scheme: - A string that indicates the signature scheme used to generate - 'signature'. Currently supported RSA signature schemes are defined in - `securesystemslib.keys.RSA_SIGNATURE_SCHEMES`. - - public_key: - The RSA public key, a string in PEM format. - - data: - Data used by securesystemslib.keys.create_signature() to generate - 'signature'. 'data' (a string) is needed here to verify 'signature'. - - - securesystemslib.exceptions.FormatError, if 'signature', - 'signature_scheme', 'public_key', or 'data' are improperly formatted. - - securesystemslib.exceptions.UnsupportedAlgorithmError, if the signature - scheme used by 'signature' is not one supported by - securesystemslib.keys.create_signature(). - - securesystemslib.exceptions.CryptoError, if the private key cannot be - decoded or its key type is unsupported. - - securesystemslib.exceptions.UnsupportedLibraryError, if the cryptography - module is not available. - - - pyca/cryptography's RSAPublicKey.verifier() called to do the actual - verification. - - - Boolean. True if the signature is valid, False otherwise. - """ - - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - - # Does 'public_key' have the correct format? - # This check will ensure 'public_key' conforms to - # 'securesystemslib.formats.PEMRSA_SCHEMA'. Raise - # 'securesystemslib.exceptions.FormatError' if the check fails. - formats.PEMRSA_SCHEMA.check_match(public_key) - - # Does 'signature_scheme' have the correct format? - formats.RSA_SCHEME_SCHEMA.check_match(signature_scheme) - - # Does 'signature' have the correct format? - formats.PYCACRYPTOSIGNATURE_SCHEMA.check_match(signature) - - # What about 'data'? - formats.DATA_SCHEMA.check_match(data) - - # Verify the RSASSA-PSS signature with pyca/cryptography. - try: - public_key_object = serialization.load_pem_public_key( - public_key.encode("utf-8"), backend=default_backend() - ) - - digest_obj = digest_from_rsa_scheme(signature_scheme, "pyca_crypto") - - # verify() raises 'cryptography.exceptions.InvalidSignature' if the - # signature is invalid. 'salt_length' is automatically - # determined when verifying the signature. - try: - if signature_scheme.startswith("rsassa-pss"): - public_key_object.verify( - signature, - data, - padding.PSS( - mgf=padding.MGF1(digest_obj.algorithm), - salt_length=padding.PSS.AUTO, - ), - digest_obj.algorithm, - ) - - elif signature_scheme.startswith("rsa-pkcs1v15"): - public_key_object.verify( - signature, data, padding.PKCS1v15(), digest_obj.algorithm - ) - - # The RSA_SCHEME_SCHEMA.check_match() above should have validated 'scheme'. - # This is a defensive check check.. - else: # pragma: no cover - raise exceptions.UnsupportedAlgorithmError( - "Unsupported" - " signature scheme is specified: " + repr(signature_scheme) - ) - - return True - - except InvalidSignature: - return False - - # Raised by load_pem_public_key(). - except (ValueError, UnsupportedAlgorithm) as e: - raise exceptions.CryptoError( - "The PEM could not be" - " decoded successfully, or contained an unsupported key type: " - + str(e) - ) - - -def create_rsa_encrypted_pem(private_key, passphrase): - """ - - Return a string in PEM format (TraditionalOpenSSL), where the private part - of the RSA key is encrypted using the best available encryption for a given - key's backend. This is a curated (by cryptography.io) encryption choice and - the algorithm may change over time. - - c.f. cryptography.io/en/latest/hazmat/primitives/asymmetric/serialization/ - #cryptography.hazmat.primitives.serialization.BestAvailableEncryption - - >>> public, private = generate_rsa_public_and_private(2048) - >>> passphrase = 'secret' - >>> encrypted_pem = create_rsa_encrypted_pem(private, passphrase) - >>> securesystemslib.formats.PEMRSA_SCHEMA.matches(encrypted_pem) - True - - - private_key: - The private key string in PEM format. - - passphrase: - The passphrase, or password, to encrypt the private part of the RSA - key. - - - securesystemslib.exceptions.FormatError, if the arguments are improperly - formatted. - - securesystemslib.exceptions.CryptoError, if the passed RSA key cannot be - deserialized by pyca cryptography. - - ValueError, if 'private_key' is unset. - - securesystemslib.exceptions.UnsupportedLibraryError, if the cryptography - module is not available. - - - - A string in PEM format (TraditionalOpenSSL), where the private RSA key is - encrypted. Conforms to 'securesystemslib.formats.PEMRSA_SCHEMA'. - """ - - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - - # This check will ensure 'private_key' has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'securesystemslib.exceptions.FormatError' if the check fails. - formats.PEMRSA_SCHEMA.check_match(private_key) - - # Does 'passphrase' have the correct format? - formats.PASSWORD_SCHEMA.check_match(passphrase) - - # 'private_key' may still be a NULL string after the - # 'securesystemslib.formats.PEMRSA_SCHEMA' so we need an additional check - if len(private_key): - try: - private_key = load_pem_private_key( - private_key.encode("utf-8"), - password=None, - backend=default_backend(), - ) - except ValueError: - raise exceptions.CryptoError( # pylint: disable=raise-missing-from - "The private key" - " (in PEM format) could not be deserialized." # pylint: disable=implicit-str-concat - ) - - else: - raise ValueError("The required private key is unset.") - - encrypted_pem = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.BestAvailableEncryption( - passphrase.encode("utf-8") - ), - ).strip() - - return encrypted_pem.decode() - - -def create_rsa_public_and_private_from_pem(pem, passphrase=None): - """ - - Generate public and private RSA keys from an optionally encrypted PEM. The - public and private keys returned conform to - 'securesystemslib.formats.PEMRSA_SCHEMA' and have the form: - - '-----BEGIN RSA PUBLIC KEY----- ... -----END RSA PUBLIC KEY-----' - - and - - '-----BEGIN RSA PRIVATE KEY----- ...-----END RSA PRIVATE KEY-----' - - The public and private keys are returned as strings in PEM format. - - In case the private key part of 'pem' is encrypted pyca/cryptography's - load_pem_private_key() method is passed passphrase. In the default case - here, pyca/cryptography will decrypt with a PBKDF1+MD5 - strengthened'passphrase', and 3DES with CBC mode for encryption/decryption. - Alternatively, key data may be encrypted with AES-CTR-Mode and the - passphrase strengthened with PBKDF2+SHA256, although this method is used - only with securesystemslib encrypted key files. - - >>> public, private = generate_rsa_public_and_private(2048) - >>> passphrase = 'secret' - >>> encrypted_pem = create_rsa_encrypted_pem(private, passphrase) - >>> returned_public, returned_private = \ - create_rsa_public_and_private_from_pem(encrypted_pem, passphrase) - >>> securesystemslib.formats.PEMRSA_SCHEMA.matches(returned_public) - True - >>> securesystemslib.formats.PEMRSA_SCHEMA.matches(returned_private) - True - >>> public == returned_public - True - >>> private == returned_private - True - - - pem: - A byte string in PEM format, where the private key can be encrypted. - It has the form: - - '-----BEGIN RSA PRIVATE KEY-----\n - Proc-Type: 4,ENCRYPTED\nDEK-Info: DES-EDE3-CBC ...' - - passphrase: (optional) - The passphrase, or password, to decrypt the private part of the RSA - key. 'passphrase' is not directly used as the encryption key, instead - it is used to derive a stronger symmetric key. - - - securesystemslib.exceptions.FormatError, if the arguments are improperly - formatted. - - securesystemslib.exceptions.CryptoError, if the public and private RSA keys - cannot be generated from 'pem', or exported in PEM format. - - securesystemslib.exceptions.UnsupportedLibraryError, if the cryptography - module is not available. - - - pyca/cryptography's 'serialization.load_pem_private_key()' called to - perform the actual conversion from an encrypted RSA private key to - PEM format. - - - A (public, private) tuple containing the RSA keys in PEM format. - """ - - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - - # Does 'encryped_pem' have the correct format? - # This check will ensure 'pem' has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'securesystemslib.exceptions.FormatError' if the check fails. - formats.PEMRSA_SCHEMA.check_match(pem) - - # If passed, does 'passphrase' have the correct format? - if passphrase is not None: - formats.PASSWORD_SCHEMA.check_match(passphrase) - passphrase = passphrase.encode("utf-8") - - # Generate a pyca/cryptography key object from 'pem'. The generated - # pyca/cryptography key contains the required export methods needed to - # generate the PEM-formatted representations of the public and private RSA - # key. - try: - private_key = load_pem_private_key( - pem.encode("utf-8"), passphrase, backend=default_backend() - ) - - # pyca/cryptography's expected exceptions for 'load_pem_private_key()': - # ValueError: If the PEM data could not be decrypted. - # (possibly because the passphrase is wrong)." - # TypeError: If a password was given and the private key was not encrypted. - # Or if the key was encrypted but no password was supplied. - # UnsupportedAlgorithm: If the private key (or if the key is encrypted with - # an unsupported symmetric cipher) is not supported by the backend. - except (ValueError, TypeError, UnsupportedAlgorithm) as e: - # Raise 'securesystemslib.exceptions.CryptoError' and pyca/cryptography's - # exception message. Avoid propogating pyca/cryptography's exception trace - # to avoid revealing sensitive error. - raise exceptions.CryptoError( - "RSA (public, private) tuple" - " cannot be generated from the encrypted PEM string: " + str(e) - ) - - # Export the public and private halves of the pyca/cryptography RSA key - # object. The (public, private) tuple returned contains the public and - # private RSA keys in PEM format, as strings. - # Extract the public & private halves of the RSA key and generate their - # PEM-formatted representations. Return the key pair as a (public, private) - # tuple, where each RSA is a string in PEM format. - private_pem = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - ).strip() - - # Need to generate the public key from the private one before serializing - # to PEM format. - public_key = private_key.public_key() - public_pem = public_key.public_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PublicFormat.SubjectPublicKeyInfo, - ).strip() - - return public_pem.decode(), private_pem.decode() - - -def encrypt_key(key_object, password): - """ - - Return a string containing 'key_object' in encrypted form. Encrypted - strings may be safely saved to a file. The corresponding decrypt_key() - function can be applied to the encrypted string to restore the original key - object. 'key_object' is a securesystemslib key (e.g., RSAKEY_SCHEMA, - ED25519KEY_SCHEMA). This function calls the pyca/cryptography library to - perform the encryption and derive a suitable encryption key. - - Whereas an encrypted PEM file uses the Triple Data Encryption Algorithm - (3DES), the Cipher-block chaining (CBC) mode of operation, and the Password - Based Key Derivation Function 1 (PBKF1) + MD5 to strengthen 'password', - encrypted securesystemslib keys use AES-256-CTR-Mode and passwords - strengthened with PBKDF2-HMAC-SHA256 (100K iterations by default, but may - be overriden in 'settings.PBKDF2_ITERATIONS' by the user). - - http://en.wikipedia.org/wiki/Advanced_Encryption_Standard - http://en.wikipedia.org/wiki/CTR_mode#Counter_.28CTR.29 - https://en.wikipedia.org/wiki/PBKDF2 - - >>> ed25519_key = {'keytype': 'ed25519', \ - 'scheme': 'ed25519', \ - 'keyid': \ - 'd62247f817883f593cf6c66a5a55292488d457bcf638ae03207dbbba9dbe457d', \ - 'keyval': {'public': \ - '74addb5ad544a4306b34741bc1175a3613a8d7dc69ff64724243efdec0e301ad', \ - 'private': \ - '1f26964cc8d4f7ee5f3c5da2fbb7ab35811169573ac367b860a537e47789f8c4'}} - >>> passphrase = 'secret' - >>> encrypted_key = encrypt_key(ed25519_key, passphrase) - >>> securesystemslib.formats.ENCRYPTEDKEY_SCHEMA.matches(encrypted_key.encode('utf-8')) - True - - - key_object: - The securesystemslib key object that should contain the private portion - of the ED25519 key. - - password: - The password, or passphrase, to encrypt the private part of the RSA - key. 'password' is not used directly as the encryption key, a stronger - encryption key is derived from it. - - - securesystemslib.exceptions.FormatError, if any of the arguments are - improperly formatted or 'key_object' does not contain the private portion - of the key. - - securesystemslib.exceptions.CryptoError, if an Ed25519 key in encrypted - securesystemslib format cannot be created. - - securesystemslib.exceptions.UnsupportedLibraryError, if the cryptography - module is not available. - - - - pyca/Cryptography cryptographic operations called to perform the actual - encryption of 'key_object'. 'password' used to derive a suitable - encryption key. - - - An encrypted string in 'securesystemslib.formats.ENCRYPTEDKEY_SCHEMA' format. - """ - - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - - # Do the arguments have the correct format? - # Ensure the arguments have the appropriate number of objects and object - # types, and that all dict keys are properly named. - # Raise 'securesystemslib.exceptions.FormatError' if the check fails. - formats.ANYKEY_SCHEMA.check_match(key_object) - - # Does 'password' have the correct format? - formats.PASSWORD_SCHEMA.check_match(password) - - # Ensure the private portion of the key is included in 'key_object'. - if ( - "private" not in key_object["keyval"] - or not key_object["keyval"]["private"] - ): - raise exceptions.FormatError( - "Key object does not contain a private part." - ) - - # Derive a key (i.e., an appropriate encryption key and not the - # user's password) from the given 'password'. Strengthen 'password' with - # PBKDF2-HMAC-SHA256 (100K iterations by default, but may be overriden in - # 'settings.PBKDF2_ITERATIONS' by the user). - salt, iterations, derived_key = _generate_derived_key(password) - - # Store the derived key info in a dictionary, the object expected - # by the non-public _encrypt() routine. - derived_key_information = { - "salt": salt, - "iterations": iterations, - "derived_key": derived_key, - } - - # Convert the key object to json string format and encrypt it with the - # derived key. - encrypted_key = _encrypt(json.dumps(key_object), derived_key_information) - - return encrypted_key - - -def decrypt_key(encrypted_key, password): - """ - - Return a string containing 'encrypted_key' in non-encrypted form. - The decrypt_key() function can be applied to the encrypted string to restore - the original key object, a securesystemslib key (e.g., RSAKEY_SCHEMA, - ED25519KEY_SCHEMA). This function calls the appropriate cryptography module - (i.e., rsa_keys.py) to perform the decryption. - - Encrypted securesystemslib keys use AES-256-CTR-Mode and passwords - strengthened with PBKDF2-HMAC-SHA256 (100K iterations be default, but may - be overriden in 'settings.py' by the user). - - http://en.wikipedia.org/wiki/Advanced_Encryption_Standard - http://en.wikipedia.org/wiki/CTR_mode#Counter_.28CTR.29 - https://en.wikipedia.org/wiki/PBKDF2 - - >>> ed25519_key = {'keytype': 'ed25519', \ - 'scheme': 'ed25519', \ - 'keyid': \ - 'd62247f817883f593cf6c66a5a55292488d457bcf638ae03207dbbba9dbe457d', \ - 'keyval': {'public': \ - '74addb5ad544a4306b34741bc1175a3613a8d7dc69ff64724243efdec0e301ad', \ - 'private': \ - '1f26964cc8d4f7ee5f3c5da2fbb7ab35811169573ac367b860a537e47789f8c4'}} - >>> passphrase = 'secret' - >>> encrypted_key = encrypt_key(ed25519_key, passphrase) - >>> decrypted_key = decrypt_key(encrypted_key.encode('utf-8'), passphrase) - >>> securesystemslib.formats.ED25519KEY_SCHEMA.matches(decrypted_key) - True - >>> decrypted_key == ed25519_key - True - - - encrypted_key: - An encrypted securesystemslib key (additional data is also included, such - as salt, number of password iterations used for the derived encryption - key, etc) of the form 'securesystemslib.formats.ENCRYPTEDKEY_SCHEMA'. - 'encrypted_key' should have been generated with encrypted_key(). - - password: - The password, or passphrase, to encrypt the private part of the RSA - key. 'password' is not used directly as the encryption key, a stronger - encryption key is derived from it. - - - securesystemslib.exceptions.FormatError, if the arguments are improperly - formatted. - - securesystemslib.exceptions.CryptoError, if a securesystemslib key cannot - be decrypted from 'encrypted_key'. - - securesystemslib.exceptions.Error, if a valid securesystemslib key object - is not found in 'encrypted_key'. - - securesystemslib.exceptions.UnsupportedLibraryError, if the cryptography - module is not available. - - - The pyca/cryptography is library called to perform the actual decryption - of 'encrypted_key'. The key derivation data stored in 'encrypted_key' is - used to re-derive the encryption/decryption key. - - - The decrypted key object in 'securesystemslib.formats.ANYKEY_SCHEMA' format. - """ - - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - - # Do the arguments have the correct format? - # Ensure the arguments have the appropriate number of objects and object - # types, and that all dict keys are properly named. - # Raise 'securesystemslib.exceptions.FormatError' if the check fails. - formats.ENCRYPTEDKEY_SCHEMA.check_match(encrypted_key) - - # Does 'password' have the correct format? - formats.PASSWORD_SCHEMA.check_match(password) - - # Decrypt 'encrypted_key', using 'password' (and additional key derivation - # data like salts and password iterations) to re-derive the decryption key. - json_data = _decrypt(encrypted_key, password) - - # Raise 'securesystemslib.exceptions.Error' if 'json_data' cannot be - # deserialized to a valid 'securesystemslib.formats.ANYKEY_SCHEMA' key - # object. - key_object = util.load_json_string(json_data.decode()) - - return key_object - - -def _generate_derived_key(password, salt=None, iterations=None): - """ - Generate a derived key by feeding 'password' to the Password-Based Key - Derivation Function (PBKDF2). pyca/cryptography's PBKDF2 implementation is - used in this module. 'salt' may be specified so that a previous derived key - may be regenerated, otherwise '_SALT_SIZE' is used by default. 'iterations' - is the number of SHA-256 iterations to perform, otherwise - '_PBKDF2_ITERATIONS' is used by default. - """ - - # Use pyca/cryptography's default backend (e.g., openSSL, CommonCrypto, etc.) - # The default backend is not fixed and can be changed by pyca/cryptography - # over time. - backend = default_backend() - - # If 'salt' and 'iterations' are unspecified, a new derived key is generated. - # If specified, a deterministic key is derived according to the given - # 'salt' and 'iterrations' values. - if salt is None: - salt = os.urandom(_SALT_SIZE) - - if iterations is None: - iterations = _PBKDF2_ITERATIONS - - # Derive an AES key with PBKDF2. The 'length' is the desired key length of - # the derived key. - pbkdf_object = PBKDF2HMAC( - algorithm=hashes.SHA256(), - length=32, - salt=salt, - iterations=iterations, - backend=backend, - ) - - derived_key = pbkdf_object.derive(password.encode("utf-8")) - - return salt, iterations, derived_key - - -def _encrypt(key_data, derived_key_information): - """ - Encrypt 'key_data' using the Advanced Encryption Standard (AES-256) algorithm. - 'derived_key_information' should contain a key strengthened by PBKDF2. The - key size is 256 bits and AES's mode of operation is set to CTR (CounTeR Mode). - The HMAC of the ciphertext is generated to ensure the ciphertext has not been - modified. - - 'key_data' is the JSON string representation of the key. In the case - of RSA keys, this format would be 'securesystemslib.formats.RSAKEY_SCHEMA': - - {'keytype': 'rsa', - 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', - 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} - - 'derived_key_information' is a dictionary of the form: - {'salt': '...', - 'derived_key': '...', - 'iterations': '...'} - - 'securesystemslib.exceptions.CryptoError' raised if the encryption fails. - """ - - # Generate a random Initialization Vector (IV). Follow the provably secure - # encrypt-then-MAC approach, which affords the ability to verify ciphertext - # without needing to decrypt it and preventing an attacker from feeding the - # block cipher malicious data. Modes like GCM provide both encryption and - # authentication, whereas CTR only provides encryption. - - # Generate a random 128-bit IV. Random bits of data is needed for salts and - # initialization vectors suitable for the encryption algorithms used in - # 'rsa_keys.py'. - iv = os.urandom(16) - - # Construct an AES-CTR Cipher object with the given key and a randomly - # generated IV. - symmetric_key = derived_key_information["derived_key"] - encryptor = Cipher( - algorithms.AES(symmetric_key), modes.CTR(iv), backend=default_backend() - ).encryptor() - - # Encrypt the plaintext and get the associated ciphertext. - # Do we need to check for any exceptions? - ciphertext = ( - encryptor.update(key_data.encode("utf-8")) + encryptor.finalize() - ) - - # Generate the hmac of the ciphertext to ensure it has not been modified. - # The decryption routine may verify a ciphertext without having to perform - # a decryption operation. - symmetric_key = derived_key_information["derived_key"] - salt = derived_key_information["salt"] - hmac_object = hmac.HMAC( - symmetric_key, hashes.SHA256(), backend=default_backend() - ) - hmac_object.update(ciphertext) - hmac_value = binascii.hexlify(hmac_object.finalize()) - - # Store the number of PBKDF2 iterations used to derive the symmetric key so - # that the decryption routine can regenerate the symmetric key successfully. - # The PBKDF2 iterations are allowed to vary for the keys loaded and saved. - iterations = derived_key_information["iterations"] - - # Return the salt, iterations, hmac, initialization vector, and ciphertext - # as a single string. These five values are delimited by - # '_ENCRYPTION_DELIMITER' to make extraction easier. This delimiter is - # arbitrarily chosen and should not occur in the hexadecimal representations - # of the fields it is separating. - return ( - binascii.hexlify(salt).decode() - + _ENCRYPTION_DELIMITER - + str(iterations) - + _ENCRYPTION_DELIMITER - + hmac_value.decode() - + _ENCRYPTION_DELIMITER - + binascii.hexlify(iv).decode() - + _ENCRYPTION_DELIMITER - + binascii.hexlify(ciphertext).decode() - ) - - -def _decrypt(file_contents, password): - """ - The corresponding decryption routine for _encrypt(). - - 'securesystemslib.exceptions.CryptoError' raised if the decryption fails. - """ - - # Extract the salt, iterations, hmac, initialization vector, and ciphertext - # from 'file_contents'. These five values are delimited by - # '_ENCRYPTION_DELIMITER'. This delimiter is arbitrarily chosen and should - # not occur in the hexadecimal representations of the fields it is - # separating. Raise 'securesystemslib.exceptions.CryptoError', if - # 'file_contents' does not contains the expected data layout. - try: - salt, iterations, read_hmac, iv, ciphertext = file_contents.split( - _ENCRYPTION_DELIMITER - ) - - except ValueError: - raise exceptions.CryptoError( # pylint: disable=raise-missing-from - "Invalid encrypted file." - ) - - # Ensure we have the expected raw data for the delimited cryptographic data. - salt = binascii.unhexlify(salt.encode("utf-8")) - iterations = int(iterations) - iv = binascii.unhexlify(iv.encode("utf-8")) - ciphertext = binascii.unhexlify(ciphertext.encode("utf-8")) - - # Generate derived key from 'password'. The salt and iterations are - # specified so that the expected derived key is regenerated correctly. - # Discard the old "salt" and "iterations" values, as we only need the old - # derived key. - _, _, symmetric_key = _generate_derived_key(password, salt, iterations) - - # Verify the hmac to ensure the ciphertext is valid and has not been altered. - # See the encryption routine for why we use the encrypt-then-MAC approach. - # The decryption routine may verify a ciphertext without having to perform - # a decryption operation. - generated_hmac_object = hmac.HMAC( - symmetric_key, hashes.SHA256(), backend=default_backend() - ) - generated_hmac_object.update(ciphertext) - generated_hmac = binascii.hexlify(generated_hmac_object.finalize()) - - if not util.digests_are_equal(generated_hmac.decode(), read_hmac): - raise exceptions.CryptoError("Decryption failed.") - - # Construct a Cipher object, with the key and iv. - decryptor = Cipher( - algorithms.AES(symmetric_key), modes.CTR(iv), backend=default_backend() - ).decryptor() - - # Decryption gets us the authenticated plaintext. - plaintext = decryptor.update(ciphertext) + decryptor.finalize() - - return plaintext - - -if __name__ == "__main__": - # The interactive sessions of the documentation strings can be tested by - # running 'rsa_keys.py' as a standalone module: - # $ python rsa_keys.py - import doctest - - doctest.testmod() diff --git a/tests/check_public_interfaces.py b/tests/check_public_interfaces.py index 5e0e85df..c3f2ee3c 100644 --- a/tests/check_public_interfaces.py +++ b/tests/check_public_interfaces.py @@ -12,8 +12,8 @@ See LICENSE for licensing information. - Public facing modules (e.g. interface.py and keys.py) must be - importable, even if the optional dependencies are not installed. + Public facing modules must be importable, even if the optional dependencies + are not installed. Each public facing function should always be callable and present meaningful user-feedback if an optional dependency that is required for @@ -31,14 +31,11 @@ import shutil import tempfile import unittest -from unittest import mock import securesystemslib.exceptions # pylint: disable=wrong-import-position import securesystemslib.gpg.constants # pylint: disable=wrong-import-position import securesystemslib.gpg.functions # pylint: disable=wrong-import-position import securesystemslib.gpg.util # pylint: disable=wrong-import-position -import securesystemslib.interface # pylint: disable=wrong-import-position -import securesystemslib.keys # pylint: disable=wrong-import-position from securesystemslib.exceptions import ( UnsupportedLibraryError, VerificationError, @@ -66,231 +63,6 @@ def setUpClass(cls): def tearDownClass(cls): shutil.rmtree(cls.temp_dir) - def test_interface(self): - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.interface._generate_and_write_rsa_keypair( # pylint: disable=protected-access - password="pw" - ) - - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.interface.generate_and_write_rsa_keypair("pw") - - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.interface.generate_and_write_rsa_keypair("pw") - - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - # Mock entry on prompt which is presented before lower-level functions - # raise UnsupportedLibraryError - with mock.patch( - "securesystemslib.interface.get_password", return_value="" - ): - securesystemslib.interface.generate_and_write_rsa_keypair_with_prompt() - - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.interface.generate_and_write_unencrypted_rsa_keypair() - - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - path = os.path.join(self.temp_dir, "rsa_key") - with open(path, "a"): # pylint: disable=unspecified-encoding - securesystemslib.interface.import_rsa_privatekey_from_file(path) - - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.interface._generate_and_write_ed25519_keypair( # pylint: disable=protected-access - password="pw" - ) - - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.interface.generate_and_write_ed25519_keypair("pw") - - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - # Mock entry on prompt which is presented before lower-level functions - # raise UnsupportedLibraryError - with mock.patch( - "securesystemslib.interface.get_password", return_value="" - ): - securesystemslib.interface.generate_and_write_ed25519_keypair_with_prompt() - - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.interface.generate_and_write_unencrypted_ed25519_keypair() - - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - path = os.path.join(self.temp_dir, "ed25519_priv.json") - with open(path, "a") as f: # pylint: disable=unspecified-encoding - f.write("{}") - securesystemslib.interface.import_ed25519_privatekey_from_file( - path, "pw" - ) - - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.interface._generate_and_write_ecdsa_keypair( # pylint: disable=protected-access - password="pw" - ) - - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.interface.generate_and_write_ecdsa_keypair("pw") - - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - # Mock entry on prompt which is presented before lower-level functions - # raise UnsupportedLibraryError - with mock.patch( - "securesystemslib.interface.get_password", return_value="" - ): - securesystemslib.interface.generate_and_write_ecdsa_keypair_with_prompt() - - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.interface.generate_and_write_unencrypted_ecdsa_keypair() - - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - path = os.path.join(self.temp_dir, "ecddsa.priv") - with open(path, "a") as f: # pylint: disable=unspecified-encoding - f.write("{}") - securesystemslib.interface.import_ecdsa_privatekey_from_file( - path, password="pw" - ) - - def test_keys(self): - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.keys.generate_rsa_key() - - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.keys.generate_ecdsa_key() - - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.keys.generate_ed25519_key() - - data = "foo" - keydict = { - "keytype": "ed25519", - "scheme": "ed25519", - "keyid": "f00", - "keyval": {"private": "f001", "public": "b00f"}, - } - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.keys.create_signature(keydict, data) - - keydict["keytype"] = "ecdsa" - keydict["scheme"] = "ecdsa-sha2-nistp256" - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.keys.create_signature(keydict, data) - - keydict["keytype"] = "rsa" - keydict["scheme"] = "rsassa-pss-sha256" - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.keys.create_signature(keydict, data) - - keydict["keytype"] = "ecdsa" - keydict["scheme"] = "ecdsa-sha2-nistp256" - sig = { - "keyid": "f00", - "sig": "cfbce8e23eef478975a4339036de2335002d57c7b1632dd01e526a3bc52a5b261508ad50b9e25f1b819d61017e7347e912db1af019bf47ee298cc58bbdef9703", - } - # NOTE: we don't test ed25519 keys as they can be verified in pure python - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.keys.verify_signature(keydict, sig, data) - - keydict["keytype"] = "rsa" - keydict["scheme"] = "rsassa-pss-sha256" - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.keys.verify_signature(keydict, sig, data) - - priv = "-----BEGIN RSA PRIVATE KEY-----\nMIIG5AIBAAKCAYEA2WC/pM+6/NbOE/b+N9L+5BOa5sLHCF88okpiCJAZhtIEMw8O\n/EX4CjSy5Qilrmj7ZXmwRyPf7ksd6dbgxAJYk555lE2dywdvzsd31B+nKuAky8/K\nNjpfH4bn2sBKxbA9FFrBenpBkBrq0qDyK85VGJO7ieUdjQepiBQbqctU/PxmPJcE\neO0f1X4IjA+MQv6j/Wt+dnCQSFpCHgOEA0CBWByfRR+DIX74y8RYyKHgj+LpNv1A\nUD1K2vbNc/LrZWEIojCz+2QcXtz/g0kXX5DmRP3feGMC/S/r9bIjEdP55XP70LQU\ndaly64Y/nOlwWHhDNRjtu0lfdqxrK30/O8S8NC6A+nXrav1DzOufffd6wuRKiEqc\nEXZGitSyt/Bg5z70jIHgP6sZ69F0uORr3CaX/YAcQdjPzvSkJEvSj1/sSa+iKOPe\nixQx3VoEpdI3wWu7TQBmTOA3gi2XEZFYdThMGUA5Yv/qNHQVHBkEvOdtTRbWFX0m\npBHLTwBoMO+VJI6hAgMBAAECggGATAC5wOQomrJ4Bx76r4YEPLZmGHzNni2+Q3gC\nYsAPTMYtVbTUJnxIRzk5uz6UvzBRhZ9QdO8kImr9IH9SwvWXBrYICERDAXOuMfwn\n93DBwAnyk5gpOWCbVaiTdDZ7bjc6g91ffHU2ay4eIFrJkWto8Vjl30bOWDrvmXZ+\nXZWMN5AAJvseQzGVSc3xKxdckSf7KmXlJ4Af0kxMhbXw+DobfzUysrZb4OBGGOij\nqjJ/E4/gvqs5S1TC0WAtYXbzutR7zVGuZUFVK7Lk1fq8XcJP5wXCrIjxGnP6V97y\nWn1h64eD+7Gt4wQ+IGr0zKxhSYWI4ou+6QIV3kGlFv9ZRI22yym9MalG1Z1g2GP4\nrgcBZ6j87siSG2L5WoA62pxPPm+vfgEW3GYty1sYqVVQEQhy7GGHWT1kYcc0H7Sr\nALspSr3VbDJtylMQ+wl2IHs8qQ2GAW/utHwPyPzgY2wswi/6L8oYKBrEKK66gSlF\nPHek3uSbho2cPVW7RpG3NA5AHJBhAoHBAO48GEnmacBvMwHfhHex6XUX+VW0QxMl\n/8uNbAp4MEgdyqLw1TLUUAvEbV6qOwL3IWxAvJjXl/9zPtiBUiniZfUI7Rm0LMlv\n1jUlXfzuLwZtL8dHUDFBaZNWlY+eG5dniWkhzMnKqYYGbs9DDO741AKWUtM9UtBA\nm6g0AP6maa3RRAFQ+JtoVFuMYg6R4oE621pKI5ZJ1Zmz/L6H1xoj1QH0JPND1Mxa\nqYEj5SAKE+tj4dbsHjKeaPjk30qnlulQPQKBwQDpln8mJ3z7EXGCYMQrbtg94YuR\n/AVM5pZL9V1YNB8jiydg3j5tMjXWSxd+Hc3Kg1Ey0SjWGtPGD1RQQM+ZQgubRFHP\n7RwQwhxwxji5Azl5LoupsNueMGLQ0bBxSQWTx8zxc4z5oVBcZgD4Pm+5wi17L/77\nqM9Md2nw4ONbsxMiNol65dc/XUPuxaUpPAe2XlV4EGsyWDee6OhH288WhOAzpixS\nB1Ywc6f7LNLc065w2rjzogzyONAFkTP4kKe/2jUCgcEAxznuPe64RTs49roLN2XL\nDCcOVgO3jA3dCkasMV0tU0HGsdihEi7G+fA8XkwRqXstsi+5CEBTVkb0KW6MXYZ9\nKRtb3ID2a0ZhZnRnUxuEq+UnbYlPoMFJHvPrgvz/qe/l08t2TNJ0TiaXCDDUYgwo\nkDlR7mF8HbfJ9DH5GvvjqH42Vrt2C9CFq0GMxw5s0xF7WthhRk9cl3sTQ+qpkayh\nd07Kj70L+hFfayWveMm0usb+mBNBdadPtcUAjpfz9g0pAoHBALWdULDOpQrkThfr\nurp2TWUXlxfjFg/rfNIELRZmOAu/ptdXFLx7/IXoDpT9AUNChIB5RUHqy9tDke9v\n5LkpM7L+FIoQtfCFq+03AWVAD5Cb0vUV0DuXLU1kq8X424BCKaNVjzeL59pfaMOa\nb+3C/u+3qo3qe3rdoZ4qjDuA6RCBzLSkPY5DqozcWQTNasWtZNCcG2yiUGSae/da\n/RFqMJOX0P/aOnYjhmjxOeV+JDQUqxaqWVx/NaYOdpT9i5/MPQKBwGaMbFVt0+CR\nRT5Ts/ZS1qCmyoIepFMOI0SyU8h5+qk4dGutXCm1zjyyxwdJAjG1PYny5imsc795\nR7g7PLSUA+pkXWU8aoiCuCkY6IYz8JFLAw74mxZdLaFQUfBBtSqMz4B9YvUOysr1\nj7Og3AYXob4Me1+ueq59YLM9fEd4Tbw+aBg5T27jwZEmmNripamNFFb6RuPq6u6H\nMZW81M7ahgizqGQsRcOskA/uBC1w3N7o/lUYa3I+OY6EqA4KigIuGw==\n-----END RSA PRIVATE KEY-----\n" - - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.keys.import_rsakey_from_private_pem("") - - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.keys.encrypt_key(keydict, "foo") - - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.keys.decrypt_key("enc", "pw") - - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.keys.create_rsa_encrypted_pem(priv, "pw") - - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.keys.import_ed25519key_from_private_json( - "".encode("utf-8"), "" - ) - - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError - ): - securesystemslib.keys.import_ecdsakey_from_private_pem(priv) - - def test_purepy_ed25519(self): - data = b"The quick brown fox jumps over the lazy dog" - pub = b"\xbe\xb7\\&\x82\x06UN\x96 - test_ecdsa_keys.py - - - Vladimir Diaz - - - November 23, 2016. - - - See LICENSE for licensing information. - - - Test cases for test_ecdsa_keys.py. -""" - -import unittest - -import securesystemslib.ecdsa_keys -import securesystemslib.exceptions -import securesystemslib.formats -import securesystemslib.rsa_keys - -public, private = securesystemslib.ecdsa_keys.generate_public_and_private() -FORMAT_ERROR_MSG = ( - "securesystemslib.exceptions.FormatError raised. Check object's format." -) - - -class TestECDSA_keys( - unittest.TestCase -): # pylint: disable=missing-class-docstring,invalid-name - def setUp(self): - pass - - def test_generate_public_and_private(self): - ( - public, # pylint: disable=redefined-outer-name - private, # pylint: disable=redefined-outer-name - ) = securesystemslib.ecdsa_keys.generate_public_and_private() - - # Check format of 'public' and 'private'. - self.assertEqual( - True, securesystemslib.formats.PEMECDSA_SCHEMA.matches(public) - ) - self.assertEqual( - True, securesystemslib.formats.PEMECDSA_SCHEMA.matches(private) - ) - - # Test for invalid argument. - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.ecdsa_keys.generate_public_and_private, - "bad_algo", - ) - - def test_create_ecdsa_public_and_private_from_pem(self): - global public # pylint: disable=global-statement - global private # pylint: disable=global-statement - - # Check format of 'public' and 'private'. - self.assertEqual( - True, securesystemslib.formats.PEMECDSA_SCHEMA.matches(public) - ) - self.assertEqual( - True, securesystemslib.formats.PEMECDSA_SCHEMA.matches(private) - ) - - # Check for a valid private pem. - ( - public, - private, - ) = securesystemslib.ecdsa_keys.create_ecdsa_public_and_private_from_pem( - private - ) - - # Check for an invalid pem (non-private). - self.assertRaises( - securesystemslib.exceptions.CryptoError, - securesystemslib.ecdsa_keys.create_ecdsa_public_and_private_from_pem, - public, - ) - - # Test for invalid argument. - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.ecdsa_keys.create_ecdsa_public_and_private_from_pem, - 123, - ) - - def test_create_signature(self): - global public # pylint: disable=global-variable-not-assigned - global private # pylint: disable=global-variable-not-assigned - data = b"The quick brown fox jumps over the lazy dog" - signature, method = securesystemslib.ecdsa_keys.create_signature( - public, private, data - ) - - # Verify format of returned values. - self.assertEqual( - True, - securesystemslib.formats.ECDSASIGNATURE_SCHEMA.matches(signature), - ) - - self.assertEqual( - True, securesystemslib.formats.NAME_SCHEMA.matches(method) - ) - self.assertEqual("ecdsa-sha2-nistp256", method) - - # Check for improperly formatted argument. - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.ecdsa_keys.create_signature, - 123, - private, - data, - ) - - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.ecdsa_keys.create_signature, - public, - 123, - data, - ) - - # Check for invalid 'data'. - self.assertRaises( - securesystemslib.exceptions.CryptoError, - securesystemslib.ecdsa_keys.create_signature, - public, - private, - 123, - ) - - def test_verify_signature(self): - global public # pylint: disable=global-variable-not-assigned - global private # pylint: disable=global-variable-not-assigned - data = b"The quick brown fox jumps over the lazy dog" - scheme = "ecdsa-sha2-nistp256" - signature, scheme = securesystemslib.ecdsa_keys.create_signature( - public, private, data, scheme - ) - - valid_signature = securesystemslib.ecdsa_keys.verify_signature( - public, scheme, signature, data - ) - self.assertEqual(True, valid_signature) - - # Generate an RSA key so that we can verify that non-ECDSA keys are - # rejected. - rsa_pem, _ = securesystemslib.rsa_keys.generate_rsa_public_and_private() - - # Verify that a non-ECDSA key (via the PEM argument) is rejected. - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.ecdsa_keys.verify_signature, - rsa_pem, - scheme, - signature, - data, - ) - - # Check for improperly formatted arguments. - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.ecdsa_keys.verify_signature, - 123, - scheme, - signature, - data, - ) - - # Signature method improperly formatted. - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.ecdsa_keys.verify_signature, - public, - 123, - signature, - data, - ) - - # Invalid signature method. - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.ecdsa_keys.verify_signature, - public, - "unsupported_scheme", - signature, - data, - ) - - # Signature not a string. - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.ecdsa_keys.verify_signature, - public, - scheme, - 123, - data, - ) - - # Invalid signature.. - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.ecdsa_keys.verify_signature, - public, - scheme, - "bad_signature", - data, - ) - - # Check for invalid signature and data. - self.assertEqual( - False, - securesystemslib.ecdsa_keys.verify_signature( - public, scheme, signature, b"123" - ), - ) - - # Mismatched signature. - bad_signature = b"a" * 64 - self.assertEqual( - False, - securesystemslib.ecdsa_keys.verify_signature( - public, scheme, bad_signature, data - ), - ) - - # Generated signature created with different data. - new_signature, scheme = securesystemslib.ecdsa_keys.create_signature( - public, private, b"mismatched data" - ) - - self.assertEqual( - False, - securesystemslib.ecdsa_keys.verify_signature( - public, scheme, new_signature, data - ), - ) - - -# Run the unit tests. -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_ed25519_keys.py b/tests/test_ed25519_keys.py deleted file mode 100755 index f60ac1a7..00000000 --- a/tests/test_ed25519_keys.py +++ /dev/null @@ -1,200 +0,0 @@ -""" - - test_ed25519_keys.py - - - Vladimir Diaz - - - October 11, 2013. - - - See LICENSE for licensing information. - - - Test cases for test_ed25519_keys.py. -""" - -import os -import unittest - -import securesystemslib.ed25519_keys -import securesystemslib.exceptions -import securesystemslib.formats - -public, private = securesystemslib.ed25519_keys.generate_public_and_private() -FORMAT_ERROR_MSG = ( - "securesystemslib.exceptions.FormatError raised. Check object's format." -) - - -class TestEd25519_keys( - unittest.TestCase -): # pylint: disable=missing-class-docstring,invalid-name - def setUp(self): - pass - - def test_generate_public_and_private(self): - pub, priv = securesystemslib.ed25519_keys.generate_public_and_private() - - # Check format of 'pub' and 'priv'. - self.assertEqual( - True, securesystemslib.formats.ED25519PUBLIC_SCHEMA.matches(pub) - ) - self.assertEqual( - True, securesystemslib.formats.ED25519SEED_SCHEMA.matches(priv) - ) - - def test_create_signature(self): - global public # pylint: disable=global-variable-not-assigned - global private # pylint: disable=global-variable-not-assigned - data = b"The quick brown fox jumps over the lazy dog" - scheme = "ed25519" - signature, scheme = securesystemslib.ed25519_keys.create_signature( - public, private, data, scheme - ) - - # Verify format of returned values. - self.assertEqual( - True, - securesystemslib.formats.ED25519SIGNATURE_SCHEMA.matches(signature), - ) - - self.assertEqual( - True, securesystemslib.formats.ED25519_SIG_SCHEMA.matches(scheme) - ) - self.assertEqual("ed25519", scheme) - - # Check for improperly formatted argument. - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.ed25519_keys.create_signature, - 123, - private, - data, - scheme, - ) - - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.ed25519_keys.create_signature, - public, - 123, - data, - scheme, - ) - - # Check for invalid 'data'. - self.assertRaises( - securesystemslib.exceptions.CryptoError, - securesystemslib.ed25519_keys.create_signature, - public, - private, - 123, - scheme, - ) - - def test_verify_signature(self): - global public # pylint: disable=global-variable-not-assigned - global private # pylint: disable=global-variable-not-assigned - data = b"The quick brown fox jumps over the lazy dog" - scheme = "ed25519" - signature, scheme = securesystemslib.ed25519_keys.create_signature( - public, private, data, scheme - ) - - valid_signature = securesystemslib.ed25519_keys.verify_signature( - public, scheme, signature, data - ) - self.assertEqual(True, valid_signature) - - bad_signature = os.urandom(64) - valid_signature = securesystemslib.ed25519_keys.verify_signature( - public, scheme, bad_signature, data - ) - self.assertEqual(False, valid_signature) - - # Check for improperly formatted arguments. - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.ed25519_keys.verify_signature, - 123, - scheme, - signature, - data, - ) - - # Signature method improperly formatted. - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.ed25519_keys.verify_signature, - public, - 123, - signature, - data, - ) - - # Invalid signature method. - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.ed25519_keys.verify_signature, - public, - "unsupported_scheme", - signature, - data, - ) - - # Signature not a string. - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.ed25519_keys.verify_signature, - public, - scheme, - 123, - data, - ) - - # Invalid signature length, which must be exactly 64 bytes.. - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.ed25519_keys.verify_signature, - public, - scheme, - "bad_signature", - data, - ) - - # Check for invalid signature and data. - # Mismatched data. - self.assertEqual( - False, - securesystemslib.ed25519_keys.verify_signature( - public, scheme, signature, b"123" - ), - ) - - # Mismatched signature. - bad_signature = b"a" * 64 - self.assertEqual( - False, - securesystemslib.ed25519_keys.verify_signature( - public, scheme, bad_signature, data - ), - ) - - # Generated signature created with different data. - new_signature, scheme = securesystemslib.ed25519_keys.create_signature( - public, private, b"mismatched data", scheme - ) - - self.assertEqual( - False, - securesystemslib.ed25519_keys.verify_signature( - public, scheme, new_signature, data - ), - ) - - -# Run the unit tests. -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_interface.py b/tests/test_interface.py deleted file mode 100755 index 32093184..00000000 --- a/tests/test_interface.py +++ /dev/null @@ -1,1039 +0,0 @@ -""" - - test_interface.py - - - Vladimir Diaz - - - January 5, 2017. - - - See LICENSE for licensing information. - - - Unit test for 'interface.py'. -""" - -import os -import shutil -import stat -import tempfile -import unittest -from unittest import mock - -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.serialization import load_pem_private_key - -from securesystemslib import ( # pylint: disable=wrong-import-position - KEY_TYPE_ECDSA, - KEY_TYPE_ED25519, - KEY_TYPE_RSA, -) -from securesystemslib.exceptions import ( # pylint: disable=wrong-import-position - CryptoError, - Error, - FormatError, -) -from securesystemslib.formats import ( # pylint: disable=wrong-import-position - ANY_PUBKEY_DICT_SCHEMA, - ECDSAKEY_SCHEMA, - ED25519KEY_SCHEMA, - PUBLIC_KEY_SCHEMA, - RSAKEY_SCHEMA, -) -from securesystemslib.interface import ( # pylint: disable=wrong-import-position - _generate_and_write_ecdsa_keypair, - _generate_and_write_ed25519_keypair, - _generate_and_write_rsa_keypair, - generate_and_write_ecdsa_keypair, - generate_and_write_ecdsa_keypair_with_prompt, - generate_and_write_ed25519_keypair, - generate_and_write_ed25519_keypair_with_prompt, - generate_and_write_rsa_keypair, - generate_and_write_rsa_keypair_with_prompt, - generate_and_write_unencrypted_ecdsa_keypair, - generate_and_write_unencrypted_ed25519_keypair, - generate_and_write_unencrypted_rsa_keypair, - import_ecdsa_privatekey_from_file, - import_ecdsa_publickey_from_file, - import_ed25519_privatekey_from_file, - import_ed25519_publickey_from_file, - import_privatekey_from_file, - import_publickeys_from_file, - import_rsa_privatekey_from_file, - import_rsa_publickey_from_file, -) - - -class TestInterfaceFunctions( - unittest.TestCase -): # pylint: disable=missing-class-docstring - @classmethod - def setUpClass(cls): - cls.test_data_dir = os.path.join( - os.path.dirname(os.path.realpath(__file__)), "data" - ) - - cls.path_rsa = os.path.join(cls.test_data_dir, "keystore", "rsa_key") - cls.path_ed25519 = os.path.join( - cls.test_data_dir, "keystore", "ed25519_key" - ) - cls.path_ecdsa = os.path.join( - cls.test_data_dir, "keystore", "ecdsa_key" - ) - cls.path_no_key = os.path.join(cls.test_data_dir, "keystore", "no_key") - - cls.orig_cwd = os.getcwd() - - def setUp(self): - self.tmp_dir = tempfile.mkdtemp(dir=self.orig_cwd) - os.chdir(self.tmp_dir) - - def tearDown(self): - os.chdir(self.orig_cwd) - shutil.rmtree(self.tmp_dir) - - def test_rsa(self): # pylint: disable=too-many-locals,too-many-statements - """Test RSA key _generation and import interface functions.""" - - # TEST: Generate default keys and import - # Assert location and format - fn_default = "default" - fn_default_ret = _generate_and_write_rsa_keypair(filepath=fn_default) - - pub = import_rsa_publickey_from_file(fn_default + ".pub") - priv = import_rsa_privatekey_from_file(fn_default) - - self.assertEqual(fn_default, fn_default_ret) - self.assertTrue(RSAKEY_SCHEMA.matches(pub)) - self.assertTrue(PUBLIC_KEY_SCHEMA.matches(pub)) - self.assertTrue(RSAKEY_SCHEMA.matches(priv)) - # NOTE: There is no private key schema, at least check it has a value - self.assertTrue(priv["keyval"]["private"]) - - # TEST: Generate unencrypted keys with empty prompt - # Assert importable without password - fn_empty_prompt = "empty_prompt" - with mock.patch( - "securesystemslib.interface.get_password", return_value="" - ): - _generate_and_write_rsa_keypair( - filepath=fn_empty_prompt, prompt=True - ) - import_rsa_privatekey_from_file(fn_empty_prompt) - - # TEST: Generate keys with auto-filename, i.e. keyid - # Assert filename is keyid - fn_keyid = _generate_and_write_rsa_keypair() - pub = import_rsa_publickey_from_file(fn_keyid + ".pub") - priv = import_rsa_privatekey_from_file(fn_keyid) - self.assertTrue( - os.path.basename(fn_keyid) == pub["keyid"] == priv["keyid"] - ) - - # TEST: Generate keys with custom bits - # Assert length - bits = 4096 - fn_bits = "bits" - _generate_and_write_rsa_keypair(filepath=fn_bits, bits=bits) - - priv = import_rsa_privatekey_from_file(fn_bits) - # NOTE: Parse PEM with pyca/cryptography to get the key size property - obj_bits = load_pem_private_key( - priv["keyval"]["private"].encode("utf-8"), - password=None, - backend=default_backend(), - ) - - self.assertEqual(obj_bits.key_size, bits) - - # TEST: Generate two keypairs with encrypted private keys using ... - pw = "pw" - fn_encrypted = "encrypted" - fn_prompt = "prompt" - - # ... a passed pw ... - _generate_and_write_rsa_keypair(filepath=fn_encrypted, password=pw) - with mock.patch( - "securesystemslib.interface.get_password", return_value=pw - ): - # ... and a prompted pw. - _generate_and_write_rsa_keypair(filepath=fn_prompt, prompt=True) - - # Assert that both private keys are importable using the prompted pw ... - import_rsa_privatekey_from_file(fn_prompt, prompt=True) - import_rsa_privatekey_from_file(fn_encrypted, prompt=True) - - # ... and the passed pw. - import_rsa_privatekey_from_file(fn_prompt, password=pw) - import_rsa_privatekey_from_file(fn_encrypted, password=pw) - - # TEST: Import existing keys with encrypted private key (test regression) - # Assert format - pub = import_rsa_publickey_from_file(self.path_rsa + ".pub") - priv = import_rsa_privatekey_from_file(self.path_rsa, "password") - - self.assertTrue(RSAKEY_SCHEMA.matches(pub)) - self.assertTrue(PUBLIC_KEY_SCHEMA.matches(pub)) - self.assertTrue(RSAKEY_SCHEMA.matches(priv)) - # NOTE: There is no private key schema, at least check it has a value - self.assertTrue(priv["keyval"]["private"]) - - # TEST: Generation errors - for idx, (kwargs, err_msg) in enumerate( - [ - # Error on empty password - ( - {"password": ""}, - "encryption password must be 1 or more characters long", - ), - # Error on 'password' and 'prompt=True' - ( - {"password": pw, "prompt": True}, - "passing 'password' and 'prompt=True' is not allowed", - ), - ] - ): - with self.assertRaises( - ValueError, - msg="(row {})".format( # pylint: disable=consider-using-f-string - idx - ), - ) as ctx: - _generate_and_write_rsa_keypair(**kwargs) - - self.assertEqual( - err_msg, - str(ctx.exception), - "expected: '{}' got: '{}' (row {})".format( # pylint: disable=consider-using-f-string - err_msg, ctx.exception, idx - ), - ) - - # Error on bad argument format - for idx, kwargs in enumerate( - [ - {"bits": 1024}, # Too low - {"bits": "not-an-int"}, - {"filepath": 123456}, # Not a string - {"password": 123456}, # Not a string - {"prompt": "not-a-bool"}, - ] - ): - with self.assertRaises( - FormatError, - msg="(row {})".format( # pylint: disable=consider-using-f-string - idx - ), - ): - _generate_and_write_rsa_keypair(**kwargs) - - # TEST: Import errors - - # Error public key import - err_msg = "Invalid public pem" - with self.assertRaises(Error) as ctx: - import_rsa_publickey_from_file(fn_default) - - self.assertTrue( - err_msg in str(ctx.exception), - "expected: '{}' got: '{}'".format( # pylint: disable=consider-using-f-string - err_msg, ctx.exception - ), - ) - - # Error on private key import... - for idx, (args, kwargs, err, err_msg) in enumerate( - [ - # Error on not a private key - ( - [fn_default + ".pub"], - {}, - CryptoError, - "Could not deserialize key data", - ), - # Error on not encrypted - ( - [fn_default], - {"password": pw}, - CryptoError, - "Password was given but private key is not encrypted", - ), - # Error on encrypted but no pw - ( - [fn_encrypted], - {}, - CryptoError, - "Password was not given but private key is encrypted", - ), - # Error on encrypted but empty pw passed - ( - [fn_encrypted], - {"password": ""}, - CryptoError, - "Password was not given but private key is encrypted", - ), - # Error on pw and prompt - ( - [fn_default], - {"password": pw, "prompt": True}, - ValueError, - "passing 'password' and 'prompt=True' is not allowed", - ), - ] - ): - with self.assertRaises( - err, - msg="(row {})".format( # pylint: disable=consider-using-f-string - idx - ), - ) as ctx: - import_rsa_privatekey_from_file(*args, **kwargs) - - self.assertTrue( - err_msg in str(ctx.exception), - "expected: '{}' got: '{}' (row {})".format( # pylint: disable=consider-using-f-string - err_msg, ctx.exception, idx - ), - ) - - # Error on encrypted but bad pw passed - # NOTE: for some key+pw combos the error differs, see pyca/cryptography#8563 - with self.assertRaises(CryptoError) as ctx: - import_rsa_privatekey_from_file(fn_encrypted, password="bad pw") - - error = str(ctx.exception) - self.assertTrue( - "Bad decrypt. Incorrect password?" in error - or "Could not deserialize key data" in error, - f"unexpected: {error}", - ) - - # Error on encrypted but bad pw prompted - err_msg = "Password was not given but private key is encrypted" - with self.assertRaises(CryptoError) as ctx, mock.patch( - "securesystemslib.interface.get_password", return_value="bad_pw" - ): - import_rsa_privatekey_from_file(fn_encrypted) - - self.assertTrue( - err_msg in str(ctx.exception), - "expected: '{}' got: '{}'".format( # pylint: disable=consider-using-f-string - err_msg, ctx.exception - ), - ) - - # Error on bad argument format - for idx, (args, kwargs) in enumerate( - [ - ([123456], {}), # bad path - ([fn_default], {"scheme": 123456}), # bad scheme - ([fn_default], {"scheme": "bad scheme"}), # bad scheme - ] - ): - with self.assertRaises( - FormatError, - msg="(row {})".format( # pylint: disable=consider-using-f-string - idx - ), - ): - import_rsa_publickey_from_file(*args, **kwargs) - with self.assertRaises( - FormatError, - msg="(row {})".format( # pylint: disable=consider-using-f-string - idx - ), - ): - import_rsa_privatekey_from_file(*args, **kwargs) - - # bad password - with self.assertRaises(FormatError): - import_rsa_privatekey_from_file(fn_default, password=123456) - - # bad prompt - with self.assertRaises(FormatError): - import_rsa_privatekey_from_file(fn_default, prompt="not-a-bool") - - def test_ed25519( - self, - ): # pylint: disable=too-many-locals,too-many-statements - """Test ed25519 key _generation and import interface functions.""" - - # TEST: Generate default keys and import - # Assert location and format - fn_default = "default" - fn_default_ret = _generate_and_write_ed25519_keypair( - filepath=fn_default - ) - - pub = import_ed25519_publickey_from_file(fn_default + ".pub") - priv = import_ed25519_privatekey_from_file(fn_default) - - self.assertEqual(fn_default, fn_default_ret) - self.assertTrue(ED25519KEY_SCHEMA.matches(pub)) - self.assertTrue(PUBLIC_KEY_SCHEMA.matches(pub)) - self.assertTrue(ED25519KEY_SCHEMA.matches(priv)) - # NOTE: There is no private key schema, at least check it has a value - self.assertTrue(priv["keyval"]["private"]) - - # TEST: Generate unencrypted keys with empty prompt - # Assert importable with empty prompt password and without password - fn_empty_prompt = "empty_prompt" - with mock.patch( - "securesystemslib.interface.get_password", return_value="" - ): - _generate_and_write_ed25519_keypair(filepath=fn_empty_prompt) - import_ed25519_privatekey_from_file(fn_empty_prompt, prompt=True) - import_ed25519_privatekey_from_file(fn_empty_prompt) - - # TEST: Generate keys with auto-filename, i.e. keyid - # Assert filename is keyid - fn_keyid = _generate_and_write_ed25519_keypair() - pub = import_ed25519_publickey_from_file(fn_keyid + ".pub") - priv = import_ed25519_privatekey_from_file(fn_keyid) - self.assertTrue( - os.path.basename(fn_keyid) == pub["keyid"] == priv["keyid"] - ) - - # TEST: Generate two keypairs with encrypted private keys using ... - pw = "pw" - fn_encrypted = "encrypted" - fn_prompt = "prompt" - # ... a passed pw ... - _generate_and_write_ed25519_keypair(filepath=fn_encrypted, password=pw) - with mock.patch( - "securesystemslib.interface.get_password", return_value=pw - ): - # ... and a prompted pw. - _generate_and_write_ed25519_keypair(filepath=fn_prompt, prompt=True) - - # Assert that both private keys are importable using the prompted pw ... - import_ed25519_privatekey_from_file(fn_prompt, prompt=True) - import_ed25519_privatekey_from_file(fn_encrypted, prompt=True) - - # ... and the passed pw. - import_ed25519_privatekey_from_file(fn_prompt, password=pw) - import_ed25519_privatekey_from_file(fn_encrypted, password=pw) - - # TEST: Import existing keys with encrypted private key (test regression) - # Assert format - pub = import_ed25519_publickey_from_file(self.path_ed25519 + ".pub") - priv = import_ed25519_privatekey_from_file( - self.path_ed25519, "password" - ) - - self.assertTrue(PUBLIC_KEY_SCHEMA.matches(pub)) - self.assertTrue(ED25519KEY_SCHEMA.matches(pub)) - self.assertTrue(ED25519KEY_SCHEMA.matches(priv)) - # NOTE: There is no private key schema, at least check it has a value - self.assertTrue(priv["keyval"]["private"]) - - # TEST: Unexpected behavior - # FIXME: Should 'import_ed25519_publickey_from_file' be able to import a - # a non-encrypted ed25519 private key? I think it should not, but it is: - priv = import_ed25519_publickey_from_file(fn_default) - self.assertTrue(ED25519KEY_SCHEMA.matches(priv)) - self.assertTrue(priv["keyval"]["private"]) - - # FIXME: Should 'import_ed25519_privatekey_from_file' be able to import a - # an ed25519 public key? I think it should not, but it is: - pub = import_ed25519_privatekey_from_file(fn_default + ".pub") - self.assertTrue(PUBLIC_KEY_SCHEMA.matches(pub)) - - # TEST: Generation errors - for idx, (kwargs, err_msg) in enumerate( - [ - # Error on empty password - ( - {"password": ""}, - "encryption password must be 1 or more characters long", - ), - # Error on 'password' and 'prompt=True' - ( - {"password": pw, "prompt": True}, - "passing 'password' and 'prompt=True' is not allowed", - ), - ] - ): - with self.assertRaises( - ValueError, - msg="(row {})".format( # pylint: disable=consider-using-f-string - idx - ), - ) as ctx: - _generate_and_write_ed25519_keypair(**kwargs) - - self.assertEqual( - err_msg, - str(ctx.exception), - "expected: '{}' got: '{}' (row {})".format( # pylint: disable=consider-using-f-string - err_msg, ctx.exception, idx - ), - ) - - # Error on bad argument format - for idx, kwargs in enumerate( - [ - {"filepath": 123456}, # Not a string - {"password": 123456}, # Not a string - {"prompt": "not-a-bool"}, - ] - ): - with self.assertRaises( - FormatError, - msg="(row {})".format( # pylint: disable=consider-using-f-string - idx - ), - ): - _generate_and_write_ed25519_keypair(**kwargs) - - # TEST: Import errors - # Error on public key import... - for idx, (fn, err_msg) in enumerate( - [ - # Error on invalid json (custom key format) - (fn_encrypted, "Cannot deserialize to a Python object"), - # Error on invalid custom key format - (self.path_no_key, "Missing key"), - # Error on invalid key type - (self.path_ecdsa + ".pub", "Invalid key type loaded"), - ] - ): - with self.assertRaises( - Error, - msg="(row {})".format( # pylint: disable=consider-using-f-string - idx - ), - ) as ctx: - import_ed25519_publickey_from_file(fn) - - self.assertTrue( - err_msg in str(ctx.exception), - "expected: '{}' got: '{}' (row {})".format( # pylint: disable=consider-using-f-string - err_msg, ctx.exception, idx - ), - ) - - # Error on private key import... - for idx, (args, kwargs, err, err_msg) in enumerate( - [ - # Error on not an ed25519 private key - ( - [self.path_ecdsa], - {}, - CryptoError, - "Malformed Ed25519 key JSON, possibly due to encryption, " - "but no password provided?", - ), - # Error on not encrypted - ( - [fn_default], - {"password": pw}, - CryptoError, - "Invalid encrypted file.", - ), - # Error on encrypted but no pw - ( - [fn_encrypted], - {}, - CryptoError, - "Malformed Ed25519 key JSON, possibly due to encryption, " - "but no password provided?", - ), - # Error on encrypted but empty pw - ( - [fn_encrypted], - {"password": ""}, - CryptoError, - "Decryption failed.", - ), - # Error on encrypted but bad pw passed - ( - [fn_encrypted], - {"password": "bad pw"}, - CryptoError, - "Decryption failed.", - ), - # Error on pw and prompt - ( - [fn_default], - {"password": pw, "prompt": True}, - ValueError, - "passing 'password' and 'prompt=True' is not allowed", - ), - ] - ): - with self.assertRaises( - err, - msg="(row {})".format( # pylint: disable=consider-using-f-string - idx - ), - ) as ctx: - import_ed25519_privatekey_from_file(*args, **kwargs) - - self.assertTrue( - err_msg in str(ctx.exception), - "expected: '{}' got: '{}' (row {})".format( # pylint: disable=consider-using-f-string - err_msg, ctx.exception, idx - ), - ) - - # Error on encrypted but bad pw prompted - err_msg = ( - "Malformed Ed25519 key JSON, possibly due to encryption, " - "but no password provided?" - ) - with self.assertRaises(CryptoError) as ctx, mock.patch( - "securesystemslib.interface.get_password", return_value="bad_pw" - ): - import_ed25519_privatekey_from_file(fn_encrypted) - - self.assertTrue( - err_msg in str(ctx.exception), - "expected: '{}' got: '{}'".format( # pylint: disable=consider-using-f-string - err_msg, ctx.exception - ), - ) - - # Error on bad path format - with self.assertRaises(FormatError): - import_ed25519_publickey_from_file(123456) - with self.assertRaises(FormatError): - import_ed25519_privatekey_from_file(123456) - - # Error on bad password format - with self.assertRaises(FormatError): - import_ed25519_privatekey_from_file(fn_default, password=123456) - - # Error on bad prompt format - with self.assertRaises(FormatError): - import_ed25519_privatekey_from_file(fn_default, prompt="not-a-bool") - - def test_ecdsa(self): # pylint: disable=too-many-locals,too-many-statements - """Test ecdsa key _generation and import interface functions.""" - # TEST: Generate default keys and import - # Assert location and format - fn_default = "default" - fn_default_ret = _generate_and_write_ecdsa_keypair(filepath=fn_default) - - pub = import_ecdsa_publickey_from_file(fn_default + ".pub") - priv = import_ecdsa_privatekey_from_file(fn_default) - - self.assertEqual(fn_default, fn_default_ret) - self.assertTrue(ECDSAKEY_SCHEMA.matches(pub)) - self.assertTrue(PUBLIC_KEY_SCHEMA.matches(pub)) - self.assertTrue(ECDSAKEY_SCHEMA.matches(priv)) - # NOTE: There is no private key schema, at least check it has a value - self.assertTrue(priv["keyval"]["private"]) - - # TEST: Generate unencrypted keys with empty prompt - # Assert importable with empty prompt password and without password - fn_empty_prompt = "empty_prompt" - with mock.patch( - "securesystemslib.interface.get_password", return_value="" - ): - _generate_and_write_ecdsa_keypair(filepath=fn_empty_prompt) - import_ecdsa_privatekey_from_file(fn_empty_prompt, prompt=True) - import_ecdsa_privatekey_from_file(fn_empty_prompt) - - # TEST: Generate keys with auto-filename, i.e. keyid - # Assert filename is keyid - fn_keyid = _generate_and_write_ecdsa_keypair() - pub = import_ecdsa_publickey_from_file(fn_keyid + ".pub") - priv = import_ecdsa_privatekey_from_file(fn_keyid) - self.assertTrue( - os.path.basename(fn_keyid) == pub["keyid"] == priv["keyid"] - ) - - # TEST: Generate two key pairs with encrypted private keys using ... - pw = "pw" - fn_encrypted = "encrypted" - fn_prompt = "prompt" - # ... a passed pw ... - _generate_and_write_ecdsa_keypair(filepath=fn_encrypted, password=pw) - with mock.patch( - "securesystemslib.interface.get_password", return_value=pw - ): - # ... and a prompted pw. - _generate_and_write_ecdsa_keypair(filepath=fn_prompt, prompt=True) - - # Assert that both private keys are importable using the prompted pw ... - import_ecdsa_privatekey_from_file(fn_prompt, prompt=True) - import_ecdsa_privatekey_from_file(fn_encrypted, prompt=True) - - # ... and the passed pw. - import_ecdsa_privatekey_from_file(fn_prompt, password=pw) - import_ecdsa_privatekey_from_file(fn_encrypted, password=pw) - - # TEST: Import existing keys with encrypted private key (test regression) - # Assert format - pub = import_ecdsa_publickey_from_file(self.path_ecdsa + ".pub") - priv = import_ecdsa_privatekey_from_file(self.path_ecdsa, "password") - - self.assertTrue(ECDSAKEY_SCHEMA.matches(pub)) - self.assertTrue(PUBLIC_KEY_SCHEMA.matches(pub)) - self.assertTrue(ECDSAKEY_SCHEMA.matches(priv)) - # NOTE: There is no private key schema, at least check it has a value - self.assertTrue(priv["keyval"]["private"]) - - # FIXME: Should 'import_ecdsa_publickey_from_file' be able to import a - # an ed25519 public key? I think it should not, but it is: - import_ecdsa_publickey_from_file(self.path_ed25519 + ".pub") - self.assertTrue(ECDSAKEY_SCHEMA.matches(pub)) - - # TEST: Generation errors - for idx, (kwargs, err_msg) in enumerate( - [ - # Error on empty password - ( - {"password": ""}, - "encryption password must be 1 or more characters long", - ), - # Error on 'password' and 'prompt=True' - ( - {"password": pw, "prompt": True}, - "passing 'password' and 'prompt=True' is not allowed", - ), - ] - ): - with self.assertRaises( - ValueError, - msg="(row {})".format( # pylint: disable=consider-using-f-string - idx - ), - ) as ctx: - _generate_and_write_ecdsa_keypair(**kwargs) - - self.assertEqual( - err_msg, - str(ctx.exception), - "expected: '{}' got: '{}' (row {})".format( # pylint: disable=consider-using-f-string - err_msg, ctx.exception, idx - ), - ) - - # Error on bad argument format - for idx, kwargs in enumerate( - [ - {"filepath": 123456}, # Not a string - {"password": 123456}, # Not a string - {"prompt": "not-a-bool"}, - ] - ): - with self.assertRaises( - FormatError, - msg="(row {})".format( # pylint: disable=consider-using-f-string - idx - ), - ): - _generate_and_write_ecdsa_keypair(**kwargs) - - # TEST: Import errors - - # Error on public key import... - for idx, (fn, err_msg) in enumerate( - [ - # Error on invalid json (custom key format) - (fn_encrypted, "Cannot deserialize to a Python object"), - # Error on invalid custom key format - (self.path_no_key, "Missing key"), - ] - ): - with self.assertRaises( - Error, - msg="(row {})".format( # pylint: disable=consider-using-f-string - idx - ), - ) as ctx: - import_ecdsa_publickey_from_file(fn) - - self.assertTrue( - err_msg in str(ctx.exception), - "expected: '{}' got: '{}' (row {})".format( # pylint: disable=consider-using-f-string - err_msg, ctx.exception, idx - ), - ) - - # Error on private key import... - for idx, (args, kwargs, err, err_msg) in enumerate( - [ - # Error on not an ecdsa private key - ( - [self.path_ed25519], - {}, - Error, - "Cannot deserialize to a Python object", - ), - # Error on not encrypted - ( - [fn_default], - {"password": pw}, - CryptoError, - "Invalid encrypted file.", - ), - # Error on encrypted but no pw - ( - [fn_encrypted], - {}, - Error, - "Cannot deserialize to a Python object", - ), - # Error on encrypted but empty pw - ( - [fn_encrypted], - {"password": ""}, - CryptoError, - "Decryption failed.", - ), - # Error on encrypted but bad pw passed - ( - [fn_encrypted], - {"password": "bad pw"}, - CryptoError, - "Decryption failed.", - ), - # Error on pw and prompt - ( - [fn_default], - {"password": pw, "prompt": True}, - ValueError, - "passing 'password' and 'prompt=True' is not allowed", - ), - ] - ): - with self.assertRaises( - err, - msg="(row {})".format( # pylint: disable=consider-using-f-string - idx - ), - ) as ctx: - import_ecdsa_privatekey_from_file(*args, **kwargs) - - self.assertTrue( - err_msg in str(ctx.exception), - "expected: '{}' got: '{}' (row {})".format( # pylint: disable=consider-using-f-string - err_msg, ctx.exception, idx - ), - ) - - # Error on encrypted but bad pw prompted - err_msg = "Decryption failed" - with self.assertRaises(CryptoError) as ctx, mock.patch( - "securesystemslib.interface.get_password", return_value="bad_pw" - ): - import_ecdsa_privatekey_from_file(fn_encrypted, prompt=True) - - self.assertTrue( - err_msg in str(ctx.exception), - "expected: '{}' got: '{}'".format( # pylint: disable=consider-using-f-string - err_msg, ctx.exception - ), - ) - - # Error on bad path format - with self.assertRaises(FormatError): - import_ecdsa_publickey_from_file(123456) - with self.assertRaises(FormatError): - import_ecdsa_privatekey_from_file(123456) - - # Error on bad password format - with self.assertRaises(FormatError): # bad password - import_ecdsa_privatekey_from_file(fn_default, password=123456) - - # Error on bad prompt format - with self.assertRaises(FormatError): - import_ecdsa_privatekey_from_file(fn_default, prompt="not-a-bool") - - def test_generate_keypair_wrappers(self): - """Basic tests for thin wrappers around _generate_and_write_*_keypair. - See 'test_rsa', 'test_ed25519' and 'test_ecdsa' for more thorough key - generation tests for each key type. - - """ - key_pw = "pw" - expected_priv_mode = stat.S_IFREG | stat.S_IRUSR | stat.S_IWUSR - if os.name == "nt": - expected_priv_mode = ( - stat.S_IFREG - | stat.S_IWUSR - | stat.S_IRUSR - | stat.S_IWGRP - | stat.S_IRGRP - | stat.S_IWOTH - | stat.S_IROTH - ) - - for idx, (gen, gen_prompt, gen_plain, import_priv, schema) in enumerate( - [ - ( - generate_and_write_rsa_keypair, - generate_and_write_rsa_keypair_with_prompt, - generate_and_write_unencrypted_rsa_keypair, - import_rsa_privatekey_from_file, - RSAKEY_SCHEMA, - ), - ( - generate_and_write_ed25519_keypair, - generate_and_write_ed25519_keypair_with_prompt, - generate_and_write_unencrypted_ed25519_keypair, - import_ed25519_privatekey_from_file, - ED25519KEY_SCHEMA, - ), - ( - generate_and_write_ecdsa_keypair, - generate_and_write_ecdsa_keypair_with_prompt, - generate_and_write_unencrypted_ecdsa_keypair, - import_ecdsa_privatekey_from_file, - ECDSAKEY_SCHEMA, - ), - ] - ): - assert_msg = ( - "(row {})".format( # pylint: disable=consider-using-f-string - idx - ) - ) - # Test generate_and_write_*_keypair creates an encrypted private key - fn_encrypted = gen(key_pw) - priv = import_priv(fn_encrypted, key_pw) - self.assertTrue(schema.matches(priv), assert_msg) - - # Test that encrypted private key is generated with read and write - # permissions for user only - self.assertEqual(os.stat(fn_encrypted).st_mode, expected_priv_mode) - - # Test generate_and_write_*_keypair errors if password is None or empty - with self.assertRaises(FormatError, msg=assert_msg): - fn_encrypted = gen(None) - with self.assertRaises(ValueError, msg=assert_msg): - fn_encrypted = gen("") - - # Test generate_and_write_*_keypair_with_prompt creates encrypted private - # key - with mock.patch( - "securesystemslib.interface.get_password", return_value=key_pw - ): - fn_prompt = gen_prompt() - priv = import_priv(fn_prompt, key_pw) - self.assertTrue(schema.matches(priv), assert_msg) - - # Test generate_and_write_*_keypair_with_prompt creates unencrypted - # private key if no password is entered - with mock.patch( - "securesystemslib.interface.get_password", return_value="" - ): - fn_empty_prompt = gen_prompt() - priv = import_priv(fn_empty_prompt) - self.assertTrue(schema.matches(priv), assert_msg) - - # Test generate_and_write_unencrypted_*_keypair doesn't encrypt - fn_unencrypted = gen_plain() - priv = import_priv(fn_unencrypted) - self.assertTrue(schema.matches(priv), assert_msg) - - # Test that unencrypted private key is generated with read and write - # permissions for user only - self.assertEqual( - os.stat(fn_unencrypted).st_mode, expected_priv_mode - ) - - def test_import_publickeys_from_file(self): - """Test import multiple public keys with different types.""" - - # Successfully import key dict with one key per supported key type - key_dict = import_publickeys_from_file( - [ - self.path_rsa + ".pub", - self.path_ed25519 + ".pub", - self.path_ecdsa + ".pub", - ], - [KEY_TYPE_RSA, KEY_TYPE_ED25519, KEY_TYPE_ECDSA], - ) - - ANY_PUBKEY_DICT_SCHEMA.check_match(key_dict) - self.assertListEqual( - sorted([key["keytype"] for key in key_dict.values()]), - sorted([KEY_TYPE_RSA, KEY_TYPE_ED25519, KEY_TYPE_ECDSA]), - ) - - # Successfully import default rsa key - key_dict = import_publickeys_from_file([self.path_rsa + ".pub"]) - ANY_PUBKEY_DICT_SCHEMA.check_match(key_dict) - RSAKEY_SCHEMA.check_match(list(key_dict.values()).pop()) - - # Bad default rsa key type for ed25519 - with self.assertRaises(Error): - import_publickeys_from_file([self.path_ed25519 + ".pub"]) - - # Bad ed25519 key type for rsa key - with self.assertRaises(Error): - import_publickeys_from_file( - [self.path_rsa + ".pub"], [KEY_TYPE_ED25519] - ) - - # Unsupported key type - with self.assertRaises(FormatError): - import_publickeys_from_file( - [self.path_ed25519 + ".pub"], ["KEY_TYPE_UNSUPPORTED"] - ) - - # Mismatching arguments lists lenghts - with self.assertRaises(FormatError): - import_publickeys_from_file( - [self.path_rsa + ".pub", self.path_ed25519 + ".pub"], - [KEY_TYPE_ED25519], - ) - - def test_import_privatekey_from_file(self): - """Test generic private key import function.""" - - pw = "password" - for idx, (path, key_type, key_schema) in enumerate( - [ - (self.path_rsa, None, RSAKEY_SCHEMA), # default key type - (self.path_rsa, KEY_TYPE_RSA, RSAKEY_SCHEMA), - (self.path_ed25519, KEY_TYPE_ED25519, ED25519KEY_SCHEMA), - (self.path_ecdsa, KEY_TYPE_ECDSA, ECDSAKEY_SCHEMA), - ] - ): - # Successfully import key per supported type, with ... - # ... passed password - key = import_privatekey_from_file( - path, key_type=key_type, password=pw - ) - self.assertTrue( - key_schema.matches(key), - "(row {})".format( # pylint: disable=consider-using-f-string - idx - ), - ) - - # ... entered password on mock-prompt - with mock.patch( - "securesystemslib.interface.get_password", return_value=pw - ): - key = import_privatekey_from_file( - path, key_type=key_type, prompt=True - ) - self.assertTrue( - key_schema.matches(key), - "(row {})".format( # pylint: disable=consider-using-f-string - idx - ), - ) - - # Error on wrong key for default key type - with self.assertRaises(Error): - import_privatekey_from_file(self.path_ed25519, password=pw) - - # Error on unsupported key type - with self.assertRaises(FormatError): - import_privatekey_from_file( - self.path_rsa, key_type="KEY_TYPE_UNSUPPORTED", password=pw - ) - - -# Run the test cases. -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_keys.py b/tests/test_keys.py deleted file mode 100755 index 731c73b4..00000000 --- a/tests/test_keys.py +++ /dev/null @@ -1,933 +0,0 @@ -""" - - test_keys.py - - - Vladimir Diaz - - - October 10, 2013. - - - See LICENSE for licensing information. - - - Test cases for test_keys.py. -""" - -import copy -import unittest - -import securesystemslib.ecdsa_keys -import securesystemslib.exceptions -import securesystemslib.formats -import securesystemslib.keys - -KEYS = securesystemslib.keys -FORMAT_ERROR_MSG = ( - "securesystemslib.exceptions.FormatError was raised!" - + " Check object's format." -) -DATA_STR = "SOME DATA REQUIRING AUTHENTICITY." -DATA = securesystemslib.formats.encode_canonical(DATA_STR).encode("utf-8") - - -class TestKeys(unittest.TestCase): # pylint: disable=missing-class-docstring - @classmethod - def setUpClass(cls): - cls.rsakey_dict = KEYS.generate_rsa_key() - cls.ed25519key_dict = KEYS.generate_ed25519_key() - cls.ecdsakey_dict = KEYS.generate_ecdsa_key() - - def test_generate_rsa_key(self): - _rsakey_dict = KEYS.generate_rsa_key() # pylint: disable=invalid-name - - # Check if the format of the object returned by generate() corresponds - # to RSAKEY_SCHEMA format. - self.assertEqual( - None, - securesystemslib.formats.RSAKEY_SCHEMA.check_match(_rsakey_dict), - FORMAT_ERROR_MSG, - ) - - # Passing a bit value that is <2048 to generate() - should raise - # 'securesystemslib.exceptions.FormatError'. - self.assertRaises( - securesystemslib.exceptions.FormatError, KEYS.generate_rsa_key, 555 - ) - - # Passing a string instead of integer for a bit value. - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.generate_rsa_key, - "bits", - ) - - # NOTE if random bit value >=2048 (not 4096) is passed generate(bits) - # does not raise any errors and returns a valid key. - self.assertTrue( - securesystemslib.formats.RSAKEY_SCHEMA.matches( - KEYS.generate_rsa_key(2048) - ) - ) - self.assertTrue( - securesystemslib.formats.RSAKEY_SCHEMA.matches( - KEYS.generate_rsa_key(4096) - ) - ) - - def test_generate_ecdsa_key(self): - _ecdsakey_dict = ( # pylint: disable=invalid-name - KEYS.generate_ecdsa_key() - ) - - # Check if the format of the object returned by generate_ecdsa_key() - # corresponds to ECDSAKEY_SCHEMA format. - self.assertEqual( - None, - securesystemslib.formats.ECDSAKEY_SCHEMA.check_match( - _ecdsakey_dict - ), - FORMAT_ERROR_MSG, - ) - - # Passing an invalid algorithm to generate() should raise - # 'securesystemslib.exceptions.FormatError'. - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.generate_rsa_key, - "bad_algorithm", - ) - - # Passing a string instead of integer for a bit value. - self.assertRaises( - securesystemslib.exceptions.FormatError, KEYS.generate_rsa_key, 123 - ) - - def test_format_keyval_to_metadata(self): - keyvalue = self.rsakey_dict["keyval"] - keytype = self.rsakey_dict["keytype"] - scheme = self.rsakey_dict["scheme"] - - key_meta = KEYS.format_keyval_to_metadata(keytype, scheme, keyvalue) - - # Check if the format of the object returned by this function corresponds - # to KEY_SCHEMA format. - self.assertEqual( - None, - securesystemslib.formats.KEY_SCHEMA.check_match(key_meta), - FORMAT_ERROR_MSG, - ) - key_meta = KEYS.format_keyval_to_metadata( - keytype, scheme, keyvalue, private=True - ) - - # Check if the format of the object returned by this function corresponds - # to KEY_SCHEMA format. - self.assertEqual( - None, - securesystemslib.formats.KEY_SCHEMA.check_match(key_meta), - FORMAT_ERROR_MSG, - ) - - # Supplying a 'bad' keyvalue. - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.format_keyval_to_metadata, - "bad_keytype", - scheme, - keyvalue, - private=True, - ) - - # Test for missing 'public' entry. - public = keyvalue["public"] - del keyvalue["public"] - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.format_keyval_to_metadata, - keytype, - scheme, - keyvalue, - ) - keyvalue["public"] = public - - # Test for missing 'private' entry. - private = keyvalue["private"] - del keyvalue["private"] - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.format_keyval_to_metadata, - keytype, - scheme, - keyvalue, - private=True, - ) - keyvalue["private"] = private - - def test_format_metadata_to_key(self): - # Copying self.rsakey_dict so that rsakey_dict remains - # unchanged during and after this test execution. - test_rsakey_dict = copy.copy(self.rsakey_dict) - del test_rsakey_dict["keyid"] - - # Call format_metadata_to_key by using the default value for keyid_hash_algorithms - rsakey_formatted, _ = KEYS.format_metadata_to_key(test_rsakey_dict) - - # Check if the format of the object returned by calling this function with - # default hash algorithms e.g. securesystemslib.settings.HASH_ALGORITHMS corresponds - # to RSAKEY_SCHEMA format. - self.assertTrue( - securesystemslib.formats.RSAKEY_SCHEMA.matches(rsakey_formatted), - FORMAT_ERROR_MSG, - ) - - self.assertTrue( - securesystemslib.formats.KEY_SCHEMA.matches(rsakey_formatted), - FORMAT_ERROR_MSG, - ) - - # Call format_metadata_to_key by using custom value for keyid_hash_algorithms - rsakey_dict_from_meta_custom, _ = KEYS.format_metadata_to_key( - test_rsakey_dict, keyid_hash_algorithms=["sha384"] - ) - - # Check if the format of the object returned by calling this function with - # custom hash algorithms corresponds to RSAKEY_SCHEMA format. - self.assertTrue( - securesystemslib.formats.RSAKEY_SCHEMA.matches( - rsakey_dict_from_meta_custom - ), - FORMAT_ERROR_MSG, - ) - - self.assertTrue( - securesystemslib.formats.KEY_SCHEMA.matches( - rsakey_dict_from_meta_custom - ), - FORMAT_ERROR_MSG, - ) - - test_rsakey_dict["keyid"] = self.rsakey_dict["keyid"] - - # Supplying a wrong number of arguments. - self.assertRaises(TypeError, KEYS.format_metadata_to_key) - args = (test_rsakey_dict, test_rsakey_dict) - self.assertRaises(TypeError, KEYS.format_metadata_to_key, *args) - - # Supplying a malformed argument to the function - should get FormatError - del test_rsakey_dict["keyval"] - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.format_metadata_to_key, - test_rsakey_dict, - ) - - def test_helper_get_keyid(self): - keytype = self.rsakey_dict["keytype"] - keyvalue = self.rsakey_dict["keyval"] - scheme = self.rsakey_dict["scheme"] - - # Check format of 'keytype'. - self.assertEqual( - None, - securesystemslib.formats.KEYTYPE_SCHEMA.check_match(keytype), - FORMAT_ERROR_MSG, - ) - - # Check format of 'keyvalue'. - self.assertEqual( - None, - securesystemslib.formats.KEYVAL_SCHEMA.check_match(keyvalue), - FORMAT_ERROR_MSG, - ) - - # Check format of 'scheme'. - self.assertEqual( - None, - securesystemslib.formats.RSA_SCHEME_SCHEMA.check_match(scheme), - FORMAT_ERROR_MSG, - ) - - keyid = KEYS._get_keyid( # pylint: disable=protected-access - keytype, scheme, keyvalue - ) - - # Check format of 'keyid' - the output of '_get_keyid()' function. - self.assertEqual( - None, - securesystemslib.formats.KEYID_SCHEMA.check_match(keyid), - FORMAT_ERROR_MSG, - ) - - def test_create_signature(self): - # Creating a signature for 'DATA'. - rsa_signature = KEYS.create_signature(self.rsakey_dict, DATA) - ed25519_signature = KEYS.create_signature(self.ed25519key_dict, DATA) - - # Check format of output. - self.assertEqual( - None, - securesystemslib.formats.SIGNATURE_SCHEMA.check_match( - rsa_signature - ), - FORMAT_ERROR_MSG, - ) - self.assertEqual( - None, - securesystemslib.formats.SIGNATURE_SCHEMA.check_match( - ed25519_signature - ), - FORMAT_ERROR_MSG, - ) - - # Test for invalid signature scheme. - args = (self.rsakey_dict, DATA) - - valid_scheme = self.rsakey_dict["scheme"] - self.rsakey_dict["scheme"] = "invalid_scheme" - self.assertRaises( - securesystemslib.exceptions.UnsupportedAlgorithmError, - KEYS.create_signature, - *args, - ) - self.rsakey_dict["scheme"] = valid_scheme - - # Removing private key from 'rsakey_dict' - should raise a TypeError. - private = self.rsakey_dict["keyval"]["private"] - self.rsakey_dict["keyval"]["private"] = "" - - self.assertRaises(ValueError, KEYS.create_signature, *args) - - # Supplying an incorrect number of arguments. - self.assertRaises(TypeError, KEYS.create_signature) - self.rsakey_dict["keyval"]["private"] = private - - # Test generation of ECDSA signatures. - - # Creating a signature for 'DATA'. - ecdsa_signature = KEYS.create_signature(self.ecdsakey_dict, DATA) - - # Check format of output. - self.assertEqual( - None, - securesystemslib.formats.SIGNATURE_SCHEMA.check_match( - ecdsa_signature - ), - FORMAT_ERROR_MSG, - ) - - # Removing private key from 'ecdsakey_dict' - should raise a TypeError. - private = self.ecdsakey_dict["keyval"]["private"] - self.ecdsakey_dict["keyval"]["private"] = "" - - args = (self.ecdsakey_dict, DATA) - self.assertRaises(ValueError, KEYS.create_signature, *args) - - # Supplying an incorrect number of arguments. - self.assertRaises(TypeError, KEYS.create_signature) - self.ecdsakey_dict["keyval"]["private"] = private - - def test_verify_signature(self): # pylint: disable=too-many-statements - # Creating a signature of 'DATA' to be verified. - rsa_signature = KEYS.create_signature(self.rsakey_dict, DATA) - ed25519_signature = KEYS.create_signature(self.ed25519key_dict, DATA) - ecdsa_signature = KEYS.create_signature(self.ecdsakey_dict, DATA) - - # Verifying the 'signature' of 'DATA'. - verified = KEYS.verify_signature(self.rsakey_dict, rsa_signature, DATA) - self.assertTrue(verified, "Incorrect signature.") - - # Verifying the 'ed25519_signature' of 'DATA'. - verified = KEYS.verify_signature( - self.ed25519key_dict, ed25519_signature, DATA - ) - self.assertTrue(verified, "Incorrect signature.") - - # Verify that an invalid ed25519 signature scheme is rejected. - valid_scheme = self.ed25519key_dict["scheme"] - self.ed25519key_dict["scheme"] = "invalid_scheme" - self.assertRaises( - securesystemslib.exceptions.UnsupportedAlgorithmError, - KEYS.verify_signature, - self.ed25519key_dict, - ed25519_signature, - DATA, - ) - self.ed25519key_dict["scheme"] = valid_scheme - - # Verifying the 'ecdsa_signature' of 'DATA'. - verified = KEYS.verify_signature( - self.ecdsakey_dict, ecdsa_signature, DATA - ) - self.assertTrue(verified, "Incorrect signature.") - - # Verifying the 'ecdsa_signature' of 'DATA' with an old-style key dict - old_key_dict = self.ecdsakey_dict.copy() - old_key_dict["keytype"] = "ecdsa-sha2-nistp256" - verified = KEYS.verify_signature(old_key_dict, ecdsa_signature, DATA) - self.assertTrue(verified, "Incorrect signature.") - - # Test for an invalid ecdsa signature scheme. - valid_scheme = self.ecdsakey_dict["scheme"] - self.ecdsakey_dict["scheme"] = "invalid_scheme" - self.assertRaises( - securesystemslib.exceptions.UnsupportedAlgorithmError, - KEYS.verify_signature, - self.ecdsakey_dict, - ecdsa_signature, - DATA, - ) - self.ecdsakey_dict["scheme"] = valid_scheme - - # Testing invalid signatures. Same signature is passed, with 'DATA' being - # different than the original 'DATA' that was used in creating the - # 'rsa_signature'. Function should return 'False'. - - # Modifying 'DATA'. - _DATA_STR = "1111" + DATA_STR + "1111" # pylint: disable=invalid-name - _DATA = securesystemslib.formats.encode_canonical( # pylint: disable=invalid-name - _DATA_STR - ).encode( - "utf-8" - ) - - # Verifying the 'signature' of modified '_DATA'. - verified = KEYS.verify_signature(self.rsakey_dict, rsa_signature, _DATA) - self.assertFalse(verified, "Returned 'True' on an incorrect signature.") - - verified = KEYS.verify_signature( - self.ed25519key_dict, ed25519_signature, _DATA - ) - self.assertFalse(verified, "Returned 'True' on an incorrect signature.") - - verified = KEYS.verify_signature( - self.ecdsakey_dict, ecdsa_signature, _DATA - ) - self.assertFalse(verified, "Returned 'True' on an incorrect signature.") - - # Modifying 'rsakey_dict' to pass an incorrect scheme. - valid_scheme = self.rsakey_dict["scheme"] - self.rsakey_dict["scheme"] = "Biff" - - args = (self.rsakey_dict, rsa_signature, DATA) - self.assertRaises( - securesystemslib.exceptions.UnsupportedAlgorithmError, - KEYS.verify_signature, - *args, - ) - - # Restore - self.rsakey_dict["scheme"] = valid_scheme - - # Verify that the KEYIDS of 'key_dict' and 'signature' match. - valid_keyid = self.rsakey_dict["keyid"] = "12345" - self.rsakey_dict["keyid"] = "bad123" - - self.assertRaises( - securesystemslib.exceptions.CryptoError, - KEYS.verify_signature, - self.rsakey_dict, - rsa_signature, - DATA, - ) - self.rsakey_dict["keyid"] = valid_keyid - - # Passing incorrect number of arguments. - self.assertRaises(TypeError, KEYS.verify_signature) - - # Verify that the pure python 'ed25519' base case (triggered if 'pynacl' - # is unavailable) is executed in securesystemslib.keys.verify_signature(). - KEYS._ED25519_CRYPTO_LIBRARY = ( # pylint: disable=protected-access - "invalid" - ) - KEYS._available_crypto_libraries = [ # pylint: disable=protected-access - "invalid" - ] - verified = KEYS.verify_signature( - self.ed25519key_dict, ed25519_signature, DATA - ) - self.assertTrue(verified, "Incorrect signature.") - - # Verify ecdsa key with HEX encoded keyval instead of PEM encoded keyval - ecdsa_key = KEYS.generate_ecdsa_key() - ecdsa_key["keyval"]["public"] = "abcd" - # sig is not important as long as keyid is the same as the one in ecdsa_key - sig = {"keyid": ecdsa_key["keyid"], "sig": "bb"} - with self.assertRaises(securesystemslib.exceptions.FormatError): - KEYS.verify_signature(ecdsa_key, sig, b"data") - - # Verify ed25519 key with PEM encoded keyval instead of HEX encoded keyval - ed25519 = KEYS.generate_ed25519_key() - ed25519["keyval"][ - "public" - ] = "-----BEGIN PUBLIC KEY-----\nfoo\n-----END PUBLIC KEY-----\n" - # sig is not important as long as keyid is the same as the one in ed25519 - sig = {"keyid": ed25519["keyid"], "sig": "bb"} - with self.assertRaises(securesystemslib.exceptions.FormatError): - KEYS.verify_signature(ed25519, sig, b"data") - - def test_create_rsa_encrypted_pem(self): - # Test valid arguments. - private = self.rsakey_dict["keyval"]["private"] - passphrase = "secret" - scheme = "rsassa-pss-sha256" - encrypted_pem = KEYS.create_rsa_encrypted_pem(private, passphrase) - self.assertTrue( - securesystemslib.formats.PEMRSA_SCHEMA.matches(encrypted_pem) - ) - self.assertTrue(KEYS.is_pem_private(encrypted_pem)) - - # Try to import the encrypted PEM file. - rsakey = KEYS.import_rsakey_from_private_pem( - encrypted_pem, scheme, passphrase - ) - self.assertTrue(securesystemslib.formats.RSAKEY_SCHEMA.matches(rsakey)) - - # Test improperly formatted arguments. - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.create_rsa_encrypted_pem, - 8, - passphrase, - ) - - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.create_rsa_encrypted_pem, - private, - 8, - ) - - def test_import_rsakey_from_private_pem(self): - # Try to import an rsakey from a valid PEM. - private_pem = self.rsakey_dict["keyval"]["private"] - _ = KEYS.import_rsakey_from_private_pem(private_pem) - - # Test for invalid arguments. - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.import_rsakey_from_private_pem, - 123, - ) - - def test_import_rsakey_from_public_pem(self): - # Try to import an rsakey from a public PEM. - pem = self.rsakey_dict["keyval"]["public"] - rsa_key = KEYS.import_rsakey_from_public_pem(pem) - - # Check if the format of the object returned by this function corresponds - # to 'securesystemslib.formats.RSAKEY_SCHEMA' format. - self.assertTrue(securesystemslib.formats.RSAKEY_SCHEMA.matches(rsa_key)) - - # Verify whitespace is stripped. - self.assertEqual( - rsa_key, KEYS.import_rsakey_from_public_pem(pem + "\n") - ) - - # Supplying a 'bad_pem' argument. - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.import_rsakey_from_public_pem, - "bad_pem", - ) - - # Supplying an improperly formatted PEM. - # Strip the PEM header and footer. - pem_header = "-----BEGIN PUBLIC KEY-----" - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.import_rsakey_from_public_pem, - pem[len(pem_header) :], - ) - - pem_footer = "-----END PUBLIC KEY-----" - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.import_rsakey_from_public_pem, - pem[: -len(pem_footer)], - ) - - # Test for invalid arguments. - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.import_rsakey_from_public_pem, - 123, - ) - - def test_import_rsakey_from_pem(self): - # Try to import an rsakey from a public PEM. - public_pem = self.rsakey_dict["keyval"]["public"] - private_pem = self.rsakey_dict["keyval"]["private"] - public_rsakey = KEYS.import_rsakey_from_pem(public_pem) - private_rsakey = KEYS.import_rsakey_from_pem(private_pem) - - # Check if the format of the object returned by this function corresponds - # to 'securesystemslib.formats.RSAKEY_SCHEMA' format. - self.assertTrue( - securesystemslib.formats.RSAKEY_SCHEMA.matches(public_rsakey) - ) - self.assertTrue( - securesystemslib.formats.RSAKEY_SCHEMA.matches(private_rsakey) - ) - - # Verify whitespace is stripped. - self.assertEqual( - public_rsakey, KEYS.import_rsakey_from_pem(public_pem + "\n") - ) - self.assertEqual( - private_rsakey, KEYS.import_rsakey_from_pem(private_pem + "\n") - ) - - # Supplying a 'bad_pem' argument. - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.import_rsakey_from_pem, - "bad_pem", - ) - - # Supplying an improperly formatted public PEM. - # Strip the PEM header and footer. - pem_header = "-----BEGIN PUBLIC KEY-----" - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.import_rsakey_from_pem, - public_pem[len(pem_header) :], - ) - - pem_footer = "-----END PUBLIC KEY-----" - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.import_rsakey_from_pem, - public_pem[: -len(pem_footer)], - ) - - # Supplying an improperly formatted private PEM. - # Strip the PEM header and footer. - pem_header = "-----BEGIN PRIVATE KEY-----" - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.import_rsakey_from_pem, - private_pem[len(pem_header) :], - ) - - pem_footer = "-----END PRIVATE KEY-----" - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.import_rsakey_from_pem, - private_pem[: -len(pem_footer)], - ) - - # Test for invalid arguments. - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.import_rsakey_from_pem, - 123, - ) - - def test_import_ecdsakey_from_private_pem(self): - # Try to import an ecdsakey from a valid PEM. - private_pem = self.ecdsakey_dict["keyval"]["private"] - _ = KEYS.import_ecdsakey_from_private_pem(private_pem) - - # Test for an encrypted PEM. - scheme = "ecdsa-sha2-nistp256" - encrypted_pem = securesystemslib.ecdsa_keys.create_ecdsa_encrypted_pem( - private_pem, "password" - ) - _ = KEYS.import_ecdsakey_from_private_pem( - encrypted_pem.decode("utf-8"), scheme, "password" - ) - - # Test for invalid arguments. - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.import_ecdsakey_from_private_pem, - 123, - ) - - def test_import_ecdsakey_from_public_pem(self): - # Try to import an ecdsakey from a public PEM. - pem = self.ecdsakey_dict["keyval"]["public"] - ecdsa_key = KEYS.import_ecdsakey_from_public_pem(pem) - - # Check if the format of the object returned by this function corresponds - # to 'securesystemslib.formats.ECDSAKEY_SCHEMA' format. - self.assertTrue( - securesystemslib.formats.ECDSAKEY_SCHEMA.matches(ecdsa_key) - ) - - # Verify whitespace is stripped. - self.assertEqual( - ecdsa_key, KEYS.import_ecdsakey_from_public_pem(pem + "\n") - ) - - # Supplying a 'bad_pem' argument. - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.import_ecdsakey_from_public_pem, - "bad_pem", - ) - - # Supplying an improperly formatted PEM. Strip the PEM header and footer. - pem_header = "-----BEGIN PUBLIC KEY-----" - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.import_ecdsakey_from_public_pem, - pem[len(pem_header) :], - ) - - pem_footer = "-----END PUBLIC KEY-----" - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.import_ecdsakey_from_public_pem, - pem[: -len(pem_footer)], - ) - - # Test for invalid arguments. - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.import_ecdsakey_from_public_pem, - 123, - ) - - def test_import_ecdsakey_from_pem(self): - # Try to import an ecdsakey from a public PEM. - public_pem = self.ecdsakey_dict["keyval"]["public"] - private_pem = self.ecdsakey_dict["keyval"]["private"] - public_ecdsakey = KEYS.import_ecdsakey_from_pem(public_pem) - private_ecdsakey = KEYS.import_ecdsakey_from_pem(private_pem) - - # Check if the format of the object returned by this function corresponds - # to 'securesystemslib.formats.ECDSAKEY_SCHEMA' format. - self.assertTrue( - securesystemslib.formats.ECDSAKEY_SCHEMA.matches(public_ecdsakey) - ) - self.assertTrue( - securesystemslib.formats.ECDSAKEY_SCHEMA.matches(private_ecdsakey) - ) - - # Verify whitespace is stripped. - self.assertEqual( - public_ecdsakey, KEYS.import_ecdsakey_from_pem(public_pem + "\n") - ) - self.assertEqual( - private_ecdsakey, KEYS.import_ecdsakey_from_pem(private_pem + "\n") - ) - - # Supplying a 'bad_pem' argument. - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.import_ecdsakey_from_pem, - "bad_pem", - ) - - # Supplying an improperly formatted public PEM. Strip the PEM header and - # footer. - pem_header = "-----BEGIN PUBLIC KEY-----" - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.import_ecdsakey_from_pem, - public_pem[len(pem_header) :], - ) - - pem_footer = "-----END PUBLIC KEY-----" - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.import_ecdsakey_from_pem, - public_pem[: -len(pem_footer)], - ) - - # Supplying an improperly formatted private PEM. Strip the PEM header and - # footer. - pem_header = "-----BEGIN EC PRIVATE KEY-----" - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.import_ecdsakey_from_pem, - private_pem[len(pem_header) :], - ) - - pem_footer = "-----END EC PRIVATE KEY-----" - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.import_ecdsakey_from_pem, - private_pem[: -len(pem_footer)], - ) - - # Test for invalid arguments. - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.import_ecdsakey_from_pem, - 123, - ) - - def test_decrypt_key(self): - # Test valid arguments. - passphrase = "secret" - encrypted_key = KEYS.encrypt_key(self.rsakey_dict, passphrase) - decrypted_key = KEYS.decrypt_key(encrypted_key, passphrase) - - self.assertTrue( - securesystemslib.formats.ANYKEY_SCHEMA.matches(decrypted_key) - ) - - # Test improperly formatted arguments. - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.decrypt_key, - 8, - passphrase, - ) - - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.decrypt_key, - encrypted_key, - 8, - ) - - def test_extract_pem(self): - # Normal case. - private_pem = KEYS.extract_pem( - self.rsakey_dict["keyval"]["private"], private_pem=True - ) - self.assertTrue( - securesystemslib.formats.PEMRSA_SCHEMA.matches(private_pem) - ) - - public_pem = KEYS.extract_pem( - self.rsakey_dict["keyval"]["public"], private_pem=False - ) - self.assertTrue( - securesystemslib.formats.PEMRSA_SCHEMA.matches(public_pem) - ) - - # Test encrypted private pem - encrypted_private_pem = KEYS.create_rsa_encrypted_pem(private_pem, "pw") - encrypted_private_pem_stripped = KEYS.extract_pem( - encrypted_private_pem, private_pem=True - ) - self.assertTrue( - securesystemslib.formats.PEMRSA_SCHEMA.matches( - encrypted_private_pem_stripped - ) - ) - - # Test for an invalid PEM. - pem_header = "-----BEGIN RSA PRIVATE KEY-----" - pem_footer = "-----END RSA PRIVATE KEY-----" - - private_header_start = private_pem.index(pem_header) - private_footer_start = private_pem.index( - pem_footer, private_header_start + len(pem_header) - ) - - private_missing_header = private_pem[ - private_header_start - + len(pem_header) : private_footer_start - + len(pem_footer) - ] - private_missing_footer = private_pem[ - private_header_start:private_footer_start - ] - - pem_header = "-----BEGIN PUBLIC KEY-----" - pem_footer = "-----END PUBLIC KEY-----" - - public_header_start = public_pem.index(pem_header) - public_footer_start = public_pem.index( - pem_footer, public_header_start + len(pem_header) - ) - - public_missing_header = public_pem[ - public_header_start - + len(pem_header) : public_footer_start - + len(pem_footer) - ] - public_missing_footer = public_pem[ - public_header_start:public_footer_start - ] - - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.extract_pem, - "invalid_pem", - private_pem=False, - ) - - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.extract_pem, - public_missing_header, - private_pem=False, - ) - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.extract_pem, - private_missing_header, - private_pem=True, - ) - - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.extract_pem, - public_missing_footer, - private_pem=False, - ) - - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.extract_pem, - private_missing_footer, - private_pem=True, - ) - - def test_is_pem_public(self): - # Test for a valid PEM string. - public_pem = self.rsakey_dict["keyval"]["public"] - self.assertTrue(KEYS.is_pem_public(public_pem)) - - # Test for a valid non-public PEM string. - private_pem = self.rsakey_dict["keyval"]["private"] - self.assertFalse(KEYS.is_pem_public(private_pem)) - - # Test for an invalid PEM string. - self.assertRaises( - securesystemslib.exceptions.FormatError, KEYS.is_pem_public, 123 - ) - - def test_is_pem_private(self): - # Test for a valid PEM string. - private_pem_rsa = self.rsakey_dict["keyval"]["private"] - private_pem_ec = self.ecdsakey_dict["keyval"]["private"] - encrypted_private_pem_rsa = KEYS.create_rsa_encrypted_pem( - private_pem_rsa, "pw" - ) - - self.assertTrue(KEYS.is_pem_private(private_pem_rsa)) - self.assertTrue(KEYS.is_pem_private(private_pem_ec, "ec")) - self.assertTrue(KEYS.is_pem_private(encrypted_private_pem_rsa)) - - # Test for a valid non-private PEM string. - public_pem = self.rsakey_dict["keyval"]["public"] - public_pem_ec = self.ecdsakey_dict["keyval"]["public"] - self.assertFalse(KEYS.is_pem_private(public_pem)) - self.assertFalse(KEYS.is_pem_private(public_pem_ec, "ec")) - - # Test for unsupported keytype. - self.assertRaises( - securesystemslib.exceptions.FormatError, - KEYS.is_pem_private, - private_pem_rsa, - "bad_keytype", - ) - - # Test for an invalid PEM string. - self.assertRaises( - securesystemslib.exceptions.FormatError, KEYS.is_pem_private, 123 - ) - - -# Run the unit tests. -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_rsa_keys.py b/tests/test_rsa_keys.py deleted file mode 100755 index 03750dd2..00000000 --- a/tests/test_rsa_keys.py +++ /dev/null @@ -1,458 +0,0 @@ -""" - - test_rsa_keys.py - - - Vladimir Diaz - - - June 3, 2015. - - - See LICENSE for licensing information. - - - Test cases for 'rsa_keys.py'. -""" - -import unittest - -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.asymmetric import padding -from cryptography.hazmat.primitives.serialization import load_pem_private_key - -import securesystemslib.exceptions -import securesystemslib.formats -import securesystemslib.hash -import securesystemslib.keys -import securesystemslib.rsa_keys - -( - public_rsa, - private_rsa, -) = securesystemslib.rsa_keys.generate_rsa_public_and_private() -FORMAT_ERROR_MSG = ( - "securesystemslib.exceptions.FormatError raised. Check object's format." -) - - -class TestRSA_keys( - unittest.TestCase -): # pylint: disable=missing-class-docstring,invalid-name - def setUp(self): - pass - - def test_generate_rsa_public_and_private(self): - pub, priv = securesystemslib.rsa_keys.generate_rsa_public_and_private() - - # Check format of 'pub' and 'priv'. - self.assertEqual( - None, - securesystemslib.formats.PEMRSA_SCHEMA.check_match(pub), - FORMAT_ERROR_MSG, - ) - self.assertEqual( - None, - securesystemslib.formats.PEMRSA_SCHEMA.check_match(priv), - FORMAT_ERROR_MSG, - ) - - # Check for an invalid "bits" argument. bits >= 2048. - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.rsa_keys.generate_rsa_public_and_private, - 1024, - ) - - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.rsa_keys.generate_rsa_public_and_private, - "2048", - ) - - def test_create_rsa_signature(self): - global private_rsa # pylint: disable=global-variable-not-assigned - global public_rsa # pylint: disable=global-variable-not-assigned - data = "The quick brown fox jumps over the lazy dog".encode("utf-8") - - for rsa_scheme in securesystemslib.keys.RSA_SIGNATURE_SCHEMES: - signature, scheme = securesystemslib.rsa_keys.create_rsa_signature( - private_rsa, data, rsa_scheme - ) - - # Verify format of returned values. - self.assertNotEqual(None, signature) - self.assertEqual( - None, - securesystemslib.formats.RSA_SCHEME_SCHEMA.check_match(scheme), - FORMAT_ERROR_MSG, - ) - self.assertEqual(rsa_scheme, scheme) - - # Check for improperly formatted arguments. - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.rsa_keys.create_rsa_signature, - 123, - data, - ) - - # Check for an unset private key. - self.assertRaises( - ValueError, - securesystemslib.rsa_keys.create_rsa_signature, - "", - data, - ) - - # Check for an invalid PEM. - self.assertRaises( - securesystemslib.exceptions.CryptoError, - securesystemslib.rsa_keys.create_rsa_signature, - "123", - data, - ) - - # Check for invalid 'data'. - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.rsa_keys.create_rsa_signature, - private_rsa, - "", - ) - - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.rsa_keys.create_rsa_signature, - private_rsa, - 123, - ) - - # Check for a missing private key. - self.assertRaises( - securesystemslib.exceptions.CryptoError, - securesystemslib.rsa_keys.create_rsa_signature, - public_rsa, - data, - ) - - # Check for a TypeError by attempting to create a signature with an - # encrypted key. - encrypted_pem = securesystemslib.rsa_keys.create_rsa_encrypted_pem( - private_rsa, "pw" - ) - self.assertRaises( - securesystemslib.exceptions.CryptoError, - securesystemslib.rsa_keys.create_rsa_signature, - encrypted_pem, - data, - ) - - def test_verify_rsa_signature(self): - global public_rsa # pylint: disable=global-variable-not-assigned - global private_rsa # pylint: disable=global-variable-not-assigned - data = "The quick brown fox jumps over the lazy dog".encode("utf-8") - - for rsa_scheme in securesystemslib.keys.RSA_SIGNATURE_SCHEMES: - signature, scheme = securesystemslib.rsa_keys.create_rsa_signature( - private_rsa, data, rsa_scheme - ) - - valid_signature = securesystemslib.rsa_keys.verify_rsa_signature( - signature, scheme, public_rsa, data - ) - self.assertEqual(True, valid_signature) - - # Check for an invalid public key. - self.assertRaises( - securesystemslib.exceptions.CryptoError, - securesystemslib.rsa_keys.verify_rsa_signature, - signature, - scheme, - private_rsa, - data, - ) - - # Check for improperly formatted arguments. - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.rsa_keys.verify_rsa_signature, - signature, - 123, - public_rsa, - data, - ) - - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.rsa_keys.verify_rsa_signature, - signature, - scheme, - 123, - data, - ) - - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.rsa_keys.verify_rsa_signature, - 123, - scheme, - public_rsa, - data, - ) - - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.rsa_keys.verify_rsa_signature, - signature, - "invalid_scheme", - public_rsa, - data, - ) - - # Check for invalid 'signature' and 'data' arguments. - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.rsa_keys.verify_rsa_signature, - signature, - scheme, - public_rsa, - 123, - ) - - self.assertEqual( - False, - securesystemslib.rsa_keys.verify_rsa_signature( - signature, scheme, public_rsa, b"mismatched data" - ), - ) - - ( - mismatched_signature, - scheme, - ) = securesystemslib.rsa_keys.create_rsa_signature( - private_rsa, b"mismatched data" - ) - - self.assertEqual( - False, - securesystemslib.rsa_keys.verify_rsa_signature( - mismatched_signature, scheme, public_rsa, data - ), - ) - - def test_verify_rsa_pss_different_salt_lengths(self): - rsa_scheme = "rsassa-pss-sha256" - data = "The ancients say, salt length does not matter that much".encode( - "utf-8" - ) - - private_key = load_pem_private_key( - private_rsa.encode("utf-8"), - password=None, - backend=default_backend(), - ) - digest = securesystemslib.hash.digest_from_rsa_scheme( - rsa_scheme, "pyca_crypto" - ) - - # Make sure digest size and max salt length are not accidentally the same - self.assertNotEqual( - digest.algorithm.digest_size, - padding.calculate_max_pss_salt_length( - private_key, digest.algorithm - ), - ) - - # Sign with max salt length (briefly available in sslib v0.24.0): - max_salt_sig = private_key.sign( - data, - padding.PSS( - mgf=padding.MGF1(digest.algorithm), - salt_length=padding.PSS.MAX_LENGTH, - ), - digest.algorithm, - ) - - # Sign with salt length == digest length - fix_salt_sig, _ = securesystemslib.rsa_keys.create_rsa_signature( - private_rsa, data - ) - - # Verification infers salt length automatically and so works for both - for signature in (max_salt_sig, fix_salt_sig): - verified = securesystemslib.rsa_keys.verify_rsa_signature( - signature, rsa_scheme, public_rsa, data - ) - self.assertTrue(verified) - - def test_create_rsa_encrypted_pem(self): - global public_rsa # pylint: disable=global-variable-not-assigned - global private_rsa # pylint: disable=global-variable-not-assigned - - encrypted_pem = securesystemslib.rsa_keys.create_rsa_encrypted_pem( - private_rsa, "password" - ) - self.assertTrue( - securesystemslib.formats.PEMRSA_SCHEMA.matches(encrypted_pem) - ) - - # Test for invalid private key (via PEM). - self.assertRaises( - securesystemslib.exceptions.CryptoError, - securesystemslib.rsa_keys.create_rsa_encrypted_pem, - public_rsa, - "password", - ) - - # Test for invalid arguments. - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.rsa_keys.create_rsa_encrypted_pem, - public_rsa, - 123, - ) - - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.rsa_keys.create_rsa_encrypted_pem, - 123, - "password", - ) - - self.assertRaises( - ValueError, - securesystemslib.rsa_keys.create_rsa_encrypted_pem, - "", - "password", - ) - - def test_create_rsa_public_and_private_from_pem(self): - global public_rsa # pylint: disable=global-variable-not-assigned - global private_rsa # pylint: disable=global-variable-not-assigned - - ( - public, - private, - ) = securesystemslib.rsa_keys.create_rsa_public_and_private_from_pem( - private_rsa - ) - - self.assertTrue(securesystemslib.formats.PEMRSA_SCHEMA.matches(public)) - self.assertTrue(securesystemslib.formats.PEMRSA_SCHEMA.matches(private)) - - self.assertRaises( - securesystemslib.exceptions.CryptoError, - securesystemslib.rsa_keys.create_rsa_public_and_private_from_pem, - public_rsa, - ) - - def test_encrypt_key(self): - global public_rsa # pylint: disable=global-variable-not-assigned - global private_rsa # pylint: disable=global-variable-not-assigned - - key_object = { - "keytype": "rsa", - "scheme": "rsassa-pss-sha256", - "keyid": "1223", - "keyval": {"public": public_rsa, "private": private_rsa}, - } - - encrypted_key = securesystemslib.rsa_keys.encrypt_key( - key_object, "password" - ) - self.assertTrue( - securesystemslib.formats.ENCRYPTEDKEY_SCHEMA.matches(encrypted_key) - ) - - key_object["keyval"]["private"] = "" - self.assertRaises( - securesystemslib.exceptions.FormatError, - securesystemslib.rsa_keys.encrypt_key, - key_object, - "password", - ) - - def test_decrypt_key(self): - # Test for valid arguments. - global public_rsa # pylint: disable=global-variable-not-assigned - global private_rsa # pylint: disable=global-variable-not-assigned - passphrase = "pw" - - rsa_key = { - "keytype": "rsa", - "scheme": "rsassa-pss-sha256", - "keyid": "d62247f817883f593cf6c66a5a55292488d457bcf638ae03207dbbba9dbe457d", - "keyval": {"public": public_rsa, "private": private_rsa}, - } - - encrypted_rsa_key = securesystemslib.rsa_keys.encrypt_key( - rsa_key, passphrase - ) - - _ = securesystemslib.rsa_keys.decrypt_key(encrypted_rsa_key, passphrase) - - # Test for invalid arguments. - self.assertRaises( - securesystemslib.exceptions.CryptoError, - securesystemslib.rsa_keys.decrypt_key, - "bad", - passphrase, - ) - - # Test for invalid encrypted content (i.e., invalid hmac and ciphertext.) - encryption_delimiter = ( - securesystemslib.rsa_keys._ENCRYPTION_DELIMITER # pylint: disable=protected-access - ) - salt, iterations, hmac, iv, ciphertext = encrypted_rsa_key.split( - encryption_delimiter - ) - - # Set an invalid hmac. The decryption routine sould raise a - # securesystemslib.exceptions.CryptoError exception because 'hmac' does not - # match the hmac calculated by the decryption routine. - bad_hmac = "12345abcd" - invalid_encrypted_rsa_key = ( - salt - + encryption_delimiter - + iterations - + encryption_delimiter - + bad_hmac - + encryption_delimiter - + iv - + encryption_delimiter - + ciphertext - ) - - self.assertRaises( - securesystemslib.exceptions.CryptoError, - securesystemslib.rsa_keys.decrypt_key, - invalid_encrypted_rsa_key, - passphrase, - ) - - # Test for invalid 'ciphertext' - bad_ciphertext = "12345abcde" - invalid_encrypted_rsa_key = ( - salt - + encryption_delimiter - + iterations - + encryption_delimiter - + hmac - + encryption_delimiter - + iv - + encryption_delimiter - + bad_ciphertext - ) - - self.assertRaises( - securesystemslib.exceptions.CryptoError, - securesystemslib.rsa_keys.decrypt_key, - invalid_encrypted_rsa_key, - passphrase, - ) - - -# Run the unit tests. -if __name__ == "__main__": - unittest.main() From 51bb41c1868b9425d18d9fe08489119f4b8c969d Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Tue, 9 Apr 2024 11:07:49 +0200 Subject: [PATCH 3/5] Remove obsolete custom exception classes With the removal of legacy key modules, some custom exception classes are no longer needed. Signed-off-by: Lukas Puehringer --- securesystemslib/exceptions.py | 83 ---------------------------------- tests/test_exceptions.py | 59 ------------------------ 2 files changed, 142 deletions(-) delete mode 100755 tests/test_exceptions.py diff --git a/securesystemslib/exceptions.py b/securesystemslib/exceptions.py index 7e4bc892..c1eb8914 100755 --- a/securesystemslib/exceptions.py +++ b/securesystemslib/exceptions.py @@ -22,93 +22,18 @@ class Error(Exception): """Indicate a generic error.""" -class Warning(Warning): # pylint: disable=redefined-builtin - """Generic warning. It is used by the 'warnings' module.""" - - class FormatError(Error): """Indicate an error while validating an object's format.""" -class InvalidMetadataJSONError(FormatError): - """Indicate that a metadata file is not valid JSON.""" - - def __init__(self, exception): # pylint: disable=super-init-not-called - # Store the original exception. - self.exception = exception - - def __str__(self): - # Show the original exception. - return repr(self.exception) - - class UnsupportedAlgorithmError(Error): """Indicate an error while trying to identify a user-specified algorithm.""" -class BadHashError(Error): - """Indicate an error while checking the value a hash object.""" - - def __init__( - self, expected_hash, observed_hash - ): # pylint: disable=super-init-not-called - self.expected_hash = expected_hash - self.observed_hash = observed_hash - - def __str__(self): - return ( - "Observed hash (" - + repr(self.observed_hash) - + ") != expected hash (" - + repr(self.expected_hash) - + ")" - ) - - -class BadPasswordError(Error): - """Indicate an error after encountering an invalid password.""" - - -class CryptoError(Error): - """Indicate any cryptography-related errors.""" - - -class BadSignatureError(CryptoError): - """Indicate that some metadata has a bad signature.""" - - def __init__( - self, metadata_role_name - ): # pylint: disable=super-init-not-called - self.metadata_role_name = metadata_role_name - - def __str__(self): - return repr(self.metadata_role_name) + " metadata has bad signature." - - -class UnknownMethodError(CryptoError): - """Indicate that a user-specified cryptograpthic method is unknown.""" - - class UnsupportedLibraryError(Error): """Indicate that a supported library could not be located or imported.""" -class InvalidNameError(Error): - """Indicate an error while trying to validate any type of named object.""" - - -class NotFoundError(Error): - """If a required configuration or resource is not found.""" - - -class URLMatchesNoPatternError(Error): - """If a URL does not match a user-specified regular expression.""" - - -class InvalidConfigurationError(Error): - """If a configuration object does not match the expected format.""" - - class StorageError(Error): """Indicate an error occured during interaction with an abstracted storage backend.""" @@ -121,11 +46,3 @@ class UnverifiedSignatureError(Error): class VerificationError(UnverifiedSignatureError): """Signature could not be verified because something failed in the process""" - - -class SerializationError(Error): - """Error during serialization.""" - - -class DeserializationError(Error): - """Error during deserialization.""" diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py deleted file mode 100755 index 3f3af413..00000000 --- a/tests/test_exceptions.py +++ /dev/null @@ -1,59 +0,0 @@ -""" - - test_exceptions.py - - - Vladimir Diaz - - - December 20, 2016. - - - See LICENSE for licensing information. - - - Test cases for exceptions.py (mainly the exceptions defined there). -""" - -import logging -import unittest - -import securesystemslib.exceptions - -logger = logging.getLogger(__name__) - - -class TestExceptions( - unittest.TestCase -): # pylint: disable=missing-class-docstring - def setUp(self): - pass - - def tearDown(self): - pass - - def test_bad_signature_error(self): - bad_signature_error = securesystemslib.exceptions.BadSignatureError( - "bad sig" - ) - logger.error(bad_signature_error) - - def test_bad_hash_error(self): - bad_hash_error = securesystemslib.exceptions.BadHashError( - "01234", "56789" - ) - logger.error(bad_hash_error) - - def test_invalid_metadata_json_error(self): - format_error = securesystemslib.exceptions.FormatError( - "Improperly formatted JSON" - ) - invalid_metadata_json_error = ( - securesystemslib.exceptions.InvalidMetadataJSONError(format_error) - ) - logger.error(invalid_metadata_json_error) - - -# Run the unit tests. -if __name__ == "__main__": - unittest.main() From 68046f18a3e7adebd416dd9eb19f99841699a296 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Tue, 9 Apr 2024 10:35:51 +0200 Subject: [PATCH 4/5] Remove obsolete settings module With the removal of legacy key modules, settings is no longer used or needed. fixes #219 Signed-off-by: Lukas Puehringer --- securesystemslib/settings.py | 37 ------------------------------------ tests/test_util.py | 1 - 2 files changed, 38 deletions(-) delete mode 100755 securesystemslib/settings.py diff --git a/securesystemslib/settings.py b/securesystemslib/settings.py deleted file mode 100755 index 0556e539..00000000 --- a/securesystemslib/settings.py +++ /dev/null @@ -1,37 +0,0 @@ -""" - - settings.py - - - Vladimir Diaz - - - December 7, 2016 - - - See LICENSE for licensing information. - - - Store all crypto-related settings used by securesystemslib. -""" - -# Set a directory that should be used for all temporary files. If this -# is None, then the system default will be used. The system default -# will also be used if a directory path set here is invalid or -# unusable. -temporary_directory = None - -# The current "good enough" number of PBKDF2 passphrase iterations. We -# recommend that important keys, such as root, be kept offline. -# 'toto.settings.PBKDF2_ITERATIONS' should increase as CPU speeds increase, set -# here at 100,000 iterations by default (in 2013). The repository maintainer -# may opt to modify the default setting according to their security needs and -# computational restrictions. A strong user password is still important. -# Modifying the number of iterations will result in a new derived key+PBDKF2 -# combination if the key is loaded and re-saved, overriding any previous -# iteration setting used in the old '' key file. -# https://en.wikipedia.org/wiki/PBKDF2 -PBKDF2_ITERATIONS = 100000 - -# The algorithm(s) in HASH_ALGORITHMS are used to generate key IDs. -HASH_ALGORITHMS = ["sha256", "sha512"] diff --git a/tests/test_util.py b/tests/test_util.py index d94fb9bb..e108b348 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -23,7 +23,6 @@ import unittest import securesystemslib.hash -import securesystemslib.settings import securesystemslib.util from securesystemslib import exceptions, unittest_toolbox From c39123eb1ac31a56bf42f2d28b5263f4735dc94b Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Tue, 9 Apr 2024 10:46:42 +0200 Subject: [PATCH 5/5] Remove obsolete pynacl optional dependency pynacl was used in legacy key modules, in addition to pyca/cryptography, for its ed25519 implementation. The replacement CryptoSigner only depends on pyca/cryptography, which implements all needed key types (including ed25519). Signed-off-by: Lukas Puehringer --- pyproject.toml | 1 - requirements-pinned.txt | 5 +---- requirements.txt | 1 - 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4d4ca204..d4f28b68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,6 @@ gcpkms = ["google-cloud-kms", "cryptography>=40.0.0"] azurekms = ["azure-identity", "azure-keyvault-keys", "cryptography>=40.0.0"] awskms = ["boto3", "botocore", "cryptography>=40.0.0"] hsm = ["asn1crypto", "cryptography>=40.0.0", "PyKCS11"] -pynacl = ["pynacl>1.2.0"] PySPX = ["PySPX>=0.5.0"] sigstore = ["sigstore~=2.0"] diff --git a/requirements-pinned.txt b/requirements-pinned.txt index d908dc9c..9eef3930 100644 --- a/requirements-pinned.txt +++ b/requirements-pinned.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile --output-file=requirements-pinned.txt requirements.txt @@ -9,7 +9,6 @@ asn1crypto==1.5.1 cffi==1.16.0 # via # cryptography - # pynacl # pyspx cryptography==42.0.5 # via -r requirements.txt @@ -17,7 +16,5 @@ pycparser==2.22 # via cffi pykcs11==1.5.14 # via -r requirements.txt -pynacl==1.5.0 - # via -r requirements.txt pyspx==0.5.0 ; platform_system != "Windows" # via -r requirements.txt diff --git a/requirements.txt b/requirements.txt index 7bf2c867..b9e800e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,6 @@ # 'requirements-pinned.txt' is updated on GitHub with Dependabot, which # triggers CI/CD builds to automatically test against updated dependencies. cryptography >= 37.0.0 -pynacl PySPX; platform_system != 'Windows' PyKCS11 asn1crypto