Signing Software The Easy Way with Sigstore and Cosign
Tags: security, cloud, tutorial
Published: 2021-09-02 15:10:28

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](https://dev.to/martinheinz/sigstore-a-solution-to-software-supply-chain-security-163o), 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](https://github.com/sigstore/cosign/releases/tag/v1.0.0) 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:

```shell
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>
...
```

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](https://sigstore.dev/swagger/?urls.primaryName=Fulcio).

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](https://sigstore.dev/swagger/?urls.primaryName=Rekor).

We will however want to install the CLI so that we interact with the `rekor` server. The binaries are available in GitHub [release page](https://github.com/sigstore/rekor/releases). If you're on linux you can use the following: 

```shell
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]
...
```

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](https://github.com/sigstore/rekor/blob/main/release-verify.md).

## 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`:

```dockerfile
FROM alpine:3.14
ENTRYPOINT ["echo", "Hello sigstore"]
```

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

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

# Generate artifact digest for signing
cosign generate martinheinz/sigstore-hello > artifact
```

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: 

```shell
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`
```

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

```shell
# 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
```

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:

```shell
# ... 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)"
}
```

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:

```shell
# 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
...
```

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:

```shell
# 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
```

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:

```shell
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
```

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](https://github.com/sigstore/cosign/blob/main/USAGE.md) 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:

```shell
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
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9u5svacm9pivtrddje6r.png)

## 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`:

```shell
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
```

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:

```shell
cosign verify -key cosign.pub docker.io/martinheinz/sigstore-hello:latest | jq .
```

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:

```shell
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.
...
```

## 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](https://github.com/sigstore/cosign/blob/main/USAGE.md), so make sure to check that out too.

Also, if you want to dig even deeper, you can checkout ["sigstore the hard way"](https://github.com/lukehinds/sigstore-the-hard-way), which is a guide to setting everything up, for scratch - including `fulcio` CA and `rekor` transparency log server.