From f278f7b49b8475252ed7e0e4e38ba8b34490cb76 Mon Sep 17 00:00:00 2001 From: Predrag Rogic Date: Fri, 14 Jun 2024 03:23:58 +0100 Subject: [PATCH 1/5] Enable signing using a custom sigstore instance This PR extends plugin schema to allow users to specify a custom, ie, non-Public-Good Sigstore Instance to use, including the TUF URLs (used to initialise cosign). Sensible defaults use the Public-Good Sigstore Instance with their current URLs (known to cosign) and the buildkite-agent as the OIDC provider. Documentation was amended to emphasise usage of image digest vs tag and to add more examples for both keyless and keyed signing using the Public-Good and a custom sigstore instance. Bonus: - fix BUILDKITE_PLUGIN_COSIGN variables names - remove the need to explicitly state the Public-Good Sigstore Instance default params (ie, the URLs) - those might change and cosign would know them - refactor code to group and then reuse the common logic, increase readability - bump cosign default version to 2.2.4 - bump plugin-tester to version 4.1.1 - use specific plugin-linter version 2.1.0 instead of latest - fix tests/pre-checkout.bats to work with v2.1.0 --- .github/workflows/release.yml | 1 - Makefile | 6 +- README.md | 131 ++++++++++++++++++++++----- hooks/post-command | 162 +++++++++++++++++++++++----------- hooks/pre-checkout | 8 +- plugin.yml | 31 +++++-- tests/pre-checkout.bats | 2 +- 7 files changed, 248 insertions(+), 93 deletions(-) mode change 100644 => 100755 hooks/post-command diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5a021bb..c2d710c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,4 +20,3 @@ jobs: uses: softprops/action-gh-release@v1 with: generate_release_notes: true - diff --git a/Makefile b/Makefile index 49d756a..59bd238 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 5a278b9..4da4e4b 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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) diff --git a/hooks/post-command b/hooks/post-command old mode 100644 new mode 100755 index ed45983..88df6dd --- a/hooks/post-command +++ b/hooks/post-command @@ -33,80 +33,134 @@ display_success() { buildkite-agent annotate --style success "$message
" --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}") + + 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 + 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 < Date: Wed, 19 Jun 2024 10:20:57 +0100 Subject: [PATCH 2/5] make status and init_flags local vars --- hooks/post-command | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hooks/post-command b/hooks/post-command index 88df6dd..c9d1bb9 100755 --- a/hooks/post-command +++ b/hooks/post-command @@ -43,7 +43,7 @@ use_image_digest() { local digest digest=$(docker inspect --format='{{index .RepoDigests 0}}' "${image}") - status=$? + 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 @@ -76,7 +76,7 @@ cosign_init() { echo "--- :key: Init cosign" # flags for the cosign initialize command - init_flags=() + local init_flags=() if [[ "${is_keyless}" == true ]]; then local tuf_mirror_url=${BUILDKITE_PLUGIN_COSIGN_KEYLESS_CONFIG_TUF_MIRROR_URL} @@ -99,7 +99,7 @@ cosign_init() { cosign initialize "${init_flags[@]}" - status=$? + local status=$? if [[ $status -ne 0 ]]; then fail_with_message "cosign" "Failed to initialise" fi @@ -163,7 +163,7 @@ cosign_sign() { "${sign_flags[@]}" \ "${image}" - status=$? + local status=$? if [[ $status -ne 0 ]]; then fail_with_message "cosign" "Failed to sign image" fi From 0eadbd6d143f63bbc05a3291db7bec02a560cbe4 Mon Sep 17 00:00:00 2001 From: Predrag Rogic Date: Thu, 20 Jun 2024 17:04:02 +0100 Subject: [PATCH 3/5] enable safe concurrent plugin runs by using randomm locations --- hooks/post-command | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/hooks/post-command b/hooks/post-command index c9d1bb9..2c96aa0 100755 --- a/hooks/post-command +++ b/hooks/post-command @@ -63,8 +63,18 @@ if [[ -z "${image}" ]]; then fi use_image_digest +# generate a random number to enable safe concurrent plugin runs +random="${RANDOM}" + +# set the TUF root directory to a custom location +# ref: https://github.com/sigstore/sigstore/blob/b777e4be352ebf9394d534271f3dd888908e839a/pkg/tuf/client.go#L53-L54 +export TUF_ROOT="${HOME}/.sigstore-${random}/root" + +# set the output signature to a custom file name +sigfile="sigstore-${random}.sig" + # flags for the cosign sign command -sign_flags=("-y" "--output-signature" "out.sig") +sign_flags=("-y" "--output-signature" "${sigfile}") is_keyless=${BUILDKITE_PLUGIN_COSIGN_KEYLESS:-true} @@ -95,7 +105,7 @@ cosign_init() { fi if [ ${#init_flags[@]} -gt 0 ]; then - rm -rf ~/.sigstore + rm -rf "${TUF_ROOT}" cosign initialize "${init_flags[@]}" @@ -157,7 +167,7 @@ setup_keyed() { cosign_sign() { echo "--- :key: Signing image with cosign" - rm -f out.sig + rm -f "${sigfile}" cosign sign \ "${sign_flags[@]}" \ @@ -169,7 +179,7 @@ cosign_sign() { fi local signature - signature=$(cat out.sig) + signature=$(cat "${sigfile}") display_success "cosign" "Successfully signed image" cat < Date: Wed, 26 Jun 2024 12:52:26 +0100 Subject: [PATCH 4/5] default to keyed signing --- README.md | 16 ++++++++++------ hooks/post-command | 2 +- plugin.yml | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4da4e4b..1488b7d 100644 --- a/README.md +++ b/README.md @@ -36,20 +36,24 @@ repository as the container image. #### Using the Public-Good Sigstore Instance +>WARNING: risk of data leakage - sensitive information may be unintentionally exposed to the public, do not use for non-public repos! + ```yml steps: - plugins: - equinixmetal-buildkite/cosign#v0.1.0: image: "ghcr.io/my-project/my-image@sha256:1e1e4f97dd84970160975922715909577d6c12eaaf6047021875674fa7166c27" + keyless: true ``` -#### Using a custom Sigstore Instance +#### Using a custom/private Sigstore Instance ```yml steps: - plugins: - equinixmetal-buildkite/cosign#v0.1.0: image: "ghcr.io/my-project/my-image@sha256:1e1e4f97dd84970160975922715909577d6c12eaaf6047021875674fa7166c27" + keyless: true keyless-config: tuf-mirror-url: "https://tuf.my-sigstore.dev" tuf-root-url: "https://tuf.my-sigstore.dev/root.json" @@ -57,30 +61,30 @@ steps: fulcio-url: "https://fulcio.my-sigstore.dev" ``` -### Keyed signing +### Keyed signing (default) Note: Currently, only the file-based keyed signing is supported. #### Using the Public-Good Sigstore Instance +>WARNING: risk of data leakage - sensitive information may be unintentionally exposed to the public, do not use for non-public repos! + ```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 +#### Using a custom/private 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" @@ -100,7 +104,7 @@ See `Important notes` above for details. ### `keyless` (Optional, boolean) If set to `true`, the plugin will use keyless signatures. If set to `false`, the -plugin will use a keypair. If not specified, the plugin will default to `true`. +plugin will use a keypair. If not specified, the plugin will default to `false`. ### `keyless-config` (Optional, object) diff --git a/hooks/post-command b/hooks/post-command index 2c96aa0..7a4a606 100755 --- a/hooks/post-command +++ b/hooks/post-command @@ -76,7 +76,7 @@ sigfile="sigstore-${random}.sig" # flags for the cosign sign command sign_flags=("-y" "--output-signature" "${sigfile}") -is_keyless=${BUILDKITE_PLUGIN_COSIGN_KEYLESS:-true} +is_keyless=${BUILDKITE_PLUGIN_COSIGN_KEYLESS:-false} # Hook functions ################ diff --git a/plugin.yml b/plugin.yml index 6854e53..f8c84d7 100644 --- a/plugin.yml +++ b/plugin.yml @@ -14,7 +14,7 @@ configuration: keyless: type: boolean description: "Use keyless signing" - default: true + default: false keyless-config: type: object properties: From bb24bfa2d0ace0f5adab9b9d41ef24c04898b660 Mon Sep 17 00:00:00 2001 From: Predrag Rogic Date: Mon, 1 Jul 2024 14:11:34 +0100 Subject: [PATCH 5/5] respect least surprise principle - keep default keyless and warnings --- README.md | 10 +++++----- hooks/post-command | 2 +- plugin.yml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1488b7d..c3be136 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ 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 +### Keyless signing (default) #### Using the Public-Good Sigstore Instance @@ -43,7 +43,6 @@ steps: - plugins: - equinixmetal-buildkite/cosign#v0.1.0: image: "ghcr.io/my-project/my-image@sha256:1e1e4f97dd84970160975922715909577d6c12eaaf6047021875674fa7166c27" - keyless: true ``` #### Using a custom/private Sigstore Instance @@ -53,7 +52,6 @@ steps: - plugins: - equinixmetal-buildkite/cosign#v0.1.0: image: "ghcr.io/my-project/my-image@sha256:1e1e4f97dd84970160975922715909577d6c12eaaf6047021875674fa7166c27" - keyless: true keyless-config: tuf-mirror-url: "https://tuf.my-sigstore.dev" tuf-root-url: "https://tuf.my-sigstore.dev/root.json" @@ -61,7 +59,7 @@ steps: fulcio-url: "https://fulcio.my-sigstore.dev" ``` -### Keyed signing (default) +### Keyed signing Note: Currently, only the file-based keyed signing is supported. @@ -74,6 +72,7 @@ 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" ``` @@ -85,6 +84,7 @@ 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" @@ -104,7 +104,7 @@ See `Important notes` above for details. ### `keyless` (Optional, boolean) If set to `true`, the plugin will use keyless signatures. If set to `false`, the -plugin will use a keypair. If not specified, the plugin will default to `false`. +plugin will use a keypair. If not specified, the plugin will default to `true`. ### `keyless-config` (Optional, object) diff --git a/hooks/post-command b/hooks/post-command index 7a4a606..2c96aa0 100755 --- a/hooks/post-command +++ b/hooks/post-command @@ -76,7 +76,7 @@ sigfile="sigstore-${random}.sig" # flags for the cosign sign command sign_flags=("-y" "--output-signature" "${sigfile}") -is_keyless=${BUILDKITE_PLUGIN_COSIGN_KEYLESS:-false} +is_keyless=${BUILDKITE_PLUGIN_COSIGN_KEYLESS:-true} # Hook functions ################ diff --git a/plugin.yml b/plugin.yml index f8c84d7..6854e53 100644 --- a/plugin.yml +++ b/plugin.yml @@ -14,7 +14,7 @@ configuration: keyless: type: boolean description: "Use keyless signing" - default: false + default: true keyless-config: type: object properties: