Skip to main content

Verifying an Artifact with a Witness Policy

💡 Tip: If this is your first time using Witness, you might benefit from trying the Getting Started tutorial first!

Intro​

This quick tutorial will walk you through a simple example of how Witness can be used. To complete it successfully, you will need the following:

You will also of course need to have witness installed, which can be achieved by following the Quick Start.

Let's Go!​

Generating a Key Pair​

The first step is to generate a key pair that will be used to sign the attestations. This can be done with the following openssl command:

openssl genrsa -out buildkey.pem 2048

Next, we will extract the public key from the key pair:

openssl rsa -in buildkey.pem -outform PEM -pubout -out buildpublic.pem

Generating the Attestations​

Now that we have created the key pairs, we can use them creating and signing an attestation by running the following command:

Important Note: Witness generates the product attestation based on new files in the working directory. Make sure ./hello.txt does NOT exist when running this command.

witness run -s build -a environment -k buildkey.pem -o build-attestation.json -- \
bash -c "echo 'hello' > hello.txt"

In this command you will notice a few flags:

  • -s build specifies the step name. This is helpful for identifying which step of the supply chain these particular attestations are from.
  • -a environment specifies the attestor to use. There are a wide variety of attestors available which can called in a list using this flag.
  • -k buildkey.pem specifies the private key we generated to use for signing the attestations.
  • -o build-attestation.json specifies the output file for the attestations to be written to in json format.

Upon running this command (and it exiting successfully), you should see a file named build-attestation.json in your current working directory. This file contains the attestations that were generated by the command.

Viewing the Attestation​

If you view the build-attestation.json file, you might be disappointed to find a load of jibberish. Do not fear, it is meant to be this way! The attestation is base64 encoded and stored in a DSSE Envelope, which is a simple, foolproof way of signing arbitrary data.

To view the contents of the attestation, you can use the following command:

cat build-attestation.json | jq -r .payload | base64 -d | jq .

This will print the contents of the attestation in a human-readable format. The output should look something like:

