diff --git a/.editorconfig b/.editorconfig index c4458812..85f21c82 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,7 +2,7 @@ root = true [*] charset = utf-8 -indent_size = 2 +indent_size = 4 insert_final_newline = true end_of_line = lf indent_style = space diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..a207e5f2 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,8 @@ +# Ignore black, isort and pylint (auto-disable) +# +# For change details see commit messages +# For usage see +# https://black.readthedocs.io/en/stable/guides/introducing_black_to_your_project.html +8531f7d8edc8732a959cf283e64b0d19b49f7a16 +b48bc23acc7e49cdd1029d207ef61deca45c100d +91ac1f3154cea57e1bf1a8bebd368fce5b07176c diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c09fc53..f3768153 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: toxenv: py38-no-gpg - python-version: 3.8 os: ubuntu-latest - toxenv: mypy + toxenv: lint runs-on: ${{ matrix.os }} diff --git a/.gitignore b/.gitignore index 974c41f6..087afc91 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ env/* tests/htmlcov/* .DS_Store .python-version +.pre-commit-config.yaml diff --git a/pylintrc b/pylintrc new file mode 100644 index 00000000..4af6dd05 --- /dev/null +++ b/pylintrc @@ -0,0 +1,50 @@ +# Pylint section + +# Minimal pylint configuration file for Secure Systems Lab Python Style Guide: +# https://github.com/secure-systems-lab/code-style-guidelines +# +# Based on Google Python Style Guide pylintrc and pylint defaults: +# https://google.github.io/styleguide/pylintrc +# http://pylint.pycqa.org/en/latest/technical_reference/features.html +[MASTER] +ignore = _vendor + +[message_control] +# Disable the message, report, category or checker with the given id(s). +# NOTE: To keep this config as short as possible we only disable checks that +# are currently in conflict with our code. If new code displeases the linter +# (for good reasons) consider updating this config file, or disable checks with. +disable = + fixme, + too-few-public-methods, + too-many-arguments, + format, + duplicate-code + +[basic] +good-names = i,j,k,v,e,f,fn,fp,_type,_ +# Regexes for allowed names are copied from the Google pylintrc +# NOTE: Pylint captures regex name groups such as 'snake_case' or 'camel_case'. +# If there are multiple groups it enfoces the prevalent naming style inside +# each modules. Names in the exempt capturing group are ignored. +function-rgx = ^(?:(?PsetUp|tearDown|setUpModule|tearDownModule)|(?P_?[A-Z][a-zA-Z0-9]*)|(?P_?[a-z][a-z0-9_]*))$ +method-rgx = (?x)^(?:(?P_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P_{0,2}[a-z][a-z0-9_]*))$ +argument-rgx = ^[a-z][a-z0-9_]*$ +attr-rgx = ^_{0,2}[a-z][a-z0-9_]*$ +class-attribute-rgx = ^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ +class-rgx = ^_?[A-Z][a-zA-Z0-9]*$ +const-rgx = ^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ +inlinevar-rgx = ^[a-z][a-z0-9_]*$ +module-rgx = ^(_?[a-z][a-z0-9_]*|__init__)$ +no-docstring-rgx = (__.*__|main|test.*|.*test|.*Test)$ +variable-rgx = ^[a-z][a-z0-9_]*$ +docstring-min-length = 10 + +[logging] +logging-format-style=old + +[miscellaneous] +notes=TODO + +[STRING] +check-quote-consistency=yes diff --git a/requirements-test.txt b/requirements-test.txt index 899e9d51..a4dddf0c 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,3 +1,7 @@ # additional test tools coverage -mypy \ No newline at end of file +mypy +black +isort +pylint +bandit diff --git a/securesystemslib/__init__.py b/securesystemslib/__init__.py index 19dcaece..95b143cb 100755 --- a/securesystemslib/__init__.py +++ b/securesystemslib/__init__.py @@ -1,3 +1,4 @@ +# pylint: disable=missing-module-docstring import logging # Configure a basic 'securesystemslib' top-level logger with a StreamHandler diff --git a/securesystemslib/ecdsa_keys.py b/securesystemslib/ecdsa_keys.py index f39b4aba..65098f39 100755 --- a/securesystemslib/ecdsa_keys.py +++ b/securesystemslib/ecdsa_keys.py @@ -34,472 +34,491 @@ CRYPTO = True NO_CRYPTO_MSG = "ECDSA key support requires the cryptography library" try: - from cryptography.hazmat.backends import default_backend - from cryptography.hazmat.primitives import hashes - from cryptography.hazmat.primitives.asymmetric import ec - - from cryptography.hazmat.primitives import serialization - - from cryptography.hazmat.primitives.serialization import load_pem_public_key - from cryptography.hazmat.primitives.serialization import load_pem_private_key - - from cryptography.exceptions import (InvalidSignature, UnsupportedAlgorithm) - - _SCHEME_HASHER = { - 'ecdsa-sha2-nistp256': ec.ECDSA(hashes.SHA256()), - 'ecdsa-sha2-nistp384': ec.ECDSA(hashes.SHA384()) - } + 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 + CRYPTO = False # Perform object format-checking and add ability to handle/raise exceptions. -from securesystemslib import exceptions -from securesystemslib import formats +from securesystemslib import ( # pylint: disable=wrong-import-position + exceptions, + formats, +) -_SUPPORTED_ECDSA_SCHEMES = ['ecdsa-sha2-nistp256'] +_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. +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. - - None. + 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? - - A (public, private) tuple that conform to - 'securesystemslib.formats.PEMECDSA_SCHEMA' and - 'securesystemslib.formats.PEMECDSA_SCHEMA', respectively. - """ + '-----BEGIN PUBLIC KEY----- - 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) + '-----END PUBLIC KEY-----' - 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)) + The private ECDSA private key has the PEM format: - private_pem = private_key.private_bytes(encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption()) + '-----BEGIN EC PRIVATE KEY----- - public_pem = public_key.public_bytes(encoding=serialization.Encoding.PEM, - format=serialization.PublicFormat.SubjectPublicKeyInfo) + ... - return public_pem.decode('utf-8'), private_pem.decode('utf-8') + -----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(), + ) + + public_pem = public_key.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + + 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'. -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. + + securesystemslib.exceptions.FormatError, if the arguments are improperly + formatted. - - None. + securesystemslib.exceptions.CryptoError, if a signature cannot be created. - - A signature dictionary conformat to - 'securesystemslib.format.SIGNATURE_SCHEMA'. ECDSA signatures are XX bytes, - however, the hexlified signature is stored in the dictionary returned. - """ + securesystemslib.exceptions.UnsupportedAlgorithmError, if 'scheme' is not + one of the supported signature schemes. - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) + securesystemslib.exceptions.UnsupportedLibraryError, if the cryptography + module is not available. - # 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) + + None. - # Is 'private_key' properly formatted? - formats.PEMECDSA_SCHEMA.check_match(private_key) + + A signature dictionary conformat to + 'securesystemslib.format.SIGNATURE_SCHEMA'. ECDSA signatures are XX bytes, + however, the hexlified signature is stored in the dictionary returned. + """ - # Is 'scheme' properly formatted? - formats.ECDSA_SCHEME_SCHEMA.check_match(scheme) + if not CRYPTO: # pragma: no cover + raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - # '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()) + # 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) - signature = private_key.sign(data, ec.ECDSA(hashes.SHA256())) + # Is 'private_key' properly formatted? + formats.PEMECDSA_SCHEMA.check_match(private_key) - except TypeError as e: - raise exceptions.CryptoError('Could not create' - ' signature: ' + str(e)) + # Is 'scheme' properly formatted? + formats.ECDSA_SCHEME_SCHEMA.check_match(scheme) - # 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)) + # '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(), + ) - return signature, scheme + 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'. + """ + + 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'. - 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) + + securesystemslib.exceptions.FormatError, if any of the arguments are + improperly formatted. - 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 + securesystemslib.exceptions.UnsupportedAlgorithmError, if 'scheme' is + not one of the supported signature schemes. - if not isinstance(ecdsa_key, ec.EllipticCurvePublicKey): - raise exceptions.FormatError('Invalid ECDSA public' - ' key: ' + repr(public_key)) + securesystemslib.exceptions.UnsupportedLibraryError, if the cryptography + module is not available. - else: - logger.debug('Loaded a valid ECDSA public key.') + + None. - # verify() raises an 'InvalidSignature' exception if 'signature' - # is invalid. - try: - ecdsa_key.verify(signature, data, _SCHEME_HASHER[scheme]) - return True + + Boolean, indicating whether the 'signature' of data was generated by + the private key associated with 'public_key'. + """ - except (TypeError, InvalidSignature): - return False + 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( # pylint: disable=no-else-raise + ecdsa_key, ec.EllipticCurvePublicKey + ): + raise exceptions.FormatError( + "Invalid ECDSA public" " key: " + repr(public_key) + ) + + else: + 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: + """ + + 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. - 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. + + securesystemslib.exceptions.FormatError, if the arguments are improperly + formatted. - - A dictionary containing the ECDSA keys and other identifying information. - Conforms to 'securesystemslib.formats.ECDSAKEY_SCHEMA'. - """ + securesystemslib.exceptions.UnsupportedAlgorithmError, if the ECDSA key + pair could not be extracted, possibly due to an unsupported algorithm. - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) + securesystemslib.exceptions.UnsupportedLibraryError, if the cryptography + module is not available. - # Does 'pem' have the correct format? - # This check will ensure 'pem' conforms to - # 'securesystemslib.formats.ECDSARSA_SCHEMA'. - formats.PEMECDSA_SCHEMA.check_match(pem) + + None. - if password is not None: - formats.PASSWORD_SCHEMA.check_match(password) - password = password.encode('utf-8') + + A dictionary containing the ECDSA keys and other identifying information. + Conforms to 'securesystemslib.formats.ECDSAKEY_SCHEMA'. + """ - else: - logger.debug('The password/passphrase is unset. The PEM is expected' - ' to be unencrypted.') + if not CRYPTO: # pragma: no cover + raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - public = None - private = None + # Does 'pem' have the correct format? + # This check will ensure 'pem' conforms to + # 'securesystemslib.formats.ECDSARSA_SCHEMA'. + formats.PEMECDSA_SCHEMA.check_match(pem) - # 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()) + if password is not None: + formats.PASSWORD_SCHEMA.check_match(password) + password = password.encode("utf-8") - except (ValueError, UnsupportedAlgorithm) as e: - raise exceptions.CryptoError('Could not import private' - ' PEM.\n' + str(e)) + else: + logger.debug( + "The password/passphrase is unset. The PEM is expected" + " to be unencrypted." + ) - public = private.public_key() + public = None + private = None - # Serialize public and private keys to PEM format. - private = private.private_bytes(encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption()) + # 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() + ) - public = public.public_bytes(encoding=serialization.Encoding.PEM, - format=serialization.PublicFormat.SubjectPublicKeyInfo) + except (ValueError, UnsupportedAlgorithm) as e: + raise exceptions.CryptoError( + "Could not import private" " PEM.\n" + str(e) + ) - return public.decode('utf-8'), private.decode('utf-8') + 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(), + ) + public = public.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + 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'))) - - 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() + """ + + 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") + ), + ) + + 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 index a2f7bb96..b4ad62e9 100755 --- a/securesystemslib/ed25519_keys.py +++ b/securesystemslib/ed25519_keys.py @@ -64,87 +64,88 @@ NACL = True NO_NACL_MSG = "ed25519 key support requires the nacl library" try: - from nacl.encoding import RawEncoder - from nacl.signing import (SigningKey, VerifyKey) - # avoid conflicts with own exceptions of same name - from nacl import exceptions as nacl_exceptions + # 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 -from securesystemslib import exceptions -from securesystemslib import formats +# 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'] +_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. + """ + + 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. + 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 + >>> public, private = generate_public_and_private() + >>> securesystemslib.formats.ED25519PUBLIC_SCHEMA.matches(public) + True + >>> securesystemslib.formats.ED25519SEED_SCHEMA.matches(private) + True - - None. + + None. - - securesystemslib.exceptions.UnsupportedLibraryError, if the PyNaCl ('nacl') - module is unavailable. + + securesystemslib.exceptions.UnsupportedLibraryError, if the PyNaCl ('nacl') + module is unavailable. - NotImplementedError, if a randomness source is not found by 'os.urandom'. + 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(). + + 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. - """ + + 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) + 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 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 + # 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 @@ -203,52 +204,52 @@ def create_signature(public_key, private_key, data, scheme): returned. """ - if not NACL: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_NACL_MSG) + 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) + # 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 'private_key' properly formatted? + formats.ED25519SEED_SCHEMA.check_match(private_key) - # Is 'scheme' properly formatted? - formats.ED25519_SIG_SCHEMA.check_match(scheme) + # 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 + # 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 + # 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 @@ -299,58 +300,64 @@ def verify_signature(public_key, scheme, signature, data): 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: - 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() + # 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/exceptions.py b/securesystemslib/exceptions.py index da5a60b1..2ace2679 100755 --- a/securesystemslib/exceptions.py +++ b/securesystemslib/exceptions.py @@ -17,99 +17,124 @@ 'Error' (except where there is a good reason not to). """ + class Error(Exception): - """Indicate a generic error.""" - pass + """Indicate a generic error.""" + + pass # pylint: disable=unnecessary-pass -class Warning(Warning): - """Generic warning. It is used by the 'warnings' module.""" - pass +class Warning(Warning): # pylint: disable=redefined-builtin + """Generic warning. It is used by the 'warnings' module.""" + + pass # pylint: disable=unnecessary-pass class FormatError(Error): - """Indicate an error while validating an object's format.""" - pass + """Indicate an error while validating an object's format.""" + + pass # pylint: disable=unnecessary-pass class InvalidMetadataJSONError(FormatError): - """Indicate that a metadata file is not valid JSON.""" + """Indicate that a metadata file is not valid JSON.""" - def __init__(self, exception): - # Store the original exception. - self.exception = exception + 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) - 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.""" - pass + """Indicate an error while trying to identify a user-specified algorithm.""" + + pass # pylint: disable=unnecessary-pass class BadHashError(Error): - """Indicate an error while checking the value a hash object.""" + """Indicate an error while checking the value a hash object.""" - def __init__(self, expected_hash, observed_hash): - self.expected_hash = expected_hash - self.observed_hash = observed_hash + 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)+')' + 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.""" - pass + """Indicate an error after encountering an invalid password.""" + + pass # pylint: disable=unnecessary-pass class CryptoError(Error): - """Indicate any cryptography-related errors.""" - pass + """Indicate any cryptography-related errors.""" + + pass # pylint: disable=unnecessary-pass class BadSignatureError(CryptoError): - """Indicate that some metadata has a bad signature.""" + """Indicate that some metadata has a bad signature.""" - def __init__(self, metadata_role_name): - self.metadata_role_name = metadata_role_name + 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.' + 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.""" - pass + """Indicate that a user-specified cryptograpthic method is unknown.""" + + pass # pylint: disable=unnecessary-pass class UnsupportedLibraryError(Error): - """Indicate that a supported library could not be located or imported.""" - pass + """Indicate that a supported library could not be located or imported.""" + + pass # pylint: disable=unnecessary-pass class InvalidNameError(Error): - """Indicate an error while trying to validate any type of named object.""" - pass + """Indicate an error while trying to validate any type of named object.""" + + pass # pylint: disable=unnecessary-pass class NotFoundError(Error): - """If a required configuration or resource is not found.""" - pass + """If a required configuration or resource is not found.""" + + pass # pylint: disable=unnecessary-pass class URLMatchesNoPatternError(Error): - """If a URL does not match a user-specified regular expression.""" - pass + """If a URL does not match a user-specified regular expression.""" + + pass # pylint: disable=unnecessary-pass class InvalidConfigurationError(Error): - """If a configuration object does not match the expected format.""" - pass + """If a configuration object does not match the expected format.""" + + pass # pylint: disable=unnecessary-pass + class StorageError(Error): - """Indicate an error occured during interaction with an abstracted storage - backend.""" - pass + """Indicate an error occured during interaction with an abstracted storage + backend.""" + + pass # pylint: disable=unnecessary-pass diff --git a/securesystemslib/formats.py b/securesystemslib/formats.py index b19f6eb7..ad1f07c7 100755 --- a/securesystemslib/formats.py +++ b/securesystemslib/formats.py @@ -85,7 +85,9 @@ # for the zero UTC offset is always used (i.e., a numerical offset is not # supported.) Example: '2015-10-21T13:20:00Z'. Note: This is a simple format # check, and an ISO8601 string should be fully verified when it is parsed. -ISO8601_DATETIME_SCHEMA = SCHEMA.RegularExpression(r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z') +ISO8601_DATETIME_SCHEMA = SCHEMA.RegularExpression( + r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z" +) # A Unix/POSIX time format. An integer representing the number of seconds # since the epoch (January 1, 1970.) Metadata uses this format for the @@ -94,14 +96,14 @@ UNIX_TIMESTAMP_SCHEMA = SCHEMA.Integer(lo=0, hi=2147483647) # A hexadecimal value in '23432df87ab..' format. -HEX_SCHEMA = SCHEMA.RegularExpression(r'[a-fA-F0-9]+') +HEX_SCHEMA = SCHEMA.RegularExpression(r"[a-fA-F0-9]+") HASH_SCHEMA = HEX_SCHEMA # A dict in {'sha256': '23432df87ab..', 'sha512': '34324abc34df..', ...} format. HASHDICT_SCHEMA = SCHEMA.DictOf( - key_schema = SCHEMA.AnyString(), - value_schema = HASH_SCHEMA) + key_schema=SCHEMA.AnyString(), value_schema=HASH_SCHEMA +) # Uniform Resource Locator identifier (e.g., 'https://www.updateframework.com/'). # TODO: Some level of restriction here would be good.... Note that I pulled @@ -138,12 +140,21 @@ TEXT_SCHEMA = SCHEMA.AnyString() # Supported hash algorithms. -HASHALGORITHMS_SCHEMA = SCHEMA.ListOf(SCHEMA.OneOf( - [SCHEMA.String('md5'), SCHEMA.String('sha1'), - SCHEMA.String('sha224'), SCHEMA.String('sha256'), - SCHEMA.String('sha384'), SCHEMA.String('sha512'), - SCHEMA.String('blake2s'), SCHEMA.String('blake2b'), - SCHEMA.String('blake2b-256')])) +HASHALGORITHMS_SCHEMA = SCHEMA.ListOf( + SCHEMA.OneOf( + [ + SCHEMA.String("md5"), + SCHEMA.String("sha1"), + SCHEMA.String("sha224"), + SCHEMA.String("sha256"), + SCHEMA.String("sha384"), + SCHEMA.String("sha512"), + SCHEMA.String("blake2s"), + SCHEMA.String("blake2b"), + SCHEMA.String("blake2b-256"), + ] + ) +) # The contents of an encrypted key. Encrypted keys are saved to files # in this format. @@ -158,7 +169,7 @@ RSAKEYBITS_SCHEMA = SCHEMA.Integer(lo=2048) # The supported ECDSA signature schemes -ECDSA_SCHEME_SCHEMA = SCHEMA.RegularExpression(r'ecdsa-sha2-nistp(256|384)') +ECDSA_SCHEME_SCHEMA = SCHEMA.RegularExpression(r"ecdsa-sha2-nistp(256|384)") # A pyca-cryptography signature. PYCACRYPTOSIGNATURE_SCHEMA = SCHEMA.AnyBytes() @@ -179,79 +190,102 @@ # key identifier ('rsa', 233df889cb). For RSA keys, the key value is a pair of # public and private keys in PEM Format stored as strings. KEYVAL_SCHEMA = SCHEMA.Object( - object_name = 'KEYVAL_SCHEMA', - public = SCHEMA.AnyString(), - private = SCHEMA.Optional(SCHEMA.AnyString())) + object_name="KEYVAL_SCHEMA", + public=SCHEMA.AnyString(), + private=SCHEMA.Optional(SCHEMA.AnyString()), +) # Public keys CAN have a private portion (for backwards compatibility) which # MUST be an empty string PUBLIC_KEYVAL_SCHEMA = SCHEMA.Object( - object_name = 'KEYVAL_SCHEMA', - public = SCHEMA.AnyString(), - private = SCHEMA.Optional(SCHEMA.String(""))) + object_name="KEYVAL_SCHEMA", + public=SCHEMA.AnyString(), + private=SCHEMA.Optional(SCHEMA.String("")), +) # Supported securesystemslib key types. KEYTYPE_SCHEMA = SCHEMA.OneOf( - [SCHEMA.String('rsa'), SCHEMA.String('ed25519'), SCHEMA.String('ecdsa'), - SCHEMA.RegularExpression(r'ecdsa-sha2-nistp(256|384)')]) + [ + SCHEMA.String("rsa"), + SCHEMA.String("ed25519"), + SCHEMA.String("ecdsa"), + SCHEMA.RegularExpression(r"ecdsa-sha2-nistp(256|384)"), + ] +) # A generic securesystemslib key. All securesystemslib keys should be saved to # metadata files in this format. KEY_SCHEMA = SCHEMA.Object( - object_name = 'KEY_SCHEMA', - keytype = SCHEMA.AnyString(), - scheme = SCHEME_SCHEMA, - keyval = KEYVAL_SCHEMA, - expires = SCHEMA.Optional(ISO8601_DATETIME_SCHEMA)) + object_name="KEY_SCHEMA", + keytype=SCHEMA.AnyString(), + scheme=SCHEME_SCHEMA, + keyval=KEYVAL_SCHEMA, + expires=SCHEMA.Optional(ISO8601_DATETIME_SCHEMA), +) # Like KEY_SCHEMA, but requires keyval's private portion to be unset or empty, # and optionally includes the supported keyid hash algorithms used to generate # the key's keyid. PUBLIC_KEY_SCHEMA = SCHEMA.Object( - object_name = 'PUBLIC_KEY_SCHEMA', - keytype = SCHEMA.AnyString(), - keyid_hash_algorithms = SCHEMA.Optional(HASHALGORITHMS_SCHEMA), - keyval = PUBLIC_KEYVAL_SCHEMA, - expires = SCHEMA.Optional(ISO8601_DATETIME_SCHEMA)) + object_name="PUBLIC_KEY_SCHEMA", + keytype=SCHEMA.AnyString(), + keyid_hash_algorithms=SCHEMA.Optional(HASHALGORITHMS_SCHEMA), + keyval=PUBLIC_KEYVAL_SCHEMA, + expires=SCHEMA.Optional(ISO8601_DATETIME_SCHEMA), +) # A securesystemslib key object. This schema simplifies validation of keys # that may be one of the supported key types. Supported key types: 'rsa', # 'ed25519'. ANYKEY_SCHEMA = SCHEMA.Object( - object_name = 'ANYKEY_SCHEMA', - keytype = KEYTYPE_SCHEMA, - scheme = SCHEME_SCHEMA, - keyid = KEYID_SCHEMA, - keyid_hash_algorithms = SCHEMA.Optional(HASHALGORITHMS_SCHEMA), - keyval = KEYVAL_SCHEMA, - expires = SCHEMA.Optional(ISO8601_DATETIME_SCHEMA)) + object_name="ANYKEY_SCHEMA", + keytype=KEYTYPE_SCHEMA, + scheme=SCHEME_SCHEMA, + keyid=KEYID_SCHEMA, + keyid_hash_algorithms=SCHEMA.Optional(HASHALGORITHMS_SCHEMA), + keyval=KEYVAL_SCHEMA, + expires=SCHEMA.Optional(ISO8601_DATETIME_SCHEMA), +) # A list of securesystemslib key objects. ANYKEYLIST_SCHEMA = SCHEMA.ListOf(ANYKEY_SCHEMA) # RSA signature schemes. -RSA_SCHEME_SCHEMA = SCHEMA.OneOf([ - SCHEMA.RegularExpression(r'rsassa-pss-(md5|sha1|sha224|sha256|sha384|sha512)'), - SCHEMA.RegularExpression(r'rsa-pkcs1v15-(md5|sha1|sha224|sha256|sha384|sha512)')]) +RSA_SCHEME_SCHEMA = SCHEMA.OneOf( + [ + SCHEMA.RegularExpression( + r"rsassa-pss-(md5|sha1|sha224|sha256|sha384|sha512)" + ), + SCHEMA.RegularExpression( + r"rsa-pkcs1v15-(md5|sha1|sha224|sha256|sha384|sha512)" + ), + ] +) # An RSA securesystemslib key. RSAKEY_SCHEMA = SCHEMA.Object( - object_name = 'RSAKEY_SCHEMA', - keytype = SCHEMA.String('rsa'), - scheme = RSA_SCHEME_SCHEMA, - keyid = KEYID_SCHEMA, - keyid_hash_algorithms = SCHEMA.Optional(HASHALGORITHMS_SCHEMA), - keyval = KEYVAL_SCHEMA) + object_name="RSAKEY_SCHEMA", + keytype=SCHEMA.String("rsa"), + scheme=RSA_SCHEME_SCHEMA, + keyid=KEYID_SCHEMA, + keyid_hash_algorithms=SCHEMA.Optional(HASHALGORITHMS_SCHEMA), + keyval=KEYVAL_SCHEMA, +) # An ECDSA securesystemslib key. ECDSAKEY_SCHEMA = SCHEMA.Object( - object_name = 'ECDSAKEY_SCHEMA', - keytype = SCHEMA.OneOf([SCHEMA.String('ecdsa'), - SCHEMA.RegularExpression(r'ecdsa-sha2-nistp(256|384)')]), - scheme = ECDSA_SCHEME_SCHEMA, - keyid = KEYID_SCHEMA, - keyid_hash_algorithms = SCHEMA.Optional(HASHALGORITHMS_SCHEMA), - keyval = KEYVAL_SCHEMA) + object_name="ECDSAKEY_SCHEMA", + keytype=SCHEMA.OneOf( + [ + SCHEMA.String("ecdsa"), + SCHEMA.RegularExpression(r"ecdsa-sha2-nistp(256|384)"), + ] + ), + scheme=ECDSA_SCHEME_SCHEMA, + keyid=KEYID_SCHEMA, + keyid_hash_algorithms=SCHEMA.Optional(HASHALGORITHMS_SCHEMA), + keyval=KEYVAL_SCHEMA, +) # An ED25519 raw public key, which must be 32 bytes. ED25519PUBLIC_SCHEMA = SCHEMA.LengthBytes(32) @@ -267,16 +301,17 @@ # Ed25519 signature schemes. The vanilla Ed25519 signature scheme is currently # supported. -ED25519_SIG_SCHEMA = SCHEMA.OneOf([SCHEMA.String('ed25519')]) +ED25519_SIG_SCHEMA = SCHEMA.OneOf([SCHEMA.String("ed25519")]) # An ed25519 key. ED25519KEY_SCHEMA = SCHEMA.Object( - object_name = 'ED25519KEY_SCHEMA', - keytype = SCHEMA.String('ed25519'), - scheme = ED25519_SIG_SCHEMA, - keyid = KEYID_SCHEMA, - keyid_hash_algorithms = SCHEMA.Optional(HASHALGORITHMS_SCHEMA), - keyval = KEYVAL_SCHEMA) + object_name="ED25519KEY_SCHEMA", + keytype=SCHEMA.String("ed25519"), + scheme=ED25519_SIG_SCHEMA, + keyid=KEYID_SCHEMA, + keyid_hash_algorithms=SCHEMA.Optional(HASHALGORITHMS_SCHEMA), + keyval=KEYVAL_SCHEMA, +) # GPG key scheme definitions GPG_HASH_ALGORITHM_STRING = "pgp+SHA2" @@ -284,108 +319,110 @@ GPG_DSA_PUBKEY_METHOD_STRING = "pgp+dsa-fips-180-2" GPG_ED25519_PUBKEY_METHOD_STRING = "pgp+eddsa-ed25519" + def _create_gpg_pubkey_with_subkey_schema(pubkey_schema): - """Helper method to extend the passed public key schema with an optional - dictionary of sub public keys "subkeys" with the same schema.""" - schema = pubkey_schema - subkey_schema_tuple = ("subkeys", SCHEMA.Optional( - SCHEMA.DictOf( - key_schema=KEYID_SCHEMA, - value_schema=pubkey_schema - ) - ) - ) - # Any subclass of `securesystemslib.schema.Object` stores the schemas that - # define the attributes of the object in its `_required` property, even if - # such a schema is of type `Optional`. - # TODO: Find a way that does not require to access a protected member - schema._required.append(subkey_schema_tuple) # pylint: disable=protected-access - return schema + """Helper method to extend the passed public key schema with an optional + dictionary of sub public keys "subkeys" with the same schema.""" + schema = pubkey_schema + subkey_schema_tuple = ( + "subkeys", + SCHEMA.Optional( + SCHEMA.DictOf(key_schema=KEYID_SCHEMA, value_schema=pubkey_schema) + ), + ) + # Any subclass of `securesystemslib.schema.Object` stores the schemas that + # define the attributes of the object in its `_required` property, even if + # such a schema is of type `Optional`. + # TODO: Find a way that does not require to access a protected member + schema._required.append( # pylint: disable=protected-access + subkey_schema_tuple + ) # pylint: disable=protected-access + return schema + GPG_RSA_PUBKEYVAL_SCHEMA = SCHEMA.Object( - object_name = "GPG_RSA_PUBKEYVAL_SCHEMA", - e = SCHEMA.AnyString(), - n = HEX_SCHEMA + object_name="GPG_RSA_PUBKEYVAL_SCHEMA", e=SCHEMA.AnyString(), n=HEX_SCHEMA ) # We have to define GPG_RSA_PUBKEY_SCHEMA in two steps, because it is # self-referential. Here we define a shallow _GPG_RSA_PUBKEY_SCHEMA, which we # use below to create the self-referential GPG_RSA_PUBKEY_SCHEMA. _GPG_RSA_PUBKEY_SCHEMA = SCHEMA.Object( - object_name = "GPG_RSA_PUBKEY_SCHEMA", - type = SCHEMA.String("rsa"), - method = SCHEMA.String(GPG_RSA_PUBKEY_METHOD_STRING), - hashes = SCHEMA.ListOf(SCHEMA.String(GPG_HASH_ALGORITHM_STRING)), - creation_time = SCHEMA.Optional(UNIX_TIMESTAMP_SCHEMA), - validity_period = SCHEMA.Optional(SCHEMA.Integer(lo=0)), - keyid = KEYID_SCHEMA, - keyval = SCHEMA.Object( - public = GPG_RSA_PUBKEYVAL_SCHEMA, - private = SCHEMA.String("") - ) + object_name="GPG_RSA_PUBKEY_SCHEMA", + type=SCHEMA.String("rsa"), + method=SCHEMA.String(GPG_RSA_PUBKEY_METHOD_STRING), + hashes=SCHEMA.ListOf(SCHEMA.String(GPG_HASH_ALGORITHM_STRING)), + creation_time=SCHEMA.Optional(UNIX_TIMESTAMP_SCHEMA), + validity_period=SCHEMA.Optional(SCHEMA.Integer(lo=0)), + keyid=KEYID_SCHEMA, + keyval=SCHEMA.Object( + public=GPG_RSA_PUBKEYVAL_SCHEMA, private=SCHEMA.String("") + ), ) GPG_RSA_PUBKEY_SCHEMA = _create_gpg_pubkey_with_subkey_schema( - _GPG_RSA_PUBKEY_SCHEMA) + _GPG_RSA_PUBKEY_SCHEMA +) GPG_DSA_PUBKEYVAL_SCHEMA = SCHEMA.Object( - object_name = "GPG_DSA_PUBKEYVAL_SCHEMA", - y = HEX_SCHEMA, - p = HEX_SCHEMA, - q = HEX_SCHEMA, - g = HEX_SCHEMA + object_name="GPG_DSA_PUBKEYVAL_SCHEMA", + y=HEX_SCHEMA, + p=HEX_SCHEMA, + q=HEX_SCHEMA, + g=HEX_SCHEMA, ) # C.f. comment above _GPG_RSA_PUBKEY_SCHEMA definition _GPG_DSA_PUBKEY_SCHEMA = SCHEMA.Object( - object_name = "GPG_DSA_PUBKEY_SCHEMA", - type = SCHEMA.String("dsa"), - method = SCHEMA.String(GPG_DSA_PUBKEY_METHOD_STRING), - hashes = SCHEMA.ListOf(SCHEMA.String(GPG_HASH_ALGORITHM_STRING)), - creation_time = SCHEMA.Optional(UNIX_TIMESTAMP_SCHEMA), - validity_period = SCHEMA.Optional(SCHEMA.Integer(lo=0)), - keyid = KEYID_SCHEMA, - keyval = SCHEMA.Object( - public = GPG_DSA_PUBKEYVAL_SCHEMA, - private = SCHEMA.String("") - ) + object_name="GPG_DSA_PUBKEY_SCHEMA", + type=SCHEMA.String("dsa"), + method=SCHEMA.String(GPG_DSA_PUBKEY_METHOD_STRING), + hashes=SCHEMA.ListOf(SCHEMA.String(GPG_HASH_ALGORITHM_STRING)), + creation_time=SCHEMA.Optional(UNIX_TIMESTAMP_SCHEMA), + validity_period=SCHEMA.Optional(SCHEMA.Integer(lo=0)), + keyid=KEYID_SCHEMA, + keyval=SCHEMA.Object( + public=GPG_DSA_PUBKEYVAL_SCHEMA, private=SCHEMA.String("") + ), ) GPG_DSA_PUBKEY_SCHEMA = _create_gpg_pubkey_with_subkey_schema( - _GPG_DSA_PUBKEY_SCHEMA) + _GPG_DSA_PUBKEY_SCHEMA +) GPG_ED25519_PUBKEYVAL_SCHEMA = SCHEMA.Object( - object_name = "GPG_ED25519_PUBKEYVAL_SCHEMA", - q = HEX_SCHEMA, + object_name="GPG_ED25519_PUBKEYVAL_SCHEMA", + q=HEX_SCHEMA, ) # C.f. comment above _GPG_RSA_PUBKEY_SCHEMA definition _GPG_ED25519_PUBKEY_SCHEMA = SCHEMA.Object( - object_name = "GPG_ED25519_PUBKEY_SCHEMA", - type = SCHEMA.String("eddsa"), - method = SCHEMA.String(GPG_ED25519_PUBKEY_METHOD_STRING), - hashes = SCHEMA.ListOf(SCHEMA.String(GPG_HASH_ALGORITHM_STRING)), - creation_time = SCHEMA.Optional(UNIX_TIMESTAMP_SCHEMA), - validity_period = SCHEMA.Optional(SCHEMA.Integer(lo=0)), - keyid = KEYID_SCHEMA, - keyval = SCHEMA.Object( - public = GPG_ED25519_PUBKEYVAL_SCHEMA, - private = SCHEMA.String("") - ) + object_name="GPG_ED25519_PUBKEY_SCHEMA", + type=SCHEMA.String("eddsa"), + method=SCHEMA.String(GPG_ED25519_PUBKEY_METHOD_STRING), + hashes=SCHEMA.ListOf(SCHEMA.String(GPG_HASH_ALGORITHM_STRING)), + creation_time=SCHEMA.Optional(UNIX_TIMESTAMP_SCHEMA), + validity_period=SCHEMA.Optional(SCHEMA.Integer(lo=0)), + keyid=KEYID_SCHEMA, + keyval=SCHEMA.Object( + public=GPG_ED25519_PUBKEYVAL_SCHEMA, private=SCHEMA.String("") + ), ) GPG_ED25519_PUBKEY_SCHEMA = _create_gpg_pubkey_with_subkey_schema( - _GPG_ED25519_PUBKEY_SCHEMA) + _GPG_ED25519_PUBKEY_SCHEMA +) -GPG_PUBKEY_SCHEMA = SCHEMA.OneOf([GPG_RSA_PUBKEY_SCHEMA, - GPG_DSA_PUBKEY_SCHEMA, GPG_ED25519_PUBKEY_SCHEMA]) +GPG_PUBKEY_SCHEMA = SCHEMA.OneOf( + [GPG_RSA_PUBKEY_SCHEMA, GPG_DSA_PUBKEY_SCHEMA, GPG_ED25519_PUBKEY_SCHEMA] +) GPG_SIGNATURE_SCHEMA = SCHEMA.Object( - object_name = "SIGNATURE_SCHEMA", - keyid = KEYID_SCHEMA, - short_keyid = SCHEMA.Optional(KEYID_SCHEMA), - other_headers = HEX_SCHEMA, - signature = HEX_SCHEMA, - info = SCHEMA.Optional(SCHEMA.Any()), - ) + object_name="SIGNATURE_SCHEMA", + keyid=KEYID_SCHEMA, + short_keyid=SCHEMA.Optional(KEYID_SCHEMA), + other_headers=HEX_SCHEMA, + signature=HEX_SCHEMA, + info=SCHEMA.Optional(SCHEMA.Any()), +) # A single signature of an object. Indicates the signature, and the KEYID of # the signing key. I debated making the signature schema not contain the key @@ -394,335 +431,325 @@ def _create_gpg_pubkey_with_subkey_schema(pubkey_schema): # That would be under the argument that a key should only be able to sign a # file once. SIGNATURE_SCHEMA = SCHEMA.Object( - object_name = 'SIGNATURE_SCHEMA', - keyid = KEYID_SCHEMA, - sig = HEX_SCHEMA) + object_name="SIGNATURE_SCHEMA", keyid=KEYID_SCHEMA, sig=HEX_SCHEMA +) # A dict where the dict keys hold a keyid and the dict values a key object. -KEYDICT_SCHEMA = SCHEMA.DictOf( - key_schema = KEYID_SCHEMA, - value_schema = KEY_SCHEMA) +KEYDICT_SCHEMA = SCHEMA.DictOf(key_schema=KEYID_SCHEMA, value_schema=KEY_SCHEMA) -ANY_SIGNATURE_SCHEMA = SCHEMA.OneOf([SIGNATURE_SCHEMA, - GPG_SIGNATURE_SCHEMA]) +ANY_SIGNATURE_SCHEMA = SCHEMA.OneOf([SIGNATURE_SCHEMA, GPG_SIGNATURE_SCHEMA]) # List of ANY_SIGNATURE_SCHEMA. SIGNATURES_SCHEMA = SCHEMA.ListOf(ANY_SIGNATURE_SCHEMA) # A signable object. Holds the signing role and its associated signatures. SIGNABLE_SCHEMA = SCHEMA.Object( - object_name = 'SIGNABLE_SCHEMA', - signed = SCHEMA.Any(), - signatures = SIGNATURES_SCHEMA) + object_name="SIGNABLE_SCHEMA", + signed=SCHEMA.Any(), + signatures=SIGNATURES_SCHEMA, +) # Note: Verification keys can have private portions but in case of GPG we # only have a PUBKEY_SCHEMA (because we never export private gpg keys from # the gpg keyring) -ANY_VERIFICATION_KEY_SCHEMA = SCHEMA.OneOf([ANYKEY_SCHEMA, - GPG_PUBKEY_SCHEMA]) +ANY_VERIFICATION_KEY_SCHEMA = SCHEMA.OneOf([ANYKEY_SCHEMA, GPG_PUBKEY_SCHEMA]) VERIFICATION_KEY_DICT_SCHEMA = SCHEMA.DictOf( - key_schema = KEYID_SCHEMA, - value_schema = ANY_VERIFICATION_KEY_SCHEMA) + key_schema=KEYID_SCHEMA, value_schema=ANY_VERIFICATION_KEY_SCHEMA +) -ANY_KEYDICT_SCHEMA = SCHEMA.OneOf([KEYDICT_SCHEMA, - VERIFICATION_KEY_DICT_SCHEMA]) +ANY_KEYDICT_SCHEMA = SCHEMA.OneOf( + [KEYDICT_SCHEMA, VERIFICATION_KEY_DICT_SCHEMA] +) ANY_PUBKEY_SCHEMA = SCHEMA.OneOf([PUBLIC_KEY_SCHEMA, GPG_PUBKEY_SCHEMA]) ANY_PUBKEY_DICT_SCHEMA = SCHEMA.DictOf( - key_schema = KEYID_SCHEMA, - value_schema = ANY_PUBKEY_SCHEMA) - - - + key_schema=KEYID_SCHEMA, value_schema=ANY_PUBKEY_SCHEMA +) def datetime_to_unix_timestamp(datetime_object): - """ - - Convert 'datetime_object' (in datetime.datetime()) format) to a Unix/POSIX - timestamp. For example, Python's time.time() returns a Unix timestamp, and - includes the number of microseconds. 'datetime_object' is converted to UTC. - - >>> datetime_object = datetime.datetime(1985, 10, 26, 1, 22) - >>> timestamp = datetime_to_unix_timestamp(datetime_object) - >>> timestamp - 499137720 - - - datetime_object: - The datetime.datetime() object to convert to a Unix timestamp. - - - securesystemslib.exceptions.FormatError, if 'datetime_object' is not a - datetime.datetime() object. - - - None. - - - A unix (posix) timestamp (e.g., 499137660). - """ - - # Is 'datetime_object' a datetime.datetime() object? - # Raise 'securesystemslib.exceptions.FormatError' if not. - if not isinstance(datetime_object, datetime.datetime): - message = repr(datetime_object) + ' is not a datetime.datetime() object.' - raise exceptions.FormatError(message) - - unix_timestamp = calendar.timegm(datetime_object.timetuple()) - - return unix_timestamp + """ + + Convert 'datetime_object' (in datetime.datetime()) format) to a Unix/POSIX + timestamp. For example, Python's time.time() returns a Unix timestamp, and + includes the number of microseconds. 'datetime_object' is converted to UTC. + + >>> datetime_object = datetime.datetime(1985, 10, 26, 1, 22) + >>> timestamp = datetime_to_unix_timestamp(datetime_object) + >>> timestamp + 499137720 + + + datetime_object: + The datetime.datetime() object to convert to a Unix timestamp. + + + securesystemslib.exceptions.FormatError, if 'datetime_object' is not a + datetime.datetime() object. + + + None. + + + A unix (posix) timestamp (e.g., 499137660). + """ + + # Is 'datetime_object' a datetime.datetime() object? + # Raise 'securesystemslib.exceptions.FormatError' if not. + if not isinstance(datetime_object, datetime.datetime): + message = ( + repr(datetime_object) + " is not a datetime.datetime() object." + ) + raise exceptions.FormatError(message) + unix_timestamp = calendar.timegm(datetime_object.timetuple()) + return unix_timestamp def unix_timestamp_to_datetime(unix_timestamp): - """ - - Convert 'unix_timestamp' (i.e., POSIX time, in UNIX_TIMESTAMP_SCHEMA format) - to a datetime.datetime() object. 'unix_timestamp' is the number of seconds - since the epoch (January 1, 1970.) + """ + + Convert 'unix_timestamp' (i.e., POSIX time, in UNIX_TIMESTAMP_SCHEMA format) + to a datetime.datetime() object. 'unix_timestamp' is the number of seconds + since the epoch (January 1, 1970.) - >>> datetime_object = unix_timestamp_to_datetime(1445455680) - >>> datetime_object - datetime.datetime(2015, 10, 21, 19, 28) + >>> datetime_object = unix_timestamp_to_datetime(1445455680) + >>> datetime_object + datetime.datetime(2015, 10, 21, 19, 28) - - unix_timestamp: - An integer representing the time (e.g., 1445455680). Conformant to - 'securesystemslib.formats.UNIX_TIMESTAMP_SCHEMA'. + + unix_timestamp: + An integer representing the time (e.g., 1445455680). Conformant to + 'securesystemslib.formats.UNIX_TIMESTAMP_SCHEMA'. - - securesystemslib.exceptions.FormatError, if 'unix_timestamp' is improperly - formatted. + + securesystemslib.exceptions.FormatError, if 'unix_timestamp' is improperly + formatted. - - None. + + None. - - A datetime.datetime() object corresponding to 'unix_timestamp'. - """ + + A datetime.datetime() object corresponding to 'unix_timestamp'. + """ - # Is 'unix_timestamp' properly formatted? - # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch. - UNIX_TIMESTAMP_SCHEMA.check_match(unix_timestamp) + # Is 'unix_timestamp' properly formatted? + # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch. + UNIX_TIMESTAMP_SCHEMA.check_match(unix_timestamp) - # Convert 'unix_timestamp' to a 'time.struct_time', in UTC. The Daylight - # Savings Time (DST) flag is set to zero. datetime.fromtimestamp() is not - # used because it returns a local datetime. - struct_time = time.gmtime(unix_timestamp) - - # Extract the (year, month, day, hour, minutes, seconds) arguments for the - # datetime object to be returned. - datetime_object = datetime.datetime(*struct_time[:6]) - - return datetime_object + # Convert 'unix_timestamp' to a 'time.struct_time', in UTC. The Daylight + # Savings Time (DST) flag is set to zero. datetime.fromtimestamp() is not + # used because it returns a local datetime. + struct_time = time.gmtime(unix_timestamp) + # Extract the (year, month, day, hour, minutes, seconds) arguments for the + # datetime object to be returned. + datetime_object = datetime.datetime(*struct_time[:6]) + return datetime_object def format_base64(data): - """ - - Return the base64 encoding of 'data' with whitespace and '=' signs omitted. - - - data: - Binary or buffer of data to convert. + """ + + Return the base64 encoding of 'data' with whitespace and '=' signs omitted. - - securesystemslib.exceptions.FormatError, if the base64 encoding fails or the - argument is invalid. + + data: + Binary or buffer of data to convert. - - None. + + securesystemslib.exceptions.FormatError, if the base64 encoding fails or the + argument is invalid. - - A base64-encoded string. - """ + + None. - try: - return binascii.b2a_base64(data).decode('utf-8').rstrip('=\n ') - - except (TypeError, binascii.Error) as e: - raise exceptions.FormatError('Invalid base64' - ' encoding: ' + str(e)) + + A base64-encoded string. + """ + try: + return binascii.b2a_base64(data).decode("utf-8").rstrip("=\n ") + except (TypeError, binascii.Error) as e: + raise exceptions.FormatError("Invalid base64" " encoding: " + str(e)) def parse_base64(base64_string): - """ - - Parse a base64 encoding with whitespace and '=' signs omitted. - - - base64_string: - A string holding a base64 value. - - - securesystemslib.exceptions.FormatError, if 'base64_string' cannot be parsed - due to an invalid base64 encoding. + """ + + Parse a base64 encoding with whitespace and '=' signs omitted. - - None. + + base64_string: + A string holding a base64 value. - - A byte string representing the parsed based64 encoding of - 'base64_string'. - """ + + securesystemslib.exceptions.FormatError, if 'base64_string' cannot be parsed + due to an invalid base64 encoding. - if not isinstance(base64_string, str): - message = 'Invalid argument: '+repr(base64_string) - raise exceptions.FormatError(message) + + None. - extra = len(base64_string) % 4 - if extra: - padding = '=' * (4 - extra) - base64_string = base64_string + padding + + A byte string representing the parsed based64 encoding of + 'base64_string'. + """ - try: - return binascii.a2b_base64(base64_string.encode('utf-8')) + if not isinstance(base64_string, str): + message = "Invalid argument: " + repr(base64_string) + raise exceptions.FormatError(message) - except (TypeError, binascii.Error) as e: - raise exceptions.FormatError('Invalid base64' - ' encoding: ' + str(e)) + extra = len(base64_string) % 4 + if extra: + padding = "=" * (4 - extra) + base64_string = base64_string + padding + try: + return binascii.a2b_base64(base64_string.encode("utf-8")) + except (TypeError, binascii.Error) as e: + raise exceptions.FormatError("Invalid base64" " encoding: " + str(e)) def _canonical_string_encoder(string): - """ - - Encode 'string' to canonical string format. - - - string: - The string to encode. - - - None. - - - None. - - - A string with the canonical-encoded 'string' embedded. - """ - - string = '"%s"' % string.replace('\\', '\\\\').replace('"', '\\"') - - return string - - -def _encode_canonical(object, output_function): - # Helper for encode_canonical. Older versions of json.encoder don't - # even let us replace the separators. - - if isinstance(object, str): - output_function(_canonical_string_encoder(object)) - elif object is True: - output_function("true") - elif object is False: - output_function("false") - elif object is None: - output_function("null") - elif isinstance(object, int): - output_function(str(object)) - elif isinstance(object, (tuple, list)): - output_function("[") - if len(object): - for item in object[:-1]: - _encode_canonical(item, output_function) - output_function(",") - _encode_canonical(object[-1], output_function) - output_function("]") - elif isinstance(object, dict): - output_function("{") - if len(object): - items = sorted(object.items()) - for key, value in items[:-1]: - output_function(_canonical_string_encoder(key)) - output_function(":") - _encode_canonical(value, output_function) - output_function(",") - key, value = items[-1] - output_function(_canonical_string_encoder(key)) - output_function(":") - _encode_canonical(value, output_function) - output_function("}") - else: - raise exceptions.FormatError('I cannot encode '+repr(object)) - - -def encode_canonical(object, output_function=None): - """ - - Encode 'object' in canonical JSON form, as specified at - http://wiki.laptop.org/go/Canonical_JSON . It's a restricted - dialect of JSON in which keys are always lexically sorted, - there is no whitespace, floats aren't allowed, and only quote - and backslash get escaped. The result is encoded in UTF-8, - and the resulting bits are passed to output_function (if provided), - or joined into a string and returned. - - Note: This function should be called prior to computing the hash or - signature of a JSON object in securesystemslib. For example, generating a - signature of a signing role object such as 'ROOT_SCHEMA' is required to - ensure repeatable hashes are generated across different json module - versions and platforms. Code elsewhere is free to dump JSON objects in any - format they wish (e.g., utilizing indentation and single quotes around - object keys). These objects are only required to be in "canonical JSON" - format when their hashes or signatures are needed. - - >>> encode_canonical("") - '""' - >>> encode_canonical([1, 2, 3]) - '[1,2,3]' - >>> encode_canonical([]) - '[]' - >>> encode_canonical({"A": [99]}) - '{"A":[99]}' - >>> encode_canonical({"x" : 3, "y" : 2}) - '{"x":3,"y":2}' - - - object: - The object to be encoded. - - output_function: - The result will be passed as arguments to 'output_function' - (e.g., output_function('result')). - - - securesystemslib.exceptions.FormatError, if 'object' cannot be encoded or - 'output_function' is not callable. - - - The results are fed to 'output_function()' if 'output_function' is set. - - - A string representing the 'object' encoded in canonical JSON form. - """ - - result = None - # If 'output_function' is unset, treat it as - # appending to a list. - if output_function is None: - result = [] - output_function = result.append - - try: - _encode_canonical(object, output_function) - - except (TypeError, exceptions.FormatError) as e: - message = 'Could not encode ' + repr(object) + ': ' + str(e) - raise exceptions.FormatError(message) - - # Return the encoded 'object' as a string. - # Note: Implies 'output_function' is None, - # otherwise results are sent to 'output_function'. - if result is not None: - return ''.join(result) + """ + + Encode 'string' to canonical string format. + + + string: + The string to encode. + + + None. + + + None. + + + A string with the canonical-encoded 'string' embedded. + """ + + string = '"%s"' % string.replace("\\", "\\\\").replace('"', '\\"') + + return string + + +def _encode_canonical( + object, output_function +): # pylint: disable=missing-function-docstring,redefined-builtin + # Helper for encode_canonical. Older versions of json.encoder don't + # even let us replace the separators. + + if isinstance(object, str): + output_function(_canonical_string_encoder(object)) + elif object is True: + output_function("true") + elif object is False: + output_function("false") + elif object is None: + output_function("null") + elif isinstance(object, int): + output_function(str(object)) + elif isinstance(object, (tuple, list)): + output_function("[") + if len(object): + for item in object[:-1]: + _encode_canonical(item, output_function) + output_function(",") + _encode_canonical(object[-1], output_function) + output_function("]") + elif isinstance(object, dict): + output_function("{") + if len(object): + items = sorted(object.items()) + for key, value in items[:-1]: + output_function(_canonical_string_encoder(key)) + output_function(":") + _encode_canonical(value, output_function) + output_function(",") + key, value = items[-1] + output_function(_canonical_string_encoder(key)) + output_function(":") + _encode_canonical(value, output_function) + output_function("}") + else: + raise exceptions.FormatError("I cannot encode " + repr(object)) + + +def encode_canonical( # pylint: disable=inconsistent-return-statements + object, output_function=None # pylint: disable=redefined-builtin +): + """ + + Encode 'object' in canonical JSON form, as specified at + http://wiki.laptop.org/go/Canonical_JSON . It's a restricted + dialect of JSON in which keys are always lexically sorted, + there is no whitespace, floats aren't allowed, and only quote + and backslash get escaped. The result is encoded in UTF-8, + and the resulting bits are passed to output_function (if provided), + or joined into a string and returned. + + Note: This function should be called prior to computing the hash or + signature of a JSON object in securesystemslib. For example, generating a + signature of a signing role object such as 'ROOT_SCHEMA' is required to + ensure repeatable hashes are generated across different json module + versions and platforms. Code elsewhere is free to dump JSON objects in any + format they wish (e.g., utilizing indentation and single quotes around + object keys). These objects are only required to be in "canonical JSON" + format when their hashes or signatures are needed. + + >>> encode_canonical("") + '""' + >>> encode_canonical([1, 2, 3]) + '[1,2,3]' + >>> encode_canonical([]) + '[]' + >>> encode_canonical({"A": [99]}) + '{"A":[99]}' + >>> encode_canonical({"x" : 3, "y" : 2}) + '{"x":3,"y":2}' + + + object: + The object to be encoded. + + output_function: + The result will be passed as arguments to 'output_function' + (e.g., output_function('result')). + + + securesystemslib.exceptions.FormatError, if 'object' cannot be encoded or + 'output_function' is not callable. + + + The results are fed to 'output_function()' if 'output_function' is set. + + + A string representing the 'object' encoded in canonical JSON form. + """ + + result = None + # If 'output_function' is unset, treat it as + # appending to a list. + if output_function is None: + result = [] + output_function = result.append + + try: + _encode_canonical(object, output_function) + + except (TypeError, exceptions.FormatError) as e: + message = "Could not encode " + repr(object) + ": " + str(e) + raise exceptions.FormatError(message) + + # Return the encoded 'object' as a string. + # Note: Implies 'output_function' is None, + # otherwise results are sent to 'output_function'. + if result is not None: + return "".join(result) diff --git a/securesystemslib/gpg/common.py b/securesystemslib/gpg/common.py index 72ef9592..9d8c4d8f 100644 --- a/securesystemslib/gpg/common.py +++ b/securesystemslib/gpg/common.py @@ -17,771 +17,894 @@ call them. """ -import struct import binascii -import logging import collections +import logging +import struct from securesystemslib import formats from securesystemslib.gpg import util as gpg_util -from securesystemslib.gpg.exceptions import (PacketVersionNotSupportedError, - SignatureAlgorithmNotSupportedError, KeyNotFoundError, PacketParsingError) from securesystemslib.gpg.constants import ( - PACKET_TYPE_PRIMARY_KEY, PACKET_TYPE_USER_ID, PACKET_TYPE_USER_ATTR, - PACKET_TYPE_SUB_KEY, PACKET_TYPE_SIGNATURE, - SUPPORTED_PUBKEY_PACKET_VERSIONS, SIGNATURE_TYPE_BINARY, - SIGNATURE_TYPE_CERTIFICATES, SIGNATURE_TYPE_SUB_KEY_BINDING, + FULL_KEYID_SUBPACKET, + KEY_EXPIRATION_SUBPACKET, + PACKET_TYPE_PRIMARY_KEY, + PACKET_TYPE_SIGNATURE, + PACKET_TYPE_SUB_KEY, + PACKET_TYPE_USER_ATTR, + PACKET_TYPE_USER_ID, + PARTIAL_KEYID_SUBPACKET, + PRIMARY_USERID_SUBPACKET, + SHA1, + SHA256, + SHA512, + SIG_CREATION_SUBPACKET, + SIGNATURE_TYPE_BINARY, + SIGNATURE_TYPE_CERTIFICATES, + SIGNATURE_TYPE_SUB_KEY_BINDING, + SUPPORTED_PUBKEY_PACKET_VERSIONS, SUPPORTED_SIGNATURE_PACKET_VERSIONS, - FULL_KEYID_SUBPACKET, PARTIAL_KEYID_SUBPACKET, - SHA1,SHA256, SHA512, KEY_EXPIRATION_SUBPACKET, PRIMARY_USERID_SUBPACKET, - SIG_CREATION_SUBPACKET) +) +from securesystemslib.gpg.exceptions import ( + KeyNotFoundError, + PacketParsingError, + PacketVersionNotSupportedError, + SignatureAlgorithmNotSupportedError, +) from securesystemslib.gpg.handlers import ( - SIGNATURE_HANDLERS, SUPPORTED_SIGNATURE_ALGORITHMS) + SIGNATURE_HANDLERS, + SUPPORTED_SIGNATURE_ALGORITHMS, +) log = logging.getLogger(__name__) def parse_pubkey_payload(data): - """ - - Parse the passed public-key packet (payload only) and construct a - public key dictionary. - - - data: - An RFC4880 public key packet payload as described in section 5.5.2. - (version 4) of the RFC. - - NOTE: The payload can be parsed from a full key packet (header + - payload) by using securesystemslib.gpg.util.parse_packet_header. - - WARNING: this doesn't support armored pubkey packets, so use with - care. pubkey packets are a little bit more complicated than the - signature ones - - - ValueError - If the passed public key data is empty. - - securesystemslib.gpg.exceptions.PacketVersionNotSupportedError - If the packet version does not match - securesystemslib.gpg.constants.SUPPORTED_PUBKEY_PACKET_VERSIONS - - securesystemslib.gpg.exceptions.SignatureAlgorithmNotSupportedError - If the signature algorithm does not match one of - securesystemslib.gpg.constants.SUPPORTED_SIGNATURE_ALGORITHMS - - - None. - - - A public key in the format securesystemslib.formats.GPG_PUBKEY_SCHEMA - - """ - if not data: - raise ValueError("Could not parse empty pubkey payload.") - - ptr = 0 - keyinfo = {} - version_number = data[ptr] - ptr += 1 - if version_number not in SUPPORTED_PUBKEY_PACKET_VERSIONS: - raise PacketVersionNotSupportedError( - "Pubkey packet version '{}' not supported, must be one of {}".format( - version_number, SUPPORTED_PUBKEY_PACKET_VERSIONS)) - - # NOTE: Uncomment this line to decode the time of creation - time_of_creation = struct.unpack(">I", data[ptr:ptr + 4]) - ptr += 4 - - algorithm = data[ptr] - - ptr += 1 - - # TODO: Should we only export keys with signing capabilities? - # Section 5.5.2 of RFC4880 describes a public-key algorithm octet with one - # of the values described in section 9.1 that could be used to determine the - # capabilities. However, in case of RSA subkeys this field doesn't seem to - # correctly encode the capabilities. It always has the value 1, i.e. - # RSA (Encrypt or Sign). - # For RSA public keys we would have to parse the subkey's signature created - # with the master key, for the signature's key flags subpacket, identified - # by the value 27 (see section 5.2.3.1.) containing a list of binary flags - # as described in section 5.2.3.21. - if algorithm not in SUPPORTED_SIGNATURE_ALGORITHMS: - raise SignatureAlgorithmNotSupportedError("Signature algorithm '{}' not " - "supported, please verify that your gpg configuration is creating " - "either DSA, RSA, or EdDSA signatures (see RFC4880 9.1. Public-Key " - "Algorithms).".format(algorithm)) - - keyinfo['type'] = SUPPORTED_SIGNATURE_ALGORITHMS[algorithm]['type'] - keyinfo['method'] = SUPPORTED_SIGNATURE_ALGORITHMS[algorithm]['method'] - handler = SIGNATURE_HANDLERS[keyinfo['type']] - keyinfo['keyid'] = gpg_util.compute_keyid(data) - key_params = handler.get_pubkey_params(data[ptr:]) - - return { - "method": keyinfo['method'], - "type": keyinfo['type'], - "hashes": [formats.GPG_HASH_ALGORITHM_STRING], - "creation_time": time_of_creation[0], - "keyid": keyinfo['keyid'], - "keyval" : { - "private": "", - "public": key_params - } + """ + + Parse the passed public-key packet (payload only) and construct a + public key dictionary. + + + data: + An RFC4880 public key packet payload as described in section 5.5.2. + (version 4) of the RFC. + + NOTE: The payload can be parsed from a full key packet (header + + payload) by using securesystemslib.gpg.util.parse_packet_header. + + WARNING: this doesn't support armored pubkey packets, so use with + care. pubkey packets are a little bit more complicated than the + signature ones + + + ValueError + If the passed public key data is empty. + + securesystemslib.gpg.exceptions.PacketVersionNotSupportedError + If the packet version does not match + securesystemslib.gpg.constants.SUPPORTED_PUBKEY_PACKET_VERSIONS + + securesystemslib.gpg.exceptions.SignatureAlgorithmNotSupportedError + If the signature algorithm does not match one of + securesystemslib.gpg.constants.SUPPORTED_SIGNATURE_ALGORITHMS + + + None. + + + A public key in the format securesystemslib.formats.GPG_PUBKEY_SCHEMA + + """ + if not data: + raise ValueError("Could not parse empty pubkey payload.") + + ptr = 0 + keyinfo = {} + version_number = data[ptr] + ptr += 1 + if version_number not in SUPPORTED_PUBKEY_PACKET_VERSIONS: + raise PacketVersionNotSupportedError( + "Pubkey packet version '{}' not supported, must be one of {}".format( # pylint: disable=consider-using-f-string + version_number, SUPPORTED_PUBKEY_PACKET_VERSIONS + ) + ) + + # NOTE: Uncomment this line to decode the time of creation + time_of_creation = struct.unpack(">I", data[ptr : ptr + 4]) + ptr += 4 + + algorithm = data[ptr] + + ptr += 1 + + # TODO: Should we only export keys with signing capabilities? + # Section 5.5.2 of RFC4880 describes a public-key algorithm octet with one + # of the values described in section 9.1 that could be used to determine the + # capabilities. However, in case of RSA subkeys this field doesn't seem to + # correctly encode the capabilities. It always has the value 1, i.e. + # RSA (Encrypt or Sign). + # For RSA public keys we would have to parse the subkey's signature created + # with the master key, for the signature's key flags subpacket, identified + # by the value 27 (see section 5.2.3.1.) containing a list of binary flags + # as described in section 5.2.3.21. + if algorithm not in SUPPORTED_SIGNATURE_ALGORITHMS: + raise SignatureAlgorithmNotSupportedError( + "Signature algorithm '{}' not " # pylint: disable=consider-using-f-string + "supported, please verify that your gpg configuration is creating " + "either DSA, RSA, or EdDSA signatures (see RFC4880 9.1. Public-Key " + "Algorithms).".format(algorithm) + ) + + keyinfo["type"] = SUPPORTED_SIGNATURE_ALGORITHMS[algorithm]["type"] + keyinfo["method"] = SUPPORTED_SIGNATURE_ALGORITHMS[algorithm]["method"] + handler = SIGNATURE_HANDLERS[keyinfo["type"]] + keyinfo["keyid"] = gpg_util.compute_keyid(data) + key_params = handler.get_pubkey_params(data[ptr:]) + + return { + "method": keyinfo["method"], + "type": keyinfo["type"], + "hashes": [formats.GPG_HASH_ALGORITHM_STRING], + "creation_time": time_of_creation[0], + "keyid": keyinfo["keyid"], + "keyval": {"private": "", "public": key_params}, } def parse_pubkey_bundle(data): - """ - - Parse packets from passed gpg public key data, associating self-signatures - with the packets they correspond to, based on the structure of V4 keys - defined in RFC4880 12.1 Key Structures. - - The returned raw key bundle may be used to further enrich the master key, - with certified information (e.g. key expiration date) taken from - self-signatures, and/or to verify that the parsed subkeys are bound to the - primary key via signatures. - - - data: - Public key data as written to stdout by gpg_export_pubkey_command. - - - securesystemslib.gpg.exceptions.PacketParsingError - If data is empty. - If data cannot be parsed. - - - None. - - - A raw public key bundle where self-signatures are associated with their - corresponding packets. See `key_bundle` for details. - - """ - if not data: - raise PacketParsingError("Cannot parse keys from empty gpg data.") - - # Temporary data structure to hold parsed gpg packets - key_bundle = { - PACKET_TYPE_PRIMARY_KEY: { - "key": {}, - "packet": None, - "signatures": [] - }, - PACKET_TYPE_USER_ID: collections.OrderedDict(), - PACKET_TYPE_USER_ATTR: collections.OrderedDict(), - PACKET_TYPE_SUB_KEY: collections.OrderedDict() - } - - # Iterate over gpg data and parse out packets of different types - position = 0 - while position < len(data): - try: - packet_type, header_len, body_len, packet_length = \ - gpg_util.parse_packet_header(data[position:]) - - packet = data[position:position+packet_length] - payload = packet[header_len:] - # The first (and only the first) packet in the bundle must be the master - # key. See RFC4880 12.1 Key Structures, V4 version keys - # TODO: Do we need additional key structure assertions? e.g. - # - there must be least one User ID packet, or - # - order and type of signatures, or - # - disallow duplicate packets - if packet_type != PACKET_TYPE_PRIMARY_KEY and \ - not key_bundle[PACKET_TYPE_PRIMARY_KEY]["key"]: - raise PacketParsingError("First packet must be a primary key ('{}'), " - "got '{}'.".format(PACKET_TYPE_PRIMARY_KEY, packet_type)) - - elif packet_type == PACKET_TYPE_PRIMARY_KEY and \ - key_bundle[PACKET_TYPE_PRIMARY_KEY]["key"]: - raise PacketParsingError("Unexpected primary key.") - - # Fully parse master key to fail early, e.g. if key is malformed - # or not supported, but also retain original packet for subkey binding - # signature verification - elif packet_type == PACKET_TYPE_PRIMARY_KEY: - key_bundle[PACKET_TYPE_PRIMARY_KEY] = { - "key": parse_pubkey_payload(bytearray(payload)), - "packet": packet, - "signatures": [] - } - - # Other non-signature packets in the key bundle include User IDs and User - # Attributes, required to verify primary key certificates, and subkey - # packets. For each packet we create a new ordered dictionary entry. We - # use a dictionary to aggregate signatures by packet below, - # and it must be ordered because each signature packet belongs to the - # most recently parsed packet of a type. - elif packet_type in {PACKET_TYPE_USER_ID, PACKET_TYPE_USER_ATTR, - PACKET_TYPE_SUB_KEY}: - key_bundle[packet_type][packet] = { - "header_len": header_len, - "body_len": body_len, - "signatures": [] - } - - # The remaining relevant packets are signatures, required to bind subkeys - # to the primary key, or to gather additional information about the - # primary key, e.g. expiration date. - # A signature corresponds to the most recently parsed packet of a type, - # where the type is given by the availability of respective packets. - # We test availability and assign accordingly as per the order of packet - # types defined in RFC4880 12.1 (bottom-up). - elif packet_type == PACKET_TYPE_SIGNATURE: - for _type in [PACKET_TYPE_SUB_KEY, PACKET_TYPE_USER_ATTR, - PACKET_TYPE_USER_ID]: - if key_bundle[_type]: - # Add to most recently added packet's signatures of matching type - key_bundle[_type][next(reversed(key_bundle[_type]))]\ - ["signatures"].append(packet) - break + """ + + Parse packets from passed gpg public key data, associating self-signatures + with the packets they correspond to, based on the structure of V4 keys + defined in RFC4880 12.1 Key Structures. + + The returned raw key bundle may be used to further enrich the master key, + with certified information (e.g. key expiration date) taken from + self-signatures, and/or to verify that the parsed subkeys are bound to the + primary key via signatures. + + + data: + Public key data as written to stdout by gpg_export_pubkey_command. + + + securesystemslib.gpg.exceptions.PacketParsingError + If data is empty. + If data cannot be parsed. + + + None. + + + A raw public key bundle where self-signatures are associated with their + corresponding packets. See `key_bundle` for details. + + """ + if not data: + raise PacketParsingError("Cannot parse keys from empty gpg data.") + + # Temporary data structure to hold parsed gpg packets + key_bundle = { + PACKET_TYPE_PRIMARY_KEY: {"key": {}, "packet": None, "signatures": []}, + PACKET_TYPE_USER_ID: collections.OrderedDict(), + PACKET_TYPE_USER_ATTR: collections.OrderedDict(), + PACKET_TYPE_SUB_KEY: collections.OrderedDict(), + } - else: - # If no packets are available for any of above types (yet), the - # signature belongs to the primary key - key_bundle[PACKET_TYPE_PRIMARY_KEY]["signatures"].append(packet) + # Iterate over gpg data and parse out packets of different types + position = 0 + while position < len(data): + try: + ( + packet_type, + header_len, + body_len, + packet_length, + ) = gpg_util.parse_packet_header(data[position:]) + + packet = data[position : position + packet_length] + payload = packet[header_len:] + # The first (and only the first) packet in the bundle must be the master + # key. See RFC4880 12.1 Key Structures, V4 version keys + # TODO: Do we need additional key structure assertions? e.g. + # - there must be least one User ID packet, or + # - order and type of signatures, or + # - disallow duplicate packets + if ( # pylint: disable=no-else-raise + packet_type != PACKET_TYPE_PRIMARY_KEY + and not key_bundle[PACKET_TYPE_PRIMARY_KEY]["key"] + ): + raise PacketParsingError( + "First packet must be a primary key ('{}'), " # pylint: disable=consider-using-f-string + "got '{}'.".format(PACKET_TYPE_PRIMARY_KEY, packet_type) + ) + + elif ( + packet_type == PACKET_TYPE_PRIMARY_KEY + and key_bundle[PACKET_TYPE_PRIMARY_KEY]["key"] + ): + raise PacketParsingError("Unexpected primary key.") + + # Fully parse master key to fail early, e.g. if key is malformed + # or not supported, but also retain original packet for subkey binding + # signature verification + elif packet_type == PACKET_TYPE_PRIMARY_KEY: + key_bundle[PACKET_TYPE_PRIMARY_KEY] = { + "key": parse_pubkey_payload(bytearray(payload)), + "packet": packet, + "signatures": [], + } + + # Other non-signature packets in the key bundle include User IDs and User + # Attributes, required to verify primary key certificates, and subkey + # packets. For each packet we create a new ordered dictionary entry. We + # use a dictionary to aggregate signatures by packet below, + # and it must be ordered because each signature packet belongs to the + # most recently parsed packet of a type. + elif packet_type in { + PACKET_TYPE_USER_ID, + PACKET_TYPE_USER_ATTR, + PACKET_TYPE_SUB_KEY, + }: + key_bundle[packet_type][packet] = { + "header_len": header_len, + "body_len": body_len, + "signatures": [], + } + + # The remaining relevant packets are signatures, required to bind subkeys + # to the primary key, or to gather additional information about the + # primary key, e.g. expiration date. + # A signature corresponds to the most recently parsed packet of a type, + # where the type is given by the availability of respective packets. + # We test availability and assign accordingly as per the order of packet + # types defined in RFC4880 12.1 (bottom-up). + elif packet_type == PACKET_TYPE_SIGNATURE: + for _type in [ + PACKET_TYPE_SUB_KEY, + PACKET_TYPE_USER_ATTR, + PACKET_TYPE_USER_ID, + ]: + if key_bundle[_type]: + # Add to most recently added packet's signatures of matching type + key_bundle[_type][next(reversed(key_bundle[_type]))][ + "signatures" + ].append(packet) + break + + else: + # If no packets are available for any of above types (yet), the + # signature belongs to the primary key + key_bundle[PACKET_TYPE_PRIMARY_KEY]["signatures"].append( + packet + ) + + else: + log.info( + "Ignoring gpg key packet '{}', we only handle packets of " # pylint: disable=logging-format-interpolation,consider-using-f-string + "types '{}' (see RFC4880 4.3. Packet Tags).".format( + packet_type, + [ + PACKET_TYPE_PRIMARY_KEY, + PACKET_TYPE_USER_ID, + PACKET_TYPE_USER_ATTR, + PACKET_TYPE_SUB_KEY, + PACKET_TYPE_SIGNATURE, + ], + ) + ) + + # Both errors might be raised in parse_packet_header and in this loop + except (PacketParsingError, IndexError) as e: + raise PacketParsingError( # pylint: disable=raise-missing-from + "Invalid public key data at position {}: {}.".format( # pylint: disable=consider-using-f-string + position, e + ) + ) + + # Go to next packet + position += packet_length + + return key_bundle - else: - log.info("Ignoring gpg key packet '{}', we only handle packets of " - "types '{}' (see RFC4880 4.3. Packet Tags).".format(packet_type, - [PACKET_TYPE_PRIMARY_KEY, PACKET_TYPE_USER_ID, - PACKET_TYPE_USER_ATTR, PACKET_TYPE_SUB_KEY, - PACKET_TYPE_SIGNATURE])) - # Both errors might be raised in parse_packet_header and in this loop - except (PacketParsingError, IndexError) as e: - raise PacketParsingError("Invalid public key data at position {}: {}." - .format(position, e)) +def _assign_certified_key_info(bundle): + """ + + Helper function to verify User ID certificates corresponding to a gpg + master key, in order to enrich the master key with additional information + (e.g. expiration dates). The enriched master key is returned. + + NOTE: Currently we only consider User ID certificates. We can do the same + for User Attribute certificates by iterating over + bundle[PACKET_TYPE_USER_ATTR] instead of bundle[PACKET_TYPE_USER_ID], and + replacing the signed_content constant '\xb4' with '\xd1' (see RFC4880 + section 5.2.4. paragraph 4). + + + bundle: + GPG key bundle as parsed in parse_pubkey_bundle(). + + + None. + + + None. + + + A public key in the format securesystemslib.formats.GPG_PUBKEY_SCHEMA. + + """ + # Create handler shortcut + handler = SIGNATURE_HANDLERS[bundle[PACKET_TYPE_PRIMARY_KEY]["key"]["type"]] + + is_primary_user = False + validity_period = None + sig_creation_time = None + + # Verify User ID signatures to gather information about primary key + # (see Notes about certification signatures in RFC 4880 5.2.3.3.) + for user_id_packet, packet_data in bundle[PACKET_TYPE_USER_ID].items(): + # Construct signed content (see RFC4880 section 5.2.4. paragraph 4) + signed_content = ( + bundle[PACKET_TYPE_PRIMARY_KEY]["packet"] + + b"\xb4\x00\x00\x00" + + user_id_packet[1:] + ) + for signature_packet in packet_data["signatures"]: + try: + signature = parse_signature_packet( + signature_packet, + supported_hash_algorithms={SHA1, SHA256, SHA512}, + supported_signature_types=SIGNATURE_TYPE_CERTIFICATES, + include_info=True, + ) + # verify_signature requires a "keyid" even if it is short. + # (see parse_signature_packet for more information about keyids) + signature["keyid"] = ( + signature["keyid"] or signature["short_keyid"] + ) + + # TODO: Revise exception taxonomy: + # It's okay to ignore some exceptions (unsupported algorithms etc.) but + # we should blow up if a signature is malformed (missing subpackets). + except Exception as e: # pylint: disable=broad-except + log.info(e) + continue + + if not bundle[PACKET_TYPE_PRIMARY_KEY]["key"]["keyid"].endswith( + signature["keyid"] + ): + log.info( + "Ignoring User ID certificate issued by '{}'.".format( # pylint: disable=logging-format-interpolation,consider-using-f-string + signature["keyid"] + ) + ) + continue + + is_valid = handler.verify_signature( + signature, + bundle[PACKET_TYPE_PRIMARY_KEY]["key"], + signed_content, + signature["info"]["hash_algorithm"], + ) + + if not is_valid: + log.info( + "Ignoring invalid User ID self-certificate issued " # pylint: disable=logging-format-interpolation,consider-using-f-string + "by '{}'.".format(signature["keyid"]) + ) + continue + + # If the signature is valid, we try to extract subpackets relevant to + # the primary key, i.e. expiration time. + # NOTE: There might be multiple User IDs per primary key and multiple + # certificates per User ID. RFC4880 5.2.3.19. and last paragraph of + # 5.2.3.3. provides some suggestions about ambiguity, but delegates the + # responsibility to the implementer. + + # Ambiguity resolution scheme: + # We take the key expiration time from the most recent certificate, i.e. + # the certificate with the highest signature creation time. Additionally, + # we prioritize certificates with primary user id flag set True. Note + # that, if the ultimately prioritized certificate does not have a key + # expiration time subpacket, we don't assign one, even if there were + # certificates of lower priority carrying that subpacket. + tmp_validity_period = signature["info"]["subpackets"].get( + KEY_EXPIRATION_SUBPACKET + ) + + # No key expiration time, go to next certificate + if tmp_validity_period is None: + continue + + # Create shortcut to mandatory pre-parsed creation time subpacket + tmp_sig_creation_time = signature["info"]["creation_time"] + + tmp_is_primary_user = signature["info"]["subpackets"].get( + PRIMARY_USERID_SUBPACKET + ) + + if tmp_is_primary_user is not None: + tmp_is_primary_user = bool(tmp_is_primary_user[0]) + + # If we already have a primary user certified expiration date and this + # is none, we don't consider it, and go to next certificate + if is_primary_user and not tmp_is_primary_user: + continue + + if ( + not sig_creation_time + or sig_creation_time < tmp_sig_creation_time + ): + # This is the most recent certificate that has a validity_period and + # doesn't have lower priority in regard to the primary user id flag. We + # accept it the keys validty_period, until we get a newer value from + # a certificate with higher priority. + validity_period = struct.unpack(">I", tmp_validity_period)[0] + # We also keep track of the used certificate's primary user id flag and + # the signature creation time, for prioritization. + is_primary_user = tmp_is_primary_user + sig_creation_time = tmp_sig_creation_time - # Go to next packet - position += packet_length + if validity_period is not None: + bundle[PACKET_TYPE_PRIMARY_KEY]["key"][ + "validity_period" + ] = validity_period - return key_bundle + return bundle[PACKET_TYPE_PRIMARY_KEY]["key"] -def _assign_certified_key_info(bundle): - """ - - Helper function to verify User ID certificates corresponding to a gpg - master key, in order to enrich the master key with additional information - (e.g. expiration dates). The enriched master key is returned. - - NOTE: Currently we only consider User ID certificates. We can do the same - for User Attribute certificates by iterating over - bundle[PACKET_TYPE_USER_ATTR] instead of bundle[PACKET_TYPE_USER_ID], and - replacing the signed_content constant '\xb4' with '\xd1' (see RFC4880 - section 5.2.4. paragraph 4). - - - bundle: - GPG key bundle as parsed in parse_pubkey_bundle(). - - - None. - - - None. - - - A public key in the format securesystemslib.formats.GPG_PUBKEY_SCHEMA. - - """ - # Create handler shortcut - handler = SIGNATURE_HANDLERS[bundle[PACKET_TYPE_PRIMARY_KEY]["key"]["type"]] - - is_primary_user = False - validity_period = None - sig_creation_time = None - - # Verify User ID signatures to gather information about primary key - # (see Notes about certification signatures in RFC 4880 5.2.3.3.) - for user_id_packet, packet_data in bundle[PACKET_TYPE_USER_ID].items(): - # Construct signed content (see RFC4880 section 5.2.4. paragraph 4) - signed_content = (bundle[PACKET_TYPE_PRIMARY_KEY]["packet"] + - b"\xb4\x00\x00\x00" + user_id_packet[1:]) - for signature_packet in packet_data["signatures"]: - try: - signature = parse_signature_packet(signature_packet, - supported_hash_algorithms={SHA1, SHA256, SHA512}, - supported_signature_types=SIGNATURE_TYPE_CERTIFICATES, - include_info=True) - # verify_signature requires a "keyid" even if it is short. - # (see parse_signature_packet for more information about keyids) - signature["keyid"] = signature["keyid"] or signature["short_keyid"] - - # TODO: Revise exception taxonomy: - # It's okay to ignore some exceptions (unsupported algorithms etc.) but - # we should blow up if a signature is malformed (missing subpackets). - except Exception as e: - log.info(e) - continue - - if not bundle[PACKET_TYPE_PRIMARY_KEY]["key"]["keyid"].endswith( - signature["keyid"]): - log.info("Ignoring User ID certificate issued by '{}'.".format( - signature["keyid"])) - continue - - is_valid = handler.verify_signature(signature, - bundle[PACKET_TYPE_PRIMARY_KEY]["key"], signed_content, - signature["info"]["hash_algorithm"]) - - if not is_valid: - log.info("Ignoring invalid User ID self-certificate issued " - "by '{}'.".format(signature["keyid"])) - continue - - # If the signature is valid, we try to extract subpackets relevant to - # the primary key, i.e. expiration time. - # NOTE: There might be multiple User IDs per primary key and multiple - # certificates per User ID. RFC4880 5.2.3.19. and last paragraph of - # 5.2.3.3. provides some suggestions about ambiguity, but delegates the - # responsibility to the implementer. - - # Ambiguity resolution scheme: - # We take the key expiration time from the most recent certificate, i.e. - # the certificate with the highest signature creation time. Additionally, - # we prioritize certificates with primary user id flag set True. Note - # that, if the ultimately prioritized certificate does not have a key - # expiration time subpacket, we don't assign one, even if there were - # certificates of lower priority carrying that subpacket. - tmp_validity_period = \ - signature["info"]["subpackets"].get(KEY_EXPIRATION_SUBPACKET) - - # No key expiration time, go to next certificate - if tmp_validity_period is None: - continue - - # Create shortcut to mandatory pre-parsed creation time subpacket - tmp_sig_creation_time = signature["info"]["creation_time"] - - tmp_is_primary_user = \ - signature["info"]["subpackets"].get(PRIMARY_USERID_SUBPACKET) - - if tmp_is_primary_user is not None: - tmp_is_primary_user = bool(tmp_is_primary_user[0]) - - # If we already have a primary user certified expiration date and this - # is none, we don't consider it, and go to next certificate - if is_primary_user and not tmp_is_primary_user: - continue - - if not sig_creation_time or sig_creation_time < tmp_sig_creation_time: - # This is the most recent certificate that has a validity_period and - # doesn't have lower priority in regard to the primary user id flag. We - # accept it the keys validty_period, until we get a newer value from - # a certificate with higher priority. - validity_period = struct.unpack(">I", tmp_validity_period)[0] - # We also keep track of the used certificate's primary user id flag and - # the signature creation time, for prioritization. - is_primary_user = tmp_is_primary_user - sig_creation_time = tmp_sig_creation_time - - if validity_period is not None: - bundle[PACKET_TYPE_PRIMARY_KEY]["key"]["validity_period"] = validity_period - - return bundle[PACKET_TYPE_PRIMARY_KEY]["key"] +def _get_verified_subkeys(bundle): + """ + + Helper function to verify the subkey binding signature for all subkeys in + the passed bundle in order to enrich subkeys with additional information + (e.g. expiration dates). Only valid (i.e. parsable) subkeys that are + verifiably bound to the the master key of the bundle are returned. All + other subkeys are discarded. + + + bundle: + GPG key bundle as parsed in parse_pubkey_bundle(). + + + None. + + + None. + + + A dictionary of public keys in the format + securesystemslib.formats.GPG_PUBKEY_SCHEMA, with keyids as dict keys. + + """ + # Create handler shortcut + handler = SIGNATURE_HANDLERS[bundle[PACKET_TYPE_PRIMARY_KEY]["key"]["type"]] + + # Verify subkey binding signatures and only keep verified keys + # See notes about subkey binding signature in RFC4880 5.2.3.3 + verified_subkeys = {} + for subkey_packet, packet_data in bundle[PACKET_TYPE_SUB_KEY].items(): + try: + # Parse subkey if possible and skip if invalid (e.g. not-supported) + subkey = parse_pubkey_payload( + bytearray(subkey_packet[-packet_data["body_len"] :]) + ) + + # TODO: Revise exception taxonomy + except Exception as e: # pylint: disable=broad-except + log.info(e) + continue + + # Construct signed content (see RFC4880 section 5.2.4. paragraph 3) + signed_content = ( + bundle[PACKET_TYPE_PRIMARY_KEY]["packet"] + + b"\x99" + + subkey_packet[1:] + ) + + # Filter sub key binding signature from other signatures, e.g. subkey + # binding revocation signatures + key_binding_signatures = [] + for signature_packet in packet_data["signatures"]: + try: + signature = parse_signature_packet( + signature_packet, + supported_hash_algorithms={SHA1, SHA256, SHA512}, + supported_signature_types={SIGNATURE_TYPE_SUB_KEY_BINDING}, + include_info=True, + ) + # verify_signature requires a "keyid" even if it is short. + # (see parse_signature_packet for more information about keyids) + signature["keyid"] = ( + signature["keyid"] or signature["short_keyid"] + ) + key_binding_signatures.append(signature) + + # TODO: Revise exception taxonomy + except Exception as e: # pylint: disable=broad-except + log.info(e) + continue + # NOTE: As per the V4 key structure diagram in RFC4880 section 12.1., a + # subkey must be followed by exactly one Primary-Key-Binding-Signature. + # Based on inspection of real-world keys and other parts of the RFC (e.g. + # the paragraph below the diagram and paragraph 0x18: Subkey Binding + # Signature in section 5.2.1.) the mandated signature is actually a + # *subkey binding signature*, which in case of a signing subkey, must have + # an *embedded primary key binding signature*. + if len(key_binding_signatures) != 1: + log.info( + "Ignoring subkey '{}' due to wrong amount of key binding " # pylint: disable=logging-format-interpolation,consider-using-f-string + "signatures ({}), must be exactly 1.".format( + subkey["keyid"], len(key_binding_signatures) + ) + ) + continue + is_valid = handler.verify_signature( + signature, + bundle[PACKET_TYPE_PRIMARY_KEY]["key"], + signed_content, + signature["info"]["hash_algorithm"], + ) + + if not is_valid: + log.info( + "Ignoring subkey '{}' due to invalid key binding signature.".format( # pylint: disable=logging-format-interpolation,consider-using-f-string + subkey["keyid"] + ) + ) + continue + + # If the signature is valid, we may also extract relevant information from + # its "info" field (e.g. subkey expiration date) and assign to it to the + # subkey here + validity_period = signature["info"]["subpackets"].get( + KEY_EXPIRATION_SUBPACKET + ) + if validity_period is not None: + subkey["validity_period"] = struct.unpack(">I", validity_period)[0] + + verified_subkeys[subkey["keyid"]] = subkey + + return verified_subkeys -def _get_verified_subkeys(bundle): - """ - - Helper function to verify the subkey binding signature for all subkeys in - the passed bundle in order to enrich subkeys with additional information - (e.g. expiration dates). Only valid (i.e. parsable) subkeys that are - verifiably bound to the the master key of the bundle are returned. All - other subkeys are discarded. - - - bundle: - GPG key bundle as parsed in parse_pubkey_bundle(). - - - None. - - - None. - - - A dictionary of public keys in the format - securesystemslib.formats.GPG_PUBKEY_SCHEMA, with keyids as dict keys. - - """ - # Create handler shortcut - handler = SIGNATURE_HANDLERS[bundle[PACKET_TYPE_PRIMARY_KEY]["key"]["type"]] - - # Verify subkey binding signatures and only keep verified keys - # See notes about subkey binding signature in RFC4880 5.2.3.3 - verified_subkeys = {} - for subkey_packet, packet_data in bundle[PACKET_TYPE_SUB_KEY].items(): - try: - # Parse subkey if possible and skip if invalid (e.g. not-supported) - subkey = parse_pubkey_payload( - bytearray(subkey_packet[-packet_data["body_len"]:])) - - # TODO: Revise exception taxonomy - except Exception as e: - log.info(e) - continue - - # Construct signed content (see RFC4880 section 5.2.4. paragraph 3) - signed_content = (bundle[PACKET_TYPE_PRIMARY_KEY]["packet"] + b"\x99" + - subkey_packet[1:]) - - # Filter sub key binding signature from other signatures, e.g. subkey - # binding revocation signatures - key_binding_signatures = [] - for signature_packet in packet_data["signatures"]: - try: - signature = parse_signature_packet(signature_packet, - supported_hash_algorithms={SHA1, SHA256, SHA512}, - supported_signature_types={SIGNATURE_TYPE_SUB_KEY_BINDING}, - include_info=True) - # verify_signature requires a "keyid" even if it is short. - # (see parse_signature_packet for more information about keyids) - signature["keyid"] = signature["keyid"] or signature["short_keyid"] - key_binding_signatures.append(signature) - - # TODO: Revise exception taxonomy - except Exception as e: - log.info(e) - continue - # NOTE: As per the V4 key structure diagram in RFC4880 section 12.1., a - # subkey must be followed by exactly one Primary-Key-Binding-Signature. - # Based on inspection of real-world keys and other parts of the RFC (e.g. - # the paragraph below the diagram and paragraph 0x18: Subkey Binding - # Signature in section 5.2.1.) the mandated signature is actually a - # *subkey binding signature*, which in case of a signing subkey, must have - # an *embedded primary key binding signature*. - if len(key_binding_signatures) != 1: - log.info("Ignoring subkey '{}' due to wrong amount of key binding " - "signatures ({}), must be exactly 1.".format(subkey["keyid"], - len(key_binding_signatures))) - continue - is_valid = handler.verify_signature(signature, - bundle[PACKET_TYPE_PRIMARY_KEY]["key"], signed_content, - signature["info"]["hash_algorithm"]) - - if not is_valid: - log.info("Ignoring subkey '{}' due to invalid key binding signature." - .format(subkey["keyid"])) - continue - - # If the signature is valid, we may also extract relevant information from - # its "info" field (e.g. subkey expiration date) and assign to it to the - # subkey here - validity_period = \ - signature["info"]["subpackets"].get(KEY_EXPIRATION_SUBPACKET) - if validity_period is not None: - subkey["validity_period"] = struct.unpack(">I", validity_period)[0] +def get_pubkey_bundle(data, keyid): + """ + + Call function to extract and verify master key and subkeys from the passed + gpg key data, where either the master key or one of the subkeys matches the + passed keyid. + + NOTE: + - If the keyid matches one of the subkeys, a warning is issued to notify + the user about potential privilege escalation + - Subkeys with invalid key binding signatures are discarded + + + data: + Public key data as written to stdout by + securesystemslib.gpg.constants.gpg_export_pubkey_command. + + keyid: + The keyid of the master key or one of its subkeys expected to be + contained in the passed gpg data. + + + securesystemslib.gpg.exceptions.PacketParsingError + If the key data could not be parsed + + securesystemslib.gpg.exceptions.KeyNotFoundError + If the passed data is empty. + If no master key or subkeys could be found that matches the passed + keyid. + + securesystemslib.exceptions.FormatError + If the passed keyid does not match + securesystemslib.formats.KEYID_SCHEMA + + + None. + + + A public key in the format securesystemslib.formats.GPG_PUBKEY_SCHEMA with + optional subkeys. + + """ + formats.KEYID_SCHEMA.check_match(keyid) + if not data: + raise KeyNotFoundError( + "Could not find gpg key '{}' in empty exported key " # pylint: disable=consider-using-f-string + "data.".format(keyid) + ) + + # Parse out master key and subkeys (enriched and verified via certificates + # and binding signatures) + raw_key_bundle = parse_pubkey_bundle(data) + master_public_key = _assign_certified_key_info(raw_key_bundle) + sub_public_keys = _get_verified_subkeys(raw_key_bundle) + + # Since GPG returns all pubkeys associated with a keyid (master key and + # subkeys) we check which key matches the passed keyid. + # If the matching key is a subkey, we warn the user because we return + # the whole bundle (master plus all subkeys) and not only the subkey. + # If no matching key is found we raise a KeyNotFoundError. + for idx, public_key in enumerate( + [master_public_key] + list(sub_public_keys.values()) + ): + if public_key and public_key["keyid"].endswith(keyid.lower()): + if idx > 1: + log.warning( + "Exporting master key '{}' including subkeys '{}' for" # pylint: disable=logging-format-interpolation,consider-using-f-string + " passed keyid '{}'.".format( + master_public_key["keyid"], + ", ".join(list(sub_public_keys.keys())), + keyid, + ) + ) + break - verified_subkeys[subkey["keyid"]] = subkey + else: + raise KeyNotFoundError( + "Could not find gpg key '{}' in exported key data.".format( # pylint: disable=consider-using-f-string + keyid + ) + ) + + # Add subkeys dictionary to master pubkey "subkeys" field if subkeys exist + if sub_public_keys: + master_public_key["subkeys"] = sub_public_keys + + return master_public_key + + +def parse_signature_packet( # pylint: disable=too-many-locals,too-many-branches,too-many-statements + data, + supported_signature_types=None, + supported_hash_algorithms=None, + include_info=False, +): + """ + + Parse the signature information on an RFC4880-encoded binary signature data + buffer. + + NOTE: Older gpg versions (< FULLY_SUPPORTED_MIN_VERSION) might only + reveal the partial key id. It is the callers responsibility to determine + the full keyid based on the partial keyid, e.g. by exporting the related + public and replacing the partial keyid with the full keyid. + + + data: + the RFC4880-encoded binary signature data buffer as described in + section 5.2 (and 5.2.3.1). + supported_signature_types: (optional) + a set of supported signature_types, the signature packet may be + (see securesystemslib.gpg.constants for available types). If None is + specified the signature packet must be of type SIGNATURE_TYPE_BINARY. + supported_hash_algorithms: (optional) + a set of supported hash algorithm ids, the signature packet + may use. Available ids are SHA1, SHA256, SHA512 (see + securesystemslib.gpg.constants). If None is specified, the signature + packet must use SHA256. + include_info: (optional) + a boolean that indicates whether an opaque dictionary should be + added to the returned signature under the key "info". Default is + False. + + + ValueError: if the signature packet is not supported or the data is + malformed + IndexError: if the signature packet is incomplete + + + None. + + + A signature dictionary matching + securesystemslib.formats.GPG_SIGNATURE_SCHEMA with the following special + characteristics: + - The "keyid" field is an empty string if it cannot be determined + - The "short_keyid" is not added if it cannot be determined + - At least one of non-empty "keyid" or "short_keyid" are part of the + signature + + """ + if not supported_signature_types: + supported_signature_types = {SIGNATURE_TYPE_BINARY} + + if not supported_hash_algorithms: + supported_hash_algorithms = {SHA256} + + _, header_len, _, packet_len = gpg_util.parse_packet_header( + data, PACKET_TYPE_SIGNATURE + ) + + data = bytearray(data[header_len:packet_len]) + + ptr = 0 + + # we get the version number, which we also expect to be v4, or we bail + # FIXME: support v3 type signatures (which I haven't seen in the wild) + version_number = data[ptr] + ptr += 1 + if version_number not in SUPPORTED_SIGNATURE_PACKET_VERSIONS: + raise ValueError( + "Signature version '{}' not supported, must be one of " # pylint: disable=consider-using-f-string + "{}.".format(version_number, SUPPORTED_SIGNATURE_PACKET_VERSIONS) + ) + + # Per default we only parse "signatures of a binary document". Other types + # may be allowed by passing type constants via `supported_signature_types`. + # Types include revocation signatures, key binding signatures, persona + # certifications, etc. (see RFC 4880 section 5.2.1.). + signature_type = data[ptr] + ptr += 1 + + if signature_type not in supported_signature_types: + raise ValueError( + "Signature type '{}' not supported, must be one of {} " # pylint: disable=consider-using-f-string + "(see RFC4880 5.2.1. Signature Types).".format( + signature_type, supported_signature_types + ) + ) + + signature_algorithm = data[ptr] + ptr += 1 + + if signature_algorithm not in SUPPORTED_SIGNATURE_ALGORITHMS: + raise ValueError( + "Signature algorithm '{}' not " # pylint: disable=consider-using-f-string + "supported, please verify that your gpg configuration is creating " + "either DSA, RSA, or EdDSA signatures (see RFC4880 9.1. Public-Key " + "Algorithms).".format(signature_algorithm) + ) + + key_type = SUPPORTED_SIGNATURE_ALGORITHMS[signature_algorithm]["type"] + handler = SIGNATURE_HANDLERS[key_type] + + hash_algorithm = data[ptr] + ptr += 1 + + if hash_algorithm not in supported_hash_algorithms: + raise ValueError( + "Hash algorithm '{}' not supported, must be one of {}" # pylint: disable=consider-using-f-string + " (see RFC4880 9.4. Hash Algorithms).".format( + hash_algorithm, supported_hash_algorithms + ) + ) + + # Obtain the hashed octets + hashed_octet_count = struct.unpack(">H", data[ptr : ptr + 2])[0] + ptr += 2 + hashed_subpackets = data[ptr : ptr + hashed_octet_count] + hashed_subpacket_info = gpg_util.parse_subpackets(hashed_subpackets) + + # Check whether we were actually able to read this much hashed octets + if len(hashed_subpackets) != hashed_octet_count: # pragma: no cover + raise ValueError( + "This signature packet seems to be corrupted." + "It is missing hashed octets!" + ) + + ptr += hashed_octet_count + other_headers_ptr = ptr + + unhashed_octet_count = struct.unpack(">H", data[ptr : ptr + 2])[0] + ptr += 2 + + unhashed_subpackets = data[ptr : ptr + unhashed_octet_count] + unhashed_subpacket_info = gpg_util.parse_subpackets(unhashed_subpackets) + + ptr += unhashed_octet_count + + # Use the info dict to return further signature information that may be + # needed for intermediate processing, but does not have to be on the eventual + # signature datastructure + info = { + "signature_type": signature_type, + "hash_algorithm": hash_algorithm, + "creation_time": None, + "subpackets": {}, + } + + keyid = "" + short_keyid = "" + + # Parse "Issuer" (short keyid) and "Issuer Fingerprint" (full keyid) type + # subpackets + # Strategy: Loop over all unhashed and hashed subpackets (in that order!) and + # store only the last of a type. Due to the order in the loop, hashed + # subpackets are prioritized over unhashed subpackets (see NOTEs below). + + # NOTE: A subpacket may be found either in the hashed or unhashed subpacket + # sections of a signature. If a subpacket is not hashed, then the information + # in it cannot be considered definitive because it is not part of the + # signature proper. (see RFC4880 5.2.3.2.) + # NOTE: Signatures may contain conflicting information in subpackets. In most + # cases, an implementation SHOULD use the last subpacket, but MAY use any + # conflict resolution scheme that makes more sense. (see RFC4880 5.2.4.1.) + for idx, subpacket_tuple in enumerate( + unhashed_subpacket_info + hashed_subpacket_info + ): + + # The idx indicates if the info is from the unhashed (first) or + # hashed (second) of the above concatenated lists + is_hashed = idx >= len(unhashed_subpacket_info) + subpacket_type, subpacket_data = subpacket_tuple + + # Warn if expiration subpacket is not hashed + if subpacket_type == KEY_EXPIRATION_SUBPACKET: + if not is_hashed: + log.warning( + "Expiration subpacket not hashed, gpg client possibly " + "exporting a weakly configured key." + ) + + # Full keyids are only available in newer signatures + # (see RFC4880 and rfc4880bis-06 5.2.3.1.) + if subpacket_type == FULL_KEYID_SUBPACKET: # pragma: no cover + # Exclude from coverage for consistent results across test envs + # NOTE: The first byte of the subpacket payload is a version number + # (see rfc4880bis-06 5.2.3.28.) + keyid = binascii.hexlify(subpacket_data[1:]).decode("ascii") + + # We also return the short keyid, because the full might not be available + if subpacket_type == PARTIAL_KEYID_SUBPACKET: + short_keyid = binascii.hexlify(subpacket_data).decode("ascii") + + if subpacket_type == SIG_CREATION_SUBPACKET: + info["creation_time"] = struct.unpack(">I", subpacket_data)[0] + + info["subpackets"][subpacket_type] = subpacket_data + + # Fail if there is no keyid at all (this should not happen) + if not (keyid or short_keyid): # pragma: no cover + raise ValueError( + "This signature packet seems to be corrupted. It does " + "not have an 'Issuer' or 'Issuer Fingerprint' subpacket (see RFC4880 " + "and rfc4880bis-06 5.2.3.1. Signature Subpacket Specification)." + ) + + # Fail if keyid and short keyid are specified but don't match + if keyid and not keyid.endswith(short_keyid): # pragma: no cover + raise ValueError( + "This signature packet seems to be corrupted. The key ID " # pylint: disable=consider-using-f-string + "'{}' of the 'Issuer' subpacket must match the lower 64 bits of the " + "fingerprint '{}' of the 'Issuer Fingerprint' subpacket (see RFC4880 " + "and rfc4880bis-06 5.2.3.28. Issuer Fingerprint).".format( + short_keyid, keyid + ) + ) + + if not info["creation_time"]: # pragma: no cover + raise ValueError( + "This signature packet seems to be corrupted. It does " + "not have a 'Signature Creation Time' subpacket (see RFC4880 5.2.3.4 " + "Signature Creation Time)." + ) + + # Uncomment this variable to obtain the left-hash-bits information (used for + # early rejection) + # left_hash_bits = struct.unpack(">H", data[ptr:ptr+2])[0] + ptr += 2 + + # Finally, fetch the actual signature (as opposed to signature metadata). + signature = handler.get_signature_params(data[ptr:]) + + signature_data = { + "keyid": "{}".format(keyid), # pylint: disable=consider-using-f-string + "other_headers": binascii.hexlify(data[:other_headers_ptr]).decode( + "ascii" + ), + "signature": binascii.hexlify(signature).decode("ascii"), + } - return verified_subkeys + if short_keyid: # pragma: no branch + signature_data["short_keyid"] = short_keyid + if include_info: + signature_data["info"] = info -def get_pubkey_bundle(data, keyid): - """ - - Call function to extract and verify master key and subkeys from the passed - gpg key data, where either the master key or one of the subkeys matches the - passed keyid. - - NOTE: - - If the keyid matches one of the subkeys, a warning is issued to notify - the user about potential privilege escalation - - Subkeys with invalid key binding signatures are discarded - - - data: - Public key data as written to stdout by - securesystemslib.gpg.constants.gpg_export_pubkey_command. - - keyid: - The keyid of the master key or one of its subkeys expected to be - contained in the passed gpg data. - - - securesystemslib.gpg.exceptions.PacketParsingError - If the key data could not be parsed - - securesystemslib.gpg.exceptions.KeyNotFoundError - If the passed data is empty. - If no master key or subkeys could be found that matches the passed - keyid. - - securesystemslib.exceptions.FormatError - If the passed keyid does not match - securesystemslib.formats.KEYID_SCHEMA - - - None. - - - A public key in the format securesystemslib.formats.GPG_PUBKEY_SCHEMA with - optional subkeys. - - """ - formats.KEYID_SCHEMA.check_match(keyid) - if not data: - raise KeyNotFoundError("Could not find gpg key '{}' in empty exported key " - "data.".format(keyid)) - - # Parse out master key and subkeys (enriched and verified via certificates - # and binding signatures) - raw_key_bundle = parse_pubkey_bundle(data) - master_public_key = _assign_certified_key_info(raw_key_bundle) - sub_public_keys = _get_verified_subkeys(raw_key_bundle) - - # Since GPG returns all pubkeys associated with a keyid (master key and - # subkeys) we check which key matches the passed keyid. - # If the matching key is a subkey, we warn the user because we return - # the whole bundle (master plus all subkeys) and not only the subkey. - # If no matching key is found we raise a KeyNotFoundError. - for idx, public_key in enumerate( - [master_public_key] + list(sub_public_keys.values())): - if public_key and public_key["keyid"].endswith(keyid.lower()): - if idx > 1: - log.warning("Exporting master key '{}' including subkeys '{}' for" - " passed keyid '{}'.".format(master_public_key["keyid"], - ", ".join(list(sub_public_keys.keys())), keyid)) - break - - else: - raise KeyNotFoundError("Could not find gpg key '{}' in exported key data." - .format(keyid)) - - # Add subkeys dictionary to master pubkey "subkeys" field if subkeys exist - if sub_public_keys: - master_public_key["subkeys"] = sub_public_keys - - return master_public_key - - -def parse_signature_packet(data, supported_signature_types=None, - supported_hash_algorithms=None, include_info=False): - """ - - Parse the signature information on an RFC4880-encoded binary signature data - buffer. - - NOTE: Older gpg versions (< FULLY_SUPPORTED_MIN_VERSION) might only - reveal the partial key id. It is the callers responsibility to determine - the full keyid based on the partial keyid, e.g. by exporting the related - public and replacing the partial keyid with the full keyid. - - - data: - the RFC4880-encoded binary signature data buffer as described in - section 5.2 (and 5.2.3.1). - supported_signature_types: (optional) - a set of supported signature_types, the signature packet may be - (see securesystemslib.gpg.constants for available types). If None is - specified the signature packet must be of type SIGNATURE_TYPE_BINARY. - supported_hash_algorithms: (optional) - a set of supported hash algorithm ids, the signature packet - may use. Available ids are SHA1, SHA256, SHA512 (see - securesystemslib.gpg.constants). If None is specified, the signature - packet must use SHA256. - include_info: (optional) - a boolean that indicates whether an opaque dictionary should be - added to the returned signature under the key "info". Default is - False. - - - ValueError: if the signature packet is not supported or the data is - malformed - IndexError: if the signature packet is incomplete - - - None. - - - A signature dictionary matching - securesystemslib.formats.GPG_SIGNATURE_SCHEMA with the following special - characteristics: - - The "keyid" field is an empty string if it cannot be determined - - The "short_keyid" is not added if it cannot be determined - - At least one of non-empty "keyid" or "short_keyid" are part of the - signature - - """ - if not supported_signature_types: - supported_signature_types = {SIGNATURE_TYPE_BINARY} - - if not supported_hash_algorithms: - supported_hash_algorithms = {SHA256} - - _, header_len, _, packet_len = gpg_util.parse_packet_header( - data, PACKET_TYPE_SIGNATURE) - - data = bytearray(data[header_len:packet_len]) - - ptr = 0 - - # we get the version number, which we also expect to be v4, or we bail - # FIXME: support v3 type signatures (which I haven't seen in the wild) - version_number = data[ptr] - ptr += 1 - if version_number not in SUPPORTED_SIGNATURE_PACKET_VERSIONS: - raise ValueError("Signature version '{}' not supported, must be one of " - "{}.".format(version_number, SUPPORTED_SIGNATURE_PACKET_VERSIONS)) - - # Per default we only parse "signatures of a binary document". Other types - # may be allowed by passing type constants via `supported_signature_types`. - # Types include revocation signatures, key binding signatures, persona - # certifications, etc. (see RFC 4880 section 5.2.1.). - signature_type = data[ptr] - ptr += 1 - - if signature_type not in supported_signature_types: - raise ValueError("Signature type '{}' not supported, must be one of {} " - "(see RFC4880 5.2.1. Signature Types).".format(signature_type, - supported_signature_types)) - - signature_algorithm = data[ptr] - ptr += 1 - - if signature_algorithm not in SUPPORTED_SIGNATURE_ALGORITHMS: - raise ValueError("Signature algorithm '{}' not " - "supported, please verify that your gpg configuration is creating " - "either DSA, RSA, or EdDSA signatures (see RFC4880 9.1. Public-Key " - "Algorithms).".format(signature_algorithm)) - - key_type = SUPPORTED_SIGNATURE_ALGORITHMS[signature_algorithm]['type'] - handler = SIGNATURE_HANDLERS[key_type] - - hash_algorithm = data[ptr] - ptr += 1 - - if hash_algorithm not in supported_hash_algorithms: - raise ValueError("Hash algorithm '{}' not supported, must be one of {}" - " (see RFC4880 9.4. Hash Algorithms).".format(hash_algorithm, - supported_hash_algorithms)) - - # Obtain the hashed octets - hashed_octet_count = struct.unpack(">H", data[ptr:ptr+2])[0] - ptr += 2 - hashed_subpackets = data[ptr:ptr+hashed_octet_count] - hashed_subpacket_info = gpg_util.parse_subpackets(hashed_subpackets) - - # Check whether we were actually able to read this much hashed octets - if len(hashed_subpackets) != hashed_octet_count: # pragma: no cover - raise ValueError("This signature packet seems to be corrupted." - "It is missing hashed octets!") - - ptr += hashed_octet_count - other_headers_ptr = ptr - - unhashed_octet_count = struct.unpack(">H", data[ptr: ptr + 2])[0] - ptr += 2 - - unhashed_subpackets = data[ptr:ptr+unhashed_octet_count] - unhashed_subpacket_info = gpg_util.parse_subpackets(unhashed_subpackets) - - ptr += unhashed_octet_count - - # Use the info dict to return further signature information that may be - # needed for intermediate processing, but does not have to be on the eventual - # signature datastructure - info = { - "signature_type": signature_type, - "hash_algorithm": hash_algorithm, - "creation_time": None, - "subpackets": {}, - } - - keyid = "" - short_keyid = "" - - - # Parse "Issuer" (short keyid) and "Issuer Fingerprint" (full keyid) type - # subpackets - # Strategy: Loop over all unhashed and hashed subpackets (in that order!) and - # store only the last of a type. Due to the order in the loop, hashed - # subpackets are prioritized over unhashed subpackets (see NOTEs below). - - # NOTE: A subpacket may be found either in the hashed or unhashed subpacket - # sections of a signature. If a subpacket is not hashed, then the information - # in it cannot be considered definitive because it is not part of the - # signature proper. (see RFC4880 5.2.3.2.) - # NOTE: Signatures may contain conflicting information in subpackets. In most - # cases, an implementation SHOULD use the last subpacket, but MAY use any - # conflict resolution scheme that makes more sense. (see RFC4880 5.2.4.1.) - for idx, subpacket_tuple in \ - enumerate(unhashed_subpacket_info + hashed_subpacket_info): - - # The idx indicates if the info is from the unhashed (first) or - # hashed (second) of the above concatenated lists - is_hashed = (idx >= len(unhashed_subpacket_info)) - subpacket_type, subpacket_data = subpacket_tuple - - # Warn if expiration subpacket is not hashed - if subpacket_type == KEY_EXPIRATION_SUBPACKET: - if not is_hashed: - log.warning("Expiration subpacket not hashed, gpg client possibly " - "exporting a weakly configured key.") - - # Full keyids are only available in newer signatures - # (see RFC4880 and rfc4880bis-06 5.2.3.1.) - if subpacket_type == FULL_KEYID_SUBPACKET: # pragma: no cover - # Exclude from coverage for consistent results across test envs - # NOTE: The first byte of the subpacket payload is a version number - # (see rfc4880bis-06 5.2.3.28.) - keyid = binascii.hexlify(subpacket_data[1:]).decode("ascii") - - # We also return the short keyid, because the full might not be available - if subpacket_type == PARTIAL_KEYID_SUBPACKET: - short_keyid = binascii.hexlify(subpacket_data).decode("ascii") - - if subpacket_type == SIG_CREATION_SUBPACKET: - info["creation_time"] = struct.unpack(">I", subpacket_data)[0] - - info["subpackets"][subpacket_type] = subpacket_data - - - # Fail if there is no keyid at all (this should not happen) - if not (keyid or short_keyid): # pragma: no cover - raise ValueError("This signature packet seems to be corrupted. It does " - "not have an 'Issuer' or 'Issuer Fingerprint' subpacket (see RFC4880 " - "and rfc4880bis-06 5.2.3.1. Signature Subpacket Specification).") - - # Fail if keyid and short keyid are specified but don't match - if keyid and not keyid.endswith(short_keyid): # pragma: no cover - raise ValueError("This signature packet seems to be corrupted. The key ID " - "'{}' of the 'Issuer' subpacket must match the lower 64 bits of the " - "fingerprint '{}' of the 'Issuer Fingerprint' subpacket (see RFC4880 " - "and rfc4880bis-06 5.2.3.28. Issuer Fingerprint).".format( - short_keyid, keyid)) - - if not info["creation_time"]: # pragma: no cover - raise ValueError("This signature packet seems to be corrupted. It does " - "not have a 'Signature Creation Time' subpacket (see RFC4880 5.2.3.4 " - "Signature Creation Time).") - - # Uncomment this variable to obtain the left-hash-bits information (used for - # early rejection) - #left_hash_bits = struct.unpack(">H", data[ptr:ptr+2])[0] - ptr += 2 - - # Finally, fetch the actual signature (as opposed to signature metadata). - signature = handler.get_signature_params(data[ptr:]) - - signature_data = { - 'keyid': "{}".format(keyid), - 'other_headers': binascii.hexlify(data[:other_headers_ptr]).decode('ascii'), - 'signature': binascii.hexlify(signature).decode('ascii') - } - - if short_keyid: # pragma: no branch - signature_data["short_keyid"] = short_keyid - - if include_info: - signature_data["info"] = info - - return signature_data + return signature_data diff --git a/securesystemslib/gpg/constants.py b/securesystemslib/gpg/constants.py index c5afcba6..df023428 100644 --- a/securesystemslib/gpg/constants.py +++ b/securesystemslib/gpg/constants.py @@ -18,63 +18,72 @@ import functools import logging import os -import subprocess +import subprocess # nosec from securesystemslib import process log = logging.getLogger(__name__) + @functools.lru_cache(maxsize=3) def is_available_gnupg(gnupg: str) -> bool: - """Returns whether gnupg points to a gpg binary.""" - gpg_version_cmd = gnupg + " --version" - try: - process.run(gpg_version_cmd, stdout=process.PIPE, stderr=process.PIPE) - return True - except (OSError, subprocess.TimeoutExpired): - return False + """Returns whether gnupg points to a gpg binary.""" + gpg_version_cmd = gnupg + " --version" + try: + process.run(gpg_version_cmd, stdout=process.PIPE, stderr=process.PIPE) + return True + except (OSError, subprocess.TimeoutExpired): + return False -GPG_ENV_COMMAND = os.environ.get('GNUPG') +GPG_ENV_COMMAND = os.environ.get("GNUPG") GPG2_COMMAND = "gpg2" GPG1_COMMAND = "gpg" def gpg_command() -> str: - """Returns command to run GPG, or ``""``` if not found).""" - # By default, we allow providing GPG client through the environment - # assuming gpg2 as default value and test if exists. Otherwise, we assume gpg - # exists. - if GPG_ENV_COMMAND: - if is_available_gnupg(GPG_ENV_COMMAND): - return GPG_ENV_COMMAND - elif is_available_gnupg(GPG2_COMMAND): - return GPG2_COMMAND - elif is_available_gnupg(GPG1_COMMAND): - return GPG1_COMMAND - return "" + """Returns command to run GPG, or ``""``` if not found).""" + # By default, we allow providing GPG client through the environment + # assuming gpg2 as default value and test if exists. Otherwise, we assume gpg + # exists. + if GPG_ENV_COMMAND: + if is_available_gnupg(GPG_ENV_COMMAND): + return GPG_ENV_COMMAND + elif is_available_gnupg(GPG2_COMMAND): + return GPG2_COMMAND + elif is_available_gnupg(GPG1_COMMAND): + return GPG1_COMMAND + return "" + def have_gpg() -> bool: - """Returns True if a gpg_command is available.""" - return bool(gpg_command()) + """Returns True if a gpg_command is available.""" + return bool(gpg_command()) + def gpg_version_command() -> str: - """Returns the command to get the current GPG version.""" - return f"{gpg_command()} --version" + """Returns the command to get the current GPG version.""" + return f"{gpg_command()} --version" + FULLY_SUPPORTED_MIN_VERSION = "2.1.0" NO_GPG_MSG = ( - f"GPG support requires a GPG client. 'gpg2' or 'gpg' with version " - f"{FULLY_SUPPORTED_MIN_VERSION} or newer is fully supported." + f"GPG support requires a GPG client. 'gpg2' or 'gpg' with version " + f"{FULLY_SUPPORTED_MIN_VERSION} or newer is fully supported." ) + def gpg_sign_command(keyarg: str, homearg: str) -> str: - """Returns the command to use GPG to sign STDIN.""" - return f"{gpg_command()} --detach-sign --digest-algo SHA256 {keyarg} {homearg}" + """Returns the command to use GPG to sign STDIN.""" + return ( + f"{gpg_command()} --detach-sign --digest-algo SHA256 {keyarg} {homearg}" + ) + def gpg_export_pubkey_command(homearg: str, keyid: str): - """Returns the GPG command to export a public key.""" - return f"{gpg_command()} {homearg} --export {keyid}" + """Returns the GPG command to export a public key.""" + return f"{gpg_command()} {homearg} --export {keyid}" + # See RFC4880 section 4.3. Packet Tags for a list of all packet types The # relevant packets defined below are described in sections 5.2 (signature), diff --git a/securesystemslib/gpg/dsa.py b/securesystemslib/gpg/dsa.py index 3610899f..979b8381 100644 --- a/securesystemslib/gpg/dsa.py +++ b/securesystemslib/gpg/dsa.py @@ -17,228 +17,230 @@ import binascii CRYPTO = True -NO_CRYPTO_MSG = 'DSA key support for GPG requires the cryptography library' +NO_CRYPTO_MSG = "DSA key support for GPG requires the cryptography library" try: - from cryptography.exceptions import InvalidSignature - from cryptography.hazmat import backends - from cryptography.hazmat.primitives.asymmetric import dsa - from cryptography.hazmat.primitives.asymmetric import utils as dsautils + from cryptography.exceptions import InvalidSignature + from cryptography.hazmat import backends + from cryptography.hazmat.primitives.asymmetric import dsa + from cryptography.hazmat.primitives.asymmetric import utils as dsautils except ImportError: - CRYPTO = False + CRYPTO = False -from securesystemslib import exceptions -from securesystemslib import formats -from securesystemslib.gpg.exceptions import PacketParsingError +# pylint: disable=wrong-import-position +from securesystemslib import exceptions, formats from securesystemslib.gpg import util as gpg_util +from securesystemslib.gpg.exceptions import PacketParsingError +# pylint: enable=wrong-import-position -def create_pubkey(pubkey_info): - """ - - Create and return a DSAPublicKey object from the passed pubkey_info - using pyca/cryptography. - - - pubkey_info: - The DSA pubkey info dictionary as specified by - securesystemslib.formats.GPG_DSA_PUBKEY_SCHEMA - - - securesystemslib.exceptions.FormatError if - pubkey_info does not match securesystemslib.formats.GPG_DSA_PUBKEY_SCHEMA - - securesystemslib.exceptions.UnsupportedLibraryError if - the cryptography module is not available - - - A cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey based on the - passed pubkey_info. - - """ - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - - formats.GPG_DSA_PUBKEY_SCHEMA.check_match(pubkey_info) - y = int(pubkey_info['keyval']['public']['y'], 16) - g = int(pubkey_info['keyval']['public']['g'], 16) - p = int(pubkey_info['keyval']['public']['p'], 16) - q = int(pubkey_info['keyval']['public']['q'], 16) - parameter_numbers = dsa.DSAParameterNumbers(p, q, g) - pubkey = dsa.DSAPublicNumbers(y, parameter_numbers).public_key( - backends.default_backend()) +def create_pubkey(pubkey_info): + """ + + Create and return a DSAPublicKey object from the passed pubkey_info + using pyca/cryptography. + + + pubkey_info: + The DSA pubkey info dictionary as specified by + securesystemslib.formats.GPG_DSA_PUBKEY_SCHEMA + + + securesystemslib.exceptions.FormatError if + pubkey_info does not match securesystemslib.formats.GPG_DSA_PUBKEY_SCHEMA + + securesystemslib.exceptions.UnsupportedLibraryError if + the cryptography module is not available + + + A cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey based on the + passed pubkey_info. + + """ + if not CRYPTO: # pragma: no cover + raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) + + formats.GPG_DSA_PUBKEY_SCHEMA.check_match(pubkey_info) + + y = int(pubkey_info["keyval"]["public"]["y"], 16) + g = int(pubkey_info["keyval"]["public"]["g"], 16) + p = int(pubkey_info["keyval"]["public"]["p"], 16) + q = int(pubkey_info["keyval"]["public"]["q"], 16) + parameter_numbers = dsa.DSAParameterNumbers(p, q, g) + pubkey = dsa.DSAPublicNumbers(y, parameter_numbers).public_key( + backends.default_backend() + ) - return pubkey + return pubkey def get_pubkey_params(data): - """ - - Parse the public-key parameters as multi-precision-integers. - - - data: - the RFC4880-encoded public key parameters data buffer as described - in the fifth paragraph of section 5.5.2. - - - securesystemslib.gpg.exceptions.PacketParsingError: - if the public key parameters are malformed - - - None. - - - The parsed DSA public key in the format - securesystemslib.formats.GPG_DSA_PUBKEY_SCHEMA. - - """ - ptr = 0 - - prime_p_length = gpg_util.get_mpi_length(data[ptr: ptr + 2]) - ptr += 2 - prime_p = data[ptr:ptr + prime_p_length] - if len(prime_p) != prime_p_length: # pragma: no cover - raise PacketParsingError("This MPI was truncated!") - ptr += prime_p_length - - group_order_q_length = gpg_util.get_mpi_length(data[ptr: ptr + 2]) - ptr += 2 - group_order_q = data[ptr:ptr + group_order_q_length] - if len(group_order_q) != group_order_q_length: # pragma: no cover - raise PacketParsingError("This MPI has been truncated!") - ptr += group_order_q_length - - generator_length = gpg_util.get_mpi_length(data[ptr: ptr + 2]) - ptr += 2 - generator = data[ptr:ptr + generator_length] - if len(generator) != generator_length: # pragma: no cover - raise PacketParsingError("This MPI has been truncated!") - ptr += generator_length - - value_y_length = gpg_util.get_mpi_length(data[ptr: ptr + 2]) - ptr += 2 - value_y = data[ptr:ptr + value_y_length] - if len(value_y) != value_y_length: # pragma: no cover - raise PacketParsingError("This MPI has been truncated!") - - return { - "y": binascii.hexlify(value_y).decode('ascii'), - "p": binascii.hexlify(prime_p).decode("ascii"), - "g": binascii.hexlify(generator).decode("ascii"), - "q": binascii.hexlify(group_order_q).decode("ascii"), - } + """ + + Parse the public-key parameters as multi-precision-integers. + + + data: + the RFC4880-encoded public key parameters data buffer as described + in the fifth paragraph of section 5.5.2. + + + securesystemslib.gpg.exceptions.PacketParsingError: + if the public key parameters are malformed + + + None. + + + The parsed DSA public key in the format + securesystemslib.formats.GPG_DSA_PUBKEY_SCHEMA. + + """ + ptr = 0 + + prime_p_length = gpg_util.get_mpi_length(data[ptr : ptr + 2]) + ptr += 2 + prime_p = data[ptr : ptr + prime_p_length] + if len(prime_p) != prime_p_length: # pragma: no cover + raise PacketParsingError("This MPI was truncated!") + ptr += prime_p_length + + group_order_q_length = gpg_util.get_mpi_length(data[ptr : ptr + 2]) + ptr += 2 + group_order_q = data[ptr : ptr + group_order_q_length] + if len(group_order_q) != group_order_q_length: # pragma: no cover + raise PacketParsingError("This MPI has been truncated!") + ptr += group_order_q_length + + generator_length = gpg_util.get_mpi_length(data[ptr : ptr + 2]) + ptr += 2 + generator = data[ptr : ptr + generator_length] + if len(generator) != generator_length: # pragma: no cover + raise PacketParsingError("This MPI has been truncated!") + ptr += generator_length + + value_y_length = gpg_util.get_mpi_length(data[ptr : ptr + 2]) + ptr += 2 + value_y = data[ptr : ptr + value_y_length] + if len(value_y) != value_y_length: # pragma: no cover + raise PacketParsingError("This MPI has been truncated!") + + return { + "y": binascii.hexlify(value_y).decode("ascii"), + "p": binascii.hexlify(prime_p).decode("ascii"), + "g": binascii.hexlify(generator).decode("ascii"), + "q": binascii.hexlify(group_order_q).decode("ascii"), + } def get_signature_params(data): - """ - - Parse the signature parameters as multi-precision-integers. - - - data: - the RFC4880-encoded signature data buffer as described - in the fourth paragraph of section 5.2.2 + """ + + Parse the signature parameters as multi-precision-integers. - - securesystemslib.gpg.exceptions.PacketParsingError: - if the public key parameters are malformed + + data: + the RFC4880-encoded signature data buffer as described + in the fourth paragraph of section 5.2.2 - securesystemslib.exceptions.UnsupportedLibraryError: - if the cryptography module is not available + + securesystemslib.gpg.exceptions.PacketParsingError: + if the public key parameters are malformed - - None. + securesystemslib.exceptions.UnsupportedLibraryError: + if the cryptography module is not available - - The decoded signature buffer - """ - if not CRYPTO: # pragma: no cover - return exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) + + None. - ptr = 0 - r_length = gpg_util.get_mpi_length(data[ptr:ptr+2]) - ptr += 2 - r = data[ptr:ptr + r_length] - if len(r) != r_length: # pragma: no cover - raise PacketParsingError("r-value truncated in signature") - ptr += r_length + + The decoded signature buffer + """ + if not CRYPTO: # pragma: no cover + return exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - s_length = gpg_util.get_mpi_length(data[ptr: ptr+2]) - ptr += 2 - s = data[ptr: ptr + s_length] - if len(s) != s_length: # pragma: no cover - raise PacketParsingError("s-value truncated in signature") + ptr = 0 + r_length = gpg_util.get_mpi_length(data[ptr : ptr + 2]) + ptr += 2 + r = data[ptr : ptr + r_length] + if len(r) != r_length: # pragma: no cover + raise PacketParsingError("r-value truncated in signature") + ptr += r_length - s = int(binascii.hexlify(s), 16) - r = int(binascii.hexlify(r), 16) + s_length = gpg_util.get_mpi_length(data[ptr : ptr + 2]) + ptr += 2 + s = data[ptr : ptr + s_length] + if len(s) != s_length: # pragma: no cover + raise PacketParsingError("s-value truncated in signature") - signature = dsautils.encode_dss_signature(r, s) + s = int(binascii.hexlify(s), 16) + r = int(binascii.hexlify(r), 16) - return signature + signature = dsautils.encode_dss_signature(r, s) + return signature -def verify_signature(signature_object, pubkey_info, content, - hash_algorithm_id): - """ - - Verify the passed signature against the passed content with the passed - DSA public key using pyca/cryptography. - - signature_object: - A signature dictionary as specified by - securesystemslib.formats.GPG_SIGNATURE_SCHEMA +def verify_signature(signature_object, pubkey_info, content, hash_algorithm_id): + """ + + Verify the passed signature against the passed content with the passed + DSA public key using pyca/cryptography. - pubkey_info: - The DSA public key info dictionary as specified by - securesystemslib.formats.GPG_DSA_PUBKEY_SCHEMA + + signature_object: + A signature dictionary as specified by + securesystemslib.formats.GPG_SIGNATURE_SCHEMA - hash_algorithm_id: - one of SHA1, SHA256, SHA512 (see securesystemslib.gpg.constants) - used to verify the signature - NOTE: Overrides any hash algorithm specification in "pubkey_info"'s - "hashes" or "method" fields. + pubkey_info: + The DSA public key info dictionary as specified by + securesystemslib.formats.GPG_DSA_PUBKEY_SCHEMA - content: - The signed bytes against which the signature is verified + hash_algorithm_id: + one of SHA1, SHA256, SHA512 (see securesystemslib.gpg.constants) + used to verify the signature + NOTE: Overrides any hash algorithm specification in "pubkey_info"'s + "hashes" or "method" fields. - - securesystemslib.exceptions.FormatError if: - signature_object does not match securesystemslib.formats.GPG_SIGNATURE_SCHEMA - pubkey_info does not match securesystemslib.formats.GPG_DSA_PUBKEY_SCHEMA + content: + The signed bytes against which the signature is verified - securesystemslib.exceptions.UnsupportedLibraryError if: - the cryptography module is not available + + securesystemslib.exceptions.FormatError if: + signature_object does not match securesystemslib.formats.GPG_SIGNATURE_SCHEMA + pubkey_info does not match securesystemslib.formats.GPG_DSA_PUBKEY_SCHEMA - ValueError: - if the passed hash_algorithm_id is not supported (see - securesystemslib.gpg.util.get_hashing_class) + securesystemslib.exceptions.UnsupportedLibraryError if: + the cryptography module is not available - - True if signature verification passes and False otherwise + ValueError: + if the passed hash_algorithm_id is not supported (see + securesystemslib.gpg.util.get_hashing_class) - """ - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) + + True if signature verification passes and False otherwise - formats.GPG_SIGNATURE_SCHEMA.check_match(signature_object) - formats.GPG_DSA_PUBKEY_SCHEMA.check_match(pubkey_info) + """ + if not CRYPTO: # pragma: no cover + raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - hasher = gpg_util.get_hashing_class(hash_algorithm_id) + formats.GPG_SIGNATURE_SCHEMA.check_match(signature_object) + formats.GPG_DSA_PUBKEY_SCHEMA.check_match(pubkey_info) - pubkey_object = create_pubkey(pubkey_info) + hasher = gpg_util.get_hashing_class(hash_algorithm_id) - digest = gpg_util.hash_object( - binascii.unhexlify(signature_object['other_headers']), - hasher(), content) + pubkey_object = create_pubkey(pubkey_info) - try: - pubkey_object.verify( - binascii.unhexlify(signature_object['signature']), - digest, - dsautils.Prehashed(hasher()) + digest = gpg_util.hash_object( + binascii.unhexlify(signature_object["other_headers"]), hasher(), content ) - return True - except InvalidSignature: - return False + + try: + pubkey_object.verify( + binascii.unhexlify(signature_object["signature"]), + digest, + dsautils.Prehashed(hasher()), + ) + return True + except InvalidSignature: + return False diff --git a/securesystemslib/gpg/eddsa.py b/securesystemslib/gpg/eddsa.py index eed54f08..a052f398 100644 --- a/securesystemslib/gpg/eddsa.py +++ b/securesystemslib/gpg/eddsa.py @@ -18,18 +18,19 @@ """ import binascii -from securesystemslib import exceptions -from securesystemslib import formats +from securesystemslib import exceptions, formats from securesystemslib.gpg import util as gpg_util from securesystemslib.gpg.exceptions import PacketParsingError CRYPTO = True -NO_CRYPTO_MSG = 'EdDSA key support for GPG requires the cryptography library' +NO_CRYPTO_MSG = "EdDSA key support for GPG requires the cryptography library" try: - from cryptography.hazmat.primitives.asymmetric import ed25519 as pyca_ed25519 - from cryptography.exceptions import InvalidSignature + from cryptography.exceptions import InvalidSignature + from cryptography.hazmat.primitives.asymmetric import ( + ed25519 as pyca_ed25519, + ) except ImportError: - CRYPTO = False + CRYPTO = False # ECC Curve OID (see RFC4880-bis8 9.2.) ED25519_PUBLIC_KEY_OID = bytearray.fromhex("2B 06 01 04 01 DA 47 0F 01") @@ -41,214 +42,212 @@ ED25519_SIG_LENGTH = 64 - def get_pubkey_params(data): - """ - - Parse algorithm-specific part for EdDSA public keys - - See RFC4880-bis8 sections 5.6.5. Algorithm-Specific Part for EdDSA Keys, - 9.2. ECC Curve OID and 13.3. EdDSA Point Format for more details. + """ + + Parse algorithm-specific part for EdDSA public keys - - data: - The EdDSA public key data AFTER the one-octet number denoting the - public-key algorithm of this key. + See RFC4880-bis8 sections 5.6.5. Algorithm-Specific Part for EdDSA Keys, + 9.2. ECC Curve OID and 13.3. EdDSA Point Format for more details. - - securesystemslib.gpg.exceptions.PacketParsingError or IndexError: - if the public key data is malformed. + + data: + The EdDSA public key data AFTER the one-octet number denoting the + public-key algorithm of this key. - - None. + + securesystemslib.gpg.exceptions.PacketParsingError or IndexError: + if the public key data is malformed. - - A dictionary with an element "q" that holds the ascii hex representation - of the MPI of an EC point representing an EdDSA public key that conforms - with securesystemslib.formats.GPG_ED25519_PUBKEY_SCHEMA. + + None. - """ - ptr = 0 + + A dictionary with an element "q" that holds the ascii hex representation + of the MPI of an EC point representing an EdDSA public key that conforms + with securesystemslib.formats.GPG_ED25519_PUBKEY_SCHEMA. - curve_oid_len = data[ptr] - ptr += 1 + """ + ptr = 0 - curve_oid = data[ptr:ptr + curve_oid_len] - ptr += curve_oid_len + curve_oid_len = data[ptr] + ptr += 1 - # See 9.2. ECC Curve OID - if curve_oid != ED25519_PUBLIC_KEY_OID: - raise PacketParsingError( - "bad ed25519 curve OID '{}', expected {}'".format( - curve_oid, ED25519_PUBLIC_KEY_OID)) + curve_oid = data[ptr : ptr + curve_oid_len] + ptr += curve_oid_len - # See 13.3. EdDSA Point Format - public_key_len = gpg_util.get_mpi_length(data[ptr:ptr + 2]) - ptr += 2 + # See 9.2. ECC Curve OID + if curve_oid != ED25519_PUBLIC_KEY_OID: + raise PacketParsingError( + "bad ed25519 curve OID '{}', expected {}'".format( # pylint: disable=consider-using-f-string + curve_oid, ED25519_PUBLIC_KEY_OID + ) + ) - if public_key_len != ED25519_PUBLIC_KEY_LENGTH: - raise PacketParsingError( - "bad ed25519 MPI length '{}', expected {}'".format( - public_key_len, ED25519_PUBLIC_KEY_LENGTH)) + # See 13.3. EdDSA Point Format + public_key_len = gpg_util.get_mpi_length(data[ptr : ptr + 2]) + ptr += 2 - public_key_prefix = data[ptr] - ptr += 1 + if public_key_len != ED25519_PUBLIC_KEY_LENGTH: + raise PacketParsingError( + "bad ed25519 MPI length '{}', expected {}'".format( # pylint: disable=consider-using-f-string + public_key_len, ED25519_PUBLIC_KEY_LENGTH + ) + ) - if public_key_prefix != ED25519_PUBLIC_KEY_PREFIX: - raise PacketParsingError( - "bad ed25519 MPI prefix '{}', expected '{}'".format( - public_key_prefix, ED25519_PUBLIC_KEY_PREFIX)) + public_key_prefix = data[ptr] + ptr += 1 - public_key = data[ptr:ptr + public_key_len - 1] + if public_key_prefix != ED25519_PUBLIC_KEY_PREFIX: + raise PacketParsingError( + "bad ed25519 MPI prefix '{}', expected '{}'".format( # pylint: disable=consider-using-f-string + public_key_prefix, ED25519_PUBLIC_KEY_PREFIX + ) + ) - return { - "q": binascii.hexlify(public_key).decode("ascii") - } + public_key = data[ptr : ptr + public_key_len - 1] + return {"q": binascii.hexlify(public_key).decode("ascii")} def get_signature_params(data): - """ - - Parse algorithm-specific fields for EdDSA signatures. + """ + + Parse algorithm-specific fields for EdDSA signatures. - See RFC4880-bis8 section 5.2.3. Version 4 and 5 Signature Packet Formats - for more details. + See RFC4880-bis8 section 5.2.3. Version 4 and 5 Signature Packet Formats + for more details. - - data: - The EdDSA signature data AFTER the two-octet field holding the - left 16 bits of the signed hash value. + + data: + The EdDSA signature data AFTER the two-octet field holding the + left 16 bits of the signed hash value. - - IndexError if the signature data is malformed. + + IndexError if the signature data is malformed. - - None. + + None. - - The concatenation of the parsed MPI R and S values of the EdDSA signature, - i.e. ENC(R) || ENC(S) (see RFC8032 3.4 Verify). + + The concatenation of the parsed MPI R and S values of the EdDSA signature, + i.e. ENC(R) || ENC(S) (see RFC8032 3.4 Verify). - """ - ptr = 0 - r_length = gpg_util.get_mpi_length(data[ptr:ptr + 2]) + """ + ptr = 0 + r_length = gpg_util.get_mpi_length(data[ptr : ptr + 2]) - ptr += 2 - r = data[ptr:ptr + r_length] - ptr += r_length + ptr += 2 + r = data[ptr : ptr + r_length] + ptr += r_length - s_length = gpg_util.get_mpi_length(data[ptr:ptr + 2]) - ptr += 2 - s = data[ptr:ptr + s_length] + s_length = gpg_util.get_mpi_length(data[ptr : ptr + 2]) + ptr += 2 + s = data[ptr : ptr + s_length] - # Left-zero-pad 'r' and 's' values that are shorter than required by RFC 8032 - # (5.1.6.), to make up for omitted leading zeros in RFC 4880 (3.2.) MPIs. - # This is especially important for 's', which is little-endian. - r = r.rjust(ED25519_SIG_LENGTH // 2, b"\x00") - s = s.rjust(ED25519_SIG_LENGTH // 2, b"\x00") - - return r + s + # Left-zero-pad 'r' and 's' values that are shorter than required by RFC 8032 + # (5.1.6.), to make up for omitted leading zeros in RFC 4880 (3.2.) MPIs. + # This is especially important for 's', which is little-endian. + r = r.rjust(ED25519_SIG_LENGTH // 2, b"\x00") + s = s.rjust(ED25519_SIG_LENGTH // 2, b"\x00") + return r + s def create_pubkey(pubkey_info): - """ - - Create and return an Ed25519PublicKey object from the passed pubkey_info - using pyca/cryptography. - - - pubkey_info: - The ED25519 public key dictionary as specified by - securesystemslib.formats.GPG_ED25519_PUBKEY_SCHEMA - - - securesystemslib.exceptions.FormatError if - pubkey_info does not match securesystemslib.formats.GPG_DSA_PUBKEY_SCHEMA + """ + + Create and return an Ed25519PublicKey object from the passed pubkey_info + using pyca/cryptography. + + + pubkey_info: + The ED25519 public key dictionary as specified by + securesystemslib.formats.GPG_ED25519_PUBKEY_SCHEMA - securesystemslib.exceptions.UnsupportedLibraryError if - the cryptography module is unavailable + + securesystemslib.exceptions.FormatError if + pubkey_info does not match securesystemslib.formats.GPG_DSA_PUBKEY_SCHEMA - - A cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey based - on the passed pubkey_info. + securesystemslib.exceptions.UnsupportedLibraryError if + the cryptography module is unavailable - """ - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) + + A cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey based + on the passed pubkey_info. - formats.GPG_ED25519_PUBKEY_SCHEMA.check_match(pubkey_info) + """ + if not CRYPTO: # pragma: no cover + raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - public_bytes = binascii.unhexlify(pubkey_info["keyval"]["public"]["q"]) - public_key = pyca_ed25519.Ed25519PublicKey.from_public_bytes(public_bytes) + formats.GPG_ED25519_PUBKEY_SCHEMA.check_match(pubkey_info) - return public_key + public_bytes = binascii.unhexlify(pubkey_info["keyval"]["public"]["q"]) + public_key = pyca_ed25519.Ed25519PublicKey.from_public_bytes(public_bytes) + return public_key -def verify_signature(signature_object, pubkey_info, content, - hash_algorithm_id): - """ - - Verify the passed signature against the passed content with the passed - ED25519 public key using pyca/cryptography. +def verify_signature(signature_object, pubkey_info, content, hash_algorithm_id): + """ + + Verify the passed signature against the passed content with the passed + ED25519 public key using pyca/cryptography. - - signature_object: - A signature dictionary as specified by - securesystemslib.formats.GPG_SIGNATURE_SCHEMA + + signature_object: + A signature dictionary as specified by + securesystemslib.formats.GPG_SIGNATURE_SCHEMA - pubkey_info: - The DSA public key info dictionary as specified by - securesystemslib.formats.GPG_ED25519_PUBKEY_SCHEMA + pubkey_info: + The DSA public key info dictionary as specified by + securesystemslib.formats.GPG_ED25519_PUBKEY_SCHEMA - hash_algorithm_id: - one of SHA1, SHA256, SHA512 (see securesystemslib.gpg.constants) - used to verify the signature - NOTE: Overrides any hash algorithm specification in "pubkey_info"'s - "hashes" or "method" fields. + hash_algorithm_id: + one of SHA1, SHA256, SHA512 (see securesystemslib.gpg.constants) + used to verify the signature + NOTE: Overrides any hash algorithm specification in "pubkey_info"'s + "hashes" or "method" fields. - content: - The signed bytes against which the signature is verified + content: + The signed bytes against which the signature is verified - - securesystemslib.exceptions.FormatError if: - signature_object does not match securesystemslib.formats.GPG_SIGNATURE_SCHEMA - pubkey_info does not match securesystemslib.formats.GPG_ED25519_PUBKEY_SCHEMA + + securesystemslib.exceptions.FormatError if: + signature_object does not match securesystemslib.formats.GPG_SIGNATURE_SCHEMA + pubkey_info does not match securesystemslib.formats.GPG_ED25519_PUBKEY_SCHEMA - securesystemslib.exceptions.UnsupportedLibraryError if: - the cryptography module is unavailable + securesystemslib.exceptions.UnsupportedLibraryError if: + the cryptography module is unavailable - ValueError: - if the passed hash_algorithm_id is not supported (see - securesystemslib.gpg.util.get_hashing_class) + ValueError: + if the passed hash_algorithm_id is not supported (see + securesystemslib.gpg.util.get_hashing_class) - - True if signature verification passes and False otherwise. + + True if signature verification passes and False otherwise. - """ - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) + """ + if not CRYPTO: # pragma: no cover + raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - formats.GPG_SIGNATURE_SCHEMA.check_match(signature_object) - formats.GPG_ED25519_PUBKEY_SCHEMA.check_match(pubkey_info) + formats.GPG_SIGNATURE_SCHEMA.check_match(signature_object) + formats.GPG_ED25519_PUBKEY_SCHEMA.check_match(pubkey_info) - hasher = gpg_util.get_hashing_class(hash_algorithm_id) + hasher = gpg_util.get_hashing_class(hash_algorithm_id) - pubkey_object = create_pubkey(pubkey_info) + pubkey_object = create_pubkey(pubkey_info) - # See RFC4880-bis8 14.8. EdDSA and 5.2.4 "Computing Signatures" - digest = gpg_util.hash_object( - binascii.unhexlify(signature_object["other_headers"]), - hasher(), content) - - try: - pubkey_object.verify( - binascii.unhexlify(signature_object["signature"]), - digest + # See RFC4880-bis8 14.8. EdDSA and 5.2.4 "Computing Signatures" + digest = gpg_util.hash_object( + binascii.unhexlify(signature_object["other_headers"]), hasher(), content ) - return True - except InvalidSignature: - return False + try: + pubkey_object.verify( + binascii.unhexlify(signature_object["signature"]), digest + ) + return True + + except InvalidSignature: + return False diff --git a/securesystemslib/gpg/exceptions.py b/securesystemslib/gpg/exceptions.py index 959467ad..695bba94 100644 --- a/securesystemslib/gpg/exceptions.py +++ b/securesystemslib/gpg/exceptions.py @@ -22,32 +22,47 @@ class PacketParsingError(Exception): - pass + pass + class KeyNotFoundError(Exception): - pass + pass + class PacketVersionNotSupportedError(Exception): - pass + pass + class SignatureAlgorithmNotSupportedError(Exception): - pass + pass + class CommandError(Exception): - pass - -class KeyExpirationError(Exception): - def __init__(self, key): - super(KeyExpirationError, self).__init__() - self.key = key - - def __str__(self): - creation_time = datetime.datetime.utcfromtimestamp( - self.key["creation_time"]) - expiration_time = datetime.datetime.utcfromtimestamp( - self.key["creation_time"] + self.key["validity_period"]) - validity_period = expiration_time - creation_time - - return ("GPG key '{}' created on '{:%Y-%m-%d %H:%M} UTC' with validity " - "period '{}' expired on '{:%Y-%m-%d %H:%M} UTC'.".format( - self.key["keyid"], creation_time, validity_period, expiration_time)) + pass + + +class KeyExpirationError(Exception): # pylint: disable=missing-class-docstring + def __init__(self, key): + super( # pylint: disable=super-with-arguments + KeyExpirationError, self + ).__init__() + self.key = key + + def __str__(self): + creation_time = datetime.datetime.utcfromtimestamp( + self.key["creation_time"] + ) + expiration_time = datetime.datetime.utcfromtimestamp( + self.key["creation_time"] + self.key["validity_period"] + ) + validity_period = expiration_time - creation_time + + return ( + "GPG key '{}' created on '{:%Y-%m-%d %H:%M} UTC' with validity " # pylint: disable=consider-using-f-string + "period '{}' expired on '{:%Y-%m-%d %H:%M} UTC'.".format( + self.key["keyid"], + creation_time, + validity_period, + expiration_time, + ) + ) diff --git a/securesystemslib/gpg/functions.py b/securesystemslib/gpg/functions.py index 7167fe0f..c299bea1 100644 --- a/securesystemslib/gpg/functions.py +++ b/securesystemslib/gpg/functions.py @@ -18,23 +18,21 @@ import logging import time -from securesystemslib import exceptions -from securesystemslib import formats +from securesystemslib import exceptions, formats, process from securesystemslib.gpg.common import ( - get_pubkey_bundle, parse_signature_packet) -from securesystemslib.gpg.exceptions import ( - CommandError, KeyExpirationError) + get_pubkey_bundle, + parse_signature_packet, +) from securesystemslib.gpg.constants import ( FULLY_SUPPORTED_MIN_VERSION, + NO_GPG_MSG, + SHA256, gpg_export_pubkey_command, gpg_sign_command, have_gpg, - NO_GPG_MSG, - SHA256) -from securesystemslib.gpg.handlers import ( - SIGNATURE_HANDLERS) - -from securesystemslib import process +) +from securesystemslib.gpg.exceptions import CommandError, KeyExpirationError +from securesystemslib.gpg.handlers import SIGNATURE_HANDLERS from securesystemslib.gpg.rsa import CRYPTO log = logging.getLogger(__name__) @@ -42,277 +40,309 @@ NO_CRYPTO_MSG = "GPG support requires the cryptography library" - def create_signature(content, keyid=None, homedir=None): - """ - - Calls the gpg command line utility to sign the passed content with the key - identified by the passed keyid from the gpg keyring at the passed homedir. - - The executed base command is defined in - securesystemslib.gpg.constants.gpg_sign_command. - - NOTE: On not fully supported versions of GPG, i.e. versions below - securesystemslib.gpg.constants.FULLY_SUPPORTED_MIN_VERSION the returned - signature does not contain the full keyid. As a work around, we export the - public key bundle identified by the short keyid to compute the full keyid - and add it to the returned signature. - - - content: - The content to be signed. (bytes) - - keyid: (optional) - The keyid of the gpg signing keyid. If not passed the default - key in the keyring is used. - - homedir: (optional) - Path to the gpg keyring. If not passed the default keyring is used. - - - securesystemslib.exceptions.FormatError: - If the keyid was passed and does not match - securesystemslib.formats.KEYID_SCHEMA - - ValueError: - If the gpg command failed to create a valid signature. - - OSError: - If the gpg command is not present or non-executable. - - securesystemslib.exceptions.UnsupportedLibraryError: - If the gpg command is not available, or - the cryptography library is not installed. - - securesystemslib.gpg.exceptions.CommandError: - If the gpg command returned a non-zero exit code - - securesystemslib.gpg.exceptions.KeyNotFoundError: - If the used gpg version is not fully supported - and no public key can be found for short keyid. - - - None. - - - The created signature in the format: - securesystemslib.formats.GPG_SIGNATURE_SCHEMA. - - """ - if not have_gpg(): # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_GPG_MSG) - - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - - keyarg = "" - if keyid: - formats.KEYID_SCHEMA.check_match(keyid) - keyarg = "--local-user {}".format(keyid) - - homearg = "" - if homedir: - homearg = "--homedir {}".format(homedir).replace("\\", "/") - - command = gpg_sign_command(keyarg=keyarg, homearg=homearg) - - gpg_process = process.run(command, input=content, check=False, - stdout=process.PIPE, stderr=process.PIPE) - - # TODO: It's suggested to take a look at `--status-fd` for proper error - # reporting, as there is no clear distinction between the return codes - # https://lists.gnupg.org/pipermail/gnupg-devel/2005-December/022559.html - if gpg_process.returncode != 0: - raise CommandError("Command '{}' returned " - "non-zero exit status '{}', stderr was:\n{}.".format(gpg_process.args, - gpg_process.returncode, gpg_process.stderr.decode())) - - signature_data = gpg_process.stdout - signature = parse_signature_packet(signature_data) - - # On GPG < 2.1 we cannot derive the full keyid from the signature data. - # Instead we try to compute the keyid from the public part of the signing - # key or its subkeys, identified by the short keyid. - # parse_signature_packet is guaranteed to return at least one of keyid or - # short_keyid. - # Exclude the following code from coverage for consistent coverage across - # test environments. - if not signature["keyid"]: # pragma: no cover - log.warning("The created signature does not include the hashed subpacket" - " '33' (full keyid). You probably have a gpg version <{}." - " We will export the public keys associated with the short keyid to" - " compute the full keyid.".format(FULLY_SUPPORTED_MIN_VERSION)) - - short_keyid = signature["short_keyid"] - - # Export public key bundle (master key including with optional subkeys) - public_key_bundle = export_pubkey(short_keyid, homedir) - - # Test if the short keyid matches the master key ... - master_key_full_keyid = public_key_bundle["keyid"] - if master_key_full_keyid.endswith(short_keyid.lower()): - signature["keyid"] = master_key_full_keyid - - # ... or one of the subkeys, and add the full keyid to the signature dict. - else: - for sub_key_full_keyid in list( - public_key_bundle.get("subkeys", {}).keys()): - - if sub_key_full_keyid.endswith(short_keyid.lower()): - signature["keyid"] = sub_key_full_keyid - break - - # If there is still no full keyid something went wrong - if not signature["keyid"]: # pragma: no cover - raise ValueError("Full keyid could not be determined for signature '{}'". - format(signature)) - - # It is okay now to remove the optional short keyid to save space - signature.pop("short_keyid", None) - - return signature + """ + + Calls the gpg command line utility to sign the passed content with the key + identified by the passed keyid from the gpg keyring at the passed homedir. + + The executed base command is defined in + securesystemslib.gpg.constants.gpg_sign_command. + + NOTE: On not fully supported versions of GPG, i.e. versions below + securesystemslib.gpg.constants.FULLY_SUPPORTED_MIN_VERSION the returned + signature does not contain the full keyid. As a work around, we export the + public key bundle identified by the short keyid to compute the full keyid + and add it to the returned signature. + + + content: + The content to be signed. (bytes) + + keyid: (optional) + The keyid of the gpg signing keyid. If not passed the default + key in the keyring is used. + + homedir: (optional) + Path to the gpg keyring. If not passed the default keyring is used. + + + securesystemslib.exceptions.FormatError: + If the keyid was passed and does not match + securesystemslib.formats.KEYID_SCHEMA + + ValueError: + If the gpg command failed to create a valid signature. + + OSError: + If the gpg command is not present or non-executable. + + securesystemslib.exceptions.UnsupportedLibraryError: + If the gpg command is not available, or + the cryptography library is not installed. + + securesystemslib.gpg.exceptions.CommandError: + If the gpg command returned a non-zero exit code + + securesystemslib.gpg.exceptions.KeyNotFoundError: + If the used gpg version is not fully supported + and no public key can be found for short keyid. + + + None. + + + The created signature in the format: + securesystemslib.formats.GPG_SIGNATURE_SCHEMA. + + """ + if not have_gpg(): # pragma: no cover + raise exceptions.UnsupportedLibraryError(NO_GPG_MSG) + + if not CRYPTO: # pragma: no cover + raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) + + keyarg = "" + if keyid: + formats.KEYID_SCHEMA.check_match(keyid) + keyarg = ( + "--local-user {}".format( # pylint: disable=consider-using-f-string + keyid + ) + ) + + homearg = "" + if homedir: + homearg = ( + "--homedir {}".format( # pylint: disable=consider-using-f-string + homedir + ).replace("\\", "/") + ) + + command = gpg_sign_command(keyarg=keyarg, homearg=homearg) + + gpg_process = process.run( + command, + input=content, + check=False, + stdout=process.PIPE, + stderr=process.PIPE, + ) + + # TODO: It's suggested to take a look at `--status-fd` for proper error + # reporting, as there is no clear distinction between the return codes + # https://lists.gnupg.org/pipermail/gnupg-devel/2005-December/022559.html + if gpg_process.returncode != 0: + raise CommandError( + "Command '{}' returned " # pylint: disable=consider-using-f-string + "non-zero exit status '{}', stderr was:\n{}.".format( + gpg_process.args, + gpg_process.returncode, + gpg_process.stderr.decode(), + ) + ) + + signature_data = gpg_process.stdout + signature = parse_signature_packet(signature_data) + + # On GPG < 2.1 we cannot derive the full keyid from the signature data. + # Instead we try to compute the keyid from the public part of the signing + # key or its subkeys, identified by the short keyid. + # parse_signature_packet is guaranteed to return at least one of keyid or + # short_keyid. + # Exclude the following code from coverage for consistent coverage across + # test environments. + if not signature["keyid"]: # pragma: no cover + log.warning( + "The created signature does not include the hashed subpacket" # pylint: disable=logging-format-interpolation,consider-using-f-string + " '33' (full keyid). You probably have a gpg version <{}." + " We will export the public keys associated with the short keyid to" + " compute the full keyid.".format(FULLY_SUPPORTED_MIN_VERSION) + ) + + short_keyid = signature["short_keyid"] + + # Export public key bundle (master key including with optional subkeys) + public_key_bundle = export_pubkey(short_keyid, homedir) + + # Test if the short keyid matches the master key ... + master_key_full_keyid = public_key_bundle["keyid"] + if master_key_full_keyid.endswith(short_keyid.lower()): + signature["keyid"] = master_key_full_keyid + + # ... or one of the subkeys, and add the full keyid to the signature dict. + else: + for sub_key_full_keyid in list( + public_key_bundle.get("subkeys", {}).keys() + ): + + if sub_key_full_keyid.endswith(short_keyid.lower()): + signature["keyid"] = sub_key_full_keyid + break + + # If there is still no full keyid something went wrong + if not signature["keyid"]: # pragma: no cover + raise ValueError( + "Full keyid could not be determined for signature '{}'".format( # pylint: disable=consider-using-f-string + signature + ) + ) + + # It is okay now to remove the optional short keyid to save space + signature.pop("short_keyid", None) + + return signature def verify_signature(signature_object, pubkey_info, content): - """ - - Verifies the passed signature against the passed content using the - passed public key, or one of its subkeys, associated by the signature's - keyid. - - The function selects the appropriate verification algorithm (rsa or dsa) - based on the "type" field in the passed public key object. + """ + + Verifies the passed signature against the passed content using the + passed public key, or one of its subkeys, associated by the signature's + keyid. - - signature_object: - A signature object in the format: - securesystemslib.formats.GPG_SIGNATURE_SCHEMA + The function selects the appropriate verification algorithm (rsa or dsa) + based on the "type" field in the passed public key object. - pubkey_info: - A public key object in the format: - securesystemslib.formats.GPG_PUBKEY_SCHEMA + + signature_object: + A signature object in the format: + securesystemslib.formats.GPG_SIGNATURE_SCHEMA - content: - The content to be verified. (bytes) + pubkey_info: + A public key object in the format: + securesystemslib.formats.GPG_PUBKEY_SCHEMA - - securesystemslib.gpg.exceptions.KeyExpirationError: - if the passed public key has expired + content: + The content to be verified. (bytes) - securesystemslib.exceptions.UnsupportedLibraryError: - if the cryptography module is unavailable + + securesystemslib.gpg.exceptions.KeyExpirationError: + if the passed public key has expired - - None. + securesystemslib.exceptions.UnsupportedLibraryError: + if the cryptography module is unavailable - - True if signature verification passes, False otherwise. + + None. - """ - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) + + True if signature verification passes, False otherwise. - formats.GPG_PUBKEY_SCHEMA.check_match(pubkey_info) - formats.GPG_SIGNATURE_SCHEMA.check_match(signature_object) + """ + if not CRYPTO: # pragma: no cover + raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - handler = SIGNATURE_HANDLERS[pubkey_info['type']] - sig_keyid = signature_object["keyid"] + formats.GPG_PUBKEY_SCHEMA.check_match(pubkey_info) + formats.GPG_SIGNATURE_SCHEMA.check_match(signature_object) - verification_key = pubkey_info + handler = SIGNATURE_HANDLERS[pubkey_info["type"]] + sig_keyid = signature_object["keyid"] - # If the keyid on the signature matches a subkey of the passed key, - # we use that subkey for verification instead of the master key. - if sig_keyid in list(pubkey_info.get("subkeys", {}).keys()): - verification_key = pubkey_info["subkeys"][sig_keyid] + verification_key = pubkey_info + # If the keyid on the signature matches a subkey of the passed key, + # we use that subkey for verification instead of the master key. + if sig_keyid in list(pubkey_info.get("subkeys", {}).keys()): + verification_key = pubkey_info["subkeys"][sig_keyid] - creation_time = verification_key.get("creation_time") - validity_period = verification_key.get("validity_period") + creation_time = verification_key.get("creation_time") + validity_period = verification_key.get("validity_period") - if creation_time and validity_period and \ - creation_time + validity_period < time.time(): - raise KeyExpirationError(verification_key) + if ( + creation_time + and validity_period + and creation_time + validity_period < time.time() + ): + raise KeyExpirationError(verification_key) - return handler.verify_signature( - signature_object, verification_key, content, SHA256) + return handler.verify_signature( + signature_object, verification_key, content, SHA256 + ) def export_pubkey(keyid, homedir=None): - """Exports a public key from a GnuPG keyring. + """Exports a public key from a GnuPG keyring. - Arguments: - keyid: An OpenPGP keyid in KEYID_SCHEMA format. - homedir (optional): A path to the GnuPG home directory. If not set the - default GnuPG home directory is used. + Arguments: + keyid: An OpenPGP keyid in KEYID_SCHEMA format. + homedir (optional): A path to the GnuPG home directory. If not set the + default GnuPG home directory is used. - Raises: - ValueError: Keyid is not a string. - UnsupportedLibraryError: The gpg command or pyca/cryptography are not - available. - KeyNotFoundError: No key or subkey was found for that keyid. + Raises: + ValueError: Keyid is not a string. + UnsupportedLibraryError: The gpg command or pyca/cryptography are not + available. + KeyNotFoundError: No key or subkey was found for that keyid. - Side Effects: - Calls system gpg command in a subprocess. + Side Effects: + Calls system gpg command in a subprocess. - Returns: - An OpenPGP public key object in GPG_PUBKEY_SCHEMA format. + Returns: + An OpenPGP public key object in GPG_PUBKEY_SCHEMA format. - """ - if not have_gpg(): # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_GPG_MSG) + """ + if not have_gpg(): # pragma: no cover + raise exceptions.UnsupportedLibraryError(NO_GPG_MSG) - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) + if not CRYPTO: # pragma: no cover + raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - if not formats.KEYID_SCHEMA.matches(keyid): - # FIXME: probably needs smarter parsing of what a valid keyid is so as to - # not export more than one pubkey packet. - raise ValueError("we need to export an individual key. Please provide a " - " valid keyid! Keyid was '{}'.".format(keyid)) + if not formats.KEYID_SCHEMA.matches(keyid): + # FIXME: probably needs smarter parsing of what a valid keyid is so as to + # not export more than one pubkey packet. + raise ValueError( + "we need to export an individual key. Please provide a " # pylint: disable=consider-using-f-string + " valid keyid! Keyid was '{}'.".format(keyid) + ) - homearg = "" - if homedir: - homearg = "--homedir {}".format(homedir).replace("\\", "/") + homearg = "" + if homedir: + homearg = ( + "--homedir {}".format( # pylint: disable=consider-using-f-string + homedir + ).replace("\\", "/") + ) - # TODO: Consider adopting command error handling from `create_signature` - # above, e.g. in a common 'run gpg command' utility function - command = gpg_export_pubkey_command(keyid=keyid, homearg=homearg) - gpg_process = process.run(command, stdout=process.PIPE, stderr=process.PIPE) + # TODO: Consider adopting command error handling from `create_signature` + # above, e.g. in a common 'run gpg command' utility function + command = gpg_export_pubkey_command(keyid=keyid, homearg=homearg) + gpg_process = process.run(command, stdout=process.PIPE, stderr=process.PIPE) - key_packet = gpg_process.stdout - key_bundle = get_pubkey_bundle(key_packet, keyid) + key_packet = gpg_process.stdout + key_bundle = get_pubkey_bundle(key_packet, keyid) - return key_bundle + return key_bundle def export_pubkeys(keyids, homedir=None): - """Exports multiple public keys from a GnuPG keyring. + """Exports multiple public keys from a GnuPG keyring. - Arguments: - keyids: A list of OpenPGP keyids in KEYID_SCHEMA format. - homedir (optional): A path to the GnuPG home directory. If not set the - default GnuPG home directory is used. + Arguments: + keyids: A list of OpenPGP keyids in KEYID_SCHEMA format. + homedir (optional): A path to the GnuPG home directory. If not set the + default GnuPG home directory is used. - Raises: - TypeError: Keyids is not iterable. - ValueError: A Keyid is not a string. - UnsupportedLibraryError: The gpg command or pyca/cryptography are not - available. - KeyNotFoundError: No key or subkey was found for that keyid. + Raises: + TypeError: Keyids is not iterable. + ValueError: A Keyid is not a string. + UnsupportedLibraryError: The gpg command or pyca/cryptography are not + available. + KeyNotFoundError: No key or subkey was found for that keyid. - Side Effects: - Calls system gpg command in a subprocess. + Side Effects: + Calls system gpg command in a subprocess. - Returns: - A dict of OpenPGP public key objects in GPG_PUBKEY_SCHEMA format as values, - and their keyids as dict keys. + Returns: + A dict of OpenPGP public key objects in GPG_PUBKEY_SCHEMA format as values, + and their keyids as dict keys. - """ - public_key_dict = {} - for gpg_keyid in keyids: - public_key = export_pubkey(gpg_keyid, homedir=homedir) - keyid = public_key["keyid"] - public_key_dict[keyid] = public_key + """ + public_key_dict = {} + for gpg_keyid in keyids: + public_key = export_pubkey(gpg_keyid, homedir=homedir) + keyid = public_key["keyid"] + public_key_dict[keyid] = public_key - return public_key_dict + return public_key_dict diff --git a/securesystemslib/gpg/handlers.py b/securesystemslib/gpg/handlers.py index 91a17010..cd51866f 100644 --- a/securesystemslib/gpg/handlers.py +++ b/securesystemslib/gpg/handlers.py @@ -16,31 +16,13 @@ the signature verification and key parsing. """ -from securesystemslib.gpg import rsa -from securesystemslib.gpg import dsa -from securesystemslib.gpg import eddsa +from securesystemslib.gpg import dsa, eddsa, rsa # See section 9.1. (public-key algorithms) of RFC4880 (-bis8) SUPPORTED_SIGNATURE_ALGORITHMS = { - 0x01: { - "type":"rsa", - "method": "pgp+rsa-pkcsv1.5", - "handler": rsa - }, - 0x11: { - "type": "dsa", - "method": "pgp+dsa-fips-180-2", - "handler": dsa - }, - 0x16: { - "type": "eddsa", - "method": "pgp+eddsa-ed25519", - "handler": eddsa - } + 0x01: {"type": "rsa", "method": "pgp+rsa-pkcsv1.5", "handler": rsa}, + 0x11: {"type": "dsa", "method": "pgp+dsa-fips-180-2", "handler": dsa}, + 0x16: {"type": "eddsa", "method": "pgp+eddsa-ed25519", "handler": eddsa}, } -SIGNATURE_HANDLERS = { - "rsa": rsa, - "dsa": dsa, - "eddsa": eddsa -} +SIGNATURE_HANDLERS = {"rsa": rsa, "dsa": dsa, "eddsa": eddsa} diff --git a/securesystemslib/gpg/rsa.py b/securesystemslib/gpg/rsa.py index f77726fb..ed28f875 100644 --- a/securesystemslib/gpg/rsa.py +++ b/securesystemslib/gpg/rsa.py @@ -17,206 +17,208 @@ import binascii CRYPTO = True -NO_CRYPTO_MSG = 'RSA key support for GPG requires the cryptography library' +NO_CRYPTO_MSG = "RSA key support for GPG requires the cryptography library" try: - from cryptography.hazmat.primitives.asymmetric import rsa - from cryptography.hazmat import backends - from cryptography.hazmat.primitives.asymmetric import padding - from cryptography.hazmat.primitives.asymmetric import utils - from cryptography.exceptions import InvalidSignature + from cryptography.exceptions import InvalidSignature + from cryptography.hazmat import backends + from cryptography.hazmat.primitives.asymmetric import padding, rsa, utils except ImportError: - CRYPTO = False + CRYPTO = False -from securesystemslib import exceptions -from securesystemslib import formats +# pylint: disable=wrong-import-position +from securesystemslib import exceptions, formats from securesystemslib.gpg import util as gpg_util from securesystemslib.gpg.exceptions import PacketParsingError +# pylint: enable=wrong-import-position + def create_pubkey(pubkey_info): - """ - - Create and return an RSAPublicKey object from the passed pubkey_info - using pyca/cryptography. + """ + + Create and return an RSAPublicKey object from the passed pubkey_info + using pyca/cryptography. - - pubkey_info: - The RSA pubkey info dictionary as specified by - securesystemslib.formats.GPG_RSA_PUBKEY_SCHEMA + + pubkey_info: + The RSA pubkey info dictionary as specified by + securesystemslib.formats.GPG_RSA_PUBKEY_SCHEMA - - securesystemslib.exceptions.FormatError if - pubkey_info does not match securesystemslib.formats.GPG_RSA_PUBKEY_SCHEMA + + securesystemslib.exceptions.FormatError if + pubkey_info does not match securesystemslib.formats.GPG_RSA_PUBKEY_SCHEMA - securesystemslib.exceptions.UnsupportedLibraryError if - the cryptography module is unavailable + securesystemslib.exceptions.UnsupportedLibraryError if + the cryptography module is unavailable - - A cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey based on the - passed pubkey_info. + + A cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey based on the + passed pubkey_info. - """ - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) + """ + if not CRYPTO: # pragma: no cover + raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - formats.GPG_RSA_PUBKEY_SCHEMA.check_match(pubkey_info) + formats.GPG_RSA_PUBKEY_SCHEMA.check_match(pubkey_info) - e = int(pubkey_info['keyval']['public']['e'], 16) - n = int(pubkey_info['keyval']['public']['n'], 16) - pubkey = rsa.RSAPublicNumbers(e, n).public_key(backends.default_backend()) + e = int(pubkey_info["keyval"]["public"]["e"], 16) + n = int(pubkey_info["keyval"]["public"]["n"], 16) + pubkey = rsa.RSAPublicNumbers(e, n).public_key(backends.default_backend()) - return pubkey + return pubkey def get_pubkey_params(data): - """ - - Parse the public key parameters as multi-precision-integers. + """ + + Parse the public key parameters as multi-precision-integers. - - data: - the RFC4880-encoded public key parameters data buffer as described - in the fifth paragraph of section 5.5.2. + + data: + the RFC4880-encoded public key parameters data buffer as described + in the fifth paragraph of section 5.5.2. - - securesystemslib.gpg.exceptions.PacketParsingError: - if the public key parameters are malformed + + securesystemslib.gpg.exceptions.PacketParsingError: + if the public key parameters are malformed - - None. + + None. - - The parsed RSA public key in the format - securesystemslib.formats.GPG_RSA_PUBKEY_SCHEMA. + + The parsed RSA public key in the format + securesystemslib.formats.GPG_RSA_PUBKEY_SCHEMA. - """ - ptr = 0 + """ + ptr = 0 - modulus_length = gpg_util.get_mpi_length(data[ptr: ptr + 2]) - ptr += 2 - modulus = data[ptr:ptr + modulus_length] - if len(modulus) != modulus_length: # pragma: no cover - raise PacketParsingError("This modulus MPI was truncated!") - ptr += modulus_length + modulus_length = gpg_util.get_mpi_length(data[ptr : ptr + 2]) + ptr += 2 + modulus = data[ptr : ptr + modulus_length] + if len(modulus) != modulus_length: # pragma: no cover + raise PacketParsingError("This modulus MPI was truncated!") + ptr += modulus_length - exponent_e_length = gpg_util.get_mpi_length(data[ptr: ptr + 2]) - ptr += 2 - exponent_e = data[ptr:ptr + exponent_e_length] - if len(exponent_e) != exponent_e_length: # pragma: no cover - raise PacketParsingError("This e MPI has been truncated!") + exponent_e_length = gpg_util.get_mpi_length(data[ptr : ptr + 2]) + ptr += 2 + exponent_e = data[ptr : ptr + exponent_e_length] + if len(exponent_e) != exponent_e_length: # pragma: no cover + raise PacketParsingError("This e MPI has been truncated!") - return { - "e": binascii.hexlify(exponent_e).decode('ascii'), - "n": binascii.hexlify(modulus).decode("ascii"), - } + return { + "e": binascii.hexlify(exponent_e).decode("ascii"), + "n": binascii.hexlify(modulus).decode("ascii"), + } def get_signature_params(data): - """ - - Parse the signature parameters as multi-precision-integers. - - - data: - the RFC4880-encoded signature data buffer as described - in the third paragraph of section 5.2.2. - - - securesystemslib.gpg.exceptions.PacketParsingError: - if the public key parameters are malformed - - - None. - - - The decoded signature buffer - """ - - ptr = 0 - signature_length = gpg_util.get_mpi_length(data[ptr:ptr+2]) - ptr += 2 - signature = data[ptr:ptr + signature_length] - if len(signature) != signature_length: # pragma: no cover - raise PacketParsingError("This signature was truncated!") - - return signature - - -def verify_signature(signature_object, pubkey_info, content, - hash_algorithm_id): - """ - - Verify the passed signature against the passed content with the passed - RSA public key using pyca/cryptography. - - - signature_object: - A signature dictionary as specified by - securesystemslib.formats.GPG_SIGNATURE_SCHEMA - - pubkey_info: - The RSA public key info dictionary as specified by - securesystemslib.formats.GPG_RSA_PUBKEY_SCHEMA - - content: - The signed bytes against which the signature is verified - - hash_algorithm_id: - one of SHA1, SHA256, SHA512 (see securesystemslib.gpg.constants) - used to verify the signature - NOTE: Overrides any hash algorithm specification in "pubkey_info"'s - "hashes" or "method" fields. - - - securesystemslib.exceptions.FormatError if: - signature_object does not match - securesystemslib.formats.GPG_SIGNATURE_SCHEMA, - pubkey_info does not match securesystemslib.formats.GPG_RSA_PUBKEY_SCHEMA - - securesystemslib.exceptions.UnsupportedLibraryError if: - the cryptography module is unavailable - - ValueError: - if the passed hash_algorithm_id is not supported (see - securesystemslib.gpg.util.get_hashing_class) - - - True if signature verification passes and False otherwise - - """ - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - - formats.GPG_SIGNATURE_SCHEMA.check_match(signature_object) - formats.GPG_RSA_PUBKEY_SCHEMA.check_match(pubkey_info) - - hasher = gpg_util.get_hashing_class(hash_algorithm_id) - - pubkey_object = create_pubkey(pubkey_info) - - # zero-pad the signature due to a discrepancy between the openssl backend - # and the gnupg interpretation of PKCSv1.5. Read more at: - # https://github.com/in-toto/in-toto/issues/171#issuecomment-440039256 - # we are skipping this if on the tests because well, how would one test this - # deterministically. - pubkey_length = len(pubkey_info['keyval']['public']['n']) - signature_length = len(signature_object['signature']) - if pubkey_length != signature_length: # pragma: no cover - zero_pad = "0"*(pubkey_length - signature_length) - signature_object['signature'] = "{}{}".format(zero_pad, - signature_object['signature']) - - digest = gpg_util.hash_object( - binascii.unhexlify(signature_object['other_headers']), - hasher(), content) - - try: - pubkey_object.verify( - binascii.unhexlify(signature_object['signature']), - digest, - padding.PKCS1v15(), - utils.Prehashed(hasher()) + """ + + Parse the signature parameters as multi-precision-integers. + + + data: + the RFC4880-encoded signature data buffer as described + in the third paragraph of section 5.2.2. + + + securesystemslib.gpg.exceptions.PacketParsingError: + if the public key parameters are malformed + + + None. + + + The decoded signature buffer + """ + + ptr = 0 + signature_length = gpg_util.get_mpi_length(data[ptr : ptr + 2]) + ptr += 2 + signature = data[ptr : ptr + signature_length] + if len(signature) != signature_length: # pragma: no cover + raise PacketParsingError("This signature was truncated!") + + return signature + + +def verify_signature(signature_object, pubkey_info, content, hash_algorithm_id): + """ + + Verify the passed signature against the passed content with the passed + RSA public key using pyca/cryptography. + + + signature_object: + A signature dictionary as specified by + securesystemslib.formats.GPG_SIGNATURE_SCHEMA + + pubkey_info: + The RSA public key info dictionary as specified by + securesystemslib.formats.GPG_RSA_PUBKEY_SCHEMA + + content: + The signed bytes against which the signature is verified + + hash_algorithm_id: + one of SHA1, SHA256, SHA512 (see securesystemslib.gpg.constants) + used to verify the signature + NOTE: Overrides any hash algorithm specification in "pubkey_info"'s + "hashes" or "method" fields. + + + securesystemslib.exceptions.FormatError if: + signature_object does not match + securesystemslib.formats.GPG_SIGNATURE_SCHEMA, + pubkey_info does not match securesystemslib.formats.GPG_RSA_PUBKEY_SCHEMA + + securesystemslib.exceptions.UnsupportedLibraryError if: + the cryptography module is unavailable + + ValueError: + if the passed hash_algorithm_id is not supported (see + securesystemslib.gpg.util.get_hashing_class) + + + True if signature verification passes and False otherwise + + """ + if not CRYPTO: # pragma: no cover + raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) + + formats.GPG_SIGNATURE_SCHEMA.check_match(signature_object) + formats.GPG_RSA_PUBKEY_SCHEMA.check_match(pubkey_info) + + hasher = gpg_util.get_hashing_class(hash_algorithm_id) + + pubkey_object = create_pubkey(pubkey_info) + + # zero-pad the signature due to a discrepancy between the openssl backend + # and the gnupg interpretation of PKCSv1.5. Read more at: + # https://github.com/in-toto/in-toto/issues/171#issuecomment-440039256 + # we are skipping this if on the tests because well, how would one test this + # deterministically. + pubkey_length = len(pubkey_info["keyval"]["public"]["n"]) + signature_length = len(signature_object["signature"]) + if pubkey_length != signature_length: # pragma: no cover + zero_pad = "0" * (pubkey_length - signature_length) + signature_object[ + "signature" + ] = "{}{}".format( # pylint: disable=consider-using-f-string + zero_pad, signature_object["signature"] + ) + + digest = gpg_util.hash_object( + binascii.unhexlify(signature_object["other_headers"]), hasher(), content ) - return True - except InvalidSignature: - return False + + try: + pubkey_object.verify( + binascii.unhexlify(signature_object["signature"]), + digest, + padding.PKCS1v15(), + utils.Prehashed(hasher()), + ) + return True + except InvalidSignature: + return False diff --git a/securesystemslib/gpg/util.py b/securesystemslib/gpg/util.py index d09719f1..8ae712ec 100644 --- a/securesystemslib/gpg/util.py +++ b/securesystemslib/gpg/util.py @@ -14,24 +14,22 @@ general-purpose utilities for binary data handling and pgp data parsing """ -import struct import binascii -import re -import logging import dataclasses - +import logging +import re +import struct CRYPTO = True -NO_CRYPTO_MSG = 'gpg.utils requires the cryptography library' +NO_CRYPTO_MSG = "gpg.utils requires the cryptography library" try: - from cryptography.hazmat import backends - from cryptography.hazmat.primitives import hashes as hashing + from cryptography.hazmat import backends + from cryptography.hazmat.primitives import hashes as hashing except ImportError: - CRYPTO = False + CRYPTO = False # pylint: disable=wrong-import-position -from securesystemslib import exceptions -from securesystemslib import process +from securesystemslib import exceptions, process from securesystemslib.gpg import constants from securesystemslib.gpg.exceptions import PacketParsingError @@ -39,397 +37,422 @@ def get_mpi_length(data): - """ - - parses an MPI (Multi-Precision Integer) buffer and returns the appropriate - length. This is mostly done to perform bitwise to byte-wise conversion. + """ + + parses an MPI (Multi-Precision Integer) buffer and returns the appropriate + length. This is mostly done to perform bitwise to byte-wise conversion. - See RFC4880 section 3.2. Multiprecision Integers for details. + See RFC4880 section 3.2. Multiprecision Integers for details. - - data: The MPI data + + data: The MPI data - - None + + None - - None + + None - - The length of the MPI contained at the beginning of this data buffer. - """ - bitlength = int(struct.unpack(">H", data)[0]) - # Notice the /8 at the end, this length is the bitlength, not the length of - # the data in bytes (as len reports it) - return int((bitlength - 1)/8) + 1 + + The length of the MPI contained at the beginning of this data buffer. + """ + bitlength = int(struct.unpack(">H", data)[0]) + # Notice the /8 at the end, this length is the bitlength, not the length of + # the data in bytes (as len reports it) + return int((bitlength - 1) / 8) + 1 def hash_object(headers, algorithm, content): - """ - - Hash data prior to signature verification in conformance of the RFC4880 - openPGP standard. - - - headers: the additional OpenPGP headers as populated from - gpg_generate_signature - - algorithm: The hash algorithm object defined by the cryptography.io hashes - module - - content: the signed content - - - securesystemslib.exceptions.UnsupportedLibraryError if: - the cryptography module is unavailable - - - None - - - The RFC4880-compliant hashed buffer - """ - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - - # As per RFC4880 Section 5.2.4., we need to hash the content, - # signature headers and add a very opinionated trailing header - hasher = hashing.Hash(algorithm, backend=backends.default_backend()) - hasher.update(content) - hasher.update(headers) - hasher.update(b'\x04\xff') - hasher.update(struct.pack(">I", len(headers))) - - return hasher.finalize() - - -def parse_packet_header(data, expected_type=None): - """ - - Parse out packet type and header and body lengths from an RFC4880 packet. - - - data: - An RFC4880 packet as described in section 4.2 of the rfc. - - expected_type: (optional) - Used to error out if the packet does not have the expected - type. See securesystemslib.gpg.constants.PACKET_TYPE_* for - available types. - - - securesystemslib.gpg.exceptions.PacketParsingError - If the new format packet length encodes a partial body length - If the old format packet length encodes an indeterminate length - If header or body length could not be determined - If the expected_type was passed and does not match the packet type - - IndexError - If the passed data is incomplete - - - None. - - - A tuple of packet type, header length, body length and packet length. - (see RFC4880 4.3. for the list of available packet types) - - """ - data = bytearray(data) - header_len = None - body_len = None - - # If Bit 6 of 1st octet is set we parse a New Format Packet Length, and - # an Old Format Packet Lengths otherwise - if data[0] & 0b01000000: - # In new format packet lengths the packet type is encoded in Bits 5-0 of - # the 1st octet of the packet - packet_type = data[0] & 0b00111111 - - # The rest of the packet header is the body length header, which may - # consist of one, two or five octets. To disambiguate the RFC, the first - # octet of the body length header is the second octet of the packet. - if data[1] < 192: - header_len = 2 - body_len = data[1] - - elif data[1] >= 192 and data[1] <= 223: - header_len = 3 - body_len = (data[1] - 192 << 8) + data[2] + 192 - - elif data[1] >= 224 and data[1] < 255: - raise PacketParsingError("New length " - "format packets of partial body lengths are not supported") - - elif data[1] == 255: - header_len = 6 - body_len = data[2] << 24 | data[3] << 16 | data[4] << 8 | data[5] - - else: # pragma: no cover - # Unreachable: octet must be between 0 and 255 - raise PacketParsingError("Invalid new length") - - else: - # In old format packet lengths the packet type is encoded in Bits 5-2 of - # the 1st octet and the length type in Bits 1-0 - packet_type = (data[0] & 0b00111100) >> 2 - length_type = data[0] & 0b00000011 - - # The body length is encoded using one, two, or four octets, starting - # with the second octet of the packet - if length_type == 0: - body_len = data[1] - header_len = 2 - - elif length_type == 1: - header_len = 3 - body_len = struct.unpack(">H", data[1:header_len])[0] - - elif length_type == 2: - header_len = 5 - body_len = struct.unpack(">I", data[1:header_len])[0] - - elif length_type == 3: - raise PacketParsingError("Old length " - "format packets of indeterminate length are not supported") - - else: # pragma: no cover (unreachable) - # Unreachable: bits 1-0 must be one of 0 to 3 - raise PacketParsingError("Invalid old length") - - if header_len is None or body_len is None: # pragma: no cover - # Unreachable: One of above must have assigned lengths or raised error - raise PacketParsingError("Could not determine packet length") - - if expected_type is not None and packet_type != expected_type: - raise PacketParsingError("Expected packet " - "{}, but got {} instead!".format(expected_type, packet_type)) - - return packet_type, header_len, body_len, header_len + body_len + """ + + Hash data prior to signature verification in conformance of the RFC4880 + openPGP standard. + + headers: the additional OpenPGP headers as populated from + gpg_generate_signature -def compute_keyid(pubkey_packet_data): - """ - - compute a keyid from an RFC4880 public-key buffer + algorithm: The hash algorithm object defined by the cryptography.io hashes + module - - pubkey_packet_data: the public-key packet buffer + content: the signed content - - securesystemslib.exceptions.UnsupportedLibraryError if: - the cryptography module is unavailable + + securesystemslib.exceptions.UnsupportedLibraryError if: + the cryptography module is unavailable - - None + + None - - The RFC4880-compliant hashed buffer - """ - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) + + The RFC4880-compliant hashed buffer + """ + if not CRYPTO: # pragma: no cover + raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - hasher = hashing.Hash(hashing.SHA1(), backend=backends.default_backend()) - hasher.update(b'\x99') - hasher.update(struct.pack(">H", len(pubkey_packet_data))) - hasher.update(bytes(pubkey_packet_data)) - return binascii.hexlify(hasher.finalize()).decode("ascii") + # As per RFC4880 Section 5.2.4., we need to hash the content, + # signature headers and add a very opinionated trailing header + hasher = hashing.Hash(algorithm, backend=backends.default_backend()) + hasher.update(content) + hasher.update(headers) + hasher.update(b"\x04\xff") + hasher.update(struct.pack(">I", len(headers))) + return hasher.finalize() -def parse_subpacket_header(data): - """ Parse out subpacket header as per RFC4880 5.2.3.1. Signature Subpacket - Specification. """ - # NOTE: Although the RFC does not state it explicitly, the length encoded - # in the header must be greater equal 1, as it includes the mandatory - # subpacket type octet. - # Hence, passed bytearrays like [0] or [255, 0, 0, 0, 0], which encode a - # subpacket length 0 are invalid. - # The caller has to deal with the resulting IndexError. - if data[0] < 192: - length_len = 1 - length = data[0] - - elif data[0] >= 192 and data[0] < 255: - length_len = 2 - length = ((data[0] - 192 << 8) + (data[1] + 192)) - - elif data[0] == 255: - length_len = 5 - length = struct.unpack(">I", data[1:length_len])[0] - - else: # pragma: no cover (unreachable) - raise PacketParsingError("Invalid subpacket header") - - return data[length_len], length_len + 1, length - 1, length_len + length -def parse_subpackets(data): - """ - - parse the subpackets fields +def parse_packet_header( + data, expected_type=None +): # pylint: disable=too-many-branches + """ + + Parse out packet type and header and body lengths from an RFC4880 packet. - - data: the unparsed subpacketoctets + + data: + An RFC4880 packet as described in section 4.2 of the rfc. - - IndexErrorif the subpackets octets are incomplete or malformed + expected_type: (optional) + Used to error out if the packet does not have the expected + type. See securesystemslib.gpg.constants.PACKET_TYPE_* for + available types. - - None + + securesystemslib.gpg.exceptions.PacketParsingError + If the new format packet length encodes a partial body length + If the old format packet length encodes an indeterminate length + If header or body length could not be determined + If the expected_type was passed and does not match the packet type - - A list of tuples with like: - [ (packet_type, data), - (packet_type, data), - ... - ] - """ - parsed_subpackets = [] - position = 0 + IndexError + If the passed data is incomplete - while position < len(data): - subpacket_type, header_len, _, subpacket_len = \ - parse_subpacket_header(data[position:]) + + None. - payload = data[position+header_len:position+subpacket_len] - parsed_subpackets.append((subpacket_type, payload)) + + A tuple of packet type, header length, body length and packet length. + (see RFC4880 4.3. for the list of available packet types) - position += subpacket_len + """ + data = bytearray(data) + header_len = None + body_len = None + + # If Bit 6 of 1st octet is set we parse a New Format Packet Length, and + # an Old Format Packet Lengths otherwise + if data[0] & 0b01000000: + # In new format packet lengths the packet type is encoded in Bits 5-0 of + # the 1st octet of the packet + packet_type = data[0] & 0b00111111 + + # The rest of the packet header is the body length header, which may + # consist of one, two or five octets. To disambiguate the RFC, the first + # octet of the body length header is the second octet of the packet. + if data[1] < 192: + header_len = 2 + body_len = data[1] + + elif data[1] >= 192 and data[1] <= 223: + header_len = 3 + body_len = (data[1] - 192 << 8) + data[2] + 192 + + elif data[1] >= 224 and data[1] < 255: + raise PacketParsingError( + "New length " + "format packets of partial body lengths are not supported" + ) + + elif data[1] == 255: + header_len = 6 + body_len = data[2] << 24 | data[3] << 16 | data[4] << 8 | data[5] + + else: # pragma: no cover + # Unreachable: octet must be between 0 and 255 + raise PacketParsingError("Invalid new length") + + else: + # In old format packet lengths the packet type is encoded in Bits 5-2 of + # the 1st octet and the length type in Bits 1-0 + packet_type = (data[0] & 0b00111100) >> 2 + length_type = data[0] & 0b00000011 + + # The body length is encoded using one, two, or four octets, starting + # with the second octet of the packet + if length_type == 0: + body_len = data[1] + header_len = 2 + + elif length_type == 1: + header_len = 3 + body_len = struct.unpack(">H", data[1:header_len])[0] + + elif length_type == 2: + header_len = 5 + body_len = struct.unpack(">I", data[1:header_len])[0] + + elif length_type == 3: + raise PacketParsingError( + "Old length " + "format packets of indeterminate length are not supported" + ) + + else: # pragma: no cover (unreachable) + # Unreachable: bits 1-0 must be one of 0 to 3 + raise PacketParsingError("Invalid old length") + + if header_len is None or body_len is None: # pragma: no cover + # Unreachable: One of above must have assigned lengths or raised error + raise PacketParsingError("Could not determine packet length") + + if expected_type is not None and packet_type != expected_type: + raise PacketParsingError( + "Expected packet " # pylint: disable=consider-using-f-string + "{}, but got {} instead!".format(expected_type, packet_type) + ) + + return packet_type, header_len, body_len, header_len + body_len - return parsed_subpackets +def compute_keyid(pubkey_packet_data): + """ + + compute a keyid from an RFC4880 public-key buffer -@dataclasses.dataclass(order=True) -class Version: - """A version of GPG.""" + + pubkey_packet_data: the public-key packet buffer - major: int - minor: int - patch: int + + securesystemslib.exceptions.UnsupportedLibraryError if: + the cryptography module is unavailable - VERSION_RE = re.compile(r'(\d)\.(\d)\.(\d+)') - EXAMPLE = '1.3.22' + + None - @classmethod - def from_string(cls, value: str) -> 'Version': + + The RFC4880-compliant hashed buffer """ - - Parses `value` as a `Version`. + if not CRYPTO: # pragma: no cover + raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) + + hasher = hashing.Hash( + hashing.SHA1(), backend=backends.default_backend() # nosec + ) + hasher.update(b"\x99") + hasher.update(struct.pack(">H", len(pubkey_packet_data))) + hasher.update(bytes(pubkey_packet_data)) + return binascii.hexlify(hasher.finalize()).decode("ascii") + + +def parse_subpacket_header(data): + """Parse out subpacket header as per RFC4880 5.2.3.1. Signature Subpacket + Specification.""" + # NOTE: Although the RFC does not state it explicitly, the length encoded + # in the header must be greater equal 1, as it includes the mandatory + # subpacket type octet. + # Hence, passed bytearrays like [0] or [255, 0, 0, 0, 0], which encode a + # subpacket length 0 are invalid. + # The caller has to deal with the resulting IndexError. + if data[0] < 192: + length_len = 1 + length = data[0] + + elif data[0] >= 192 and data[0] < 255: + length_len = 2 + length = (data[0] - 192 << 8) + (data[1] + 192) + + elif data[0] == 255: + length_len = 5 + length = struct.unpack(">I", data[1:length_len])[0] + + else: # pragma: no cover (unreachable) + raise PacketParsingError("Invalid subpacket header") + + return data[length_len], length_len + 1, length - 1, length_len + length + - Expects a version in the format `major.minor.patch`. `major` and `minor` - must be one-digit numbers; `patch` can be any integer. +def parse_subpackets(data): + """ + + parse the subpackets fields - value: - The version string to parse. + data: the unparsed subpacketoctets - ValueError: - If the version string is invalid. + IndexErrorif the subpackets octets are incomplete or malformed + + + None - Version + A list of tuples with like: + [ (packet_type, data), + (packet_type, data), + ... + ] """ - match = cls.VERSION_RE.fullmatch(value) - if not match: - raise ValueError( - f"Invalid version number '{value}'; " - f"expected MAJOR.MINOR.PATCH (e.g., '{cls.EXAMPLE}')" - ) - major, minor, patch = map(int, match.groups()) - return cls(major, minor, patch) + parsed_subpackets = [] + position = 0 + + while position < len(data): + subpacket_type, header_len, _, subpacket_len = parse_subpacket_header( + data[position:] + ) + + payload = data[position + header_len : position + subpacket_len] + parsed_subpackets.append((subpacket_type, payload)) + + position += subpacket_len + + return parsed_subpackets + + +@dataclasses.dataclass(order=True) +class Version: + """A version of GPG.""" - def __str__(self): - return f"{self.major}.{self.minor}.{self.patch}" + major: int + minor: int + patch: int + + VERSION_RE = re.compile(r"(\d)\.(\d)\.(\d+)") + EXAMPLE = "1.3.22" + + @classmethod + def from_string(cls, value: str) -> "Version": + """ + + Parses `value` as a `Version`. + + Expects a version in the format `major.minor.patch`. `major` and `minor` + must be one-digit numbers; `patch` can be any integer. + + + value: + The version string to parse. + + + ValueError: + If the version string is invalid. + + + Version + """ + match = cls.VERSION_RE.fullmatch(value) + if not match: + raise ValueError( + f"Invalid version number '{value}'; " + f"expected MAJOR.MINOR.PATCH (e.g., '{cls.EXAMPLE}')" + ) + major, minor, patch = map(int, match.groups()) + return cls(major, minor, patch) + + def __str__(self): + return f"{self.major}.{self.minor}.{self.patch}" def get_version() -> Version: - """ - - Uses `gpg2 --version` to get the version info of the installed gpg2 - and extracts and returns the version number. - - The executed base command is defined in constants.gpg_version_command. - - - securesystemslib.exceptions.UnsupportedLibraryError: - If the gpg command is not available - - - Executes a command: constants.gpg_version_command. - - - Version of GPG. - - """ - if not constants.have_gpg(): # pragma: no cover - raise exceptions.UnsupportedLibraryError(constants.NO_GPG_MSG) - - command = constants.gpg_version_command() - gpg_process = process.run(command, stdout=process.PIPE, - stderr=process.PIPE, universal_newlines=True) - - full_version_info = gpg_process.stdout - try: - match = Version.VERSION_RE.search(full_version_info) - if not match: - raise ValueError( - f"Couldn't find version number (ex. '{Version.EXAMPLE}') " - f"in the output of `{command}`:\n" - + full_version_info + """ + + Uses `gpg2 --version` to get the version info of the installed gpg2 + and extracts and returns the version number. + + The executed base command is defined in constants.gpg_version_command. + + + securesystemslib.exceptions.UnsupportedLibraryError: + If the gpg command is not available + + + Executes a command: constants.gpg_version_command. + + + Version of GPG. + + """ + if not constants.have_gpg(): # pragma: no cover + raise exceptions.UnsupportedLibraryError(constants.NO_GPG_MSG) + + command = constants.gpg_version_command() + gpg_process = process.run( + command, + stdout=process.PIPE, + stderr=process.PIPE, + universal_newlines=True, ) - version = Version.from_string(match.group(0)) - except ValueError as err: - raise exceptions.UnsupportedLibraryError(constants.NO_GPG_MSG) from err - return version + full_version_info = gpg_process.stdout + try: + match = Version.VERSION_RE.search(full_version_info) + if not match: + raise ValueError( + f"Couldn't find version number (ex. '{Version.EXAMPLE}') " + f"in the output of `{command}`:\n" + full_version_info + ) + version = Version.from_string(match.group(0)) + except ValueError as err: + raise exceptions.UnsupportedLibraryError(constants.NO_GPG_MSG) from err + + return version def is_version_fully_supported(): - """ - - Compares the version of installed gpg2 with the minimal fully supported - gpg2 version (2.1.0). + """ + + Compares the version of installed gpg2 with the minimal fully supported + gpg2 version (2.1.0). - - True if the version returned by `get_version` is greater-equal - constants.FULLY_SUPPORTED_MIN_VERSION, False otherwise. + + True if the version returned by `get_version` is greater-equal + constants.FULLY_SUPPORTED_MIN_VERSION, False otherwise. - """ - min_version = constants.FULLY_SUPPORTED_MIN_VERSION - return get_version() >= Version.from_string(min_version) + """ + min_version = constants.FULLY_SUPPORTED_MIN_VERSION + return get_version() >= Version.from_string(min_version) def get_hashing_class(hash_algorithm_id): - """ - - Return a pyca/cryptography hashing class reference for the passed RFC4880 - hash algorithm ID. - - - hash_algorithm_id: - one of SHA1, SHA256, SHA512 (see securesystemslib.gpg.constants) - - - ValueError - if the passed hash_algorithm_id is not supported. - - - A pyca/cryptography hashing class - - """ - supported_hashing_algorithms = [constants.SHA1, constants.SHA256, - constants.SHA512] - corresponding_hashing_classes = [hashing.SHA1, hashing.SHA256, - hashing.SHA512] - - # Map supported hash algorithm ids to corresponding hashing classes - hashing_class = dict(zip(supported_hashing_algorithms, - corresponding_hashing_classes)) - - try: - return hashing_class[hash_algorithm_id] - - except KeyError: - raise ValueError("Hash algorithm '{}' not supported, must be one of '{}' " - "(see RFC4880 9.4. Hash Algorithms).".format(hash_algorithm_id, - supported_hashing_algorithms)) + """ + + Return a pyca/cryptography hashing class reference for the passed RFC4880 + hash algorithm ID. + + + hash_algorithm_id: + one of SHA1, SHA256, SHA512 (see securesystemslib.gpg.constants) + + + ValueError + if the passed hash_algorithm_id is not supported. + + + A pyca/cryptography hashing class + + """ + supported_hashing_algorithms = [ + constants.SHA1, + constants.SHA256, + constants.SHA512, + ] + corresponding_hashing_classes = [ + hashing.SHA1, + hashing.SHA256, + hashing.SHA512, + ] + + # Map supported hash algorithm ids to corresponding hashing classes + hashing_class = dict( + zip(supported_hashing_algorithms, corresponding_hashing_classes) + ) + + try: + return hashing_class[hash_algorithm_id] + + except KeyError: + raise ValueError( # pylint: disable=raise-missing-from + "Hash algorithm '{}' not supported, must be one of '{}' " # pylint: disable=consider-using-f-string + "(see RFC4880 9.4. Hash Algorithms).".format( + hash_algorithm_id, supported_hashing_algorithms + ) + ) diff --git a/securesystemslib/hash.py b/securesystemslib/hash.py index d2ed73e6..4ea04e17 100755 --- a/securesystemslib/hash.py +++ b/securesystemslib/hash.py @@ -24,403 +24,411 @@ import hashlib -from securesystemslib import exceptions -from securesystemslib import formats +from securesystemslib import exceptions, formats from securesystemslib.storage import FilesystemBackend - DEFAULT_CHUNK_SIZE = 4096 -DEFAULT_HASH_ALGORITHM = 'sha256' -DEFAULT_HASH_LIBRARY = 'hashlib' -SUPPORTED_LIBRARIES = ['hashlib'] +DEFAULT_HASH_ALGORITHM = "sha256" +DEFAULT_HASH_LIBRARY = "hashlib" +SUPPORTED_LIBRARIES = ["hashlib"] # If `pyca_crypto` is installed, add it to supported libraries try: - from cryptography.hazmat.backends import default_backend - from cryptography.hazmat.primitives import hashes as _pyca_hashes - import binascii - - # Dictionary of `pyca/cryptography` supported hash algorithms. - PYCA_DIGEST_OBJECTS_CACHE = { - "md5": _pyca_hashes.MD5, - "sha1": _pyca_hashes.SHA1, - "sha224": _pyca_hashes.SHA224, - "sha256": _pyca_hashes.SHA256, - "sha384": _pyca_hashes.SHA384, - "sha512": _pyca_hashes.SHA512 - } - - SUPPORTED_LIBRARIES.append('pyca_crypto') - - class PycaDiggestWrapper(object): + import binascii + + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import hashes as _pyca_hashes + + # Dictionary of `pyca/cryptography` supported hash algorithms. + PYCA_DIGEST_OBJECTS_CACHE = { + "md5": _pyca_hashes.MD5, + "sha1": _pyca_hashes.SHA1, + "sha224": _pyca_hashes.SHA224, + "sha256": _pyca_hashes.SHA256, + "sha384": _pyca_hashes.SHA384, + "sha512": _pyca_hashes.SHA512, + } + + SUPPORTED_LIBRARIES.append("pyca_crypto") + + class PycaDiggestWrapper( + object + ): # pylint: disable=useless-object-inheritance + """ + + A wrapper around `cryptography.hazmat.primitives.hashes.Hash` which adds + additional methods to meet expected interface for digest objects: + + digest_object.digest_size + digest_object.hexdigest() + digest_object.update('data') + digest_object.digest() + + + algorithm: + Specific for `cryptography.hazmat.primitives.hashes.Hash` object, but + needed for `rsa_keys.py` + + digest_size: + Returns original's object digest size. + + + digest(self) -> bytes: + Calls original's object `finalize` method and returns digest as bytes. + NOTE: `cryptography.hazmat.primitives.hashes.Hash` allows calling + `finalize` method just once on the same instance, so everytime `digest` + methods is called, we replace internal object (`_digest_obj`). + + hexdigest(self) -> str: + Returns a string hex representation of digest. + + update(self, data) -> None: + Updates digest object data by calling the original's object `update` + method. + """ + + def __init__(self, digest_obj): + self._digest_obj = digest_obj + + @property + def algorithm(self): + return self._digest_obj.algorithm + + @property + def digest_size(self): + return self._digest_obj.algorithm.digest_size + + def digest(self): + digest_obj_copy = self._digest_obj.copy() + digest = ( # pylint: disable=redefined-outer-name + self._digest_obj.finalize() + ) + self._digest_obj = digest_obj_copy + return digest + + def hexdigest(self): + return binascii.hexlify(self.digest()).decode("utf-8") + + def update(self, data): + self._digest_obj.update(data) + +except ImportError: # pragma: no cover + pass + + +def digest(algorithm=DEFAULT_HASH_ALGORITHM, hash_library=DEFAULT_HASH_LIBRARY): """ - A wrapper around `cryptography.hazmat.primitives.hashes.Hash` which adds - additional methods to meet expected interface for digest objects: + Provide the caller with the ability to create digest objects without having + to worry about crypto library availability or which library to use. The + caller also has the option of specifying which hash algorithm and/or + library to use. + + # Creation of a digest object using defaults or by specifying hash + # algorithm and library. + digest_object = securesystemslib.hash.digest() + digest_object = securesystemslib.hash.digest('sha384') + digest_object = securesystemslib.hash.digest('sha256', 'hashlib') + + # The expected interface for digest objects. + digest_object.digest_size + digest_object.hexdigest() + digest_object.update('data') + digest_object.digest() + + # Added hash routines by this module. + digest_object = securesystemslib.hash.digest_fileobject(file_object) + digest_object = securesystemslib.hash.digest_filename(filename) + + + algorithm: + The hash algorithm (e.g., 'md5', 'sha1', 'sha256'). - digest_object.digest_size - digest_object.hexdigest() - digest_object.update('data') - digest_object.digest() + hash_library: + The crypto library to use for the given hash algorithm (e.g., 'hashlib'). - - algorithm: - Specific for `cryptography.hazmat.primitives.hashes.Hash` object, but - needed for `rsa_keys.py` + + securesystemslib.exceptions.FormatError, if the arguments are + improperly formatted. - digest_size: - Returns original's object digest size. + securesystemslib.exceptions.UnsupportedAlgorithmError, if an unsupported + hashing algorithm is specified, or digest could not be generated with given + the algorithm. - - digest(self) -> bytes: - Calls original's object `finalize` method and returns digest as bytes. - NOTE: `cryptography.hazmat.primitives.hashes.Hash` allows calling - `finalize` method just once on the same instance, so everytime `digest` - methods is called, we replace internal object (`_digest_obj`). + securesystemslib.exceptions.UnsupportedLibraryError, if an unsupported + library was requested via 'hash_library'. - hexdigest(self) -> str: - Returns a string hex representation of digest. + + None. - update(self, data) -> None: - Updates digest object data by calling the original's object `update` - method. - """ + + Digest object - def __init__(self, digest_obj): - self._digest_obj = digest_obj + e.g. + hashlib.new(algorithm) or + PycaDiggestWrapper object + """ - @property - def algorithm(self): - return self._digest_obj.algorithm + # Are the arguments properly formatted? If not, raise + # 'securesystemslib.exceptions.FormatError'. + formats.NAME_SCHEMA.check_match(algorithm) + formats.NAME_SCHEMA.check_match(hash_library) + + # Was a hashlib digest object requested and is it supported? + # If so, return the digest object. + if hash_library == "hashlib" and hash_library in SUPPORTED_LIBRARIES: + try: + if algorithm == "blake2b-256": # pylint: disable=no-else-return + return hashlib.new("blake2b", digest_size=32) + else: + return hashlib.new(algorithm) + + except (ValueError, TypeError): + # ValueError: the algorithm value was unknown + # TypeError: unexpected argument digest_size (on old python) + raise exceptions.UnsupportedAlgorithmError( # pylint: disable=raise-missing-from + algorithm + ) + + # Was a pyca_crypto digest object requested and is it supported? + elif hash_library == "pyca_crypto" and hash_library in SUPPORTED_LIBRARIES: + try: + hash_algorithm = PYCA_DIGEST_OBJECTS_CACHE[algorithm]() + return PycaDiggestWrapper( + _pyca_hashes.Hash(hash_algorithm, default_backend()) + ) + + except KeyError: + raise exceptions.UnsupportedAlgorithmError( # pylint: disable=raise-missing-from + algorithm + ) + + # The requested hash library is not supported. + else: + raise exceptions.UnsupportedLibraryError( + "Unsupported" + " library requested. Supported hash" + " libraries: " + repr(SUPPORTED_LIBRARIES) + ) + + +def digest_fileobject( + file_object, + algorithm=DEFAULT_HASH_ALGORITHM, + hash_library=DEFAULT_HASH_LIBRARY, + normalize_line_endings=False, +): + """ + + Generate a digest object given a file object. The new digest object + is updated with the contents of 'file_object' prior to returning the + object to the caller. - @property - def digest_size(self): - return self._digest_obj.algorithm.digest_size + + file_object: + File object whose contents will be used as the data + to update the hash of a digest object to be returned. - def digest(self): - digest_obj_copy = self._digest_obj.copy() - digest = self._digest_obj.finalize() - self._digest_obj = digest_obj_copy - return digest + algorithm: + The hash algorithm (e.g., 'md5', 'sha1', 'sha256'). - def hexdigest(self): - return binascii.hexlify(self.digest()).decode('utf-8') + hash_library: + The library providing the hash algorithms (e.g., 'hashlib'). - def update(self, data): - self._digest_obj.update(data) + normalize_line_endings: (default False) + Whether or not to normalize line endings for cross-platform support. + Note that this results in ambiguous hashes (e.g. 'abc\n' and 'abc\r\n' + will produce the same hash), so be careful to only apply this to text + files (not binary), when that equivalence is desirable and cannot result + in easily-maliciously-corrupted files producing the same hash as a valid + file. -except ImportError: #pragma: no cover - pass + + securesystemslib.exceptions.FormatError, if the arguments are + improperly formatted. + securesystemslib.exceptions.UnsupportedAlgorithmError, if an unsupported + hashing algorithm was specified via 'algorithm'. + securesystemslib.exceptions.UnsupportedLibraryError, if an unsupported + crypto library was specified via 'hash_library'. + + None. -def digest(algorithm=DEFAULT_HASH_ALGORITHM, hash_library=DEFAULT_HASH_LIBRARY): - """ - - Provide the caller with the ability to create digest objects without having - to worry about crypto library availability or which library to use. The - caller also has the option of specifying which hash algorithm and/or - library to use. - - # Creation of a digest object using defaults or by specifying hash - # algorithm and library. - digest_object = securesystemslib.hash.digest() - digest_object = securesystemslib.hash.digest('sha384') - digest_object = securesystemslib.hash.digest('sha256', 'hashlib') - - # The expected interface for digest objects. - digest_object.digest_size - digest_object.hexdigest() - digest_object.update('data') - digest_object.digest() - - # Added hash routines by this module. - digest_object = securesystemslib.hash.digest_fileobject(file_object) - digest_object = securesystemslib.hash.digest_filename(filename) - - - algorithm: - The hash algorithm (e.g., 'md5', 'sha1', 'sha256'). - - hash_library: - The crypto library to use for the given hash algorithm (e.g., 'hashlib'). - - - securesystemslib.exceptions.FormatError, if the arguments are - improperly formatted. - - securesystemslib.exceptions.UnsupportedAlgorithmError, if an unsupported - hashing algorithm is specified, or digest could not be generated with given - the algorithm. - - securesystemslib.exceptions.UnsupportedLibraryError, if an unsupported - library was requested via 'hash_library'. - - - None. - - - Digest object - - e.g. - hashlib.new(algorithm) or - PycaDiggestWrapper object - """ - - # Are the arguments properly formatted? If not, raise - # 'securesystemslib.exceptions.FormatError'. - formats.NAME_SCHEMA.check_match(algorithm) - formats.NAME_SCHEMA.check_match(hash_library) - - # Was a hashlib digest object requested and is it supported? - # If so, return the digest object. - if hash_library == 'hashlib' and hash_library in SUPPORTED_LIBRARIES: - try: - if algorithm == 'blake2b-256': - return hashlib.new('blake2b', digest_size=32) - else: - return hashlib.new(algorithm) - - except (ValueError, TypeError): - # ValueError: the algorithm value was unknown - # TypeError: unexpected argument digest_size (on old python) - raise exceptions.UnsupportedAlgorithmError(algorithm) - - # Was a pyca_crypto digest object requested and is it supported? - elif hash_library == 'pyca_crypto' and hash_library in SUPPORTED_LIBRARIES: - try: - hash_algorithm = PYCA_DIGEST_OBJECTS_CACHE[algorithm]() - return PycaDiggestWrapper( - _pyca_hashes.Hash(hash_algorithm, default_backend())) - - except KeyError: - raise exceptions.UnsupportedAlgorithmError(algorithm) - - # The requested hash library is not supported. - else: - raise exceptions.UnsupportedLibraryError('Unsupported' - ' library requested. Supported hash' - ' libraries: ' + repr(SUPPORTED_LIBRARIES)) - - - - - -def digest_fileobject(file_object, algorithm=DEFAULT_HASH_ALGORITHM, - hash_library=DEFAULT_HASH_LIBRARY, normalize_line_endings=False): - """ - - Generate a digest object given a file object. The new digest object - is updated with the contents of 'file_object' prior to returning the - object to the caller. - - - file_object: - File object whose contents will be used as the data - to update the hash of a digest object to be returned. - - algorithm: - The hash algorithm (e.g., 'md5', 'sha1', 'sha256'). - - hash_library: - The library providing the hash algorithms (e.g., 'hashlib'). - - normalize_line_endings: (default False) - Whether or not to normalize line endings for cross-platform support. - Note that this results in ambiguous hashes (e.g. 'abc\n' and 'abc\r\n' - will produce the same hash), so be careful to only apply this to text - files (not binary), when that equivalence is desirable and cannot result - in easily-maliciously-corrupted files producing the same hash as a valid - file. - - - securesystemslib.exceptions.FormatError, if the arguments are - improperly formatted. - - securesystemslib.exceptions.UnsupportedAlgorithmError, if an unsupported - hashing algorithm was specified via 'algorithm'. - - securesystemslib.exceptions.UnsupportedLibraryError, if an unsupported - crypto library was specified via 'hash_library'. - - - None. - - - Digest object - - e.g. - hashlib.new(algorithm) or - PycaDiggestWrapper object - """ - - # Are the arguments properly formatted? If not, raise - # 'securesystemslib.exceptions.FormatError'. - formats.NAME_SCHEMA.check_match(algorithm) - formats.NAME_SCHEMA.check_match(hash_library) - - # Digest object returned whose hash will be updated using 'file_object'. - # digest() raises: - # securesystemslib.exceptions.UnsupportedAlgorithmError - # securesystemslib.exceptions.UnsupportedLibraryError - digest_object = digest(algorithm, hash_library) - - # Defensively seek to beginning, as there's no case where we don't - # intend to start from the beginning of the file. - file_object.seek(0) - - # Read the contents of the file object in at most 4096-byte chunks. - # Update the hash with the data read from each chunk and return after - # the entire file is processed. - while True: - data = file_object.read(DEFAULT_CHUNK_SIZE) - if not data: - break - - if normalize_line_endings: - while data[-1:] == b'\r': - c = file_object.read(1) - if not c: - break - - data += c - - data = ( - data - # First Windows - .replace(b'\r\n', b'\n') - # Then Mac - .replace(b'\r', b'\n') - ) + + Digest object - if not isinstance(data, bytes): - digest_object.update(data.encode('utf-8')) + e.g. + hashlib.new(algorithm) or + PycaDiggestWrapper object + """ - else: - digest_object.update(data) + # Are the arguments properly formatted? If not, raise + # 'securesystemslib.exceptions.FormatError'. + formats.NAME_SCHEMA.check_match(algorithm) + formats.NAME_SCHEMA.check_match(hash_library) - return digest_object + # Digest object returned whose hash will be updated using 'file_object'. + # digest() raises: + # securesystemslib.exceptions.UnsupportedAlgorithmError + # securesystemslib.exceptions.UnsupportedLibraryError + digest_object = digest(algorithm, hash_library) + + # Defensively seek to beginning, as there's no case where we don't + # intend to start from the beginning of the file. + file_object.seek(0) + + # Read the contents of the file object in at most 4096-byte chunks. + # Update the hash with the data read from each chunk and return after + # the entire file is processed. + while True: + data = file_object.read(DEFAULT_CHUNK_SIZE) + if not data: + break + + if normalize_line_endings: + while data[-1:] == b"\r": + c = file_object.read(1) + if not c: + break + + data += c + + data = ( + data + # First Windows + .replace(b"\r\n", b"\n") + # Then Mac + .replace(b"\r", b"\n") + ) + + if not isinstance(data, bytes): + digest_object.update(data.encode("utf-8")) + + else: + digest_object.update(data) + + return digest_object + + +def digest_filename( + filename, + algorithm=DEFAULT_HASH_ALGORITHM, + hash_library=DEFAULT_HASH_LIBRARY, + normalize_line_endings=False, + storage_backend=None, +): + """ + + Generate a digest object, update its hash using a file object + specified by filename, and then return it to the caller. + + filename: + The filename belonging to the file object to be used. + algorithm: + The hash algorithm (e.g., 'md5', 'sha1', 'sha256'). + hash_library: + The library providing the hash algorithms (e.g., 'hashlib'). + normalize_line_endings: + Whether or not to normalize line endings for cross-platform support. -def digest_filename(filename, algorithm=DEFAULT_HASH_ALGORITHM, - hash_library=DEFAULT_HASH_LIBRARY, normalize_line_endings=False, - storage_backend=None): - """ - - Generate a digest object, update its hash using a file object - specified by filename, and then return it to the caller. + storage_backend: + An object which implements + securesystemslib.storage.StorageBackendInterface. When no object is + passed a FilesystemBackend will be instantiated and used. - - filename: - The filename belonging to the file object to be used. + + securesystemslib.exceptions.FormatError, if the arguments are + improperly formatted. - algorithm: - The hash algorithm (e.g., 'md5', 'sha1', 'sha256'). + securesystemslib.exceptions.UnsupportedAlgorithmError, if the given + 'algorithm' is unsupported. - hash_library: - The library providing the hash algorithms (e.g., 'hashlib'). + securesystemslib.exceptions.UnsupportedLibraryError, if the given + 'hash_library' is unsupported. - normalize_line_endings: - Whether or not to normalize line endings for cross-platform support. + securesystemslib.exceptions.StorageError, if the file cannot be opened. - storage_backend: - An object which implements - securesystemslib.storage.StorageBackendInterface. When no object is - passed a FilesystemBackend will be instantiated and used. + + None. - - securesystemslib.exceptions.FormatError, if the arguments are - improperly formatted. + + Digest object - securesystemslib.exceptions.UnsupportedAlgorithmError, if the given - 'algorithm' is unsupported. + e.g. + hashlib.new(algorithm) or + PycaDiggestWrapper object + """ + # Are the arguments properly formatted? If not, raise + # 'securesystemslib.exceptions.FormatError'. + formats.PATH_SCHEMA.check_match(filename) + formats.NAME_SCHEMA.check_match(algorithm) + formats.NAME_SCHEMA.check_match(hash_library) - securesystemslib.exceptions.UnsupportedLibraryError, if the given - 'hash_library' is unsupported. + digest_object = None - securesystemslib.exceptions.StorageError, if the file cannot be opened. + if storage_backend is None: + storage_backend = FilesystemBackend() - - None. + # Open 'filename' in read+binary mode. + with storage_backend.get(filename) as file_object: + # Create digest_object and update its hash data from file_object. + # digest_fileobject() raises: + # securesystemslib.exceptions.UnsupportedAlgorithmError + # securesystemslib.exceptions.UnsupportedLibraryError + digest_object = digest_fileobject( + file_object, algorithm, hash_library, normalize_line_endings + ) - - Digest object + return digest_object - e.g. - hashlib.new(algorithm) or - PycaDiggestWrapper object - """ - # Are the arguments properly formatted? If not, raise - # 'securesystemslib.exceptions.FormatError'. - formats.PATH_SCHEMA.check_match(filename) - formats.NAME_SCHEMA.check_match(algorithm) - formats.NAME_SCHEMA.check_match(hash_library) - digest_object = None +def digest_from_rsa_scheme(scheme, hash_library=DEFAULT_HASH_LIBRARY): + """ + + Get digest object from RSA scheme. - if storage_backend is None: - storage_backend = FilesystemBackend() + + scheme: + A string that indicates the signature scheme used to generate + 'signature'. Currently supported RSA schemes are defined in + `securesystemslib.keys.RSA_SIGNATURE_SCHEMES` - # Open 'filename' in read+binary mode. - with storage_backend.get(filename) as file_object: - # Create digest_object and update its hash data from file_object. - # digest_fileobject() raises: - # securesystemslib.exceptions.UnsupportedAlgorithmError - # securesystemslib.exceptions.UnsupportedLibraryError - digest_object = digest_fileobject( - file_object, algorithm, hash_library, normalize_line_endings) + hash_library: + The crypto library to use for the given hash algorithm (e.g., 'hashlib'). - return digest_object + + securesystemslib.exceptions.FormatError, if the arguments are + improperly formatted. + securesystemslib.exceptions.UnsupportedAlgorithmError, if an unsupported + hashing algorithm is specified, or digest could not be generated with given + the algorithm. + securesystemslib.exceptions.UnsupportedLibraryError, if an unsupported + library was requested via 'hash_library'. + + None. + + Digest object -def digest_from_rsa_scheme(scheme, hash_library=DEFAULT_HASH_LIBRARY): - """ - - Get digest object from RSA scheme. - - - scheme: - A string that indicates the signature scheme used to generate - 'signature'. Currently supported RSA schemes are defined in - `securesystemslib.keys.RSA_SIGNATURE_SCHEMES` - - hash_library: - The crypto library to use for the given hash algorithm (e.g., 'hashlib'). - - - securesystemslib.exceptions.FormatError, if the arguments are - improperly formatted. - - securesystemslib.exceptions.UnsupportedAlgorithmError, if an unsupported - hashing algorithm is specified, or digest could not be generated with given - the algorithm. - - securesystemslib.exceptions.UnsupportedLibraryError, if an unsupported - library was requested via 'hash_library'. - - - None. - - - Digest object - - e.g. - hashlib.new(algorithm) or - PycaDiggestWrapper object - """ - # Are the arguments properly formatted? If not, raise - # 'securesystemslib.exceptions.FormatError'. - formats.RSA_SCHEME_SCHEMA.check_match(scheme) - - # Get hash algorithm from rsa scheme (hash algorithm id is specified after - # the last dash; e.g. rsassa-pss-sha256 -> sha256) - hash_algorithm = scheme.split('-')[-1] - return digest(hash_algorithm, hash_library) + e.g. + hashlib.new(algorithm) or + PycaDiggestWrapper object + """ + # Are the arguments properly formatted? If not, raise + # 'securesystemslib.exceptions.FormatError'. + formats.RSA_SCHEME_SCHEMA.check_match(scheme) + + # Get hash algorithm from rsa scheme (hash algorithm id is specified after + # the last dash; e.g. rsassa-pss-sha256 -> sha256) + hash_algorithm = scheme.split("-")[-1] + return digest(hash_algorithm, hash_library) diff --git a/securesystemslib/interface.py b/securesystemslib/interface.py index 73fe6453..4238bfdb 100644 --- a/securesystemslib/interface.py +++ b/securesystemslib/interface.py @@ -20,32 +20,38 @@ 'securesystemslib/README' for the complete guide to using 'interface.py'. """ -import os -import sys import getpass +import json import logging +import os +import sys import tempfile -import json -from securesystemslib import exceptions -from securesystemslib import formats -from securesystemslib import keys -from securesystemslib import settings -from securesystemslib import util +from securesystemslib import ( + KEY_TYPE_ECDSA, + KEY_TYPE_ED25519, + KEY_TYPE_RSA, + exceptions, + formats, + keys, + settings, + util, +) from securesystemslib.storage import FilesystemBackend -from securesystemslib import KEY_TYPE_RSA, KEY_TYPE_ED25519, KEY_TYPE_ECDSA - logger = logging.getLogger(__name__) try: - from colorama import Fore - TERM_RED = Fore.RED - TERM_RESET = Fore.RESET -except ImportError: # pragma: no cover - logger.debug("Failed to find colorama module, terminal output won't be colored") - TERM_RED = '' - TERM_RESET = '' + from colorama import Fore + + TERM_RED = Fore.RED + TERM_RESET = Fore.RESET +except ImportError: # pragma: no cover + logger.debug( + "Failed to find colorama module, terminal output won't be colored" + ) + TERM_RED = "" + TERM_RESET = "" # Recommended RSA key sizes: # https://en.wikipedia.org/wiki/Key_size#Asymmetric_algorithm_key_lengths @@ -54,1017 +60,1046 @@ 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. +def get_password(prompt="Password: ", confirm=False): + """Prompts user to enter a password. - Returns: - The password entered on the prompt. + 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. - """ - formats.TEXT_SCHEMA.check_match(prompt) - formats.BOOLEAN_SCHEMA.check_match(confirm) + Returns: + The password entered on the prompt. - while True: - # getpass() prompts the user for a password without echoing - # the user input. - password = getpass.getpass(prompt, sys.stderr) + """ + formats.TEXT_SCHEMA.check_match(prompt) + formats.BOOLEAN_SCHEMA.check_match(confirm) - if not confirm: - return password - password2 = getpass.getpass('Confirm: ', sys.stderr) + while True: + # getpass() prompts the user for a password without echoing + # the user input. + password = getpass.getpass(prompt, sys.stderr) - if password == password2: - return password + if not confirm: + return password + password2 = getpass.getpass("Confirm: ", sys.stderr) - else: - print('Mismatch; try again.') + 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 " - "'" + TERM_RED + str(path) + TERM_RESET + "' (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): - 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): - raise ValueError("encryption password must be 1 or more characters long") - - return password - + """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 " + "'" + TERM_RED + str(path) + TERM_RESET + "' (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 " - "'" + TERM_RED + str(path) + TERM_RESET + "' " - "(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): - 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. + """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 " + "'" + TERM_RED + str(path) + TERM_RESET + "' " + "(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). - Raises: - UnsupportedLibraryError: pyca/cryptography is not available. - FormatError: Arguments are malformed. - StorageError: Key files cannot be written. + 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. - Side Effects: - Prompts user for a password. - Writes key files to disk. - Overwrites files if they already exist. + 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. - 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) + 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. - password = _get_key_file_decryption_password(password, prompt, filepath) + Side Effects: + Prompts user for a password. + Writes key files to disk. + Overwrites files if they already exist. - if storage_backend is None: - storage_backend = FilesystemBackend() + Returns: + The private key filepath. - with storage_backend.get(filepath) as file_object: - pem_key = file_object.read().decode('utf-8') + """ + return _generate_and_write_rsa_keypair( + filepath=filepath, bits=bits, password=None, prompt=True + ) - # 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 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. -def import_rsa_publickey_from_file(filepath, scheme='rsassa-pss-sha256', - storage_backend=None): - """Imports PEM-encoded RSA public key from file storage. + Raises: + UnsupportedLibraryError: pyca/cryptography is not available. + FormatError: Arguments are malformed. + StorageError: Key files cannot be written. - The expected key format is X.509 SubjectPublicKeyInfo. + Side Effects: + Writes unencrypted key files to disk. + Overwrites files if they already exist. - 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. + Returns: + The private key filepath. - Raises: - UnsupportedLibraryError: pyca/cryptography is not available. - FormatError: Arguments are malformed. - StorageError: Key file cannot be read. - Error: Public key is malformed. + """ + return _generate_and_write_rsa_keypair( + filepath=filepath, bits=bits, password=None, prompt=False + ) - Returns: - An RSA public key object conformant with 'RSAKEY_SCHEMA'. - """ - formats.PATH_SCHEMA.check_match(filepath) - formats.RSA_SCHEME_SCHEMA.check_match(scheme) +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. - if storage_backend is None: - storage_backend = FilesystemBackend() + 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. - with storage_backend.get(filepath) as file_object: - rsa_pubkey_pem = file_object.read().decode('utf-8') + 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. - # Convert PEM-encoded key to 'RSAKEY_SCHEMA' format - try: - rsakey_dict = keys.import_rsakey_from_public_pem(rsa_pubkey_pem, scheme) + 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) - except exceptions.FormatError as e: - raise exceptions.Error('Cannot import improperly formatted' - ' PEM file.' + repr(str(e))) + if storage_backend is None: + storage_backend = FilesystemBackend() - return rsakey_dict + 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 _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. +def import_rsa_publickey_from_file( + filepath, scheme="rsassa-pss-sha256", storage_backend=None +): + """Imports PEM-encoded RSA public key from file storage. - NOTE: The custom key format includes 'ed25519' as signing scheme. + The expected key format is X.509 SubjectPublicKeyInfo. - 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. + 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/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. + Raises: + UnsupportedLibraryError: pyca/cryptography is not available. + FormatError: Arguments are malformed. + StorageError: Key file cannot be read. + Error: Public key is malformed. - Side Effects: - Prompts user for a password if 'prompt' is True. - Writes key files to disk. - Overwrites files if they already exist. + 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. - Returns: - The private key filepath. + 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. - """ - ed25519_key = keys.generate_ed25519_key() + 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. - # Use passed 'filepath' or keyid as file name - if not filepath: - filepath = os.path.join(os.getcwd(), ed25519_key['keyid']) + Side Effects: + Prompts user for a password if 'prompt' is True. + Writes key files to disk. + Overwrites files if they already exist. - formats.PATH_SCHEMA.check_match(filepath) + Returns: + The private key filepath. - password = _get_key_file_encryption_password(password, prompt, filepath) + """ + ed25519_key = keys.generate_ed25519_key() - # Create intermediate directories as required - util.ensure_parent_dir(filepath) + # Use passed 'filepath' or keyid as file name + if not filepath: + filepath = os.path.join(os.getcwd(), ed25519_key["keyid"]) - # 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) + formats.PATH_SCHEMA.check_match(filepath) - # 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') + password = _get_key_file_encryption_password(password, prompt, filepath) - # 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) + # Create intermediate directories as required + util.ensure_parent_dir(filepath) - # 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) + # 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) - return filepath + # 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. + """Generates ed25519 key pair and writes custom JSON-formatted keys to disk. - NOTE: The custom key format includes 'ed25519' as signing scheme. + The private key is encrypted using AES-256 in CTR mode, with the passed + password strengthened in PBKDF2-HMAC-SHA256. - 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'. + NOTE: The custom key format includes 'ed25519' as signing scheme. - 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. + 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'. - Side Effects: - Writes key files to disk. - Overwrites files if they already exist. + 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. - Returns: - The private key filepath. + Side Effects: + Writes key files to disk. + Overwrites files if they already exist. - """ - formats.PASSWORD_SCHEMA.check_match(password) - return _generate_and_write_ed25519_keypair( - filepath=filepath, password=password, prompt=False) + 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. + """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. + 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. + 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'. + 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. + 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. + 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) + 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. + """Generates ed25519 key pair and writes custom JSON-formatted keys to disk. - NOTE: The custom key format includes 'ed25519' as signing scheme. + 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'. + 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. + 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. + Side Effects: + Writes unencrypted key files to disk. + Overwrites files if they already exist. - Returns: - The private key filepath. + Returns: + The private key filepath. - """ - return _generate_and_write_ed25519_keypair( - filepath=filepath, password=None, prompt=False) + """ + 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. + """Imports custom JSON-formatted ed25519 public key from disk. - Raises: - FormatError: Argument is malformed. - StorageError: Key file cannot be read. - Error: Public key is malformed. + NOTE: The signing scheme is set at key generation (see generate function). - Returns: - An ed25519 public key object conformant with 'ED25519KEY_SCHEMA'. + Arguments: + filepath: The path to read the file from. - """ - formats.PATH_SCHEMA.check_match(filepath) + Raises: + FormatError: Argument is malformed. + StorageError: Key file cannot be read. + Error: Public key is malformed. - # 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) + Returns: + An ed25519 public key object conformant with 'ED25519KEY_SCHEMA'. - # 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) + """ + formats.PATH_SCHEMA.check_match(filepath) - return ed25519_key + # 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. +def import_ed25519_privatekey_from_file( + filepath, password=None, prompt=False, storage_backend=None +): + """Imports custom JSON-formatted ed25519 private key from file storage. - NOTE: The signing scheme is set at key generation (see generate function). + 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. - 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. + 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. + 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) + Returns: + An ed25519 private key object conformant with 'ED25519KEY_SCHEMA'. - if storage_backend is None: - storage_backend = FilesystemBackend() + """ + formats.PATH_SCHEMA.check_match(filepath) + password = _get_key_file_decryption_password(password, prompt, filepath) - with storage_backend.get(filepath) as file_object: - json_str = file_object.read() + if storage_backend is None: + storage_backend = FilesystemBackend() - # 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) + 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. +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. + 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. + 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. + 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. + 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. + 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. + Returns: + The private key filepath. - """ - ecdsa_key = keys.generate_ecdsa_key() + """ + 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']) + # 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) + formats.PATH_SCHEMA.check_match(filepath) - password = _get_key_file_encryption_password(password, prompt, filepath) + password = _get_key_file_encryption_password(password, prompt, filepath) - # Create intermediate directories as required - util.ensure_parent_dir(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) + # 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) + # 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) - return filepath + # 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. + """Generates ecdsa key pair and writes custom JSON-formatted keys to disk. - NOTE: The custom key format includes 'ecdsa-sha2-nistp256' as signing scheme. + The private key is encrypted using AES-256 in CTR mode, with the passed + password strengthened in PBKDF2-HMAC-SHA256. - 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'. + NOTE: The custom key format includes 'ecdsa-sha2-nistp256' as signing scheme. - 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. + 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'. - Side Effects: - Writes key files to disk. - Overwrites files if they already exist. + 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. - Returns: - The private key filepath. + Side Effects: + Writes key files to disk. + Overwrites files if they already exist. - """ - formats.PASSWORD_SCHEMA.check_match(password) - return _generate_and_write_ecdsa_keypair( - filepath=filepath, password=password, prompt=False) + 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. + """Generates ecdsa key pair and writes custom JSON-formatted keys to disk. - NOTE: The custom key format includes 'ecdsa-sha2-nistp256' as signing scheme. + The private key is encrypted using AES-256 in CTR mode, with the password + entered on the prompt strengthened in PBKDF2-HMAC-SHA256. - 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'. + NOTE: The custom key format includes 'ecdsa-sha2-nistp256' as signing scheme. - Raises: - UnsupportedLibraryError: pyca/cryptography is not available. - FormatError: Arguments are malformed. - StorageError: Key files cannot be written. + 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'. - Side Effects: - Prompts user for a password. - Writes key files to disk. - Overwrites files if they already exist. + Raises: + UnsupportedLibraryError: pyca/cryptography is not available. + FormatError: Arguments are malformed. + StorageError: Key files cannot be written. - Returns: - The private key filepath. + Side Effects: + Prompts user for a password. + Writes key files to disk. + Overwrites files if they already exist. - """ - return _generate_and_write_ecdsa_keypair( - filepath=filepath, password=None, prompt=True) + 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. + """Generates ecdsa key pair and writes custom JSON-formatted keys to disk. - 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'. + NOTE: The custom key format includes 'ecdsa-sha2-nistp256' as signing scheme. - Raises: - UnsupportedLibraryError: pyca/cryptography is not available. - FormatError: Arguments are malformed. - StorageError: Key files cannot be written. + 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'. - Side Effects: - Writes unencrypted key files to disk. - Overwrites files if they already exist. + Raises: + UnsupportedLibraryError: pyca/cryptography is not available. + FormatError: Arguments are malformed. + StorageError: Key files cannot be written. - Returns: - The private key filepath. + Side Effects: + Writes unencrypted key files to disk. + Overwrites files if they already exist. - """ - return _generate_and_write_ecdsa_keypair( - filepath=filepath, password=None, prompt=False) + 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. + """Imports custom JSON-formatted ecdsa public key from disk. - NOTE: The signing scheme is set at key generation (see generate function). + NOTE: The signing scheme is set at key generation (see generate function). - Arguments: - filepath: The path to read the file from. + 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. + 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'. + Returns: + An ecdsa public key object conformant with 'ECDSAKEY_SCHEMA'. - """ - formats.PATH_SCHEMA.check_match(filepath) + """ + 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) + # 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 + 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. -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. - 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). - 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. - 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. - 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'. - Returns: - An ecdsa private key object conformant with 'ED25519KEY_SCHEMA'. + """ + formats.PATH_SCHEMA.check_match(filepath) - """ - formats.PATH_SCHEMA.check_match(filepath) + password = _get_key_file_decryption_password(password, prompt, filepath) - password = _get_key_file_decryption_password(password, prompt, filepath) + if storage_backend is None: + storage_backend = FilesystemBackend() - if storage_backend is None: - storage_backend = FilesystemBackend() + with storage_backend.get(filepath) as file_object: + key_data = file_object.read().decode("utf-8") - 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) + # 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) - # 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 + # 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) - return key_object + # 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 {}), " - "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) + """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( - 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: - 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( - key_type, KEY_TYPE_RSA, KEY_TYPE_ED25519, KEY_TYPE_ECDSA)) + 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 -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() + doctest.testmod() diff --git a/securesystemslib/keys.py b/securesystemslib/keys.py index 0baf5a16..940f97a5 100755 --- a/securesystemslib/keys.py +++ b/securesystemslib/keys.py @@ -50,21 +50,21 @@ # Required for hexadecimal conversions. Signatures and public/private keys are # hexlified. import binascii - import logging -from securesystemslib import ecdsa_keys -from securesystemslib import ed25519_keys -from securesystemslib import exceptions -from securesystemslib import formats -from securesystemslib import rsa_keys -from securesystemslib import settings -from securesystemslib import util +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' +_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 @@ -74,295 +74,283 @@ RSA_SIGNATURE_SCHEMES = [ - 'rsassa-pss-md5', - 'rsassa-pss-sha1', - 'rsassa-pss-sha224', - 'rsassa-pss-sha256', - 'rsassa-pss-sha384', - 'rsassa-pss-sha512', - 'rsa-pkcs1v15-md5', - 'rsa-pkcs1v15-sha1', - 'rsa-pkcs1v15-sha224', - 'rsa-pkcs1v15-sha256', - 'rsa-pkcs1v15-sha384', - 'rsa-pkcs1v15-sha512', + "rsassa-pss-md5", + "rsassa-pss-sha1", + "rsassa-pss-sha224", + "rsassa-pss-sha256", + "rsassa-pss-sha384", + "rsassa-pss-sha512", + "rsa-pkcs1v15-md5", + "rsa-pkcs1v15-sha1", + "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 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 @@ -421,45 +409,47 @@ def format_keyval_to_metadata(keytype, scheme, key_value, private=False): 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 '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 'key_value' have the correct format? - formats.KEYVAL_SCHEMA.check_match(key_value) + # Does 'scheme' have the correct format? + formats.SCHEME_SCHEMA.check_match(scheme) - 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)) + # Does 'key_value' have the correct format? + formats.KEYVAL_SCHEMA.check_match(key_value) - else: - return {'keytype': keytype, 'scheme': scheme, 'keyval': key_value} - - else: - public_key_value = {'public': key_value['public']} - - return {'keytype': keytype, - 'scheme': scheme, - 'keyid_hash_algorithms': settings.HASH_ALGORITHMS, - 'keyval': public_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: # pylint: disable=no-else-raise + raise exceptions.FormatError( + "The required private key" + " is missing from: " + repr(key_value) + ) + else: + return {"keytype": keytype, "scheme": scheme, "keyval": key_value} + else: + 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): - """ +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 @@ -522,1366 +512,1363 @@ def format_metadata_to_key(key_metadata, default_keyid=None, '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) + # 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) - if keyid_hash_algorithms is None: - keyid_hash_algorithms = settings.HASH_ALGORITHMS + # Construct the dictionary to be returned. + key_dict = {} + keytype = key_metadata["keytype"] + scheme = key_metadata["scheme"] + key_value = key_metadata["keyval"] - for hash_algorithm in keyid_hash_algorithms: - keyid = _get_keyid(keytype, scheme, key_value, hash_algorithm) - keyids.add(keyid) + # 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) - # 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 + if keyid_hash_algorithms is None: + keyid_hash_algorithms = settings.HASH_ALGORITHMS - return key_dict, keyids + 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) +def _get_keyid(keytype, scheme, key_value, hash_algorithm="sha256"): + """Return the keyid of 'key_value'.""" - # Convert the key to JSON Canonical format, suitable for adding - # to digest objects. - key_update_data = formats.encode_canonical(key_meta) + # '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 + ) - # 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 + # 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) + """ + + 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"]: # pylint: disable=no-else-raise + raise exceptions.CryptoError( + "The KEYID (" + " " + repr(key_dict["keyid"]) + " ) in the given key does not match" + " the KEYID ( " + repr(signature["keyid"]) + " ) in the signature." + ) 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): - """ - - 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: + 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': 'f30a0870d026980100c0573bd557394f8c1bbd6...', + 'keyid': keyid, 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} - The public and private keys are strings in PEM format. + The private key is a string in PEM format. - signature: - The signature dictionary produced by one of the key generation functions. - 'signature' has the form: + >>> 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 - {'keyid': 'f30a0870d026980100c0573bd557394f8c1bbd6...', - 'sig': sig}. + + pem: + A string in PEM format. The private key is extracted and returned in + an rsakey object. - 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. + scheme: + The signature scheme used by the imported key. - securesystemslib.exceptions.UnsupportedAlgorithmError, if 'key_dict' or - 'signature' specifies an unsupported algorithm. + 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.CryptoError, if the KEYID in the given - 'key_dict' does not match the KEYID in 'signature'. + + securesystemslib.exceptions.FormatError, if the arguments are improperly + formatted. - - The cryptography library specified in 'settings' called to do the actual - verification. + securesystemslib.exceptions.UnsupportedAlgorithmError, if 'pem' specifies + an unsupported key type. - - Boolean. True if the signature is valid, False otherwise. - """ + + None. - # 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.') - - else: - 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) + + A dictionary containing the RSA keys and other identifying information. + Conforms to 'securesystemslib.formats.RSAKEY_SCHEMA'. + """ - 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) + # Does 'pem' have the correct format? + # This check will ensure 'pem' conforms to + # 'securesystemslib.formats.PEMRSA_SCHEMA'. + formats.PEMRSA_SCHEMA.check_match(pem) - else: - raise exceptions.UnsupportedAlgorithmError('Unsupported' - ' signature scheme is specified: ' + repr(scheme)) + # Is 'scheme' properly formatted? + formats.RSA_SCHEME_SCHEMA.check_match(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) + if password is not None: + formats.PASSWORD_SCHEMA.check_match(password) 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: + 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) + {'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. - # 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'. + + A dictionary containing the RSA keys and other identifying information. + Conforms to 'securesystemslib.formats.RSAKEY_SCHEMA'. + """ - if is_pem_public(pem): - public_pem = extract_pem(pem, private_pem=False) + # 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) - else: - raise exceptions.FormatError('Invalid public pem: ' + repr(pem)) + # Does 'scheme' have the correct format? + formats.RSA_SCHEME_SCHEMA.check_match(scheme) - # Begin building the RSA key dictionary. - rsakey_dict = {} - keytype = 'rsa' + # 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'. - # 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) + if is_pem_public(pem): + public_pem = extract_pem(pem, private_pem=False) - rsakey_dict['keytype'] = keytype - rsakey_dict['scheme'] = scheme - rsakey_dict['keyid'] = keyid - rsakey_dict['keyval'] = key_value + else: + raise exceptions.FormatError("Invalid public pem: " + repr(pem)) - # 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 + # Begin building the RSA key dictionary. + rsakey_dict = {} + keytype = "rsa" - return rsakey_dict + # 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. +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. + + pem: + A string in PEM format. - scheme: - The signature scheme used by the imported key. + scheme: + The signature scheme used by the imported key. - - securesystemslib.exceptions.FormatError, if 'pem' is improperly formatted. + + securesystemslib.exceptions.FormatError, if 'pem' is improperly formatted. - - None. + + None. - - A dictionary containing the RSA keys and other identifying information. - Conforms to 'securesystemslib.formats.RSAKEY_SCHEMA'. - """ + + 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 '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) + # Is 'scheme' properly formatted? + formats.RSA_SCHEME_SCHEMA.check_match(scheme) - public_pem = '' + 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) + # 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) + 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) + else: + raise exceptions.FormatError( + "PEM contains neither a" " public nor private key: " + repr(pem) + ) - rsakey_dict['keytype'] = keytype - rsakey_dict['scheme'] = scheme - rsakey_dict['keyid'] = keyid - rsakey_dict['keyval'] = key_value + # Begin building the RSA key dictionary. + rsakey_dict = {} + keytype = "rsa" - # 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 + # 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) - return rsakey_dict + 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-----' + """ + + 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: - or + '-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----' - '-----BEGIN RSA PRIVATE KEY----- ... -----END RSA PRIVATE KEY-----' + or - 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. + '-----BEGIN RSA PRIVATE KEY----- ... -----END RSA PRIVATE KEY-----' - - 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. + 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. - - 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. + + 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. - if private_pem: - pem_header = '-----BEGIN RSA PRIVATE KEY-----' - pem_footer = '-----END RSA PRIVATE KEY-----' + + securesystemslib.exceptions.FormatError, if 'pem' is improperly formatted. - else: - pem_header = '-----BEGIN PUBLIC KEY-----' - pem_footer = '-----END PUBLIC KEY-----' + + Only the public and private portion of the PEM is extracted. Leading or + trailing whitespace is not included in the returned PEM string. - header_start = 0 - footer_start = 0 + + A PEM string (excluding leading and trailing newline characters). + That is: pem header + key material + pem footer. - # 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('Required PEM' - ' header ' + repr(pem_header) + '\n not found in PEM' - ' string: ' + repr(pem)) + if private_pem: + pem_header = "-----BEGIN RSA PRIVATE KEY-----" + pem_footer = "-----END RSA PRIVATE KEY-----" else: - raise exceptions.FormatError('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)) + pem_header = "-----BEGIN PUBLIC KEY-----" + pem_footer = "-----END PUBLIC KEY-----" - except ValueError: - # Be careful not to print private key material in exception message. - if not private_pem: - raise exceptions.FormatError('Required PEM' - ' footer ' + repr(pem_footer) + '\n not found in PEM' - ' string ' + repr(pem)) + header_start = 0 + footer_start = 0 - else: - raise exceptions.FormatError('Required PEM' - ' footer ' + repr(pem_footer) + '\n not found in private PEM string.') + # 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: # pylint: disable=no-else-raise + raise exceptions.FormatError( # pylint: disable=raise-missing-from + "Required PEM" + " header " + repr(pem_header) + "\n not found in PEM" + " string: " + repr(pem) + ) + + else: + raise exceptions.FormatError( # pylint: disable=raise-missing-from + "Required PEM" + " header " + + repr(pem_header) + + "\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)] + try: + # Search for 'pem_footer' after the PEM header. + footer_start = pem.index(pem_footer, header_start + len(pem_header)) - return pem + except ValueError: + # Be careful not to print private key material in exception message. + if not private_pem: # pylint: disable=no-else-raise + raise exceptions.FormatError( # pylint: disable=raise-missing-from + "Required PEM" + " footer " + repr(pem_footer) + "\n not found in PEM" + " string " + repr(pem) + ) + else: + 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 + """ + + 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 - - - + """ + + 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 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. - 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-----' + c.f. cryptography.io/en/latest/hazmat/primitives/asymmetric/serialization/ + #cryptography.hazmat.primitives.serialization.BestAvailableEncryption >>> rsa_key = generate_rsa_key() - >>> public = rsa_key['keyval']['public'] >>> private = rsa_key['keyval']['private'] - >>> is_pem_public(public) + >>> passphrase = 'secret' + >>> encrypted_pem = create_rsa_encrypted_pem(private, passphrase) + >>> securesystemslib.formats.PEMRSA_SCHEMA.matches(encrypted_pem) True - >>> is_pem_public(private) - False - - pem: - A string in PEM format. + + private_key: + The private key string in PEM format. - - securesystemslib.exceptions.FormatError, if 'pem' is improperly formatted. - - - None + 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. - - True if 'pem' is public and false otherwise. - """ + + securesystemslib.exceptions.FormatError, if the arguments are improperly + formatted. - # 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) + securesystemslib.exceptions.CryptoError, if an RSA key in encrypted PEM + format cannot be created. - pem_header = '-----BEGIN PUBLIC KEY-----' - pem_footer = '-----END PUBLIC KEY-----' + TypeError, 'private_key' is unset. - try: - header_start = pem.index(pem_header) - pem.index(pem_footer, header_start + len(pem_header)) + + None. - except ValueError: - return False + + A string in PEM format, where the private RSA key is encrypted. + Conforms to 'securesystemslib.formats.PEMRSA_SCHEMA'. + """ - return True + # 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) -def is_pem_private(pem, keytype='rsa'): - """ - - Checks if a passed PEM formatted string is a PRIVATE key, by looking for - the following patterns: + return encrypted_pem - '-----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)) +def is_pem_public(pem): + """ + + Checks if a passed PEM formatted string is a PUBLIC key, by looking for the + following pattern: - except ValueError: - return False + '-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----' - return True + >>> 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. + """ -def import_ed25519key_from_private_json(json_str, password=None): - 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) + # 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) - # 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) + pem_header = "-----BEGIN PUBLIC KEY-----" + pem_footer = "-----END PUBLIC KEY-----" - 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('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. + header_start = pem.index(pem_header) + pem.index(pem_footer, header_start + len(pem_header)) - - securesystemslib.exceptions.FormatError, if the arguments are improperly - formatted. - - securesystemslib.exceptions.UnsupportedAlgorithmError, if 'pem' specifies - an unsupported key type. + except ValueError: + return False - - 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) + return True - 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 +def is_pem_private(pem, keytype="rsa"): + """ + + Checks if a passed PEM formatted string is a PRIVATE key, by looking for + the following patterns: - public, private = \ - ecdsa_keys.create_ecdsa_public_and_private_from_pem(pem, password) + '-----BEGIN RSA PRIVATE KEY----- ... -----END RSA PRIVATE KEY-----' + '-----BEGIN EC PRIVATE KEY----- ... -----END EC PRIVATE KEY-----' - # 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) + >>> 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 - # 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 + + pem: + A string in PEM format. - ecdsakey_dict['keytype'] = keytype - ecdsakey_dict['scheme'] = scheme - ecdsakey_dict['keyid'] = keyid - ecdsakey_dict['keyval'] = key_value + + securesystemslib.exceptions.FormatError, if any of the arguments are + improperly formatted. - # 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 + + None - return ecdsakey_dict + + 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"]' + ) -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' + try: + header_start = pem.index(pem_header) + pem.index(pem_footer, header_start + len(pem_header)) - # 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) + except ValueError: + return False - ecdsakey_dict['keytype'] = keytype - ecdsakey_dict['scheme'] = scheme - ecdsakey_dict['keyid'] = keyid - ecdsakey_dict['keyval'] = key_value + return True - # 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_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'. + """ -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. + # 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) - - pem: - A string in PEM format. + # Is 'scheme' properly formatted? + formats.ECDSA_SCHEME_SCHEMA.check_match(scheme) - scheme: - The signature scheme used by the imported key. - - securesystemslib.exceptions.FormatError, if 'pem' is improperly formatted. + # 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'. - - None. + if is_pem_public(pem): + public_pem = extract_pem(pem, private_pem=False) - - A dictionary containing the ECDSA keys and other identifying information. - Conforms to 'securesystemslib.formats.ECDSAKEY_SCHEMA'. - """ + 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) - # 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() + 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/process.py b/securesystemslib/process.py index ed5a7f69..54419066 100644 --- a/securesystemslib/process.py +++ b/securesystemslib/process.py @@ -21,208 +21,219 @@ - provide a special `run_duplicate_streams` function """ +import io +import logging import os +import shlex +import subprocess # nosec import sys -import io import tempfile -import logging import time -import shlex -import subprocess -from securesystemslib import formats -from securesystemslib import settings +from securesystemslib import formats, settings DEVNULL = subprocess.DEVNULL PIPE = subprocess.PIPE log = logging.getLogger(__name__) -def _default_timeout(): - """Helper to use securesystemslib.settings.SUBPROCESS_TIMEOUT as default - argument, and still be able to modify it after the function definitions are - evaluated. """ - return settings.SUBPROCESS_TIMEOUT +def _default_timeout(): + """Helper to use securesystemslib.settings.SUBPROCESS_TIMEOUT as default + argument, and still be able to modify it after the function definitions are + evaluated.""" + return settings.SUBPROCESS_TIMEOUT def run(cmd, check=True, timeout=_default_timeout(), **kwargs): - """ - - Provide wrapper for `subprocess.run` (see - https://github.com/python/cpython/blob/3.5/Lib/subprocess.py#L352-L399) - where: - - * `timeout` has a default (securesystemslib.settings.SUBPROCESS_TIMEOUT), - * `check` is `True` by default, - * there is only one positional argument, i.e. `cmd` that can be either - a str (will be split with shlex) or a list of str and - * instead of raising a ValueError if both `input` and `stdin` are passed, - `stdin` is ignored. - - - - cmd: - The command and its arguments. (list of str, or str) - Splits a string specifying a command and its argument into a list - of substrings, if necessary. - - check: (default True) - "If check is true, and the process exits with a non-zero exit code, - a CalledProcessError exception will be raised. Attributes of that - exception hold the arguments, the exit code, and stdout and stderr - if they were captured." - - timeout: (default see securesystemslib.settings.SUBPROCESS_TIMEOUT) - "The timeout argument is passed to Popen.communicate(). If the - timeout expires, the child process will be killed and waited for. - The TimeoutExpired exception will be re-raised after the child - process has terminated." - - **kwargs: - See subprocess.run and Frequently Used Arguments to Popen - constructor for available kwargs. - https://docs.python.org/3.5/library/subprocess.html#subprocess.run - https://docs.python.org/3.5/library/subprocess.html#frequently-used-arguments - - - securesystemslib.exceptions.FormatError: - If the `cmd` is a list and does not match - securesystemslib.formats.LIST_OF_ANY_STRING_SCHEMA. - - OSError: - If the given command is not present or non-executable. - - subprocess.TimeoutExpired: - If the process does not terminate after timeout seconds. Default - is `settings.SUBPROCESS_TIMEOUT` - - - The side effects of executing the given command in this environment. - - - A subprocess.CompletedProcess instance. - - """ - # Make list of command passed as string for convenience - if isinstance(cmd, str): - cmd = shlex.split(cmd) - else: - formats.LIST_OF_ANY_STRING_SCHEMA.check_match(cmd) - - # NOTE: The CPython implementation would raise a ValueError here, we just - # don't pass on `stdin` if the user passes `input` and `stdin` - # https://github.com/python/cpython/blob/3.5/Lib/subprocess.py#L378-L381 - if kwargs.get("input") is not None and "stdin" in kwargs: - log.debug("stdin and input arguments may not both be used. " - "Ignoring passed stdin: " + str(kwargs["stdin"])) - del kwargs["stdin"] - - return subprocess.run(cmd, check=check, timeout=timeout, **kwargs) - - + """ + + Provide wrapper for `subprocess.run` (see + https://github.com/python/cpython/blob/3.5/Lib/subprocess.py#L352-L399) + where: + + * `timeout` has a default (securesystemslib.settings.SUBPROCESS_TIMEOUT), + * `check` is `True` by default, + * there is only one positional argument, i.e. `cmd` that can be either + a str (will be split with shlex) or a list of str and + * instead of raising a ValueError if both `input` and `stdin` are passed, + `stdin` is ignored. + + + + cmd: + The command and its arguments. (list of str, or str) + Splits a string specifying a command and its argument into a list + of substrings, if necessary. + + check: (default True) + "If check is true, and the process exits with a non-zero exit code, + a CalledProcessError exception will be raised. Attributes of that + exception hold the arguments, the exit code, and stdout and stderr + if they were captured." + + timeout: (default see securesystemslib.settings.SUBPROCESS_TIMEOUT) + "The timeout argument is passed to Popen.communicate(). If the + timeout expires, the child process will be killed and waited for. + The TimeoutExpired exception will be re-raised after the child + process has terminated." + + **kwargs: + See subprocess.run and Frequently Used Arguments to Popen + constructor for available kwargs. + https://docs.python.org/3.5/library/subprocess.html#subprocess.run + https://docs.python.org/3.5/library/subprocess.html#frequently-used-arguments + + + securesystemslib.exceptions.FormatError: + If the `cmd` is a list and does not match + securesystemslib.formats.LIST_OF_ANY_STRING_SCHEMA. + + OSError: + If the given command is not present or non-executable. + + subprocess.TimeoutExpired: + If the process does not terminate after timeout seconds. Default + is `settings.SUBPROCESS_TIMEOUT` + + + The side effects of executing the given command in this environment. + + + A subprocess.CompletedProcess instance. + + """ + # Make list of command passed as string for convenience + if isinstance(cmd, str): + cmd = shlex.split(cmd) + else: + formats.LIST_OF_ANY_STRING_SCHEMA.check_match(cmd) + + # NOTE: The CPython implementation would raise a ValueError here, we just + # don't pass on `stdin` if the user passes `input` and `stdin` + # https://github.com/python/cpython/blob/3.5/Lib/subprocess.py#L378-L381 + if kwargs.get("input") is not None and "stdin" in kwargs: + log.debug( # pylint: disable=logging-not-lazy + "stdin and input arguments may not both be used. " + "Ignoring passed stdin: " + str(kwargs["stdin"]) + ) + del kwargs["stdin"] + + return subprocess.run(cmd, check=check, timeout=timeout, **kwargs) # nosec def run_duplicate_streams(cmd, timeout=_default_timeout()): - """ - - Provide a function that executes a command in a subprocess and, upon - termination, returns its exit code and the contents of what was printed to - its standard streams. - - * Might behave unexpectedly with interactive commands. - * Might not duplicate output in real time, if the command buffers it (see - e.g. `print("foo")` vs. `print("foo", flush=True)` in Python 3). - - - cmd: - The command and its arguments. (list of str, or str) - Splits a string specifying a command and its argument into a list - of substrings, if necessary. - - timeout: (default see settings.SUBPROCESS_TIMEOUT) - If the timeout expires, the child process will be killed and waited - for and then subprocess.TimeoutExpired will be raised. - - - securesystemslib.exceptions.FormatError: - If the `cmd` is a list and does not match - securesystemslib.formats.LIST_OF_ANY_STRING_SCHEMA. - - OSError: - If the given command is not present or non-executable. - - subprocess.TimeoutExpired: - If the process does not terminate after timeout seconds. Default - is `settings.SUBPROCESS_TIMEOUT` - - - The side effects of executing the given command in this environment. - - - A tuple of command's exit code, standard output and standard error - contents. - - """ - if isinstance(cmd, str): - cmd = shlex.split(cmd) - else: - formats.LIST_OF_ANY_STRING_SCHEMA.check_match(cmd) - - # Use temporary files as targets for child process standard stream redirects - # They seem to work better (i.e. do not hang) than pipes, when using - # interactive commands like `vi`. - stdout_fd, stdout_name = tempfile.mkstemp() - stderr_fd, stderr_name = tempfile.mkstemp() - try: - with io.open(stdout_name, "r") as stdout_reader, \ - os.fdopen(stdout_fd, "w") as stdout_writer, \ - io.open(stderr_name, "r") as stderr_reader, \ - os.fdopen(stderr_fd, "w") as stderr_writer: - - # Store stream results in mutable dict to update it inside nested helper - _std = {"out": "", "err": ""} - def _duplicate_streams(): - """Helper to read from child process standard streams, write their - contents to parent process standard streams, and build up return values - for outer function. - """ - # Read until EOF but at most `io.DEFAULT_BUFFER_SIZE` bytes per call. - # Reading and writing in reasonably sized chunks prevents us from - # subverting a timeout, due to being busy for too long or indefinitely. - stdout_part = stdout_reader.read(io.DEFAULT_BUFFER_SIZE) - stderr_part = stderr_reader.read(io.DEFAULT_BUFFER_SIZE) - sys.stdout.write(stdout_part) - sys.stderr.write(stderr_part) - sys.stdout.flush() - sys.stderr.flush() - _std["out"] += stdout_part - _std["err"] += stderr_part - - # Start child process, writing its standard streams to temporary files - proc = subprocess.Popen(cmd, stdout=stdout_writer, - stderr=stderr_writer, universal_newlines=True) - proc_start_time = time.time() - - # Duplicate streams until the process exits (or times out) - while proc.poll() is None: - # Time out as Python's `subprocess` would do it - if (timeout is not None and - time.time() > proc_start_time + timeout): - proc.kill() - proc.wait() - raise subprocess.TimeoutExpired(cmd, timeout) - - _duplicate_streams() - - # Read/write once more to grab everything that the process wrote between - # our last read in the loop and exiting, i.e. breaking the loop. - _duplicate_streams() - - finally: - # The work is done or was interrupted, the temp files can be removed - os.remove(stdout_name) - os.remove(stderr_name) - - # Return process exit code and captured streams - return proc.poll(), _std["out"], _std["err"] + """ + + Provide a function that executes a command in a subprocess and, upon + termination, returns its exit code and the contents of what was printed to + its standard streams. + + * Might behave unexpectedly with interactive commands. + * Might not duplicate output in real time, if the command buffers it (see + e.g. `print("foo")` vs. `print("foo", flush=True)` in Python 3). + + + cmd: + The command and its arguments. (list of str, or str) + Splits a string specifying a command and its argument into a list + of substrings, if necessary. + + timeout: (default see settings.SUBPROCESS_TIMEOUT) + If the timeout expires, the child process will be killed and waited + for and then subprocess.TimeoutExpired will be raised. + + + securesystemslib.exceptions.FormatError: + If the `cmd` is a list and does not match + securesystemslib.formats.LIST_OF_ANY_STRING_SCHEMA. + + OSError: + If the given command is not present or non-executable. + + subprocess.TimeoutExpired: + If the process does not terminate after timeout seconds. Default + is `settings.SUBPROCESS_TIMEOUT` + + + The side effects of executing the given command in this environment. + + + A tuple of command's exit code, standard output and standard error + contents. + + """ + if isinstance(cmd, str): + cmd = shlex.split(cmd) + else: + formats.LIST_OF_ANY_STRING_SCHEMA.check_match(cmd) + + # Use temporary files as targets for child process standard stream redirects + # They seem to work better (i.e. do not hang) than pipes, when using + # interactive commands like `vi`. + stdout_fd, stdout_name = tempfile.mkstemp() + stderr_fd, stderr_name = tempfile.mkstemp() + try: + with io.open( # pylint: disable=unspecified-encoding + stdout_name, "r" + ) as stdout_reader, os.fdopen( # pylint: disable=unspecified-encoding + stdout_fd, "w" + ) as stdout_writer, io.open( # pylint: disable=unspecified-encoding + stderr_name, "r" + ) as stderr_reader, os.fdopen( + stderr_fd, "w" + ) as stderr_writer: + + # Store stream results in mutable dict to update it inside nested helper + _std = {"out": "", "err": ""} # pylint: disable=invalid-name + + def _duplicate_streams(): + """Helper to read from child process standard streams, write their + contents to parent process standard streams, and build up return values + for outer function. + """ + # Read until EOF but at most `io.DEFAULT_BUFFER_SIZE` bytes per call. + # Reading and writing in reasonably sized chunks prevents us from + # subverting a timeout, due to being busy for too long or indefinitely. + stdout_part = stdout_reader.read(io.DEFAULT_BUFFER_SIZE) + stderr_part = stderr_reader.read(io.DEFAULT_BUFFER_SIZE) + sys.stdout.write(stdout_part) + sys.stderr.write(stderr_part) + sys.stdout.flush() + sys.stderr.flush() + _std["out"] += stdout_part + _std["err"] += stderr_part + + # Start child process, writing its standard streams to temporary files + proc = subprocess.Popen( # pylint: disable=consider-using-with # nosec + cmd, + stdout=stdout_writer, + stderr=stderr_writer, + universal_newlines=True, + ) + proc_start_time = time.time() + + # Duplicate streams until the process exits (or times out) + while proc.poll() is None: + # Time out as Python's `subprocess` would do it + if ( + timeout is not None + and time.time() > proc_start_time + timeout + ): + proc.kill() + proc.wait() + raise subprocess.TimeoutExpired(cmd, timeout) + + _duplicate_streams() + + # Read/write once more to grab everything that the process wrote between + # our last read in the loop and exiting, i.e. breaking the loop. + _duplicate_streams() + + finally: + # The work is done or was interrupted, the temp files can be removed + os.remove(stdout_name) + os.remove(stderr_name) + + # Return process exit code and captured streams + return proc.poll(), _std["out"], _std["err"] diff --git a/securesystemslib/rsa_keys.py b/securesystemslib/rsa_keys.py index fcf10077..a567ef8c 100755 --- a/securesystemslib/rsa_keys.py +++ b/securesystemslib/rsa_keys.py @@ -50,75 +50,72 @@ Key Derivation Function 1 (PBKF1) + MD5. """ -import os import binascii import json +import os CRYPTO = True -NO_CRYPTO_MSG = 'RSA key support requires the cryptography library' +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. - from cryptography.hazmat.primitives import serialization - from cryptography.hazmat.primitives.serialization import load_pem_private_key - from cryptography.hazmat.backends import default_backend - - # Import Exception classes need to catch pyca/cryptography exceptions. - from cryptography.exceptions import ( - InvalidSignature, UnsupportedAlgorithm) - - # '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 rsa - - # 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 - from cryptography.hazmat.primitives import hmac - - # 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. - from cryptography.hazmat.primitives.asymmetric import padding - - # 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 - - # 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 - - # 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. - from cryptography.hazmat.primitives.ciphers import modes + # 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 exceptions -from securesystemslib import formats -from securesystemslib import settings -from securesystemslib import util -from securesystemslib.hash import digest_from_rsa_scheme - + 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 @@ -130,7 +127,7 @@ # 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 = '@@@@' +_ENCRYPTION_DELIMITER = "@@@@" # AES key size. Default key size = 32 bytes = AES-256. _AES_KEY_SIZE = 32 @@ -153,420 +150,445 @@ _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()) - - # 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) - - 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): - 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('The private key' - ' (in PEM format) could not be deserialized.') - - # '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('The private key was' - ' unexpectedly encrypted.') - - # '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('The private key is' - ' encrypted with an unsupported algorithm.') - - return signature, scheme - - + """ + + 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(), + ) + + # 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, + ) + + 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. + """ + + 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: - 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)) - - - + 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. + """ + + 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 + 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. + >>> 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 - passphrase: - The passphrase, or password, to encrypt the private part of the RSA - key. - - - securesystemslib.exceptions.FormatError, if the arguments are improperly - formatted. + + private_key: + The private key string in PEM format. - securesystemslib.exceptions.CryptoError, if the passed RSA key cannot be - deserialized by pyca cryptography. + passphrase: + The passphrase, or password, to encrypt the private part of the RSA + key. - ValueError, if 'private_key' is unset. + + securesystemslib.exceptions.FormatError, if the arguments are improperly + formatted. - securesystemslib.exceptions.UnsupportedLibraryError, if the cryptography - module is not available. + securesystemslib.exceptions.CryptoError, if the passed RSA key cannot be + deserialized by pyca cryptography. + ValueError, if 'private_key' is unset. - - A string in PEM format (TraditionalOpenSSL), where the private RSA key is - encrypted. Conforms to 'securesystemslib.formats.PEMRSA_SCHEMA'. - """ + securesystemslib.exceptions.UnsupportedLibraryError, if the cryptography + module is not available. - 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) + + A string in PEM format (TraditionalOpenSSL), where the private RSA key is + encrypted. Conforms to 'securesystemslib.formats.PEMRSA_SCHEMA'. + """ - # 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('The private key' - ' (in PEM format) could not be deserialized.') - - 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'))) - - return encrypted_pem.decode() + 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") + ), + ) + + 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 @@ -634,66 +656,70 @@ def create_rsa_public_and_private_from_pem(pem, passphrase=None): 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()) - - # 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) - - return public_pem.decode(), private_pem.decode() + 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(), + ) + + # 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, + ) + + 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() @@ -757,45 +783,50 @@ def encrypt_key(key_object, password): An encrypted string in 'securesystemslib.formats.ENCRYPTEDKEY_SCHEMA' format. """ - if not CRYPTO: # pragma: no cover - raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) + 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) + # 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) + # 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.') + # 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 + # 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 @@ -861,206 +892,217 @@ def decrypt_key(encrypted_key, password): 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) + if not CRYPTO: # pragma: no cover + raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) - # 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) + # 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) - # 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 + # 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 - - - + """ + 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() - - - + """ + 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('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. - junk_old_salt, junk_old_iterations, 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 - - - - + """ + 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 + ) -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() + 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. + ( + junk_old_salt, # pylint: disable=unused-variable + junk_old_iterations, # pylint: disable=unused-variable + 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/securesystemslib/schema.py b/securesystemslib/schema.py index 30a27f51..d1d1e612 100755 --- a/securesystemslib/schema.py +++ b/securesystemslib/schema.py @@ -52,957 +52,945 @@ class Schema: - """ - - A schema matches a set of possible Python objects, of types - that are encodable in JSON. 'Schema' is the base class for - the other classes defined in this module. All derived classes - should implement check_match(). - """ - - def matches(self, object): - """ - - Return True if 'object' matches this schema, False if it doesn't. - If the caller wishes to signal an error on a failed match, check_match() - should be called, which will raise a 'exceptions.FormatError' exception. - """ - - try: - self.check_match(object) - except exceptions.FormatError: - return False - else: - return True - - - def check_match(self, object): """ - Abstract method. Classes that inherit from 'Schema' must - implement check_match(). If 'object' matches the schema, check_match() - should simply return. If 'object' does not match the schema, - 'exceptions.FormatError' should be raised. + A schema matches a set of possible Python objects, of types + that are encodable in JSON. 'Schema' is the base class for + the other classes defined in this module. All derived classes + should implement check_match(). """ - raise NotImplementedError() + def matches(self, object): # pylint: disable=redefined-builtin + """ + + Return True if 'object' matches this schema, False if it doesn't. + If the caller wishes to signal an error on a failed match, check_match() + should be called, which will raise a 'exceptions.FormatError' exception. + """ + try: + self.check_match(object) + except exceptions.FormatError: + return False + else: + return True + def check_match(self, object): # pylint: disable=redefined-builtin + """ + + Abstract method. Classes that inherit from 'Schema' must + implement check_match(). If 'object' matches the schema, check_match() + should simply return. If 'object' does not match the schema, + 'exceptions.FormatError' should be raised. + """ + raise NotImplementedError() class Any(Schema): - """ - - Matches any single object. Whereas other schemas explicitly state - the required type of its argument, Any() does not. It simply does a - 'pass' when 'check_match()' is called and at the point where the schema - is instantiated. - - Supported methods include: - matches(): returns a Boolean result. - check_match(): passed - - - - >>> schema = Any() - >>> schema.matches('A String') - True - >>> schema.matches([1, 'list']) - True - """ - - def __init__(self): - pass - - - def check_match(self, object): - pass - + """ + + Matches any single object. Whereas other schemas explicitly state + the required type of its argument, Any() does not. It simply does a + 'pass' when 'check_match()' is called and at the point where the schema + is instantiated. + + Supported methods include: + matches(): returns a Boolean result. + check_match(): passed + + + + >>> schema = Any() + >>> schema.matches('A String') + True + >>> schema.matches([1, 'list']) + True + """ + def __init__(self): + pass + def check_match(self, object): # pylint: disable=redefined-builtin + pass class String(Schema): - """ - - Matches a particular string. The argument object must be a string and be - equal to a specific string value. At instantiation, the string is set and - any future comparisons are checked against this internal string value. - - Supported methods include: - matches(): returns a Boolean result. - check_match(): raises 'exceptions.FormatError' on a mismatch. - - - - >>> schema = String('Hi') - >>> schema.matches('Hi') - True - >>> schema.matches('Not hi') - False - """ - - def __init__(self, string): - if not isinstance(string, str): - raise exceptions.FormatError('Expected a string but' - ' got ' + repr(string)) + """ + + Matches a particular string. The argument object must be a string and be + equal to a specific string value. At instantiation, the string is set and + any future comparisons are checked against this internal string value. - self._string = string + Supported methods include: + matches(): returns a Boolean result. + check_match(): raises 'exceptions.FormatError' on a mismatch. + - def check_match(self, object): - if self._string != object: - raise exceptions.FormatError( - 'Expected ' + repr(self._string) + ' got ' + repr(object)) + >>> schema = String('Hi') + >>> schema.matches('Hi') + True + >>> schema.matches('Not hi') + False + """ + def __init__(self, string): + if not isinstance(string, str): + raise exceptions.FormatError( + "Expected a string but" " got " + repr(string) + ) + self._string = string + def check_match(self, object): # pylint: disable=redefined-builtin + if self._string != object: + raise exceptions.FormatError( + "Expected " + repr(self._string) + " got " + repr(object) + ) class AnyString(Schema): - """ - - Matches any string, but not a non-string object. This schema - can be viewed as the Any() schema applied to Strings, but an - additional check is performed to ensure only strings are considered. - - Supported methods include: - matches(): returns a Boolean result. - check_match(): raises 'exceptions.FormatError' on a mismatch. - - - - >>> schema = AnyString() - >>> schema.matches('') - True - >>> schema.matches('a string') - True - >>> schema.matches(['a']) - False - >>> schema.matches(3) - False - >>> schema.matches(u'a unicode string') - True - >>> schema.matches({}) - False - """ - - def __init__(self): - pass - - - def check_match(self, object): - if not isinstance(object, str): - raise exceptions.FormatError('Expected a string' - ' but got ' + repr(object)) - + """ + + Matches any string, but not a non-string object. This schema + can be viewed as the Any() schema applied to Strings, but an + additional check is performed to ensure only strings are considered. + + Supported methods include: + matches(): returns a Boolean result. + check_match(): raises 'exceptions.FormatError' on a mismatch. + + + + >>> schema = AnyString() + >>> schema.matches('') + True + >>> schema.matches('a string') + True + >>> schema.matches(['a']) + False + >>> schema.matches(3) + False + >>> schema.matches(u'a unicode string') + True + >>> schema.matches({}) + False + """ + def __init__(self): + pass + def check_match(self, object): # pylint: disable=redefined-builtin + if not isinstance(object, str): + raise exceptions.FormatError( + "Expected a string" " but got " + repr(object) + ) class AnyNonemptyString(AnyString): - """ - - Matches any string with one or more characters. - This schema can be viewed as the Any() schema applied to Strings, but an - additional check is performed to ensure only strings are considered and - that said strings have at least one character. - - Supported methods include: - matches(): returns a Boolean result. - check_match(): raises 'exceptions.FormatError' on a mismatch. - - - - >>> schema = AnyNonemptyString() - >>> schema.matches('') - False - >>> schema.matches('a string') - True - >>> schema.matches(['a']) - False - >>> schema.matches(3) - False - >>> schema.matches(u'a unicode string') - True - >>> schema.matches({}) - False - """ - - def check_match(self, object): - AnyString.check_match(self, object) - - if object == "": - raise exceptions.FormatError('Expected a string' - ' with at least one character but got ' + repr(object)) - + """ + + Matches any string with one or more characters. + This schema can be viewed as the Any() schema applied to Strings, but an + additional check is performed to ensure only strings are considered and + that said strings have at least one character. + + Supported methods include: + matches(): returns a Boolean result. + check_match(): raises 'exceptions.FormatError' on a mismatch. + + + + >>> schema = AnyNonemptyString() + >>> schema.matches('') + False + >>> schema.matches('a string') + True + >>> schema.matches(['a']) + False + >>> schema.matches(3) + False + >>> schema.matches(u'a unicode string') + True + >>> schema.matches({}) + False + """ + def check_match(self, object): # pylint: disable=redefined-builtin + AnyString.check_match(self, object) + if object == "": + raise exceptions.FormatError( + "Expected a string" + " with at least one character but got " + repr(object) + ) class AnyBytes(Schema): - """ - - Matches any byte string, but not a non-byte object. This schema can be - viewed as the Any() schema applied to byte strings, but an additional check - is performed to ensure only strings are considered. Supported methods - include: - matches(): returns a Boolean result. - check_match(): raises 'exceptions.FormatError' on a mismatch. - - - - >>> schema = AnyBytes() - >>> schema.matches(b'') - True - >>> schema.matches(b'a string') - True - >>> schema.matches(['a']) - False - >>> schema.matches(3) - False - >>> schema.matches({}) - False - """ - - def __init__(self): - pass - - - def check_match(self, object): - if not isinstance(object, bytes): - raise exceptions.FormatError('Expected a byte string' - ' but got ' + repr(object)) - + """ + + Matches any byte string, but not a non-byte object. This schema can be + viewed as the Any() schema applied to byte strings, but an additional check + is performed to ensure only strings are considered. Supported methods + include: + matches(): returns a Boolean result. + check_match(): raises 'exceptions.FormatError' on a mismatch. + + + + >>> schema = AnyBytes() + >>> schema.matches(b'') + True + >>> schema.matches(b'a string') + True + >>> schema.matches(['a']) + False + >>> schema.matches(3) + False + >>> schema.matches({}) + False + """ + def __init__(self): + pass + def check_match(self, object): # pylint: disable=redefined-builtin + if not isinstance(object, bytes): + raise exceptions.FormatError( + "Expected a byte string" " but got " + repr(object) + ) class LengthString(Schema): - """ - - Matches any string of a specified length. The argument object must be a - string. At instantiation, the string length is set and any future - comparisons are checked against this internal string value length. - - Supported methods include: - matches(): returns a Boolean result. - check_match(): raises 'exceptions.FormatError' on a mismatch. - - - - >>> schema = LengthString(5) - >>> schema.matches('Hello') - True - >>> schema.matches('Hi') - False - """ - - def __init__(self, length): - if isinstance(length, bool) or not isinstance(length, int): - # We need to check for bool as a special case, since bool - # is for historical reasons a subtype of int. - raise exceptions.FormatError( - 'Got ' + repr(length) + ' instead of an integer.') + """ + + Matches any string of a specified length. The argument object must be a + string. At instantiation, the string length is set and any future + comparisons are checked against this internal string value length. - self._string_length = length + Supported methods include: + matches(): returns a Boolean result. + check_match(): raises 'exceptions.FormatError' on a mismatch. + - def check_match(self, object): - if not isinstance(object, str): - raise exceptions.FormatError('Expected a string but' - ' got ' + repr(object)) + >>> schema = LengthString(5) + >>> schema.matches('Hello') + True + >>> schema.matches('Hi') + False + """ - if len(object) != self._string_length: - raise exceptions.FormatError('Expected a string of' - ' length ' + repr(self._string_length)) + def __init__(self, length): + if isinstance(length, bool) or not isinstance(length, int): + # We need to check for bool as a special case, since bool + # is for historical reasons a subtype of int. + raise exceptions.FormatError( + "Got " + repr(length) + " instead of an integer." + ) + self._string_length = length + def check_match(self, object): # pylint: disable=redefined-builtin + if not isinstance(object, str): + raise exceptions.FormatError( + "Expected a string but" " got " + repr(object) + ) + if len(object) != self._string_length: + raise exceptions.FormatError( + "Expected a string of" " length " + repr(self._string_length) + ) class LengthBytes(Schema): - """ - - Matches any Bytes of a specified length. The argument object must be either - a str() in Python 2, or bytes() in Python 3. At instantiation, the bytes - length is set and any future comparisons are checked against this internal - bytes value length. - - Supported methods include: - matches(): returns a Boolean result. - check_match(): raises 'exceptions.FormatError' on a mismatch. - - - - >>> schema = LengthBytes(5) - >>> schema.matches(b'Hello') - True - >>> schema.matches(b'Hi') - False - """ - - def __init__(self, length): - if isinstance(length, bool) or not isinstance(length, int): - # We need to check for bool as a special case, since bool - # is for historical reasons a subtype of int. - raise exceptions.FormatError( - 'Got ' + repr(length) + ' instead of an integer.') - - self._bytes_length = length - - - def check_match(self, object): - if not isinstance(object, bytes): - raise exceptions.FormatError('Expected a byte but' - ' got ' + repr(object)) + """ + + Matches any Bytes of a specified length. The argument object must be either + a str() in Python 2, or bytes() in Python 3. At instantiation, the bytes + length is set and any future comparisons are checked against this internal + bytes value length. + + Supported methods include: + matches(): returns a Boolean result. + check_match(): raises 'exceptions.FormatError' on a mismatch. + + + + >>> schema = LengthBytes(5) + >>> schema.matches(b'Hello') + True + >>> schema.matches(b'Hi') + False + """ - if len(object) != self._bytes_length: - raise exceptions.FormatError('Expected a byte of' - ' length ' + repr(self._bytes_length)) + def __init__(self, length): + if isinstance(length, bool) or not isinstance(length, int): + # We need to check for bool as a special case, since bool + # is for historical reasons a subtype of int. + raise exceptions.FormatError( + "Got " + repr(length) + " instead of an integer." + ) + self._bytes_length = length + def check_match(self, object): # pylint: disable=redefined-builtin + if not isinstance(object, bytes): + raise exceptions.FormatError( + "Expected a byte but" " got " + repr(object) + ) + if len(object) != self._bytes_length: + raise exceptions.FormatError( + "Expected a byte of" " length " + repr(self._bytes_length) + ) class OneOf(Schema): - """ - - Matches an object that matches any one of several schemas. OneOf() returns - a result as soon as one of its recognized sub-schemas is encountered in the - object argument. When OneOf() is instantiated, its supported sub-schemas - are specified by a sequence type (e.g., a list, tuple, etc.). A mismatch - is returned after checking all sub-schemas and not finding a supported - type. - - Supported methods include: - matches(): returns a Boolean result. - check_match(): raises 'exceptions.FormatError' on a mismatch. - - - >>> schema = OneOf([ListOf(Integer()), String('Hello'), String('bye')]) - >>> schema.matches(3) - False - >>> schema.matches('bye') - True - >>> schema.matches([]) - True - >>> schema.matches([1,2]) - True - >>> schema.matches(['Hi']) - False - """ - - def __init__(self, alternatives): - # Ensure each item of the list contains the expected object type. - if not isinstance(alternatives, list): - raise exceptions.FormatError('Expected a list but' - ' got ' + repr(alternatives)) - - for alternative in alternatives: - if not isinstance(alternative, Schema): - raise exceptions.FormatError('List contains an' - ' invalid item ' + repr(alternative)) - - self._alternatives = alternatives - - - def check_match(self, object): - # Simply return as soon as we find a match. - # Raise 'exceptions.FormatError' if no matches are found. - for alternative in self._alternatives: - if alternative.matches(object): - return - raise exceptions.FormatError('Object did not match a' - ' recognized alternative.') - - + """ + + Matches an object that matches any one of several schemas. OneOf() returns + a result as soon as one of its recognized sub-schemas is encountered in the + object argument. When OneOf() is instantiated, its supported sub-schemas + are specified by a sequence type (e.g., a list, tuple, etc.). A mismatch + is returned after checking all sub-schemas and not finding a supported + type. + + Supported methods include: + matches(): returns a Boolean result. + check_match(): raises 'exceptions.FormatError' on a mismatch. + + + >>> schema = OneOf([ListOf(Integer()), String('Hello'), String('bye')]) + >>> schema.matches(3) + False + >>> schema.matches('bye') + True + >>> schema.matches([]) + True + >>> schema.matches([1,2]) + True + >>> schema.matches(['Hi']) + False + """ + def __init__(self, alternatives): + # Ensure each item of the list contains the expected object type. + if not isinstance(alternatives, list): + raise exceptions.FormatError( + "Expected a list but" " got " + repr(alternatives) + ) + + for alternative in alternatives: + if not isinstance(alternative, Schema): + raise exceptions.FormatError( + "List contains an" " invalid item " + repr(alternative) + ) + + self._alternatives = alternatives + + def check_match(self, object): # pylint: disable=redefined-builtin + # Simply return as soon as we find a match. + # Raise 'exceptions.FormatError' if no matches are found. + for alternative in self._alternatives: + if alternative.matches(object): + return + raise exceptions.FormatError( + "Object did not match a" + " recognized alternative." # pylint: disable=implicit-str-concat + ) class AllOf(Schema): - """ - - Matches the intersection of a list of schemas. The object being tested - must match all of the required sub-schemas. Unlike OneOf(), which can - return a result as soon as a match is found in one of its supported - sub-schemas, AllOf() must verify each sub-schema before returning a result. - - Supported methods include: - matches(): returns a Boolean result. - check_match(): raises 'exceptions.FormatError' on a mismatch. - - - >>> schema = AllOf([Any(), AnyString(), String('a')]) - >>> schema.matches('b') - False - >>> schema.matches('a') - True - """ - - def __init__(self, required_schemas): - # Ensure each item of the list contains the expected object type. - if not isinstance(required_schemas, list): - raise exceptions.FormatError('Expected a list but' - ' got' + repr(required_schemas)) - - for schema in required_schemas: - if not isinstance(schema, Schema): - raise exceptions.FormatError('List contains an' - ' invalid item ' + repr(schema)) - - self._required_schemas = required_schemas[:] - + """ + + Matches the intersection of a list of schemas. The object being tested + must match all of the required sub-schemas. Unlike OneOf(), which can + return a result as soon as a match is found in one of its supported + sub-schemas, AllOf() must verify each sub-schema before returning a result. + + Supported methods include: + matches(): returns a Boolean result. + check_match(): raises 'exceptions.FormatError' on a mismatch. + + + >>> schema = AllOf([Any(), AnyString(), String('a')]) + >>> schema.matches('b') + False + >>> schema.matches('a') + True + """ - def check_match(self, object): - for required_schema in self._required_schemas: - required_schema.check_match(object) + def __init__(self, required_schemas): + # Ensure each item of the list contains the expected object type. + if not isinstance(required_schemas, list): + raise exceptions.FormatError( + "Expected a list but" " got" + repr(required_schemas) + ) + for schema in required_schemas: + if not isinstance(schema, Schema): + raise exceptions.FormatError( + "List contains an" " invalid item " + repr(schema) + ) + self._required_schemas = required_schemas[:] + def check_match(self, object): # pylint: disable=redefined-builtin + for required_schema in self._required_schemas: + required_schema.check_match(object) class Boolean(Schema): - """ - - Matches a boolean. The object argument must be one of True or False. All - other types are flagged as mismatches. - - Supported methods include: - matches(): returns a Boolean result. - check_match(): raises 'exceptions.FormatError' on a mismatch. - - - >>> schema = Boolean() - >>> schema.matches(True) and schema.matches(False) - True - >>> schema.matches(11) - False - """ - - def __init__(self): - pass - - - def check_match(self, object): - if not isinstance(object, bool): - raise exceptions.FormatError( - 'Got ' + repr(object) + ' instead of a boolean.') - + """ + + Matches a boolean. The object argument must be one of True or False. All + other types are flagged as mismatches. + + Supported methods include: + matches(): returns a Boolean result. + check_match(): raises 'exceptions.FormatError' on a mismatch. + + + >>> schema = Boolean() + >>> schema.matches(True) and schema.matches(False) + True + >>> schema.matches(11) + False + """ + def __init__(self): + pass + def check_match(self, object): # pylint: disable=redefined-builtin + if not isinstance(object, bool): + raise exceptions.FormatError( + "Got " + repr(object) + " instead of a boolean." + ) class ListOf(Schema): - """ - - Matches a homogeneous list of some sub-schema. That is, all the sub-schema - must be of the same type. The object argument must be a sequence type - (e.g., a list, tuple, etc.). When ListOf() is instantiated, a minimum and - maximum count can be specified for the homogeneous sub-schema list. If - min_count is set to 'n', the object argument sequence must contain 'n' - items. See ListOf()'s __init__ method for the expected arguments. - - Supported methods include: - matches(): returns a Boolean result. - check_match(): raises 'exceptions.FormatError' on a mismatch. - - - >>> schema = ListOf(RegularExpression('(?:..)*')) - >>> schema.matches('hi') - False - >>> schema.matches([]) - True - >>> schema.matches({}) - False - >>> schema.matches(['Hi', 'this', 'list', 'is', 'full', 'of', 'even', 'strs']) - True - >>> schema.matches(['This', 'one', 'is not']) - False - >>> schema = ListOf(Integer(), min_count=3, max_count=10) - >>> schema.matches([3]*2) - False - >>> schema.matches([3]*3) - True - >>> schema.matches([3]*10) - True - >>> schema.matches([3]*11) - False - """ - - def __init__(self, schema, min_count=0, max_count=sys.maxsize, list_name='list'): """ - Create a new ListOf schema. - - - schema: The pattern to match. - min_count: The minimum number of sub-schema in 'schema'. - max_count: The maximum number of sub-schema in 'schema'. - list_name: A string identifier for the ListOf object. + Matches a homogeneous list of some sub-schema. That is, all the sub-schema + must be of the same type. The object argument must be a sequence type + (e.g., a list, tuple, etc.). When ListOf() is instantiated, a minimum and + maximum count can be specified for the homogeneous sub-schema list. If + min_count is set to 'n', the object argument sequence must contain 'n' + items. See ListOf()'s __init__ method for the expected arguments. + + Supported methods include: + matches(): returns a Boolean result. + check_match(): raises 'exceptions.FormatError' on a mismatch. + + + >>> schema = ListOf(RegularExpression('(?:..)*')) + >>> schema.matches('hi') + False + >>> schema.matches([]) + True + >>> schema.matches({}) + False + >>> schema.matches(['Hi', 'this', 'list', 'is', 'full', 'of', 'even', 'strs']) + True + >>> schema.matches(['This', 'one', 'is not']) + False + >>> schema = ListOf(Integer(), min_count=3, max_count=10) + >>> schema.matches([3]*2) + False + >>> schema.matches([3]*3) + True + >>> schema.matches([3]*10) + True + >>> schema.matches([3]*11) + False """ - if not isinstance(schema, Schema): - message = 'Expected Schema type but got '+repr(schema) - raise exceptions.FormatError(message) - - self._schema = schema - self._min_count = min_count - self._max_count = max_count - self._list_name = list_name - - - def check_match(self, object): - if not isinstance(object, (list, tuple)): - raise exceptions.FormatError( - 'Expected object of type {} but got type {}'.format( - self._list_name, type(object).__name__)) - - - # Check if all the items in the 'object' list - # match 'schema'. - for item in object: - try: - self._schema.check_match(item) - - except exceptions.FormatError as e: - raise exceptions.FormatError( - str(e) + ' in ' + repr(self._list_name)) - - # Raise exception if the number of items in the list is - # not within the expected range. - if not (self._min_count <= len(object) <= self._max_count): - raise exceptions.FormatError( - 'Length of ' + repr(self._list_name) + ' out of range.') - - - + def __init__( + self, schema, min_count=0, max_count=sys.maxsize, list_name="list" + ): + """ + + Create a new ListOf schema. + + + schema: The pattern to match. + min_count: The minimum number of sub-schema in 'schema'. + max_count: The maximum number of sub-schema in 'schema'. + list_name: A string identifier for the ListOf object. + """ + + if not isinstance(schema, Schema): + message = "Expected Schema type but got " + repr(schema) + raise exceptions.FormatError(message) + + self._schema = schema + self._min_count = min_count + self._max_count = max_count + self._list_name = list_name + + def check_match(self, object): # pylint: disable=redefined-builtin + if not isinstance(object, (list, tuple)): + raise exceptions.FormatError( + "Expected object of type {} but got type {}".format( # pylint: disable=consider-using-f-string + self._list_name, type(object).__name__ + ) + ) + + # Check if all the items in the 'object' list + # match 'schema'. + for item in object: + try: + self._schema.check_match(item) + + except exceptions.FormatError as e: + raise exceptions.FormatError( + str(e) + " in " + repr(self._list_name) + ) + + # Raise exception if the number of items in the list is + # not within the expected range. + if not (self._min_count <= len(object) <= self._max_count): + raise exceptions.FormatError( + "Length of " + repr(self._list_name) + " out of range." + ) class Integer(Schema): - """ - - Matches an integer. A range can be specified. For example, only integers - between 8 and 42 can be set as a requirement. The object argument is also - checked against a Boolean type, since booleans have historically been - considered a sub-type of integer. - - Supported methods include: - matches(): returns a Boolean result. - check_match(): raises 'exceptions.FormatError' on a mismatch. - - - >>> schema = Integer() - >>> schema.matches(99) - True - >>> schema.matches(False) - False - >>> schema.matches('a string') - False - >>> Integer(lo=10, hi=30).matches(25) - True - >>> Integer(lo=10, hi=30).matches(5) - False - """ - - def __init__(self, lo = -2147483648, hi = 2147483647): """ - Create a new Integer schema. - - - lo: The minimum value the int object argument can be. - hi: The maximum value the int object argument can be. + Matches an integer. A range can be specified. For example, only integers + between 8 and 42 can be set as a requirement. The object argument is also + checked against a Boolean type, since booleans have historically been + considered a sub-type of integer. + + Supported methods include: + matches(): returns a Boolean result. + check_match(): raises 'exceptions.FormatError' on a mismatch. + + + >>> schema = Integer() + >>> schema.matches(99) + True + >>> schema.matches(False) + False + >>> schema.matches('a string') + False + >>> Integer(lo=10, hi=30).matches(25) + True + >>> Integer(lo=10, hi=30).matches(5) + False """ - self._lo = lo - self._hi = hi - - - def check_match(self, object): - if isinstance(object, bool) or not isinstance(object, int): - # We need to check for bool as a special case, since bool - # is for historical reasons a subtype of int. - raise exceptions.FormatError( - 'Got ' + repr(object) + ' instead of an integer.') + def __init__(self, lo=-2147483648, hi=2147483647): + """ + + Create a new Integer schema. - elif not (self._lo <= object <= self._hi): - int_range = '[' + repr(self._lo) + ', ' + repr(self._hi) + '].' - raise exceptions.FormatError( - repr(object) + ' not in range ' + int_range) + + lo: The minimum value the int object argument can be. + hi: The maximum value the int object argument can be. + """ + self._lo = lo + self._hi = hi + def check_match(self, object): # pylint: disable=redefined-builtin + if isinstance( # pylint: disable=no-else-raise + object, bool + ) or not isinstance(object, int): + # We need to check for bool as a special case, since bool + # is for historical reasons a subtype of int. + raise exceptions.FormatError( + "Got " + repr(object) + " instead of an integer." + ) + elif not (self._lo <= object <= self._hi): + int_range = "[" + repr(self._lo) + ", " + repr(self._hi) + "]." + raise exceptions.FormatError( + repr(object) + " not in range " + int_range + ) class DictOf(Schema): - """ - - Matches a mapping from items matching a particular key-schema to items - matching a value-schema (i.e., the object being checked must be a dict). - Note that in JSON, keys must be strings. In the example below, the keys of - the dict must be one of the letters contained in 'aeiou' and the value must - be a structure containing any two strings. - - Supported methods include: - matches(): returns a Boolean result. - check_match(): raises 'exceptions.FormatError' on a mismatch. - - - >>> schema = DictOf(RegularExpression(r'[aeiou]+'), Struct([AnyString(), AnyString()])) - >>> schema.matches('') - False - >>> schema.matches({}) - True - >>> schema.matches({'a': ['x', 'y'], 'e' : ['', '']}) - True - >>> schema.matches({'a': ['x', 3], 'e' : ['', '']}) - False - >>> schema.matches({'a': ['x', 'y'], 'e' : ['', ''], 'd' : ['a', 'b']}) - False - """ - - def __init__(self, key_schema, value_schema): """ - Create a new DictOf schema. - - - key_schema: The dictionary's key. - value_schema: The dictionary's value. + Matches a mapping from items matching a particular key-schema to items + matching a value-schema (i.e., the object being checked must be a dict). + Note that in JSON, keys must be strings. In the example below, the keys of + the dict must be one of the letters contained in 'aeiou' and the value must + be a structure containing any two strings. + + Supported methods include: + matches(): returns a Boolean result. + check_match(): raises 'exceptions.FormatError' on a mismatch. + + + >>> schema = DictOf(RegularExpression(r'[aeiou]+'), Struct([AnyString(), AnyString()])) + >>> schema.matches('') + False + >>> schema.matches({}) + True + >>> schema.matches({'a': ['x', 'y'], 'e' : ['', '']}) + True + >>> schema.matches({'a': ['x', 3], 'e' : ['', '']}) + False + >>> schema.matches({'a': ['x', 'y'], 'e' : ['', ''], 'd' : ['a', 'b']}) + False """ - if not isinstance(key_schema, Schema): - raise exceptions.FormatError('Expected Schema but' - ' got ' + repr(key_schema)) - - if not isinstance(value_schema, Schema): - raise exceptions.FormatError('Expected Schema but' - ' got ' + repr(value_schema)) - - self._key_schema = key_schema - self._value_schema = value_schema + def __init__(self, key_schema, value_schema): + """ + + Create a new DictOf schema. + + key_schema: The dictionary's key. + value_schema: The dictionary's value. + """ - def check_match(self, object): - if not isinstance(object, dict): - raise exceptions.FormatError('Expected a dict but' - ' got ' + repr(object)) + if not isinstance(key_schema, Schema): + raise exceptions.FormatError( + "Expected Schema but" " got " + repr(key_schema) + ) - for key, value in object.items(): - self._key_schema.check_match(key) - self._value_schema.check_match(value) + if not isinstance(value_schema, Schema): + raise exceptions.FormatError( + "Expected Schema but" " got " + repr(value_schema) + ) + self._key_schema = key_schema + self._value_schema = value_schema + def check_match(self, object): # pylint: disable=redefined-builtin + if not isinstance(object, dict): + raise exceptions.FormatError( + "Expected a dict but" " got " + repr(object) + ) + for key, value in object.items(): + self._key_schema.check_match(key) + self._value_schema.check_match(value) class Optional(Schema): - """ - - Provide a way for the Object() schema to accept optional dictionary keys. - The Object() schema outlines how a dictionary should look, such as the - names for dict keys and the object type of the dict values. Optional()'s - intended use is as a sub-schema to Object(). Object() flags an object as a - mismatch if a required key is not encountered, however, dictionary keys - labeled Optional() are not required to appear in the object's list of - required keys. If an Optional() key IS found, Optional()'s sub-schemas are - then verified. - - Supported methods include: - matches(): returns a Boolean result. - check_match(): raises 'exceptions.FormatError' on a mismatch. - - - >>> schema = Object(k1=String('X'), k2=Optional(String('Y'))) - >>> schema.matches({'k1': 'X', 'k2': 'Y'}) - True - >>> schema.matches({'k1': 'X', 'k2': 'Z'}) - False - >>> schema.matches({'k1': 'X'}) - True - """ - - def __init__(self, schema): - if not isinstance(schema, Schema): - raise exceptions.FormatError('Expected Schema, but' - ' got ' + repr(schema)) - self._schema = schema - - - def check_match(self, object): - self._schema.check_match(object) - + """ + + Provide a way for the Object() schema to accept optional dictionary keys. + The Object() schema outlines how a dictionary should look, such as the + names for dict keys and the object type of the dict values. Optional()'s + intended use is as a sub-schema to Object(). Object() flags an object as a + mismatch if a required key is not encountered, however, dictionary keys + labeled Optional() are not required to appear in the object's list of + required keys. If an Optional() key IS found, Optional()'s sub-schemas are + then verified. + + Supported methods include: + matches(): returns a Boolean result. + check_match(): raises 'exceptions.FormatError' on a mismatch. + + + >>> schema = Object(k1=String('X'), k2=Optional(String('Y'))) + >>> schema.matches({'k1': 'X', 'k2': 'Y'}) + True + >>> schema.matches({'k1': 'X', 'k2': 'Z'}) + False + >>> schema.matches({'k1': 'X'}) + True + """ + def __init__(self, schema): + if not isinstance(schema, Schema): + raise exceptions.FormatError( + "Expected Schema, but" " got " + repr(schema) + ) + self._schema = schema + def check_match(self, object): # pylint: disable=redefined-builtin + self._schema.check_match(object) class Object(Schema): - """ - - Matches a dict from specified keys to key-specific types. Unrecognized - keys are allowed. The Object() schema outlines how a dictionary should - look, such as the names for dict keys and the object type of the dict - values. See schema.Optional() to learn how Object() incorporates optional - sub-schemas. - - Supported methods include: - matches(): returns a Boolean result. - check_match(): raises 'exceptions.FormatError' on a mismatch. - - - >>> schema = Object(a=AnyString(), bc=Struct([Integer(), Integer()])) - >>> schema.matches({'a':'ZYYY', 'bc':[5,9]}) - True - >>> schema.matches({'a':'ZYYY', 'bc':[5,9], 'xx':5}) - True - >>> schema.matches({'a':'ZYYY', 'bc':[5,9,3]}) - False - >>> schema.matches({'a':'ZYYY'}) - False - """ - - def __init__(self, object_name='object', **required): """ - Create a new Object schema. - - - object_name: A string identifier for the object argument. - - A variable number of keyword arguments is accepted. + Matches a dict from specified keys to key-specific types. Unrecognized + keys are allowed. The Object() schema outlines how a dictionary should + look, such as the names for dict keys and the object type of the dict + values. See schema.Optional() to learn how Object() incorporates optional + sub-schemas. + + Supported methods include: + matches(): returns a Boolean result. + check_match(): raises 'exceptions.FormatError' on a mismatch. + + + >>> schema = Object(a=AnyString(), bc=Struct([Integer(), Integer()])) + >>> schema.matches({'a':'ZYYY', 'bc':[5,9]}) + True + >>> schema.matches({'a':'ZYYY', 'bc':[5,9], 'xx':5}) + True + >>> schema.matches({'a':'ZYYY', 'bc':[5,9,3]}) + False + >>> schema.matches({'a':'ZYYY'}) + False """ - # Ensure valid arguments. - for key, schema in required.items(): - if not isinstance(schema, Schema): - raise exceptions.FormatError('Expected Schema but' - ' got ' + repr(schema)) - - self._object_name = object_name - self._required = list(required.items()) - - - def check_match(self, object): - if not isinstance(object, dict): - raise exceptions.FormatError( - 'Wanted a ' + repr(self._object_name) + '.') - - # (key, schema) = (a, AnyString()) = (a=AnyString()) - for key, schema in self._required: - # Check if 'object' has all the required dict keys. If not one of the - # required keys, check if it is an Optional(). - try: - item = object[key] - - except KeyError: - # If not an Optional schema, raise an exception. - if not isinstance(schema, Optional): - raise exceptions.FormatError( - 'Missing key ' + repr(key) + ' in ' + repr(self._object_name)) - - # Check that 'object's schema matches Object()'s schema for this - # particular 'key'. - else: - try: - schema.check_match(item) - - except exceptions.FormatError as e: - raise exceptions.FormatError( - str(e) + ' in ' + self._object_name + '.' + key) - - - + def __init__(self, object_name="object", **required): + """ + + Create a new Object schema. + + + object_name: A string identifier for the object argument. + + A variable number of keyword arguments is accepted. + """ + + # Ensure valid arguments. + for key, schema in required.items(): # pylint: disable=unused-variable + if not isinstance(schema, Schema): + raise exceptions.FormatError( + "Expected Schema but" " got " + repr(schema) + ) + + self._object_name = object_name + self._required = list(required.items()) + + def check_match(self, object): # pylint: disable=redefined-builtin + if not isinstance(object, dict): + raise exceptions.FormatError( + "Wanted a " + repr(self._object_name) + "." + ) + + # (key, schema) = (a, AnyString()) = (a=AnyString()) + for key, schema in self._required: + # Check if 'object' has all the required dict keys. If not one of the + # required keys, check if it is an Optional(). + try: + item = object[key] + + except KeyError: + # If not an Optional schema, raise an exception. + if not isinstance(schema, Optional): + raise exceptions.FormatError( # pylint: disable=raise-missing-from + "Missing key " + + repr(key) + + " in " + + repr(self._object_name) + ) + + # Check that 'object's schema matches Object()'s schema for this + # particular 'key'. + else: + try: + schema.check_match(item) + + except exceptions.FormatError as e: + raise exceptions.FormatError( + str(e) + " in " + self._object_name + "." + key + ) class Struct(Schema): - """ - - Matches a non-homogeneous list of items. The sub-schemas are allowed to - vary. The object argument must be a sequence type (e.g., a list, tuple, - etc.). There is also an option to specify that additional schemas not - explicitly defined at instantiation are allowed. See __init__() for the - complete list of arguments accepted. - - Supported methods include: - matches(): returns a Boolean result. - check_match(): raises 'exceptions.FormatError' on a mismatch. - - - >>> schema = Struct([ListOf(AnyString()), AnyString(), String('X')]) - >>> schema.matches(False) - False - >>> schema.matches('Foo') - False - >>> schema.matches([[], 'Q', 'X']) - True - >>> schema.matches([[], 'Q', 'D']) - False - >>> schema.matches([[3], 'Q', 'X']) - False - >>> schema.matches([[], 'Q', 'X', 'Y']) - False - >>> schema = Struct([String('X')], allow_more=True) - >>> schema.matches([]) - False - >>> schema.matches(['X']) - True - >>> schema.matches(['X', 'Y']) - True - >>> schema.matches(['X', ['Y', 'Z']]) - True - >>> schema.matches([['X']]) - False - >>> schema = Struct([String('X'), Integer()], [Integer()]) - >>> schema.matches([]) - False - >>> schema.matches({}) - False - >>> schema.matches(['X']) - False - >>> schema.matches(['X', 3]) - True - >>> schema.matches(['X', 3, 9]) - True - >>> schema.matches(['X', 3, 9, 11]) - False - >>> schema.matches(['X', 3, 'A']) - False - """ - - def __init__(self, sub_schemas, optional_schemas=None, allow_more=False, - struct_name='list'): """ - Create a new Struct schema. - - - sub_schemas: The sub-schemas recognized. - optional_schemas: Optional list. If none is given, it will be "[]". - allow_more: Specifies that an optional list of types is allowed. - struct_name: A string identifier for the Struct object. + Matches a non-homogeneous list of items. The sub-schemas are allowed to + vary. The object argument must be a sequence type (e.g., a list, tuple, + etc.). There is also an option to specify that additional schemas not + explicitly defined at instantiation are allowed. See __init__() for the + complete list of arguments accepted. + + Supported methods include: + matches(): returns a Boolean result. + check_match(): raises 'exceptions.FormatError' on a mismatch. + + + >>> schema = Struct([ListOf(AnyString()), AnyString(), String('X')]) + >>> schema.matches(False) + False + >>> schema.matches('Foo') + False + >>> schema.matches([[], 'Q', 'X']) + True + >>> schema.matches([[], 'Q', 'D']) + False + >>> schema.matches([[3], 'Q', 'X']) + False + >>> schema.matches([[], 'Q', 'X', 'Y']) + False + >>> schema = Struct([String('X')], allow_more=True) + >>> schema.matches([]) + False + >>> schema.matches(['X']) + True + >>> schema.matches(['X', 'Y']) + True + >>> schema.matches(['X', ['Y', 'Z']]) + True + >>> schema.matches([['X']]) + False + >>> schema = Struct([String('X'), Integer()], [Integer()]) + >>> schema.matches([]) + False + >>> schema.matches({}) + False + >>> schema.matches(['X']) + False + >>> schema.matches(['X', 3]) + True + >>> schema.matches(['X', 3, 9]) + True + >>> schema.matches(['X', 3, 9, 11]) + False + >>> schema.matches(['X', 3, 'A']) + False """ - if optional_schemas is None: - optional_schemas = [] - - # Ensure each item of the list contains the expected object type. - if not isinstance(sub_schemas, (list, tuple)): - raise exceptions.FormatError( - 'Expected Schema but got ' + repr(sub_schemas)) - - for schema in sub_schemas: - if not isinstance(schema, Schema): - raise exceptions.FormatError('Expected Schema but' - ' got ' + repr(schema)) - - self._sub_schemas = sub_schemas + optional_schemas - self._min = len(sub_schemas) - self._allow_more = allow_more - self._struct_name = struct_name - - - def check_match(self, object): - if not isinstance(object, (list, tuple)): - raise exceptions.FormatError( - 'Expected ' + repr(self._struct_name) + '; but got ' + repr(object)) - - elif len(object) < self._min: - raise exceptions.FormatError( - 'Too few fields in ' + self._struct_name) - - elif len(object) > len(self._sub_schemas) and not self._allow_more: - raise exceptions.FormatError( - 'Too many fields in ' + self._struct_name) - - # Iterate through the items of 'object', checking against each schema in - # the list of schemas allowed (i.e., the sub-schemas and also any optional - # schemas. The lenth of 'object' must be less than the length of the - # required schemas + the optional schemas. However, 'object' is allowed to - # be only as large as the length of the required schemas. In the while - # loop below, we check against these two cases. - index = 0 - while index < len(object) and index < len(self._sub_schemas): - item = object[index] - schema = self._sub_schemas[index] - schema.check_match(item) - index = index + 1 - - - + def __init__( + self, + sub_schemas, + optional_schemas=None, + allow_more=False, + struct_name="list", + ): + """ + + Create a new Struct schema. + + + sub_schemas: The sub-schemas recognized. + optional_schemas: Optional list. If none is given, it will be "[]". + allow_more: Specifies that an optional list of types is allowed. + struct_name: A string identifier for the Struct object. + """ + + if optional_schemas is None: + optional_schemas = [] + + # Ensure each item of the list contains the expected object type. + if not isinstance(sub_schemas, (list, tuple)): + raise exceptions.FormatError( + "Expected Schema but got " + repr(sub_schemas) + ) + + for schema in sub_schemas: + if not isinstance(schema, Schema): + raise exceptions.FormatError( + "Expected Schema but" " got " + repr(schema) + ) + + self._sub_schemas = sub_schemas + optional_schemas + self._min = len(sub_schemas) + self._allow_more = allow_more + self._struct_name = struct_name + + def check_match(self, object): # pylint: disable=redefined-builtin + if not isinstance( # pylint: disable=no-else-raise + object, (list, tuple) + ): + raise exceptions.FormatError( + "Expected " + + repr(self._struct_name) + + "; but got " + + repr(object) + ) + + elif len(object) < self._min: + raise exceptions.FormatError( + "Too few fields in " + self._struct_name + ) + + elif len(object) > len(self._sub_schemas) and not self._allow_more: + raise exceptions.FormatError( + "Too many fields in " + self._struct_name + ) + + # Iterate through the items of 'object', checking against each schema in + # the list of schemas allowed (i.e., the sub-schemas and also any optional + # schemas. The lenth of 'object' must be less than the length of the + # required schemas + the optional schemas. However, 'object' is allowed to + # be only as large as the length of the required schemas. In the while + # loop below, we check against these two cases. + index = 0 + while index < len(object) and index < len(self._sub_schemas): + item = object[index] + schema = self._sub_schemas[index] + schema.check_match(item) + index = index + 1 class RegularExpression(Schema): - """ - - Matches any string that matches a given regular expression. The RE pattern - set when RegularExpression is instantiated must not be None. See - __init__() for a complete list of accepted arguments. - - Supported methods include: - matches(): returns a Boolean result. - check_match(): raises 'exceptions.FormatError' on a mismatch. - - - >>> schema = RegularExpression('h.*d') - >>> schema.matches('hello world') - True - >>> schema.matches('Hello World') - False - >>> schema.matches('hello world!') - False - >>> schema.matches([33, 'Hello']) - False - """ - - def __init__(self, pattern=None, modifiers=0, re_object=None, re_name=None): """ - Create a new regular expression schema. - - - pattern: The pattern to match, or None if re_object is provided. - modifiers: Flags to use when compiling the pattern. - re_object: A compiled regular expression object. - re_name: Identifier for the regular expression object. + Matches any string that matches a given regular expression. The RE pattern + set when RegularExpression is instantiated must not be None. See + __init__() for a complete list of accepted arguments. + + Supported methods include: + matches(): returns a Boolean result. + check_match(): raises 'exceptions.FormatError' on a mismatch. + + + >>> schema = RegularExpression('h.*d') + >>> schema.matches('hello world') + True + >>> schema.matches('Hello World') + False + >>> schema.matches('hello world!') + False + >>> schema.matches([33, 'Hello']) + False """ - if not isinstance(pattern, str): - if pattern is not None: - raise exceptions.FormatError( - repr(pattern) + ' is not a string.') - - if re_object is None: - if pattern is None: - raise exceptions.FormatError( - 'Cannot compare against an unset regular expression') - - if not pattern.endswith('$'): - pattern += '$' - re_object = re.compile(pattern, modifiers) - self._re_object = re_object - - if re_name is None: - if pattern is not None: - re_name = 'pattern /' + pattern + '/' - - else: - re_name = 'pattern' - self._re_name = re_name - - - def check_match(self, object): - if not isinstance(object, str) or not self._re_object.match(object): - raise exceptions.FormatError( - repr(object) + ' did not match ' + repr(self._re_name)) - - - -if __name__ == '__main__': - # The interactive sessions of the documentation strings can - # be tested by running schema.py as a standalone module. - # python -B schema.py. - import doctest - doctest.testmod() - + def __init__(self, pattern=None, modifiers=0, re_object=None, re_name=None): + """ + + Create a new regular expression schema. + + + pattern: The pattern to match, or None if re_object is provided. + modifiers: Flags to use when compiling the pattern. + re_object: A compiled regular expression object. + re_name: Identifier for the regular expression object. + """ + + if not isinstance(pattern, str): + if pattern is not None: + raise exceptions.FormatError( + repr(pattern) + " is not a string." + ) + + if re_object is None: + if pattern is None: + raise exceptions.FormatError( + "Cannot compare against an unset regular expression" + ) + + if not pattern.endswith("$"): + pattern += "$" + re_object = re.compile(pattern, modifiers) + self._re_object = re_object + + if re_name is None: + if pattern is not None: + re_name = "pattern /" + pattern + "/" + + else: + re_name = "pattern" + self._re_name = re_name + + def check_match(self, object): # pylint: disable=redefined-builtin + if not isinstance(object, str) or not self._re_object.match(object): + raise exceptions.FormatError( + repr(object) + " did not match " + repr(self._re_name) + ) + + +if __name__ == "__main__": + # The interactive sessions of the documentation strings can + # be tested by running schema.py as a standalone module. + # python -B schema.py. + import doctest + + doctest.testmod() diff --git a/securesystemslib/settings.py b/securesystemslib/settings.py index 0054f948..16a14be4 100755 --- a/securesystemslib/settings.py +++ b/securesystemslib/settings.py @@ -36,7 +36,7 @@ PBKDF2_ITERATIONS = 100000 # The algorithm(s) in HASH_ALGORITHMS are used to generate key IDs. -HASH_ALGORITHMS = ['sha256', 'sha512'] +HASH_ALGORITHMS = ["sha256", "sha512"] # Used in securesystemslib.process, to raise a subprocess.TimeoutExpired if # a started subprocess does not terminate before the here specified seconds diff --git a/securesystemslib/signer.py b/securesystemslib/signer.py index c595eb3f..4b9e6c68 100644 --- a/securesystemslib/signer.py +++ b/securesystemslib/signer.py @@ -6,10 +6,10 @@ """ import abc -from typing import Any, Dict, Optional, Mapping +from typing import Any, Dict, Mapping, Optional -import securesystemslib.keys as sslib_keys import securesystemslib.gpg.functions as gpg +import securesystemslib.keys as sslib_keys class Signature: @@ -225,7 +225,9 @@ class GPGSigner(Signer): """ - def __init__(self, keyid: Optional[str] = None, homedir: Optional[str] = None): + def __init__( + self, keyid: Optional[str] = None, homedir: Optional[str] = None + ): self.keyid = keyid self.homedir = homedir diff --git a/securesystemslib/storage.py b/securesystemslib/storage.py index d488402e..9ff1e086 100644 --- a/securesystemslib/storage.py +++ b/securesystemslib/storage.py @@ -22,276 +22,284 @@ import shutil import stat from contextlib import contextmanager +from typing import IO, BinaryIO, Iterator, List, Optional + from securesystemslib import exceptions -from typing import BinaryIO, IO, Iterator, List, Optional logger = logging.getLogger(__name__) -class StorageBackendInterface(): - """ - - Defines an interface for abstract storage operations which can be implemented - for a variety of storage solutions, such as remote and local filesystems. - """ - - __metaclass__ = abc.ABCMeta - - - @abc.abstractmethod - @contextmanager - def get(self, filepath: str) -> Iterator[BinaryIO]: - """ - - A context manager for 'with' statements that is used for retrieving files - from a storage backend and cleans up the files upon exit. - - with storage_backend.get('/path/to/file') as file_object: - # operations - # file is now closed - - - filepath: - The full path of the file to be retrieved. - - - securesystemslib.exceptions.StorageError, if the file does not exist or is - no accessible. - - - A ContextManager object that emits a file-like object for the file at - 'filepath'. - """ - raise NotImplementedError # pragma: no cover - - - @abc.abstractmethod - def put(self, fileobj: IO, filepath: str, restrict: Optional[bool] = False - ) -> None: - """ - - Store a file-like object in the storage backend. - The file-like object is read from the beginning, not its current - offset (if any). - - - fileobj: - The file-like object to be stored. - - filepath: - The full path to the location where 'fileobj' will be stored. - - restrict: - Whether the file should be created with restricted permissions. - What counts as restricted is backend-specific. For a filesystem on a - UNIX-like operating system, that may mean read/write permissions only - for the user (octal mode 0o600). For a cloud storage system, that - likely means Cloud provider specific ACL restrictions. - - - securesystemslib.exceptions.StorageError, if the file can not be stored. - - - None - """ - raise NotImplementedError # pragma: no cover - - - @abc.abstractmethod - def remove(self, filepath: str) -> None: - """ - - Remove the file at 'filepath' from the storage. - - - filepath: - The full path to the file. - - - securesystemslib.exceptions.StorageError, if the file can not be removed. - - - None - """ - raise NotImplementedError # pragma: no cover - - - @abc.abstractmethod - def getsize(self, filepath: str) -> int: +class StorageBackendInterface: """ - Retrieve the size, in bytes, of the file at 'filepath'. - - - filepath: - The full path to the file. - - - securesystemslib.exceptions.StorageError, if the file does not exist or is - not accessible. - - - The size in bytes of the file at 'filepath'. - """ - raise NotImplementedError # pragma: no cover - - - @abc.abstractmethod - def create_folder(self, filepath: str) -> None: + Defines an interface for abstract storage operations which can be implemented + for a variety of storage solutions, such as remote and local filesystems. """ - - Create a folder at filepath and ensure all intermediate components of the - path exist. - Passing an empty string for filepath does nothing and does not raise an - exception. - - filepath: - The full path of the folder to be created. + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + @contextmanager + def get(self, filepath: str) -> Iterator[BinaryIO]: + """ + + A context manager for 'with' statements that is used for retrieving files + from a storage backend and cleans up the files upon exit. + + with storage_backend.get('/path/to/file') as file_object: + # operations + # file is now closed + + + filepath: + The full path of the file to be retrieved. + + + securesystemslib.exceptions.StorageError, if the file does not exist or is + no accessible. + + + A ContextManager object that emits a file-like object for the file at + 'filepath'. + """ + raise NotImplementedError # pragma: no cover + + @abc.abstractmethod + def put( + self, fileobj: IO, filepath: str, restrict: Optional[bool] = False + ) -> None: + """ + + Store a file-like object in the storage backend. + The file-like object is read from the beginning, not its current + offset (if any). + + + fileobj: + The file-like object to be stored. + + filepath: + The full path to the location where 'fileobj' will be stored. + + restrict: + Whether the file should be created with restricted permissions. + What counts as restricted is backend-specific. For a filesystem on a + UNIX-like operating system, that may mean read/write permissions only + for the user (octal mode 0o600). For a cloud storage system, that + likely means Cloud provider specific ACL restrictions. + + + securesystemslib.exceptions.StorageError, if the file can not be stored. + + + None + """ + raise NotImplementedError # pragma: no cover + + @abc.abstractmethod + def remove(self, filepath: str) -> None: + """ + + Remove the file at 'filepath' from the storage. + + + filepath: + The full path to the file. + + + securesystemslib.exceptions.StorageError, if the file can not be removed. + + + None + """ + raise NotImplementedError # pragma: no cover + + @abc.abstractmethod + def getsize(self, filepath: str) -> int: + """ + + Retrieve the size, in bytes, of the file at 'filepath'. + + + filepath: + The full path to the file. + + + securesystemslib.exceptions.StorageError, if the file does not exist or is + not accessible. + + + The size in bytes of the file at 'filepath'. + """ + raise NotImplementedError # pragma: no cover + + @abc.abstractmethod + def create_folder(self, filepath: str) -> None: + """ + + Create a folder at filepath and ensure all intermediate components of the + path exist. + Passing an empty string for filepath does nothing and does not raise an + exception. + + + filepath: + The full path of the folder to be created. + + + securesystemslib.exceptions.StorageError, if the folder can not be + created. + + + None + """ + raise NotImplementedError # pragma: no cover + + @abc.abstractmethod + def list_folder(self, filepath: str) -> List[str]: + """ + + List the contents of the folder at 'filepath'. + + + filepath: + The full path of the folder to be listed. + + + securesystemslib.exceptions.StorageError, if the file does not exist or is + not accessible. + + + A list containing the names of the files in the folder. May be an empty + list. + """ + raise NotImplementedError # pragma: no cover - - securesystemslib.exceptions.StorageError, if the folder can not be - created. - - None - """ - raise NotImplementedError # pragma: no cover - - - @abc.abstractmethod - def list_folder(self, filepath: str) -> List[str]: +class FilesystemBackend(StorageBackendInterface): """ - List the contents of the folder at 'filepath'. - - - filepath: - The full path of the folder to be listed. - - - securesystemslib.exceptions.StorageError, if the file does not exist or is - not accessible. - - - A list containing the names of the files in the folder. May be an empty - list. + A concrete implementation of StorageBackendInterface which interacts with + local filesystems using Python standard library functions. """ - raise NotImplementedError # pragma: no cover - - - - -class FilesystemBackend(StorageBackendInterface): - """ - - A concrete implementation of StorageBackendInterface which interacts with - local filesystems using Python standard library functions. - """ - - # As FilesystemBackend is effectively a stateless wrapper around various - # standard library operations, we only ever need a single instance of it. - # That single instance is safe to be (re-)used by all callers. Therefore - # implement the singleton pattern to avoid uneccesarily creating multiple - # objects. - _instance = None - - def __new__(cls, *args, **kwargs): - if cls._instance is None: - cls._instance = object.__new__(cls, *args, **kwargs) - return cls._instance - - - @contextmanager - def get(self, filepath:str) -> Iterator[BinaryIO]: - file_object = None - try: - file_object = open(filepath, 'rb') - yield file_object - except OSError: - raise exceptions.StorageError( - "Can't open %s" % filepath) - finally: - if file_object is not None: - file_object.close() - - - def put(self, fileobj: IO, filepath: str, restrict: Optional[bool] = False - ) -> None: - # If we are passed an open file, seek to the beginning such that we are - # copying the entire contents - if not fileobj.closed: - fileobj.seek(0) - - # If a file with the same name already exists, the new permissions - # may not be applied. - try: - os.remove(filepath) - except OSError: - pass - - try: - if restrict: - # On UNIX-based systems restricted files are created with read and - # write permissions for the user only (octal value 0o600). - fd = os.open(filepath, os.O_WRONLY|os.O_CREAT, - stat.S_IRUSR|stat.S_IWUSR) - else: - # Non-restricted files use the default 'mode' argument of os.open() - # granting read, write, and execute for all users (octal mode 0o777). - # NOTE: mode may be modified by the user's file mode creation mask - # (umask) or on Windows limited to the smaller set of OS supported - # permisssions. - fd = os.open(filepath, os.O_WRONLY|os.O_CREAT) - - with os.fdopen(fd, "wb") as destination_file: - shutil.copyfileobj(fileobj, destination_file) - # Force the destination file to be written to disk from Python's internal - # and the operating system's buffers. os.fsync() should follow flush(). - destination_file.flush() - os.fsync(destination_file.fileno()) - except OSError: - raise exceptions.StorageError( - "Can't write file %s" % filepath) - - - def remove(self, filepath: str) -> None: - try: - os.remove(filepath) - except (FileNotFoundError, PermissionError, OSError): # pragma: no cover - raise exceptions.StorageError( - "Can't remove file %s" % filepath) - - - def getsize(self, filepath: str) -> int: - try: - return os.path.getsize(filepath) - except OSError: - raise exceptions.StorageError( - "Can't access file %s" % filepath) - - - def create_folder(self, filepath: str) -> None: - try: - os.makedirs(filepath) - except OSError as e: - # 'OSError' raised if the leaf directory already exists or cannot be - # created. Check for case where 'filepath' has already been created and - # silently ignore. - if e.errno == errno.EEXIST: - pass - elif e.errno == errno.ENOENT and not filepath: - raise exceptions.StorageError( - "Can't create a folder with an empty filepath!") - else: - raise exceptions.StorageError( - "Can't create folder at %s" % filepath) - - - def list_folder(self, filepath: str) -> List[str]: - try: - return os.listdir(filepath) - except FileNotFoundError: - raise exceptions.StorageError( - "Can't list folder at %s" % filepath) + # As FilesystemBackend is effectively a stateless wrapper around various + # standard library operations, we only ever need a single instance of it. + # That single instance is safe to be (re-)used by all callers. Therefore + # implement the singleton pattern to avoid uneccesarily creating multiple + # objects. + _instance = None + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = object.__new__(cls, *args, **kwargs) + return cls._instance + + @contextmanager + def get(self, filepath: str) -> Iterator[BinaryIO]: + file_object = None + try: + file_object = open(filepath, "rb") + yield file_object + except OSError: + raise exceptions.StorageError( # pylint: disable=raise-missing-from + "Can't open %s" # pylint: disable=consider-using-f-string + % filepath + ) + finally: + if file_object is not None: + file_object.close() + + def put( + self, fileobj: IO, filepath: str, restrict: Optional[bool] = False + ) -> None: + # If we are passed an open file, seek to the beginning such that we are + # copying the entire contents + if not fileobj.closed: + fileobj.seek(0) + + # If a file with the same name already exists, the new permissions + # may not be applied. + try: + os.remove(filepath) + except OSError: + pass + + try: + if restrict: + # On UNIX-based systems restricted files are created with read and + # write permissions for the user only (octal value 0o600). + fd = os.open( + filepath, + os.O_WRONLY | os.O_CREAT, + stat.S_IRUSR | stat.S_IWUSR, + ) + else: + # Non-restricted files use the default 'mode' argument of os.open() + # granting read, write, and execute for all users (octal mode 0o777). + # NOTE: mode may be modified by the user's file mode creation mask + # (umask) or on Windows limited to the smaller set of OS supported + # permisssions. + fd = os.open(filepath, os.O_WRONLY | os.O_CREAT) + + with os.fdopen(fd, "wb") as destination_file: + shutil.copyfileobj(fileobj, destination_file) + # Force the destination file to be written to disk from Python's internal + # and the operating system's buffers. os.fsync() should follow flush(). + destination_file.flush() + os.fsync(destination_file.fileno()) + except OSError: + raise exceptions.StorageError( # pylint: disable=raise-missing-from + "Can't write file %s" # pylint: disable=consider-using-f-string + % filepath + ) + + def remove(self, filepath: str) -> None: + try: + os.remove(filepath) + except ( + FileNotFoundError, + PermissionError, + OSError, + ): # pragma: no cover + raise exceptions.StorageError( # pylint: disable=raise-missing-from + "Can't remove file %s" # pylint: disable=consider-using-f-string + % filepath + ) + + def getsize(self, filepath: str) -> int: + try: + return os.path.getsize(filepath) + except OSError: + raise exceptions.StorageError( # pylint: disable=raise-missing-from + "Can't access file %s" # pylint: disable=consider-using-f-string + % filepath + ) + + def create_folder(self, filepath: str) -> None: + try: + os.makedirs(filepath) + except OSError as e: + # 'OSError' raised if the leaf directory already exists or cannot be + # created. Check for case where 'filepath' has already been created and + # silently ignore. + if e.errno == errno.EEXIST: + pass + elif e.errno == errno.ENOENT and not filepath: + raise exceptions.StorageError( + "Can't create a folder with an empty filepath!" + ) + else: + raise exceptions.StorageError( + "Can't create folder at %s" # pylint: disable=consider-using-f-string + % filepath + ) + + def list_folder(self, filepath: str) -> List[str]: + try: + return os.listdir(filepath) + except FileNotFoundError: + raise exceptions.StorageError( # pylint: disable=raise-missing-from + "Can't list folder at %s" # pylint: disable=consider-using-f-string + % filepath + ) diff --git a/securesystemslib/unittest_toolbox.py b/securesystemslib/unittest_toolbox.py index d0984248..0b6223e8 100755 --- a/securesystemslib/unittest_toolbox.py +++ b/securesystemslib/unittest_toolbox.py @@ -18,124 +18,119 @@ """ import os -import shutil -import unittest -import tempfile import random +import shutil import string +import tempfile +import unittest -class Modified_TestCase(unittest.TestCase): - """ - - Provide additional test-setup methods to make testing - of module's methods-under-test as independent as possible. - - If you want to modify setUp()/tearDown() do: - class Your_Test_Class(modified_TestCase): - def setUp(): - your setup modification - your setup modification - ... - modified_TestCase.setUp(self) - - - make_temp_directory(self, directory=None): - Creates and returns an absolute path of a temporary directory. - - make_temp_file(self, suffix='.txt', directory=None): - Creates and returns an absolute path of an empty temp file. - - make_temp_data_file(self, suffix='', directory=None, data = junk_data): - Returns an absolute path of a temp file containing some data. - - random_path(self, length = 7): - Generate a 'random' path consisting of n-length strings of random chars. - - - Static Methods: - -------------- - Following methods are static because they technically don't operate - on any instances of the class, what they do is: they modify class variables - (dictionaries) that are shared among all instances of the class. So - it is possible to call them without instantiating the class. - - random_string(length=7): - Generate a 'length' long string of random characters. - """ - - - def setUp(self): - self._cleanup = [] - +class Modified_TestCase(unittest.TestCase): # pylint: disable=invalid-name + """ + + Provide additional test-setup methods to make testing + of module's methods-under-test as independent as possible. + If you want to modify setUp()/tearDown() do: + class Your_Test_Class(modified_TestCase): + def setUp(): + your setup modification + your setup modification + ... + modified_TestCase.setUp(self) - def tearDown(self): - for cleanup_function in self._cleanup: - # Perform clean up by executing clean-up functions. - try: - # OSError will occur if the directory was already removed. - cleanup_function() + + make_temp_directory(self, directory=None): + Creates and returns an absolute path of a temporary directory. - except OSError: - pass + make_temp_file(self, suffix='.txt', directory=None): + Creates and returns an absolute path of an empty temp file. + make_temp_data_file(self, suffix='', directory=None, data = junk_data): + Returns an absolute path of a temp file containing some data. + random_path(self, length = 7): + Generate a 'random' path consisting of n-length strings of random chars. - def make_temp_directory(self, directory=None): - """Creates and returns an absolute path of a directory.""" - prefix = self.__class__.__name__+'_' - temp_directory = tempfile.mkdtemp(prefix=prefix, dir=directory) - def _destroy_temp_directory(): - shutil.rmtree(temp_directory) - self._cleanup.append(_destroy_temp_directory) - return temp_directory + Static Methods: + -------------- + Following methods are static because they technically don't operate + on any instances of the class, what they do is: they modify class variables + (dictionaries) that are shared among all instances of the class. So + it is possible to call them without instantiating the class. + random_string(length=7): + Generate a 'length' long string of random characters. + """ + def setUp(self): + self._cleanup = [] - def make_temp_file(self, suffix='.txt', directory=None): - """Creates and returns an absolute path of an empty file.""" + def tearDown(self): + for cleanup_function in self._cleanup: + # Perform clean up by executing clean-up functions. + try: + # OSError will occur if the directory was already removed. + cleanup_function() - prefix='tmp_file_'+self.__class__.__name__+'_' - temp_file = tempfile.mkstemp(suffix=suffix, prefix=prefix, dir=directory) + except OSError: + pass - def _destroy_temp_file(): - os.unlink(temp_file[1]) - self._cleanup.append(_destroy_temp_file) - return temp_file[1] + def make_temp_directory(self, directory=None): + """Creates and returns an absolute path of a directory.""" + prefix = self.__class__.__name__ + "_" + temp_directory = tempfile.mkdtemp(prefix=prefix, dir=directory) + def _destroy_temp_directory(): + shutil.rmtree(temp_directory) + self._cleanup.append(_destroy_temp_directory) + return temp_directory - def make_temp_data_file(self, suffix='', directory=None, data = 'junk data'): - """Returns an absolute path of a temp file containing data.""" + def make_temp_file(self, suffix=".txt", directory=None): + """Creates and returns an absolute path of an empty file.""" - temp_file_path = self.make_temp_file(suffix=suffix, directory=directory) - temp_file = open(temp_file_path, 'wt') - temp_file.write(data) - temp_file.close() + prefix = "tmp_file_" + self.__class__.__name__ + "_" + temp_file = tempfile.mkstemp( + suffix=suffix, prefix=prefix, dir=directory + ) - return temp_file_path + def _destroy_temp_file(): + os.unlink(temp_file[1]) + self._cleanup.append(_destroy_temp_file) + return temp_file[1] + def make_temp_data_file(self, suffix="", directory=None, data="junk data"): + """Returns an absolute path of a temp file containing data.""" - def random_path(self, length = 7): - """Generate a 'random' path consisting of random n-length strings.""" + temp_file_path = self.make_temp_file(suffix=suffix, directory=directory) + temp_file = ( + open( # pylint: disable=unspecified-encoding,consider-using-with + temp_file_path, "wt" + ) + ) + temp_file.write(data) + temp_file.close() - rand_path = '/'+self.random_string(length) - for i in range(2): - rand_path = os.path.join(rand_path, self.random_string(length)) + return temp_file_path - return rand_path + def random_path(self, length=7): + """Generate a 'random' path consisting of random n-length strings.""" + rand_path = "/" + self.random_string(length) + for i in range(2): # pylint: disable=unused-variable + rand_path = os.path.join(rand_path, self.random_string(length)) + return rand_path - @staticmethod - def random_string(length=15): - """Generate a random string of specified length.""" + @staticmethod + def random_string(length=15): + """Generate a random string of specified length.""" - rand_str = '' - for letter in range(length): - rand_str += random.choice('abcdefABCDEF' +string.digits) + rand_str = "" + for letter in range(length): # pylint: disable=unused-variable + rand_str += random.choice("abcdefABCDEF" + string.digits) # nosec - return rand_str + return rand_str diff --git a/securesystemslib/util.py b/securesystemslib/util.py index d716358a..755f13aa 100644 --- a/securesystemslib/util.py +++ b/securesystemslib/util.py @@ -18,161 +18,157 @@ """ import json -import os import logging +import os +from typing import IO, Any, Dict, List, Optional, Sequence, Tuple, Union -from securesystemslib import exceptions -from securesystemslib import formats +from securesystemslib import exceptions, formats from securesystemslib.hash import digest_fileobject from securesystemslib.storage import FilesystemBackend, StorageBackendInterface -from typing import Any, Dict, IO, List, Optional, Sequence, Tuple, Union - logger = logging.getLogger(__name__) -def get_file_details( +def get_file_details( # pylint: disable=dangerous-default-value filepath: str, - hash_algorithms: List[str] = ['sha256'], - storage_backend: Optional[StorageBackendInterface] = None + hash_algorithms: List[str] = ["sha256"], + storage_backend: Optional[StorageBackendInterface] = None, ) -> Tuple[int, Dict[str, str]]: - """ - - To get file's length and hash information. The hash is computed using the - sha256 algorithm. This function is used in the signerlib.py and updater.py - modules. + """ + + To get file's length and hash information. The hash is computed using the + sha256 algorithm. This function is used in the signerlib.py and updater.py + modules. - - filepath: - Absolute file path of a file. + + filepath: + Absolute file path of a file. - hash_algorithms: - A list of hash algorithms with which the file's hash should be computed. - Defaults to ['sha256'] + hash_algorithms: + A list of hash algorithms with which the file's hash should be computed. + Defaults to ['sha256'] - storage_backend: - An object which implements - securesystemslib.storage.StorageBackendInterface. When no object is - passed a FilesystemBackend will be instantiated and used. + storage_backend: + An object which implements + securesystemslib.storage.StorageBackendInterface. When no object is + passed a FilesystemBackend will be instantiated and used. - - securesystemslib.exceptions.FormatError: If hash of the file does not match - HASHDICT_SCHEMA. + + securesystemslib.exceptions.FormatError: If hash of the file does not match + HASHDICT_SCHEMA. - securesystemslib.exceptions.StorageError: The file at "filepath" cannot be - opened or found. + securesystemslib.exceptions.StorageError: The file at "filepath" cannot be + opened or found. - - A tuple (length, hashes) describing 'filepath'. - """ + + A tuple (length, hashes) describing 'filepath'. + """ - # Making sure that the format of 'filepath' is a path string. - # 'securesystemslib.exceptions.FormatError' is raised on incorrect format. - formats.PATH_SCHEMA.check_match(filepath) - formats.HASHALGORITHMS_SCHEMA.check_match(hash_algorithms) + # Making sure that the format of 'filepath' is a path string. + # 'securesystemslib.exceptions.FormatError' is raised on incorrect format. + formats.PATH_SCHEMA.check_match(filepath) + formats.HASHALGORITHMS_SCHEMA.check_match(hash_algorithms) - if storage_backend is None: - storage_backend = FilesystemBackend() + if storage_backend is None: + storage_backend = FilesystemBackend() - file_length = get_file_length(filepath, storage_backend) - file_hashes = get_file_hashes(filepath, hash_algorithms, storage_backend) + file_length = get_file_length(filepath, storage_backend) + file_hashes = get_file_hashes(filepath, hash_algorithms, storage_backend) - return file_length, file_hashes + return file_length, file_hashes -def get_file_hashes( +def get_file_hashes( # pylint: disable=dangerous-default-value filepath: str, - hash_algorithms: List[str] = ['sha256'], - storage_backend: Optional[StorageBackendInterface] = None + hash_algorithms: List[str] = ["sha256"], + storage_backend: Optional[StorageBackendInterface] = None, ) -> Dict[str, str]: - """ - - Compute hash(es) of the file at filepath using each of the specified - hash algorithms. If no algorithms are specified, then the hash is - computed using the SHA-256 algorithm. - - - filepath: - Absolute file path of a file. + """ + + Compute hash(es) of the file at filepath using each of the specified + hash algorithms. If no algorithms are specified, then the hash is + computed using the SHA-256 algorithm. - hash_algorithms: - A list of hash algorithms with which the file's hash should be computed. - Defaults to ['sha256'] + + filepath: + Absolute file path of a file. - storage_backend: - An object which implements - securesystemslib.storage.StorageBackendInterface. When no object is - passed a FilesystemBackend will be instantiated and used. + hash_algorithms: + A list of hash algorithms with which the file's hash should be computed. + Defaults to ['sha256'] - - securesystemslib.exceptions.FormatError: If hash of the file does not match - HASHDICT_SCHEMA. + storage_backend: + An object which implements + securesystemslib.storage.StorageBackendInterface. When no object is + passed a FilesystemBackend will be instantiated and used. - securesystemslib.exceptions.StorageError: The file at "filepath" cannot be - opened or found. + + securesystemslib.exceptions.FormatError: If hash of the file does not match + HASHDICT_SCHEMA. - - A dictionary conforming to securesystemslib.formats.HASHDICT_SCHEMA - containing information about the hashes of the file at "filepath". - """ + securesystemslib.exceptions.StorageError: The file at "filepath" cannot be + opened or found. - # Making sure that the format of 'filepath' is a path string. - # 'securesystemslib.exceptions.FormatError' is raised on incorrect format. - formats.PATH_SCHEMA.check_match(filepath) - formats.HASHALGORITHMS_SCHEMA.check_match(hash_algorithms) + + A dictionary conforming to securesystemslib.formats.HASHDICT_SCHEMA + containing information about the hashes of the file at "filepath". + """ - if storage_backend is None: - storage_backend = FilesystemBackend() + # Making sure that the format of 'filepath' is a path string. + # 'securesystemslib.exceptions.FormatError' is raised on incorrect format. + formats.PATH_SCHEMA.check_match(filepath) + formats.HASHALGORITHMS_SCHEMA.check_match(hash_algorithms) - file_hashes = {} + if storage_backend is None: + storage_backend = FilesystemBackend() - with storage_backend.get(filepath) as fileobj: - # Obtaining hash of the file. - for algorithm in hash_algorithms: - digest_object = digest_fileobject(fileobj, algorithm) - file_hashes.update({algorithm: digest_object.hexdigest()}) + file_hashes = {} - # Performing a format check to ensure 'file_hash' corresponds HASHDICT_SCHEMA. - # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch. - formats.HASHDICT_SCHEMA.check_match(file_hashes) + with storage_backend.get(filepath) as fileobj: + # Obtaining hash of the file. + for algorithm in hash_algorithms: + digest_object = digest_fileobject(fileobj, algorithm) + file_hashes.update({algorithm: digest_object.hexdigest()}) - return file_hashes + # Performing a format check to ensure 'file_hash' corresponds HASHDICT_SCHEMA. + # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch. + formats.HASHDICT_SCHEMA.check_match(file_hashes) + return file_hashes def get_file_length( - filepath: str, - storage_backend: Optional[StorageBackendInterface] = None + filepath: str, storage_backend: Optional[StorageBackendInterface] = None ) -> int: - """ - - To get file's length information. + """ + + To get file's length information. - - filepath: - Absolute file path of a file. + + filepath: + Absolute file path of a file. - storage_backend: - An object which implements - securesystemslib.storage.StorageBackendInterface. When no object is - passed a FilesystemBackend will be instantiated and used. + storage_backend: + An object which implements + securesystemslib.storage.StorageBackendInterface. When no object is + passed a FilesystemBackend will be instantiated and used. - - securesystemslib.exceptions.StorageError: The file at "filepath" cannot be - opened or found. + + securesystemslib.exceptions.StorageError: The file at "filepath" cannot be + opened or found. - - The length, in bytes, of the file at 'filepath'. - """ + + The length, in bytes, of the file at 'filepath'. + """ - # Making sure that the format of 'filepath' is a path string. - # 'securesystemslib.exceptions.FormatError' is raised on incorrect format. - formats.PATH_SCHEMA.check_match(filepath) + # Making sure that the format of 'filepath' is a path string. + # 'securesystemslib.exceptions.FormatError' is raised on incorrect format. + formats.PATH_SCHEMA.check_match(filepath) - if storage_backend is None: - storage_backend = FilesystemBackend() + if storage_backend is None: + storage_backend = FilesystemBackend() - return storage_backend.getsize(filepath) + return storage_backend.getsize(filepath) def persist_temp_file( @@ -180,284 +176,284 @@ def persist_temp_file( persist_path: str, storage_backend: Optional[StorageBackendInterface] = None, should_close: bool = True, - restrict: bool = False + restrict: bool = False, ) -> None: - """ - - Copies 'temp_file' (a file like object) to a newly created non-temp file at - 'persist_path'. + """ + + Copies 'temp_file' (a file like object) to a newly created non-temp file at + 'persist_path'. - - temp_file: - File object to persist, typically a file object returned by one of the - interfaces in the tempfile module of the standard library. + + temp_file: + File object to persist, typically a file object returned by one of the + interfaces in the tempfile module of the standard library. - persist_path: - File path to create the persistent file in. + persist_path: + File path to create the persistent file in. - storage_backend: - An object which implements - securesystemslib.storage.StorageBackendInterface. When no object is - passed a FilesystemBackend will be instantiated and used. + storage_backend: + An object which implements + securesystemslib.storage.StorageBackendInterface. When no object is + passed a FilesystemBackend will be instantiated and used. - should_close: - A boolean indicating whether the file should be closed after it has been - persisted. Default is True, the file is closed. + should_close: + A boolean indicating whether the file should be closed after it has been + persisted. Default is True, the file is closed. - restrict: - A boolean indicating whether the file should have restricted privileges. - What evactly counts as restricted privileges is an implementation detail - of the backing StorageBackendInterface implementation. + restrict: + A boolean indicating whether the file should have restricted privileges. + What evactly counts as restricted privileges is an implementation detail + of the backing StorageBackendInterface implementation. - - securesystemslib.exceptions.StorageError: If file cannot be written. + + securesystemslib.exceptions.StorageError: If file cannot be written. - - None. - """ + + None. + """ - if storage_backend is None: - storage_backend = FilesystemBackend() + if storage_backend is None: + storage_backend = FilesystemBackend() - storage_backend.put(temp_file, persist_path, restrict=restrict) + storage_backend.put(temp_file, persist_path, restrict=restrict) - if should_close: - temp_file.close() + if should_close: + temp_file.close() def ensure_parent_dir( - filename: str, - storage_backend: Optional[StorageBackendInterface] = None + filename: str, storage_backend: Optional[StorageBackendInterface] = None ) -> None: - """ - - To ensure existence of the parent directory of 'filename'. If the parent - directory of 'name' does not exist, create it. + """ + + To ensure existence of the parent directory of 'filename'. If the parent + directory of 'name' does not exist, create it. - Example: If 'filename' is '/a/b/c/d.txt', and only the directory '/a/b/' - exists, then directory '/a/b/c/d/' will be created. + Example: If 'filename' is '/a/b/c/d.txt', and only the directory '/a/b/' + exists, then directory '/a/b/c/d/' will be created. - - filename: - A path string. + + filename: + A path string. - storage_backend: - An object which implements - securesystemslib.storage.StorageBackendInterface. When no object is - passed a FilesystemBackend will be instantiated and used. + storage_backend: + An object which implements + securesystemslib.storage.StorageBackendInterface. When no object is + passed a FilesystemBackend will be instantiated and used. - - securesystemslib.exceptions.FormatError: If 'filename' is improperly - formatted. - securesystemslib.exceptions.StorageError: When folder cannot be created. + + securesystemslib.exceptions.FormatError: If 'filename' is improperly + formatted. + securesystemslib.exceptions.StorageError: When folder cannot be created. - - A directory is created whenever the parent directory of 'filename' does not - exist. + + A directory is created whenever the parent directory of 'filename' does not + exist. - - None. - """ + + None. + """ - # Ensure 'filename' corresponds to 'PATH_SCHEMA'. - # Raise 'securesystemslib.exceptions.FormatError' on a mismatch. - formats.PATH_SCHEMA.check_match(filename) + # Ensure 'filename' corresponds to 'PATH_SCHEMA'. + # Raise 'securesystemslib.exceptions.FormatError' on a mismatch. + formats.PATH_SCHEMA.check_match(filename) - if storage_backend is None: - storage_backend = FilesystemBackend() + if storage_backend is None: + storage_backend = FilesystemBackend() - # Split 'filename' into head and tail, check if head exists. - directory = os.path.split(filename)[0] + # Split 'filename' into head and tail, check if head exists. + directory = os.path.split(filename)[0] - # Check for cases where filename is without directory like 'file.txt' - # and as a result directory is an empty string - if directory: - storage_backend.create_folder(directory) + # Check for cases where filename is without directory like 'file.txt' + # and as a result directory is an empty string + if directory: + storage_backend.create_folder(directory) def file_in_confined_directories( - filepath: str, - confined_directories: Sequence[str] + filepath: str, confined_directories: Sequence[str] ) -> bool: - """ - - Check if the directory containing 'filepath' is in the list/tuple of - 'confined_directories'. - - - filepath: - A string representing the path of a file. The following example path - strings are viewed as files and not directories: 'a/b/c', 'a/b/c.txt'. - - confined_directories: - A sequence (such as list, or tuple) of directory strings. - - - securesystemslib.exceptions.FormatError: On incorrect format of the input. - - - Boolean. True, if path is either the empty string - or in 'confined_paths'; False, otherwise. - """ - - # Do the arguments have the correct format? - # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch. - formats.PATH_SCHEMA.check_match(filepath) - formats.NAMES_SCHEMA.check_match(confined_directories) - - for confined_directory in confined_directories: - # The empty string (arbitrarily chosen) signifies the client is confined - # to all directories and subdirectories. No need to check 'filepath'. - if confined_directory == '': - return True - - # Normalized paths needed, to account for up-level references, etc. - # callers have the option of setting the list of directories in - # 'confined_directories'. - filepath = os.path.normpath(filepath) - confined_directory = os.path.normpath(confined_directory) - - # A caller may restrict himself to specific directories on the - # remote repository. The list of paths in 'confined_path', not including - # each path's subdirectories, are the only directories the client will - # download targets from. - if os.path.dirname(filepath) == confined_directory: - return True - - return False + """ + + Check if the directory containing 'filepath' is in the list/tuple of + 'confined_directories'. + + + filepath: + A string representing the path of a file. The following example path + strings are viewed as files and not directories: 'a/b/c', 'a/b/c.txt'. + + confined_directories: + A sequence (such as list, or tuple) of directory strings. + + + securesystemslib.exceptions.FormatError: On incorrect format of the input. + + + Boolean. True, if path is either the empty string + or in 'confined_paths'; False, otherwise. + """ + + # Do the arguments have the correct format? + # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch. + formats.PATH_SCHEMA.check_match(filepath) + formats.NAMES_SCHEMA.check_match(confined_directories) + + for confined_directory in confined_directories: + # The empty string (arbitrarily chosen) signifies the client is confined + # to all directories and subdirectories. No need to check 'filepath'. + if confined_directory == "": + return True + + # Normalized paths needed, to account for up-level references, etc. + # callers have the option of setting the list of directories in + # 'confined_directories'. + filepath = os.path.normpath(filepath) + confined_directory = os.path.normpath(confined_directory) + + # A caller may restrict himself to specific directories on the + # remote repository. The list of paths in 'confined_path', not including + # each path's subdirectories, are the only directories the client will + # download targets from. + if os.path.dirname(filepath) == confined_directory: + return True + + return False def load_json_string(data: Union[str, bytes]) -> Any: - """ - - Deserialize 'data' (JSON string) to a Python object. + """ + + Deserialize 'data' (JSON string) to a Python object. - - data: - A JSON string. + + data: + A JSON string. - - securesystemslib.exceptions.Error, if 'data' cannot be deserialized to a - Python object. + + securesystemslib.exceptions.Error, if 'data' cannot be deserialized to a + Python object. - - None. + + None. - - Deserialized object. For example, a dictionary. - """ + + Deserialized object. For example, a dictionary. + """ - deserialized_object = None + deserialized_object = None - try: - deserialized_object = json.loads(data) + try: + deserialized_object = json.loads(data) - except TypeError: - message = 'Invalid JSON string: ' + repr(data) - raise exceptions.Error(message) + except TypeError: + message = "Invalid JSON string: " + repr(data) + raise exceptions.Error(message) # pylint: disable=raise-missing-from - except ValueError: - message = 'Cannot deserialize to a Python object: ' + repr(data) - raise exceptions.Error(message) + except ValueError: + message = "Cannot deserialize to a Python object: " + repr(data) + raise exceptions.Error(message) # pylint: disable=raise-missing-from - else: - return deserialized_object + else: + return deserialized_object def load_json_file( - filepath: str, - storage_backend: Optional[StorageBackendInterface] = None + filepath: str, storage_backend: Optional[StorageBackendInterface] = None ) -> Any: - """ - - Deserialize a JSON object from a file containing the object. + """ + + Deserialize a JSON object from a file containing the object. - - filepath: - Absolute path of JSON file. + + filepath: + Absolute path of JSON file. - storage_backend: - An object which implements - securesystemslib.storage.StorageBackendInterface. When no object is - passed a FilesystemBackend will be instantiated and used. + storage_backend: + An object which implements + securesystemslib.storage.StorageBackendInterface. When no object is + passed a FilesystemBackend will be instantiated and used. - - securesystemslib.exceptions.FormatError: If 'filepath' is improperly - formatted. + + securesystemslib.exceptions.FormatError: If 'filepath' is improperly + formatted. - securesystemslib.exceptions.Error: If 'filepath' cannot be deserialized to - a Python object. + securesystemslib.exceptions.Error: If 'filepath' cannot be deserialized to + a Python object. - securesystemslib.exceptions.StorageError: If file cannot be loaded. + securesystemslib.exceptions.StorageError: If file cannot be loaded. - IOError in case of runtime IO exceptions. + IOError in case of runtime IO exceptions. - - None. + + None. - - Deserialized object. For example, a dictionary. - """ + + Deserialized object. For example, a dictionary. + """ - # Making sure that the format of 'filepath' is a path string. - # securesystemslib.exceptions.FormatError is raised on incorrect format. - formats.PATH_SCHEMA.check_match(filepath) + # Making sure that the format of 'filepath' is a path string. + # securesystemslib.exceptions.FormatError is raised on incorrect format. + formats.PATH_SCHEMA.check_match(filepath) - if storage_backend is None: - storage_backend = FilesystemBackend() + if storage_backend is None: + storage_backend = FilesystemBackend() - deserialized_object = None - with storage_backend.get(filepath) as file_obj: - raw_data = file_obj.read().decode('utf-8') + deserialized_object = None + with storage_backend.get(filepath) as file_obj: + raw_data = file_obj.read().decode("utf-8") - try: - deserialized_object = json.loads(raw_data) + try: + deserialized_object = json.loads(raw_data) - except (ValueError, TypeError): - raise exceptions.Error('Cannot deserialize to a' - ' Python object: ' + filepath) + except (ValueError, TypeError): + raise exceptions.Error( # pylint: disable=raise-missing-from + "Cannot deserialize to a" " Python object: " + filepath + ) - else: - return deserialized_object + else: + return deserialized_object def digests_are_equal(digest1: str, digest2: str) -> bool: - """ - - While protecting against timing attacks, compare the hexadecimal arguments - and determine if they are equal. + """ + + While protecting against timing attacks, compare the hexadecimal arguments + and determine if they are equal. - - digest1: - The first hexadecimal string value to compare. + + digest1: + The first hexadecimal string value to compare. - digest2: - The second hexadecimal string value to compare. + digest2: + The second hexadecimal string value to compare. - - securesystemslib.exceptions.FormatError: If the arguments are improperly - formatted. + + securesystemslib.exceptions.FormatError: If the arguments are improperly + formatted. - - None. + + None. - - Return True if 'digest1' is equal to 'digest2', False otherwise. - """ + + Return True if 'digest1' is equal to 'digest2', False otherwise. + """ - # 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 there is a mismatch. - formats.HEX_SCHEMA.check_match(digest1) - formats.HEX_SCHEMA.check_match(digest2) + # 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 there is a mismatch. + formats.HEX_SCHEMA.check_match(digest1) + formats.HEX_SCHEMA.check_match(digest2) - if len(digest1) != len(digest2): - return False + if len(digest1) != len(digest2): + return False - are_equal = True + are_equal = True - for element in range(len(digest1)): - if digest1[element] != digest2[element]: - are_equal = False + for element in range( # pylint: disable=consider-using-enumerate + len(digest1) + ): + if digest1[element] != digest2[element]: + are_equal = False - return are_equal + return are_equal diff --git a/setup.py b/setup.py index d425e64c..5763dd42 100644 --- a/setup.py +++ b/setup.py @@ -59,52 +59,51 @@ $ export PATH=$PATH:~/.local/bin """ -from setuptools import setup -from setuptools import find_packages +from setuptools import find_packages, setup - -with open('README.rst') as file_object: - long_description = file_object.read() +with open("README.rst") as file_object: + long_description = file_object.read() setup( - name = 'securesystemslib', - version = '0.25.0', - description = 'A library that provides cryptographic and general-purpose' - ' routines for Secure Systems Lab projects at NYU', - license = 'MIT', - long_description = long_description, - long_description_content_type = 'text/x-rst', - author = 'https://www.updateframework.com', - author_email = 'theupdateframework@googlegroups.com', - url = 'https://github.com/secure-systems-lab/securesystemslib', - keywords = 'cryptography, keys, signatures, rsa, ed25519, ecdsa', - classifiers = [ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Natural Language :: English', - 'Operating System :: POSIX', - 'Operating System :: POSIX :: Linux', - 'Operating System :: MacOS :: MacOS X', - 'Operating System :: Microsoft :: Windows', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: Implementation :: CPython', - 'Topic :: Security', - 'Topic :: Software Development' - ], - project_urls = { - 'Source': 'https://github.com/secure-systems-lab/securesystemslib', - 'Issues': 'https://github.com/secure-systems-lab/securesystemslib/issues', - }, - python_requires = "~=3.7", - extras_require = { - 'colors': ['colorama>=0.3.9'], - 'crypto': ['cryptography>=37.0.0'], - 'pynacl': ['pynacl>1.2.0']}, - packages = find_packages(exclude=['tests', 'debian']), - scripts = [] + name="securesystemslib", + version="0.25.0", + description="A library that provides cryptographic and general-purpose" + " routines for Secure Systems Lab projects at NYU", + license="MIT", + long_description=long_description, + long_description_content_type="text/x-rst", + author="https://www.updateframework.com", + author_email="theupdateframework@googlegroups.com", + url="https://github.com/secure-systems-lab/securesystemslib", + keywords="cryptography, keys, signatures, rsa, ed25519, ecdsa", + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Operating System :: POSIX", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: Implementation :: CPython", + "Topic :: Security", + "Topic :: Software Development", + ], + project_urls={ + "Source": "https://github.com/secure-systems-lab/securesystemslib", + "Issues": "https://github.com/secure-systems-lab/securesystemslib/issues", + }, + python_requires="~=3.7", + extras_require={ + "colors": ["colorama>=0.3.9"], + "crypto": ["cryptography>=37.0.0"], + "pynacl": ["pynacl>1.2.0"], + }, + packages=find_packages(exclude=["tests", "debian"]), + scripts=[], ) diff --git a/tests/aggregate_tests.py b/tests/aggregate_tests.py index 43ff22d7..3a2939d0 100755 --- a/tests/aggregate_tests.py +++ b/tests/aggregate_tests.py @@ -26,9 +26,12 @@ import sys import unittest -if __name__ == '__main__': - suite = unittest.TestLoader().discover("tests", top_level_dir=".") - all_tests_passed = unittest.TextTestRunner( - verbosity=1, buffer=True).run(suite).wasSuccessful() - if not all_tests_passed: - sys.exit(1) +if __name__ == "__main__": + suite = unittest.TestLoader().discover("tests", top_level_dir=".") + all_tests_passed = ( + unittest.TextTestRunner(verbosity=1, buffer=True) + .run(suite) + .wasSuccessful() + ) + if not all_tests_passed: + sys.exit(1) diff --git a/tests/check_gpg_available.py b/tests/check_gpg_available.py index 4a38f2d8..8b95cc42 100644 --- a/tests/check_gpg_available.py +++ b/tests/check_gpg_available.py @@ -32,11 +32,12 @@ class TestGpgAvailable(unittest.TestCase): - """Test that securesystemslib finds some GPG executable in the environment.""" + """Test that securesystemslib finds some GPG executable in the environment.""" + + def test_gpg_available(self): + """Test that GPG is available.""" + self.assertTrue(securesystemslib.gpg.constants.have_gpg()) - def test_gpg_available(self): - """Test that GPG is available.""" - self.assertTrue(securesystemslib.gpg.constants.have_gpg()) if __name__ == "__main__": - unittest.main(verbosity=1, buffer=True) + unittest.main(verbosity=1, buffer=True) diff --git a/tests/check_public_interfaces.py b/tests/check_public_interfaces.py index e42ad89e..8c6190f7 100644 --- a/tests/check_public_interfaces.py +++ b/tests/check_public_interfaces.py @@ -29,8 +29,8 @@ when explicitly invoked. """ -import inspect -import json +import inspect # pylint: disable=unused-import +import json # pylint: disable=unused-import import os import shutil import sys @@ -38,225 +38,272 @@ import unittest if sys.version_info >= (3, 3): - import unittest.mock as mock + import unittest.mock as mock # pylint: disable=consider-using-from-import else: - import mock - -import securesystemslib.exceptions -import securesystemslib.gpg.constants -import securesystemslib.gpg.functions -import securesystemslib.gpg.util -import securesystemslib.interface -import securesystemslib.keys - - - -class TestPublicInterfaces(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.temp_dir = tempfile.mkdtemp(dir=os.getcwd()) - - @classmethod - 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(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'): - securesystemslib.interface.import_rsa_privatekey_from_file( - path) - - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError): - securesystemslib.interface._generate_and_write_ed25519_keypair( - 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: - f.write('{}') - securesystemslib.interface.import_ed25519_privatekey_from_file( - path, 'pw') - - with self.assertRaises( - securesystemslib.exceptions.UnsupportedLibraryError): - securesystemslib.interface._generate_and_write_ecdsa_keypair( - 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: - 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= (3, 3): - from unittest.mock import patch # pylint: disable=no-name-in-module,import-error + from unittest.mock import ( # pylint: disable=no-name-in-module,import-error + patch, + ) else: - from mock import patch # pylint: disable=import-error + from mock import patch # pylint: disable=import-error -from copy import deepcopy +# pylint: disable=wrong-import-position from collections import OrderedDict +from copy import deepcopy -import cryptography.hazmat.primitives.serialization as serialization -import cryptography.hazmat.backends as backends +import cryptography.hazmat.backends as backends # pylint: disable=consider-using-from-import import cryptography.hazmat.primitives.hashes as hashing - -from securesystemslib import exceptions -from securesystemslib import process -from securesystemslib.gpg.functions import (create_signature, export_pubkey, - verify_signature, export_pubkeys) -from securesystemslib.gpg.util import (get_version, is_version_fully_supported, - get_hashing_class, parse_packet_header, parse_subpacket_header, Version) -from securesystemslib.gpg.rsa import create_pubkey as rsa_create_pubkey +import cryptography.hazmat.primitives.serialization as serialization # pylint: disable=consider-using-from-import + +from securesystemslib import exceptions, process +from securesystemslib.formats import ANY_PUBKEY_DICT_SCHEMA, GPG_PUBKEY_SCHEMA +from securesystemslib.gpg.common import ( + _assign_certified_key_info, + _get_verified_subkeys, + get_pubkey_bundle, + parse_pubkey_bundle, + parse_pubkey_payload, + parse_signature_packet, +) +from securesystemslib.gpg.constants import ( + PACKET_TYPE_PRIMARY_KEY, + PACKET_TYPE_SUB_KEY, + PACKET_TYPE_USER_ATTR, + PACKET_TYPE_USER_ID, + SHA1, + SHA256, + SHA512, + gpg_export_pubkey_command, + have_gpg, +) from securesystemslib.gpg.dsa import create_pubkey as dsa_create_pubkey -from securesystemslib.gpg.eddsa import create_pubkey as eddsa_create_pubkey + +# pylint: disable=unused-import from securesystemslib.gpg.eddsa import ED25519_SIG_LENGTH -from securesystemslib.gpg.common import (parse_pubkey_payload, - parse_pubkey_bundle, get_pubkey_bundle, _assign_certified_key_info, - _get_verified_subkeys, parse_signature_packet) -from securesystemslib.gpg.constants import (SHA1, SHA256, SHA512, - gpg_export_pubkey_command, PACKET_TYPE_PRIMARY_KEY, PACKET_TYPE_USER_ID, - PACKET_TYPE_USER_ATTR, PACKET_TYPE_SUB_KEY, have_gpg) -from securesystemslib.gpg.exceptions import (PacketParsingError, - PacketVersionNotSupportedError, SignatureAlgorithmNotSupportedError, - KeyNotFoundError, CommandError, KeyExpirationError) -from securesystemslib.formats import (GPG_PUBKEY_SCHEMA, - ANY_PUBKEY_DICT_SCHEMA) +from securesystemslib.gpg.eddsa import create_pubkey as eddsa_create_pubkey + +# pylint: enable=unused-import +from securesystemslib.gpg.exceptions import ( + CommandError, + KeyExpirationError, + KeyNotFoundError, + PacketParsingError, + PacketVersionNotSupportedError, + SignatureAlgorithmNotSupportedError, +) +from securesystemslib.gpg.functions import ( + create_signature, + export_pubkey, + export_pubkeys, + verify_signature, +) +from securesystemslib.gpg.rsa import create_pubkey as rsa_create_pubkey +from securesystemslib.gpg.util import ( + Version, + get_hashing_class, + get_version, + is_version_fully_supported, + parse_packet_header, + parse_subpacket_header, +) + +# pylint: enable=wrong-import-position class GPGTestUtils: """GPG Test utility class""" @staticmethod - def ignore_not_found_error(function, path, exc_info): + def ignore_not_found_error( + function, path, exc_info + ): # pylint: disable=unused-argument,unused-argument """Callback that ignores FileNotFoundError""" _, error, _ = exc_info if not isinstance(error, FileNotFoundError): @@ -73,774 +110,881 @@ def ignore_not_found_error(function, path, exc_info): @unittest.skipIf(not have_gpg(), "gpg not found") class TestUtil(unittest.TestCase): - """Test util functions. """ - - def test_version_utils_return_types(self): - """Run dummy tests for coverage. """ - self.assertTrue(isinstance(get_version(), Version)) - self.assertTrue(isinstance(is_version_fully_supported(), bool)) - - def test_version_utils_error(self): - """Run dummy tests for coverage. """ - with patch('securesystemslib.gpg.constants.have_gpg', return_value=False): - with self.assertRaises(exceptions.UnsupportedLibraryError): - get_version() - - def test_get_hashing_class(self): - # Assert return expected hashing class - expected_hashing_class = [hashing.SHA1, hashing.SHA256, hashing.SHA512] - for idx, hashing_id in enumerate([SHA1, SHA256, SHA512]): - result = get_hashing_class(hashing_id) - self.assertEqual(result, expected_hashing_class[idx]) - - # Assert raises ValueError with non-supported hashing id - with self.assertRaises(ValueError): - get_hashing_class("bogus_hashing_id") - - def test_parse_packet_header(self): - """Test parse_packet_header with manually crafted data. """ - data_list = [ - ## New format packet length with mock packet type 100001 - # one-octet length, header len: 2, body len: 0 to 191 - [0b01100001, 0], - [0b01100001, 191], - # two-octet length, header len: 3, body len: 192 to 8383 - [0b01100001, 192, 0], - [0b01100001, 223, 255], - # five-octet length, header len: 6, body len: 0 to 4,294,967,295 - [0b01100001, 255, 0, 0, 0, 0], - [0b01100001, 255, 255, 255, 255, 255], - - ## Old format packet lengths with mock packet type 1001 - # one-octet length, header len: 2, body len: 0 to 255 - [0b00100100, 0], - [0b00100100, 255], - # two-octet length, header len: 3, body len: 0 to 65,535 - [0b00100101, 0, 0], - [0b00100101, 255, 255], - # four-octet length, header len: 5, body len: 0 to 4,294,967,295 - [0b00100110, 0, 0, 0, 0, 0], - [0b00100110, 255, 255, 255, 255, 255], - ] - - # packet_type | header_len | body_len | packet_len - expected = [ - (33, 2, 0, 2), - (33, 2, 191, 193), - (33, 3, 192, 195), - (33, 3, 8383, 8386), - (33, 6, 0, 6), - (33, 6, 4294967295, 4294967301), - (9, 2, 0, 2), - (9, 2, 255, 257), - (9, 3, 0, 3), - (9, 3, 65535, 65538), - (9, 5, 0, 5), - (9, 5, 4294967295, 4294967300), - ] - - for idx, data in enumerate(data_list): - result = parse_packet_header(bytearray(data)) - self.assertEqual(result, expected[idx]) - - - # New Format Packet Lengths with Partial Body Lengths range - for second_octet in [224, 254]: - with self.assertRaises(PacketParsingError): - parse_packet_header(bytearray([0b01100001, second_octet])) - - # Old Format Packet Lengths with indeterminate length (length type 3) - with self.assertRaises(PacketParsingError): - parse_packet_header(bytearray([0b00100111])) - - # Get expected type - parse_packet_header(bytearray([0b01100001, 0]), expected_type=33) - - # Raise with unexpected type - with self.assertRaises(PacketParsingError): - parse_packet_header(bytearray([0b01100001, 0]), expected_type=34) - - - def test_parse_subpacket_header(self): - """Test parse_subpacket_header with manually crafted data. """ - # All items until last item encode the length of the subpacket, - # the last item encodes the mock subpacket type. - data_list = [ - # length of length 1, subpacket length 0 to 191 - [0, 33], # NOTE: Nonsense 0-length - [191, 33], - # # length of length 2, subpacket length 192 to 16,319 - [192, 0, 33], - [254, 255, 33], - # # length of length 5, subpacket length 0 to 4,294,967,295 - [255, 0, 0, 0, 0, 33], # NOTE: Nonsense 0-length - [255, 255, 255, 255, 255, 33], - ] - # packet_type | header_len | body_len | packet_len - expected = [ - (33, 2, -1, 1), # NOTE: Nonsense negative payload - (33, 2, 190, 192), - (33, 3, 191, 194), - (33, 3, 16318, 16321), - (33, 6, -1, 5), # NOTE: Nonsense negative payload - (33, 6, 4294967294, 4294967300) - ] - - for idx, data in enumerate(data_list): - result = parse_subpacket_header(bytearray(data)) - self.assertEqual(result, expected[idx]) + """Test util functions.""" + + def test_version_utils_return_types(self): + """Run dummy tests for coverage.""" + self.assertTrue(isinstance(get_version(), Version)) + self.assertTrue(isinstance(is_version_fully_supported(), bool)) + + def test_version_utils_error(self): + """Run dummy tests for coverage.""" + with patch( + "securesystemslib.gpg.constants.have_gpg", return_value=False + ): + with self.assertRaises(exceptions.UnsupportedLibraryError): + get_version() + + def test_get_hashing_class(self): + # Assert return expected hashing class + expected_hashing_class = [hashing.SHA1, hashing.SHA256, hashing.SHA512] + for idx, hashing_id in enumerate([SHA1, SHA256, SHA512]): + result = get_hashing_class(hashing_id) + self.assertEqual(result, expected_hashing_class[idx]) + + # Assert raises ValueError with non-supported hashing id + with self.assertRaises(ValueError): + get_hashing_class("bogus_hashing_id") + + def test_parse_packet_header(self): + """Test parse_packet_header with manually crafted data.""" + data_list = [ + ## New format packet length with mock packet type 100001 + # one-octet length, header len: 2, body len: 0 to 191 + [0b01100001, 0], + [0b01100001, 191], + # two-octet length, header len: 3, body len: 192 to 8383 + [0b01100001, 192, 0], + [0b01100001, 223, 255], + # five-octet length, header len: 6, body len: 0 to 4,294,967,295 + [0b01100001, 255, 0, 0, 0, 0], + [0b01100001, 255, 255, 255, 255, 255], + ## Old format packet lengths with mock packet type 1001 + # one-octet length, header len: 2, body len: 0 to 255 + [0b00100100, 0], + [0b00100100, 255], + # two-octet length, header len: 3, body len: 0 to 65,535 + [0b00100101, 0, 0], + [0b00100101, 255, 255], + # four-octet length, header len: 5, body len: 0 to 4,294,967,295 + [0b00100110, 0, 0, 0, 0, 0], + [0b00100110, 255, 255, 255, 255, 255], + ] + + # packet_type | header_len | body_len | packet_len + expected = [ + (33, 2, 0, 2), + (33, 2, 191, 193), + (33, 3, 192, 195), + (33, 3, 8383, 8386), + (33, 6, 0, 6), + (33, 6, 4294967295, 4294967301), + (9, 2, 0, 2), + (9, 2, 255, 257), + (9, 3, 0, 3), + (9, 3, 65535, 65538), + (9, 5, 0, 5), + (9, 5, 4294967295, 4294967300), + ] + + for idx, data in enumerate(data_list): + result = parse_packet_header(bytearray(data)) + self.assertEqual(result, expected[idx]) + + # New Format Packet Lengths with Partial Body Lengths range + for second_octet in [224, 254]: + with self.assertRaises(PacketParsingError): + parse_packet_header(bytearray([0b01100001, second_octet])) + + # Old Format Packet Lengths with indeterminate length (length type 3) + with self.assertRaises(PacketParsingError): + parse_packet_header(bytearray([0b00100111])) + + # Get expected type + parse_packet_header(bytearray([0b01100001, 0]), expected_type=33) + + # Raise with unexpected type + with self.assertRaises(PacketParsingError): + parse_packet_header(bytearray([0b01100001, 0]), expected_type=34) + + def test_parse_subpacket_header(self): + """Test parse_subpacket_header with manually crafted data.""" + # All items until last item encode the length of the subpacket, + # the last item encodes the mock subpacket type. + data_list = [ + # length of length 1, subpacket length 0 to 191 + [0, 33], # NOTE: Nonsense 0-length + [191, 33], + # # length of length 2, subpacket length 192 to 16,319 + [192, 0, 33], + [254, 255, 33], + # # length of length 5, subpacket length 0 to 4,294,967,295 + [255, 0, 0, 0, 0, 33], # NOTE: Nonsense 0-length + [255, 255, 255, 255, 255, 33], + ] + # packet_type | header_len | body_len | packet_len + expected = [ + (33, 2, -1, 1), # NOTE: Nonsense negative payload + (33, 2, 190, 192), + (33, 3, 191, 194), + (33, 3, 16318, 16321), + (33, 6, -1, 5), # NOTE: Nonsense negative payload + (33, 6, 4294967294, 4294967300), + ] + + for idx, data in enumerate(data_list): + result = parse_subpacket_header(bytearray(data)) + self.assertEqual(result, expected[idx]) @unittest.skipIf(not have_gpg(), "gpg not found") class TestCommon(unittest.TestCase): - """Test common functions of the securesystemslib.gpg module. """ - @classmethod - def setUpClass(self): - gpg_keyring_path = os.path.join( - os.path.dirname(os.path.realpath(__file__)), "gpg_keyrings", "rsa") - homearg = "--homedir {}".format(gpg_keyring_path).replace("\\", "/") - - # Load test raw public key bundle from rsa keyring, used to construct - # erroneous gpg data in tests below. - keyid = "F557D0FF451DEF45372591429EA70BD13D883381" - cmd = gpg_export_pubkey_command(keyid=keyid, homearg=homearg) - proc = process.run(cmd, stdout=process.PIPE, stderr=process.PIPE) - self.raw_key_data = proc.stdout - self.raw_key_bundle = parse_pubkey_bundle(self.raw_key_data) - - # Export pubkey bundle with expired key for key expiration tests - keyid = "E8AC80C924116DABB51D4B987CB07D6D2C199C7C" - cmd = gpg_export_pubkey_command(keyid=keyid, homearg=homearg) - proc = process.run(cmd, stdout=process.PIPE, stderr=process.PIPE) - self.raw_expired_key_bundle = parse_pubkey_bundle(proc.stdout) - - def test_parse_pubkey_payload_errors(self): - """ Test parse_pubkey_payload errors with manually crafted data. """ - # passed data | expected error | expected error message - test_data = [ - (None, ValueError, "empty pubkey"), - (bytearray([0x03]), PacketVersionNotSupportedError, - "packet version '3' not supported"), - (bytearray([0x04, 0, 0, 0, 0, 255]), SignatureAlgorithmNotSupportedError, - "Signature algorithm '255' not supported") - ] - - for data, error, error_str in test_data: - with self.assertRaises(error) as ctx: - parse_pubkey_payload(data) - self.assertTrue(error_str in str(ctx.exception)) - - - def test_parse_pubkey_bundle_errors(self): - """Test parse_pubkey_bundle errors with manually crafted data partially - based on real gpg key data (see self.raw_key_bundle). """ - # Extract sample (legitimate) user ID packet and pass as first packet to - # raise first packet must be primary key error - user_id_packet = list(self.raw_key_bundle[PACKET_TYPE_USER_ID].keys())[0] - # Extract sample (legitimate) primary key packet and pass as first two - # packets to raise unexpected second primary key error - primary_key_packet = self.raw_key_bundle[PACKET_TYPE_PRIMARY_KEY]["packet"] - # Create incomplete packet to re-raise header parsing IndexError as - # PacketParsingError - incomplete_packet = bytearray([0b01111111]) - - # passed data | expected error message - test_data = [ - (None, "empty gpg data"), - (user_id_packet, "must be a primary key"), - (primary_key_packet + primary_key_packet, "Unexpected primary key"), - (incomplete_packet, "index out of range") - ] - for data, error_str in test_data: - with self.assertRaises(PacketParsingError) as ctx: - parse_pubkey_bundle(data) - self.assertTrue(error_str in str(ctx.exception)) - - # Create empty packet of unsupported type 66 (bit 0-5) and length 0 and - # pass as second packet to provoke skipping of unsupported packet - unsupported_packet = bytearray([0b01111111, 0]) - with patch("securesystemslib.gpg.common.log") as mock_log: - parse_pubkey_bundle(primary_key_packet + unsupported_packet) - self.assertTrue("Ignoring gpg key packet '63'" in - mock_log.info.call_args[0][0]) - - - def test_parse_pubkey_bundle(self): - """Assert presence of packets expected returned from `parse_pubkey_bundle` + """Test common functions of the securesystemslib.gpg module.""" + + @classmethod + def setUpClass(self): # pylint: disable=bad-classmethod-argument + gpg_keyring_path = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "gpg_keyrings", "rsa" + ) + homearg = ( + "--homedir {}".format( # pylint: disable=consider-using-f-string + gpg_keyring_path + ).replace("\\", "/") + ) + + # Load test raw public key bundle from rsa keyring, used to construct + # erroneous gpg data in tests below. + keyid = "F557D0FF451DEF45372591429EA70BD13D883381" + cmd = gpg_export_pubkey_command(keyid=keyid, homearg=homearg) + proc = process.run(cmd, stdout=process.PIPE, stderr=process.PIPE) + self.raw_key_data = proc.stdout + self.raw_key_bundle = parse_pubkey_bundle(self.raw_key_data) + + # Export pubkey bundle with expired key for key expiration tests + keyid = "E8AC80C924116DABB51D4B987CB07D6D2C199C7C" + cmd = gpg_export_pubkey_command(keyid=keyid, homearg=homearg) + proc = process.run(cmd, stdout=process.PIPE, stderr=process.PIPE) + self.raw_expired_key_bundle = parse_pubkey_bundle(proc.stdout) + + def test_parse_pubkey_payload_errors(self): + """Test parse_pubkey_payload errors with manually crafted data.""" + # passed data | expected error | expected error message + test_data = [ + (None, ValueError, "empty pubkey"), + ( + bytearray([0x03]), + PacketVersionNotSupportedError, + "packet version '3' not supported", + ), + ( + bytearray([0x04, 0, 0, 0, 0, 255]), + SignatureAlgorithmNotSupportedError, + "Signature algorithm '255' not supported", + ), + ] + + for data, error, error_str in test_data: + with self.assertRaises(error) as ctx: + parse_pubkey_payload(data) + self.assertTrue(error_str in str(ctx.exception)) + + def test_parse_pubkey_bundle_errors(self): + """Test parse_pubkey_bundle errors with manually crafted data partially + based on real gpg key data (see self.raw_key_bundle).""" + # Extract sample (legitimate) user ID packet and pass as first packet to + # raise first packet must be primary key error + user_id_packet = list(self.raw_key_bundle[PACKET_TYPE_USER_ID].keys())[ + 0 + ] + # Extract sample (legitimate) primary key packet and pass as first two + # packets to raise unexpected second primary key error + primary_key_packet = self.raw_key_bundle[PACKET_TYPE_PRIMARY_KEY][ + "packet" + ] + # Create incomplete packet to re-raise header parsing IndexError as + # PacketParsingError + incomplete_packet = bytearray([0b01111111]) + + # passed data | expected error message + test_data = [ + (None, "empty gpg data"), + (user_id_packet, "must be a primary key"), + (primary_key_packet + primary_key_packet, "Unexpected primary key"), + (incomplete_packet, "index out of range"), + ] + for data, error_str in test_data: + with self.assertRaises(PacketParsingError) as ctx: + parse_pubkey_bundle(data) + self.assertTrue(error_str in str(ctx.exception)) + + # Create empty packet of unsupported type 66 (bit 0-5) and length 0 and + # pass as second packet to provoke skipping of unsupported packet + unsupported_packet = bytearray([0b01111111, 0]) + with patch("securesystemslib.gpg.common.log") as mock_log: + parse_pubkey_bundle(primary_key_packet + unsupported_packet) + self.assertTrue( + "Ignoring gpg key packet '63'" in mock_log.info.call_args[0][0] + ) + + def test_parse_pubkey_bundle(self): + """Assert presence of packets expected returned from `parse_pubkey_bundle` for specific test key). See ``` gpg --homedir tests/gpg_keyrings/rsa/ --export 9EA70BD13D883381 | \ gpg --list-packets ``` """ - # Expect parsed primary key matching GPG_PUBKEY_SCHEMA - self.assertTrue(GPG_PUBKEY_SCHEMA.matches( - self.raw_key_bundle[PACKET_TYPE_PRIMARY_KEY]["key"])) - - # Parse corresponding raw packet for comparison - _, header_len, _, _ = parse_packet_header( - self.raw_key_bundle[PACKET_TYPE_PRIMARY_KEY]["packet"]) - - # pylint: disable=unsubscriptable-object - parsed_raw_packet = parse_pubkey_payload(bytearray( - self.raw_key_bundle[PACKET_TYPE_PRIMARY_KEY]["packet"][header_len:])) - - # And compare - self.assertDictEqual( - self.raw_key_bundle[PACKET_TYPE_PRIMARY_KEY]["key"], - parsed_raw_packet) - - # Expect one primary key signature (revocation signature) - self.assertEqual( - len(self.raw_key_bundle[PACKET_TYPE_PRIMARY_KEY]["signatures"]), 1) - - # Expect one User ID packet, one User Attribute packet and one Subkey, - # each with correct data - for _type in [PACKET_TYPE_USER_ID, PACKET_TYPE_USER_ATTR, - PACKET_TYPE_SUB_KEY]: - # Of each type there is only one packet - self.assertTrue(len(self.raw_key_bundle[_type]) == 1) - # The raw packet is stored as key in the per-packet type collection - raw_packet = next(iter(self.raw_key_bundle[_type])) - # Its values are the raw packets header and body length - self.assertEqual(len(raw_packet), - self.raw_key_bundle[_type][raw_packet]["header_len"] + - self.raw_key_bundle[_type][raw_packet]["body_len"]) - # and one self-signature - self.assertEqual( - len(self.raw_key_bundle[_type][raw_packet]["signatures"]), 1) - - - def test_assign_certified_key_info_errors(self): - """Test _assign_certified_key_info errors with manually crafted data - based on real gpg key data (see self.raw_key_bundle). """ - - # Replace legitimate user certifacte with a bogus packet - wrong_cert_bundle = deepcopy(self.raw_key_bundle) - packet, packet_data = wrong_cert_bundle[PACKET_TYPE_USER_ID].popitem() - packet_data["signatures"] = [bytearray([0b01111111, 0])] - wrong_cert_bundle[PACKET_TYPE_USER_ID][packet] = packet_data - - # Replace primary key id with a non-associated keyid - wrong_keyid_bundle = deepcopy(self.raw_key_bundle) - wrong_keyid_bundle[PACKET_TYPE_PRIMARY_KEY]["key"]["keyid"] = \ - "8465A1E2E0FB2B40ADB2478E18FB3F537E0C8A17" - - # Remove a byte in user id packet to make signature verification fail - invalid_cert_bundle = deepcopy(self.raw_key_bundle) - packet, packet_data = invalid_cert_bundle[PACKET_TYPE_USER_ID].popitem() - packet = packet[:-1] - invalid_cert_bundle[PACKET_TYPE_USER_ID][packet] = packet_data - - test_data = [ - # Skip and log parse_signature_packet error - (wrong_cert_bundle, "Expected packet 2, but got 63 instead"), - # Skip and log signature packet that doesn't match primary key id - (wrong_keyid_bundle, "Ignoring User ID certificate issued by"), - # Skip and log invalid signature - (invalid_cert_bundle, "Ignoring invalid User ID self-certificate") - ] - - for bundle, expected_msg in test_data: - with patch("securesystemslib.gpg.common.log") as mock_log: - _assign_certified_key_info(bundle) - msg = str(mock_log.info.call_args[0][0]) - self.assertTrue(expected_msg in msg, - "'{}' not in '{}'".format(expected_msg, msg)) - - - def test_assign_certified_key_info_expiration(self): - """Test assignment of key expiration date in - gpg.common._assign_certified_key_info using real gpg data (with ambiguity - resolution / prioritization). - - # FIXME: Below tests are missing proper assertions for which User ID - self-certificate is considered for the expiration date. Reasons are: - - gpg does not let you (easily) modify individual expiration dates of User - IDs (changing one changes all), hence we cannot assert the chosen packet - by the particular date - - _assign_certified_key_info first verifies all self-certificates and then - only considers successfully verified ones, hence we cannot modify the - certificate data, before passing it to _assign_certified_key_info - - IMO the best solution is a better separation of concerns, e.g. separate - self-certificate verification and packet prioritization. - - """ - # Test ambiguity resolution scheme with 3 User IDs - # :user ID packet: "Test Expiration I " - # :user ID packet: "Test Expiration II " - # :user ID packet: "Test Expiration III " - # User ID packets are ordered by their creation time in ascending order. - # "Test Expiration II" has the primary user ID flag set and therefor has - # the highest priority. - key = _assign_certified_key_info(self.raw_expired_key_bundle) - self.assertTrue(key["validity_period"] == 87901) # ~ 1 day - - # Test ambiguity resolution scheme with 2 User IDs - # :user ID packet: "Test Expiration III " - # :user ID packet: "Test Expiration I " - # User ID packets are ordered by their creation time in descending order. - # Neither packet has the primary user ID flag set. - # "Test Expiration III" has the highest priority. - raw_key_bundle = deepcopy(self.raw_expired_key_bundle) - user_id_items = list(reversed(raw_key_bundle[PACKET_TYPE_USER_ID].items())) - del user_id_items[1] - raw_key_bundle[PACKET_TYPE_USER_ID] = OrderedDict(user_id_items) - key = _assign_certified_key_info(raw_key_bundle) - self.assertTrue(key["validity_period"] == 87901) # ~ 1 day - - - def test_get_verified_subkeys_errors(self): - """Test _get_verified_subkeys errors with manually crafted data based on - real gpg key data (see self.raw_key_bundle). """ - - # Tamper with subkey (change version number) to trigger key parsing error - bad_subkey_bundle = deepcopy(self.raw_key_bundle) - packet, packet_data = bad_subkey_bundle[PACKET_TYPE_SUB_KEY].popitem() - packet = bytes(packet[:packet_data["header_len"]] + - bytearray([0x03]) + packet[packet_data["header_len"]+1:]) - bad_subkey_bundle[PACKET_TYPE_SUB_KEY][packet] = packet_data - - # Add bogus sig to trigger sig parsing error - wrong_sig_bundle = deepcopy(self.raw_key_bundle) - packet, packet_data = wrong_sig_bundle[PACKET_TYPE_SUB_KEY].popitem() - # NOTE: We can't only pass the bogus sig, because that would also trigger - # the not enough sigs error (see not_enough_sigs_bundle) and mock only - # lets us assert for the most recent log statement - packet_data["signatures"].append(bytearray([0b01111111, 0])) - wrong_sig_bundle[PACKET_TYPE_SUB_KEY][packet] = packet_data - - # Remove sigs to trigger not enough sigs error - not_enough_sigs_bundle = deepcopy(self.raw_key_bundle) - packet, packet_data = not_enough_sigs_bundle[PACKET_TYPE_SUB_KEY].popitem() - packet_data["signatures"] = [] - not_enough_sigs_bundle[PACKET_TYPE_SUB_KEY][packet] = packet_data - - # Duplicate sig to trigger wrong amount signatures - too_many_sigs_bundle = deepcopy(self.raw_key_bundle) - packet, packet_data = too_many_sigs_bundle[PACKET_TYPE_SUB_KEY].popitem() - packet_data["signatures"] = packet_data["signatures"] * 2 - too_many_sigs_bundle[PACKET_TYPE_SUB_KEY][packet] = packet_data - - # Tamper with primary key to trigger signature verification error - invalid_sig_bundle = deepcopy(self.raw_key_bundle) - invalid_sig_bundle[PACKET_TYPE_PRIMARY_KEY]["packet"] = \ - invalid_sig_bundle[PACKET_TYPE_PRIMARY_KEY]["packet"][:-1] - - - test_data = [ - (bad_subkey_bundle, "Pubkey packet version '3' not supported"), - (wrong_sig_bundle, "Expected packet 2, but got 63 instead"), - (not_enough_sigs_bundle, "wrong amount of key binding signatures (0)"), - (too_many_sigs_bundle, "wrong amount of key binding signatures (2)"), - (invalid_sig_bundle, "invalid key binding signature"), - ] - - for bundle, expected_msg in test_data: - with patch("securesystemslib.gpg.common.log") as mock_log: - _get_verified_subkeys(bundle) - msg = str(mock_log.info.call_args[0][0]) - self.assertTrue(expected_msg in msg, - "'{}' not in '{}'".format(expected_msg, msg)) - - - def test_get_verified_subkeys(self): - """Test correct assignment of subkey expiration date in - gpg.common._get_verified_subkeys using real gpg data. """ - subkeys = _get_verified_subkeys(self.raw_expired_key_bundle) - # Test subkey with validity period 175451, i.e. ~ 2 days - self.assertTrue(subkeys["0ce427fa3f0f50bc83a4a760ed95e1581691db4d"].get( - "validity_period") == 175451) - - # Test subkey without validity period, i.e. it does not expire - self.assertTrue(subkeys["70cfabf1e2f1dc60ac5c7bca10cd20d3d5bcb6ef"].get( - "validity_period") == None) - - - def test_get_pubkey_bundle_errors(self): - """Test correct error raising in get_pubkey_bundle. """ - # Call without key data - with self.assertRaises(KeyNotFoundError): - get_pubkey_bundle(None, "deadbeef") - - # Pass wrong keyid with valid gpg data to trigger KeyNotFoundError. - not_associated_keyid = "8465A1E2E0FB2B40ADB2478E18FB3F537E0C8A17" - with self.assertRaises(KeyNotFoundError): - get_pubkey_bundle(self.raw_key_data, not_associated_keyid) - - - def test_parse_signature_packet_errors(self): - """Test parse_signature_packet errors with manually crafted data. """ - - # passed data | expected error message - test_data = [ - (bytearray([0b01000010, 1, 255]), - "Signature version '255' not supported"), - (bytearray([0b01000010, 2, 4, 255]), - "Signature type '255' not supported"), - (bytearray([0b01000010, 3, 4, 0, 255]), - "Signature algorithm '255' not supported"), - (bytearray([0b01000010, 4, 4, 0, 1, 255]), - "Hash algorithm '255' not supported"), - ] - for data, expected_error_str in test_data: - with self.assertRaises(ValueError) as ctx: - parse_signature_packet(data) - self.assertTrue(expected_error_str in str(ctx.exception), - "'{}' not in '{}'".format(expected_error_str, str(ctx.exception))) + # Expect parsed primary key matching GPG_PUBKEY_SCHEMA + self.assertTrue( + GPG_PUBKEY_SCHEMA.matches( + self.raw_key_bundle[PACKET_TYPE_PRIMARY_KEY]["key"] + ) + ) + + # Parse corresponding raw packet for comparison + _, header_len, _, _ = parse_packet_header( + self.raw_key_bundle[PACKET_TYPE_PRIMARY_KEY]["packet"] + ) + + # pylint: disable=unsubscriptable-object + parsed_raw_packet = parse_pubkey_payload( + bytearray( + self.raw_key_bundle[PACKET_TYPE_PRIMARY_KEY]["packet"][ + header_len: + ] + ) + ) + + # And compare + self.assertDictEqual( + self.raw_key_bundle[PACKET_TYPE_PRIMARY_KEY]["key"], + parsed_raw_packet, + ) + + # Expect one primary key signature (revocation signature) + self.assertEqual( + len(self.raw_key_bundle[PACKET_TYPE_PRIMARY_KEY]["signatures"]), 1 + ) + + # Expect one User ID packet, one User Attribute packet and one Subkey, + # each with correct data + for _type in [ + PACKET_TYPE_USER_ID, + PACKET_TYPE_USER_ATTR, + PACKET_TYPE_SUB_KEY, + ]: + # Of each type there is only one packet + self.assertTrue(len(self.raw_key_bundle[_type]) == 1) + # The raw packet is stored as key in the per-packet type collection + raw_packet = next(iter(self.raw_key_bundle[_type])) + # Its values are the raw packets header and body length + self.assertEqual( + len(raw_packet), + self.raw_key_bundle[_type][raw_packet]["header_len"] + + self.raw_key_bundle[_type][raw_packet]["body_len"], + ) + # and one self-signature + self.assertEqual( + len(self.raw_key_bundle[_type][raw_packet]["signatures"]), 1 + ) + + def test_assign_certified_key_info_errors(self): + """Test _assign_certified_key_info errors with manually crafted data + based on real gpg key data (see self.raw_key_bundle).""" + + # Replace legitimate user certifacte with a bogus packet + wrong_cert_bundle = deepcopy(self.raw_key_bundle) + packet, packet_data = wrong_cert_bundle[PACKET_TYPE_USER_ID].popitem() + packet_data["signatures"] = [bytearray([0b01111111, 0])] + wrong_cert_bundle[PACKET_TYPE_USER_ID][packet] = packet_data + + # Replace primary key id with a non-associated keyid + wrong_keyid_bundle = deepcopy(self.raw_key_bundle) + wrong_keyid_bundle[PACKET_TYPE_PRIMARY_KEY]["key"][ + "keyid" + ] = "8465A1E2E0FB2B40ADB2478E18FB3F537E0C8A17" + + # Remove a byte in user id packet to make signature verification fail + invalid_cert_bundle = deepcopy(self.raw_key_bundle) + packet, packet_data = invalid_cert_bundle[PACKET_TYPE_USER_ID].popitem() + packet = packet[:-1] + invalid_cert_bundle[PACKET_TYPE_USER_ID][packet] = packet_data + + test_data = [ + # Skip and log parse_signature_packet error + (wrong_cert_bundle, "Expected packet 2, but got 63 instead"), + # Skip and log signature packet that doesn't match primary key id + (wrong_keyid_bundle, "Ignoring User ID certificate issued by"), + # Skip and log invalid signature + (invalid_cert_bundle, "Ignoring invalid User ID self-certificate"), + ] + + for bundle, expected_msg in test_data: + with patch("securesystemslib.gpg.common.log") as mock_log: + _assign_certified_key_info(bundle) + msg = str(mock_log.info.call_args[0][0]) + self.assertTrue( + expected_msg in msg, + "'{}' not in '{}'".format( # pylint: disable=consider-using-f-string + expected_msg, msg + ), + ) + + def test_assign_certified_key_info_expiration(self): + """Test assignment of key expiration date in + gpg.common._assign_certified_key_info using real gpg data (with ambiguity + resolution / prioritization). + + # FIXME: Below tests are missing proper assertions for which User ID + self-certificate is considered for the expiration date. Reasons are: + - gpg does not let you (easily) modify individual expiration dates of User + IDs (changing one changes all), hence we cannot assert the chosen packet + by the particular date + - _assign_certified_key_info first verifies all self-certificates and then + only considers successfully verified ones, hence we cannot modify the + certificate data, before passing it to _assign_certified_key_info + + IMO the best solution is a better separation of concerns, e.g. separate + self-certificate verification and packet prioritization. + + """ + # Test ambiguity resolution scheme with 3 User IDs + # :user ID packet: "Test Expiration I " + # :user ID packet: "Test Expiration II " + # :user ID packet: "Test Expiration III " + # User ID packets are ordered by their creation time in ascending order. + # "Test Expiration II" has the primary user ID flag set and therefor has + # the highest priority. + key = _assign_certified_key_info(self.raw_expired_key_bundle) + self.assertTrue(key["validity_period"] == 87901) # ~ 1 day + + # Test ambiguity resolution scheme with 2 User IDs + # :user ID packet: "Test Expiration III " + # :user ID packet: "Test Expiration I " + # User ID packets are ordered by their creation time in descending order. + # Neither packet has the primary user ID flag set. + # "Test Expiration III" has the highest priority. + raw_key_bundle = deepcopy(self.raw_expired_key_bundle) + user_id_items = list( + reversed(raw_key_bundle[PACKET_TYPE_USER_ID].items()) + ) + del user_id_items[1] + raw_key_bundle[PACKET_TYPE_USER_ID] = OrderedDict(user_id_items) + key = _assign_certified_key_info(raw_key_bundle) + self.assertTrue(key["validity_period"] == 87901) # ~ 1 day + + def test_get_verified_subkeys_errors(self): + """Test _get_verified_subkeys errors with manually crafted data based on + real gpg key data (see self.raw_key_bundle).""" + + # Tamper with subkey (change version number) to trigger key parsing error + bad_subkey_bundle = deepcopy(self.raw_key_bundle) + packet, packet_data = bad_subkey_bundle[PACKET_TYPE_SUB_KEY].popitem() + packet = bytes( + packet[: packet_data["header_len"]] + + bytearray([0x03]) + + packet[packet_data["header_len"] + 1 :] + ) + bad_subkey_bundle[PACKET_TYPE_SUB_KEY][packet] = packet_data + + # Add bogus sig to trigger sig parsing error + wrong_sig_bundle = deepcopy(self.raw_key_bundle) + packet, packet_data = wrong_sig_bundle[PACKET_TYPE_SUB_KEY].popitem() + # NOTE: We can't only pass the bogus sig, because that would also trigger + # the not enough sigs error (see not_enough_sigs_bundle) and mock only + # lets us assert for the most recent log statement + packet_data["signatures"].append(bytearray([0b01111111, 0])) + wrong_sig_bundle[PACKET_TYPE_SUB_KEY][packet] = packet_data + + # Remove sigs to trigger not enough sigs error + not_enough_sigs_bundle = deepcopy(self.raw_key_bundle) + packet, packet_data = not_enough_sigs_bundle[ + PACKET_TYPE_SUB_KEY + ].popitem() + packet_data["signatures"] = [] + not_enough_sigs_bundle[PACKET_TYPE_SUB_KEY][packet] = packet_data + + # Duplicate sig to trigger wrong amount signatures + too_many_sigs_bundle = deepcopy(self.raw_key_bundle) + packet, packet_data = too_many_sigs_bundle[ + PACKET_TYPE_SUB_KEY + ].popitem() + packet_data["signatures"] = packet_data["signatures"] * 2 + too_many_sigs_bundle[PACKET_TYPE_SUB_KEY][packet] = packet_data + + # Tamper with primary key to trigger signature verification error + invalid_sig_bundle = deepcopy(self.raw_key_bundle) + invalid_sig_bundle[PACKET_TYPE_PRIMARY_KEY][ + "packet" + ] = invalid_sig_bundle[PACKET_TYPE_PRIMARY_KEY]["packet"][:-1] + + test_data = [ + (bad_subkey_bundle, "Pubkey packet version '3' not supported"), + (wrong_sig_bundle, "Expected packet 2, but got 63 instead"), + ( + not_enough_sigs_bundle, + "wrong amount of key binding signatures (0)", + ), + ( + too_many_sigs_bundle, + "wrong amount of key binding signatures (2)", + ), + (invalid_sig_bundle, "invalid key binding signature"), + ] + + for bundle, expected_msg in test_data: + with patch("securesystemslib.gpg.common.log") as mock_log: + _get_verified_subkeys(bundle) + msg = str(mock_log.info.call_args[0][0]) + self.assertTrue( + expected_msg in msg, + "'{}' not in '{}'".format( # pylint: disable=consider-using-f-string + expected_msg, msg + ), + ) + + def test_get_verified_subkeys(self): + """Test correct assignment of subkey expiration date in + gpg.common._get_verified_subkeys using real gpg data.""" + subkeys = _get_verified_subkeys(self.raw_expired_key_bundle) + # Test subkey with validity period 175451, i.e. ~ 2 days + self.assertTrue( + subkeys["0ce427fa3f0f50bc83a4a760ed95e1581691db4d"].get( + "validity_period" + ) + == 175451 + ) + + # Test subkey without validity period, i.e. it does not expire + self.assertTrue( + subkeys[ # pylint: disable=singleton-comparison + "70cfabf1e2f1dc60ac5c7bca10cd20d3d5bcb6ef" + ].get("validity_period") + == None + ) + + def test_get_pubkey_bundle_errors(self): + """Test correct error raising in get_pubkey_bundle.""" + # Call without key data + with self.assertRaises(KeyNotFoundError): + get_pubkey_bundle(None, "deadbeef") + + # Pass wrong keyid with valid gpg data to trigger KeyNotFoundError. + not_associated_keyid = "8465A1E2E0FB2B40ADB2478E18FB3F537E0C8A17" + with self.assertRaises(KeyNotFoundError): + get_pubkey_bundle(self.raw_key_data, not_associated_keyid) + + def test_parse_signature_packet_errors(self): + """Test parse_signature_packet errors with manually crafted data.""" + + # passed data | expected error message + test_data = [ + ( + bytearray([0b01000010, 1, 255]), + "Signature version '255' not supported", + ), + ( + bytearray([0b01000010, 2, 4, 255]), + "Signature type '255' not supported", + ), + ( + bytearray([0b01000010, 3, 4, 0, 255]), + "Signature algorithm '255' not supported", + ), + ( + bytearray([0b01000010, 4, 4, 0, 1, 255]), + "Hash algorithm '255' not supported", + ), + ] + for data, expected_error_str in test_data: + with self.assertRaises(ValueError) as ctx: + parse_signature_packet(data) + self.assertTrue( + expected_error_str in str(ctx.exception), + "'{}' not in '{}'".format( # pylint: disable=consider-using-f-string + expected_error_str, str(ctx.exception) + ), + ) @unittest.skipIf(not have_gpg(), "gpg not found") class TestGPGRSA(unittest.TestCase): - """Test signature creation, verification and key export from the gpg - module""" - default_keyid = "8465A1E2E0FB2B40ADB2478E18FB3F537E0C8A17" - signing_subkey_keyid = "C5A0ABE6EC19D0D65F85E2C39BE9DF5131D924E9" - encryption_subkey_keyid = "6A112FD3390B2E53AFC2E57F8FC8E12099AECEEA" - unsupported_subkey_keyid = "611A9B648E16F54E8A7FAD5DA51E8CDF3B06524F" - expired_key_keyid = "E8AC80C924116DABB51D4B987CB07D6D2C199C7C" - - keyid_768C43 = "7B3ABB26B97B655AB9296BD15B0BD02E1C768C43" - - @classmethod - def setUpClass(self): - # Create directory to run the tests without having everything blow up - self.working_dir = os.getcwd() - - # Find demo files - gpg_keyring_path = os.path.join( - os.path.dirname(os.path.realpath(__file__)), "gpg_keyrings", "rsa") - - self.test_dir = os.path.realpath(tempfile.mkdtemp()) - self.gnupg_home = os.path.join(self.test_dir, "rsa") - shutil.copytree(gpg_keyring_path, self.gnupg_home) - os.chdir(self.test_dir) - - - @classmethod - def tearDownClass(self): - """Change back to initial working dir and remove temp test directory. """ - os.chdir(self.working_dir) - shutil.rmtree(self.test_dir, onerror=GPGTestUtils.ignore_not_found_error) - - def test_export_pubkey_error(self): - """Test correct error is raised if function called incorrectly. """ - with self.assertRaises(ValueError): - export_pubkey("not-a-key-id") - - def test_export_pubkey(self): - """ export a public key and make sure the parameters are the right ones: - - since there's very little we can do to check rsa key parameters are right - we pre-exported the public key to an ssh key, which we can load with - cryptography for the sake of comparison """ - - # export our gpg key, using our functions - key_data = export_pubkey(self.default_keyid, homedir=self.gnupg_home) - our_exported_key = rsa_create_pubkey(key_data) - - # load the equivalent ssh key, and make sure that we get the same RSA key - # parameters - ssh_key_basename = "{}.ssh".format(self.default_keyid) - ssh_key_path = os.path.join(self.gnupg_home, ssh_key_basename) - with open(ssh_key_path, "rb") as fp: - keydata = fp.read() - - ssh_key = serialization.load_ssh_public_key(keydata, - backends.default_backend()) - - self.assertEqual(ssh_key.public_numbers().n, - our_exported_key.public_numbers().n) - self.assertEqual(ssh_key.public_numbers().e, - our_exported_key.public_numbers().e) - - subkey_keyids = list(key_data["subkeys"].keys()) - # We export the whole master key bundle which must contain the subkeys - self.assertTrue(self.signing_subkey_keyid.lower() in subkey_keyids) - # Currently we do not exclude encryption subkeys - self.assertTrue(self.encryption_subkey_keyid.lower() in subkey_keyids) - # However we do exclude subkeys, whose algorithm we do not support - self.assertFalse(self.unsupported_subkey_keyid.lower() in subkey_keyids) - - # When passing the subkey keyid we also export the whole keybundle - key_data2 = export_pubkey(self.signing_subkey_keyid, - homedir=self.gnupg_home) - self.assertDictEqual(key_data, key_data2) - - - def test_export_pubkeys(self): - """Test export multiple pubkeys at once. """ - key_dict = export_pubkeys([self.default_keyid, self.keyid_768C43], - homedir=self.gnupg_home) - - ANY_PUBKEY_DICT_SCHEMA.check_match(key_dict) - self.assertListEqual( - sorted([self.default_keyid.lower(), self.keyid_768C43.lower()]), - sorted(key_dict.keys())) - - - def test_gpg_sign_and_verify_object_with_default_key(self): - """Create a signature using the default key on the keyring """ - - test_data = b'test_data' - wrong_data = b'something malicious' - - signature = create_signature(test_data, homedir=self.gnupg_home) - key_data = export_pubkey(self.default_keyid, homedir=self.gnupg_home) - - self.assertTrue(verify_signature(signature, key_data, test_data)) - self.assertFalse(verify_signature(signature, key_data, wrong_data)) - - - def test_gpg_sign_and_verify_object(self): - """Create a signature using a specific key on the keyring """ - - test_data = b'test_data' - wrong_data = b'something malicious' - - signature = create_signature(test_data, keyid=self.default_keyid, - homedir=self.gnupg_home) - key_data = export_pubkey(self.default_keyid, homedir=self.gnupg_home) - self.assertTrue(verify_signature(signature, key_data, test_data)) - self.assertFalse(verify_signature(signature, key_data, wrong_data)) - - - def test_gpg_sign_and_verify_object_default_keyring(self): - """Sign/verify using keyring from envvar. """ - - test_data = b'test_data' - - gnupg_home_backup = os.environ.get("GNUPGHOME") - os.environ["GNUPGHOME"] = self.gnupg_home - - signature = create_signature(test_data, keyid=self.default_keyid) - key_data = export_pubkey(self.default_keyid) - self.assertTrue(verify_signature(signature, key_data, test_data)) - - # Reset GNUPGHOME - if gnupg_home_backup: - os.environ["GNUPGHOME"] = gnupg_home_backup - else: - del os.environ["GNUPGHOME"] - - - def test_create_signature_with_expired_key(self): - """Test signing with expired key raises gpg CommandError. """ - with self.assertRaises(CommandError) as ctx: - create_signature(b"livestock", keyid=self.expired_key_keyid, - homedir=self.gnupg_home) - - expected = "returned non-zero exit status '2'" - self.assertTrue(expected in str(ctx.exception), "{} not in {}".format( - expected, ctx.exception)) - - - def test_verify_signature_with_expired_key(self): - """Test sig verification with expired key raises KeyExpirationError. """ - signature = { - "keyid": self.expired_key_keyid, - "other_headers": "deadbeef", - "signature": "deadbeef", - } - content = b"livestock" - key = export_pubkey(self.expired_key_keyid, - homedir=self.gnupg_home) - - with self.assertRaises(KeyExpirationError) as ctx: - verify_signature(signature, key, content) - - expected = ("GPG key 'e8ac80c924116dabb51d4b987cb07d6d2c199c7c' " - "created on '2019-03-25 12:46 UTC' with validity period '1 day, " - "0:25:01' expired on '2019-03-26 13:11 UTC'.") - self.assertTrue(expected == str(ctx.exception), - "\nexpected: {}" - "\ngot: {}".format(expected, ctx.exception)) + """Test signature creation, verification and key export from the gpg + module""" + + default_keyid = "8465A1E2E0FB2B40ADB2478E18FB3F537E0C8A17" + signing_subkey_keyid = "C5A0ABE6EC19D0D65F85E2C39BE9DF5131D924E9" + encryption_subkey_keyid = "6A112FD3390B2E53AFC2E57F8FC8E12099AECEEA" + unsupported_subkey_keyid = "611A9B648E16F54E8A7FAD5DA51E8CDF3B06524F" + expired_key_keyid = "E8AC80C924116DABB51D4B987CB07D6D2C199C7C" + + keyid_768C43 = "7B3ABB26B97B655AB9296BD15B0BD02E1C768C43" # pylint: disable=invalid-name + + @classmethod + def setUpClass(self): # pylint: disable=bad-classmethod-argument + # Create directory to run the tests without having everything blow up + self.working_dir = os.getcwd() + + # Find demo files + gpg_keyring_path = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "gpg_keyrings", "rsa" + ) + + self.test_dir = os.path.realpath(tempfile.mkdtemp()) + self.gnupg_home = os.path.join(self.test_dir, "rsa") + shutil.copytree(gpg_keyring_path, self.gnupg_home) + os.chdir(self.test_dir) + + @classmethod + def tearDownClass(self): # pylint: disable=bad-classmethod-argument + """Change back to initial working dir and remove temp test directory.""" + os.chdir(self.working_dir) + shutil.rmtree( + self.test_dir, onerror=GPGTestUtils.ignore_not_found_error + ) + + def test_export_pubkey_error(self): + """Test correct error is raised if function called incorrectly.""" + with self.assertRaises(ValueError): + export_pubkey("not-a-key-id") + + def test_export_pubkey(self): + """export a public key and make sure the parameters are the right ones: + + since there's very little we can do to check rsa key parameters are right + we pre-exported the public key to an ssh key, which we can load with + cryptography for the sake of comparison""" + + # export our gpg key, using our functions + key_data = export_pubkey(self.default_keyid, homedir=self.gnupg_home) + our_exported_key = rsa_create_pubkey(key_data) + + # load the equivalent ssh key, and make sure that we get the same RSA key + # parameters + ssh_key_basename = ( + "{}.ssh".format( # pylint: disable=consider-using-f-string + self.default_keyid + ) + ) + ssh_key_path = os.path.join(self.gnupg_home, ssh_key_basename) + with open(ssh_key_path, "rb") as fp: + keydata = fp.read() + + ssh_key = serialization.load_ssh_public_key( + keydata, backends.default_backend() + ) + + self.assertEqual( + ssh_key.public_numbers().n, our_exported_key.public_numbers().n + ) + self.assertEqual( + ssh_key.public_numbers().e, our_exported_key.public_numbers().e + ) + + subkey_keyids = list(key_data["subkeys"].keys()) + # We export the whole master key bundle which must contain the subkeys + self.assertTrue(self.signing_subkey_keyid.lower() in subkey_keyids) + # Currently we do not exclude encryption subkeys + self.assertTrue(self.encryption_subkey_keyid.lower() in subkey_keyids) + # However we do exclude subkeys, whose algorithm we do not support + self.assertFalse(self.unsupported_subkey_keyid.lower() in subkey_keyids) + + # When passing the subkey keyid we also export the whole keybundle + key_data2 = export_pubkey( + self.signing_subkey_keyid, homedir=self.gnupg_home + ) + self.assertDictEqual(key_data, key_data2) + + def test_export_pubkeys(self): + """Test export multiple pubkeys at once.""" + key_dict = export_pubkeys( + [self.default_keyid, self.keyid_768C43], homedir=self.gnupg_home + ) + + ANY_PUBKEY_DICT_SCHEMA.check_match(key_dict) + self.assertListEqual( + sorted([self.default_keyid.lower(), self.keyid_768C43.lower()]), + sorted(key_dict.keys()), + ) + + def test_gpg_sign_and_verify_object_with_default_key(self): + """Create a signature using the default key on the keyring""" + + test_data = b"test_data" + wrong_data = b"something malicious" + + signature = create_signature(test_data, homedir=self.gnupg_home) + key_data = export_pubkey(self.default_keyid, homedir=self.gnupg_home) + + self.assertTrue(verify_signature(signature, key_data, test_data)) + self.assertFalse(verify_signature(signature, key_data, wrong_data)) + + def test_gpg_sign_and_verify_object(self): + """Create a signature using a specific key on the keyring""" + + test_data = b"test_data" + wrong_data = b"something malicious" + + signature = create_signature( + test_data, keyid=self.default_keyid, homedir=self.gnupg_home + ) + key_data = export_pubkey(self.default_keyid, homedir=self.gnupg_home) + self.assertTrue(verify_signature(signature, key_data, test_data)) + self.assertFalse(verify_signature(signature, key_data, wrong_data)) + + def test_gpg_sign_and_verify_object_default_keyring(self): + """Sign/verify using keyring from envvar.""" + + test_data = b"test_data" + + gnupg_home_backup = os.environ.get("GNUPGHOME") + os.environ["GNUPGHOME"] = self.gnupg_home + + signature = create_signature(test_data, keyid=self.default_keyid) + key_data = export_pubkey(self.default_keyid) + self.assertTrue(verify_signature(signature, key_data, test_data)) + + # Reset GNUPGHOME + if gnupg_home_backup: + os.environ["GNUPGHOME"] = gnupg_home_backup + else: + del os.environ["GNUPGHOME"] + + def test_create_signature_with_expired_key(self): + """Test signing with expired key raises gpg CommandError.""" + with self.assertRaises(CommandError) as ctx: + create_signature( + b"livestock", + keyid=self.expired_key_keyid, + homedir=self.gnupg_home, + ) + + expected = "returned non-zero exit status '2'" + self.assertTrue( + expected in str(ctx.exception), + "{} not in {}".format( # pylint: disable=consider-using-f-string + expected, ctx.exception + ), + ) + + def test_verify_signature_with_expired_key(self): + """Test sig verification with expired key raises KeyExpirationError.""" + signature = { + "keyid": self.expired_key_keyid, + "other_headers": "deadbeef", + "signature": "deadbeef", + } + content = b"livestock" + key = export_pubkey(self.expired_key_keyid, homedir=self.gnupg_home) + + with self.assertRaises(KeyExpirationError) as ctx: + verify_signature(signature, key, content) + + expected = ( + "GPG key 'e8ac80c924116dabb51d4b987cb07d6d2c199c7c' " + "created on '2019-03-25 12:46 UTC' with validity period '1 day, " + "0:25:01' expired on '2019-03-26 13:11 UTC'." + ) + self.assertTrue( + expected == str(ctx.exception), + "\nexpected: {}" # pylint: disable=consider-using-f-string + "\ngot: {}".format( # pylint: disable=consider-using-f-string + expected, ctx.exception + ), + ) @unittest.skipIf(not have_gpg(), "gpg not found") class TestGPGDSA(unittest.TestCase): - """ Test signature creation, verification and key export from the gpg - module """ - - default_keyid = "C242A830DAAF1C2BEF604A9EF033A3A3E267B3B1" - - @classmethod - def setUpClass(self): - # Create directory to run the tests without having everything blow up - self.working_dir = os.getcwd() - self.test_dir = os.path.realpath(tempfile.mkdtemp()) - self.gnupg_home = os.path.join(self.test_dir, "dsa") - - # Find keyrings - keyrings = os.path.join( - os.path.dirname(os.path.realpath(__file__)), "gpg_keyrings", "dsa") - - shutil.copytree(keyrings, self.gnupg_home) - os.chdir(self.test_dir) - - @classmethod - def tearDownClass(self): - """Change back to initial working dir and remove temp test directory. """ - os.chdir(self.working_dir) - shutil.rmtree(self.test_dir, onerror=GPGTestUtils.ignore_not_found_error) - - def test_export_pubkey(self): - """ export a public key and make sure the parameters are the right ones: - - since there's very little we can do to check key parameters are right - we pre-exported the public key to an x.509 SubjectPublicKeyInfo key, - which we can load with cryptography for the sake of comparison """ - - # export our gpg key, using our functions - key_data = export_pubkey(self.default_keyid, homedir=self.gnupg_home) - our_exported_key = dsa_create_pubkey(key_data) - - # load same key, pre-exported with 3rd-party tooling - pem_key_basename = "{}.pem".format(self.default_keyid) - pem_key_path = os.path.join(self.gnupg_home, pem_key_basename) - with open(pem_key_path, "rb") as fp: - keydata = fp.read() - - pem_key = serialization.load_pem_public_key(keydata, - backends.default_backend()) - - # make sure keys match - self.assertEqual(pem_key.public_numbers().y, - our_exported_key.public_numbers().y) - self.assertEqual(pem_key.public_numbers().parameter_numbers.g, - our_exported_key.public_numbers().parameter_numbers.g) - self.assertEqual(pem_key.public_numbers().parameter_numbers.q, - our_exported_key.public_numbers().parameter_numbers.q) - self.assertEqual(pem_key.public_numbers().parameter_numbers.p, - our_exported_key.public_numbers().parameter_numbers.p) - - def test_gpg_sign_and_verify_object_with_default_key(self): - """Create a signature using the default key on the keyring """ - - test_data = b'test_data' - wrong_data = b'something malicious' - - signature = create_signature(test_data, homedir=self.gnupg_home) - key_data = export_pubkey(self.default_keyid, homedir=self.gnupg_home) - - self.assertTrue(verify_signature(signature, key_data, test_data)) - self.assertFalse(verify_signature(signature, key_data, wrong_data)) - - - def test_gpg_sign_and_verify_object(self): - """Create a signature using a specific key on the keyring """ - - test_data = b'test_data' - wrong_data = b'something malicious' - - signature = create_signature(test_data, keyid=self.default_keyid, - homedir=self.gnupg_home) - key_data = export_pubkey(self.default_keyid, homedir=self.gnupg_home) - - self.assertTrue(verify_signature(signature, key_data, test_data)) - self.assertFalse(verify_signature(signature, key_data, wrong_data)) - + """Test signature creation, verification and key export from the gpg + module""" + + default_keyid = "C242A830DAAF1C2BEF604A9EF033A3A3E267B3B1" + + @classmethod + def setUpClass(self): # pylint: disable=bad-classmethod-argument + # Create directory to run the tests without having everything blow up + self.working_dir = os.getcwd() + self.test_dir = os.path.realpath(tempfile.mkdtemp()) + self.gnupg_home = os.path.join(self.test_dir, "dsa") + + # Find keyrings + keyrings = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "gpg_keyrings", "dsa" + ) + + shutil.copytree(keyrings, self.gnupg_home) + os.chdir(self.test_dir) + + @classmethod + def tearDownClass(self): # pylint: disable=bad-classmethod-argument + """Change back to initial working dir and remove temp test directory.""" + os.chdir(self.working_dir) + shutil.rmtree( + self.test_dir, onerror=GPGTestUtils.ignore_not_found_error + ) + + def test_export_pubkey(self): + """export a public key and make sure the parameters are the right ones: + + since there's very little we can do to check key parameters are right + we pre-exported the public key to an x.509 SubjectPublicKeyInfo key, + which we can load with cryptography for the sake of comparison""" + + # export our gpg key, using our functions + key_data = export_pubkey(self.default_keyid, homedir=self.gnupg_home) + our_exported_key = dsa_create_pubkey(key_data) + + # load same key, pre-exported with 3rd-party tooling + pem_key_basename = ( + "{}.pem".format( # pylint: disable=consider-using-f-string + self.default_keyid + ) + ) + pem_key_path = os.path.join(self.gnupg_home, pem_key_basename) + with open(pem_key_path, "rb") as fp: + keydata = fp.read() + + pem_key = serialization.load_pem_public_key( + keydata, backends.default_backend() + ) + + # make sure keys match + self.assertEqual( + pem_key.public_numbers().y, our_exported_key.public_numbers().y + ) + self.assertEqual( + pem_key.public_numbers().parameter_numbers.g, + our_exported_key.public_numbers().parameter_numbers.g, + ) + self.assertEqual( + pem_key.public_numbers().parameter_numbers.q, + our_exported_key.public_numbers().parameter_numbers.q, + ) + self.assertEqual( + pem_key.public_numbers().parameter_numbers.p, + our_exported_key.public_numbers().parameter_numbers.p, + ) + + def test_gpg_sign_and_verify_object_with_default_key(self): + """Create a signature using the default key on the keyring""" + + test_data = b"test_data" + wrong_data = b"something malicious" + + signature = create_signature(test_data, homedir=self.gnupg_home) + key_data = export_pubkey(self.default_keyid, homedir=self.gnupg_home) + + self.assertTrue(verify_signature(signature, key_data, test_data)) + self.assertFalse(verify_signature(signature, key_data, wrong_data)) + + def test_gpg_sign_and_verify_object(self): + """Create a signature using a specific key on the keyring""" + + test_data = b"test_data" + wrong_data = b"something malicious" + + signature = create_signature( + test_data, keyid=self.default_keyid, homedir=self.gnupg_home + ) + key_data = export_pubkey(self.default_keyid, homedir=self.gnupg_home) + + self.assertTrue(verify_signature(signature, key_data, test_data)) + self.assertFalse(verify_signature(signature, key_data, wrong_data)) @unittest.skipIf(not have_gpg(), "gpg not found") class TestGPGEdDSA(unittest.TestCase): - """ Test signature creation, verification and key export from the gpg - module """ - - default_keyid = "4E630F84838BF6F7447B830B22692F5FEA9E2DD2" - - @classmethod - def setUpClass(self): - # Create directory to run the tests without having everything blow up - self.working_dir = os.getcwd() - self.test_dir = os.path.realpath(tempfile.mkdtemp()) - self.gnupg_home = os.path.join(self.test_dir, "dsa") + """Test signature creation, verification and key export from the gpg + module""" - # Find keyrings - keyrings = os.path.join( - os.path.dirname(os.path.realpath(__file__)), "gpg_keyrings", "eddsa") + default_keyid = "4E630F84838BF6F7447B830B22692F5FEA9E2DD2" - shutil.copytree(keyrings, self.gnupg_home) - os.chdir(self.test_dir) + @classmethod + def setUpClass(self): # pylint: disable=bad-classmethod-argument + # Create directory to run the tests without having everything blow up + self.working_dir = os.getcwd() + self.test_dir = os.path.realpath(tempfile.mkdtemp()) + self.gnupg_home = os.path.join(self.test_dir, "dsa") - @classmethod - def tearDownClass(self): - """Change back to initial working dir and remove temp test directory. """ - os.chdir(self.working_dir) - shutil.rmtree(self.test_dir, onerror=GPGTestUtils.ignore_not_found_error) + # Find keyrings + keyrings = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "gpg_keyrings", "eddsa" + ) - def test_gpg_sign_and_verify_object_with_default_key(self): - """Create a signature using the default key on the keyring """ + shutil.copytree(keyrings, self.gnupg_home) + os.chdir(self.test_dir) - test_data = b'test_data' - wrong_data = b'something malicious' + @classmethod + def tearDownClass(self): # pylint: disable=bad-classmethod-argument + """Change back to initial working dir and remove temp test directory.""" + os.chdir(self.working_dir) + shutil.rmtree( + self.test_dir, onerror=GPGTestUtils.ignore_not_found_error + ) - signature = create_signature(test_data, homedir=self.gnupg_home) - key_data = export_pubkey(self.default_keyid, homedir=self.gnupg_home) + def test_gpg_sign_and_verify_object_with_default_key(self): + """Create a signature using the default key on the keyring""" - self.assertTrue(verify_signature(signature, key_data, test_data)) - self.assertFalse(verify_signature(signature, key_data, wrong_data)) + test_data = b"test_data" + wrong_data = b"something malicious" + signature = create_signature(test_data, homedir=self.gnupg_home) + key_data = export_pubkey(self.default_keyid, homedir=self.gnupg_home) - def test_gpg_sign_and_verify_object_with_specific_key(self): - """Create a signature using a specific key on the keyring """ + self.assertTrue(verify_signature(signature, key_data, test_data)) + self.assertFalse(verify_signature(signature, key_data, wrong_data)) - test_data = b'test_data' - wrong_data = b'something malicious' + def test_gpg_sign_and_verify_object_with_specific_key(self): + """Create a signature using a specific key on the keyring""" - signature = create_signature(test_data, keyid=self.default_keyid, - homedir=self.gnupg_home) - key_data = export_pubkey(self.default_keyid, homedir=self.gnupg_home) + test_data = b"test_data" + wrong_data = b"something malicious" - self.assertTrue(verify_signature(signature, key_data, test_data)) - self.assertFalse(verify_signature(signature, key_data, wrong_data)) + signature = create_signature( + test_data, keyid=self.default_keyid, homedir=self.gnupg_home + ) + key_data = export_pubkey(self.default_keyid, homedir=self.gnupg_home) + self.assertTrue(verify_signature(signature, key_data, test_data)) + self.assertFalse(verify_signature(signature, key_data, wrong_data)) - def test_verify_short_signature(self): - """Correctly verify a special-crafted short signature. """ + def test_verify_short_signature(self): + """Correctly verify a special-crafted short signature.""" - test_data = b"hello" - signature_path = os.path.join(self.gnupg_home, "short.sig") + test_data = b"hello" + signature_path = os.path.join(self.gnupg_home, "short.sig") - # Read special-crafted raw gpg signature that is one byte too short - with open(signature_path, "rb") as f: - signature_data = f.read() + # Read special-crafted raw gpg signature that is one byte too short + with open(signature_path, "rb") as f: + signature_data = f.read() - # Check that the signature is padded upon parsing - # NOTE: The returned signature is a hex string and thus twice as long - signature = parse_signature_packet(signature_data) - self.assertTrue(len(signature["signature"]) == (ED25519_SIG_LENGTH * 2)) + # Check that the signature is padded upon parsing + # NOTE: The returned signature is a hex string and thus twice as long + signature = parse_signature_packet(signature_data) + self.assertTrue(len(signature["signature"]) == (ED25519_SIG_LENGTH * 2)) - # Check that the signature can be successfully verified - key = export_pubkey(self.default_keyid, homedir=self.gnupg_home) - self.assertTrue(verify_signature(signature, key, test_data)) + # Check that the signature can be successfully verified + key = export_pubkey(self.default_keyid, homedir=self.gnupg_home) + self.assertTrue(verify_signature(signature, key, test_data)) class TestVersion(unittest.TestCase): - """Tests for the Version utility class.""" - - def test_version_roundtrip_string(self): - """Version parses and formats strings correctly.""" - for value, expected in [ - ('1.3.0', Version(1, 3, 0)), - ('1.3.1', Version(1, 3, 1)), - ('1.3.22', Version(1, 3, 22)), - ]: - self.assertEqual(Version.from_string(value), expected) - self.assertEqual(str(expected), value) - - def test_version_from_string_invalid(self): - """Version.from_string rejects invalid inputs.""" - for value in [ - '1.3', - '1.33.0', - '1.3.-1', - '1.3.1a', - ]: - with self.assertRaises(ValueError, msg=f"expected error for input '{value}'"): - Version.from_string(value) + """Tests for the Version utility class.""" + + def test_version_roundtrip_string(self): + """Version parses and formats strings correctly.""" + for value, expected in [ + ("1.3.0", Version(1, 3, 0)), + ("1.3.1", Version(1, 3, 1)), + ("1.3.22", Version(1, 3, 22)), + ]: + self.assertEqual(Version.from_string(value), expected) + self.assertEqual(str(expected), value) + + def test_version_from_string_invalid(self): + """Version.from_string rejects invalid inputs.""" + for value in [ + "1.3", + "1.33.0", + "1.3.-1", + "1.3.1a", + ]: + with self.assertRaises( + ValueError, msg=f"expected error for input '{value}'" + ): + Version.from_string(value) if __name__ == "__main__": - unittest.main() + unittest.main() diff --git a/tests/test_hash.py b/tests/test_hash.py index df0ddec8..f13ce3bb 100755 --- a/tests/test_hash.py +++ b/tests/test_hash.py @@ -19,9 +19,9 @@ """ import io -import os import logging -import sys +import os +import sys # pylint: disable=unused-import import tempfile import unittest @@ -31,259 +31,300 @@ logger = logging.getLogger(__name__) -if not 'hashlib' in securesystemslib.hash.SUPPORTED_LIBRARIES: - logger.warning('Not testing hashlib: could not be imported.') - - -class TestHash(unittest.TestCase): - - @staticmethod - def _is_supported_combination(library, algorithm): - blake_algos = ['blake2b', 'blake2b-256', 'blake2s'] - - # pyca does not support blake2* - if algorithm in blake_algos: - if library == 'pyca_crypto': - return False - return True - - - def _run_with_all_algos_and_libs(self, test_func): - algorithms = [ - 'md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512', - 'blake2b-256', 'blake2b', 'blake2s', - ] - for algorithm in algorithms: - self._run_with_all_hash_libraries(test_func, algorithm) - - - def _run_with_all_hash_libraries(self, test_func, algorithm): - for lib in securesystemslib.hash.SUPPORTED_LIBRARIES: - if self._is_supported_combination(lib, algorithm): - test_func(lib, algorithm) - else: +if ( + not "hashlib" # pylint: disable=unneeded-not + in securesystemslib.hash.SUPPORTED_LIBRARIES +): + logger.warning("Not testing hashlib: could not be imported.") + + +class TestHash(unittest.TestCase): # pylint: disable=missing-class-docstring + @staticmethod + def _is_supported_combination(library, algorithm): + blake_algos = ["blake2b", "blake2b-256", "blake2s"] + + # pyca does not support blake2* + if algorithm in blake_algos: + if library == "pyca_crypto": + return False + return True + + def _run_with_all_algos_and_libs( + self, test_func + ): # pylint: disable=missing-function-docstring + algorithms = [ + "md5", + "sha1", + "sha224", + "sha256", + "sha384", + "sha512", + "blake2b-256", + "blake2b", + "blake2s", + ] + for algorithm in algorithms: + self._run_with_all_hash_libraries(test_func, algorithm) + + def _run_with_all_hash_libraries(self, test_func, algorithm): + for lib in securesystemslib.hash.SUPPORTED_LIBRARIES: + if self._is_supported_combination(lib, algorithm): + test_func(lib, algorithm) + else: + self.assertRaises( + securesystemslib.exceptions.UnsupportedAlgorithmError, + test_func, + lib, + algorithm, + ) + + def _do_algorithm_update( + self, library, algorithm + ): # pylint: disable=missing-function-docstring + expected = { + "blake2b": [ + "786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce", + "333fcb4ee1aa7c115355ec66ceac917c8bfd815bf7587d325aec1864edd24e34d5abe2c6b1b5ee3face62fed78dbef802f2a85cb91d455a8f5249d330853cb3c", + "e1161a4e6e6ed9da6928b5e96c24d5b957018f997994f16c05497af059d4f32bb80b34f478aa1fc173f6e45d859958c891e53c2c0bf8eda7c6d3917263641b46", + "e1161a4e6e6ed9da6928b5e96c24d5b957018f997994f16c05497af059d4f32bb80b34f478aa1fc173f6e45d859958c891e53c2c0bf8eda7c6d3917263641b46", + ], + "blake2b-256": [ + "0e5751c026e543b2e8ab2eb06099daa1d1e5df47778f7787faab45cdf12fe3a8", + "8928aae63c84d87ea098564d1e03ad813f107add474e56aedd286349c0c03ea4", + "92af150df67e34827f3c13239c4d11cad6f488b447f72e844c10fce6c651e9f0", + "92af150df67e34827f3c13239c4d11cad6f488b447f72e844c10fce6c651e9f0", + ], + "blake2s": [ + "69217a3079908094e11121d042354a7c1f55b6482ca1a51e1b250dfd1ed0eef9", + "4a0d129873403037c2cd9b9048203687f6233fb6738956e0349bd4320fec3e90", + "2b68156e70f71280f7ad021f74620446ee49613a7ed34f5220da7b1dbae9adb2", + "2b68156e70f71280f7ad021f74620446ee49613a7ed34f5220da7b1dbae9adb2", + ], + "md5": [ + "d41d8cd98f00b204e9800998ecf8427e", + "0cc175b9c0f1b6a831c399e269772661", + "f034e93091235adbb5d2781908e2b313", + "f034e93091235adbb5d2781908e2b313", + ], + "sha1": [ + "da39a3ee5e6b4b0d3255bfef95601890afd80709", + "86f7e437faa5a7fce15d1ddcb9eaeaea377667b8", + "d7bfa42fc62b697bf6cf1cda9af1fb7f40a27817", + "d7bfa42fc62b697bf6cf1cda9af1fb7f40a27817", + ], + "sha224": [ + "d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f", + "abd37534c7d9a2efb9465de931cd7055ffdb8879563ae98078d6d6d5", + "ab1342f31c2a6f242d9a3cefb503fb49465c95eb255c16ad791d688c", + "ab1342f31c2a6f242d9a3cefb503fb49465c95eb255c16ad791d688c", + ], + "sha256": [ + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb", + "01d162a5c95d4698c0a3e766ae80d85994b549b877ed275803725f43dadc83bd", + "01d162a5c95d4698c0a3e766ae80d85994b549b877ed275803725f43dadc83bd", + ], + "sha384": [ + "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b", + "54a59b9f22b0b80880d8427e548b7c23abd873486e1f035dce9cd697e85175033caa88e6d57bc35efae0b5afd3145f31", + "f2c1438e9cc1d24bebbf3b88e60adc169db0c5c459d02054ec131438bf20ebee5ca88c17cb5f1a824fcccf8d2b20b0a9", + "f2c1438e9cc1d24bebbf3b88e60adc169db0c5c459d02054ec131438bf20ebee5ca88c17cb5f1a824fcccf8d2b20b0a9", + ], + "sha512": [ + "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", + "1f40fc92da241694750979ee6cf582f2d5d7d28e18335de05abc54d0560e0f5302860c652bf08d560252aa5e74210546f369fbbbce8c12cfc7957b2652fe9a75", + "09ade82ae3c5d54f8375f348563a372106488adef16a74b63b5591849f740bff55ceab22e117b4b09349b860f8a644adb32a9ea542abdecb80bf625160604251", + "09ade82ae3c5d54f8375f348563a372106488adef16a74b63b5591849f740bff55ceab22e117b4b09349b860f8a644adb32a9ea542abdecb80bf625160604251", + ], + } + digest_object = securesystemslib.hash.digest(algorithm, library) + + self.assertEqual(digest_object.hexdigest(), expected[algorithm][0]) + digest_object.update("a".encode("utf-8")) + self.assertEqual(digest_object.hexdigest(), expected[algorithm][1]) + digest_object.update("bbb".encode("utf-8")) + self.assertEqual(digest_object.hexdigest(), expected[algorithm][2]) + digest_object.update("".encode("utf-8")) + self.assertEqual(digest_object.hexdigest(), expected[algorithm][3]) + + def test_blake2s_update(self): + self._run_with_all_hash_libraries(self._do_algorithm_update, "blake2s") + + def test_blake2b_update(self): + self._run_with_all_hash_libraries(self._do_algorithm_update, "blake2b") + + def test_blake2b_256_update(self): + self._run_with_all_hash_libraries( + self._do_algorithm_update, "blake2b-256" + ) + + def test_md5_update(self): + self._run_with_all_hash_libraries(self._do_algorithm_update, "md5") + + def test_sha1_update(self): + self._run_with_all_hash_libraries(self._do_algorithm_update, "sha1") + + def test_sha224_update(self): + self._run_with_all_hash_libraries(self._do_algorithm_update, "sha224") + + def test_sha256_update(self): + self._run_with_all_hash_libraries(self._do_algorithm_update, "sha256") + + def test_sha384_update(self): + self._run_with_all_hash_libraries(self._do_algorithm_update, "sha384") + + def test_sha512_update(self): + self._run_with_all_hash_libraries(self._do_algorithm_update, "sha512") + + def test_unsupported_algorithm(self): + self._run_with_all_hash_libraries( + self._do_unsupported_algorithm, "bogus" + ) + + def _do_unsupported_algorithm(self, library, algorithm): self.assertRaises( securesystemslib.exceptions.UnsupportedAlgorithmError, - test_func, lib, algorithm) - - - def _do_algorithm_update(self, library, algorithm): - expected = { - 'blake2b': [ - '786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce', - '333fcb4ee1aa7c115355ec66ceac917c8bfd815bf7587d325aec1864edd24e34d5abe2c6b1b5ee3face62fed78dbef802f2a85cb91d455a8f5249d330853cb3c', - 'e1161a4e6e6ed9da6928b5e96c24d5b957018f997994f16c05497af059d4f32bb80b34f478aa1fc173f6e45d859958c891e53c2c0bf8eda7c6d3917263641b46', - 'e1161a4e6e6ed9da6928b5e96c24d5b957018f997994f16c05497af059d4f32bb80b34f478aa1fc173f6e45d859958c891e53c2c0bf8eda7c6d3917263641b46', - ], - 'blake2b-256': [ - '0e5751c026e543b2e8ab2eb06099daa1d1e5df47778f7787faab45cdf12fe3a8', - '8928aae63c84d87ea098564d1e03ad813f107add474e56aedd286349c0c03ea4', - '92af150df67e34827f3c13239c4d11cad6f488b447f72e844c10fce6c651e9f0', - '92af150df67e34827f3c13239c4d11cad6f488b447f72e844c10fce6c651e9f0', - ], - 'blake2s': [ - '69217a3079908094e11121d042354a7c1f55b6482ca1a51e1b250dfd1ed0eef9', - '4a0d129873403037c2cd9b9048203687f6233fb6738956e0349bd4320fec3e90', - '2b68156e70f71280f7ad021f74620446ee49613a7ed34f5220da7b1dbae9adb2', - '2b68156e70f71280f7ad021f74620446ee49613a7ed34f5220da7b1dbae9adb2', - ], - 'md5': [ - 'd41d8cd98f00b204e9800998ecf8427e', - '0cc175b9c0f1b6a831c399e269772661', - 'f034e93091235adbb5d2781908e2b313', - 'f034e93091235adbb5d2781908e2b313', - ], - 'sha1': [ - 'da39a3ee5e6b4b0d3255bfef95601890afd80709', - '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8', - 'd7bfa42fc62b697bf6cf1cda9af1fb7f40a27817', - 'd7bfa42fc62b697bf6cf1cda9af1fb7f40a27817', - ], - 'sha224': [ - 'd14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f', - 'abd37534c7d9a2efb9465de931cd7055ffdb8879563ae98078d6d6d5', - 'ab1342f31c2a6f242d9a3cefb503fb49465c95eb255c16ad791d688c', - 'ab1342f31c2a6f242d9a3cefb503fb49465c95eb255c16ad791d688c', - ], - 'sha256': [ - 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', - 'ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb', - '01d162a5c95d4698c0a3e766ae80d85994b549b877ed275803725f43dadc83bd', - '01d162a5c95d4698c0a3e766ae80d85994b549b877ed275803725f43dadc83bd', - ], - 'sha384': [ - '38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b', - '54a59b9f22b0b80880d8427e548b7c23abd873486e1f035dce9cd697e85175033caa88e6d57bc35efae0b5afd3145f31', - 'f2c1438e9cc1d24bebbf3b88e60adc169db0c5c459d02054ec131438bf20ebee5ca88c17cb5f1a824fcccf8d2b20b0a9', - 'f2c1438e9cc1d24bebbf3b88e60adc169db0c5c459d02054ec131438bf20ebee5ca88c17cb5f1a824fcccf8d2b20b0a9', - ], - 'sha512': [ - 'cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e', - '1f40fc92da241694750979ee6cf582f2d5d7d28e18335de05abc54d0560e0f5302860c652bf08d560252aa5e74210546f369fbbbce8c12cfc7957b2652fe9a75', - '09ade82ae3c5d54f8375f348563a372106488adef16a74b63b5591849f740bff55ceab22e117b4b09349b860f8a644adb32a9ea542abdecb80bf625160604251', - '09ade82ae3c5d54f8375f348563a372106488adef16a74b63b5591849f740bff55ceab22e117b4b09349b860f8a644adb32a9ea542abdecb80bf625160604251', - ], - } - digest_object = securesystemslib.hash.digest(algorithm, library) - - self.assertEqual(digest_object.hexdigest(), expected[algorithm][0]) - digest_object.update('a'.encode('utf-8')) - self.assertEqual(digest_object.hexdigest(), expected[algorithm][1]) - digest_object.update('bbb'.encode('utf-8')) - self.assertEqual(digest_object.hexdigest(), expected[algorithm][2]) - digest_object.update(''.encode('utf-8')) - self.assertEqual(digest_object.hexdigest(), expected[algorithm][3]) - - - def test_blake2s_update(self): - self._run_with_all_hash_libraries(self._do_algorithm_update, 'blake2s') - - - def test_blake2b_update(self): - self._run_with_all_hash_libraries(self._do_algorithm_update, 'blake2b') - - - def test_blake2b_256_update(self): - self._run_with_all_hash_libraries(self._do_algorithm_update, 'blake2b-256') - - - def test_md5_update(self): - self._run_with_all_hash_libraries(self._do_algorithm_update, 'md5') - - - def test_sha1_update(self): - self._run_with_all_hash_libraries(self._do_algorithm_update, 'sha1') - - - def test_sha224_update(self): - self._run_with_all_hash_libraries(self._do_algorithm_update, 'sha224') - - - def test_sha256_update(self): - self._run_with_all_hash_libraries(self._do_algorithm_update, 'sha256') - - - def test_sha384_update(self): - self._run_with_all_hash_libraries(self._do_algorithm_update, 'sha384') - - - def test_sha512_update(self): - self._run_with_all_hash_libraries(self._do_algorithm_update, 'sha512') - - - def test_unsupported_algorithm(self): - self._run_with_all_hash_libraries(self._do_unsupported_algorithm, 'bogus') - - - def _do_unsupported_algorithm(self, library, algorithm): - self.assertRaises(securesystemslib.exceptions.UnsupportedAlgorithmError, - securesystemslib.hash.digest, algorithm, library) - - - def test_digest_size(self): - self._run_with_all_algos_and_libs(self._do_digest_size) - - - def _do_digest_size(self, library, algorithm): - digest_sizes = { - 'md5': 16, - 'sha1': 20, - 'sha224': 28, - 'sha256': 32, - 'sha384': 48, - 'sha512': 64, - 'blake2b-256': 32, - 'blake2b': 64, - 'blake2s': 32, - } - self.assertEqual(digest_sizes[algorithm], - securesystemslib.hash.digest(algorithm, library).digest_size) - - - def test_update_filename(self): - self._run_with_all_algos_and_libs(self._do_update_filename) - - - def _do_update_filename(self, library, algorithm): - data = 'abcdefgh' * 4096 - fd, filename = tempfile.mkstemp() - try: - os.write(fd, data.encode('utf-8')) - os.close(fd) - digest_object_truth = securesystemslib.hash.digest(algorithm, library) - digest_object_truth.update(data.encode('utf-8')) - digest_object = securesystemslib.hash.digest_filename(filename, - algorithm, library) - self.assertEqual(digest_object_truth.digest(), digest_object.digest()) - - finally: - os.remove(filename) - - - def test_update_filename_normalize(self): - self._run_with_all_algos_and_libs(self._do_update_filename_normalize) - - - def _do_update_filename_normalize(self, library, algorithm): - data = b'ab\r\nd\nf\r' * 4096 - normalized_data = data.replace(b'\r\n', b'\n').replace(b'\r', b'\n') - fd, filename = tempfile.mkstemp() - try: - os.write(fd, data) - os.close(fd) - digest_object_truth = securesystemslib.hash.digest(algorithm, library) - digest_object_truth.update(normalized_data) - digest_object = securesystemslib.hash.digest_filename(filename, - algorithm, library, normalize_line_endings=True) - self.assertEqual(digest_object_truth.digest(), digest_object.digest()) - - finally: - os.remove(filename) - - - def test_update_file_obj(self): - self._run_with_all_algos_and_libs(self._do_update_file_obj) - - - def _do_update_file_obj(self, library, algorithm): - data = 'abcdefgh' * 4096 - file_obj = io.StringIO() - file_obj.write(data) - digest_object_truth = securesystemslib.hash.digest(algorithm, library) - digest_object_truth.update(data.encode('utf-8')) - digest_object = securesystemslib.hash.digest_fileobject(file_obj, - algorithm, library) - - # Note: we don't seek because the update_file_obj call is supposed - # to always seek to the beginning. - self.assertEqual(digest_object_truth.digest(), digest_object.digest()) - - - def test_digest_from_rsa_scheme(self): - self._run_with_all_hash_libraries(self._do_get_digest_from_rsa_valid_schemes, 'sha256') - self._run_with_all_hash_libraries(self._do_get_digest_from_rsa_non_valid_schemes, 'sha256') - - - def _do_get_digest_from_rsa_valid_schemes(self, library, algorithm): - scheme = 'rsassa-pss-sha256' - expected_digest_cls = type(securesystemslib.hash.digest(algorithm, library)) - - self.assertIsInstance(securesystemslib.hash.digest_from_rsa_scheme(scheme, library), - expected_digest_cls) - - def _do_get_digest_from_rsa_non_valid_schemes(self, library, algorithm): - self.assertRaises(securesystemslib.exceptions.FormatError, - securesystemslib.hash.digest_from_rsa_scheme, 'rsassa-pss-sha123', library) - - + securesystemslib.hash.digest, + algorithm, + library, + ) + + def test_digest_size(self): + self._run_with_all_algos_and_libs(self._do_digest_size) + + def _do_digest_size( + self, library, algorithm + ): # pylint: disable=missing-function-docstring + digest_sizes = { + "md5": 16, + "sha1": 20, + "sha224": 28, + "sha256": 32, + "sha384": 48, + "sha512": 64, + "blake2b-256": 32, + "blake2b": 64, + "blake2s": 32, + } + self.assertEqual( + digest_sizes[algorithm], + securesystemslib.hash.digest(algorithm, library).digest_size, + ) + + def test_update_filename(self): + self._run_with_all_algos_and_libs(self._do_update_filename) + + def _do_update_filename( + self, library, algorithm + ): # pylint: disable=missing-function-docstring + data = "abcdefgh" * 4096 + fd, filename = tempfile.mkstemp() + try: + os.write(fd, data.encode("utf-8")) + os.close(fd) + digest_object_truth = securesystemslib.hash.digest( + algorithm, library + ) + digest_object_truth.update(data.encode("utf-8")) + digest_object = securesystemslib.hash.digest_filename( + filename, algorithm, library + ) + self.assertEqual( + digest_object_truth.digest(), digest_object.digest() + ) + + finally: + os.remove(filename) + + def test_update_filename_normalize(self): + self._run_with_all_algos_and_libs(self._do_update_filename_normalize) + + def _do_update_filename_normalize( + self, library, algorithm + ): # pylint: disable=missing-function-docstring + data = b"ab\r\nd\nf\r" * 4096 + normalized_data = data.replace(b"\r\n", b"\n").replace(b"\r", b"\n") + fd, filename = tempfile.mkstemp() + try: + os.write(fd, data) + os.close(fd) + digest_object_truth = securesystemslib.hash.digest( + algorithm, library + ) + digest_object_truth.update(normalized_data) + digest_object = securesystemslib.hash.digest_filename( + filename, algorithm, library, normalize_line_endings=True + ) + self.assertEqual( + digest_object_truth.digest(), digest_object.digest() + ) + + finally: + os.remove(filename) + + def test_update_file_obj(self): + self._run_with_all_algos_and_libs(self._do_update_file_obj) + + def _do_update_file_obj( + self, library, algorithm + ): # pylint: disable=missing-function-docstring + data = "abcdefgh" * 4096 + file_obj = io.StringIO() + file_obj.write(data) + digest_object_truth = securesystemslib.hash.digest(algorithm, library) + digest_object_truth.update(data.encode("utf-8")) + digest_object = securesystemslib.hash.digest_fileobject( + file_obj, algorithm, library + ) + + # Note: we don't seek because the update_file_obj call is supposed + # to always seek to the beginning. + self.assertEqual(digest_object_truth.digest(), digest_object.digest()) + + def test_digest_from_rsa_scheme(self): + self._run_with_all_hash_libraries( + self._do_get_digest_from_rsa_valid_schemes, "sha256" + ) + self._run_with_all_hash_libraries( + self._do_get_digest_from_rsa_non_valid_schemes, "sha256" + ) + + def _do_get_digest_from_rsa_valid_schemes(self, library, algorithm): + scheme = "rsassa-pss-sha256" + expected_digest_cls = type( + securesystemslib.hash.digest(algorithm, library) + ) + + self.assertIsInstance( + securesystemslib.hash.digest_from_rsa_scheme(scheme, library), + expected_digest_cls, + ) + + def _do_get_digest_from_rsa_non_valid_schemes( + self, library, algorithm + ): # pylint: disable=unused-argument + self.assertRaises( + securesystemslib.exceptions.FormatError, + securesystemslib.hash.digest_from_rsa_scheme, + "rsassa-pss-sha123", + library, + ) - def test_unsupported_digest_algorithm_and_library(self): - self.assertRaises(securesystemslib.exceptions.UnsupportedAlgorithmError, - securesystemslib.hash.digest, 'sha123', 'hashlib') - self.assertRaises(securesystemslib.exceptions.UnsupportedLibraryError, - securesystemslib.hash.digest, 'sha256', 'badlib') + def test_unsupported_digest_algorithm_and_library(self): + self.assertRaises( + securesystemslib.exceptions.UnsupportedAlgorithmError, + securesystemslib.hash.digest, + "sha123", + "hashlib", + ) + self.assertRaises( + securesystemslib.exceptions.UnsupportedLibraryError, + securesystemslib.hash.digest, + "sha256", + "badlib", + ) # Run unit test. -if __name__ == '__main__': - unittest.main() +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_interface.py b/tests/test_interface.py index ff64975a..260bdeec 100755 --- a/tests/test_interface.py +++ b/tests/test_interface.py @@ -17,761 +17,1027 @@ Unit test for 'interface.py'. """ +import datetime # pylint: disable=unused-import +import json # pylint: disable=unused-import import os -import time -import datetime -import tempfile -import json import shutil import stat import sys +import tempfile +import time # pylint: disable=unused-import import unittest -from cryptography.hazmat.primitives.serialization import load_pem_private_key from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.serialization import load_pem_private_key # Use external backport 'mock' on versions under 3.3 if sys.version_info >= (3, 3): - import unittest.mock as mock + import unittest.mock as mock # pylint: disable=consider-using-from-import else: - import mock + import mock -from securesystemslib import ( - KEY_TYPE_RSA, +from securesystemslib import ( # pylint: disable=wrong-import-position + KEY_TYPE_ECDSA, KEY_TYPE_ED25519, - KEY_TYPE_ECDSA) - -from securesystemslib.formats import ( - RSAKEY_SCHEMA, - PUBLIC_KEY_SCHEMA, + 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, - ECDSAKEY_SCHEMA) - -from securesystemslib.exceptions import Error, FormatError, CryptoError - -from securesystemslib.interface import ( - _generate_and_write_rsa_keypair, - generate_and_write_rsa_keypair, - generate_and_write_rsa_keypair_with_prompt, - generate_and_write_unencrypted_rsa_keypair, - import_rsa_privatekey_from_file, - import_rsa_publickey_from_file, - _generate_and_write_ed25519_keypair, - generate_and_write_ed25519_keypair, - generate_and_write_ed25519_keypair_with_prompt, - generate_and_write_unencrypted_ed25519_keypair, - import_ed25519_publickey_from_file, - import_ed25519_privatekey_from_file, + 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, - import_ecdsa_publickey_from_file, + 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_privatekey_from_file) - - - -class TestInterfaceFunctions(unittest.TestCase): - @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): - """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(idx)) as ctx: - _generate_and_write_rsa_keypair(**kwargs) - - self.assertEqual(err_msg, str(ctx.exception), - "expected: '{}' got: '{}' (row {})".format( - 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(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(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 encrypted but bad pw passed - ([fn_encrypted], {"password": "bad pw"}, CryptoError, - "Bad decrypt. Incorrect password?"), - # 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(idx)) as ctx: - import_rsa_privatekey_from_file(*args, **kwargs) - - self.assertTrue(err_msg in str(ctx.exception), - "expected: '{}' got: '{}' (row {})".format( - err_msg, ctx.exception, idx)) - - # 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(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(idx)): - import_rsa_publickey_from_file(*args, **kwargs) - with self.assertRaises(FormatError, msg="(row {})".format(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): - """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(idx)) as ctx: - _generate_and_write_ed25519_keypair(**kwargs) - - self.assertEqual(err_msg, str(ctx.exception), - "expected: '{}' got: '{}' (row {})".format( - 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(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(idx)) as ctx: - import_ed25519_publickey_from_file(fn) - - self.assertTrue(err_msg in str(ctx.exception), - "expected: '{}' got: '{}' (row {})".format( - 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(idx)) as ctx: - import_ed25519_privatekey_from_file(*args, **kwargs) - - self.assertTrue(err_msg in str(ctx.exception), - "expected: '{}' got: '{}' (row {})".format( - 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(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): - """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(idx)) as ctx: - _generate_and_write_ecdsa_keypair(**kwargs) - - self.assertEqual(err_msg, str(ctx.exception), - "expected: '{}' got: '{}' (row {})".format( - 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(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(idx)) as ctx: - import_ecdsa_publickey_from_file(fn) - - self.assertTrue(err_msg in str(ctx.exception), - "expected: '{}' got: '{}' (row {})".format( - 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(idx)) as ctx: - import_ecdsa_privatekey_from_file(*args, **kwargs) - - self.assertTrue(err_msg in str(ctx.exception), - "expected: '{}' got: '{}' (row {})".format( - 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(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 - 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(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(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(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) - + 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 encrypted but bad pw passed + ( + [fn_encrypted], + {"password": "bad pw"}, + CryptoError, + "Bad decrypt. Incorrect password?", + ), + # 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 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 + 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() +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_keys.py b/tests/test_keys.py index 9a887c9a..cb7d616f 100755 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -17,716 +17,931 @@ Test cases for test_keys.py. """ -import unittest import copy +import unittest +import securesystemslib.ecdsa_keys import securesystemslib.exceptions import securesystemslib.formats import securesystemslib.keys -import securesystemslib.ecdsa_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): - - @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() - - # 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 = 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_import_rsakey_from_public_pem(self): - 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)]) - - - - 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_dict_from_meta_default, junk = 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_dict_from_meta_default), - FORMAT_ERROR_MSG) - - self.assertTrue( - securesystemslib.formats.KEY_SCHEMA.matches(rsakey_dict_from_meta_default), - FORMAT_ERROR_MSG) - - # Call format_metadata_to_key by using custom value for keyid_hash_algorithms - rsakey_dict_from_meta_custom, junk = 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(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): - # 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' - _DATA = securesystemslib.formats.encode_canonical(_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 = 'invalid' - KEYS._available_crypto_libraries = ['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'] - - private_rsakey = 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'] - ecdsakey = 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') - private_ecdsakey = 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) - +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_dict_from_meta_default, + junk, # pylint: disable=unused-variable + ) = 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_dict_from_meta_default + ), + FORMAT_ERROR_MSG, + ) + + self.assertTrue( + securesystemslib.formats.KEY_SCHEMA.matches( + rsakey_dict_from_meta_default + ), + FORMAT_ERROR_MSG, + ) + + # Call format_metadata_to_key by using custom value for keyid_hash_algorithms + rsakey_dict_from_meta_custom, junk = 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"] + + private_rsakey = KEYS.import_rsakey_from_private_pem( # pylint: disable=unused-variable + 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"] + ecdsakey = KEYS.import_ecdsakey_from_private_pem( # pylint: disable=unused-variable + private_pem + ) + + # Test for an encrypted PEM. + scheme = "ecdsa-sha2-nistp256" + encrypted_pem = securesystemslib.ecdsa_keys.create_ecdsa_encrypted_pem( + private_pem, "password" + ) + private_ecdsakey = KEYS.import_ecdsakey_from_private_pem( # pylint: disable=unused-variable + 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() +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_process.py b/tests/test_process.py index 89cf88a6..61e75451 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -16,126 +16,151 @@ Test subprocess interface. """ +import io import os -import tempfile -import unittest import shlex -import io import sys +import tempfile +import unittest + import securesystemslib.process import securesystemslib.settings -class Test_Process(unittest.TestCase): - """Test subprocess interface. """ - - def test_run_input_vs_stdin(self): - """Test that stdin kwarg is only used if input kwarg is not supplied. """ - - # Create a temporary file, passed as `stdin` argument - fd, path = tempfile.mkstemp(text=True) - os.write(fd, b"use stdin kwarg") - os.close(fd) - - stdin_file = open(path) - cmd = \ - sys.executable + " -c \"import sys; assert(sys.stdin.read() == '{}')\"" - - # input is used in favor of stdin - securesystemslib.process.run(cmd.format("use input kwarg"), - input=b"use input kwarg", - stdin=stdin_file) - - # stdin is only used if input is not supplied - securesystemslib.process.run(cmd.format("use stdin kwarg"), - stdin=stdin_file) - - # Clean up - stdin_file.close() - os.remove(path) - - - def test_run_duplicate_streams(self): - """Test output as streams and as returned. """ - # Command that prints 'foo' to stdout and 'bar' to stderr. - cmd = (sys.executable + " -c \"" - "import sys;" - "sys.stdout.write('foo');" - "sys.stderr.write('bar');\"") - - # Create and open fake targets for standard streams - stdout_fd, stdout_fn = tempfile.mkstemp() - stderr_fd, stderr_fn = tempfile.mkstemp() - with io.open(stdout_fn, "r") as fake_stdout_reader, \ - os.fdopen(stdout_fd, "w") as fake_stdout_writer, \ - io.open(stderr_fn, "r") as fake_stderr_reader, \ - os.fdopen(stderr_fd, "w") as fake_stderr_writer: - - # Backup original standard streams and redirect to fake targets - real_stdout = sys.stdout - real_stderr = sys.stderr - sys.stdout = fake_stdout_writer - sys.stderr = fake_stderr_writer - - # Run command - ret_code, ret_stdout, ret_stderr = \ - securesystemslib.process.run_duplicate_streams(cmd) - - # Rewind fake standard streams - fake_stdout_reader.seek(0) - fake_stderr_reader.seek(0) - - # Assert that what was printed and what was returned is correct - self.assertTrue(ret_stdout == fake_stdout_reader.read() == "foo") - self.assertTrue(ret_stderr == fake_stderr_reader.read() == "bar") - # Also assert the default return value - self.assertEqual(ret_code, 0) - - # Reset original streams - sys.stdout = real_stdout - sys.stderr = real_stderr - - # Remove fake standard streams - os.remove(stdout_fn) - os.remove(stderr_fn) - - - def test_run_cmd_arg_return_code(self): - """Test command arg as string and list using return code. """ - cmd_str = (sys.executable + " -c \"" - "import sys;" - "sys.exit(100)\"") - cmd_list = shlex.split(cmd_str) - - for cmd in [cmd_str, cmd_list]: - proc = securesystemslib.process.run(cmd, check=False) - self.assertEqual(proc.returncode, 100) - - return_code, _, _ = securesystemslib.process.run_duplicate_streams(cmd) - self.assertEqual(return_code, 100) - - - def test_run_duplicate_streams_timeout(self): - """Test raise TimeoutExpired. """ - with self.assertRaises(securesystemslib.process.subprocess.TimeoutExpired): - securesystemslib.process.run_duplicate_streams(sys.executable + - " -c \"while True: pass\"", timeout=-1) - - - def test__default_timeout(self): - """Test default timeout modification. """ - # Backup timeout and check that it is what's returned by _default_timeout() - timeout_old = securesystemslib.settings.SUBPROCESS_TIMEOUT - self.assertEqual(securesystemslib.process._default_timeout(), timeout_old) - - # Modify timeout and check that _default_timeout() returns the same value - timeout_new = timeout_old + 1 - securesystemslib.settings.SUBPROCESS_TIMEOUT = timeout_new - self.assertEqual(securesystemslib.process._default_timeout(), timeout_new) - - # Restore original timeout - securesystemslib.settings.SUBPROCESS_TIMEOUT = timeout_old +class Test_Process(unittest.TestCase): # pylint: disable=invalid-name + """Test subprocess interface.""" + + def test_run_input_vs_stdin(self): + """Test that stdin kwarg is only used if input kwarg is not supplied.""" + + # Create a temporary file, passed as `stdin` argument + fd, path = tempfile.mkstemp(text=True) + os.write(fd, b"use stdin kwarg") + os.close(fd) + + stdin_file = ( + open( # pylint: disable=unspecified-encoding,consider-using-with + path + ) + ) + cmd = ( + sys.executable + + " -c \"import sys; assert(sys.stdin.read() == '{}')\"" + ) + + # input is used in favor of stdin + securesystemslib.process.run( + cmd.format("use input kwarg"), + input=b"use input kwarg", + stdin=stdin_file, + ) + + # stdin is only used if input is not supplied + securesystemslib.process.run( + cmd.format("use stdin kwarg"), stdin=stdin_file + ) + + # Clean up + stdin_file.close() + os.remove(path) + + def test_run_duplicate_streams(self): + """Test output as streams and as returned.""" + # Command that prints 'foo' to stdout and 'bar' to stderr. + cmd = ( + sys.executable + ' -c "' + "import sys;" + "sys.stdout.write('foo');" + "sys.stderr.write('bar');\"" + ) + + # Create and open fake targets for standard streams + stdout_fd, stdout_fn = tempfile.mkstemp() + stderr_fd, stderr_fn = tempfile.mkstemp() + with io.open( # pylint: disable=unspecified-encoding + stdout_fn, "r" + ) as fake_stdout_reader, os.fdopen( # pylint: disable=unspecified-encoding + stdout_fd, "w" + ) as fake_stdout_writer, io.open( # pylint: disable=unspecified-encoding + stderr_fn, "r" + ) as fake_stderr_reader, os.fdopen( # pylint: disable=unspecified-encoding + stderr_fd, "w" + ) as fake_stderr_writer: + + # Backup original standard streams and redirect to fake targets + real_stdout = sys.stdout + real_stderr = sys.stderr + sys.stdout = fake_stdout_writer + sys.stderr = fake_stderr_writer + + # Run command + ( + ret_code, + ret_stdout, + ret_stderr, + ) = securesystemslib.process.run_duplicate_streams(cmd) + + # Rewind fake standard streams + fake_stdout_reader.seek(0) + fake_stderr_reader.seek(0) + + # Assert that what was printed and what was returned is correct + self.assertTrue(ret_stdout == fake_stdout_reader.read() == "foo") + self.assertTrue(ret_stderr == fake_stderr_reader.read() == "bar") + # Also assert the default return value + self.assertEqual(ret_code, 0) + + # Reset original streams + sys.stdout = real_stdout + sys.stderr = real_stderr + + # Remove fake standard streams + os.remove(stdout_fn) + os.remove(stderr_fn) + + def test_run_cmd_arg_return_code(self): + """Test command arg as string and list using return code.""" + cmd_str = sys.executable + ' -c "' "import sys;" 'sys.exit(100)"' + cmd_list = shlex.split(cmd_str) + + for cmd in [cmd_str, cmd_list]: + proc = securesystemslib.process.run(cmd, check=False) + self.assertEqual(proc.returncode, 100) + + return_code, _, _ = securesystemslib.process.run_duplicate_streams( + cmd + ) + self.assertEqual(return_code, 100) + + def test_run_duplicate_streams_timeout(self): + """Test raise TimeoutExpired.""" + with self.assertRaises( + securesystemslib.process.subprocess.TimeoutExpired + ): + securesystemslib.process.run_duplicate_streams( + sys.executable + ' -c "while True: pass"', timeout=-1 + ) + + def test__default_timeout(self): + """Test default timeout modification.""" + # Backup timeout and check that it is what's returned by _default_timeout() + timeout_old = securesystemslib.settings.SUBPROCESS_TIMEOUT + self.assertEqual( + securesystemslib.process._default_timeout(), # pylint: disable=protected-access + timeout_old, + ) + + # Modify timeout and check that _default_timeout() returns the same value + timeout_new = timeout_old + 1 + securesystemslib.settings.SUBPROCESS_TIMEOUT = timeout_new + self.assertEqual( + securesystemslib.process._default_timeout(), # pylint: disable=protected-access + timeout_new, + ) + + # Restore original timeout + securesystemslib.settings.SUBPROCESS_TIMEOUT = timeout_old if __name__ == "__main__": - unittest.main() + unittest.main() diff --git a/tests/test_rsa_keys.py b/tests/test_rsa_keys.py index 92f7c9d9..1ff26619 100755 --- a/tests/test_rsa_keys.py +++ b/tests/test_rsa_keys.py @@ -19,287 +19,445 @@ import unittest -import securesystemslib.exceptions -import securesystemslib.formats -import securesystemslib.keys -import securesystemslib.rsa_keys -import securesystemslib.hash - from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.serialization import load_pem_private_key -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): - 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 - global public_rsa - 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 - global private_rsa - 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 - global private_rsa - - 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 - global private_rsa - - 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 - global private_rsa - - 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 - global private_rsa - 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) - - decrypted_rsa_key = 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 - 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) +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 + ) + + decrypted_rsa_key = securesystemslib.rsa_keys.decrypt_key( # pylint: disable=unused-variable + 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() +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_schema.py b/tests/test_schema.py index 8e0bd4fe..ede75251 100755 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -17,435 +17,495 @@ Unit test for 'schema.py' """ -import unittest import re +import unittest import securesystemslib.exceptions import securesystemslib.schema as SCHEMA - -class TestSchema(unittest.TestCase): - def setUp(self): - pass - - - - def tearDown(self): - pass - - - - def test_Schema(self): - # Test conditions for the instantation of classes that inherit - # from class Schema(). - class NewSchema(SCHEMA.Schema): - def __init__(self): +class TestSchema(unittest.TestCase): # pylint: disable=missing-class-docstring + def setUp(self): pass - new_schema = NewSchema() - self.assertRaises(NotImplementedError, new_schema.matches, 'test') - - # Define a new schema. - class NewSchema2(SCHEMA.Schema): - def __init__(self, string): - self._string = string - - def check_match(self, object): - if self._string != object: - message = 'Expected: '+repr(self._string) - raise securesystemslib.exceptions.FormatError(message) - - new_schema2 = NewSchema2('test') - self.assertRaises(securesystemslib.exceptions.FormatError, - new_schema2.check_match, 'bad') - self.assertFalse(new_schema2.matches('bad')) - self.assertTrue(new_schema2.matches('test')) - - # Test conditions for invalid arguments. - self.assertRaises(securesystemslib.exceptions.FormatError, - new_schema2.check_match, True) - self.assertRaises(securesystemslib.exceptions.FormatError, - new_schema2.check_match, NewSchema2) - self.assertRaises(securesystemslib.exceptions.FormatError, - new_schema2.check_match, 123) - - self.assertFalse(new_schema2.matches(True)) - self.assertFalse(new_schema2.matches(NewSchema2)) - self.assertFalse(new_schema2.matches(123)) - - - - def test_Any(self): - # Test conditions for valid arguments. - any_schema = SCHEMA.Any() - - self.assertTrue(any_schema.matches('test')) - self.assertTrue(any_schema.matches(123)) - self.assertTrue(any_schema.matches(['test'])) - self.assertTrue(any_schema.matches({'word':'definition'})) - self.assertTrue(any_schema.matches(True)) - - - - def test_String(self): - # Test conditions for valid arguments. - string_schema = SCHEMA.String('test') - - self.assertTrue(string_schema.matches('test')) - - # Test conditions for invalid arguments. - self.assertFalse(string_schema.matches(True)) - self.assertFalse(string_schema.matches(['test'])) - self.assertFalse(string_schema.matches(SCHEMA.Schema)) - - # Test conditions for invalid arguments in a schema definition. - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.String, 1) - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.String, [1]) - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.String, {'a': 1}) - - - - def test_AnyString(self): - # Test conditions for valid arguments. - anystring_schema = SCHEMA.AnyString() - - self.assertTrue(anystring_schema.matches('')) - self.assertTrue(anystring_schema.matches('a string')) - - # Test conditions for invalid arguments. - self.assertFalse(anystring_schema.matches(['a'])) - self.assertFalse(anystring_schema.matches(3)) - self.assertFalse(anystring_schema.matches({'a': 'string'})) - - - - def test_AnyNonemptyString(self): - anynonemptystring_schema = SCHEMA.AnyNonemptyString() - - self.assertTrue(anynonemptystring_schema.matches("foo")) - - # Test conditions for invalid arguments. - self.assertFalse(anynonemptystring_schema.matches('')) - self.assertFalse(anynonemptystring_schema.matches(['a'])) - self.assertFalse(anynonemptystring_schema.matches(3)) - self.assertFalse(anynonemptystring_schema.matches({'a': 'string'})) - - - - def test_OneOf(self): - # Test conditions for valid arguments. - oneof_schema = SCHEMA.OneOf([SCHEMA.ListOf(SCHEMA.Integer()), - SCHEMA.String('Hello'), SCHEMA.String('bye')]) - - self.assertTrue(oneof_schema.matches([])) - self.assertTrue(oneof_schema.matches('bye')) - self.assertTrue(oneof_schema.matches([1,2])) - - # Test conditions for invalid arguments. - self.assertFalse(oneof_schema.matches(3)) - self.assertFalse(oneof_schema.matches(['Hi'])) - - # Test conditions for invalid arguments in a schema definition. - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.OneOf, 1) - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.OneOf, [1]) - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.OneOf, {'a': 1}) - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.OneOf, [SCHEMA.AnyString(), 1]) - - - - def test_AllOf(self): - # Test conditions for valid arguments. - allof_schema = SCHEMA.AllOf([SCHEMA.Any(), - SCHEMA.AnyString(), SCHEMA.String('a')]) - - self.assertTrue(allof_schema.matches('a')) - - # Test conditions for invalid arguments. - self.assertFalse(allof_schema.matches('b')) - - # Test conditions for invalid arguments in a schema definition. - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.AllOf, 1) - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.AllOf, [1]) - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.AllOf, {'a': 1}) - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.AllOf, [SCHEMA.AnyString(), 1]) - - - def test_Boolean(self): - # Test conditions for valid arguments. - boolean_schema = SCHEMA.Boolean() - - self.assertTrue(boolean_schema.matches(True) and boolean_schema.matches(False)) - - # Test conditions for invalid arguments. - self.assertFalse(boolean_schema.matches(11)) - - - - def test_ListOf(self): - # Test conditions for valid arguments. - listof_schema = SCHEMA.ListOf(SCHEMA.RegularExpression('(?:..)*')) - listof2_schema = SCHEMA.ListOf(SCHEMA.Integer(), - min_count=3, max_count=10) - - self.assertTrue(listof_schema.matches([])) - self.assertTrue(listof_schema.matches(['Hi', 'this', 'list', 'is', - 'full', 'of', 'even', 'strs'])) - - self.assertTrue(listof2_schema.matches([3]*3)) - self.assertTrue(listof2_schema.matches([3]*10)) - - # Test conditions for invalid arguments. - self.assertFalse(listof_schema.matches('hi')) - self.assertFalse(listof_schema.matches({})) - self.assertFalse(listof_schema.matches(['This', 'one', 'is not'])) - - self.assertFalse(listof2_schema.matches([3]*2)) - self.assertFalse(listof2_schema.matches(([3]*11))) - - # Test conditions for invalid arguments in a schema definition. - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.ListOf, 1) - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.ListOf, [1]) - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.ListOf, {'a': 1}) - - - - def test_Integer(self): - # Test conditions for valid arguments. - integer_schema = SCHEMA.Integer() - - self.assertTrue(integer_schema.matches(99)) - self.assertTrue(SCHEMA.Integer(lo=10, hi=30).matches(25)) - - # Test conditions for invalid arguments. - self.assertFalse(integer_schema.matches(False)) - self.assertFalse(integer_schema.matches('a string')) - self.assertFalse(SCHEMA.Integer(lo=10, hi=30).matches(5)) - - - - def test_DictOf(self): - # Test conditions for valid arguments. - dictof_schema = SCHEMA.DictOf(SCHEMA.RegularExpression(r'[aeiou]+'), - SCHEMA.Struct([SCHEMA.AnyString(), SCHEMA.AnyString()])) - - self.assertTrue(dictof_schema.matches({})) - self.assertTrue(dictof_schema.matches({'a': ['x', 'y'], 'e' : ['', '']})) - - # Test conditions for invalid arguments. - self.assertFalse(dictof_schema.matches('')) - self.assertFalse(dictof_schema.matches({'a': ['x', 3], 'e' : ['', '']})) - self.assertFalse(dictof_schema.matches({'a': ['x', 'y'], 'e' : ['', ''], - 'd' : ['a', 'b']})) - - # Test conditions for invalid arguments in a schema definition. - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.DictOf, 1, 1) - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.DictOf, [1], [1]) - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.DictOf, {'a': 1}, 1) - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.DictOf, SCHEMA.AnyString(), 1) - - - - def test_Optional(self): - # Test conditions for valid arguments. - optional_schema = SCHEMA.Object(k1=SCHEMA.String('X'), - k2=SCHEMA.Optional(SCHEMA.String('Y'))) - - self.assertTrue(optional_schema.matches({'k1': 'X', 'k2': 'Y'})) - self.assertTrue(optional_schema.matches({'k1': 'X'})) - - # Test conditions for invalid arguments. - self.assertFalse(optional_schema.matches({'k1': 'X', 'k2': 'Z'})) - - # Test conditions for invalid arguments in a schema definition. - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.Optional, 1) - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.Optional, [1]) - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.Optional, {'a': 1}) - - - - def test_Object(self): - # Test conditions for valid arguments. - object_schema = SCHEMA.Object(a=SCHEMA.AnyString(), - bc=SCHEMA.Struct([SCHEMA.Integer(), SCHEMA.Integer()])) - - self.assertTrue(object_schema.matches({'a':'ZYYY', 'bc':[5,9]})) - self.assertTrue(object_schema.matches({'a':'ZYYY', 'bc':[5,9], 'xx':5})) - - # Test conditions for invalid arguments. - self.assertFalse(object_schema.matches({'a':'ZYYY', 'bc':[5,9,3]})) - self.assertFalse(object_schema.matches({'a':'ZYYY'})) - - # Test conditions for invalid arguments in a schema definition. - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.Object, a='a') - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.Object, a=[1]) - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.Object, a=SCHEMA.AnyString(), b=1) - - # Test condition for invalid non-dict arguments. - self.assertFalse(object_schema.matches([{'a':'XYZ'}])) - self.assertFalse(object_schema.matches(8)) - - - - def test_Struct(self): - # Test conditions for valid arguments. - struct_schema = SCHEMA.Struct([SCHEMA.ListOf(SCHEMA.AnyString()), - SCHEMA.AnyString(), SCHEMA.String('X')]) - struct2_schema = SCHEMA.Struct([SCHEMA.String('X')], allow_more=True) - struct3_schema = SCHEMA.Struct([SCHEMA.String('X'), - SCHEMA.Integer()], [SCHEMA.Integer()]) - - self.assertTrue(struct_schema.matches([[], 'Q', 'X'])) - - self.assertTrue(struct2_schema.matches(['X'])) - self.assertTrue(struct2_schema.matches(['X', 'Y'])) - self.assertTrue(struct2_schema.matches(['X', ['Y', 'Z']])) - - self.assertTrue(struct3_schema.matches(['X', 3])) - self.assertTrue(struct3_schema.matches(['X', 3, 9])) - - # Test conditions for invalid arguments. - self.assertFalse(struct_schema.matches(False)) - self.assertFalse(struct_schema.matches('Foo')) - self.assertFalse(struct_schema.matches([[], 'Q', 'D'])) - self.assertFalse(struct_schema.matches([[3], 'Q', 'X'])) - self.assertFalse(struct_schema.matches([[], 'Q', 'X', 'Y'])) - - self.assertFalse(struct2_schema.matches([])) - self.assertFalse(struct2_schema.matches([['X']])) - - self.assertFalse(struct3_schema.matches([])) - self.assertFalse(struct3_schema.matches({})) - self.assertFalse(struct3_schema.matches(['X'])) - self.assertFalse(struct3_schema.matches(['X', 3, 9, 11])) - self.assertFalse(struct3_schema.matches(['X', 3, 'A'])) - - # Test conditions for invalid arguments in a schema definition. - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.Struct, 1) - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.Struct, [1]) - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.Struct, {'a': 1}) - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.Struct, [SCHEMA.AnyString(), 1]) - - - - def test_RegularExpression(self): - # Test conditions for valid arguments. - # RegularExpression(pattern, modifiers, re_object, re_name). - re_schema = SCHEMA.RegularExpression('h.*d') - - self.assertTrue(re_schema.matches('hello world')) - - # Provide a pattern that contains the trailing '$' - re_schema_2 = SCHEMA.RegularExpression(pattern='abc$', - modifiers=0, re_object=None, re_name='my_re') - - self.assertTrue(re_schema_2.matches('abc')) - - # Test for valid optional arguments. - compiled_re = re.compile('^[a-z].*') - re_schema_optional = SCHEMA.RegularExpression(pattern='abc', - modifiers=0, re_object=compiled_re, re_name='my_re') - self.assertTrue(re_schema_optional.matches('abc')) - - # Valid arguments, but the 'pattern' argument is unset (required if the - # 're_object' is 'None'.) - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.RegularExpression, None, 0, None, None) - - # Valid arguments, 're_name' is unset, and 'pattern' is None. An exception - # is not raised, but 're_name' is set to 'pattern'. - re_schema_optional = SCHEMA.RegularExpression(pattern=None, - modifiers=0, re_object=compiled_re, re_name=None) - - self.assertTrue(re_schema_optional.matches('abc')) - self.assertTrue(re_schema_optional._re_name == 'pattern') - - # Test conditions for invalid arguments. - self.assertFalse(re_schema.matches('Hello World')) - self.assertFalse(re_schema.matches('hello world!')) - self.assertFalse(re_schema.matches([33, 'Hello'])) - - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.RegularExpression, 8) - - - - def test_LengthString(self): - # Test conditions for valid arguments. - length_string = SCHEMA.LengthString(11) - - self.assertTrue(length_string.matches('Hello World')) - self.assertTrue(length_string.matches('Hello Marty')) - - # Test conditions for invalid arguments. - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.LengthString, 'hello') - - self.assertFalse(length_string.matches('hello')) - self.assertFalse(length_string.matches(8)) - - - - def test_LengthBytes(self): - # Test conditions for valid arguments. - length_bytes = SCHEMA.LengthBytes(11) - - self.assertTrue(length_bytes.matches(b'Hello World')) - self.assertTrue(length_bytes.matches(b'Hello Marty')) - - # Test conditions for invalid arguments. - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.LengthBytes, 'hello') - self.assertRaises(securesystemslib.exceptions.FormatError, - SCHEMA.LengthBytes, True) - - self.assertFalse(length_bytes.matches(b'hello')) - self.assertFalse(length_bytes.matches(8)) - - - - def test_AnyBytes(self): - # Test conditions for valid arguments. - anybytes_schema = SCHEMA.AnyBytes() - - self.assertTrue(anybytes_schema.matches(b'')) - self.assertTrue(anybytes_schema.matches(b'a string')) + def tearDown(self): + pass - # Test conditions for invalid arguments. - self.assertFalse(anybytes_schema.matches('a string')) - self.assertFalse(anybytes_schema.matches(['a'])) - self.assertFalse(anybytes_schema.matches(3)) - self.assertFalse(anybytes_schema.matches({'a': 'string'})) + def test_Schema(self): + # Test conditions for the instantation of classes that inherit + # from class Schema(). + class NewSchema(SCHEMA.Schema): # pylint: disable=abstract-method + def __init__(self): + pass + + new_schema = NewSchema() + self.assertRaises(NotImplementedError, new_schema.matches, "test") + + # Define a new schema. + class NewSchema2(SCHEMA.Schema): + def __init__(self, string): + self._string = string + + def check_match(self, object): # pylint: disable=redefined-builtin + if self._string != object: + message = "Expected: " + repr(self._string) + raise securesystemslib.exceptions.FormatError(message) + + new_schema2 = NewSchema2("test") + self.assertRaises( + securesystemslib.exceptions.FormatError, + new_schema2.check_match, + "bad", + ) + self.assertFalse(new_schema2.matches("bad")) + self.assertTrue(new_schema2.matches("test")) + + # Test conditions for invalid arguments. + self.assertRaises( + securesystemslib.exceptions.FormatError, + new_schema2.check_match, + True, + ) + self.assertRaises( + securesystemslib.exceptions.FormatError, + new_schema2.check_match, + NewSchema2, + ) + self.assertRaises( + securesystemslib.exceptions.FormatError, + new_schema2.check_match, + 123, + ) + + self.assertFalse(new_schema2.matches(True)) + self.assertFalse(new_schema2.matches(NewSchema2)) + self.assertFalse(new_schema2.matches(123)) + + def test_Any(self): + # Test conditions for valid arguments. + any_schema = SCHEMA.Any() + + self.assertTrue(any_schema.matches("test")) + self.assertTrue(any_schema.matches(123)) + self.assertTrue(any_schema.matches(["test"])) + self.assertTrue(any_schema.matches({"word": "definition"})) + self.assertTrue(any_schema.matches(True)) + + def test_String(self): + # Test conditions for valid arguments. + string_schema = SCHEMA.String("test") + + self.assertTrue(string_schema.matches("test")) + + # Test conditions for invalid arguments. + self.assertFalse(string_schema.matches(True)) + self.assertFalse(string_schema.matches(["test"])) + self.assertFalse(string_schema.matches(SCHEMA.Schema)) + + # Test conditions for invalid arguments in a schema definition. + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.String, 1 + ) + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.String, [1] + ) + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.String, {"a": 1} + ) + + def test_AnyString(self): + # Test conditions for valid arguments. + anystring_schema = SCHEMA.AnyString() + + self.assertTrue(anystring_schema.matches("")) + self.assertTrue(anystring_schema.matches("a string")) + + # Test conditions for invalid arguments. + self.assertFalse(anystring_schema.matches(["a"])) + self.assertFalse(anystring_schema.matches(3)) + self.assertFalse(anystring_schema.matches({"a": "string"})) + + def test_AnyNonemptyString(self): + anynonemptystring_schema = SCHEMA.AnyNonemptyString() + + self.assertTrue(anynonemptystring_schema.matches("foo")) + + # Test conditions for invalid arguments. + self.assertFalse(anynonemptystring_schema.matches("")) + self.assertFalse(anynonemptystring_schema.matches(["a"])) + self.assertFalse(anynonemptystring_schema.matches(3)) + self.assertFalse(anynonemptystring_schema.matches({"a": "string"})) + + def test_OneOf(self): + # Test conditions for valid arguments. + oneof_schema = SCHEMA.OneOf( + [ + SCHEMA.ListOf(SCHEMA.Integer()), + SCHEMA.String("Hello"), + SCHEMA.String("bye"), + ] + ) + + self.assertTrue(oneof_schema.matches([])) + self.assertTrue(oneof_schema.matches("bye")) + self.assertTrue(oneof_schema.matches([1, 2])) + + # Test conditions for invalid arguments. + self.assertFalse(oneof_schema.matches(3)) + self.assertFalse(oneof_schema.matches(["Hi"])) + + # Test conditions for invalid arguments in a schema definition. + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.OneOf, 1 + ) + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.OneOf, [1] + ) + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.OneOf, {"a": 1} + ) + self.assertRaises( + securesystemslib.exceptions.FormatError, + SCHEMA.OneOf, + [SCHEMA.AnyString(), 1], + ) + + def test_AllOf(self): + # Test conditions for valid arguments. + allof_schema = SCHEMA.AllOf( + [SCHEMA.Any(), SCHEMA.AnyString(), SCHEMA.String("a")] + ) + + self.assertTrue(allof_schema.matches("a")) + + # Test conditions for invalid arguments. + self.assertFalse(allof_schema.matches("b")) + + # Test conditions for invalid arguments in a schema definition. + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.AllOf, 1 + ) + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.AllOf, [1] + ) + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.AllOf, {"a": 1} + ) + self.assertRaises( + securesystemslib.exceptions.FormatError, + SCHEMA.AllOf, + [SCHEMA.AnyString(), 1], + ) + + def test_Boolean(self): + # Test conditions for valid arguments. + boolean_schema = SCHEMA.Boolean() + + self.assertTrue( + boolean_schema.matches(True) and boolean_schema.matches(False) + ) + + # Test conditions for invalid arguments. + self.assertFalse(boolean_schema.matches(11)) + + def test_ListOf(self): + # Test conditions for valid arguments. + listof_schema = SCHEMA.ListOf(SCHEMA.RegularExpression("(?:..)*")) + listof2_schema = SCHEMA.ListOf( + SCHEMA.Integer(), min_count=3, max_count=10 + ) + + self.assertTrue(listof_schema.matches([])) + self.assertTrue( + listof_schema.matches( + ["Hi", "this", "list", "is", "full", "of", "even", "strs"] + ) + ) + + self.assertTrue(listof2_schema.matches([3] * 3)) + self.assertTrue(listof2_schema.matches([3] * 10)) + + # Test conditions for invalid arguments. + self.assertFalse(listof_schema.matches("hi")) + self.assertFalse(listof_schema.matches({})) + self.assertFalse(listof_schema.matches(["This", "one", "is not"])) + + self.assertFalse(listof2_schema.matches([3] * 2)) + self.assertFalse(listof2_schema.matches(([3] * 11))) + + # Test conditions for invalid arguments in a schema definition. + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.ListOf, 1 + ) + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.ListOf, [1] + ) + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.ListOf, {"a": 1} + ) + + def test_Integer(self): + # Test conditions for valid arguments. + integer_schema = SCHEMA.Integer() + + self.assertTrue(integer_schema.matches(99)) + self.assertTrue(SCHEMA.Integer(lo=10, hi=30).matches(25)) + + # Test conditions for invalid arguments. + self.assertFalse(integer_schema.matches(False)) + self.assertFalse(integer_schema.matches("a string")) + self.assertFalse(SCHEMA.Integer(lo=10, hi=30).matches(5)) + + def test_DictOf(self): + # Test conditions for valid arguments. + dictof_schema = SCHEMA.DictOf( + SCHEMA.RegularExpression(r"[aeiou]+"), + SCHEMA.Struct([SCHEMA.AnyString(), SCHEMA.AnyString()]), + ) + + self.assertTrue(dictof_schema.matches({})) + self.assertTrue(dictof_schema.matches({"a": ["x", "y"], "e": ["", ""]})) + + # Test conditions for invalid arguments. + self.assertFalse(dictof_schema.matches("")) + self.assertFalse(dictof_schema.matches({"a": ["x", 3], "e": ["", ""]})) + self.assertFalse( + dictof_schema.matches( + {"a": ["x", "y"], "e": ["", ""], "d": ["a", "b"]} + ) + ) + + # Test conditions for invalid arguments in a schema definition. + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.DictOf, 1, 1 + ) + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.DictOf, [1], [1] + ) + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.DictOf, {"a": 1}, 1 + ) + self.assertRaises( + securesystemslib.exceptions.FormatError, + SCHEMA.DictOf, + SCHEMA.AnyString(), + 1, + ) + + def test_Optional(self): + # Test conditions for valid arguments. + optional_schema = SCHEMA.Object( + k1=SCHEMA.String("X"), k2=SCHEMA.Optional(SCHEMA.String("Y")) + ) + + self.assertTrue(optional_schema.matches({"k1": "X", "k2": "Y"})) + self.assertTrue(optional_schema.matches({"k1": "X"})) + + # Test conditions for invalid arguments. + self.assertFalse(optional_schema.matches({"k1": "X", "k2": "Z"})) + + # Test conditions for invalid arguments in a schema definition. + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.Optional, 1 + ) + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.Optional, [1] + ) + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.Optional, {"a": 1} + ) + + def test_Object(self): + # Test conditions for valid arguments. + object_schema = SCHEMA.Object( + a=SCHEMA.AnyString(), + bc=SCHEMA.Struct([SCHEMA.Integer(), SCHEMA.Integer()]), + ) + + self.assertTrue(object_schema.matches({"a": "ZYYY", "bc": [5, 9]})) + self.assertTrue( + object_schema.matches({"a": "ZYYY", "bc": [5, 9], "xx": 5}) + ) + + # Test conditions for invalid arguments. + self.assertFalse(object_schema.matches({"a": "ZYYY", "bc": [5, 9, 3]})) + self.assertFalse(object_schema.matches({"a": "ZYYY"})) + + # Test conditions for invalid arguments in a schema definition. + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.Object, a="a" + ) + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.Object, a=[1] + ) + self.assertRaises( + securesystemslib.exceptions.FormatError, + SCHEMA.Object, + a=SCHEMA.AnyString(), + b=1, + ) + + # Test condition for invalid non-dict arguments. + self.assertFalse(object_schema.matches([{"a": "XYZ"}])) + self.assertFalse(object_schema.matches(8)) + + def test_Struct(self): + # Test conditions for valid arguments. + struct_schema = SCHEMA.Struct( + [ + SCHEMA.ListOf(SCHEMA.AnyString()), + SCHEMA.AnyString(), + SCHEMA.String("X"), + ] + ) + struct2_schema = SCHEMA.Struct([SCHEMA.String("X")], allow_more=True) + struct3_schema = SCHEMA.Struct( + [SCHEMA.String("X"), SCHEMA.Integer()], [SCHEMA.Integer()] + ) + + self.assertTrue(struct_schema.matches([[], "Q", "X"])) + + self.assertTrue(struct2_schema.matches(["X"])) + self.assertTrue(struct2_schema.matches(["X", "Y"])) + self.assertTrue(struct2_schema.matches(["X", ["Y", "Z"]])) + + self.assertTrue(struct3_schema.matches(["X", 3])) + self.assertTrue(struct3_schema.matches(["X", 3, 9])) + + # Test conditions for invalid arguments. + self.assertFalse(struct_schema.matches(False)) + self.assertFalse(struct_schema.matches("Foo")) + self.assertFalse(struct_schema.matches([[], "Q", "D"])) + self.assertFalse(struct_schema.matches([[3], "Q", "X"])) + self.assertFalse(struct_schema.matches([[], "Q", "X", "Y"])) + + self.assertFalse(struct2_schema.matches([])) + self.assertFalse(struct2_schema.matches([["X"]])) + + self.assertFalse(struct3_schema.matches([])) + self.assertFalse(struct3_schema.matches({})) + self.assertFalse(struct3_schema.matches(["X"])) + self.assertFalse(struct3_schema.matches(["X", 3, 9, 11])) + self.assertFalse(struct3_schema.matches(["X", 3, "A"])) + + # Test conditions for invalid arguments in a schema definition. + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.Struct, 1 + ) + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.Struct, [1] + ) + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.Struct, {"a": 1} + ) + self.assertRaises( + securesystemslib.exceptions.FormatError, + SCHEMA.Struct, + [SCHEMA.AnyString(), 1], + ) + + def test_RegularExpression(self): + # Test conditions for valid arguments. + # RegularExpression(pattern, modifiers, re_object, re_name). + re_schema = SCHEMA.RegularExpression("h.*d") + + self.assertTrue(re_schema.matches("hello world")) + + # Provide a pattern that contains the trailing '$' + re_schema_2 = SCHEMA.RegularExpression( + pattern="abc$", modifiers=0, re_object=None, re_name="my_re" + ) + + self.assertTrue(re_schema_2.matches("abc")) + + # Test for valid optional arguments. + compiled_re = re.compile("^[a-z].*") + re_schema_optional = SCHEMA.RegularExpression( + pattern="abc", modifiers=0, re_object=compiled_re, re_name="my_re" + ) + self.assertTrue(re_schema_optional.matches("abc")) + + # Valid arguments, but the 'pattern' argument is unset (required if the + # 're_object' is 'None'.) + self.assertRaises( + securesystemslib.exceptions.FormatError, + SCHEMA.RegularExpression, + None, + 0, + None, + None, + ) + + # Valid arguments, 're_name' is unset, and 'pattern' is None. An exception + # is not raised, but 're_name' is set to 'pattern'. + re_schema_optional = SCHEMA.RegularExpression( + pattern=None, modifiers=0, re_object=compiled_re, re_name=None + ) + + self.assertTrue(re_schema_optional.matches("abc")) + self.assertTrue( + re_schema_optional._re_name # pylint: disable=protected-access + == "pattern" + ) + + # Test conditions for invalid arguments. + self.assertFalse(re_schema.matches("Hello World")) + self.assertFalse(re_schema.matches("hello world!")) + self.assertFalse(re_schema.matches([33, "Hello"])) + + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.RegularExpression, 8 + ) + + def test_LengthString(self): + # Test conditions for valid arguments. + length_string = SCHEMA.LengthString(11) + + self.assertTrue(length_string.matches("Hello World")) + self.assertTrue(length_string.matches("Hello Marty")) + + # Test conditions for invalid arguments. + self.assertRaises( + securesystemslib.exceptions.FormatError, + SCHEMA.LengthString, + "hello", + ) + + self.assertFalse(length_string.matches("hello")) + self.assertFalse(length_string.matches(8)) + + def test_LengthBytes(self): + # Test conditions for valid arguments. + length_bytes = SCHEMA.LengthBytes(11) + + self.assertTrue(length_bytes.matches(b"Hello World")) + self.assertTrue(length_bytes.matches(b"Hello Marty")) + + # Test conditions for invalid arguments. + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.LengthBytes, "hello" + ) + self.assertRaises( + securesystemslib.exceptions.FormatError, SCHEMA.LengthBytes, True + ) + + self.assertFalse(length_bytes.matches(b"hello")) + self.assertFalse(length_bytes.matches(8)) + + def test_AnyBytes(self): + # Test conditions for valid arguments. + anybytes_schema = SCHEMA.AnyBytes() + + self.assertTrue(anybytes_schema.matches(b"")) + self.assertTrue(anybytes_schema.matches(b"a string")) + + # Test conditions for invalid arguments. + self.assertFalse(anybytes_schema.matches("a string")) + self.assertFalse(anybytes_schema.matches(["a"])) + self.assertFalse(anybytes_schema.matches(3)) + self.assertFalse(anybytes_schema.matches({"a": "string"})) # Run the unit tests. -if __name__ == '__main__': - unittest.main() +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_signer.py b/tests/test_signer.py index f2018ede..e87736cb 100644 --- a/tests/test_signer.py +++ b/tests/test_signer.py @@ -4,28 +4,36 @@ import copy import os -import unittest -import tempfile import shutil +import tempfile +import unittest import securesystemslib.formats import securesystemslib.keys as KEYS from securesystemslib.exceptions import FormatError, UnsupportedAlgorithmError -from securesystemslib.signer import GPGSignature, Signature, SSlibSigner, GPGSigner from securesystemslib.gpg.constants import have_gpg -from securesystemslib.gpg.functions import export_pubkey, verify_signature as verify_sig - - -class TestSSlibSigner(unittest.TestCase): +from securesystemslib.gpg.functions import export_pubkey +from securesystemslib.gpg.functions import verify_signature as verify_sig +from securesystemslib.signer import ( + GPGSignature, + GPGSigner, + Signature, + SSlibSigner, +) + + +class TestSSlibSigner( + 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() cls.DATA_STR = "SOME DATA REQUIRING AUTHENTICITY." - cls.DATA = securesystemslib.formats.encode_canonical(cls.DATA_STR).encode( - "utf-8" - ) + cls.DATA = securesystemslib.formats.encode_canonical( + cls.DATA_STR + ).encode("utf-8") def test_sslib_sign(self): dicts = [self.rsakey_dict, self.ecdsakey_dict, self.ed25519key_dict] @@ -35,7 +43,9 @@ def test_sslib_sign(self): sig_obj = sslib_signer.sign(self.DATA) # Verify signature - verified = KEYS.verify_signature(scheme_dict, sig_obj.to_dict(), self.DATA) + verified = KEYS.verify_signature( + scheme_dict, sig_obj.to_dict(), self.DATA + ) self.assertTrue(verified, "Incorrect signature.") # Removing private key from "scheme_dict". diff --git a/tests/test_storage.py b/tests/test_storage.py index 2d1f92db..c0b24d81 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -16,89 +16,104 @@ """ import os -import tempfile import shutil +import tempfile import unittest import securesystemslib.exceptions import securesystemslib.storage -class TestStorage(unittest.TestCase): - def setUp(self): - self.storage_backend = securesystemslib.storage.FilesystemBackend() - self.temp_dir = tempfile.mkdtemp(dir=os.getcwd()) - self.filepath = os.path.join(self.temp_dir, 'testfile') - with open(self.filepath, 'wb') as test: - test.write(b'testing') - self.fileobj = open(self.filepath, 'rb') - - - def tearDown(self): - self.fileobj.close() - shutil.rmtree(self.temp_dir) - - - def test_exceptions(self): - try: - with self.storage_backend.get('/none/existent/path') as file_object: - file_object.read() - except Exception as exc: - self.assertIsInstance(exc, securesystemslib.exceptions.StorageError) - - self.assertRaises(securesystemslib.exceptions.StorageError, - self.storage_backend.put, self.fileobj, '/none/existent/path') - - self.assertRaises(securesystemslib.exceptions.StorageError, - self.storage_backend.getsize, '/none/existent/path') - - self.assertRaises(securesystemslib.exceptions.StorageError, - self.storage_backend.create_folder, '/none/existent/path') - - self.assertRaises(securesystemslib.exceptions.StorageError, - self.storage_backend.create_folder, '') - - self.assertRaises(securesystemslib.exceptions.StorageError, - self.storage_backend.list_folder, '/none/existent/path') - - - def test_files(self): - with self.storage_backend.get(self.filepath) as get_fileobj: - self.assertEqual(get_fileobj.read(), self.fileobj.read()) - - self.assertEqual(self.storage_backend.getsize(self.filepath), - os.path.getsize(self.filepath)) - - put_path = os.path.join(self.temp_dir, 'put') - with self.storage_backend.get(self.filepath) as get_fileobj: - self.storage_backend.put(get_fileobj, put_path) - self.fileobj.seek(0) - with open(put_path, 'rb') as put_file: - self.assertEqual(put_file.read(), self.fileobj.read()) - - self.assertTrue(os.path.exists(put_path)) - self.storage_backend.remove(put_path) - self.assertFalse(os.path.exists(put_path)) - - - def test_folders(self): - leaves = ['test1', 'test2', 'test3'] - folder = os.path.join(self.temp_dir, 'test_dir') - self.storage_backend.create_folder(folder) - for leaf in leaves: - with open(os.path.join(folder, leaf), 'wb') as fi: - fi.write(leaf.encode('utf-8')) - found_leaves = self.storage_backend.list_folder(folder) - self.assertListEqual(leaves, sorted(found_leaves)) - - - def test_singleton(self): - # There should only ever be a single instance of FilesystemBackend. - # An object's id is unique and constant for the object during its - # lifetime. Therefore create more than one instance of FilesystemBackend - # and compare their id's - fb1 = securesystemslib.storage.FilesystemBackend() - fb2 = securesystemslib.storage.FilesystemBackend() - self.assertEqual(id(fb1), id(fb2)) - self.assertEqual(id(self.storage_backend), id(fb1)) - self.assertEqual(id(fb2), id(self.storage_backend)) +class TestStorage(unittest.TestCase): # pylint: disable=missing-class-docstring + def setUp(self): + self.storage_backend = securesystemslib.storage.FilesystemBackend() + self.temp_dir = tempfile.mkdtemp(dir=os.getcwd()) + self.filepath = os.path.join(self.temp_dir, "testfile") + with open(self.filepath, "wb") as test: + test.write(b"testing") + self.fileobj = open( # pylint: disable=consider-using-with + self.filepath, "rb" + ) + + def tearDown(self): + self.fileobj.close() + shutil.rmtree(self.temp_dir) + + def test_exceptions(self): + try: + with self.storage_backend.get("/none/existent/path") as file_object: + file_object.read() + except Exception as exc: # pylint: disable=broad-except + self.assertIsInstance(exc, securesystemslib.exceptions.StorageError) + + self.assertRaises( + securesystemslib.exceptions.StorageError, + self.storage_backend.put, + self.fileobj, + "/none/existent/path", + ) + + self.assertRaises( + securesystemslib.exceptions.StorageError, + self.storage_backend.getsize, + "/none/existent/path", + ) + + self.assertRaises( + securesystemslib.exceptions.StorageError, + self.storage_backend.create_folder, + "/none/existent/path", + ) + + self.assertRaises( + securesystemslib.exceptions.StorageError, + self.storage_backend.create_folder, + "", + ) + + self.assertRaises( + securesystemslib.exceptions.StorageError, + self.storage_backend.list_folder, + "/none/existent/path", + ) + + def test_files(self): + with self.storage_backend.get(self.filepath) as get_fileobj: + self.assertEqual(get_fileobj.read(), self.fileobj.read()) + + self.assertEqual( + self.storage_backend.getsize(self.filepath), + os.path.getsize(self.filepath), + ) + + put_path = os.path.join(self.temp_dir, "put") + with self.storage_backend.get(self.filepath) as get_fileobj: + self.storage_backend.put(get_fileobj, put_path) + self.fileobj.seek(0) + with open(put_path, "rb") as put_file: + self.assertEqual(put_file.read(), self.fileobj.read()) + + self.assertTrue(os.path.exists(put_path)) + self.storage_backend.remove(put_path) + self.assertFalse(os.path.exists(put_path)) + + def test_folders(self): + leaves = ["test1", "test2", "test3"] + folder = os.path.join(self.temp_dir, "test_dir") + self.storage_backend.create_folder(folder) + for leaf in leaves: + with open(os.path.join(folder, leaf), "wb") as fi: + fi.write(leaf.encode("utf-8")) + found_leaves = self.storage_backend.list_folder(folder) + self.assertListEqual(leaves, sorted(found_leaves)) + + def test_singleton(self): + # There should only ever be a single instance of FilesystemBackend. + # An object's id is unique and constant for the object during its + # lifetime. Therefore create more than one instance of FilesystemBackend + # and compare their id's + fb1 = securesystemslib.storage.FilesystemBackend() + fb2 = securesystemslib.storage.FilesystemBackend() + self.assertEqual(id(fb1), id(fb2)) + self.assertEqual(id(self.storage_backend), id(fb1)) + self.assertEqual(id(fb2), id(self.storage_backend)) diff --git a/tests/test_util.py b/tests/test_util.py index ee3908f3..55747e8b 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -17,315 +17,413 @@ Unit test for 'util.py' """ -import os -import sys -import shutil import logging +import os +import shutil # pylint: disable=unused-import +import stat +import sys # pylint: disable=unused-import import tempfile -import unittest import timeit -import stat +import unittest -import securesystemslib.settings +import securesystemslib.exceptions as exceptions # pylint: disable=consider-using-from-import import securesystemslib.hash +import securesystemslib.settings +import securesystemslib.unittest_toolbox as unittest_toolbox # pylint: disable=consider-using-from-import import securesystemslib.util -import securesystemslib.unittest_toolbox as unittest_toolbox -import securesystemslib.exceptions as exceptions logger = logging.getLogger(__name__) -class TestUtil(unittest_toolbox.Modified_TestCase): - - def setUp(self): - unittest_toolbox.Modified_TestCase.setUp(self) - self.temp_fileobj = tempfile.TemporaryFile() - - - def tearDown(self): - unittest_toolbox.Modified_TestCase.tearDown(self) - self.temp_fileobj.close() - - - - def test_B1_get_file_details(self): - # Goal: Verify proper output given certain expected/unexpected input. - - # Making a temporary file. - filepath = self.make_temp_data_file() - - # Computing the hash and length of the tempfile. - digest_object = securesystemslib.hash.digest_filename(filepath, algorithm='sha256') - file_hash = {'sha256' : digest_object.hexdigest()} - file_length = os.path.getsize(filepath) - - # Test: Expected input. - self.assertEqual(securesystemslib.util.get_file_details(filepath), - (file_length, file_hash)) - - # Test: Incorrect input. - bogus_inputs = [self.random_string(), 1234, [self.random_string()], - {'a': 'b'}, None] - - for bogus_input in bogus_inputs: - if isinstance(bogus_input, str): - self.assertRaises(securesystemslib.exceptions.StorageError, - securesystemslib.util.get_file_details, bogus_input) - else: - self.assertRaises(securesystemslib.exceptions.FormatError, - securesystemslib.util.get_file_details, bogus_input) - - - - def test_B2_get_file_hashes(self): - # Goal: Verify proper output given certain expected/unexpected input. - - # Making a temporary file. - filepath = self.make_temp_data_file() - - # Computing the hash of the tempfile. - digest_object = securesystemslib.hash.digest_filename(filepath, algorithm='sha256') - file_hash = {'sha256' : digest_object.hexdigest()} - - # Test: Expected input. - self.assertEqual(securesystemslib.util.get_file_hashes(filepath), - file_hash) - - # Test: Incorrect input. - bogus_inputs = [self.random_string(), 1234, [self.random_string()], - {'a': 'b'}, None] - - for bogus_input in bogus_inputs: - if isinstance(bogus_input, str): - self.assertRaises(securesystemslib.exceptions.StorageError, - securesystemslib.util.get_file_hashes, bogus_input) - else: - self.assertRaises(securesystemslib.exceptions.FormatError, - securesystemslib.util.get_file_hashes, bogus_input) - - - - def test_B3_get_file_length(self): - # Goal: Verify proper output given certain expected/unexpected input. - - # Making a temporary file. - filepath = self.make_temp_data_file() - - # Computing the length of the tempfile. - digest_object = securesystemslib.hash.digest_filename(filepath, algorithm='sha256') - file_length = os.path.getsize(filepath) - - # Test: Expected input. - self.assertEqual(securesystemslib.util.get_file_length(filepath), file_length) - - # Test: Incorrect input. - bogus_inputs = [self.random_string(), 1234, [self.random_string()], - {'a': 'b'}, None] - - for bogus_input in bogus_inputs: - if isinstance(bogus_input, str): - self.assertRaises(securesystemslib.exceptions.StorageError, - securesystemslib.util.get_file_length, bogus_input) - else: - self.assertRaises(securesystemslib.exceptions.FormatError, - securesystemslib.util.get_file_length, bogus_input) - - - - def test_B4_ensure_parent_dir(self): - existing_parent_dir = self.make_temp_directory() - non_existing_parent_dir = os.path.join(existing_parent_dir, 'a', 'b') - - for parent_dir in [existing_parent_dir, non_existing_parent_dir, 12, [3]]: - if isinstance(parent_dir, str): - securesystemslib.util.ensure_parent_dir(os.path.join(parent_dir, 'a.txt')) - self.assertTrue(os.path.isdir(parent_dir)) - - else: - self.assertRaises(securesystemslib.exceptions.FormatError, - securesystemslib.util.ensure_parent_dir, parent_dir) - - # Check that when a folder cannot be created a StorageError is thrown - with self.assertRaises(securesystemslib.exceptions.StorageError): - securesystemslib.util.ensure_parent_dir("/a/file.txt") - - # When we call ensure_parent_dir with filepath arg like "a.txt", - # then the directory of that filepath will be an empty string. - # We want to make sure that securesyslib.storage.create_folder() - # won't be called with an empty string and thus raise an exception. - # If an exception is thrown the test will fail. - securesystemslib.util.ensure_parent_dir('a.txt') - - - - def test_B5_file_in_confined_directories(self): - # Goal: Provide invalid input for 'filepath' and 'confined_directories'. - # Include inputs like: '[1, 2, "a"]' and such... - # Reference to 'file_in_confined_directories()' to improve readability. - in_confined_directory = securesystemslib.util.file_in_confined_directories - list_of_confined_directories = ['a', 12, {'a':'a'}, [1]] - list_of_filepaths = [12, ['a'], {'a':'a'}, 'a'] - for bogus_confined_directory in list_of_confined_directories: - for filepath in list_of_filepaths: - self.assertRaises(securesystemslib.exceptions.FormatError, - in_confined_directory, filepath, bogus_confined_directory) - - # Test: Inputs that evaluate to False. - confined_directories = ['a/b/', 'a/b/c/d/'] - self.assertFalse(in_confined_directory('a/b/c/1.txt', confined_directories)) - - confined_directories = ['a/b/c/d/e/'] - self.assertFalse(in_confined_directory('a', confined_directories)) - self.assertFalse(in_confined_directory('a/b', confined_directories)) - self.assertFalse(in_confined_directory('a/b/c', confined_directories)) - self.assertFalse(in_confined_directory('a/b/c/d', confined_directories)) - # Below, 'e' is a file in the 'a/b/c/d/' directory. - self.assertFalse(in_confined_directory('a/b/c/d/e', confined_directories)) - - # Test: Inputs that evaluate to True. - self.assertTrue(in_confined_directory('a/b/c.txt', [''])) - self.assertTrue(in_confined_directory('a/b/c.txt', ['a/b/'])) - self.assertTrue(in_confined_directory('a/b/c.txt', ['x', ''])) - self.assertTrue(in_confined_directory('a/b/c/..', ['a/'])) - - - - def test_B7_load_json_string(self): - # Test normal case. - data = ['a', {'b': ['c', None, 30.3, 29]}] - json_string = securesystemslib.util.json.dumps(data) - self.assertEqual(data, securesystemslib.util.load_json_string(json_string)) - - # Test invalid arguments. - self.assertRaises(securesystemslib.exceptions.Error, - securesystemslib.util.load_json_string, 8) - invalid_json_string = json_string + '.' - self.assertRaises(securesystemslib.exceptions.Error, - securesystemslib.util.load_json_string, invalid_json_string) - - - - def test_B8_load_json_file(self): - data = ['a', {'b': ['c', None, 30.3, 29]}] - filepath = self.make_temp_file() - fileobj = open(filepath, 'wt') - securesystemslib.util.json.dump(data, fileobj) - fileobj.close() - self.assertEqual(data, securesystemslib.util.load_json_file(filepath)) - - # Improperly formatted arguments. - for bogus_arg in [1, [b'a'], {'a':b'b'}]: - self.assertRaises(securesystemslib.exceptions.FormatError, - securesystemslib.util.load_json_file, bogus_arg) - - # Non-existent path. - self.assertRaises(securesystemslib.exceptions.StorageError, - securesystemslib.util.load_json_file, 'non-existent.json') - - # Invalid JSON content. - filepath_bad_data = self.make_temp_file() - fileobj = open(filepath_bad_data, 'wt') - fileobj.write('junk data') - fileobj.close() - - self.assertRaises(securesystemslib.exceptions.Error, - securesystemslib.util.load_json_file, filepath_bad_data) - - - - def test_B9_persist_temp_file(self): - # Destination directory to save the temporary file in. - dest_temp_dir = self.make_temp_directory() - - # Test the default of persisting the file and closing the tmpfile - dest_path = os.path.join(dest_temp_dir, self.random_string()) - tmpfile = tempfile.TemporaryFile() - tmpfile.write(self.random_string().encode('utf-8')) - - # Write a file with restricted permissions - securesystemslib.util.persist_temp_file(tmpfile, dest_path, restrict=True) - self.assertTrue(dest_path) - - # Need to set also the stat.S_IFREG bit to match the st_mode output - # stat.S_IFREG - Regular file - expected_mode = stat.S_IFREG | stat.S_IRUSR | stat.S_IWUSR - if os.name == 'nt': - # Windows only supports setting the read-only attribute. - expected_mode = stat.S_IFREG | stat.S_IWUSR | stat.S_IRUSR | stat.S_IWGRP | stat.S_IRGRP | stat.S_IWOTH | stat.S_IROTH - self.assertEqual(os.stat(dest_path).st_mode, expected_mode) - self.assertTrue(tmpfile.closed) - - # Test persisting a file without automatically closing the tmpfile - dest_path2 = os.path.join(dest_temp_dir, self.random_string()) - tmpfile = tempfile.TemporaryFile() - tmpfile.write(self.random_string().encode('utf-8')) - securesystemslib.util.persist_temp_file(tmpfile, dest_path2, - should_close=False) - self.assertFalse(tmpfile.closed) - - # Test persisting a file with an empty filename - with self.assertRaises(exceptions.StorageError): - securesystemslib.util.persist_temp_file(tmpfile, "") - - tmpfile.close() - - - def test_C5_unittest_toolbox_make_temp_directory(self): - # Verify that the tearDown function does not fail when - # unittest_toolbox.make_temp_directory deletes the generated temp directory - # here. - temp_directory = self.make_temp_directory() - os.rmdir(temp_directory) - - - - def test_c5_unittest_toolbox_random_path(self): - # Verify that a random path can be generated with unittest_toolbox. - random_path = self.random_path(length=10) - self.assertTrue(securesystemslib.formats.PATH_SCHEMA.matches(random_path)) - self.assertTrue(10, len(random_path)) - - - - def test_digests_are_equal(self): - digest = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' - - # Normal case: test for digests that are equal. - self.assertTrue(securesystemslib.util.digests_are_equal(digest, digest)) - - # Normal case: test for digests that are unequal. - self.assertFalse(securesystemslib.util.digests_are_equal(digest, '0a8df1')) - - # Test for invalid arguments. - self.assertRaises(securesystemslib.exceptions.FormatError, - securesystemslib.util.digests_are_equal, 7, digest) - self.assertRaises(securesystemslib.exceptions.FormatError, - securesystemslib.util.digests_are_equal, digest, 7) - - # Test that digests_are_equal() takes the same amount of time to compare - # equal and unequal arguments. - runtime = timeit.timeit('digests_are_equal("ab8df", "ab8df")', - setup='from securesystemslib.util import digests_are_equal', - number=100000) - - runtime2 = timeit.timeit('digests_are_equal("ab8df", "1b8df")', - setup='from securesystemslib.util import digests_are_equal', - number=100000) - - runtime3 = timeit.timeit('"ab8df" == "ab8df"', number=100000) - - runtime4 = timeit.timeit('"ab8df" == "1b8df"', number=1000000) - - # The ratio for the 'digest_are_equal' runtimes should be at or near 1. - ratio_digests_are_equal = abs(runtime2 / runtime) - - # The ratio for the variable-time runtimes should be (>1) & at or near 10? - ratio_variable_compare = abs(runtime4 / runtime3) - - self.assertTrue(ratio_digests_are_equal < ratio_variable_compare) - +class TestUtil( + unittest_toolbox.Modified_TestCase +): # pylint: disable=missing-class-docstring + def setUp(self): + unittest_toolbox.Modified_TestCase.setUp(self) + self.temp_fileobj = tempfile.TemporaryFile() + + def tearDown(self): + unittest_toolbox.Modified_TestCase.tearDown(self) + self.temp_fileobj.close() + + def test_B1_get_file_details(self): + # Goal: Verify proper output given certain expected/unexpected input. + + # Making a temporary file. + filepath = self.make_temp_data_file() + + # Computing the hash and length of the tempfile. + digest_object = securesystemslib.hash.digest_filename( + filepath, algorithm="sha256" + ) + file_hash = {"sha256": digest_object.hexdigest()} + file_length = os.path.getsize(filepath) + + # Test: Expected input. + self.assertEqual( + securesystemslib.util.get_file_details(filepath), + (file_length, file_hash), + ) + + # Test: Incorrect input. + bogus_inputs = [ + self.random_string(), + 1234, + [self.random_string()], + {"a": "b"}, + None, + ] + + for bogus_input in bogus_inputs: + if isinstance(bogus_input, str): + self.assertRaises( + securesystemslib.exceptions.StorageError, + securesystemslib.util.get_file_details, + bogus_input, + ) + else: + self.assertRaises( + securesystemslib.exceptions.FormatError, + securesystemslib.util.get_file_details, + bogus_input, + ) + + def test_B2_get_file_hashes(self): + # Goal: Verify proper output given certain expected/unexpected input. + + # Making a temporary file. + filepath = self.make_temp_data_file() + + # Computing the hash of the tempfile. + digest_object = securesystemslib.hash.digest_filename( + filepath, algorithm="sha256" + ) + file_hash = {"sha256": digest_object.hexdigest()} + + # Test: Expected input. + self.assertEqual( + securesystemslib.util.get_file_hashes(filepath), file_hash + ) + + # Test: Incorrect input. + bogus_inputs = [ + self.random_string(), + 1234, + [self.random_string()], + {"a": "b"}, + None, + ] + + for bogus_input in bogus_inputs: + if isinstance(bogus_input, str): + self.assertRaises( + securesystemslib.exceptions.StorageError, + securesystemslib.util.get_file_hashes, + bogus_input, + ) + else: + self.assertRaises( + securesystemslib.exceptions.FormatError, + securesystemslib.util.get_file_hashes, + bogus_input, + ) + + def test_B3_get_file_length(self): + # Goal: Verify proper output given certain expected/unexpected input. + + # Making a temporary file. + filepath = self.make_temp_data_file() + + # Computing the length of the tempfile. + digest_object = securesystemslib.hash.digest_filename( # pylint: disable=unused-variable + filepath, algorithm="sha256" + ) + file_length = os.path.getsize(filepath) + + # Test: Expected input. + self.assertEqual( + securesystemslib.util.get_file_length(filepath), file_length + ) + + # Test: Incorrect input. + bogus_inputs = [ + self.random_string(), + 1234, + [self.random_string()], + {"a": "b"}, + None, + ] + + for bogus_input in bogus_inputs: + if isinstance(bogus_input, str): + self.assertRaises( + securesystemslib.exceptions.StorageError, + securesystemslib.util.get_file_length, + bogus_input, + ) + else: + self.assertRaises( + securesystemslib.exceptions.FormatError, + securesystemslib.util.get_file_length, + bogus_input, + ) + + def test_B4_ensure_parent_dir(self): + existing_parent_dir = self.make_temp_directory() + non_existing_parent_dir = os.path.join(existing_parent_dir, "a", "b") + + for parent_dir in [ + existing_parent_dir, + non_existing_parent_dir, + 12, + [3], + ]: + if isinstance(parent_dir, str): + securesystemslib.util.ensure_parent_dir( + os.path.join(parent_dir, "a.txt") + ) + self.assertTrue(os.path.isdir(parent_dir)) + + else: + self.assertRaises( + securesystemslib.exceptions.FormatError, + securesystemslib.util.ensure_parent_dir, + parent_dir, + ) + + # Check that when a folder cannot be created a StorageError is thrown + with self.assertRaises(securesystemslib.exceptions.StorageError): + securesystemslib.util.ensure_parent_dir("/a/file.txt") + + # When we call ensure_parent_dir with filepath arg like "a.txt", + # then the directory of that filepath will be an empty string. + # We want to make sure that securesyslib.storage.create_folder() + # won't be called with an empty string and thus raise an exception. + # If an exception is thrown the test will fail. + securesystemslib.util.ensure_parent_dir("a.txt") + + def test_B5_file_in_confined_directories(self): + # Goal: Provide invalid input for 'filepath' and 'confined_directories'. + # Include inputs like: '[1, 2, "a"]' and such... + # Reference to 'file_in_confined_directories()' to improve readability. + in_confined_directory = ( + securesystemslib.util.file_in_confined_directories + ) + list_of_confined_directories = ["a", 12, {"a": "a"}, [1]] + list_of_filepaths = [12, ["a"], {"a": "a"}, "a"] + for bogus_confined_directory in list_of_confined_directories: + for filepath in list_of_filepaths: + self.assertRaises( + securesystemslib.exceptions.FormatError, + in_confined_directory, + filepath, + bogus_confined_directory, + ) + + # Test: Inputs that evaluate to False. + confined_directories = ["a/b/", "a/b/c/d/"] + self.assertFalse( + in_confined_directory("a/b/c/1.txt", confined_directories) + ) + + confined_directories = ["a/b/c/d/e/"] + self.assertFalse(in_confined_directory("a", confined_directories)) + self.assertFalse(in_confined_directory("a/b", confined_directories)) + self.assertFalse(in_confined_directory("a/b/c", confined_directories)) + self.assertFalse(in_confined_directory("a/b/c/d", confined_directories)) + # Below, 'e' is a file in the 'a/b/c/d/' directory. + self.assertFalse( + in_confined_directory("a/b/c/d/e", confined_directories) + ) + + # Test: Inputs that evaluate to True. + self.assertTrue(in_confined_directory("a/b/c.txt", [""])) + self.assertTrue(in_confined_directory("a/b/c.txt", ["a/b/"])) + self.assertTrue(in_confined_directory("a/b/c.txt", ["x", ""])) + self.assertTrue(in_confined_directory("a/b/c/..", ["a/"])) + + def test_B7_load_json_string(self): + # Test normal case. + data = ["a", {"b": ["c", None, 30.3, 29]}] + json_string = securesystemslib.util.json.dumps(data) + self.assertEqual( + data, securesystemslib.util.load_json_string(json_string) + ) + + # Test invalid arguments. + self.assertRaises( + securesystemslib.exceptions.Error, + securesystemslib.util.load_json_string, + 8, + ) + invalid_json_string = json_string + "." + self.assertRaises( + securesystemslib.exceptions.Error, + securesystemslib.util.load_json_string, + invalid_json_string, + ) + + def test_B8_load_json_file(self): + data = ["a", {"b": ["c", None, 30.3, 29]}] + filepath = self.make_temp_file() + fileobj = ( + open( # pylint: disable=unspecified-encoding,consider-using-with + filepath, "wt" + ) + ) + securesystemslib.util.json.dump(data, fileobj) + fileobj.close() + self.assertEqual(data, securesystemslib.util.load_json_file(filepath)) + + # Improperly formatted arguments. + for bogus_arg in [1, [b"a"], {"a": b"b"}]: + self.assertRaises( + securesystemslib.exceptions.FormatError, + securesystemslib.util.load_json_file, + bogus_arg, + ) + + # Non-existent path. + self.assertRaises( + securesystemslib.exceptions.StorageError, + securesystemslib.util.load_json_file, + "non-existent.json", + ) + + # Invalid JSON content. + filepath_bad_data = self.make_temp_file() + fileobj = ( + open( # pylint: disable=unspecified-encoding,consider-using-with + filepath_bad_data, "wt" + ) + ) + fileobj.write("junk data") + fileobj.close() + + self.assertRaises( + securesystemslib.exceptions.Error, + securesystemslib.util.load_json_file, + filepath_bad_data, + ) + + def test_B9_persist_temp_file(self): + # Destination directory to save the temporary file in. + dest_temp_dir = self.make_temp_directory() + + # Test the default of persisting the file and closing the tmpfile + dest_path = os.path.join(dest_temp_dir, self.random_string()) + tmpfile = tempfile.TemporaryFile() + tmpfile.write(self.random_string().encode("utf-8")) + + # Write a file with restricted permissions + securesystemslib.util.persist_temp_file( + tmpfile, dest_path, restrict=True + ) + self.assertTrue(dest_path) + + # Need to set also the stat.S_IFREG bit to match the st_mode output + # stat.S_IFREG - Regular file + expected_mode = stat.S_IFREG | stat.S_IRUSR | stat.S_IWUSR + if os.name == "nt": + # Windows only supports setting the read-only attribute. + expected_mode = ( + stat.S_IFREG + | stat.S_IWUSR + | stat.S_IRUSR + | stat.S_IWGRP + | stat.S_IRGRP + | stat.S_IWOTH + | stat.S_IROTH + ) + self.assertEqual(os.stat(dest_path).st_mode, expected_mode) + self.assertTrue(tmpfile.closed) + + # Test persisting a file without automatically closing the tmpfile + dest_path2 = os.path.join(dest_temp_dir, self.random_string()) + tmpfile = tempfile.TemporaryFile() + tmpfile.write(self.random_string().encode("utf-8")) + securesystemslib.util.persist_temp_file( + tmpfile, dest_path2, should_close=False + ) + self.assertFalse(tmpfile.closed) + + # Test persisting a file with an empty filename + with self.assertRaises(exceptions.StorageError): + securesystemslib.util.persist_temp_file(tmpfile, "") + + tmpfile.close() + + def test_C5_unittest_toolbox_make_temp_directory(self): + # Verify that the tearDown function does not fail when + # unittest_toolbox.make_temp_directory deletes the generated temp directory + # here. + temp_directory = self.make_temp_directory() + os.rmdir(temp_directory) + + def test_c5_unittest_toolbox_random_path(self): + # Verify that a random path can be generated with unittest_toolbox. + random_path = self.random_path(length=10) + self.assertTrue( + securesystemslib.formats.PATH_SCHEMA.matches(random_path) + ) + self.assertTrue( # pylint: disable=redundant-unittest-assert + 10, len(random_path) + ) + + def test_digests_are_equal(self): + digest = ( + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ) + + # Normal case: test for digests that are equal. + self.assertTrue(securesystemslib.util.digests_are_equal(digest, digest)) + + # Normal case: test for digests that are unequal. + self.assertFalse( + securesystemslib.util.digests_are_equal(digest, "0a8df1") + ) + + # Test for invalid arguments. + self.assertRaises( + securesystemslib.exceptions.FormatError, + securesystemslib.util.digests_are_equal, + 7, + digest, + ) + self.assertRaises( + securesystemslib.exceptions.FormatError, + securesystemslib.util.digests_are_equal, + digest, + 7, + ) + + # Test that digests_are_equal() takes the same amount of time to compare + # equal and unequal arguments. + runtime = timeit.timeit( + 'digests_are_equal("ab8df", "ab8df")', + setup="from securesystemslib.util import digests_are_equal", + number=100000, + ) + + runtime2 = timeit.timeit( + 'digests_are_equal("ab8df", "1b8df")', + setup="from securesystemslib.util import digests_are_equal", + number=100000, + ) + + runtime3 = timeit.timeit('"ab8df" == "ab8df"', number=100000) + + runtime4 = timeit.timeit('"ab8df" == "1b8df"', number=1000000) + + # The ratio for the 'digest_are_equal' runtimes should be at or near 1. + ratio_digests_are_equal = abs(runtime2 / runtime) + + # The ratio for the variable-time runtimes should be (>1) & at or near 10? + ratio_variable_compare = abs(runtime4 / runtime3) + + self.assertTrue(ratio_digests_are_equal < ratio_variable_compare) # Run unit test. -if __name__ == '__main__': - unittest.main() +if __name__ == "__main__": + unittest.main() diff --git a/tox.ini b/tox.ini index 8bfe0060..24be3b28 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = mypy, py37, py38, py39, py310, purepy38, py38-no-gpg, py38-test-gpg-fails +envlist = lint, py37, py38, py39, py310, purepy38, py38-no-gpg, py38-test-gpg-fails skipsdist = True [testenv] @@ -21,7 +21,7 @@ commands = coverage report -m --fail-under 97 [testenv:purepy38] -deps = +deps = commands = python -m tests.check_gpg_available @@ -41,6 +41,13 @@ setenv = commands = python -c "import securesystemslib.gpg.constants" -[testenv:mypy] +[testenv:lint] commands = + # TODO: Move configs to pyproject.toml + black --check --diff --line-length=80 --extend-exclude=_vendor . + isort --check --diff --line-length=80 --extend-skip-glob='*/_vendor/*' \ + --profile=black --project=securesystemslib . + + pylint -j 0 --rcfile=pylintrc securesystemslib tests + bandit --recursive securesystemslib --exclude _vendor mypy