diff --git a/cosign.pub b/cosign.pub new file mode 100644 index 0000000..1007711 --- /dev/null +++ b/cosign.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyWx2eajLfc+pflz3cq2XcmopUoO9 +9oGHYfmtd+zSod22RkU4YJtIcEBesql7Wb+wjkqBxjpXgdrTB9Tu3dPVZQ== +-----END PUBLIC KEY----- diff --git a/scripts/ci/attest-release-assets.sh b/scripts/ci/attest-release-assets.sh new file mode 100755 index 0000000..3a75947 --- /dev/null +++ b/scripts/ci/attest-release-assets.sh @@ -0,0 +1,36 @@ +#!/bin/sh +# Create SLSA v1 cosign bundle attestations next to each release binary under DIR. +# Requires: cosign on PATH; COSIGN_KEY_PATH to cosign private key PEM; COSIGN_PASSWORD +# if the key is encrypted. Run from repository root so scripts/ci/slsa-predicate.py resolves. +# +# Usage: attest-release-assets.sh +set -eu + +DIR="${1:?directory}" +KEY="${COSIGN_KEY_PATH:?set COSIGN_KEY_PATH}" + +if [ ! -f "$KEY" ]; then + echo "attest-release-assets.sh: missing key file $KEY" >&2 + exit 1 +fi + +PRED="$(mktemp "${TMPDIR:-/tmp}/slsa-pred.XXXXXX")" +trap 'rm -f "$PRED"' EXIT INT + +python3 scripts/ci/slsa-predicate.py > "$PRED" + +find "$DIR" -type f ! -name '*.sha256' ! -name '*.cosign.bundle' | while IFS= read -r f; do + case "$f" in + */.git/*) continue ;; + esac + echo "attest: $f" + cosign attest-blob --yes \ + --key "$KEY" \ + --predicate "$PRED" \ + --type slsaprovenance1 \ + --bundle "${f}.cosign.bundle" \ + --tlog-upload=true \ + "$f" >/dev/null +done + +echo "attest-release-assets.sh: done" diff --git a/scripts/ci/setup-cosign.sh b/scripts/ci/setup-cosign.sh new file mode 100755 index 0000000..9966c6f --- /dev/null +++ b/scripts/ci/setup-cosign.sh @@ -0,0 +1,32 @@ +#!/bin/sh +# Install cosign from GitHub releases with SHA256 verification. +# Usage: setup-cosign.sh [version] +set -eu + +COSIGN_VERSION="${1:-3.0.5}" + +ARCH="$(uname -m)" +case "$ARCH" in + x86_64) ARCH="amd64" ;; + aarch64) ARCH="arm64" ;; + *) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; +esac + +BASE_URL="https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}" +BINARY="cosign-linux-${ARCH}" +CHECKSUMS_URL="${BASE_URL}/cosign_checksums.txt" + +curl -fsSL "$CHECKSUMS_URL" -o /tmp/cosign-checksums.txt +curl -fsSL "${BASE_URL}/${BINARY}" -o /tmp/cosign + +EXPECTED="$(grep " ${BINARY}\$" /tmp/cosign-checksums.txt | awk '{print $1}')" +ACTUAL="$(sha256sum /tmp/cosign | awk '{print $1}')" +if [ -z "$EXPECTED" ] || [ "$EXPECTED" != "$ACTUAL" ]; then + echo "SHA256 verification failed for ${BINARY}" >&2 + rm -f /tmp/cosign /tmp/cosign-checksums.txt + exit 1 +fi + +sudo install -m 0755 /tmp/cosign /usr/local/bin/cosign +rm -f /tmp/cosign /tmp/cosign-checksums.txt +cosign version diff --git a/scripts/ci/slsa-predicate.py b/scripts/ci/slsa-predicate.py new file mode 100644 index 0000000..9ae3e51 --- /dev/null +++ b/scripts/ci/slsa-predicate.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""Emit a SLSA v1 provenance predicate JSON on stdout (stdin unused).""" +from __future__ import annotations + +import json +import os +from datetime import datetime, timezone + + +def _source_uri() -> str: + server = (os.environ.get("GITHUB_SERVER_URL") or os.environ.get("GITEA_SERVER_URL") or "").rstrip("/") + repo = os.environ.get("GITHUB_REPOSITORY") or os.environ.get("GITEA_REPOSITORY") or "" + if not server or not repo: + return "" + if server.startswith("https://") or server.startswith("http://"): + return f"git+{server}/{repo}.git" + return f"git+https://{server}/{repo}.git" + + +def _build_type() -> str: + custom = os.environ.get("PROVENANCE_BUILD_TYPE") + if custom: + return custom + server = (os.environ.get("GITHUB_SERVER_URL") or os.environ.get("GITEA_SERVER_URL") or "").rstrip("/") + repo = os.environ.get("GITHUB_REPOSITORY") or os.environ.get("GITEA_REPOSITORY") or "" + if server and repo: + return f"{server}/{repo}/.gitea/workflows/build.yml" + return "https://slsa.dev/provenance/v1" + + +def _builder_id() -> str: + custom = os.environ.get("PROVENANCE_BUILDER_ID") + if custom: + return custom + server = (os.environ.get("GITHUB_SERVER_URL") or os.environ.get("GITEA_SERVER_URL") or "").rstrip("/") + if server: + return f"{server}/actions" + return "https://gitea.io/actions/runner" + + +def main() -> None: + ref = os.environ.get("GITHUB_REF", "") + sha = os.environ.get("GITHUB_SHA", "") + run_id = os.environ.get("GITHUB_RUN_ID", "") + attempt = os.environ.get("GITHUB_RUN_ATTEMPT", "1") + workflow = os.environ.get("GITHUB_WORKFLOW", "") + started = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + internal = {} + if workflow: + internal["workflow"] = workflow + + predicate = { + "buildDefinition": { + "buildType": _build_type(), + "externalParameters": { + "source": _source_uri(), + "ref": ref, + "revision": sha, + }, + "internalParameters": internal, + "resolvedDependencies": [], + }, + "runDetails": { + "builder": {"id": _builder_id()}, + "metadata": { + "invocationId": f"{run_id}-{attempt}", + "startedOn": started, + }, + }, + } + print(json.dumps(predicate, separators=(",", ":"))) + + +if __name__ == "__main__": + main() diff --git a/scripts/ci/verify-release-attestation.sh b/scripts/ci/verify-release-attestation.sh new file mode 100755 index 0000000..d7f9388 --- /dev/null +++ b/scripts/ci/verify-release-attestation.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# Verify a cosign SLSA bundle for a release binary using the repository public key. +# Checks Sigstore Rekor (public log) unless COSIGN_REKOR_URL points elsewhere. +# Usage: verify-release-attestation.sh +# Env: COSIGN_PUBLIC_KEY (default cosign.pub) +set -eu + +BLOB="${1:?blob path}" +BUNDLE="${2:?bundle path}" +PUB="${COSIGN_PUBLIC_KEY:-cosign.pub}" + +if [ ! -f "$PUB" ]; then + echo "Missing $PUB (generate a key pair with cosign and commit the .pub file)" >&2 + exit 1 +fi + +exec cosign verify-blob-attestation \ + --key "$PUB" \ + --bundle "$BUNDLE" \ + --type slsaprovenance1 \ + "$BLOB"