DEV Community

Cover image for Signing Software The Easy Way with Sigstore and Cosign
Martin Heinz
Martin Heinz

Posted on • Originally published at martinheinz.dev

Signing Software The Easy Way with Sigstore and Cosign

Signing software artifacts has many obvious benefits such as code integrity or developer (author) authentication. Yet it's oftentimes neglected, creating a software ripe for supply chain attacks. One of the reasons why people can't be bothered to sign their code is that existing tools - such as PGP - aren't very user friendly and require extensive security and/or cryptography knowledge.

Signing software can be easy though thanks to sigstore and its cosign CLI! In this article we will learn how cosign works and integrates with other sigstore components (fulcio and rekor). More importantly, we will learn how to use it to sign container image the easy way, both with and without keys, as well how we can use it to verify produced signatures and integrity of the signed software.

Note: This is a "hands-on" followup to my previous article Sigstore: A Solution to Software Supply Chain Security, which explains what's sigstore and how its components work.

Setting Up

Before we sign anything, we first need all the CLI tools for each of sigstore's components - that is cosign, fulcio and rekor. The first of them - cosign - which we need to actually sign anything, can be installed as binary or as Docker image. For the for first option, download the appropriate binary from release page and put it somewhere in your $PATH. Additionally, considering that we're dealing with security tooling, it's recommended to verify authenticity and integrity of the binary. You can do that using the commands shown on release page.

If you prefer to use Docker image, then you can use the following:

skopeo inspect docker://gcr.io/projectsigstore/cosign:v1.0.0
{
    "Name": "gcr.io/projectsigstore/cosign",
    "Digest": "sha256:5e88d8f6162c04da4fa7d63b032bac34d8c906b48e88057263d67b059ace7de4",
...
}
docker pull gcr.io/projectsigstore/cosign:v1.0.0
docker run --rm gcr.io/projectsigstore/cosign:v1.0.0
USAGE
  cosign [flags] <subcommand>
...
Enter fullscreen mode Exit fullscreen mode

For the second component - fulcio - we won't need to install anything because we will be using the public instance of fulcio. The public-good service is available at https://fulcio.sigstore.dev/api/v1 and API documentation can be found here.

Lastly, there's rekor and its CLI called rekor-cli. Same as with fulcio, we don't need to install rekor as it's available at https://rekor.sigstore.dev along with the Swagger definition here.

We will however want to install the CLI so that we interact with the rekor server. The binaries are available in GitHub release page. If you're on linux you can use the following:

wget -O rekor-cli https://github.com/sigstore/rekor/releases/download/v0.3.0/rekor-cli-linux-amd64
chmod +x rekor-cli
# Move it into $PATH directory...
./rekor-cli

Rekor command line interface tool

Usage:
  rekor [command]
...
Enter fullscreen mode Exit fullscreen mode

And again, as mentioned with cosign, you should be careful with what binaries you're using. Therefore you might want to verify rekor-cli binary using the process outlined here.

The Hard Way

With all the tools ready, we can start signing artifacts. To get better understanding about what goes on under covers, we will first try doing it the "hard way", that is - without all the fancy tools.

First we will need an artifact. For this demo we will use "hello world" Docker image created using following Dockerfile:

FROM alpine:3.14
ENTRYPOINT ["echo", "Hello sigstore"]
Enter fullscreen mode Exit fullscreen mode

We however cannot sign the image itself, instead we will sign its digest:

# Generate artifact
docker build -t dockerhub-username/sigstore-hello .

# Generate artifact digest for signing
cosign generate martinheinz/sigstore-hello > artifact
Enter fullscreen mode Exit fullscreen mode

Next we need an ephemeral keypair to sign the digest with. We can use cosign commands for this, but considering that this is the "hard way", let's use openssl directly:

openssl ecparam -genkey -name prime256v1 > ec_private.pem # Create keypair, same as `cosign generate-key-pair`
openssl ec -in ec_private.pem -pubout > ec_public.pem  # Extract public key, same as `cosign public-key`
Enter fullscreen mode Exit fullscreen mode

Now we're ready to sign it and while we're at it we can also verify the signature:

# Sign artifact digest, same as `cosign sign`
openssl dgst -sha256 -sign ec_private.pem artifact > artifact.sig

