💡 Tip: If this is your first time using Witness, you might benefit from trying the Getting Started tutorial first!
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.
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
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 injson
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.
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 .
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.
This policy specifies two rules:
- The policy must 'deny' if the exit code of the command is not 0:
deny[msg] {
input.exitcode != 0
msg := "exitcode not 0"
}
- 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.
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.
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.
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 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!
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.
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.
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!
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.