From a3bf6e79c2fa63624786551f444da76d05ded148 Mon Sep 17 00:00:00 2001 From: Sahin Yort Date: Mon, 2 Jan 2023 18:45:17 +0300 Subject: [PATCH] feat: implement cosign_sign (#36) --- BUILD.bazel | 1 + WORKSPACE | 4 ++ cosign/BUILD.bazel | 29 ++++++++++++ cosign/defs.bzl | 5 ++ cosign/private/BUILD.bazel | 25 ++++++++++ cosign/private/sign.bzl | 77 +++++++++++++++++++++++++++++++ cosign/private/sign.sh.tpl | 26 +++++++++++ cosign/private/versions.bzl | 13 ++++++ cosign/repositories.bzl | 67 +++++++++++++++++++++++++++ cosign/toolchain.bzl | 54 ++++++++++++++++++++++ docs/BUILD.bazel | 5 ++ docs/cosign_sign.md | 55 ++++++++++++++++++++++ example/BUILD.bazel | 2 +- example/sign/BUILD.bazel | 50 ++++++++++++++++++++ example/sign/app.bash | 1 + example/sign/test.bash | 32 +++++++++++++ oci/BUILD.bazel | 9 ++-- oci/private/BUILD.bazel | 21 +++++---- oci/private/toolchains_repo.bzl | 2 +- scripts/mirror_releases_cosign.sh | 44 ++++++++++++++++++ 20 files changed, 506 insertions(+), 16 deletions(-) create mode 100644 cosign/BUILD.bazel create mode 100644 cosign/defs.bzl create mode 100644 cosign/private/BUILD.bazel create mode 100644 cosign/private/sign.bzl create mode 100644 cosign/private/sign.sh.tpl create mode 100644 cosign/private/versions.bzl create mode 100644 cosign/repositories.bzl create mode 100644 cosign/toolchain.bzl create mode 100644 docs/cosign_sign.md create mode 100644 example/sign/BUILD.bazel create mode 100755 example/sign/app.bash create mode 100755 example/sign/test.bash create mode 100755 scripts/mirror_releases_cosign.sh diff --git a/BUILD.bazel b/BUILD.bazel index e7269d17..8650b803 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -6,6 +6,7 @@ gazelle_binary( languages = ["@bazel_skylib//gazelle/bzl"], ) +# gazelle:exclude example/** gazelle( name = "gazelle", gazelle = "gazelle_bin", diff --git a/WORKSPACE b/WORKSPACE index e27a987f..537aed39 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -20,6 +20,10 @@ oci_register_toolchains( zot_version = LATEST_ZOT_VERSION, ) +load("//cosign:repositories.bzl", "cosign_register_toolchains") + +cosign_register_toolchains(name = "oci_cosign") + # For running our own unit tests load("@bazel_skylib//lib:unittest.bzl", "register_unittest_toolchains") diff --git a/cosign/BUILD.bazel b/cosign/BUILD.bazel new file mode 100644 index 00000000..daaa9112 --- /dev/null +++ b/cosign/BUILD.bazel @@ -0,0 +1,29 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +toolchain_type( + name = "toolchain_type", + visibility = ["//visibility:public"], +) + +bzl_library( + name = "defs", + srcs = ["defs.bzl"], + visibility = ["//visibility:public"], + deps = ["//cosign/private:sign"], +) + +bzl_library( + name = "repositories", + srcs = ["repositories.bzl"], + visibility = ["//visibility:public"], + deps = [ + "//cosign/private:versions", + "//oci/private:toolchains_repo", + ], +) + +bzl_library( + name = "toolchain", + srcs = ["toolchain.bzl"], + visibility = ["//visibility:public"], +) diff --git a/cosign/defs.bzl b/cosign/defs.bzl new file mode 100644 index 00000000..136a5116 --- /dev/null +++ b/cosign/defs.bzl @@ -0,0 +1,5 @@ +"Public API" + +load("//cosign/private:sign.bzl", _cosign_sign = "cosign_sign") + +cosign_sign = _cosign_sign diff --git a/cosign/private/BUILD.bazel b/cosign/private/BUILD.bazel new file mode 100644 index 00000000..76fe39aa --- /dev/null +++ b/cosign/private/BUILD.bazel @@ -0,0 +1,25 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +exports_files( + glob(["*.bzl"]), + visibility = ["//docs:__pkg__"], +) + +exports_files([ + "sign.sh.tpl", +]) + +bzl_library( + name = "sign", + srcs = ["sign.bzl"], + visibility = [ + "//cosign:__subpackages__", + "//docs:__pkg__", + ], +) + +bzl_library( + name = "versions", + srcs = ["versions.bzl"], + visibility = ["//cosign:__subpackages__"], +) diff --git a/cosign/private/sign.bzl b/cosign/private/sign.bzl new file mode 100644 index 00000000..6d3c9118 --- /dev/null +++ b/cosign/private/sign.bzl @@ -0,0 +1,77 @@ +"Implementation details for sign rule" + +_DOC = """Sign an oci_image using cosign binary at a remote registry. + +It signs the image by its digest determined beforehand. + +```starlark +oci_image( + name = "image" +) + +cosign_sign( + name = "sign", + image = ":image", + repository = "index.docker.io/org/image" +) +``` + +`repository` attribute can be overridden using the `--repository` flag. + +```starlark +oci_image( + name = "image" +) + +cosign_sign( + name = "sign", + image = ":image", + repository = "index.docker.io/org/image" +) +``` + +run `bazel run :sign -- --repository=index.docker.io/org/test` +""" + +_attrs = { + "image": attr.label(allow_single_file = True, doc = "Label to an oci_image"), + "repository": attr.string(mandatory = True, doc = "Repository URL where the image will be signed at. eg: index.docker.io//image. digests and tags are disallowed."), + "_sign_sh_tpl": attr.label(default = "sign.sh.tpl", allow_single_file = True), +} + +def _cosign_sign_impl(ctx): + cosign = ctx.toolchains["@contrib_rules_oci//cosign:toolchain_type"] + yq = ctx.toolchains["@aspect_bazel_lib//lib:yq_toolchain_type"] + + if ctx.attr.repository.find(":") != -1 or ctx.attr.repository.find("@") != -1: + fail("repository attribute should not contain digest or tag.") + + executable = ctx.actions.declare_file("cosign_sign_{}.sh".format(ctx.label.name)) + ctx.actions.expand_template( + template = ctx.file._sign_sh_tpl, + output = executable, + is_executable = True, + substitutions = { + "{{cosign_path}}": cosign.cosign_info.binary.short_path, + "{{yq_path}}": yq.yqinfo.bin.short_path, + "{{image_dir}}": ctx.file.image.short_path, + "{{fixed_args}}": " ".join(["--repository", ctx.attr.repository]), + }, + ) + + runfiles = ctx.runfiles(files = [ctx.file.image]) + runfiles = runfiles.merge(yq.default.default_runfiles) + runfiles = runfiles.merge(cosign.default.default_runfiles) + + return DefaultInfo(executable = executable, runfiles = runfiles) + +cosign_sign = rule( + implementation = _cosign_sign_impl, + attrs = _attrs, + doc = _DOC, + executable = True, + toolchains = [ + "@contrib_rules_oci//cosign:toolchain_type", + "@aspect_bazel_lib//lib:yq_toolchain_type", + ], +) diff --git a/cosign/private/sign.sh.tpl b/cosign/private/sign.sh.tpl new file mode 100644 index 00000000..56f0992d --- /dev/null +++ b/cosign/private/sign.sh.tpl @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -o pipefail -o errexit -o nounset + +readonly COSIGN="{{cosign_path}}" +readonly YQ="{{yq_path}}" +readonly IMAGE_DIR="{{image_dir}}" +readonly DIGEST=$("${YQ}" '.manifests[].digest' "${IMAGE_DIR}/index.json") +readonly FIXED_ARGS=({{fixed_args}}) + +# set $@ to be FIXED_ARGS+$@ +ARGS=(${FIXED_ARGS[@]} $@) +set -- ${ARGS[@]} + +REPOSITORY="" +ARGS=() + +while (( $# > 0 )); do + case "$1" in + --repository) shift; REPOSITORY="$1"; shift ;; + (--repository=*) REPOSITORY="${1#--repository=}"; shift ;; + *) ARGS+=( "$1" ); shift ;; + esac +done + +exec "${COSIGN}" sign "${REPOSITORY}@${DIGEST}" ${ARGS[@]+"${ARGS[@]}"} + diff --git a/cosign/private/versions.bzl b/cosign/private/versions.bzl new file mode 100644 index 00000000..c0848e31 --- /dev/null +++ b/cosign/private/versions.bzl @@ -0,0 +1,13 @@ + + +COSIGN_VERSIONS = { + "v1.6.0": { + "darwin-amd64": "sha256-/P8XqU+4pQmMm5tiPi4ZDMTTxHxPXo2/dbcqVqh0shk=", + "darwin-arm64": "sha256-5Z+0mjzAOtu4Hb0vXNYgb+CUec27dCbN0bIqr5FFu7w=", + "linux-amd64": "sha256-tirIwasc2wctRC0vPbfX/+l3VmphcM0D3UjkWD2tMgM=", + "linux-arm": "sha256-y2+8Kaq6iWMLoJgWjCIMmZI/e9OCElOwb6dyiHSNdR8=", + "linux-arm64": "sha256-XxyLsrMMdfsccsJmsI2c/FF924tjLjVif9Y6rwno8b0=", + "linux-ppc64le": "sha256-Do6Fc7PUfxpAhvocpDTjpq1LZUs8AIaYIQYnmH7gPMw=", + "linux-s390x": "sha256-53+ZFZljoXRuvYGoGaxfgb2oJyUGCqesr8PbTXRnb68=" + } +} \ No newline at end of file diff --git a/cosign/repositories.bzl b/cosign/repositories.bzl new file mode 100644 index 00000000..9211fe6a --- /dev/null +++ b/cosign/repositories.bzl @@ -0,0 +1,67 @@ +"""Repository rules for fetching cosign""" + +load("//cosign/private:versions.bzl", "COSIGN_VERSIONS") + +# buildifier: disable=bzl-visibility +load("//oci/private:toolchains_repo.bzl", "PLATFORMS", "toolchains_repo") + +COSIGN_BUILD_TMPL = """\ +# Generated by container/repositories.bzl +load("@contrib_rules_oci//cosign:toolchain.bzl", "cosign_toolchain") +cosign_toolchain( + name = "cosign_toolchain", + cosign = "cosign" +) +""" + +def _cosign_repo_impl(repository_ctx): + platform = repository_ctx.attr.platform.replace("x86_64", "amd64").replace("_", "-") + url = "https://github.com/sigstore/cosign/releases/download/{version}/cosign-{platform}".format( + version = repository_ctx.attr.cosign_version, + platform = platform, + ) + repository_ctx.download( + url = url, + output = "cosign", + executable = True, + integrity = COSIGN_VERSIONS[repository_ctx.attr.cosign_version][platform], + ) + repository_ctx.file("BUILD.bazel", COSIGN_BUILD_TMPL) + +cosign_repositories = repository_rule( + _cosign_repo_impl, + doc = "Fetch external tools needed for cosign toolchain", + attrs = { + "cosign_version": attr.string(mandatory = True, values = COSIGN_VERSIONS.keys()), + "platform": attr.string(mandatory = True, values = PLATFORMS.keys()), + }, +) + +# Wrapper macro around everything above, this is the primary API +def cosign_register_toolchains(name): + """Convenience macro for users which does typical setup. + + - create a repository for each built-in platform like "cosign_linux_amd64" - + this repository is lazily fetched when node is needed for that platform. + - create a repository exposing toolchains for each platform like "oci_platforms" + - register a toolchain pointing at each platform + Users can avoid this macro and do these steps themselves, if they want more control. + Args: + name: base name for cosign repository, like "oci_cosign" + """ + toolchain_name = "{name}_toolchains".format(name = name) + + for platform in PLATFORMS.keys(): + cosign_repositories( + name = "{name}_{platform}".format(name = name, platform = platform), + platform = platform, + cosign_version = COSIGN_VERSIONS.keys()[0], + ) + native.register_toolchains("@{}//:{}_toolchain".format(toolchain_name, platform)) + + toolchains_repo( + name = toolchain_name, + toolchain_type = "@contrib_rules_oci//cosign:toolchain_type", + # avoiding use of .format since {platform} is formatted by toolchains_repo for each platform. + toolchain = "@%s_{platform}//:cosign_toolchain" % name, + ) diff --git a/cosign/toolchain.bzl b/cosign/toolchain.bzl new file mode 100644 index 00000000..95341111 --- /dev/null +++ b/cosign/toolchain.bzl @@ -0,0 +1,54 @@ +"""This module implements the cosign-specific toolchain rule.""" + +CosignInfo = provider( + doc = "Information about how to invoke the cosign executable.", + fields = { + "binary": "Executable cosign binary", + }, +) + +def _cosign_toolchain_impl(ctx): + binary = ctx.executable.cosign + + # Make the $(COSIGN_BIN) variable available in places like genrules. + # See https://docs.bazel.build/versions/main/be/make-variables.html#custom_variables + template_variables = platform_common.TemplateVariableInfo({ + "COSIGN_BIN": binary.path, + }) + default = DefaultInfo( + files = depset([binary]), + runfiles = ctx.runfiles(files = [binary]), + ) + cosign_info = CosignInfo( + binary = binary, + ) + + # Export all the providers inside our ToolchainInfo + # so the resolved_toolchain rule can grab and re-export them. + toolchain_info = platform_common.ToolchainInfo( + cosign_info = cosign_info, + template_variables = template_variables, + default = default, + ) + return [ + default, + toolchain_info, + template_variables, + ] + +cosign_toolchain = rule( + implementation = _cosign_toolchain_impl, + attrs = { + "cosign": attr.label( + doc = "A hermetically downloaded cosign executable target for the target platform.", + mandatory = True, + allow_single_file = True, + executable = True, + cfg = "exec", + ), + }, + doc = """Defines a cosign toolchain. + +For usage see https://docs.bazel.build/versions/main/toolchains.html#defining-toolchains. +""", +) diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel index 3c0f639c..2fd486cd 100644 --- a/docs/BUILD.bazel +++ b/docs/BUILD.bazel @@ -17,4 +17,9 @@ stardoc_with_diff_test( bzl_library_target = "//oci/private:image_index", ) +stardoc_with_diff_test( + name = "cosign_sign", + bzl_library_target = "//cosign/private:sign", +) + update_docs(name = "update") diff --git a/docs/cosign_sign.md b/docs/cosign_sign.md new file mode 100644 index 00000000..3e585eae --- /dev/null +++ b/docs/cosign_sign.md @@ -0,0 +1,55 @@ + + +Implementation details for sign rule + + + +## cosign_sign + +
+cosign_sign(name, image, repository)
+
+ +Sign an oci_image using cosign binary at a remote registry. + +It signs the image by its digest determined beforehand. + +```starlark +oci_image( + name = "image" +) + +cosign_sign( + name = "sign", + image = ":image", + repository = "index.docker.io/org/image" +) +``` + +`repository` attribute can be overridden using the `--repository` flag. + +```starlark +oci_image( + name = "image" +) + +cosign_sign( + name = "sign", + image = ":image", + repository = "index.docker.io/org/image" +) +``` + +run `bazel run :sign -- --repository=index.docker.io/org/test` + + +**ATTRIBUTES** + + +| Name | Description | Type | Mandatory | Default | +| :------------- | :------------- | :------------- | :------------- | :------------- | +| name | A unique name for this target. | Name | required | | +| image | Label to an oci_image | Label | optional | None | +| repository | Repository URL where the image will be signed at. eg: index.docker.io/<user>/image. digests and tags are disallowed. | String | required | | + + diff --git a/example/BUILD.bazel b/example/BUILD.bazel index 16e16666..e0d764d1 100644 --- a/example/BUILD.bazel +++ b/example/BUILD.bazel @@ -38,6 +38,6 @@ genrule( }), output_to_bindir = True, toolchains = [ - "@oci_crane_toolchains//:resolved_toolchain", + "@oci_crane_toolchains//:current_toolchain", ], ) diff --git a/example/sign/BUILD.bazel b/example/sign/BUILD.bazel new file mode 100644 index 00000000..42435a09 --- /dev/null +++ b/example/sign/BUILD.bazel @@ -0,0 +1,50 @@ +load("//cosign:defs.bzl", "cosign_sign") +load("//oci:defs.bzl", "oci_image") +load("@rules_pkg//:pkg.bzl", "pkg_tar") + +pkg_tar( + name = "app", + srcs = ["app.bash"], +) + +oci_image( + name = "image", + architecture = select({ + "@platforms//cpu:arm64": "arm64", + "@platforms//cpu:x86_64": "amd64", + }), + base = "//example:base", + cmd = ["app.bash"], + os = "linux", + tars = [":app.tar"], +) + +cosign_sign( + name = "sign", + image = ":image", + repository = "test", +) + +sh_test( + name = "test", + srcs = ["test.bash"], + args = [ + "$(CRANE_BIN)", + "$(COSIGN_BIN)", + "$(LAUNCHER)", + "$(location :sign)", + "$(location :image)", + ], + data = [ + ":image", + ":sign", + "@oci_cosign_toolchains//:current_toolchain", + "@oci_crane_toolchains//:current_toolchain", + "@oci_zot_toolchains//:current_toolchain", + ], + toolchains = [ + "@oci_zot_toolchains//:current_toolchain", + "@oci_cosign_toolchains//:current_toolchain", + "@oci_crane_toolchains//:current_toolchain", + ], +) diff --git a/example/sign/app.bash b/example/sign/app.bash new file mode 100755 index 00000000..d49fcc4a --- /dev/null +++ b/example/sign/app.bash @@ -0,0 +1 @@ +echo "hello world!" \ No newline at end of file diff --git a/example/sign/test.bash b/example/sign/test.bash new file mode 100755 index 00000000..9c679118 --- /dev/null +++ b/example/sign/test.bash @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -o pipefail -o errexit -o nounset + +readonly CRANE="$1" +readonly COSIGN="$2" +readonly REGISTRY_LAUNCHER="$3" +readonly IMAGE_SIGNER="$4" +readonly IMAGE="$5" + +# Launch a registry instance at a random port +source "${REGISTRY_LAUNCHER}" +start_registry $TEST_TMPDIR $TEST_TMPDIR/output.log +echo "Registry is running at ${REGISTRY}" + +readonly REPOSITORY="${REGISTRY}/local" + +# Create key-pair +COSIGN_PASSWORD=123 "${COSIGN}" generate-key-pair + +# Sign the image at remote registry +COSIGN_PASSWORD=123 "${IMAGE_SIGNER}" --repository="${REPOSITORY}" --key=cosign.key + + +# Now push the image +REF=$(mktemp) +"${CRANE}" push "${IMAGE}" "${REPOSITORY}" --image-refs="${REF}" + +# Verify using the Tag +"${COSIGN}" verify "${REPOSITORY}:latest" --key=cosign.pub + +# Verify using the Digest +"${COSIGN}" verify "$(cat ${REF})" --key=cosign.pub diff --git a/oci/BUILD.bazel b/oci/BUILD.bazel index 923c6df4..2e59c40c 100644 --- a/oci/BUILD.bazel +++ b/oci/BUILD.bazel @@ -1,8 +1,5 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") -# For stardoc to reference the files -exports_files(["container.bzl"]) - # These are the targets rule authors should put in their "toolchains" # attribute in order to get a crane/zot executable for the correct platform. # See https://docs.bazel.build/versions/main/toolchains.html#writing-rules-that-use-toolchains @@ -56,7 +53,11 @@ bzl_library( name = "defs", srcs = ["defs.bzl"], visibility = ["//visibility:public"], - deps = ["//oci/private:tarball"], + deps = [ + "//oci/private:image", + "//oci/private:image_index", + "//oci/private:tarball", + ], ) bzl_library( diff --git a/oci/private/BUILD.bazel b/oci/private/BUILD.bazel index 4885e838..e875561e 100644 --- a/oci/private/BUILD.bazel +++ b/oci/private/BUILD.bazel @@ -15,7 +15,7 @@ filegroup( name = "package_content", srcs = glob([ "*.bzl", - "*.bazel", + "*.sh.tpl", ]), visibility = ["//oci:__pkg__"], ) @@ -24,18 +24,12 @@ bzl_library( name = "tarball", srcs = ["tarball.bzl"], visibility = [ - "//docs:__subpackages__", + "//docs:__pkg__", "//oci:__subpackages__", ], deps = ["@rules_pkg//pkg:bzl_srcs"], ) -bzl_library( - name = "toolchains_repo", - srcs = ["toolchains_repo.bzl"], - visibility = ["//oci:__subpackages__"], -) - bzl_library( name = "image", srcs = ["image.bzl"], @@ -55,9 +49,16 @@ bzl_library( ) bzl_library( - name = "versions", - srcs = ["versions.bzl"], + name = "toolchains_repo", + srcs = ["toolchains_repo.bzl"], visibility = [ + "//cosign:__subpackages__", "//oci:__subpackages__", ], ) + +bzl_library( + name = "versions", + srcs = ["versions.bzl"], + visibility = ["//oci:__subpackages__"], +) diff --git a/oci/private/toolchains_repo.bzl b/oci/private/toolchains_repo.bzl index 7a8c590b..d374614e 100644 --- a/oci/private/toolchains_repo.bzl +++ b/oci/private/toolchains_repo.bzl @@ -112,7 +112,7 @@ BUILD_HEADER_TMPL = """\ load(":defs.bzl", "resolved_toolchain") -resolved_toolchain(name = "resolved_toolchain", visibility = ["//visibility:public"]) +resolved_toolchain(name = "current_toolchain", visibility = ["//visibility:public"]) """ def _toolchains_repo_impl(repository_ctx): diff --git a/scripts/mirror_releases_cosign.sh b/scripts/mirror_releases_cosign.sh new file mode 100755 index 00000000..9e54a590 --- /dev/null +++ b/scripts/mirror_releases_cosign.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +set -o errexit -o nounset -o pipefail + +JQ_FILTER=\ +'map( + { + "key": .tag_name, + "value": .assets + | map(select((.name | contains("cosign-")) and (.name | contains(".") | not) and (.name | contains("key") | not) )) + | map({ + "key": .name, + "value": .browser_download_url + }) + | from_entries + } +) | from_entries' + +REPOSITORY=${1:-"sigstore/cosign"} + + +# We need v1.6.0 because of https://github.com/sigstore/cosign/pull/1616. remove once https://github.com/sigstore/cosign/pull/2288 lands +VERSIONS=$(curl --silent -H "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/$REPOSITORY/releases?per_page=1&page=12" | jq "$JQ_FILTER") + + +# Replace URLs with their hash +for TAG in $(jq -r 'keys | .[]' <<< $VERSIONS); do + CHECKSUMS="$(curl --silent -L https://github.com/$REPOSITORY/releases/download/$TAG/cosign_checksums.txt)" + >&2 echo -n "$TAG " + while read -r SHA256 FILENAME; do + INTEGRITY="sha256-$(echo $SHA256 | xxd -r -p | base64)" + VERSIONS=$(jq --arg tag "$TAG" --arg filename "$FILENAME" --arg sha256 "$INTEGRITY" 'if (.[$tag] | has($filename)) then .[$tag][$filename] = $sha256 else . end' <<< $VERSIONS) + >&2 echo -n "." + done <<< "$CHECKSUMS" + >&2 echo "" +done + +clear +echo -n "COSIGN_VERSIONS = " +jq 'with_entries(.value |= with_entries(.key |= ltrimstr("cosign-")))' <<< $VERSIONS + + +echo "" +echo "Copy the version info into cosign/private/versions.bzl" \ No newline at end of file