Attestation Crafting
Introduction
In the previous section, we created a workflow definition, a contract and a robot account in the control plane. Next, we'll perform an attestation crafting example using Chainloop.
The lifecycle of a crafting process has the following stages: init
, add
, push
or reset
. As you can see, it mimics the workflow of a commonly used version control tool, and this is not by coincidence. Chainloop wants to make sure that the tooling feels familiar to developers and that no security jargon leaks into this stage of the process. For a developer, creating an attestation must be as simple as initializing it, adding materials to it and pushing it.
A brief description of the different stages
attestation init
During this stage, the crafting tool will contact the control plane to
- Signal the intent of starting an attestation.
- Retrieve the associated workflow contract.
- If the contract has a specified runner context type, check that we are compliant with it.
- Initialize environment variables explicitly stated in the contract.
In the future, during this initialization stage, we will also retrieve ephemeral signing keys for keyless signing/verification
attestation add
Add the materials required by the contract, i.e artifact, OCI image ref, SBOM.
The add
command knows how to handle each kind of material transparently to the user.
- STRING kinds will be injected as is.
- ARTIFACT kinds will be uploaded to your artifact registry and referenced by their content digest.
- CONTAINER_IMAGE kinds will be resolved to obtain their repository digests using the local authentication keychain.
attestation push
This stage will take the current crafting state, validate that it has all the required materials and
- Create a signed, in-toto attestation envelope.
- Push it to the control plane for storage
Currently, a cosign private key
needs to be provided during push for signing. In future releases this will not be needed since we will rely on keyless signing and verification.
attestation reset
By using the reset
command we can indicate to the control plane that something went wrong or we want to abort the attestation process.
attestation status
See the state of the current crafting process.
Crafting our first attestation locally
To create an attestation two things are required, the Chainloop crafting tool and a robot account.
The crafting tool is currently bundled within Chainloop command line tool. To install it just follow the installation instructions.
The robot account was created during the previous step and it's required during all the stages of the crafting process. It can be provided via the --token
flag or the $CHAINLOOP_ROBOT_ACCOUNT
environment variable.
Initialization
$ export CHAINLOOP_ROBOT_ACCOUNT=deadbeef
Options
chainloop attestation init
supports the following options
--token
robot account provided by the SecOps team. Alternatively, you can set theCHAINLOOP_ROBOT_ACCOUNT
environment variable.--revision
of the contract (default:latest
)--dry-run
; do not store the attestation in the Control plane, and do not fail if the runner context or required env variables can not be resolved. Useful for development (default:false
)
To initialize a new crafting process just run attestation init
and the system will retrieve the latest version (if no specific revision is set via the --revision
flag) of the contract.
$ chainloop attestation init --dry-run
INF Attestation initialized! now you can check its status or add materials to it
┌───────────────────┬──────────────────────────────────────┐
│ Initialized At │ 02 Nov 22 10:04 UTC │
├───────────────────┼──────────────────────────────────────┤
│ Workflow │ 2d289d33-8241-47b7-9ea2-8bd8b7c126f8 │
│ Name │ build-and-test │
│ Team │ cyberdyne core │
│ Project │ skynet │
│ Contract Revision │ 2 │
└───────────────────┴──────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ Materials │
├──────────────────────┬─────────────────┬─────┬──────────┬───────────┤
│ NAME │ TYPE │ SET │ REQUIRED │ IS OUTPUT │
├──────────────────────┼─────────────────┼─────┼──────────┼───────────┤
│ skynet-control-plane │ CONTAINER_IMAGE │ No │ Yes │ x │
│ rootfs │ ARTIFACT │ No │ Yes │ │
│ dockerfile │ ARTIFACT │ No │ No │ │
│ build-ref │ STRING │ No │ Yes │ │
│ skynet-sbom │ SBOM_CYCLONEDX_JSON│No│ Yes │ │
└──────────────────────┴─────────────────┴─────┴──────────┴───────────┘
┌───────────────────────────────┐
│ Env Variables │
├───────────────────┬───────────┤
│ GITHUB_REF │ NOT FOUND │
│ GITHUB_RUN_ID │ NOT FOUND │
│ GITHUB_REPOSITORY │ NOT FOUND │
└───────────────────┴───────────┘
As you can see, we have some work to do to complete this attestation, we have three required materials not set, let's do that next.
Adding Materials
# Add container image
$ chainloop attestation add --name skynet-control-plane --value ***.dkr.ecr.us-east-1.amazonaws.com/skynet-control-plane:v0.7.6
INF material added to attestation
# Add rootfs artifact
$ chainloop attestation add --name rootfs --value rootfs.tar.gz
rootfs.tar.gz@sha256:f8a581d4bce57f792444b2230b5706a6f902fbac19a374e76f6a56f030d35cf2 ... done! [7B in 0s; 34B/s]
INF material added to attestation
# Add build-ref artifact
$ chainloop attestation add --name build-ref --value 80e461e9b385c6986cdb8096c9dc99928943d667
# Add Software Bill Of Materials
$ chainloop attestation add --name skynet-sbom --value sbom.cyclonedx.json
Inspecting the crafting status
If we check the status of the attestation we'll see that the three required materials have been added
$ chainloop attestation status --full
┌───────────────────┬──────────────────────────────────────┐
│ Initialized At │ 02 Nov 22 10:04 UTC │
├───────────────────┼──────────────────────────────────────┤
│ Workflow │ 2d289d33-8241-47b7-9ea2-8bd8b7c126f8 │
│ Name │ build-and-test │
│ Team │ cyberdyne core │
│ Project │ skynet │
│ Contract Revision │ 2 │
└───────────────────┴──────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Materials │
├──────────────────────┬─────────────────┬─────┬──────────┬───────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ NAME │ TYPE │ SET │ REQUIRED │ IS OUTPUT │ VALUE │
├──────────────────────┼─────────────────┼─────┼──────────┼───────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ skynet-control-plane │ CONTAINER_IMAGE │ Yes │ Yes │ x │ **.dkr.ecr.us-east-1.amazonaws.com/skynet-control-plane@sha256:963237021c5fd0d31741a9b873e1e8af08c76459cf30e34332925510e0cb3731 │
│ rootfs │ ARTIFACT │ Yes │ Yes │ │ rootfs.tar.gz@sha256:f8a581d4bce57f792444b2230b5706a6f902fbac19a374e76f6a56f030d35cf2 │
│ dockerfile │ ARTIFACT │ No │ No │ │ │
│ build-ref │ STRING │ Yes │ Yes │ │ 80e461e9b385c6986cdb8096c9dc99928943d667 │
│ skynet-sbom │ SBOM_CYCLONEDX_ │ Yes │ Yes │ │ deadbeefddaaae-redacted │
└──────────────────────┴─────────────────┴─────┴──────────┴───────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────┐
│ Env Variables │
├───────────────────┬───────────┤
│ GITHUB_REF │ NOT FOUND │
│ GITHUB_RUN_ID │ NOT FOUND │
│ GITHUB_REPOSITORY │ NOT FOUND │
└───────────────────┴───────────┘
┌────────────────────────────────────────────────────────────────────┐
│ Runner context │
├─────────────────────────┬──────────────────────────────────────────┤
│ GITHUB_SHA │ a206e709cc21b1bf8e262604a23f9d0fc51a293a │
│ RUNNER_NAME │ Hosted Agent │
│ RUNNER_OS │ Linux │
│ GITHUB_ACTOR │ migmartri │
│ GITHUB_REF │ refs/tags/v0.8.9 │
│ GITHUB_REPOSITORY │ chainloop-dev/bedrock │
│ GITHUB_REPOSITORY_OWNER │ chainloop-dev │
│ GITHUB_RUN_ID │ 3410079758 │
└─────────────────────────┴──────────────────────────────────────────┘
Encode, sign and push attestation
Currently, a cosign private key
needs to be provided during push for signing. In future releases this will not be needed since we will rely on keyless signing and verification.
Since all the required materials have been attached, a signed in-toto statement can now be generated and sent for storage
$ export CHAINLOOP_SIGNING_PASSWORD="private key passphrase"
$ chainloop attestation push --key cosign-private.key
The resulting attestation will be rendered, signed and pushed to the control plane.
INF Attestation pushed!
{
"payloadType": "application/vnd.in-toto+json",
"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJjaGFpbmxvb3AuZGV2L2F0dGVzdGF0aW9uL3YwLjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiY2hhaW5sb29wLmRldi93b3JrZmxvdy9idWlsZC1hbmQtdGVzdCIsImRpZ2VzdCI6eyJzaGEyNTYiOiI3ODFkZDExMWQ3NjIzNGMxMmExYzY3NmMxM2ZhZWEwYzQ5NzZmMDRhZGQ4YzhhNzY3MTQxNjQ2ZDIyMzVjNmU4In19LHsibmFtZSI6IjUyOTM0NzEyNjE2NS5ka3IuZWNyLnVzLWVhc3QtMS5hbWF6b25hd3MuY29tL2NoYWlubG9vcC1jb250cm9sLXBsYW5lIiwiZGlnZXN0Ijp****",
"signatures": [
{
"keyid": "",
"sig": "MEUCIGtMsHEwJr9oN4PcE/X9cE84BFnGM3WuQ4bXXAc/15VPAiEAqpScGVSINSmJoida/FNWKnYt64xcSE3sEcMkJwFv/H0="
}
]
}
CI integration
Native CI/CD runner integrations (i.e Jenkins plugin, GitHub action) are under development but the process stated above can be implemented in any CI pipeline by just using the Chainloop CLI.
See below an example of Chainloop integrated with a Github Action release job that leverages goreleaser for building container images and binaries and AWS ECR for storage.
In that example we enable attestation to meet the requirements of this contract
Remember to remove the --dry-run
flag during intialization
name: Release
on:
push:
tags:
- "v*.*.*"
jobs:
release:
env:
# Version of Chainloop to install
CHAINLOOP_VERSION: 0.15.0
# Export robot-account env variable
# Used by the CLI to authenticate with the control plane
CHAINLOOP_ROBOT_ACCOUNT: ${{ secrets.CHAINLOOP_WF_RELEASE }}
name: "Release CLI and container images"
runs-on: ubuntu-latest
permissions:
id-token: write # required to use OIDC and retrieve AWS credentials
contents: write # required for goreleaser
steps:
# Cosign is used to verify the Chainloop binary (optional)
- name: Install Cosign
uses: sigstore/cosign-installer@v2.5.0
- name: Install Chainloop
run: |
curl -sfL https://docs.chainloop.dev/install.sh | bash -s -- --version v${{ env.CHAINLOOP_VERSION }}
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Initialize Attestation
run: |
chainloop attestation init --contract-revision 1
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: "1.19"
- name: Configure AWS credentials to push container images
uses: aws-actions/configure-aws-credentials@v1
with:
role-to-assume: arn:aws:iam::[REDACTED]
aws-region: us-east-1
- name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@v1
- name: Run GoReleaser
id: release
uses: goreleaser/goreleaser-action@v3
with:
distribution: goreleaser
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
# Generate SBOM using syft in cycloneDX format
- uses: anchore/sbom-action@v0
with:
image: ****.dkr.ecr.us-east-1.amazonaws.com/container-image:${{ github.ref_name }}
format: cyclonedx-json
output-file: /tmp/skynet.cyclonedx.json
- name: Add Attestation Artifacts
run: |
# Add binaries created by goreleaser
chainloop attestation add --name [binary-name] --value [binary-path]
# Created container image
chainloop attestation add --name control-plane-image --value ****.dkr.ecr.us-east-1.amazonaws.com/container-image:${{ github.ref_name }}
# This is just an example of adding a key/val material type
# Alternatively, GITHUB_SHA could have been added to the contract env variables allowList
chainloop attestation add --name build-ref --value ${GITHUB_SHA}
# Attach SBOM
chainloop attestation add --name skynet-sbom --value /tmp/skynet.cyclonedx.json
- name: Finish and Record Attestation
if: ${{ success() }}
run: |
chainloop attestation status --full
# Note that these commands are using CHAINLOOP_ROBOT_ACCOUNT env variable to authenticate
chainloop attestation push --key env://CHAINLOOP_SIGNING_KEY
env:
CHAINLOOP_SIGNING_KEY: ${{ secrets.COSIGN_KEY }}
CHAINLOOP_SIGNING_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
- name: Mark attestation as failed
if: ${{ failure() }}
run: |
chainloop attestation reset
- name: Mark attestation as cancelled
if: ${{ cancelled() }}
run: |
chainloop attestation reset --trigger cancellation
You can find other CI pipeline examples here.