From 95e01f9da04c5088da70d11746d2745d2adb67b1 Mon Sep 17 00:00:00 2001 From: Hal Blackburn Date: Wed, 17 May 2023 06:28:50 +0000 Subject: [PATCH] feat: add cosign feature --- generation/cosign/generate_install.sh | 61 +++++++ src/cosign/devcontainer-feature.json | 26 +++ src/cosign/ensure_command.sh | 28 +++ src/cosign/install.sh | 246 ++++++++++++++++++++++++++ test/cosign/scenarios.json | 10 ++ test/cosign/test.sh | 9 + test/cosign/verify_self.sh | 22 +++ 7 files changed, 402 insertions(+) create mode 100755 generation/cosign/generate_install.sh create mode 100644 src/cosign/devcontainer-feature.json create mode 100644 src/cosign/ensure_command.sh create mode 100755 src/cosign/install.sh create mode 100644 test/cosign/scenarios.json create mode 100644 test/cosign/test.sh create mode 100644 test/cosign/verify_self.sh diff --git a/generation/cosign/generate_install.sh b/generation/cosign/generate_install.sh new file mode 100755 index 0000000..61e7bcd --- /dev/null +++ b/generation/cosign/generate_install.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -euo pipefail + +# This generates install.sh. +# Usage: +# $ ./generate_install.sh v3.0.4 > ../../src/cosign/install.sh + +version="${1:?Error: first argument must be a git revision}" +cosign_installer_url="https://raw.githubusercontent.com/sigstore/cosign-installer/${version:?}/action.yml" + +action_yaml=$(curl --no-progress-meter --fail "${cosign_installer_url:?}") || { + echo "Error: failed to download cosign-installer's Action YAML from ${cosign_installer_url:?}" >&2; + exit 1 +} +action_yaml_sha256=$(sha256sum - <<<"$action_yaml" | awk '{print $1}') +default_version=$(yq -er '.inputs["cosign-release"].default' - <<<"$action_yaml") + +# actions.yaml uses these placeholders: +# ${{ inputs.cosign-release }} +# ${{ inputs.install-dir }} +# ${{ inputs.use-sudo }} +# ${{ runner.arch }} +# ${{ runner.os }} + +# shellcheck disable=SC2016 +install_script=$(yq -er ' +.runs.steps[0].run +| gsub("\\${{ *inputs.cosign-release *}}"; "${COSIGN_VERSION:?}") +| gsub("\\${{ *inputs.install-dir *}}"; "${INSTALL_DIR:?}") +| gsub("\\${{ *inputs.use-sudo *}}"; "${USE_SUDO:?}") +| gsub("\\${{ *runner.arch *}}"; "${GHACTIONS_ARCH:?}") +| gsub("\\${{ *runner.os *}}"; "${GHACTIONS_OS:?}") +| (match("\\${{ *[^} ]+ *}}") + | error("Actions placeholder remained after replacing known placeholders: \(.string)") + ) // . +' - <<<"${action_yaml:?}") + +cat<<<"\ +$(head -n 1 <<<"${install_script:?}" `# shebang`) +# +# This install script was automatically extracted from the official +# cosign-installer GitHub Action: +# ${cosign_installer_url:?} +# sha256: ${action_yaml_sha256:?} +# +COSIGN_VERSION=\${VERSION:-${default_version:?}} +INSTALL_DIR=\${INSTALLDIR:?} +USE_SUDO=\${USESUDO:?} + +declare -A GHACTIONS_ARCH_NAMES=([aarch64]=ARM64 [x86_64]=X64) +GHACTIONS_ARCH=\${GHACTIONS_ARCH_NAMES[\$(uname -m)]:-} +if [[ \$GHACTIONS_ARCH == \"\" ]]; then + echo \"Error: unsupported CPU architecture: \$(uname -m)\" >&2 + exit 1 +fi +GHACTIONS_OS=\$(uname -s) + +. ensure_command.sh +ensure_command curl + +$(tail -n +2 <<<"${install_script:?}")" diff --git a/src/cosign/devcontainer-feature.json b/src/cosign/devcontainer-feature.json new file mode 100644 index 0000000..d25d289 --- /dev/null +++ b/src/cosign/devcontainer-feature.json @@ -0,0 +1,26 @@ +{ + "name": "Sigstore cosign", + "id": "cosign", + "version": "1.0.0", + "description": "Install Sigstore cosign ( https://www.sigstore.dev/ ), a signature verification & creation tool.", + "options": { + "version": { + "type": "string", + "proposals": ["v2.0.2"], + "default": "", + "description": "cosign release version to be installed" + }, + "installDir": { + "type": "string", + "proposals": ["/usr/local/bin"], + "default": "/usr/local/bin", + "description": "Where to install the cosign binary" + }, + "useSudo": { + "type": "boolean", + "default": false, + "description": "set to true if installDir location requires sudo to write" + } + }, + "installsAfter": ["ghcr.io/devcontainers/features/common-utils"] +} diff --git a/src/cosign/ensure_command.sh b/src/cosign/ensure_command.sh new file mode 100644 index 0000000..fd1bfc8 --- /dev/null +++ b/src/cosign/ensure_command.sh @@ -0,0 +1,28 @@ +ensure_apt_updated() { + if [ "${_apt_updated:-}" != "true" ]; then + apt-get update + _apt_updated=true + fi +} + +ensure_command() { + command=${1:?} + package=${2:-$command} + + if ! which "$command" >/dev/null; then + ensure_package "$package" + fi +} + +ensure_package() { + package=${1:?} + if which apt-get >/dev/null; then + ensure_apt_updated + apt-get -y install "$package" + elif which apk >/dev/null; then + apk add "${package}" + else + echo "Unable to install $package, no supported package manager found" >&2 + exit 1 + fi +} diff --git a/src/cosign/install.sh b/src/cosign/install.sh new file mode 100755 index 0000000..19aa617 --- /dev/null +++ b/src/cosign/install.sh @@ -0,0 +1,246 @@ +#!/bin/bash +# +# This install script was automatically extracted from the official +# cosign-installer GitHub Action: +# https://raw.githubusercontent.com/sigstore/cosign-installer/v3.0.4/action.yml +# sha256: bc1c34cefc456b4eb4b8e96992d83462984138ad418b5500006ac0e978274b9f +# +COSIGN_VERSION=${VERSION:-v2.0.2} +INSTALL_DIR=${INSTALLDIR:?} +USE_SUDO=${USESUDO:?} + +declare -A GHACTIONS_ARCH_NAMES=([aarch64]=ARM64 [x86_64]=X64) +GHACTIONS_ARCH=${GHACTIONS_ARCH_NAMES[$(uname -m)]:-} +if [[ $GHACTIONS_ARCH == "" ]]; then + echo "Error: unsupported CPU architecture: $(uname -m)" >&2 + exit 1 +fi +GHACTIONS_OS=$(uname -s) + +. ensure_command.sh +ensure_command curl + +# cosign install script +shopt -s expand_aliases +if [ -z "$NO_COLOR" ]; then + alias log_info="echo -e \"\033[1;32mINFO\033[0m:\"" + alias log_error="echo -e \"\033[1;31mERROR\033[0m:\"" +else + alias log_info="echo \"INFO:\"" + alias log_error="echo \"ERROR:\"" +fi +set -e + +mkdir -p ${INSTALL_DIR:?} + +if [[ ${COSIGN_VERSION:?} == "main" ]]; then + log_info "installing cosign via 'go install' from its main version" + GOBIN=$(go env GOPATH)/bin + go install github.com/sigstore/cosign/cmd/cosign@main + ln -s $GOBIN/cosign ${INSTALL_DIR:?}/cosign + exit 0 +fi + +shaprog() { + case ${GHACTIONS_OS:?} in + Linux) + sha256sum $1 | cut -d' ' -f1 + ;; + macOS) + shasum -a256 $1 | cut -d' ' -f1 + ;; + Windows) + powershell -command "(Get-FileHash $1 -Algorithm SHA256 | Select-Object -ExpandProperty Hash).ToLower()" + ;; + *) + log_error "unsupported OS ${GHACTIONS_OS:?}" + exit 1 + ;; + esac +} + +bootstrap_version='v2.0.2' +bootstrap_linux_amd64_sha='dc641173cbda29ba48580cdde3f80f7a734f3b558a25e5950a4b19f522678c70' +bootstrap_linux_arm_sha='686ef6160889e84e5710505345b5b55cef0873907d0ef5954c837d9d647cf169' +bootstrap_linux_arm64_sha='517e96f9d036c4b77db01132cacdbef21e4266e9ad3a93e67773c590ba54e26f' +bootstrap_darwin_amd64_sha='0f51cbe19a315b919e87042f0485331821722ecb7fce22cc1b880ed4833fc8b0' +bootstrap_darwin_arm64_sha='55242a52ebca43dfb133d0fe26e11546bfa4571addd6852d782c119d74deade1' +bootstrap_windows_amd64_sha='782fcc768fca4dea9eb7464032de4b3e602f8d605b71bae686762e7622faa9ca' +cosign_executable_name=cosign + +trap "popd >/dev/null" EXIT + +pushd ${INSTALL_DIR:?} > /dev/null + +case ${GHACTIONS_OS:?} in + Linux) + case ${GHACTIONS_ARCH:?} in + X64) + bootstrap_filename='cosign-linux-amd64' + bootstrap_sha=${bootstrap_linux_amd64_sha} + desired_cosign_filename='cosign-linux-amd64' + # v0.6.0 had different filename structures from all other releases + if [[ ${COSIGN_VERSION:?} == 'v0.6.0' ]]; then + desired_cosign_filename='cosign_linux_amd64' + desired_cosign_v060_signature='cosign_linux_amd64_0.6.0_linux_amd64.sig' + fi + ;; + + ARM) + bootstrap_filename='cosign-linux-arm' + bootstrap_sha=${bootstrap_linux_arm_sha} + desired_cosign_filename='cosign-linux-arm' + if [[ ${COSIGN_VERSION:?} == 'v0.6.0' ]]; then + log_error "linux-arm build not available at v0.6.0" + exit 1 + fi + ;; + + ARM64) + bootstrap_filename='cosign-linux-arm64' + bootstrap_sha=${bootstrap_linux_arm64_sha} + desired_cosign_filename='cosign-linux-arm64' + if [[ ${COSIGN_VERSION:?} == 'v0.6.0' ]]; then + log_error "linux-arm64 build not available at v0.6.0" + exit 1 + fi + ;; + + *) + log_error "unsupported architecture $arch" + exit 1 + ;; + esac + ;; + + macOS) + case ${GHACTIONS_ARCH:?} in + X64) + bootstrap_filename='cosign-darwin-amd64' + bootstrap_sha=${bootstrap_darwin_amd64_sha} + desired_cosign_filename='cosign-darwin-amd64' + # v0.6.0 had different filename structures from all other releases + if [[ ${COSIGN_VERSION:?} == 'v0.6.0' ]]; then + desired_cosign_filename='cosign_darwin_amd64' + desired_cosign_v060_signature='cosign_darwin_amd64_0.6.0_darwin_amd64.sig' + fi + ;; + + ARM64) + bootstrap_filename='cosign-darwin-arm64' + bootstrap_sha=${bootstrap_darwin_arm64_sha} + desired_cosign_filename='cosign-darwin-arm64' + # v0.6.0 had different filename structures from all other releases + if [[ ${COSIGN_VERSION:?} == 'v0.6.0' ]]; then + desired_cosign_filename='cosign_darwin_arm64' + desired_cosign_v060_signature='cosign_darwin_arm64_0.6.0_darwin_arm64.sig' + fi + ;; + + *) + log_error "unsupported architecture $arch" + exit 1 + ;; + esac + ;; + + Windows) + case ${GHACTIONS_ARCH:?} in + X64) + bootstrap_filename='cosign-windows-amd64.exe' + bootstrap_sha=${bootstrap_windows_amd64_sha} + desired_cosign_filename='cosign-windows-amd64.exe' + cosign_executable_name=cosign.exe + # v0.6.0 had different filename structures from all other releases + if [[ ${COSIGN_VERSION:?} == 'v0.6.0' ]]; then + desired_cosign_filename='cosign_windows_amd64.exe' + desired_cosign_v060_signature='cosign_windows_amd64_0.6.0_windows_amd64.exe.sig' + fi + ;; + *) + log_error "unsupported architecture $arch" + exit 1 + ;; + esac + ;; + *) + log_error "unsupported architecture $arch" + exit 1 + ;; +esac + +SUDO= +if [[ "${USE_SUDO:?}" == "true" ]] && command -v sudo >/dev/null; then + SUDO=sudo +fi + +expected_bootstrap_version_digest=${bootstrap_sha} +log_info "Downloading bootstrap version '${bootstrap_version}' of cosign to verify version to be installed...\n https://storage.googleapis.com/cosign-releases/${bootstrap_version}/${bootstrap_filename}" +$SUDO curl -sL https://storage.googleapis.com/cosign-releases/${bootstrap_version}/${bootstrap_filename} -o ${cosign_executable_name} +shaBootstrap=$(shaprog ${cosign_executable_name}); +if [[ $shaBootstrap != ${expected_bootstrap_version_digest} ]]; then + log_error "Unable to validate cosign version: '${COSIGN_VERSION:?}'" + exit 1 +fi +$SUDO chmod +x ${cosign_executable_name} + +# If the bootstrap and specified `cosign` releases are the same, we're done. +if [[ ${COSIGN_VERSION:?} == ${bootstrap_version} ]]; then + log_info "bootstrap version successfully verified and matches requested version so nothing else to do" + exit 0 +fi + +semver='^v([0-9]+\.){0,2}(\*|[0-9]+)(-?r?c?)(\.[0-9]+)$' +if [[ ${COSIGN_VERSION:?} =~ $semver ]]; then + log_info "Custom cosign version '${COSIGN_VERSION:?}' requested" +else + log_error "Unable to validate requested cosign version: '${COSIGN_VERSION:?}'" + exit 1 +fi + +# Download custom cosign +log_info "Downloading platform-specific version '${COSIGN_VERSION:?}' of cosign...\n https://storage.googleapis.com/cosign-releases/${COSIGN_VERSION:?}/${desired_cosign_filename}" +$SUDO curl -sL https://storage.googleapis.com/cosign-releases/${COSIGN_VERSION:?}/${desired_cosign_filename} -o cosign_${COSIGN_VERSION:?} +shaCustom=$(shaprog cosign_${COSIGN_VERSION:?}); + +# same hash means it is the same release +if [[ $shaCustom != $shaBootstrap ]]; then + if [[ ${COSIGN_VERSION:?} == 'v0.6.0' && ${GHACTIONS_OS:?} == 'Linux' ]]; then + # v0.6.0's linux release has a dependency on `libpcsclite1` + log_info "Installing libpcsclite1 package if necessary..." + set +e + sudo dpkg -s libpcsclite1 + if [ $? -eq 0 ]; then + log_info "libpcsclite1 package is already installed" + else + log_info "libpcsclite1 package is not installed, installing it now." + sudo apt-get update -q -q + sudo apt-get install -yq libpcsclite1 + fi + set -e + fi + + if [[ ${COSIGN_VERSION:?} == 'v0.6.0' ]]; then + log_info "Downloading detached signature for platform-specific '${COSIGN_VERSION:?}' of cosign...\n https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION:?}/${desired_cosign_v060_signature}" + $SUDO curl -sL https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION:?}/${desired_cosign_v060_signature} -o ${desired_cosign_filename}.sig + else + log_info "Downloading detached signature for platform-specific '${COSIGN_VERSION:?}' of cosign...\n https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION:?}/${desired_cosign_filename}.sig" + $SUDO curl -sLO https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION:?}/${desired_cosign_filename}.sig + fi + + if [[ ${COSIGN_VERSION:?} < 'v0.6.0' ]]; then + log_info "Downloading cosign public key '${COSIGN_VERSION:?}' of cosign...\n https://raw.githubusercontent.com/sigstore/cosign/${COSIGN_VERSION:?}/.github/workflows/cosign.pub" + RELEASE_COSIGN_PUB_KEY=https://raw.githubusercontent.com/sigstore/cosign/${COSIGN_VERSION:?}/.github/workflows/cosign.pub + else + log_info "Downloading cosign public key '${COSIGN_VERSION:?}' of cosign...\n https://raw.githubusercontent.com/sigstore/cosign/${COSIGN_VERSION:?}/release/release-cosign.pub" + RELEASE_COSIGN_PUB_KEY=https://raw.githubusercontent.com/sigstore/cosign/${COSIGN_VERSION:?}/release/release-cosign.pub + fi + + log_info "Using bootstrap cosign to verify signature of desired cosign version" + ./cosign verify-blob --insecure-ignore-tlog --key $RELEASE_COSIGN_PUB_KEY --signature ${desired_cosign_filename}.sig cosign_${COSIGN_VERSION:?} + + $SUDO rm cosign + $SUDO mv cosign_${COSIGN_VERSION:?} ${cosign_executable_name} + $SUDO chmod +x ${cosign_executable_name} + log_info "Installation complete!" +fi diff --git a/test/cosign/scenarios.json b/test/cosign/scenarios.json new file mode 100644 index 0000000..7c9edab --- /dev/null +++ b/test/cosign/scenarios.json @@ -0,0 +1,10 @@ +{ + "verify_self": { + "image": "ubuntu:focal", + "features": { + "cosign": { + "version": "v2.0.2" + } + } + } +} diff --git a/test/cosign/test.sh b/test/cosign/test.sh new file mode 100644 index 0000000..f55ea6f --- /dev/null +++ b/test/cosign/test.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e + +# https://github.com/devcontainers/cli/blob/main/docs/features/test.md +source dev-container-features-test-lib + +check "cosign command available" cosign version + +reportResults diff --git a/test/cosign/verify_self.sh b/test/cosign/verify_self.sh new file mode 100644 index 0000000..9c06635 --- /dev/null +++ b/test/cosign/verify_self.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e + +# https://github.com/devcontainers/cli/blob/main/docs/features/test.md +source dev-container-features-test-lib + +[[ $(uname -m) == "aarch64" ]] && arch=arm64 || arch=amd64 + +curl -o cosign-release.sig --fail -L https://github.com/sigstore/cosign/releases/download/v2.0.2/cosign-linux-${arch:?}-keyless.sig +base64 -d cosign-release.sig > cosign-release.sig.decoded + +curl -o cosign-release.pem --fail -L https://github.com/sigstore/cosign/releases/download/v2.0.2/cosign-linux-${arch:?}-keyless.pem +base64 -d cosign-release.pem > cosign-release.pem.decoded + +check "cosign verifies itself" \ + cosign verify-blob "$(command -v cosign)" \ + --certificate cosign-release.pem.decoded \ + --signature cosign-release.sig.decoded \ + --certificate-identity keyless@projectsigstore.iam.gserviceaccount.com \ + --certificate-oidc-issuer https://accounts.google.com + +reportResults