def verify_all_item_rules(items, links): """ <Purpose> Iteratively verifies artifact rules of passed items (Steps or Inspections). <Arguments> items: A list containing Step or Inspection objects whose material and product rules will be verified. links: A dictionary containing link metadata per step or inspection, e.g.: { <link name> : <Metablock containing a Link object>, ... } <Exceptions> None. <Side Effects> None. """ for item in items: link = links[item.name] log.info("Verifying material rules for '{}'...".format(item.name)) verify_item_rules(item.name, "materials", item.expected_materials, links) log.info("Verifying product rules for '{}'...".format(item.name)) verify_item_rules(item.name, "products", item.expected_products, links)
def run_all_inspections(layout): """ <Purpose> Extracts all inspections from a passed Layout's inspect field and iteratively runs each command defined in the Inspection's `run` field using `runlib.in_toto_run`, which returns a Metablock object containing a Link object. If a link command returns non-zero the verification is aborted. <Arguments> layout: A Layout object which is used to extract the Inspections. <Exceptions> Calls function that raises BadReturnValueError if an inspection returned non-int or non-zero. <Returns> A dictionary of metadata about the executed inspections, e.g.: { <inspection name> : { <Metablock containing a Link object>, ... }, ... } """ inspection_links_dict = {} for inspection in layout.inspect: log.info("Executing command for inspection '{}'...".format( inspection.name)) # FIXME: We don't want to use the base path for runlib so we patch this # for now. This will not stay! base_path_backup = in_toto.settings.ARTIFACT_BASE_PATH in_toto.settings.ARTIFACT_BASE_PATH = None # FIXME: What should we record as material/product? # Is the current directory a sensible default? In general? # If so, we should probably make it a default in run_link # We could use artifact rule paths. material_list = product_list = ["."] link = in_toto.runlib.in_toto_run(inspection.name, material_list, product_list, inspection.run) _raise_on_bad_retval(link.signed.byproducts.get("return-value"), inspection.run) inspection_links_dict[inspection.name] = link # Dump the inspection link file for auditing # Keep in mind that this pollutes the verifier's (client's) filesystem. filename = FILENAME_FORMAT_SHORT.format(step_name=inspection.name) link.dump(filename) in_toto.settings.ARTIFACT_BASE_PATH = base_path_backup return inspection_links_dict
def in_toto_record_start(step_name, key, material_list): """ <Purpose> Starts creating link metadata for a multi-part in-toto step. I.e. records passed materials, creates link meta data object from it, signs it with passed key and stores it to disk with UNFINISHED_FILENAME_FORMAT. <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 material_list: List of file or directory paths that should be recorded as materials. <Exceptions> None. <Side Effects> Writes newly created link metadata file to disk using the filename scheme from link.UNFINISHED_FILENAME_FORMAT <Returns> None. """ unfinished_fn = UNFINISHED_FILENAME_FORMAT.format(step_name=step_name, keyid=key["keyid"]) log.info("Start recording '{}'...".format(step_name)) if material_list: log.info("Recording materials '{}'...".format( ", ".join(material_list))) materials_dict = record_artifacts_as_dict(material_list) log.info("Creating preliminary link metadata...") link = in_toto.models.link.Link(name=step_name, materials=materials_dict, products={}, command=[], byproducts={}, environment={"workdir": os.getcwd()}) link_metadata = Metablock(signed=link) log.info("Signing link metadata with key '{:.8}...'...".format( key["keyid"])) link_metadata.sign(key) log.info( "Storing preliminary link metadata to '{}'...".format(unfinished_fn)) link_metadata.dump(unfinished_fn)
def verify_all_steps_signatures(layout, chain_link_dict): """ <Purpose> Extracts the Steps of a passed Layout and iteratively verifies the the signatures of the Link object(s) related to each Step by the name field. The public keys used for verification are also extracted from the Layout. <Arguments> layout: A Layout object whose Steps are extracted and verified. chain_link_dict: A dictionary containing link metadata per functionary per step, e.g.: { <link name> : { <functionary key id> : <Metablock containing a Link or Layout object>, ... }, ... } <Exceptions> Raises an exception if a needed key can not be found in the passed keys_dict or if a verification fails. TBA (see https://github.com/in-toto/in-toto/issues/6) """ for step in layout.steps: # Find the according link for this step key_link_dict = chain_link_dict[step.name] for keyid, link in six.iteritems(key_link_dict): keys_dict = {} # Create the dictionary of keys for this step # Only one key with the matching keyid in the # filename is added to the dictionary which ensures # that the link has been signed by that key if keyid in step.pubkeys: keys_dict[keyid] = layout.keys[keyid] else: raise AuthorizationError( "Unauthorized Key! '{0}'".format(keyid)) log.info("Verifying signature(s) for '{0}'...".format( in_toto.models.link.FILENAME_FORMAT.format(step_name=step.name, keyid=keyid))) # Verify link metadata file's signatures verify_link_signatures(link, keys_dict)
def _sign_and_dump_metadata(metadata, args): """ <Purpose> Internal method to sign link or layout metadata and dump it to disk. <Arguments> metadata: Metablock object (contains Link or Layout object) args: see argparser <Exceptions> SystemExit(0) if signing is successful SystemExit(2) if any exception occurs """ try: if not args.append: metadata.signatures = [] for key_path in args.key: key = util.prompt_import_rsa_key_from_file(key_path) metadata.sign(key) # Only relevant when signing Link metadata, where there is only one key keyid = key["keyid"] if args.output: out_path = args.output elif metadata._type == "link": out_path = FILENAME_FORMAT.format(step_name=metadata.signed.name, keyid=keyid) elif metadata._type == "layout": out_path = args.file log.info("Dumping {0} to '{1}'...".format(metadata._type, out_path)) metadata.dump(out_path) sys.exit(0) except Exception as e: log.error("The following error occurred while signing: " "{}".format(e)) sys.exit(2)
def verify_all_steps_command_alignment(layout, chain_link_dict): """ <Purpose> Iteratively checks if all expected commands as defined in the Steps of a Layout align with the actual commands as recorded in the Link metadata. <Arguments> layout: A Layout object to extract the expected commands from. chain_link_dict: A dictionary containing link metadata per functionary per step, e.g.: { <link name> : { <functionary key id> : <Metablock containing a Link object>, ... }, ... } <Exceptions> None. <Side Effects> None. """ for step in layout.steps: # Find the according link for this step expected_command = step.expected_command key_link_dict = chain_link_dict[step.name] # FIXME: I think we could do this for one link per step only # providing that we verify command alignment AFTER threshold equality for keyid, link in six.iteritems(key_link_dict): log.info("Verifying command alignment for '{0}'...".format( in_toto.models.link.FILENAME_FORMAT.format(step_name=step.name, keyid=keyid))) command = link.signed.command verify_command_alignment(command, expected_command)
def in_toto_mock(name, link_cmd_args): """ <Purpose> in_toto_run with defaults - Records materials and products in current directory - Does not sign resulting link file - Stores resulting link file under "<name>.link" <Arguments> name: A unique name to relate mock link metadata with a step or inspection defined in the layout. link_cmd_args: A list where the first element is a command and the remaining elements are arguments passed to that command. <Exceptions> None. <Side Effects> Writes newly created link metadata file to disk using the filename scheme from link.FILENAME_FORMAT_SHORT <Returns> Newly created Metablock object containing a Link object """ link = in_toto_run(name, ["."], ["."], link_cmd_args, key=False, record_streams=True) link_metadata = Metablock(signed=link) filename = FILENAME_FORMAT_SHORT.format(step_name=name) log.info("Storing unsigned link metadata to '{}.link'...".format(filename)) link_metadata.dump(filename) return link_metadata
def in_toto_verify(layout_path, layout_key_paths): """ <Purpose> Loads the layout metadata as Metablock object (containg a Layout object) and the signature verification keys from the passed paths, calls verifylib.in_toto_verify and handles exceptions. <Arguments> layout_path: Path to layout metadata file that is being verified. layout_key_paths: List of paths to project owner public keys, used to verify the layout's signature. <Exceptions> SystemExit if any exception occurs <Side Effects> Calls sys.exit(1) if an exception is raised <Returns> None. """ try: log.info("Verifying software supply chain...") log.info("Reading layout...") layout = Metablock.load(layout_path) log.info("Reading layout key(s)...") layout_key_dict = in_toto.util.import_rsa_public_keys_from_files_as_dict( layout_key_paths) verifylib.in_toto_verify(layout, layout_key_dict) except Exception as e: log.fail_verification("{0} - {1}".format(type(e).__name__, e)) sys.exit(1)
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 in_toto_run(name, material_list, product_list, link_cmd_args, key=False, record_streams=False): """ <Purpose> Calls function to run command passed as link_cmd_args argument, storing its materials, by-products and return value, and products into a link metadata file. The link metadata file is signed with the passed key and stored to disk. <Arguments> name: A unique name to relate link metadata with a step or inspection defined in the layout. material_list: List of file or directory paths that should be recorded as materials. product_list: List of file or directory paths that should be recorded as products. link_cmd_args: A list where the first element is a command and the remaining elements are arguments passed to that command. key: (optional) Private key to sign link metadata. Format is securesystemslib.formats.KEY_SCHEMA record_streams: (optional) A bool that specifies whether to redirect standard output and and standard error to a temporary file which is returned to the caller (True) or not (False). <Exceptions> None. <Side Effects> Writes newly created link metadata file to disk using the filename scheme from link.FILENAME_FORMAT <Returns> Newly created Metablock object containing a Link object """ log.info("Running '{}'...".format(name)) # If a key is passed, it has to match the format if key: securesystemslib.formats.KEY_SCHEMA.check_match(key) #FIXME: Add private key format check to securesystemslib formats if not key["keyval"].get("private"): raise securesystemslib.exceptions.FormatError( "Signing key needs to be a private key.") if material_list: log.info("Recording materials '{}'...".format( ", ".join(material_list))) materials_dict = record_artifacts_as_dict(material_list) if link_cmd_args: log.info("Running command '{}'...".format(" ".join(link_cmd_args))) byproducts = execute_link(link_cmd_args, record_streams) else: byproducts = {} if product_list: log.info("Recording products '{}'...".format(", ".join(product_list))) products_dict = record_artifacts_as_dict(product_list) log.info("Creating link metadata...") link = in_toto.models.link.Link(name=name, materials=materials_dict, products=products_dict, command=link_cmd_args, byproducts=byproducts, environment={"workdir": os.getcwd()}) link_metadata = Metablock(signed=link) if key: log.info("Signing link metadata with key '{:.8}...'...".format( key["keyid"])) link_metadata.sign(key) filename = FILENAME_FORMAT.format(step_name=name, keyid=key["keyid"]) log.info("Storing link metadata to '{}'...".format(filename)) link_metadata.dump(filename) return link_metadata
def main(): # Load Alice's private key to later sign the layout key_alice = import_rsa_key_from_file("alice") # Fetch and load Bob's and Carl's public keys # to specify that they are authorized to perform certain step in the layout key_bob = import_rsa_key_from_file("../functionary_bob/bob.pub") key_carl = import_rsa_key_from_file("../functionary_carl/carl.pub") layout = Layout.read({ "_type": "layout", "keys": { key_bob["keyid"]: key_bob, key_carl["keyid"]: key_carl, }, "steps": [{ "name": "clone", "expected_materials": [], "expected_products": [["CREATE", "demo-project/foo.py"], ["DISALLOW", "*"]], "pubkeys": [key_bob["keyid"]], "expected_command": "git clone https://github.com/in-toto/demo-project.git", "threshold": 1, }, { "name": "update-version", "expected_materials": [["MATCH", "demo-project/*", "WITH", "PRODUCTS", "FROM", "clone"], ["DISALLOW", "*"]], "expected_products": [["ALLOW", "demo-project/foo.py"], ["DISALLOW", "*"]], "pubkeys": [key_bob["keyid"]], "expected_command": "", "threshold": 1, }, { "name": "package", "expected_materials": [ [ "MATCH", "demo-project/*", "WITH", "PRODUCTS", "FROM", "update-version" ], ["DISALLOW", "*"], ], "expected_products": [ ["CREATE", "demo-project.tar.gz"], ["DISALLOW", "*"], ], "pubkeys": [key_carl["keyid"]], "expected_command": "tar --exclude '.git' -zcvf demo-project.tar.gz demo-project", "threshold": 1, }], "inspect": [{ "name": "untar", "expected_materials": [ [ "MATCH", "demo-project.tar.gz", "WITH", "PRODUCTS", "FROM", "package" ], # FIXME: If the routine running inspections would gather the # materials/products to record from the rules we wouldn't have to # ALLOW other files that we aren't interested in. ["ALLOW", ".keep"], ["ALLOW", "alice.pub"], ["ALLOW", "root.layout"], ["DISALLOW", "*"] ], "expected_products": [ [ "MATCH", "demo-project/foo.py", "WITH", "PRODUCTS", "FROM", "update-version" ], # FIXME: See expected_materials above ["ALLOW", "demo-project/.git/*"], ["ALLOW", "demo-project.tar.gz"], ["ALLOW", ".keep"], ["ALLOW", "alice.pub"], ["ALLOW", "root.layout"], ["DISALLOW", "*"] ], "run": "tar xzf demo-project.tar.gz", }], }) metadata = Metablock(signed=layout) # Sign and dump layout to "root.layout" metadata.sign(key_alice) metadata.dump("root.layout") if in_toto.settings.VERBOSE: log.info("--Begin printing layout metadata--") log.info(Layout.display(layout)) log.info("--End printing layout metadata--")
def verify_item_rules(source_name, source_type, rules, links): """ <Purpose> Iteratively apply all passed material or product rules of one item (step or inspection) to enforce and authorize artifacts reported by the corresponding link and/or to guarantee that artifacts are linked together across links. In the beginning all artifacts are placed in a queue according to their type. If an artifact gets consumed by a rule it is removed from the queue, hence an artifact can only be consumed once. <Algorithm> 1. Create materials queue and products queue, and a generic artifacts queue based on the source_type (materials or products) 2. For each rule: 1. Apply rule on corresponding queue(s) 2. If rule verification passes, remove consumed items from the corresponding queue(s) and continue with next rule <Arguments> source_name: The name of the item (Step or Inspection) being verified (used for user logging). source_type: "materials" or "products" depending on whether the rules were in the "expected_materials" or "expected_products" field. rules: The list of rules (material or product rules) for the item being verified. links: A dictionary containing link metadata per step or inspection, e.g.: { <link name> : <Metablock containing a Link object>, ... } <Exceptions> FormatError if source_type is not "materials" or "products" RuleVerficationError if the artifacts queue is not empty after all rules were applied <Side Effects> None. """ source_materials = links[source_name].signed.materials source_products = links[source_name].signed.products source_materials_queue = source_materials.keys() source_products_queue = source_products.keys() # Create generic source artifacts list and queue depending on the source type if source_type == "materials": source_artifacts = source_materials source_artifacts_queue = source_materials_queue elif source_type == "products": source_artifacts = source_products source_artifacts_queue = source_products_queue else: raise securesystemslib.exceptions.FormatError( "Argument 'source_type' of function 'verify_item_rules' has to be" " one of 'materials' or 'products.'\n" "Got:\n\t'{}'".format(source_type)) # Apply (verify) all rule for rule in rules: log.info("Verifying '{}'...".format(" ".join(rule))) # Unpack rules for dispatching and rule format verification rule_data = in_toto.artifact_rules.unpack_rule(rule) rule_type = rule_data["type"] # MATCH, ALLOW, DISALLOW operate equally on either products or materials # depending on the source_type if rule_type == "match": source_artifacts_queue = verify_match_rule(rule, source_artifacts_queue, source_artifacts, links) elif rule_type == "allow": source_artifacts_queue = verify_allow_rule(rule, source_artifacts_queue) elif rule_type == "disallow": verify_disallow_rule(rule, source_artifacts_queue) # CREATE, DELETE and MODIFY always operate either on products, on materials # or both, independently of the source_type ... elif rule_type == "create": source_products_queue = verify_create_rule(rule, source_materials_queue, source_products_queue) # The create rule only updates the products_queue, which in turn # only affects the generic artifacts queue if source_type is "products" if source_type == "products": source_artifacts_queue = source_products_queue elif rule_type == "delete": source_materials_queue = verify_delete_rule( rule, source_materials_queue, source_products_queue) # The delete rule only updates the materials_queue, which in turn # only affects the generic artifacts queue if source_type is "materials" if source_type == "materials": source_artifacts_queue = source_materials_queue elif rule_type == "modify": source_materials_queue, source_products_queue = verify_modify_rule( rule, source_materials_queue, source_products_queue, source_materials, source_products) # The modify rule updates materials_queue and products_queue. We have to # update the generic artifacts queue accordingly. if source_type == "materials": source_artifacts_queue = source_materials_queue elif source_type == "products": source_artifacts_queue = source_products_queue
def in_toto_verify(layout, layout_key_dict): """ <Purpose> Does entire in-toto supply chain verification of a final product by performing the following actions: 1. Verify layout signature(s) 2. Verify layout expiration 3. Load link metadata for every Step defined in the layout NOTE: link files are expected to have the corresponding step and the functionary, who carried out the step, encoded in their filename. 4. Verify functionary signature for every Link 5. Verify sublayouts NOTE: Replaces the layout object in the chain_link_dict with an unsigned summary link (the actual links of the sublayouts are verified). The summary link is used just like a regular link to verify command alignments, thresholds and inspections below. 6. Verify alignment of defined (Step) and reported (Link) commands NOTE: Won't raise exception on mismatch 7. Verify threshold constraints 8. Verify rules defined in each Step's expected_materials and expected_products field NOTE: At this point no Inspection link metadata is available, hence (MATCH) rules cannot reference materials or products of Inspections. Verifying Steps' artifact rules before executing Inspections guarantees that Inspection commands don't run on compromised target files, which would be a surface for attacks. 9. Execute Inspection commands NOTE: Inspections, similar to Steps executed with 'in-toto-run', will record materials before and products after command execution. For now it records everything in the current working directory. 10. Verify rules defined in each Inspection's expected_materials and expected_products field <Arguments> layout: Layout object that is being verified. layout_key_dict: Dictionary of project owner public keys, used to verify the layout's signature. <Exceptions> None. <Side Effects> Read link metadata files from disk <Returns> A link which summarizes the materials and products of the overall software supply chain (used by super-layout verification if any) """ log.info("Verifying layout signatures...") verify_layout_signatures(layout, layout_key_dict) # For the rest of the verification we only care about the layout payload # (Layout) that carries all the information and not about the layout # container (Metablock) that also carries the signatures layout = layout.signed log.info("Verifying layout expiration...") verify_layout_expiration(layout) log.info("Reading link metadata files...") chain_link_dict = load_links_for_layout(layout) log.info("Verifying link metadata signatures...") verify_all_steps_signatures(layout, chain_link_dict) log.info("Verifying sublayouts...") chain_link_dict = verify_sublayouts(layout, chain_link_dict) log.info("Verifying alignment of reported commands...") verify_all_steps_command_alignment(layout, chain_link_dict) log.info("Verifying threshold constraints...") verify_threshold_constraints(layout, chain_link_dict) reduced_chain_link_dict = reduce_chain_links(chain_link_dict) log.info("Verifying Step rules...") verify_all_item_rules(layout.steps, reduced_chain_link_dict) log.info("Executing Inspection commands...") inspection_link_dict = run_all_inspections(layout) log.info("Verifying Inspection rules...") # Artifact rules for inspections can reference links that correspond to # Steps or Inspections, hence the concatenation of both collections of links combined_links = reduced_chain_link_dict.copy() combined_links.update(inspection_link_dict) verify_all_item_rules(layout.inspect, combined_links) # We made it this far without exception that means, verification passed log.pass_verification("The software product passed all verification.") # Return a link file which summarizes the entire software supply chain # This is mostly relevant if the currently verified supply chain is embedded # in another supply chain return get_summary_link(layout, reduced_chain_link_dict)
def verify_sublayouts(layout, chain_link_dict): """ <Purpose> Checks if any step has been delegated by the functionary, recurses into the delegation and replaces the layout object in the chain_link_dict by an equivalent link object. <Arguments> layout: The layout specified by the project owner. chain_link_dict: A dictionary containing link metadata per functionary per step, e.g.: { <link name> : { <functionary key id> : <Metablock containing a Link or Layout object>, ... }, ... } <Exceptions> raises an Exception if verification of the delegated step fails. <Side Effects> None. <Returns> The passed dictionary containing link metadata per functionary per step, with layouts replaced with summary links. e.g.: { <link name> : { <functionary key id> : <Metablock containing a Link object>, ... }, ... } """ for step_name, key_link_dict in six.iteritems(chain_link_dict): for keyid, link in six.iteritems(key_link_dict): if link._type == "layout": log.info("Verifying sublayout {}...".format(step_name)) layout_key_dict = {} # Retrieve the entire key object for the keyid # corresponding to the link layout_key_dict = {keyid: layout.keys.get(keyid)} # Make a recursive call to in_toto_verify with the # layout and the extracted key object summary_link = in_toto_verify(link, layout_key_dict) # Replace the layout object in the passed chain_link_dict # with the link file returned by in-toto-verify key_link_dict[keyid] = summary_link return chain_link_dict
def verify_threshold_constraints(layout, chain_link_dict): """ <Purpose> Verifies that each step of a layout meets its signature threshold, i.e.: For each step there are at least `step.threshold` corresponding links, signed by different functionaries. Furthermore, verifies that all links corresponding to a given step report the same materials and products. <Arguments> layout: The layout whose step thresholds are being verified chain_link_dict: A dictionary containing link metadata per functionary per step, e.g.: { <link name> : { <functionary key id> : <Metablock containing a Link object>, ... }, ... } <Exceptions> raises an Exception if threshold is not verified. ThresholdVerificationError if the step is not performed by enough functionaries or if the materials and products for a step are not same for all functionaries. <Side Effects> None. """ # We are only interested in links that are related to steps defined in the # Layout, so iterate over layout.steps for step in layout.steps: # Skip steps that don't require multiple functionaries if step.threshold <= 1: log.info("Skipping threshold verification for step '{0}' with" " threshold '{1}'...".format(step.name, step.threshold)) continue log.info("Verifying threshold for step '{0}' with" " threshold '{1}'...".format(step.name, step.threshold)) # Extract the key_link_dict for this step from the passed chain_link_dict key_link_dict = chain_link_dict[step.name] # Check if we have at least <threshold> links for this step if len(key_link_dict) < step.threshold: raise ThresholdVerificationError( "Step '{0}' not performed" " by enough functionaries!".format(step.name)) # Take a reference link (e.g. the first in the step_link_dict) reference_keyid = key_link_dict.keys()[0] reference_link = key_link_dict[reference_key] # Iterate over all links to compare their properties with a reference_link for keyid, link in six.iteritems(key_link_dict): # compare their properties if (reference_link.signed.materials != link.signed.materials or reference_link.signed.products != link.signed.products): raise ThresholdVerificationError( "Links '{0}' and '{1}' have different" " artifacts!".format( in_toto.models.link.FILENAME_FORMAT.format( step_name=step.name, keyid=reference_keyid), in_toto.models.link.FILENAME_FORMAT.format( step_name=step.name, keyid=keyid)))