def setUpClass(self): """Creates and changes into temporary directory and prepares two layouts. The superlayout, which has one step and its sublayout, which is the usual demo layout (write code, package, inspect tar). """ # Backup original cwd self.working_dir = os.getcwd() # Find demo files demo_files = os.path.join(os.path.dirname(os.path.realpath(__file__)), "demo_files") # Create and change into temporary directory self.test_dir = os.path.realpath(tempfile.mkdtemp()) os.chdir(self.test_dir) # Copy demo files to temp dir for file in os.listdir(demo_files): shutil.copy(os.path.join(demo_files, file), self.test_dir) self.demo_layout = Metablock.load("demo.layout.template") self.code_link = Metablock.load("package.2f89b927.link") self.package_link = Metablock.load("write-code.776a00e2.link") self.demo_links = { "write-code": self.code_link, "package": self.package_link }
def test_main_optional_args(self): """Test CLI command with optional arguments. """ named_args = [ "--step-name", self.test_step, "--key", self.rsa_key_path, "--materials", self.test_artifact, "--products", self.test_artifact, "--record-streams" ] positional_args = ["--", "python", "--version"] # Give wrong password whenever prompted. with mock.patch('in_toto.util.prompt_password', return_value='x'): # Test and assert recorded artifacts args1 = named_args + positional_args self.assert_cli_sys_exit(args1, 0) link_metadata = Metablock.load(self.test_link_rsa) self.assertTrue(self.test_artifact in list( link_metadata.signed.materials.keys())) self.assertTrue(self.test_artifact in list( link_metadata.signed.products.keys())) # Test and assert exlcuded artifacts args2 = named_args + ["--exclude", "*test*"] + positional_args self.assert_cli_sys_exit(args2, 0) link_metadata = Metablock.load(self.test_link_rsa) self.assertFalse(link_metadata.signed.materials) self.assertFalse(link_metadata.signed.products) # Test with base path args3 = named_args + ["--base-path", self.test_dir ] + positional_args self.assert_cli_sys_exit(args3, 0) link_metadata = Metablock.load(self.test_link_rsa) self.assertListEqual(list(link_metadata.signed.materials.keys()), [self.test_artifact]) self.assertListEqual(list(link_metadata.signed.products.keys()), [self.test_artifact]) # Test with bogus base path args4 = named_args + ["--base-path", "bogus/path" ] + positional_args self.assert_cli_sys_exit(args4, 1) # Test with lstrip path strip_prefix = self.test_artifact[:-1] args5 = named_args + ["--lstrip-paths", strip_prefix ] + positional_args self.assert_cli_sys_exit(args5, 0) link_metadata = Metablock.load(self.test_link_rsa) self.assertListEqual(list(link_metadata.signed.materials.keys()), [self.test_artifact[len(strip_prefix):]]) self.assertListEqual(list(link_metadata.signed.products.keys()), [self.test_artifact[len(strip_prefix):]])
def test_substitute(self): """Do a simple substitution on the expected_command field""" signed_layout = Metablock(signed=self.layout) signed_layout.sign(self.alice) # we will catch a LinkNotFound error because we don't have (and don't need) # the metadata. with self.assertRaises(in_toto.exceptions.LinkNotFoundError): in_toto_verify(signed_layout, self.alice_pub_dict, substitution_parameters={"EDITOR":"vim"}) self.assertEqual(self.layout.steps[0].expected_command[0], "vim")
def test_create_unfinished_metadata_with_expected_material(self): """Test record start creates metadata with expected material. """ in_toto_record_start(self.step_name, [self.test_material], self.key) link = Metablock.load(self.link_name_unfinished) self.assertEquals(list(link.signed.materials.keys()), [self.test_material]) os.remove(self.link_name_unfinished)
def test_compare_metadata_with_and_without_metadata_directory(self): """Test record stop with and without metadata directory, compare the expected product""" tmp_dir = os.path.realpath(tempfile.mkdtemp(dir=os.getcwd())) in_toto_record_start(self.step_name, [], self.key) in_toto_record_stop(self.step_name, [self.test_product], self.key, metadata_directory=tmp_dir) link_path = os.path.join(tmp_dir, self.link_name) link_with_md = Metablock.load(link_path) in_toto_record_start(self.step_name, [], self.key) in_toto_record_stop(self.step_name, [self.test_product], self.key) link_without_md = Metablock.load(self.link_name) self.assertEqual(link_with_md.signed, link_without_md.signed) os.remove(link_path) os.remove(self.link_name)
def test_create_unfininished_metadata_verify_signature(self): """Test record start creates metadata with expected signature. """ in_toto_record_start( self.step_name, [self.test_material], self.key) link = Metablock.load(self.link_name_unfinished) link.verify_signature(self.key) os.remove(self.link_name_unfinished)
def test_in_toto_run_compare_with_and_without_metadata_directory(self): """Successfully run with and without metadata directory, compare the signed is equal""" tmp_dir = os.path.realpath(tempfile.mkdtemp(dir=os.getcwd())) in_toto_run(self.step_name, [self.test_artifact], [self.test_artifact], ["python", "--version"], True, self.key, metadata_directory=tmp_dir) file_path = os.path.join(tmp_dir, FILENAME_FORMAT.format(step_name=self.step_name, keyid=self.key["keyid"])) link_dump_with_md = Metablock.load(file_path) in_toto_run(self.step_name, [self.test_artifact], [self.test_artifact], ["python", "--version"], True, self.key) link_dump_without_md = Metablock.load( FILENAME_FORMAT.format(step_name=self.step_name, keyid=self.key["keyid"])) self.assertEqual(repr(link_dump_with_md.signed), repr(link_dump_without_md.signed))
def test_create_metadata_with_expected_cwd(self): """Test record start/stop run, verify cwd. """ in_toto_record_start(self.step_name, [], self.key) in_toto_record_stop(self.step_name, [self.test_product], self.key) link = Metablock.load(self.link_name) self.assertEquals(link.signed.environment["workdir"], os.getcwd()) os.remove(self.link_name)
def test_in_toto_run_compare_dumped_with_returned_link(self): """Successfully run, compare dumped link is equal to returned link. """ link = in_toto_run(self.step_name, [self.test_artifact], [self.test_artifact], ["python", "--version"], True, self.key) link_dump = Metablock.load( FILENAME_FORMAT.format(step_name=self.step_name, keyid=self.key["keyid"])) self.assertEqual(repr(link), repr(link_dump))
def test_create_metadata_verify_signature(self): """Test record start creates metadata with expected signature. """ in_toto_record_start(self.step_name, [], self.key) in_toto_record_stop(self.step_name, [], self.key) link = Metablock.load(self.link_name) link.verify_signature(self.key) os.remove(self.link_name)
def __verify_in_toto_metadata(self, target_relpath, in_toto_inspection_packet): # Make a temporary directory in a parent directory we control. tempdir = tempfile.mkdtemp(dir=REPOSITORIES_DIR) # Copy files over into temp dir. for rel_path in in_toto_inspection_packet: # Don't confuse Python with any leading path separator. rel_path = rel_path.lstrip('/') abs_path = os.path.join(self.__targets_dir, rel_path) shutil.copy(abs_path, tempdir) # Switch to the temp dir. os.chdir(tempdir) # Load the root layout and public keys. layout = Metablock.load('root.layout') pubkeys = glob.glob('*.pub') layout_key_dict = import_public_keys_from_files_as_dict(pubkeys) # Parameter substitution. params = substitute(target_relpath) try: verifylib.in_toto_verify(layout, layout_key_dict, substitution_parameters=params) except: logger.exception('in-toto failed to verify {}'.format(target_relpath)) raise else: logger.info('in-toto verified {}'.format(target_relpath)) finally: # Switch back to a parent directory we control, so that we can # safely delete temp dir. os.chdir(REPOSITORIES_DIR) # Delete temp dir. shutil.rmtree(tempdir)
def test_verify_failing_inspection_rules(self): """Test fail verification with failing inspection artifact rule. """ layout = Metablock.load(self.layout_failing_inspection_rule_path) layout_key_dict = import_rsa_public_keys_from_files_as_dict( [self.alice_path]) with self.assertRaises(RuleVerficationError): in_toto_verify(layout, layout_key_dict)
def test_verify_failing_inspection_exits_non_zero(self): """Test fail verification with inspection returning non-zero. """ layout = Metablock.load(self.layout_failing_inspection_retval) layout_key_dict = import_rsa_public_keys_from_files_as_dict( [self.alice_path]) with self.assertRaises(BadReturnValueError): in_toto_verify(layout, layout_key_dict)
def setUpClass(self): """Copy test gpg rsa keyring, gpg demo metadata files and demo final product to tmp test dir. """ # Copy gpg demo metadata files demo_files = os.path.join( os.path.dirname(os.path.realpath(__file__)), "demo_files_gpg") # find where the scripts directory is located. scripts_directory = os.path.join( os.path.dirname(os.path.realpath(__file__)), "scripts") self.set_up_test_dir() self.set_up_gpg_keys() for fn in os.listdir(demo_files): shutil.copy(os.path.join(demo_files, fn), self.test_dir) # Change into test dir shutil.copytree(scripts_directory, 'scripts') # Sign layout template with gpg key layout_template = Metablock.load("demo.layout.template") self.layout_path = "gpg_signed.layout" layout_template.sign_gpg(self.gpg_key_0C8A17, self.gnupg_home) layout_template.dump(self.layout_path)
def test_verify_failing_layout_expired(self): """Test fail verification with expired layout. """ layout = Metablock.load(self.layout_expired_path) layout_key_dict = import_rsa_public_keys_from_files_as_dict( [self.alice_path, self.bob_path]) with self.assertRaises(LayoutExpiredError): in_toto_verify(layout, layout_key_dict)
def setUp(self): self.item_name = "item" self.sha256_1 = \ "d65165279105ca6773180500688df4bdc69a2c7b771752f0a46ef120b7fd8ec3" self.sha256_2 = \ "cfdaaf1ab2e4661952a9dec5e8fa3c360c1b06b1a073e8493a7c46d2af8c504b" self.links = { "item": Metablock(signed=Link(name="item", materials={ "foo": { "sha256": self.sha256_1 }, "foobar": { "sha256": self.sha256_1 }, "bar": { "sha256": self.sha256_1 } }, products={ "baz": { "sha256": self.sha256_1 }, "foo": { "sha256": self.sha256_1 }, "bar": { "sha256": self.sha256_2 } })) }
def test_verify_failing_bad_signature(self): """Test fail verification with bad layout signature. """ layout = Metablock.load(self.layout_bad_sig) layout_key_dict = import_rsa_public_keys_from_files_as_dict( [self.alice_path]) with self.assertRaises(SignatureVerificationError): in_toto_verify(layout, layout_key_dict)
def test_verify_failing_missing_key(self): """Test fail verification with missing layout key. """ layout = Metablock.load(self.layout_double_signed_path) layout_key_dict = import_rsa_public_keys_from_files_as_dict( [self.bob_path]) with self.assertRaises(SignatureVerificationError): in_toto_verify(layout, layout_key_dict)
def setUpClass(self): """Copy test gpg rsa keyring, gpg demo metadata files and demo final product to tmp test dir. """ self.working_dir = os.getcwd() self.test_dir = os.path.realpath(tempfile.mkdtemp()) # Copy gpg keyring gpg_keyring_path = os.path.join( os.path.dirname(os.path.realpath(__file__)), "gpg_keyrings", "rsa") self.gnupg_home = os.path.join(self.test_dir, "rsa") shutil.copytree(gpg_keyring_path, self.gnupg_home) self.owner_gpg_keyid = "8465a1e2e0fb2b40adb2478e18fb3f537e0c8a17" # Copy gpg demo metadata files demo_files = os.path.join(os.path.dirname(os.path.realpath(__file__)), "demo_files_gpg") for file in os.listdir(demo_files): shutil.copy(os.path.join(demo_files, file), self.test_dir) # Change into test dir os.chdir(self.test_dir) # Sign layout template with gpg key layout_template = Metablock.load("demo.layout.template") self.layout_path = "gpg_signed.layout" layout_template.sign_gpg(self.owner_gpg_keyid, self.gnupg_home) layout_template.dump(self.layout_path)
def test_create_grafeas_occurrence_from_in_toto_link(self): in_toto_link = Metablock.load("links/clone.776a00e2.link") grafeas_occurrence = \ GrafeasInTotoOccurrence.from_link(in_toto_link, "clone", "clone-resource-uri") self.assertIsInstance(grafeas_occurrence, GrafeasInTotoOccurrence)
def test_create_metadata_with_expected_product(self): """Test record stop records expected product. """ in_toto_record_start(self.step_name, self.key, []) in_toto_record_stop(self.step_name, self.key, [self.test_product]) link = Metablock.load(self.link_name) self.assertEquals(link.signed.products.keys(), [self.test_product]) os.remove(self.link_name)
def test_normalize_line_endings(self): """Test cross-platform line ending normalization. """ paths = [] try: # Create three artifacts with same content but different line endings for line_ending in [b"\n", b"\r", b"\r\n"]: fd, path = tempfile.mkstemp() paths.append(path) os.write(fd, b"hello" + line_ending + b"toto") os.close(fd) # Call in_toto_record start and stop and record artifacts as # materials and products with line ending normalization on in_toto_record_start(self.step_name, paths, self.key, normalize_line_endings=True) in_toto_record_stop(self.step_name, paths, self.key, normalize_line_endings=True) link = Metablock.load(self.link_name).signed # Check that all three hashes in materials and products are equal for artifact_dict in [link.materials, link.products]: hash_dicts = list(artifact_dict.values()) self.assertTrue(hash_dicts[1:] == hash_dicts[:-1]) # Clean up finally: for path in paths: os.remove(path)
def __load_root_layout(self, target_relpath): root_layout = Metablock.load(IN_TOTO_ROOT_LAYOUT) root_layout_pubkeys = glob.glob('*.pub') root_layout_pubkeys = import_public_keys_from_files_as_dict( root_layout_pubkeys) # Parameter substitution. root_layout_params = substitute(target_relpath) return root_layout, root_layout_pubkeys, root_layout_params
def test_create_in_toto_link_from_grafeas_occurrence(self): grafeas_occurrence = \ GrafeasInTotoOccurrence.load("occurrences/clone.776a00e2.occurrence") in_toto_link = grafeas_occurrence.to_link("clone") self.assertIsInstance(in_toto_link, Metablock) in_toto_link_original = Metablock.load("links/clone.776a00e2.link") self.assertEqual(in_toto_link, in_toto_link_original)
def in_toto_record_stop(step_name, key, product_list): """ <Purpose> Finishes creating link metadata for a multi-part in-toto step. Loads signing key and unfinished link metadata file from disk, verifies that the file was signed with the key, records products, updates unfinished Link object (products and signature), removes unfinished link file from and stores new link file to disk. <Arguments> step_name: A unique name to relate link metadata with a step defined in the layout. key: Private key to sign link metadata. Format is securesystemslib.formats.KEY_SCHEMA product_list: List of file or directory paths that should be recorded as products. <Exceptions> None. <Side Effects> Writes newly created link metadata file to disk using the filename scheme from link.FILENAME_FORMAT Removes unfinished link file link.UNFINISHED_FILENAME_FORMAT from disk <Returns> None. """ fn = FILENAME_FORMAT.format(step_name=step_name, keyid=key["keyid"]) unfinished_fn = UNFINISHED_FILENAME_FORMAT.format(step_name=step_name, keyid=key["keyid"]) log.info("Stop recording '{}'...".format(step_name)) # Expects an a file with name UNFINISHED_FILENAME_FORMAT in the current dir log.info("Loading preliminary link metadata '{}'...".format(unfinished_fn)) link_metadata = Metablock.load(unfinished_fn) # The file must have been signed by the same key log.info("Verifying preliminary link signature...") keydict = {key["keyid"]: key} link_metadata.verify_signatures(keydict) if product_list: log.info("Recording products '{}'...".format(", ".join(product_list))) link_metadata.signed.products = record_artifacts_as_dict(product_list) log.info("Updating signature with key '{:.8}...'...".format(key["keyid"])) link_metadata.signatures = [] link_metadata.sign(key) log.info("Storing link metadata to '{}'...".format(fn)) link_metadata.dump(fn) log.info("Removing unfinished link metadata '{}'...".format(unfinished_fn)) os.remove(unfinished_fn)
def load_links_for_layout(layout): """ <Purpose> Try to load all existing metadata files for each Step of the Layout from the current directory. For each step the metadata might consist of multiple (thresholds) Link or Layout (sub-layouts) files. <Arguments> layout: Layout object <Side Effects> Calls function to read files from disk <Returns> A dictionary carrying all the found metadata corresponding to the passed layout, e.g.: { <step name> : { <functionary key id> : <Metablock containing a Link or Layout object>, ... }, ... } """ steps_metadata = {} # Iterate over all the steps in the layout for step in layout.steps: links_per_step = {} # We try to load a link for every authorized functionary, but don't fail # if the file does not exist (authorized != required) # FIXME: Should we really pass on IOError, or just skip inexistent links for keyid in step.pubkeys: filename = FILENAME_FORMAT.format(step_name=step.name, keyid=keyid) try: metadata = Metablock.load(filename) links_per_step[keyid] = metadata except IOError as e: pass # Check if the step has been performed by enough number of functionaries if len(links_per_step) < step.threshold: raise in_toto.exceptions.LinkNotFoundError( "Step '{0}' requires '{1}'" " link metadata file(s), found '{2}'.".format( step.name, step.threshold, len(links_per_step))) steps_metadata[step.name] = links_per_step return steps_metadata
def test_verify_failing_link_metadata_files(self): """Test fail verification with link metadata files not found. """ os.rename("package.2f89b927.link", "package.link.bak") layout = Metablock.load(self.layout_single_signed_path) layout_key_dict = import_rsa_public_keys_from_files_as_dict( [self.alice_path]) with self.assertRaises(in_toto.exceptions.LinkNotFoundError): in_toto_verify(layout, layout_key_dict) os.rename("package.link.bak", "package.2f89b927.link")
def test_in_toto_run_with_metadata_directory(self): """Successfully run with metadata directory, compare dumped link is equal to returned link""" tmp_dir = os.path.realpath(tempfile.mkdtemp(dir=os.getcwd())) link = in_toto_run(self.step_name, [self.test_artifact], [self.test_artifact], ["python", "--version"], True, self.key, metadata_directory=tmp_dir) file_path = os.path.join(tmp_dir, FILENAME_FORMAT.format(step_name=self.step_name, keyid=self.key["keyid"])) link_dump = Metablock.load(file_path) self.assertEqual(repr(link), repr(link_dump))
def get_summary_link(layout, reduced_chain_link_dict, name): """ <Purpose> Merges the materials of the first step (as mentioned in the layout) and the products of the last step and returns a new link. This link reports the materials and products and summarizes the overall software supply chain. NOTE: The assumption is that the steps mentioned in the layout are to be performed sequentially. So, the first step mentioned in the layout denotes what comes into the supply chain and the last step denotes what goes out. <Arguments> layout: The layout specified by the project owner. reduced_chain_link_dict: A dictionary containing link metadata per step, e.g.: { <link name> : <Metablock containing a Link object>, ... } name: The name that the summary link will be associated with. <Exceptions> None. <Side Effects> None. <Returns> A Metablock object containing a Link which summarizes the materials and products of the overall software supply chain. """ # Create empty link object summary_link = in_toto.models.link.Link() # Take first and last link in the order the corresponding # steps appear in the layout, if there are any. if len(layout.steps) > 0: first_step_link = reduced_chain_link_dict[layout.steps[0].name] last_step_link = reduced_chain_link_dict[layout.steps[-1].name] summary_link.materials = first_step_link.signed.materials summary_link.name = name summary_link.products = last_step_link.signed.products summary_link.byproducts = last_step_link.signed.byproducts summary_link.command = last_step_link.signed.command return Metablock(signed=summary_link)
def main(): key_owner = interface.import_rsa_privatekey_from_file("./keys/owner") key_clone = interface.import_rsa_publickey_from_file("./keys/clone.pub") key_build = interface.import_rsa_publickey_from_file("./keys/build.pub") key_build_image = interface.import_rsa_publickey_from_file( "./keys/build-image.pub") layout = Layout.read({ "_type": "layout", "keys": { key_clone["keyid"]: key_clone, key_build["keyid"]: key_build, key_build_image["keyid"]: key_build_image, }, "steps": [{ "name": "clone", "expected_materials": [["DISALLOW", "*"]], "expected_products": [["CREATE", "*"]], "pubkeys": [key_clone["keyid"]], "expected_command": [ "git", "clone", "https://gitlab.com/boxboat/demos/intoto-spire/go-hello-world" ], "threshold": 1, }, { "name": "build", "expected_materials": [["MATCH", "*", "WITH", "PRODUCTS", "FROM", "clone"], ["DISALLOW", "*"]], "expected_products": [["CREATE", "go-hello-world"], ["DISALLOW", "*"]], "pubkeys": [key_build["keyid"]], "expected_command": ["go", "build", "./..."], "threshold": 1, }, { "name": "build-image", "expected_materials": [["MATCH", "*", "WITH", "PRODUCTS", "FROM", "clone"], ["DISALLOW", "*"]], "expected_products": [["CREATE", "image-id"], ["CREATE", "go-hello-world.tar"], ["DISALLOW", "*"]], "pubkeys": [key_build_image["keyid"]], "threshold": 1, }], "inspect": [] }) metadata = Metablock(signed=layout) # Sign and dump layout to "root.layout" metadata.sign(key_owner) metadata.dump("root.layout")