If you are working through this example on macOS you will need to install the following packages using a package manager such as brew
.
If necessary, you can download and install brew from https://brew.sh and follow the installation instructions. The are slight differences of certain packages on macOS and Linux.
brew install yq
brew install shasum
brew install jq
brew install openssl
brew install wget
Create a folder in the directory that you will utilize for this walkthrough. This can be done with the following command:
mkdir witness_example
cd witness_example
To start using Witness, you need to download the correct binary to run on your system to match the operating systems and architecture. The list of binaries are listed here:
https://github.com/testifysec/witness/releases
NOTE: Darwin version is for macOS
To unzip the binary run the following command:
wget https://github.com/testifysec/witness/releases/download/vx.x.xx/witness_x.x.xx-binary_version.tar.gz
Running the following command to unzip the .tar file
tar -zxf witness_x.x.xx-[binary version].tar.gz
.
Run the following command to verify if Witness is on your system:
./witness version
The first step is to generate a keypair 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 keypair:
openssl rsa -in buildkey.pem -outform PEM -pubout -out buildpublic.pem
Now that we have created the keypairs, we can you use to sign attestations we generate.
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"
This command will generate the attestations for the build step, using the private key that we generated earlier. The generated attestations will be saved to the build-attestation.json file.
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.
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
This policy specifies two rules:
The command must exit with a return code of 0. The command must be the correct command for creating the hello.txt file. But these are just examples - the rego policy can be based on any attribute in the attestation. For example, we could create a policy that checks the user who ran the command, the environment variables used, or the current working directory. This allows us to create highly customizable and granular policies to ensure the integrity and security of our build process.
Creating the Witness Policy The next step is to create the policy that will be used to verify the attestations. This policy template is written in YAML and specifies the types of attestations that will be generated, as well as the rules that the attestations must follow.
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
In this policy, we specify that the build step will generate three types of attestations: material, product, and command-run. We also specify a rego policy named exitcode that will be used to verify the exit code of the command that is run. Finally, we specify that the public key that we generated earlier will be used to sign the attestations.
It is important to note that the policy template can be used to define multiple steps in a CI pipeline, 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 our build process from start to finish.
Before we can use the policy, we need to template it by replacing the placeholders with the actual values. This can be done with the following script:
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
This script generates a key id by taking the sha256 hash of the public key used to sign attestations, base64 encodes the PEM encoded public key and the rego policy, and replaces the {{KEYID}} and {{CMD_MODULE}} placeholders in the policy template with the generated values. It then transforms the YAML policy into JSON.
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 keypair that is trusted by the verification process. This keypair can be generated using OpenSSL.
To generate the keypair that will be used to sign the policy, we can use the openssl genrsa command, as shown below:
openssl genrsa -out policykey.pem 2048
Next, we will extract the public key from the keypair, we will need this later:
openssl rsa -in policykey.pem -outform PEM -pubout -out policypublic.pem
Once the keypair has been generated, we can use the private key to sign the policy using the Witness sign command, as described in the previous section. The corresponding public key can then be used to verify the signed policy and the attestations you generated.
Now you can then sign the policy using the Witness sign command, which takes the JSON policy file as input and produces a signed policy file as output.
./witness sign -k policykey.pem -f policy.json -o policy.signed.json
The signed policy file is then used to verify the attestations generated by the CI pipeline. This ensures that the policy has not been tampered with and that it can be trusted by the verification process.
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
The -k argument specifies the public key that was used to sign the policy, and the -p argument specifies the signed policy file. The -a argument specifies the attestation file that was generated by the CI pipeline, and the -f argument 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
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.