sigstore, the local way

If you've been following the Chainguard blog, you might ask yourself: how do I run the open-source sigstore stack on my machine?

While sigstore is often deployed using Kubernetes, it is flexible enough to run nearly anywhere: from a Raspberry Pi to an IBM mainframe. This article will demonstrate how to build the sigstore stack (cosign, rekor, fulcio) on your machine and use it to sign and verify container signatures without ever leaving localhost.

Prerequisites

Consult the documentation for your favorite package manager to install:

  • The Go Programming Language (v1.16 or higher: confirm by running go version )
  • The MariaDB SQL server
  • The Git distributed version control system
  • OpenSC for managing PKCS#11 security tokens
  • SoftHSM for implementing a PKCS#11 storage interface

Examples:

  • Arch Linux: sudo pacman -S mariadb git softhsm opensc go
  • Debian|Ubuntu: sudo apt-get install -y mariadb-server git softhsm2 opensc
  • Fedora: sudo dnf install mariadb-server git go softhsm opensc
  • FreeBSD: doas pkg install mariadb105-server git softhsm2 opensc
  • Gentoo: sudo emerge mariadb git go softhsm opensc
  • macOS: brew install mariadb go softhsm opensc
  • OpenBSD: doas pkg_add mariadb-server git go softhsm2 opensc
  • NetBSD: doas pkgin install mariadb-server git go softhsm2 opensc

These dependencies may also be downloaded and installed from their respective websites.

Level I: Keyed signing with a local registry

First, we will use cosign, sigstore's container-signing tool, to sign a locally published container using a locally maintained key pair. Here are the commands we will execute, along with where they will access data from:

1.1: Running a local registry

sigstore can sign containers stored within any container registry. To keep our demonstration local, we'll install a simple registry using Go. Open a terminal and run:

go install github.com/google/go-containerregistry/cmd/registry@latest

As we begin launching several services into the foreground, now is a great time to begin a shell multiplexer such as tmux or screen, or at least a terminal that supports tabs. Start the registry service:

$HOME/go/bin/registry

The terminal will now quietly hang until a request arrives. Start a new terminal session, and let's move on!

1.2: Pushing an unsigned image to the local registry

So that we have a target container to sign, we will now build a sample container and upload it to the local registry. First, install the ko container builder:

go install github.com/google/ko@latest

Download rekor, sigstore's tamper-resistant ledger software:

mkdir -p $HOME/sigstore-local/src

cd $HOME/sigstore-local/src

git clone https://github.com/sigstore/rekor.git

Build and push an image containing the rekor CLI to our local registry:

cd $HOME/sigstore-local/src/rekor/cmd

KO_DOCKER_REPO=localhost:1338/demo $HOME/go/bin/ko publish ./rekor-cli

1.3: Keyed-signing with cosign

Now we get to use cosign, sigstore's container-signing tool, to sign the container we just published. Install the latest cosign release:

go install github.com/sigstore/cosign/cmd/cosign@latest

Create a local key pair using any password. For simplicity, I suggest an empty one:

cd $HOME/sigstore-local

$HOME/go/bin/cosign generate-key-pair

Sgn the published container using the local private key:

$HOME/go/bin/cosign sign --key cosign.key \
  localhost:1338/demo/rekor-cli-e3df3bc7cfcbe584a2639931193267e9:latest

Use cosign to verify that the published container matches the local public key:

$HOME/go/bin/cosign verify --key cosign.pub \
  localhost:1338/demo/rekor-cli-e3df3bc7cfcbe584a2639931193267e9

The output for a successful verification will look like this:

Verification for localhost:1338/demo/rekor-cli-e3df3bc7cfcbe584a2639931193267e9:latest --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- The signatures were verified against the specified public key
- Any certificates were verified against the Fulcio roots.
[{"critical":{"identity":{"docker-reference":"localhost:1338/demo/rekor-cli-e3df3bc7cfcbe584a2639931193267e9"},"image":{"docker-manifest-digest":"sha256:3a46c2e44bfe8ea0231af6ab2f7adebd0bab4a892929b307c0b48d6958863a4d"},"type":"cosign container image signature"},"optional":null}]

Congratulations! You have just signed your first container! To sign other artifacts such as binaries, see Working with other artifacts.

Level II: Certificate Transparency with Rekor

So far, verification has relied on a single mutable source of truth: the container registry. With Rekor, we will introduce a second immutable source of truth to the system:

2.1: Creating a database backend with MariaDB

