diff --git a/.travis.yml b/.travis.yml index ea00c336..cb2117b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,8 @@ matrix: env: TOXENV=py27 - python: "2.7" env: TOXENV=purepy27 + before_install: + - sudo apt-get remove -y --allow-remove-essential gnupg gnupg2 - python: "3.5" env: TOXENV=py35 - python: "3.6" @@ -18,6 +20,8 @@ matrix: env: TOXENV=py38 - python: "3.8" env: TOXENV=purepy38 + before_install: + - sudo apt-get remove -y --allow-remove-essential gnupg gnupg2 install: - pip install -U tox coveralls diff --git a/securesystemslib/gpg/constants.py b/securesystemslib/gpg/constants.py index 3b9d975e..21a87b81 100644 --- a/securesystemslib/gpg/constants.py +++ b/securesystemslib/gpg/constants.py @@ -31,6 +31,10 @@ GPG_VERSION_COMMAND = GPG_COMMAND + " --version" FULLY_SUPPORTED_MIN_VERSION = "2.1.0" +HAVE_GPG = True +NO_GPG_MSG = "GPG support requires a GPG command, {} version {} or newer is" \ + " fully supported.".format(GPG_COMMAND, FULLY_SUPPORTED_MIN_VERSION) + try: proc = process.run(GPG_VERSION_COMMAND, stdout=process.PIPE, stderr=process.PIPE) @@ -39,6 +43,13 @@ GPG_COMMAND = "gpg" GPG_VERSION_COMMAND = GPG_COMMAND + " --version" + try: + proc = process.run(GPG_VERSION_COMMAND, stdout=process.PIPE, + stderr=process.PIPE) + + except OSError: + HAVE_GPG = False + GPG_SIGN_COMMAND = GPG_COMMAND + \ " --detach-sign --digest-algo SHA256 {keyarg} {homearg}" GPG_EXPORT_PUBKEY_COMMAND = GPG_COMMAND + " {homearg} --export {keyid}" diff --git a/securesystemslib/gpg/dsa.py b/securesystemslib/gpg/dsa.py index 790e3704..f0ad45a1 100644 --- a/securesystemslib/gpg/dsa.py +++ b/securesystemslib/gpg/dsa.py @@ -16,16 +16,22 @@ """ import binascii -import cryptography.hazmat.primitives.asymmetric.dsa as dsa -import cryptography.hazmat.backends as backends -import cryptography.hazmat.primitives.asymmetric.utils as dsautils -import cryptography.exceptions +CRYPTO = True +NO_CRYPTO_MSG = 'DSA key support for GPG requires the cryptography library' +try: + import cryptography.hazmat.primitives.asymmetric.dsa as dsa + import cryptography.hazmat.backends as backends + import cryptography.hazmat.primitives.asymmetric.utils as dsautils + import cryptography.exceptions +except ImportError: + CRYPTO = False import securesystemslib.gpg.util import securesystemslib.gpg.exceptions - +import securesystemslib.exceptions import securesystemslib.formats + def create_pubkey(pubkey_info): """ @@ -41,11 +47,17 @@ def create_pubkey(pubkey_info): 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 securesystemslib.exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) + securesystemslib.formats.GPG_DSA_PUBKEY_SCHEMA.check_match(pubkey_info) y = int(pubkey_info['keyval']['public']['y'], 16) @@ -138,12 +150,18 @@ def get_signature_params(data): securesystemslib.gpg.exceptions.PacketParsingError: if the public key parameters are malformed + securesystemslib.exceptions.UnsupportedLibraryError: + if the cryptography module is not available + None. The decoded signature buffer """ + if not CRYPTO: # pragma: no cover + return securesystemslib.exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) + ptr = 0 r_length = securesystemslib.gpg.util.get_mpi_length(data[ptr:ptr+2]) ptr += 2 @@ -198,6 +216,9 @@ def verify_signature(signature_object, pubkey_info, content, signature_object does not match securesystemslib.formats.GPG_SIGNATURE_SCHEMA pubkey_info does not match securesystemslib.formats.GPG_DSA_PUBKEY_SCHEMA + securesystemslib.exceptions.UnsupportedLibraryError if: + the cryptography module is not available + ValueError: if the passed hash_algorithm_id is not supported (see securesystemslib.gpg.util.get_hashing_class) @@ -206,6 +227,9 @@ def verify_signature(signature_object, pubkey_info, content, True if signature verification passes and False otherwise """ + if not CRYPTO: # pragma: no cover + raise securesystemslib.exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) + securesystemslib.formats.GPG_SIGNATURE_SCHEMA.check_match(signature_object) securesystemslib.formats.GPG_DSA_PUBKEY_SCHEMA.check_match(pubkey_info) diff --git a/securesystemslib/gpg/eddsa.py b/securesystemslib/gpg/eddsa.py index f52ec589..cf716b3e 100644 --- a/securesystemslib/gpg/eddsa.py +++ b/securesystemslib/gpg/eddsa.py @@ -18,12 +18,19 @@ """ import binascii import struct +import securesystemslib.exceptions import securesystemslib.gpg.util -import cryptography.hazmat.primitives.asymmetric.utils as pyca_utils -import cryptography.hazmat.primitives.asymmetric.ed25519 as pyca_ed25519 -import cryptography.hazmat.backends as pyca_backends -import cryptography.hazmat.primitives.hashes as pyca_hashing -import cryptography.exceptions + +CRYPTO = True +NO_CRYPTO_MSG = 'EdDSA key support for GPG requires the cryptography library' +try: + import cryptography.hazmat.primitives.asymmetric.utils as pyca_utils + import cryptography.hazmat.primitives.asymmetric.ed25519 as pyca_ed25519 + import cryptography.hazmat.backends as pyca_backends + import cryptography.hazmat.primitives.hashes as pyca_hashing + import cryptography.exceptions +except ImportError: + 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") @@ -153,11 +160,17 @@ def create_pubkey(pubkey_info): securesystemslib.exceptions.FormatError if pubkey_info does not match securesystemslib.formats.GPG_DSA_PUBKEY_SCHEMA + securesystemslib.exceptions.UnsupportedLibraryError if + the cryptography module is unavailable + A cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey based on the passed pubkey_info. """ + if not CRYPTO: # pragma: no cover + raise securesystemslib.exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) + securesystemslib.formats.GPG_ED25519_PUBKEY_SCHEMA.check_match(pubkey_info) public_bytes = binascii.unhexlify(pubkey_info["keyval"]["public"]["q"]) @@ -197,6 +210,9 @@ def verify_signature(signature_object, pubkey_info, content, 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 + ValueError: if the passed hash_algorithm_id is not supported (see securesystemslib.gpg.util.get_hashing_class) @@ -205,6 +221,9 @@ def verify_signature(signature_object, pubkey_info, content, True if signature verification passes and False otherwise. """ + if not CRYPTO: # pragma: no cover + raise securesystemslib.exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) + securesystemslib.formats.GPG_SIGNATURE_SCHEMA.check_match(signature_object) securesystemslib.formats.GPG_ED25519_PUBKEY_SCHEMA.check_match(pubkey_info) diff --git a/securesystemslib/gpg/functions.py b/securesystemslib/gpg/functions.py index e102b9fb..af5659a3 100644 --- a/securesystemslib/gpg/functions.py +++ b/securesystemslib/gpg/functions.py @@ -18,10 +18,12 @@ import logging import time +import securesystemslib.exceptions import securesystemslib.gpg.common import securesystemslib.gpg.exceptions from securesystemslib.gpg.constants import (GPG_EXPORT_PUBKEY_COMMAND, - GPG_SIGN_COMMAND, SIGNATURE_HANDLERS, FULLY_SUPPORTED_MIN_VERSION, SHA256) + GPG_SIGN_COMMAND, SIGNATURE_HANDLERS, FULLY_SUPPORTED_MIN_VERSION, SHA256, + HAVE_GPG, NO_GPG_MSG) import securesystemslib.process import securesystemslib.formats @@ -66,6 +68,9 @@ def create_signature(content, keyid=None, homedir=None): OSError: If the gpg command is not present or non-executable. + securesystemslib.exceptions.UnsupportedLibraryError: + If the gpg command is not available + securesystemslib.gpg.exceptions.CommandError: If the gpg command returned a non-zero exit code @@ -81,6 +86,9 @@ def create_signature(content, keyid=None, homedir=None): securesystemslib.formats.GPG_SIGNATURE_SCHEMA. """ + if not HAVE_GPG: # pragma: no cover + raise securesystemslib.exceptions.UnsupportedLibraryError(NO_GPG_MSG) + keyarg = "" if keyid: securesystemslib.formats.KEYID_SCHEMA.check_match(keyid) @@ -177,6 +185,9 @@ def verify_signature(signature_object, pubkey_info, content): securesystemslib.gpg.exceptions.KeyExpirationError: if the passed public key has expired + securesystemslib.exceptions.UnsupportedLibraryError: + If the gpg command is not available + None. @@ -184,6 +195,9 @@ def verify_signature(signature_object, pubkey_info, content): True if signature verification passes, False otherwise. """ + if not HAVE_GPG: # pragma: no cover + raise securesystemslib.exceptions.UnsupportedLibraryError(NO_GPG_MSG) + securesystemslib.formats.GPG_PUBKEY_SCHEMA.check_match(pubkey_info) securesystemslib.formats.GPG_SIGNATURE_SCHEMA.check_match(signature_object) @@ -233,6 +247,9 @@ def export_pubkey(keyid, homedir=None): ValueError: if the keyid does not match the required format. + securesystemslib.exceptions.UnsupportedLibraryError: + If the gpg command is not available. + securesystemslib.gpg.execeptions.KeyNotFoundError: if no key or subkey was found for that keyid. @@ -245,6 +262,9 @@ def export_pubkey(keyid, homedir=None): securesystemslib.formats.GPG_PUBKEY_SCHEMA. """ + if not HAVE_GPG: # pragma: no cover + raise securesystemslib.exceptions.UnsupportedLibraryError(NO_GPG_MSG) + if not securesystemslib.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. diff --git a/securesystemslib/gpg/rsa.py b/securesystemslib/gpg/rsa.py index 1cbfb691..907ed03d 100644 --- a/securesystemslib/gpg/rsa.py +++ b/securesystemslib/gpg/rsa.py @@ -16,14 +16,20 @@ """ import binascii -import cryptography.hazmat.primitives.asymmetric.rsa as rsa -import cryptography.hazmat.backends as backends -import cryptography.hazmat.primitives.asymmetric.padding as padding -import cryptography.hazmat.primitives.asymmetric.utils as utils -import cryptography.exceptions +CRYPTO = True +NO_CRYPTO_MSG = 'RSA key support for GPG requires the cryptography library' +try: + import cryptography.hazmat.primitives.asymmetric.rsa as rsa + import cryptography.hazmat.backends as backends + import cryptography.hazmat.primitives.asymmetric.padding as padding + import cryptography.hazmat.primitives.asymmetric.utils as utils + import cryptography.exceptions +except ImportError: + CRYPTO = False import securesystemslib.gpg.util import securesystemslib.gpg.exceptions +import securesystemslib.exceptions import securesystemslib.formats @@ -42,11 +48,17 @@ def create_pubkey(pubkey_info): securesystemslib.exceptions.FormatError if pubkey_info does not match securesystemslib.formats.GPG_RSA_PUBKEY_SCHEMA + securesystemslib.exceptions.UnsupportedLibraryError if + the cryptography module is unavailable + A cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey based on the passed pubkey_info. """ + if not CRYPTO: # pragma: no cover + raise securesystemslib.exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) + securesystemslib.formats.GPG_RSA_PUBKEY_SCHEMA.check_match(pubkey_info) e = int(pubkey_info['keyval']['public']['e'], 16) @@ -165,6 +177,9 @@ def verify_signature(signature_object, pubkey_info, content, 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) @@ -173,6 +188,9 @@ def verify_signature(signature_object, pubkey_info, content, True if signature verification passes and False otherwise """ + if not CRYPTO: # pragma: no cover + raise securesystemslib.exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG) + securesystemslib.formats.GPG_SIGNATURE_SCHEMA.check_match(signature_object) securesystemslib.formats.GPG_RSA_PUBKEY_SCHEMA.check_match(pubkey_info) diff --git a/securesystemslib/gpg/util.py b/securesystemslib/gpg/util.py index e1407007..228335b7 100644 --- a/securesystemslib/gpg/util.py +++ b/securesystemslib/gpg/util.py @@ -21,9 +21,15 @@ from distutils.version import StrictVersion # pylint: disable=no-name-in-module,import-error -import cryptography.hazmat.backends as backends -import cryptography.hazmat.primitives.hashes as hashing - +CRYPTO = True +NO_CRYPTO_MSG = 'gpg.utils requires the cryptography library' +try: + import cryptography.hazmat.backends as backends + import cryptography.hazmat.primitives.hashes as hashing +except ImportError: + CRYPTO = False + +import securesystemslib.exceptions import securesystemslib.gpg.exceptions import securesystemslib.process import securesystemslib.gpg.constants @@ -73,7 +79,8 @@ def hash_object(headers, algorithm, content): content: the signed content - None + securesystemslib.exceptions.UnsupportedLibraryError if: + the cryptography module is unavailable None @@ -81,6 +88,9 @@ def hash_object(headers, algorithm, content): The RFC4880-compliant hashed buffer """ + if not CRYPTO: # pragma: no cover + raise securesystemslib.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()) @@ -209,7 +219,8 @@ def compute_keyid(pubkey_packet_data): pubkey_packet_data: the public-key packet buffer - None + securesystemslib.exceptions.UnsupportedLibraryError if: + the cryptography module is unavailable None @@ -217,12 +228,16 @@ def compute_keyid(pubkey_packet_data): The RFC4880-compliant hashed buffer """ + if not CRYPTO: # pragma: no cover + raise securesystemslib.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") + def parse_subpacket_header(data): """ Parse out subpacket header as per RFC4880 5.2.3.1. Signature Subpacket Specification. """ @@ -294,10 +309,18 @@ def get_version(): The executed base command is defined in constants.GPG_VERSION_COMMAND. + + securesystemslib.exceptions.UnsupportedLibraryError: + If the gpg command is not available + Version number string, e.g. "2.1.22" """ + if not securesystemslib.gpg.constants.HAVE_GPG: # pragma: no cover + raise securesystemslib.exceptions.UnsupportedLibraryError( + securesystemslib.gpg.constants.NO_GPG_MSG) + command = securesystemslib.gpg.constants.GPG_VERSION_COMMAND process = securesystemslib.process.run(command, stdout=securesystemslib.process.PIPE, diff --git a/tests/aggregate_tests.py b/tests/aggregate_tests.py index a1b23e85..a5f49096 100755 --- a/tests/aggregate_tests.py +++ b/tests/aggregate_tests.py @@ -31,30 +31,10 @@ from __future__ import division from __future__ import unicode_literals -import os import sys import unittest -import subprocess - -def check_usable_gpg(): - """Set `TEST_SKIP_GPG` environment variable if neither gpg2 nor gpg is - available. """ - os.environ["TEST_SKIP_GPG"] = "1" - for gpg in ["gpg2", "gpg"]: - try: - subprocess.check_call([gpg, "--version"]) - - except OSError: - pass - - else: - # If one of the two exists, we can unset the skip envvar and ... - os.environ.pop("TEST_SKIP_GPG", None) - # ... abort the availability check.: - break if __name__ == '__main__': - check_usable_gpg() suite = unittest.TestLoader().discover("tests", top_level_dir=".") all_tests_passed = unittest.TextTestRunner( verbosity=1, buffer=True).run(suite).wasSuccessful() diff --git a/tests/check_public_interfaces.py b/tests/check_public_interfaces.py index 970ec90c..bcfe78b7 100644 --- a/tests/check_public_interfaces.py +++ b/tests/check_public_interfaces.py @@ -43,6 +43,9 @@ import securesystemslib.exceptions +import securesystemslib.gpg.constants +import securesystemslib.gpg.functions +import securesystemslib.gpg.util import securesystemslib.interface import securesystemslib.keys @@ -194,6 +197,21 @@ def test_purepy_ed25519(self): pub, 'ed25519', bsig, data) self.assertEqual(False, invalid) + def test_gpg_cmds(self): + """Ensure functions calling GPG commands throw an appropriate error""" + + with self.assertRaises(securesystemslib.exceptions.UnsupportedLibraryError): + securesystemslib.gpg.functions.create_signature('bar') + + with self.assertRaises(securesystemslib.exceptions.UnsupportedLibraryError): + securesystemslib.gpg.functions.verify_signature(None, 'f00', 'bar') + + with self.assertRaises(securesystemslib.exceptions.UnsupportedLibraryError): + securesystemslib.gpg.functions.export_pubkey('f00') + + with self.assertRaises(securesystemslib.exceptions.UnsupportedLibraryError): + securesystemslib.gpg.util.get_version() + if __name__ == '__main__': suite = unittest.TestLoader().loadTestsFromTestCase(TestPublicInterfaces) diff --git a/tests/test_gpg.py b/tests/test_gpg.py index 4b00e57b..85ed757d 100644 --- a/tests/test_gpg.py +++ b/tests/test_gpg.py @@ -51,14 +51,14 @@ _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) + 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 -@unittest.skipIf(os.getenv("TEST_SKIP_GPG"), "gpg not found") +@unittest.skipIf(not HAVE_GPG, "gpg not found") class TestUtil(unittest.TestCase): """Test util functions. """ def test_version_utils_return_types(self): @@ -171,7 +171,7 @@ def test_parse_subpacket_header(self): self.assertEqual(result, expected[idx]) -@unittest.skipIf(os.getenv("TEST_SKIP_GPG"), "gpg not found") +@unittest.skipIf(not HAVE_GPG, "gpg not found") class TestCommon(unittest.TestCase): """Test common functions of the securesystemslib.gpg module. """ @classmethod @@ -471,7 +471,7 @@ def test_parse_signature_packet_errors(self): "'{}' not in '{}'".format(expected_error_str, str(ctx.exception))) -@unittest.skipIf(os.getenv("TEST_SKIP_GPG"), "gpg not found") +@unittest.skipIf(not HAVE_GPG, "gpg not found") class TestGPGRSA(unittest.TestCase): """Test signature creation, verification and key export from the gpg module""" @@ -625,7 +625,7 @@ def test_verify_signature_with_expired_key(self): "\ngot: {}".format(expected, ctx.exception)) -@unittest.skipIf(os.getenv("TEST_SKIP_GPG"), "gpg not found") +@unittest.skipIf(not HAVE_GPG, "gpg not found") class TestGPGDSA(unittest.TestCase): """ Test signature creation, verification and key export from the gpg module """ @@ -710,7 +710,7 @@ def test_gpg_sign_and_verify_object(self): -@unittest.skipIf(os.getenv("TEST_SKIP_GPG"), "gpg not found") +@unittest.skipIf(not HAVE_GPG, "gpg not found") class TestGPGEdDSA(unittest.TestCase): """ Test signature creation, verification and key export from the gpg module """