Sign Software Artifacts with Sigstore Cosign

source: sigstore.dev 

Sigstore, a Linux Foundation project, is a new approach for signing, verifying and protecting software. It offers a set of tools for developers to sign software releases, and for users to verify the signatures - see my post on sigstore for a quick primer. In this post, I'll discuss one of the core technologies that it relies upon - Cosign.

What is Cosign?

Attackers often take advantage of the lack of built-in integrity controls in the software supply chain, compromising the software that developers and users implicitly trust. By signing software, you ensure that all parties can explicitly verify the authenticity of the software, and trust that it is not tampered.

How sigstore works (Source: sigstore)

Cosign is an open-source project used by sigstore to generate key pairs, sign container images and store them in the Open Container Initiative (OCI) registry, and help users to verify the signatures against public keys. Cosign can also be used to sign software packages, binary objects, files, SBOMs and more. Cosign also supports keyless signatures in experimental mode, using short-lived certificates signed automatically by the Fulcio root CA.

The rest of this tutorial shows how to use Cosign to:

  • Create a public/private key pair
  • Sign a container image with the private key
  • Store the signature in the registry
  • Find the signature for a container image
  • Verify the signature with the public key

Install Cosign on a DigitalOcean Droplet

For this walk-through, I'll use DigitalOcean; if you don't have an account, sign up here - you’ll receive a $200, 60-day credit when you add a valid payment method. Set up your team and project, and deploy the Docker 1-Click Droplet from the marketplace. This droplet includes the Docker CE and Docker Compose packages, along with their respective dependencies. In addition to package installation, the droplet also configures Docker according to the official recommendations. Click Create Docker Droplet, select the data center region, the CPU option, an authentication option, the hostname, and click Create Droplet.

Once the droplet is ready, select it and launch the Droplet Console as root from the menu options. Run the following commands to install the Cosign package. Find the latest release here (v1.13.1 at the time of writing this post).

# Download and install the Cosign binary
wget "https://github.com/sigstore/cosign/releases/download/v1.13.1/cosign-linux-amd64"
mv cosign-linux-amd64 /usr/local/bin/cosign
chmod +x /usr/local/bin/cosign

# Check whether Cosign is installed properly
cosign version
Cosign version

Sign Software Artifacts and Upload Signatures to Registry

First, generate the public/private key pair using cosign generate-key-pair. The private key is stored in PEM format, while the public key is stored in PEM-encoded standard PKIX format.

You can skip the password for the private key now, but it's highly recommended in a production scenario. The private and public keys will be written to cosign.key and cosign.pub respectively.

Next, create a simple Dockerfile that describes the container image using nano Dockerfile. Type the following and save the file.

FROM alpine
CMD ["echo", "Hello, World!"]

To test Cosign, you need access to a container registry like Docker Hub. If you do, feel free to use it; I'll use the ephemeral Docker image registry provided by ttl.sh.

# Tag your image with ttl.sh, a UUID and time limit before it expires
IMAGE_NAME=$(uuidgen)
docker build -t ttl.sh/${IMAGE_NAME}:1h .
docker push ttl.sh/${IMAGE_NAME}:1h

# The output should look similar to
1h: digest: sha256:897d0cae54815d030e8541c7d7085950fbfb6414f2b289456e6dc4b0d348c2ad size: 528

Save the sha256 digest in the IMAGE_DIGEST variable - we'll use it in the next step.

IMAGE_DIGEST=897d0cae54815d030e8541c7d7085950fbfb6414f2b289456e6dc4b0d348c2ad

Now that the container image is in a registry, sign the image, and push the signature to the registry. Always sign images based on their digest (@sha256:...) rather than a tag (:latest).

cosign sign --key cosign.key ttl.sh/${IMAGE_NAME}@sha256:${IMAGE_DIGEST}

# The output should look similar to
Pushing signature to: ttl.sh/989ddfe1-9fb3-49f8-935c-e830bbab52fc

Retrieve and Verify Signatures from Registry

Finally, verify that the image signature matches the public key. The signed JSON payload in the verification includes the image digest, which helps to authenticate the signature.

cosign verify --key cosign.pub ttl.sh/${IMAGE_NAME}@sha256:${IMAGE_DIGEST}

# The output should look similar to
Verification for ttl.sh/989ddfe1-9fb3-49f8-935c-e830bbab52fc@sha256:897d0cae54815d030e8541c7d7085950fbfb6414f2b289456e6dc4b0d348c2ad --
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - The signatures were verified against the specified public key

[{"critical":{"identity":{"docker-reference":"ttl.sh/989ddfe1-9fb3-49f8-935c-e830bbab52fc"},"image":{"docker-manifest-digest":"sha256:897d0cae54815d030e8541c7d7085950fbfb6414f2b289456e6dc4b0d348c2ad"},"type":"cosign container image signature"},"optional":null}]

And that's it! Cosign is v1.0, which means that the core feature set is ready for production use. In addition to the above, you can try a few other things - use a cloud KMS to generate keys, use hardware tokens, and sign other artifact types, including text files and binary blobs. See this post for guidance on how to do that. Also, have a look at my follow-up post on immutable transparency logs with Rekor.