# Verify using public key
openssl dgst -sha256 -verify ec_public.pem -signature artifact.sig artifact
Verified OK
Enter fullscreen mode Exit fullscreen mode

Now that we signed the artifact with our private key, we want to have a proof that we were the ones who really did it. For this we need code signing certificate from fulcio. To get it, we have to authenticate with OIDC provider to get an ID token, which serves as proof of our identity for fulcio.

After that, we sign our email address which we used to authenticate using the previously used private key. We do this to prove that we have possession of the private key at the time of signing.

Finally, we ask fulcio for code signing certificate, by giving it ID token as form of authorization, the signed email address and our public key:

# ... Get token from OIDC provider
# ... Store ID token in `id_token` file

# Sign email address (to prove possession of private key)
echo "martin7.heinz@gmail.com" > email
openssl dgst -sha256 -sign ec_private.pem email > email.sig

# Submit token, public key and signed email to fulcio
curl -X POST "https://fulcio.sigstore.dev/api/v1/signingCert" \
     -H "Authorization: Bearer $(cat id_token)" \
     -H "accept: application/pem-certificate-chain" \
     -H "Content-Type: application/json" -d \
{
  "publicKey": {
    "content": "$(base64 ec_public.pem)",
    "algorithm": "ecdsa"
  },
  "signedEmailAddress": "$(base64 email.sig)"
}
Enter fullscreen mode Exit fullscreen mode

One problem with this "hard way" approach is that it's not really feasible to simulate the authentication and retrieval of ID token. Therefore, in the above snippet this step is omitted and we skip directly to submitting everything to fulcio.

Alternatively, you could also skip the interaction with fulcio entirely and use your public key instead. This approach is shown in https://github.com/sigstore/rekor/blob/main/types.md#pkixx509.

Next we can proceed with uploading the record to the transparency log (rekor). Here we show both the option with our public key and signing certificate from fulcio. When using the certificate from fulcio, we can also delete the keypair as we no longer need it:

# Delete keypair (if using signing certificate from fulcio)
rm -rf ec_private.pem ec_public.pem

rekor-cli upload --artifact artifact --signature artifact.sig --public-key=ec_public.pem --pki-format=x509  # With our public key
...
rekor-cli upload --artifact artifact --signatire artifact.sig --public-key sigingCertChain.pem --pki-format x509  # With cert from fulcio
Created entry at index 33612, available at: https://rekor.sigstore.dev/api/v1/log/entries/2f77b399fd8a162f44c75c596fb0e5917ed2f314348e135874fae9e14eff69e3

# Inspect entry
curl https://rekor.sigstore.dev/api/v1/log/entries/2f77b399fd8a162f44c75c596fb0e5917ed2f314348e135874fae9e14eff69e3 | jq .
...
rekor-cli get --uuid=2f77b399fd8a162f44c75c596fb0e5917ed2f314348e135874fae9e14eff69e3
...
Enter fullscreen mode Exit fullscreen mode

In addition to the upload we can also check presence of the record in transparency log. Above snippet uses both rekor-cli and curl to directly access the public API.

All that's left to do is upload the signature to the registry to be stored alongside container image:

# Upload to Docker Hub
cosign upload blob -f artifact.sig index.docker.io/martinheinz/sigstore-hello:new-signature.sig
Uploading file from [artifact.sig] to [index.docker.io/martinheinz/sigstore-hello:new-signature.sig] with media type [application/octet-stream]
File [artifact.sig] is available directly at [index.docker.io/v2/martinheinz/sigstore-hello/blobs/sha256:5491a7ff9960236b4f0bc7311fc8dba8e1b9fadfef7f704ec54eddaac1977ecb]
Uploaded image to:
index.docker.io/martinheinz/sigstore-hello@sha256:4c2f015295318a35b6096d6a971e42e5b5afb237dfc4fdf44364502a2e7064a1
Enter fullscreen mode Exit fullscreen mode

That's it. We have signed our image and added record of it to transparency log. This approach would work, but no one probably wants to do this on a daily basis, so let's see how the proper tools can make this easy.

The Easy Way

The "hard way" wasn't really hard, but it gets much easier if we use the tools provided:

cosign generate-key-pair

Enter password for private key: 
Enter again: 
Private key written to cosign.key
Public key written to cosign.pub

