Layout Creation Example

While it is possible to write an in-toto layout from scratch using a text editor and the JSON format, the preferred way is to use the in-toto model classes provided by the reference implementation.

This document shows how to use in-toto’s model classes and their convenience methods to create a supply chain layout like the one that is being used in the in-toto demo. Take a look at the demo repo for more details about the supply chain represented by this layout.

from securesystemslib.signer import CryptoSigner

from in_toto.models.layout import Layout, Step, Inspection
from in_toto.models.metadata import Metablock

# In this example we use in-memory signers (key pairs) for project owner and
# functionaries. More signer implementations are available in securesystemslib.

# In this example Alice is the project owner, whose private key is used to sign
# the layout. The corresponding public key will be used during final product
# verification.
alice = CryptoSigner.generate_ed25519()

# Bob and Carl are both functionaries, i.e. they are authorized to carry out
# different steps of the supply chain. Their public keys will be added to the
# layout, in order to verify the signatures of the link metadata that Bob and
# Carl will generate when carrying out their respective tasks.
# Bob and Carl will each require their private key when creating link metadata
# for a step.
bob = CryptoSigner.generate_ed25519()
carl = CryptoSigner.generate_ed25519()


# Create an empty layout
layout = Layout()

# Add functionary public keys to the layout
# Since the functionaries public keys are embedded in the layout, they don't
# need to be added separately for final product verification, as a consequence
# the layout serves as functionary PKI.
for key in [bob, carl]:
    key_dict = key.public_key.to_dict()
    key_dict["keyid"] = key.public_key.keyid
    layout.add_functionary_key(key_dict)

# Set expiration date so that the layout will expire in 4 months from now.
layout.set_relative_expiration(months=4)


# Create layout steps

# Each step describes a task that is required to be carried out for a compliant
# supply chain.
# A step must have a unique name to associate the related link metadata
# (i.e. the signed evidence that is created when a step is carried out).

# Each step should also list rules about the related files (artifacts) present
# before and after the step was carried out. These artifact rules allow to
# enforce and authorize which files are used and created by a step, and to link
# the steps of the supply chain together, i.e. to guarantee that files are not
# tampered with in transit.

# A step's pubkeys field lists the keyids of functionaries authorized to
# perform the step.

# Below step specifies the activity of cloning the source code repo.
# Bob is authorized to carry out the step, which must create the product
# 'demo-project/foo.py'.

# When using in-toto tooling (see 'in-toto-run'), Bob will automatically
# generate signed link metadata file, which provides the required information
# to verify the supply chain of the final product.
# The link metadata file must have the name "clone.<bob's keyid prefix>.link"

step_clone = Step(name="clone")
step_clone.pubkeys = [bob.public_key.keyid]

# Note: In general final product verification will not fail but only warn if
# the expected command diverges from the command that was actually used.

step_clone.set_expected_command_from_string(
    "git clone https://github.com/in-toto/demo-project.git")

step_clone.add_product_rule_from_string("CREATE demo-project/foo.py")
step_clone.add_product_rule_from_string("DISALLOW *")


# The following step does not expect a command, since modifying the source
# code might not be reflected by a single command. However, final product
# verification will still require a link metadata file with the name
# "update-version.<bob's keyid prefix>.link". In-toto also provides tooling
# to create a link metadata file for a step that is not carried out in a
# single command (see 'in-toto-record').

step_update = Step(name="update-version")
step_update.pubkeys = [bob.public_key.keyid]

# Below rules specify that the materials of this step must match the
# products of the 'clone' step and that the product of this step can be a
# (modified) file 'demo-project/foo.py'.

step_update.add_material_rule_from_string(
    "MATCH demo-project/* WITH PRODUCTS FROM clone")
step_update.add_material_rule_from_string("DISALLOW *")
step_update.add_product_rule_from_string("ALLOW demo-project/foo.py")
step_update.add_product_rule_from_string("DISALLOW *")


# Below step must be carried by Carl and expects a link file with the name
# "package.<carl's keyid prefix>.link"

step_package = Step(name="package")
step_package.pubkeys = [carl.public_key.keyid]

step_package.set_expected_command_from_string(
    "tar --exclude '.git' -zcvf demo-project.tar.gz demo-project")

step_package.add_material_rule_from_string(
    "MATCH demo-project/* WITH PRODUCTS FROM update-version")
step_package.add_material_rule_from_string("DISALLOW *")
step_package.add_product_rule_from_string("CREATE demo-project.tar.gz")
step_package.add_product_rule_from_string("DISALLOW *")



# Create inspection

# Inspections are commands that are executed upon in-toto final product
# verification. In this case, we define an inspection that untars the final
# product, which must match the product of the last step in the supply chain,
# ('package') and verifies that the contents of the archive match with what was
# put into the archive.

inspection = Inspection(name="untar")

inspection.set_run_from_string("tar xzf demo-project.tar.gz")

inspection.add_material_rule_from_string(
    "MATCH demo-project.tar.gz WITH PRODUCTS FROM package")
inspection.add_product_rule_from_string(
    "MATCH demo-project/foo.py WITH PRODUCTS FROM update-version")


# Add steps and inspections to layout
layout.steps = [step_clone, step_update, step_package]
layout.inspect = [inspection]


# Eventually the layout gets wrapped in a generic in-toto metablock, which
# provides functions to sign the metadata contents and write them to a file.
# As mentioned above the layout contains the functionaries' public keys and
# is signed by the project owner's private key.

# In order to reduce the impact of a project owner key compromise, the layout
# can and should be be signed by multiple project owners.

# Project owner public keys must be provided together with the layout and the
# link metadata files for final product verification.

metablock = Metablock(signed=layout)
metablock.create_signature(alice)
metablock.dump("root.layout")