While Sigstore can use multiple database backends, this tutorial uses MariaDB. Once you've installed the prerequisites, run the following to start the database up locally in a locked-down manner:

  • Arch Linux: sudo mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql; sudo systemctl start mariadb && sudo mysql_secure_installation
  • Debian|Ubuntu: sudo mysql_secure_installation
  • Fedora: sudo systemctl start mariadb && sudo mysql_secure_installation
  • FreeBSD: sudo sudo service mysql-server start && sudo mysql_secure_installation
  • macOS: brew services start mariadb && sudo mysql_secure_installation
  • OpenBSD: doas mysql_install_db && doas rcctl start mysqld && doas mysql_secure_installation

Follow the prompts from mysql_install_db, answering N to change the root password, and Y to everything else. Afterward, run the database creation script:

cd $HOME/sigstore-local/src/rekor/scripts

sudo sh -x createdb.sh

2.2: Installing Trillian

Trillian provides a tamper-proof append-only log based on Merkle Trees using a gRPC API. Trillian stores its records in the MariaDB database we previously created. Install Trillian:

go install github.com/google/trillian/cmd/trillian_log_server@latest
go install github.com/google/trillian/cmd/trillian_log_signer@latest
go install github.com/google/trillian/cmd/createtree@latest

Start the log_server, which provides the Trillian "personality" API, used by Rekor, and the Certificate Transparency frontend.

$HOME/go/bin/trillian_log_server --logtostderr \
  -http_endpoint=localhost:8090 -rpc_endpoint=localhost:8091

Start the log signer, which periodically checks the database and sequences data into a Merkle tree:

$HOME/go/bin/trillian_log_signer \
  --logtostderr --force_master --http_endpoint=localhost:8190 \
  --rpc_endpoint=localhost:8191

The Trillian system is multi-tenant and can support multiple independent Merkle trees. Run this command to send a gRPC request to create a tree and save the log_id for future use:

$HOME/go/bin/createtree --admin_server localhost:8091 \
  | tee $HOME/sigstore-local/trillian.log_id

2.3: Installing Rekor

The Rekor project provides a restful API-based server for validation and a transparency log for storage. Install it from source:

cd $HOME/sigstore-local/src/rekor

go install ./cmd/rekor-cli ./cmd/rekor-server

Start rekor:

$HOME/go/bin/rekor-server serve --trillian_log_server.port=8091 \
  --enable_retrieve_api=false

Upload a test artifact to verify that Rekor is functioning correctly:

cd $HOME/sigstore-local/src/rekor

$HOME/go/bin/rekor-cli upload --artifact tests/test_file.txt \
  --public-key tests/test_public_key.key \
  --signature tests/test_file.sig \
  --rekor_server http://localhost:3000

2.4: Verifiable signing with Cosign & Rekor

Upload a signature for our image, using the local key pair we created in step 1.3.

COSIGN_EXPERIMENTAL=1 $HOME/go/bin/cosign sign \
  --key $HOME/sigstore-local/cosign.key \
  --rekor-url=http://localhost:3000 \
  localhost:1338/demo/rekor-cli-e3df3bc7cfcbe584a2639931193267e9

Verify the container against the mutable OCI attestation and the immutable Rekor record:

COSIGN_EXPERIMENTAL=1 $HOME/go/bin/cosign verify \
  --key $HOME/sigstore-local/cosign.pub \
  --rekor-url=http://localhost:3000 \
  localhost:1338/demo/rekor-cli-e3df3bc7cfcbe584a2639931193267e9

Success looks like this:

Verification for localhost:1338/demo/rekor-cli-e3df3bc7cfcbe584a2639931193267e9:latest --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- The claims were present in the transparency log
- The signatures were integrated into the transparency log when the certificate was valid
- The signatures were verified against the specified public key
- Any certificates were verified against the Fulcio roots.
[{"critical":{"identity":{"docker-reference":"localhost:1338/demo/rekor-cli-e3df3bc7cfcbe584a2639931193267e9"},"image":{"docker-manifest-digest":"sha256:35b25714b56211d548b97a858a1485b254228fe9889607246e96ed03ed77017d"},"type":"cosign container image signature"},"optional":{"Bundle":{"SignedEntryTimestamp":"MEUCIG...yoIY=","Payload":{"body":"...","integratedTime":1643917737,"logIndex":1,"logID":"4d2e4...97291"}}}}]

🍦 Take an ice cream break if you got this far. You earned it!

Level III:  Keyless signing with Fulcio

Fulcio is a sigstore component that issues code-signing certificates based on an authenticated OpenID Connect identity. These certificates are short-lived, lasting only 20 minutes!