# We already uploaded signature in previous step so upload is set to false here
cosign sign -key cosign.key --upload=false martinheinz/sigstore-hello > file.sig
Enter password for private key:
...

# You can later upload the signature
cosign attach signature -signature file.sig martinheinz/sigstore-hello
Enter fullscreen mode Exit fullscreen mode

All we need to do is generate a keypair and then sign the artifact. Upon signing, cosign automatically uploads the signature to the registry where the image is located. In the above example we chose not to upload the signature and just save it to a file, because we did sign it in the previous section already. If we later decided to upload it anyway, then we can do it with cosign attach as shown above.

It's also worth pointing out, that as of right now (cosign version 1.0), the above snippet will not upload the data to rekor transparency log, for that to work, we would need to set COSIGN_EXPERIMENTAL=1, so for example COSIGN_EXPERIMENTAL=1 cosign sign -key cosign.key ....

There are also other ways to use cosign to sign artifacts depending on your use case and workflow. These are described in detail in usage page in GitHub.

"Keyless"

Even easier than the easy way is using the "keyless" method where only ephemeral keys are used, meaning you don't need to generate and maintain your own keys:

COSIGN_EXPERIMENTAL=1 cosign sign \
    -oidc-issuer "https://oauth2.sigstore.dev/auth" \
    -fulcio-url "https://fulcio.sigstore.dev" \
    -rekor-url "https://rekor.sigstore.dev" \
    docker.io/martinheinz/sigstore-hello:latest

Generating ephemeral keys...
Retrieving signed certificate...
Your browser will now be opened to:
https://oauth2.sigstore.dev/auth/auth?access_type=online&client_id=sigstore&code_challenge=...
tlog entry created with index:  33692
Pushing signature to: index.docker.io/martinheinz/sigstore-hello:sha256-af5909c54fe66d03dda41e93ca5db3f277bfc827b9758d8cfa9a5d8d60d85491.sig
Enter fullscreen mode Exit fullscreen mode

All we need to do is run cosign sign with COSIGN_EXPERIMENTAL set to 1 while at the same time omitting the -key argument. In the above example we also specified endpoints of OIDC provider, fulcio server and rekor server - these are the default values of the public-good services provided by sigstore, so they can be omitted, but are shown here for clarity and to highlight which services are being accessed/used. You could also replace those with your own instances - that would make sense if you wanted to run everything behind a firewall.

Docker Hub image and signature

Verify Everything

Now that we signed the artifact in all the ways possible we should also try verifying it, otherwise what would be the point of signing it in the first place, right?

First let's take the outputs of signing the image digest the "hard way". For that we can use rekor-cli:

rekor-cli verify --artifact artifact --signature artifact.sig --public-key ec_public.pem --pki-format x509
rekor-cli verify --artifact artifact --signature artifact.sig --public-key sigingCertChain.pem --pki-format x509
Enter fullscreen mode Exit fullscreen mode

Here we have 2 cases - if we signed the artifact with our public key, then we use that when verifying. On the other hand if we used the signing cert provided by fulcio we would use that in place of the public key.

Next up is the verification using cosign which is suitable for the basic signing with keys. All we need to do is run cosign verify providing the key and image URL:

cosign verify -key cosign.pub docker.io/martinheinz/sigstore-hello:latest | jq .
Enter fullscreen mode Exit fullscreen mode

Finally, for the keyless method - we can do essentially the same as above, but we need to add the experimental flag and we can skip the key argument:

COSIGN_EXPERIMENTAL=1 cosign verify docker.io/martinheinz/sigstore-hello:latest

Verification for docker.io/martinheinz/sigstore-hello:latest --
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - Existence of the claims in the transparency log was verified offline
  - Any certificates were verified against the Fulcio roots.
...
Enter fullscreen mode Exit fullscreen mode

Closing Thoughts

In this article I tried to outline and explain the basic use cases and approaches for signing container images using sigstore and more specifically cosign. There are however, many more options and features of cosign which might be useful to you, such as working with other types of artifacts, using hardware tokens or signing git commits, so I encourage you to mess with the tool and see what else you can use it for. A lot of these options are described in very well written usage documentation here, so make sure to check that out too.

Also, if you want to dig even deeper, you can checkout "sigstore the hard way", which is a guide to setting everything up, for scratch - including fulcio CA and rekor transparency log server.

Discussion (0)