From 31bed389942d4eb33720cf15a95992398461fbde Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Thu, 23 Nov 2023 11:59:36 +0100 Subject: [PATCH] Add --subjectPublicKeyInfo arg to in-toto-verify blocks on: - #649, and - secure-systems-lab/securesystemslib#678 + release --- This is meant as replacement for `--layout-keys`, supporting a consistent standard key file format (subjectPublicKeyInfo/pem). It is part of a series of patches to prepare for deprecation of legacy securesystemslib interfaces and key file formats. **Change details** Adds helper to load public key file as SSlibKey and uses it in in-toto-verify for keys passed with --subjectPublicKeyInfo. NOTE: uses unreleased securesystemslib API, which **blocks** this PR. SSlibKey is converted to its dictionary representation with the keyid included, to make it compatible with verifylib.in_toto_verify. In the future we might want to support Key (SSlibKey's base class) natively in in_toto_verify. This PR also adds a deprecation warning for --layout-keys and tests using the demo supply chain. Test public key files come from secure-systems-lab/securesystemslib#604. Signed-off-by: Lukas Puehringer --- in_toto/in_toto_verify.py | 40 +++++++++++++++++--- in_toto/models/_signer.py | 17 ++++++++- requirements-pinned.txt | 2 +- tests/pems/ecdsa_public.pem | 4 ++ tests/pems/ed25519_public.pem | 3 ++ tests/pems/rsa_public.pem | 9 +++++ tests/test_in_toto_verify.py | 69 +++++++++++++++++++++++++++++++++++ 7 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 tests/pems/ecdsa_public.pem create mode 100644 tests/pems/ed25519_public.pem create mode 100644 tests/pems/rsa_public.pem diff --git a/in_toto/in_toto_verify.py b/in_toto/in_toto_verify.py index f5f061477..465507fa7 100755 --- a/in_toto/in_toto_verify.py +++ b/in_toto/in_toto_verify.py @@ -51,6 +51,7 @@ sort_action_groups, title_case_action_groups, ) +from in_toto.models._signer import load_public_key_from_file from in_toto.models.metadata import Metadata # Command line interfaces should use in_toto base logger (c.f. in_toto.log) @@ -175,6 +176,19 @@ def create_parser(): ), ) + named_args.add_argument( + "--subjectPublicKeyInfo", + type=str, + dest="subject_public_key_info", + metavar="", + nargs="+", + help=( + "replacement for '--layout-keys' using a standard " + "subjectPublicKeyInfo/PEM format. Key type is detected " + "automatically and need not be specified with '--key-type'." + ), + ) + named_args.add_argument( "-g", "--gpg", @@ -227,13 +241,13 @@ def main(): LOG.setLevelVerboseOrQuiet(args.verbose, args.quiet) - # For verifying at least one of --layout-keys or --gpg must be specified - # Note: Passing both at the same time is possible. - if (args.layout_keys is None) and (args.gpg is None): + # For verifying at least one public key must be specified + if not (args.layout_keys or args.gpg or args.subject_public_key_info): parser.print_help() parser.error( - "wrong arguments: specify at least one of" - " '--layout-keys path [path ...]' or '--gpg id [id ...]'" + "wrong arguments: specify at least one layout verification key:" + " '--layout-keys path [path ...]' or '--gpg id [id ...]' or " + " '--subjectPublicKeyInfo path [path ...]'." ) try: @@ -243,6 +257,11 @@ def main(): layout_key_dict = {} if args.layout_keys is not None: LOG.info("Loading layout key(s)...") + LOG.warning( + "'-k', '--layout-keys' is deprecated, use " + "'--subjectPublicKeyInfo' instead." + ) + layout_key_dict.update( interface.import_publickeys_from_file( args.layout_keys, args.key_types @@ -255,6 +274,17 @@ def main(): gpg_interface.export_pubkeys(args.gpg, homedir=args.gpg_home) ) + if args.subject_public_key_info: + for path in args.subject_public_key_info: + key = load_public_key_from_file(path) + + # NOTE: Would be nice to support `Key` instances natively in + # `verifylib`. Until then we pass an in-toto -tailored dict + # representation of `Key`, which also includes a keyid. + key_dict = key.to_dict() + key_dict["keyid"] = key.keyid + layout_key_dict[key.keyid] = key_dict + verifylib.in_toto_verify(layout, layout_key_dict, args.link_dir) except Exception as e: # pylint: disable=broad-exception-caught diff --git a/in_toto/models/_signer.py b/in_toto/models/_signer.py index f57a93e9c..fe6012da2 100644 --- a/in_toto/models/_signer.py +++ b/in_toto/models/_signer.py @@ -24,7 +24,10 @@ import securesystemslib.gpg.exceptions as gpg_exceptions import securesystemslib.gpg.functions as gpg -from cryptography.hazmat.primitives.serialization import load_pem_private_key +from cryptography.hazmat.primitives.serialization import ( + load_pem_private_key, + load_pem_public_key, +) from securesystemslib import exceptions from securesystemslib.signer import ( CryptoSigner, @@ -32,6 +35,7 @@ SecretsHandler, Signature, Signer, + SSlibKey, ) @@ -48,6 +52,17 @@ def load_crypto_signer_from_pkcs8_file( return signer +def load_public_key_from_file(path: str) -> SSlibKey: + """Internal helper to load SSlibKey from SubjectPublicKeyInfo/PEM file.""" + with open(path, "rb") as f: + data = f.read() + + crypto_public_key = load_pem_public_key(data) + public_key = SSlibKey.from_crypto(crypto_public_key) + + return public_key + + class GPGSignature(Signature): """A container class containing information about a gpg signature. Besides the signature, it also contains other meta information diff --git a/requirements-pinned.txt b/requirements-pinned.txt index 31e6919f5..c778509df 100644 --- a/requirements-pinned.txt +++ b/requirements-pinned.txt @@ -22,7 +22,7 @@ pynacl==1.5.0 # via securesystemslib python-dateutil==2.8.2 # via -r requirements.txt -securesystemslib[crypto,pynacl] @ git+https://github.com/lukpueh/securesystemslib@refactor-crytposigner +securesystemslib[crypto,pynacl] @ git+https://github.com/lukpueh/securesystemslib@make-sslibkey-from-crypto-public # via -r requirements.txt six==1.16.0 # via python-dateutil diff --git a/tests/pems/ecdsa_public.pem b/tests/pems/ecdsa_public.pem new file mode 100644 index 000000000..6237339f9 --- /dev/null +++ b/tests/pems/ecdsa_public.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcLYSZyFGeKdWNt5dWFbnv6N9NyHC +oUNLcG6GZIxLwN8Q8MUdHdOOxGkDnyBRSJpIZ/r/oDECSTwfCYhdogweLA== +-----END PUBLIC KEY----- diff --git a/tests/pems/ed25519_public.pem b/tests/pems/ed25519_public.pem new file mode 100644 index 000000000..137861a38 --- /dev/null +++ b/tests/pems/ed25519_public.pem @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAT2bavrzzBiiWN4YAGYTAt1wXXNzzvEhVkzomKPDNCg8= +-----END PUBLIC KEY----- diff --git a/tests/pems/rsa_public.pem b/tests/pems/rsa_public.pem new file mode 100644 index 000000000..02e7bb778 --- /dev/null +++ b/tests/pems/rsa_public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwhX6rioiL/cX5Ys32InF +U52H8tL14QeX0tacZdb+AwcH6nIh97h3RSHvGD7Xy6uaMRmGldAnSVYwJHqoJ5j2 +ynVzU/RFpr+6n8Ps0QFg5GmlEqZboFjLbS0bsRQcXXnqJNsVLEPT3ULvu1rFRbWz +AMFjNtNNk5W/u0GEzXn3D03jIdhD8IKAdrTRf0VMD9TRCXLdMmEU2vkf1NVUnOTb +/dRX5QA8TtBylVnouZknbavQ0J/pPlHLfxUgsKzodwDlJmbPG9BWwXqQCmP0DgOG +NIZ1X281MOBaGbkNVEuntNjCSaQxQjfALVVU5NAfal2cwMINtqaoc7Wa+TWvpFEI +WwIDAQAB +-----END PUBLIC KEY----- diff --git a/tests/test_in_toto_verify.py b/tests/test_in_toto_verify.py index fc1bd53c3..6855e46e6 100755 --- a/tests/test_in_toto_verify.py +++ b/tests/test_in_toto_verify.py @@ -24,6 +24,7 @@ import os import shutil import unittest +from pathlib import Path from securesystemslib.gpg.constants import have_gpg from securesystemslib.interface import ( @@ -33,9 +34,14 @@ from securesystemslib.signer import SSlibSigner from in_toto.in_toto_verify import main as in_toto_verify_main +from in_toto.models._signer import load_crypto_signer_from_pkcs8_file from in_toto.models.metadata import Metadata from tests.common import CliTestCase, GPGKeysMixin, TmpDirMixin +DEMO_FILES = Path(__file__).parent / "demo_files" +PEMS = Path(__file__).parent / "pems" +SCRIPTS = Path(__file__).parent / "scripts" + class TestInTotoVerifyTool(CliTestCase, TmpDirMixin): """ @@ -503,5 +509,68 @@ def test_gpg_signed_layout_with_gpg_functionary_keys(self): self.assert_cli_sys_exit(args, 0) +class TestInTotoVerifySubjectPublicKeyInfoKeys(CliTestCase, TmpDirMixin): + """Tests in-toto-verify like TestInTotoVerifyTool but with + standard PEM/SubjectPublicKeyInfo keys.""" + + cli_main_func = staticmethod(in_toto_verify_main) + + @classmethod + def setUpClass(cls): + """Creates and changes into temporary directory. + + * Copy files needed for verification: + - demo *.link files + - final product + - inspection scripts + + * Sign layout with keys in "pems" dir + * Dump layout + + """ + cls.set_up_test_dir() + + # Copy demo files and inspection scripts + for demo_file in [ + "foo.tar.gz", + "package.2f89b927.link", + "write-code.776a00e2.link", + ]: + shutil.copy(DEMO_FILES / demo_file, demo_file) + + shutil.copytree(SCRIPTS, "scripts") + + # Load layout template + layout_template = Metadata.load( + str(DEMO_FILES / "demo.layout.template") + ) + + # Load keys and sign + cls.public_key_paths = [] + for keytype in ["rsa", "ed25519", "ecdsa"]: + cls.public_key_paths.append(str(PEMS / f"{keytype}_public.pem")) + signer = load_crypto_signer_from_pkcs8_file( + PEMS / f"{keytype}_private_unencrypted.pem" + ) + + layout_template.create_signature(signer) + + layout_template.dump("demo.layout") + + @classmethod + def tearDownClass(cls): + cls.tear_down_test_dir() + + def test_main_multiple_keys(self): + """Test in-toto-verify CLI tool with multiple keys.""" + + args = [ + "--layout", + "demo.layout", + "--subjectPublicKeyInfo", + ] + self.public_key_paths + self.assert_cli_sys_exit(args, 0) + + if __name__ == "__main__": unittest.main()