Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable signing using a custom sigstore instance #9

Merged
merged 5 commits into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,3 @@ jobs:
uses: softprops/action-gh-release@v1
with:
generate_release_notes: true

6 changes: 2 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
BUILDKITE_TESTER_IMAGE=buildkite/plugin-tester:v3.0.1
BUILDKITE_TESTER_IMAGE=buildkite/plugin-tester:v4.1.1

# NOTE(jaosorior): This hasn't been released in two years...
# we should ask for a fix.
BUILDKITE_LINTER_IMAGE=buildkite/plugin-linter:latest
BUILDKITE_LINTER_IMAGE=buildkite/plugin-linter:v2.1.0

PLUGIN_REF=equinixmetal-buildkite/cosign

Expand Down
131 changes: 107 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,101 @@
# cosign buildkite plugin
# cosign Buildkite plugin

The cosign buildkite plugin provides a convenient mechanism for running the
open-source cosign container signing tool for your containers. For more information
about cosign, please refer to their
The cosign Buildkite plugin provides a convenient mechanism for running the
open-source cosign OCI container image signing tool for your containers.
For more information about cosign, please refer to their
[documentation](https://docs.sigstore.dev/cosign/overview).

**Important notes**

To ensure you know what you're signing:

- It's best to have this plugin run as part of the image CI build step (where the
built image is stored locally) and not as a separate step (signing a remote image).
- It's strongly recommended to use image digest instead of image tag (plugin will
automatically try to infer and use digest based on the provided image tag).
Otherwise, you might get a warning from cosign, or it may even refuse to sign the image:
>WARNING: Image reference ghcr.io/my-project/my-image:v1.2.3 uses a tag, not a
digest, to identify the image to sign.
This can lead you to sign a different image than the intended one. Please use a
digest (example.com/ubuntu@sha256:abc123...) rather than tag
(example.com/ubuntu:latest) for the input to cosign. The ability to refer to
images by tag will be removed in a future release.

## Features

- Automatically downloads and verifies the cosign executable if it cannot be
- Automatically downloads and verifies the `cosign` executable if it cannot be
found in the `PATH` environment variable's directories

## Basic signing example
## Basic signing examples

The following code snippets demonstrates how to use the plugin in a pipeline
step with the configuration parameters and upload the signature to the same
repository as the container image.

### Keyless signing

#### Using the Public-Good Sigstore Instance

```yml
steps:
- plugins:
- equinixmetal-buildkite/cosign#v0.1.0:
image: "ghcr.io/my-project/my-image@sha256:1e1e4f97dd84970160975922715909577d6c12eaaf6047021875674fa7166c27"
```

The following code snippet demonstrates how to use the plugin in a pipeline
step with the default plugin configuration parameters:
#### Using a custom Sigstore Instance

```yml
steps:
- command: ls
plugins:
- plugins:
- equinixmetal-buildkite/cosign#v0.1.0:
image: "ghcr.io/my-project/my-image:latest"
keyless: true
image: "ghcr.io/my-project/my-image@sha256:1e1e4f97dd84970160975922715909577d6c12eaaf6047021875674fa7166c27"
keyless-config:
fulcio-url: "https://fulcio.sigstore.dev"
rekor-url: "https://rekor.sigstore.dev"
tuf-mirror-url: "https://tuf.my-sigstore.dev"
tuf-root-url: "https://tuf.my-sigstore.dev/root.json"
rekor-url: "https://rekor.my-sigstore.dev"
fulcio-url: "https://fulcio.my-sigstore.dev"
```

This will use keyless signatures and upload the signature to the same repository
as the image. Note that if the Fulcio URL and Rekor URL are not specified, the
plugin will use the default values presented.
### Keyed signing

Note: Currently, only the file-based keyed signing is supported.

#### Using the Public-Good Sigstore Instance

```yml
steps:
- plugins:
- equinixmetal-buildkite/cosign#v0.1.0:
image: "ghcr.io/my-project/my-image@sha256:1e1e4f97dd84970160975922715909577d6c12eaaf6047021875674fa7166c27"
keyless: false
keyed-config:
key: "/path-to/cosign.key"
```