Cosign has experimental support for using Fulcio to generate signing certificates, saving users the headache of managing certificates. As our goal is to run everything locally, we have to stand up substantially more architecture to provide secure keyless signing:

3.1: Install Fulcio

As we're going to use some experimental features in this tutorial, you will need to install Fulcio from HEAD:

cd $HOME/sigstore-local/src

git clone https://github.com/sigstore/fulcio.git

cd fulcio

go install .

3.2: Configure SoftHSM

SoftHSM implements a cryptographic store accessible through a PKCS #11 interface. You can use it to explore PKCS #11 without having a Hardware Security Module. For this demo, we will configure sigstore to reference tokens in $HOME/sigstore-local/tokens :

mkdir -p $HOME/sigstore-local/tokens

printf "directories.tokendir = $HOME/sigstore-local/tokens" \
  > $HOME/sigstore-local/softhsm2.conf

export SOFTHSM2_CONF=$HOME/sigstore-local/softhsm2.conf

Create your first HSM token, setting both PINs to 2324.

softhsm2-util --init-token --slot 0 --label fulcio

3.3: Create a CA certificate with OpenSC

Many tools need to be informed about the SoftHSM installation location, which is different in each operating system. Run this command to set and reveal the library location:

export HSM=$(find /usr/local/lib /usr/lib /usr/lib64 \
  /opt/homebrew -name libsofthsm2.so | head -n1)

file $HSM

Create a configuration file for the pkcs11 crypto library using the user PIN code you specified in the previous step:

mkdir -p $HOME/sigstore-local/config

echo "{ \"Path\": \"$HSM\", \"TokenLabel\": \"fulcio\", \"Pin\": \"2324\" }" > $HOME/sigstore-local/config/crypto11.conf

Generate a new key pair that is stored directly in the HSM. When prompted for a PIN, use 2324:

SOFTHSM2_CONF=$HOME/sigstore-local/softhsm2.conf pkcs11-tool \
  --login --login-type user --keypairgen --id 1 --label PKCS11CA \
  --key-type EC:secp384r1 --module=$HSM

Create a local CA root certificate:

cd $HOME/sigstore-local

SOFTHSM2_CONF=$HOME/sigstore-local/softhsm2.conf $HOME/go/bin/fulcio \
  createca --org=acme --country=USA --locality=Anytown \
  --province=AnyPlace --postal-code=ABCDEF \
  --street-address="123 Main St" --hsm-caroot-id 1 --out ca-root.pem

3.4: Install the Certificate Transparency Frontend

The ct_server is an RFC6962-compliant certificate transparency log that stores the code-signing certificates issued by Fulcio.

go install github.com/google/certificate-transparency-go/trillian/ctfe/ct_server@latest

Next, create a private key for the front end to use for signing certificates. For the password, I suggest using 2324 again, but you may use anything 4-characters or longer:

cd $HOME/sigstore-local

openssl ecparam -genkey -name prime256v1 -noout -out ct_unenc.key
openssl ec -in ct_unenc.key -out ct_private.pem -des
openssl ec -in ct_unenc.key -out ct_public.pem -pubout -des

rm ct_unenc.key

Store the password as a shell variable:

export PASS=<the password you just used>

Look up the Trillian log ID we previously created and set the LOG_ID variable to the resulting value:

export LOG_ID=$(cat $HOME/sigstore-local/trillian.log_id)

Populate the Certificate Transparency configuration file:

printf "config {
  log_id: $LOG_ID
  prefix: \"sigstore\"
  roots_pem_file: \"$HOME/sigstore-local/ca-root.pem\"
  private_key: {
    [type.googleapis.com/keyspb.PEMKeyFile] {
      path: \"$HOME/sigstore-local/ct_private.pem\"
      password: \"$PASS\"
    }
  }
}" | tee $HOME/sigstore-local/ct.cfg

Start the certificate transparency server:

$HOME/go/bin/ct_server -logtostderr \
  -log_config $HOME/sigstore-local/ct.cfg \
  -log_rpc_server localhost:8091 \
  -http_endpoint 0.0.0.0:6105

3.5: Installing Dex for OpenID authentication

Dex is a federated OpenID Connect Provider, connecting OpenID identities from multiple providers. We are going to use Dex to provide GitHub authentication. To build it from source (Note: BSD users should use gmake instead of make):

cd $HOME/sigstore-local/src
git clone https://github.com/dexidp/dex.git

cd dex
make build
cp bin/dex $HOME/go/bin

For this demonstration, we'll use GitHub as an OpenID provider. Visit GitHub: Register a new OAuth Application, and fill in the form accordingly:

💡
Application Name: My Local Sigstore Adventure
Homepage URL: http://localhost/
Authorization callback URL: http://localhost:5556/callback

When you click Register Application, it will output a client ID. Save it to your environment:

export GI_ID=<your id>

Click the Generate a new client secret button, and copy the long alphanumeric string it emits into your environment:

export GI_SECRET=<your client secret>

Populate the Dex configuration:

printf "issuer: http://localhost:5556
storage:
  type: sqlite3
config:
  file: ./dex.db
web:
  http: 127.0.0.1:5556
frontend:
  issuer: sigstore
oauth2:
  responseTypes: [ "code" ]
staticClients:
  - id: sigstore
    public: true
    name: sigstore
connectors:
  - type: github
    id: github-sigstore-test
    name: GitHub
    config:
      clientID: $GI_ID
      clientSecret: $GI_SECRET
      redirectURI: http://localhost:5556/callback
" | tee $HOME/sigstore-local/dex-config.yaml

Start dex:

$HOME/go/bin/dex serve $HOME/sigstore-local/dex-config.yaml

3.6: Setting up Fulcio for keyless signatures

Populate the Fulcio configuration:

printf '{
  "OIDCIssuers": {
    "http://localhost:5556": {
      "IssuerURL": "http://localhost:5556",
      "ClientID": "sigstore",
      "Type": "email"
    }
  }
}' > $HOME/sigstore-local/config/fulcio.json

Start Fulcio:

cd $HOME/sigstore-local

SOFTHSM2_CONF=$HOME/sigstore-local/softhsm2.conf \
  $HOME/go/bin/fulcio serve --config-path=config/fulcio.json \
  --ca=pkcs11ca --hsm-caroot-id=1 \
  --ct-log-url=http://localhost:6105/sigstore \
  --host=127.0.0.1 --port=5000

3.7: Local Keyless Signing

Now it is time for the finale: keyless signing! In an internet-connected environment, the command-line to sign a container is relatively simple:

COSIGN_EXPERIMENTAL=1 cosign sign <image location>

However, since we will be using our local environment, we need to override the public-key certificate used to verify identities and the endpoint locations. Run this command to sign the image we pushed locally:

SIGSTORE_CT_LOG_PUBLIC_KEY_FILE=$HOME/sigstore-local/ct_public.pem \
COSIGN_EXPERIMENTAL=1 $HOME/go/bin/cosign sign \
--oidc-issuer=http://localhost:5556 \
--fulcio-url=http://127.0.0.1:5000 \
--rekor-url=http://localhost:3000 \
localhost:1338/demo/rekor-cli-e3df3bc7cfcbe584a2639931193267e9

When you run that command, your browser will open to the local Dex instance, which will prompt you to authenticate using GitHub. Afterward, verify the certificate:

SIGSTORE_ROOT_FILE=$HOME/sigstore-local/ca-root.pem \
  COSIGN_EXPERIMENTAL=1 \
  $HOME/go/bin/cosign verify \
    --rekor-url=http://localhost:3000 \
    localhost:1338/demo/rekor-cli-e3df3bc7cfcbe584a2639931193267e9

Among other things, the output will include the OIDC issuer (our local Dex URL) and the e-mail address you authenticated to Github with. Here is an example:

Verification for localhost:1338/demo/rekor-cli-e3df3bc7cfcbe584a2639931193267e9:latest --                 
The following checks were performed on each of these signatures:                                          
  - The cosign claims were validated                                                                      
  - The claims were present in the transparency log                                                       
  - The signatures were integrated into the transparency log when the certificate was valid                                                                                                                          
  - Any certificates were verified against the Fulcio roots.                                              

[{"critical":{"identity":{"docker-reference":"localhost:1338/demo/rekor-cli-e3df3bc7cfcbe584a2639931193267e9"},"image":{"docker-manifest-digest":"sha256:dae1e7cdb03fc6f16e3a48111634f0c5fc28752229da2f96a6a4d03f60d6
d609"},"type":"cosign container image signature"},
"optional":{"Bundle":{"SignedEntryTimestamp":"MEUCI...Og4=","Payload":{"body":"...","integratedTime":1644977812,"logIndex":3,"logID":"9d0dd...a86d"}},
"Issuer":"http://localhost:5556","Subject":"blog@chainguard.dev"}}]   
😊
Pat yourself on the back! You made it through the tutorial!

Where to next?

If you encounter any problems or would like to learn more about sigstore, see:

Happy artifact signing, everyone!

Show Comments