{
"_type": "https://in-toto.io/Statement/v0.1",
"subject": [
{
"name": "https://witness.dev/attestations/product/v0.1/file:hello.txt",
"digest": {
"gitoid:sha1": "gitoid:blob:sha1:ce013625030ba8dba906f756967f9e9ca394464a",
"gitoid:sha256": "gitoid:blob:sha256:473a0f4c3be8a93681a267e3b1e9a7dcda1185436fe141f7749120a303721813",
"sha256": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"
}
}
],
"predicateType": "https://witness.testifysec.com/attestation-collection/v0.1",
"predicate": {
"name": "build",
"attestations": [
{
"type": "https://witness.dev/attestations/environment/v0.1",
"attestation": {
"os": "darwin",
...

This is all well and good, but attestations hardly make for good bedtime reading (😴), so let's see if we can define a policy so we can automate the verification process .

Specifying the Rego Constraints​

One of the key features of Witness is its ability to enforce policies using the Open Policy Agent (OPA) Rego language. This allows us to specify rules that must be followed when generating attestations, and ensures that the artifacts produced by the pipeline meet the requirements specified in the policy.

To create the Rego policy, we first need to define the rules that we want to enforce. For example, if we want to ensure that the ./hello.txt file is created with the correct contents we could use the following Rego policy:

cat <<EOF >> cmd.rego
package commandrun

deny[msg] {
input.exitcode != 0
msg := "exitcode not 0"
}

deny[msg] {
input.cmd[2] != "echo 'hello' > hello.txt"
msg := "cmd not correct"
}
EOF

You can save this to a file locally by copying the above code, pasting it into your terminal and pressing enter. This will create a file named cmd.rego in your current working directory.

But what does it mean?​

This policy specifies two rules:

  1. The policy must 'deny' if the exit code of the command is not 0:
deny[msg] {
input.exitcode != 0
msg := "exitcode not 0"
}
  1. The policy must 'deny' if the command for creating ./hello.txt is not what we expect:

deny[msg] {
input.cmd[2] != "echo 'hello' > hello.txt"
msg := "cmd not correct"
}

What's brilliant about Rego is that these are just examples. You can define a Rego policy that inspects any attribute within the attestation.

For example, we could create a policy that checks the user that ran the command:

package environment
deny[msg] {

input.username != "witty"
msg := "username not correct"
}

Or the current working directory:

package environment
deny[msg] {

input.variables.PWD != "/home/witty/secret-lab"
msg := "working directory not correct"
}

This allows you to create highly customizable and granular policies to ensure the integrity and security of your build process.

Creating the Witness Policy​

Next, we need to place our Rego policy into a Witness Policy. While Rego is going to help us verify the attestation contents, the Witness Policy is going to take care of verifying the presence of each expected attestation, as well as the signature attached to it.

Here is an example policy template:

cat <<EOF >> policy-template.yaml
expires: "2035-12-17T23:57:40-05:00"
steps:
build:
name: build
attestations:
- type: https://witness.dev/attestations/material/v0.1
- type: https://witness.dev/attestations/product/v0.1
- type: https://witness.dev/attestations/command-run/v0.1
regoPolicies:
- name: "exitcode"
module: "{{CMD_MODULE}}"
functionaries:
- type: publickey
publickeyid: "{{KEYID}}"
publickeys:
"{{KEYID}}":
keyid: "{{KEYID}}"
key:
EOF

You can save this to a file locally by copying the above code, pasting it into your terminal and pressing enter. This will create a file named policy-template.yaml in your current working directory.

But what does it mean?​

In this Witness Policy, we have defined a single step (you can define more than one) that we expect the supply chain of any artifact verified by it should have gone through:

steps:
build:
name: build

For this step, we expect to find an Attestation Collection that contains three types of attestation: material, product and command-run:

  attestations:
- type: https://witness.dev/attestations/material/v0.1
- type: https://witness.dev/attestations/product/v0.1
- type: https://witness.dev/attestations/command-run/v0.1

For the command-run attestation, we will also be using the Rego policy we defined earlier:

  - type: https://witness.dev/attestations/command-run/v0.1
regoPolicies:
- name: "exitcode"
module: "{{CMD_MODULE}}"

Finally, we will be using our public key ID (the sha256sum of our public key) to verify the signature of the attestations. This public key is our digital identity, and in this case we are the functionary and we are expected to have signed the attestations for the build step:

  functionaries:
- type: publickey
publickeyid: "{{KEYID}}"

The key IDs are mapped to the base64 encodings of the public keys in the publickeys section:

  publickeys:
"{{KEYID}}":
keyid: "{{KEYID}}"
key:

It is important to note that the policy template can be used to define multiple steps in an artifacts supply chain, and each step can have its own set of attestations and rules. This allows us to create complex and granular policies that ensure the integrity of the supply chain from start to finish.

Templating the Policy​

Before we can use the policy, we need to populate it with the base64 encoded public key, the key ID, and the Rego policy (which also needs to be base64 encoded):

Note: This script uses the shasum tool on MacOS and sha256sum on Linux. If you are using a different operating system, you may need to modify the script to use the appropriate tool. Contributions to make this script more portable are welcome!

cat << 'EOF' > template-policy.sh

# Requires yq v4.2.0
cmd_b64="$(openssl base64 -A <"cmd.rego")"
pubkey_b64="$(openssl base64 -A <"buildpublic.pem")"
cp policy-template.yaml policy.tmp.yaml

# Calculate SHA256 hash (macOS and Linux compatible)
if [[ "$(uname)" == "Darwin" ]]; then

keyid=$(shasum -a 256 buildpublic.pem | awk '{print $1}')
sed -i '' "s/{{KEYID}}/$keyid/g" policy.tmp.yaml
sed -i '' "s/{{CMD_MODULE}}/$cmd_b64/g" policy.tmp.yaml


else
keyid=$(sha256sum buildpublic.pem | awk '{print $1}')
sed -i "s/{{KEYID}}/$keyid/g" policy.tmp.yaml
sed -i "s/{{CMD_MODULE}}/$cmd_b64/g" policy.tmp.yaml


fi
yq eval ".publickeys.\"${keyid}\".key = \"${pubkey_b64}\"" --inplace policy.tmp.yaml

# Use `-o=json` instead of deprecated `--tojson`
yq eval -o=json policy.tmp.yaml > policy.json

# Clean up the temporary file
rm policy.tmp.yaml
EOF
chmod +x template-policy.sh

Once again, you can save this to a file locally by copying the above code, pasting it into your terminal and pressing enter. This will create a file named template-policy.sh in your current working directory, but also make it executable (with chmod +x).

Now you can go ahead and run the script, which will output a final policy.json file.

Signing the Witness Policy​

Signing the policy is an important step in the attestation process, as it ensures the authenticity and integrity of the policy. This is essential for ensuring the security of the build process and preventing tampering of build materials and artifacts.

In order to sign the policy, we need to use a key pair that is trusted by the verification process. Once again, we can generate this pair with openssl:

openssl genrsa -out policykey.pem 2048
openssl rsa -in policykey.pem -outform PEM -pubout -out policypublic.pem

Once the key pair has been generated, we can use the private key to sign the policy using the Witness sign command as follows:

witness sign -k policykey.pem -f policy.json -o policy.signed.json

The above command only has three flags:

  • -k policykey.pem specifies the private key that will be used to sign the policy.
  • -f policy.json specifies the policy file that will be signed.
  • -o policy.signed.json specifies the output file for the signed policy.

The signed policy file can now be used to verify the attestations we generated earlier. This ensures that the policy has not been tampered with and that it can be trusted during the verification process.

Okay, I hear ya, I hear ya, let's verify some attestations already!

Verifying the Attestations​

Once the policy has been signed and the attestations have been generated, we can use the witness verify command to verify that the attestations meet the requirements specified in the policy. This is done by running the witness verify command with the following arguments:

witness verify -k policypublic.pem -p policy.signed.json -a build-attestation.json -f hello.txt
  • -k policypublic.pem specifies the public key that was used to sign the policy.
  • -p policy.signed.json specifies the signed policy file.
  • -a build-attestation.json specifies the attestation file that we generated earlier.
  • -f hello.txt specifies the artifact that was produced by the pipeline.

You did it! 🎉​

If the attestations meet the requirements specified in the policy, the witness verify command will output a message indicating that the verification succeeded along with references to the evidence. If the attestations do not meet the requirements, the witness verify command will output an error message indicating which requirement was not met.

What's Next?​

If you enjoyed this tutorial, you might enjoy learning about how Witness can be used to sign attestations without any keys! Sound intriguing? Let's go!

Did You Know?​

One of the key benefits of using Witness is that it is not only a standalone tool, but also a library that can be embedded into other applications such as admission controllers and runtime visibility tooling. Be sure to check out go-witness here.