#### Using a custom Sigstore Instance

```yml
steps:
- plugins:
- equinixmetal-buildkite/cosign#v0.1.0:
image: "ghcr.io/my-project/my-image@sha256:1e1e4f97dd84970160975922715909577d6c12eaaf6047021875674fa7166c27"
keyless: false
keyed-config:
tuf-mirror-url: "https://tuf.my-sigstore.dev"
tuf-root-url: "https://tuf.my-sigstore.dev/root.json"
rekor-url: "https://rekor.my-sigstore.dev"
key: "/path-to/cosign.key"
```

## Configuration

### `image` (Required, string)

References the image to sign
References the image to sign.

To avoid issues, use the image digest instead of image tag.
See `Important notes` above for details.

### `keyless` (Optional, boolean)

Expand All @@ -45,18 +105,41 @@ plugin will use a keypair. If not specified, the plugin will default to `true`.
### `keyless-config` (Optional, object)

If `keyless` is set to `true`, the plugin will use the following configuration
parameters to sign the image:

- `fulcio_url` (Optional, string): The URL of the Fulcio server to use. If not
specified, the plugin will default to `https://fulcio.sigstore.dev`.
- `rekor_url` (Optional, string): The URL of the Rekor server to use. If not
specified, the plugin will default to `https://rekor.sigstore.dev`.
parameters to sign the container image:

- `tuf-mirror-url` (Optional, string):
The URL of the TUF server to use. If not specified, the plugin will use
the default TUF URL of the Public-Good Sigstore Instance.
- `tuf-root-url` (Optional, string):
The URL of the TUF root JSON file to use. If not specified, the plugin will use
the default TUF root JSON file URL of the Public-Good Sigstore Instance.
- `rekor_url` (Optional, string):
The URL of the Rekor server to use. If not specified, the plugin will use
the default Rekor URL of the Public-Good Sigstore Instance.
- `fulcio_url` (Optional, string):
The URL of the Fulcio server to use. If not specified, the plugin will use
the default Fulcio URL of the Public-Good Sigstore Instance.
- `oidc-issuer` (Optional, string):
The URL of the OIDC issuer. If not specified, the plugin will use
the default OIDC issuer URL of the Public-Good Sigstore Instance.
- `oidc-provider` (Optional, string):
The URL of the OIDC provider. If not specified, the plugin will use
the default `buildkite-agent` OIDC provider for Buildkite.

### `keyed-config` (Optional, object)

If `keyless` is set to `false`, the plugin will use the following configuration
parameters to sign the image:

- `tuf-mirror-url` (Optional, string):
The URL of the TUF server to use. If not specified, the plugin will use
the default TUF URL of the Public-Good Sigstore Instance.
- `tuf-root-url` (Optional, string):
The URL of the TUF root JSON file to use. If not specified, the plugin will use
the default TUF root JSON file URL of the Public-Good Sigstore Instance.
- `rekor_url` (Optional, string):
The URL of the Rekor server to use. If not specified, the plugin will use
the default Rekor URL of the Public-Good Sigstore Instance.
- `key` (Required, string): The path to the private key to use.

### `cosign-version` (Optional, string)
Expand Down
164 changes: 113 additions & 51 deletions hooks/post-command
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -33,90 +33,145 @@ display_success() {
buildkite-agent annotate --style success "$message<br />" --context "$ctx"
}

# if the supplied image reference does not contain a digest,
# try getting the local image digest to use it instead, and
# if that fails, warn then continue using the supplied image reference
use_image_digest() {
if [[ $image != *"@sha256:"* ]]; then
echo "--- :docker: Getting the local image digest for ${image}"

local digest
digest=$(docker inspect --format='{{index .RepoDigests 0}}' "${image}")

local status=$?
if [[ $status -ne 0 ]]; then
display_error "docker inspect" "Failed to get the local image digest, will continue using supplied image reference ${image}"
else
display_success "docker inspect" "Will continue using ${digest}"
image="${digest}"
fi
fi
}

# Parameters
############
# Common parameters
###################

# This is a required parameter
# image is a required parameter
image=${BUILDKITE_PLUGIN_COSIGN_IMAGE}
if [[ -z "${image}" ]]; then
fail_with_message "cosign" "No image specified"
fail_with_message "cosign" "Image not specified"
fi
use_image_digest

# flags for the cosign sign command
sign_flags=("-y" "--output-signature" "out.sig")

is_keyless=${BUILDKITE_PLUGIN_COSIGN_KEYLESS:-true}

# Hook functions
################

cosign_keyless() {
local fulcio_url=${BUILDKITE_PLUGIN_COSIGN_KEYLESS_CONFIG_FULCIO_URL:-"https://fulcio.sigstore.dev"}
local rekor_url=${BUILDKITE_PLUGIN_COSIG_KEYLESS_CONFIGN_REKOR_URL:-"https://rekor.sigstore.dev"}
local oidc_issuer=${BUILDKITE_PLUGIN_COSIG_KEYLESS_CONFIGN_OIDC_ISSUER:-"https://oauth2.sigstore.dev/auth"}
local oidc_provider=${BUILDKITE_PLUGIN_COSIG_KEYLESS_CONFIGN_OIDC_PROVIDER:-"buildkite-agent"}

echo "--- :key: Cosign keyless signing"

rm out.sig || true

COSIGN_EXPERIMENTAL=1 cosign sign \
-y \
--fulcio-url="${fulcio_url}" \
--rekor-url="${rekor_url}" \
--oidc-issuer="${oidc_issuer}" \
--oidc-provider="${oidc_provider}" \
--output-signature=out.sig \
"${image}"
# if provided, initialise cosign with a custom TUF configuration
cosign_init() {
echo "--- :key: Init cosign"

status=$?
if [[ $status -ne 0 ]]; then
fail_with_message "cosign" "Failed to sign image"
# flags for the cosign initialize command
local init_flags=()

if [[ "${is_keyless}" == true ]]; then
local tuf_mirror_url=${BUILDKITE_PLUGIN_COSIGN_KEYLESS_CONFIG_TUF_MIRROR_URL}
local tuf_root_url=${BUILDKITE_PLUGIN_COSIGN_KEYLESS_CONFIG_TUF_ROOT_URL}
else
local tuf_mirror_url=${BUILDKITE_PLUGIN_COSIGN_KEYED_CONFIG_TUF_MIRROR_URL}
local tuf_root_url=${BUILDKITE_PLUGIN_COSIGN_KEYED_CONFIG_TUF_ROOT_URL}
fi

local signature=$(cat out.sig)
if [[ -n "${tuf_mirror_url}" ]]; then
init_flags+=("--mirror" "${tuf_mirror_url}")
fi

display_success "cosign" "Successfully signed image."
cat <<EOF | buildkite-agent annotate --style success --context "cosign-signature"
### Signed image
\`\`\`
$image
\`\`\`
if [[ -n "${tuf_root_url}" ]]; then
init_flags+=("--root" "${tuf_root_url}")
fi

### Signature
\`\`\`
$signature
\`\`\`
EOF
if [ ${#init_flags[@]} -gt 0 ]; then
rm -rf ~/.sigstore

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering if we need to delete the whole directory. It would be unfortunate if there are other important things in there. The documentation for cosign initialize seems to claim that it updates the tuf thing:

Any updated TUF repository will be written to $HOME/.sigstore/root/.
https://github.com/sigstore/cosign/blob/main/doc/cosign_initialize.md

It looks like it gets overwritten:

$ rm -rf ~/.sigstore/
$ cosign initialize --mirror https://foo.com --root https://foo.com/root.json
(...)
$ ls -l ~/.sigstore/root/
total 10
-rw-------  1 x x 56 Jun 18 21:31 remote.json
drwx------  2 x x  5 Jun 18 21:31 targets
drwxr-xr-x  2 x x  8 Jun 18 21:31 tuf.db
$ date
Tue Jun 18 21:31:55 UTC 2024
$ date
Tue Jun 18 21:32:02 UTC 2024
$ cosign initialize --mirror https://foo.com --root https://foo.com/root.json
(...)
$ ls -l ~/.sigstore/root/
total 10
-rw-------  1 x x 56 Jun 18 21:32 remote.json
drwx------  2 x x  5 Jun 18 21:31 targets
drwxr-xr-x  2 x x  9 Jun 18 21:32 tuf.db
$ ls -l ~/.sigstore/root/tuf.db/
total 16
-rw-r--r--  1 x x 2955 Jun 18 21:32 000004.ldb
-rw-r--r--  1 x x 4928 Jun 18 21:32 000007.log
-rw-r--r--  1 x x   16 Jun 18 21:32 CURRENT
-rw-r--r--  1 x x   16 Jun 18 21:32 CURRENT.bak
-rw-r--r--  1 x x    0 Jun 18 21:31 LOCK
-rw-r--r--  1 x x 2264 Jun 18 21:32 LOG
-rw-r--r--  1 x x   87 Jun 18 21:32 MANIFEST-000008

Copy link
Contributor Author

@prezha prezha Jun 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is not expected to find ~/.sigstore/ there - this is more to ensure that there isn't one

docs suggest removing the whole dir and so that's probably a good idea:
https://docs.sigstore.dev/system_config/public_deployment/#usage and https://docs.sigstore.dev/system_config/public_deployment/#revert-back-to-production

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean this plugin can only run serially on a given host? Is there a variable one can set to control this directory? Would be ideal if it were unique per plugin run.

Copy link
Contributor Author

@prezha prezha Jun 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, thanks!

looks like that's not directly exposed/customisable by cosign initialize (via flags) but the sigstore tuf client reads the TUF_ROOT env var to figure out the location, so i'll try to use that:

// TufRootEnv is the name of the environment variable that locates an alternate local TUF root location.
TufRootEnv = "TUF_ROOT"

=> https://github.com/sigstore/sigstore/blob/b777e4be352ebf9394d534271f3dd888908e839a/pkg/tuf/client.go#L559

similarly, i made the out.sig filename "dynamic" (pseudo-random) for the same reasons (ie, to avoid potential race condition over the output file between multiple plugin runs on the same host)

waiting for the build (using this plugin) to complete and i'll share results back

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, looks like that worked:

🔑 Init cosign
Root status:
 {
	"local": "/var/lib/buildkite-agent/.sigstore-13508/root",
	"remote": "https://<REDACTED>",
	"metadata": {
		"root.json": {
			"version": 1,
			"len": 2178,
			"expiration": "01 Dec 24 19:08 UTC",
			"error": ""
		},
		"snapshot.json": {
			"version": 1,
			"len": 618,
			"expiration": "01 Dec 24 19:08 UTC",
			"error": ""
		},
		"targets.json": {
			"version": 1,
			"len": 1373,
			"expiration": "01 Dec 24 19:08 UTC",
			"error": ""
		},
		"timestamp.json": {
			"version": 1,
			"len": 619,
			"expiration": "01 Dec 24 19:08 UTC",
			"error": ""
		}
	},
	"targets": [
		"ctfe.pub",
		"fulcio_v1.crt.pem",
		"rekor.pub"
	]
}
Successfully initialised

Copy link
Contributor Author

@prezha prezha Jun 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps we could pr to update cosign initialize cmd's docs and code so users are aware they can set tuf's root directory using the TUF_ROOT env var

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fwiw, I used @prezha's TUF_ROOT environment variable finding in a separate project to run multiple instances of cosign simultaneously on the same computer. Prior to making that change, I was experiencing "resource busy" errors due to each cosign process modifying / writing files in ~/.sigstore. I think this emulates the same kind of situation we would see in a Buildkite CI runner.

In other words: @prezha's change appears to prevent cosign from stepping on other cosign processes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks @sfox-equinix for checking and confirming!

@JAORMX is there anything additional you think should be addressed?


rm out.sig || true
cosign initialize "${init_flags[@]}"

local status=$?
if [[ $status -ne 0 ]]; then
fail_with_message "cosign" "Failed to initialise"
fi
display_success "cosign" "Successfully initialised"
else
display_success "cosign" "Initialisation not required, skipping"
fi
}

cosign_keyed() {
echo "--- :key: Cosign keyed signing"
setup_keyless() {
echo "--- :key: Setup cosign keyless signing"

local rekor_url=${BUILDKITE_PLUGIN_COSIGN_KEYLESS_CONFIG_REKOR_URL}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need to set a default URL here?

Copy link
Contributor Author

@prezha prezha Jun 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks @tenyo !

the default values for the public-good sigstore instance are baked into the cosign binary (that we download and use while plugin is run), and i think we should relay on those in cosign, rather than having our own hardcoded values, that might change at some point - eg, this comment:

// TODO: change this back to api.SigstorePublicServerURL after the v1 migration is complete.

so, for rekor, it's defined here then used as the flag's default value here

for eg, tuf, it's here, etc.

if [[ -n "${rekor_url}" ]]; then
sign_flags+=("--rekor-url" "${rekor_url}")
fi

local fulcio_url=${BUILDKITE_PLUGIN_COSIGN_KEYLESS_CONFIG_FULCIO_URL}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for this - maybe these are set automatically by cosign but figured I'd ask.

Copy link
Contributor Author

@prezha prezha Jun 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above - for fulcio, it's defined here

if [[ -n "${fulcio_url}" ]]; then
sign_flags+=("--fulcio-url" "${fulcio_url}")
fi

local oidc_issuer=${BUILDKITE_PLUGIN_COSIGN_KEYLESS_CONFIG_OIDC_ISSUER}
if [[ -n "${oidc_issuer}" ]]; then
sign_flags+=("--oidc-issuer" "${oidc_issuer}")
fi

local oidc_provider=${BUILDKITE_PLUGIN_COSIGN_KEYLESS_CONFIG_OIDC_PROVIDER:-"buildkite-agent"}
if [[ -n "${oidc_provider}" ]]; then
sign_flags+=("--oidc-provider" "${oidc_provider}")
fi
}

setup_keyed() {
echo "--- :key: Setup cosign keyed signing"

local rekor_url=${BUILDKITE_PLUGIN_COSIGN_KEYED_CONFIG_REKOR_URL}
if [[ -n "${rekor_url}" ]]; then
sign_flags+=("--rekor-url" "${rekor_url}")
fi

local key=${BUILDKITE_PLUGIN_COSIGN_KEYED_CONFIG_KEY:-}
if [[ -z "${key}" ]]; then
fail_with_message "cosign" "Key not specified"
fi

if [[ ! -f "${key}" ]]; then
fail_with_message "cosign" "Key file not found in path ${key}"
fi

rm out.sig || true
sign_flags+=("--key" "${key}")
}

# sign the image
cosign_sign() {
echo "--- :key: Signing image with cosign"

rm -f out.sig

cosign sign \
-y \
--key="${key}" \
--output-signature=out.sig \
"${sign_flags[@]}" \
"${image}"

status=$?
local status=$?
if [[ $status -ne 0 ]]; then
fail_with_message "cosign" "Failed to sign image"
fi

local signature=$(cat out.sig)
local signature
signature=$(cat out.sig)

display_success "cosign" "Successfully signed image."
display_success "cosign" "Successfully signed image"
cat <<EOF | buildkite-agent annotate --style success --context "cosign-signature"
### Signed image
\`\`\`
Expand All @@ -129,11 +184,18 @@ $signature
\`\`\`
EOF

rm out.sig || true
rm -f out.sig
}

if [[ "${is_keyless}" == "true" ]]; then
cosign_keyless
# Main
#######

cosign_init

if [[ "${is_keyless}" == true ]]; then
setup_keyless
else
cosign_keyed
setup_keyed
fi

cosign_sign
Loading
Loading