diff --git a/.github/actions/generate-slsa-predicate/action.yml b/.github/actions/generate-slsa-predicate/action.yml new file mode 100644 index 00000000..750fc667 --- /dev/null +++ b/.github/actions/generate-slsa-predicate/action.yml @@ -0,0 +1,54 @@ +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: 'Generate SLSA Predicate' +description: 'Generate a SLSA Build Provenance v1 predicate JSON for cosign attest-blob' + +inputs: + workflow_file: + description: 'Workflow filename for builder ID (e.g., on-tag.yaml)' + required: true + +runs: + using: 'composite' + steps: + - name: Generate SLSA predicate + shell: bash + run: | + set -euo pipefail + PREDICATE="${RUNNER_TEMP}/slsa-predicate.json" + cat > "$PREDICATE" <<-EOF + { + "buildDefinition": { + "buildType": "https://aicr.nvidia.com/binary/v1", + "externalParameters": { + "repository": "${{ github.repository }}", + "ref": "${{ github.ref }}" + }, + "resolvedDependencies": [{ + "uri": "git+https://github.com/${{ github.repository }}@${{ github.ref }}", + "digest": { "gitCommit": "${{ github.sha }}" } + }] + }, + "runDetails": { + "builder": { + "id": "https://github.com/${{ github.repository }}/.github/workflows/${{ inputs.workflow_file }}" + }, + "metadata": { + "invocationId": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + } + } + } + EOF + echo "SLSA_PREDICATE=${PREDICATE}" >> "$GITHUB_ENV" diff --git a/.github/workflows/build-attested.yaml b/.github/workflows/build-attested.yaml new file mode 100644 index 00000000..e0e2df94 --- /dev/null +++ b/.github/workflows/build-attested.yaml @@ -0,0 +1,92 @@ +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Build attested binaries on-demand without cutting a release tag. +# Produces tar.gz archives with SLSA Build Provenance v1 attestation +# as downloadable job artifacts. + +## NOTE: THIS WORKFLOW IS FOR TESTING PURPOSES ONLY. +## if you need something attested by ci. This does not run tests or security scans. +## The only complete/valid way to attest that passes all validation is via on-tag.yaml. +## Validate attestations requires to pass the following certificate identity regexp: +## --certificate-identity-regexp 'https://github.com/NVIDIA/aicr/.github/workflows/on-tag\.yaml@refs/tags/.*' +## so this attestation is not the same as a production release. + +name: Build Attested Binaries + +on: + workflow_dispatch: {} + +permissions: + contents: read + id-token: write + +jobs: + build-and-attest: + needs: [tests, security-scan] + name: Build and Attest Binaries + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout Code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Setup Go + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 + with: + go-version-file: go.mod + cache: true + + - name: Install Cosign + uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 + + - name: Install GoReleaser + uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 + with: + install-only: true + + - name: Generate SLSA predicate + uses: ./.github/actions/generate-slsa-predicate + with: + workflow_file: build-attested.yaml + + - name: Build and attest + env: + GOFLAGS: -mod=vendor + run: | + set -euo pipefail + goreleaser release --snapshot --clean --skip=publish,ko,sbom --timeout 10m + + - name: Verify archive contents + run: | + set -euo pipefail + echo "=== Archives ===" + ls -la dist/aicr_v*.tar.gz 2>/dev/null || echo "No archives found" + echo "" + for archive in dist/aicr_v*.tar.gz; do + [ -f "$archive" ] || continue + echo "--- $(basename "$archive") ---" + tar -tzf "$archive" + echo "" + done + + - name: Upload archives + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: aicr-attested-binaries + path: dist/*.tar.gz + retention-days: 3 diff --git a/.github/workflows/on-tag.yaml b/.github/workflows/on-tag.yaml index 56e5d31c..e81a3af5 100644 --- a/.github/workflows/on-tag.yaml +++ b/.github/workflows/on-tag.yaml @@ -95,6 +95,14 @@ jobs: go-version: ${{ steps.versions.outputs.go }} cache: true + - name: Install Cosign + uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 + + - name: Generate SLSA predicate + uses: ./.github/actions/generate-slsa-predicate + with: + workflow_file: on-tag.yaml + - name: Build and Release id: release uses: ./.github/actions/go-build-release diff --git a/.goreleaser.yaml b/.goreleaser.yaml index b9d481b2..318f2fd8 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -33,6 +33,18 @@ builds: -X github.com/NVIDIA/aicr/pkg/cli.version={{.Version}} -X github.com/NVIDIA/aicr/pkg/cli.commit={{.ShortCommit}} -X github.com/NVIDIA/aicr/pkg/cli.date={{.Date}} + hooks: + post: + ## cosign v1 attestation with slsa provenance v1. + ## NOTE: below aicrd currently attests via github attestation + - cmd: >- + bash -c '[ -z "${SLSA_PREDICATE:-}" ] && exit 0; + cosign attest-blob + --predicate "${SLSA_PREDICATE}" + --type https://slsa.dev/provenance/v1 + --bundle "$(dirname "{{ .Path }}")/aicr-attestation.sigstore.json" + --yes "{{ .Path }}"' + output: true goos: - darwin - linux @@ -122,8 +134,11 @@ archives: ids: - aicr formats: - - binary - name_template: "{{ .Binary }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}" + - tar.gz + name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" + files: + - src: "dist/aicr_{{ .Os }}_{{ .Arch }}*/aicr-attestation.sigstore.json" + strip_parent: true changelog: sort: asc diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index e5b044b5..7c357401 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -549,6 +549,17 @@ See [kwok/README.md](kwok/README.md) for adding recipes, profiles, and troublesh | `make bump-minor` | Bump minor version (1.2.3 → 1.3.0) | | `make bump-patch` | Bump patch version (1.2.3 → 1.2.4) | +### Binary Attestation + +Release binaries are attested with SLSA Build Provenance v1 via a GoReleaser build +hook that calls `cosign attest-blob`. The hook is guarded by the `$SLSA_PREDICATE` +environment variable — it only runs when a workflow explicitly generates the predicate. +Local `make build` is unaffected. + +To produce attested binaries without a release tag, use the **Build Attested Binaries** +workflow (`.github/workflows/build-attested.yaml`) from the Actions tab. It runs +`goreleaser release --snapshot` with cosign and uploads tar.gz archives as artifacts. + ### Local Development | Target | Description | diff --git a/SECURITY.md b/SECURITY.md index 98955691..d3ea9a0a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -139,6 +139,36 @@ For more information: - [SPDX Specification](https://spdx.dev/) - [Sigstore Cosign](https://docs.sigstore.dev/cosign/overview/) +### CLI Binary Attestation + +CLI binary releases are attested with SLSA Build Provenance v1 using Cosign keyless +signing via GitHub Actions OIDC. Each release archive (`.tar.gz`) contains: + +- `aicr` — the binary +- `aicr-attestation.sigstore.json` — SLSA Build Provenance v1 attestation (Sigstore bundle format) + +The attestation cryptographically proves which repository, commit, and workflow produced +the binary. It is logged to the public [Rekor](https://rekor.sigstore.dev/) transparency +log and can be verified offline. + +**Verify a binary attestation:** + +```shell +cosign verify-blob-attestation \ + --bundle aicr-attestation.sigstore.json \ + --type https://slsa.dev/provenance/v1 \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --certificate-identity-regexp 'https://github.com/NVIDIA/aicr/.github/workflows/on-tag\.yaml@refs/tags/.*' \ + aicr +``` + +The install script (`./install`) performs this verification automatically when +[Cosign](https://docs.sigstore.dev/cosign/system_config/installation/) is available. + +**On-demand attested builds:** The `Build Attested Binaries` workflow +(`.github/workflows/build-attested.yaml`) can be triggered manually from the +Actions tab to produce attested binaries from any branch without cutting a release. + ### Setup Export variables for the image you want to verify: diff --git a/install b/install index 14e5eb88..d72794bc 100755 --- a/install +++ b/install @@ -54,8 +54,11 @@ has_tools() { } normalize_arch() { - local arch="$1" - [[ $arch == "x86_64" ]] && echo "amd64" || echo "$arch" + case "$1" in + x86_64) echo "amd64" ;; + aarch64) echo "arm64" ;; + *) echo "$1" ;; + esac } get_os() { @@ -70,6 +73,10 @@ get_binary_name() { echo "${BIN_NAME}_${1}_${2}_${3}" # version, os, arch } +get_archive_name() { + echo "${BIN_NAME}_${1}_${2}_${3}.tar.gz" # version, os, arch +} + # ============================================================================== # GitHub API Functions # ============================================================================== @@ -192,44 +199,70 @@ main() { has_tools "${REQUIRED_TOOLS[@]}" # Fetch release information - local release_json + local release_json archive_name release_json=$(fetch_latest_release) version=$(extract_version "$release_json") - binary_name=$(get_binary_name "$version" "$os" "$arch") - + archive_name=$(get_archive_name "$version" "$os" "$arch") + msg "Platform: $os/$arch" msg "Version: $version" - - # Download and verify binary + + # Download archive and checksums temp_dir=$(mktemp -d) trap "rm -rf $temp_dir" EXIT - # Extract asset URLs from release JSON (handles both public and private repos) - local binary_url checksum_url - binary_url=$(extract_asset_url "$release_json" "$binary_name") + local archive_url checksum_url + archive_url=$(extract_asset_url "$release_json" "$archive_name") checksum_url=$(extract_asset_url "$release_json" "$CHECKSUMS_FILE") - download_release_asset "$binary_url" "${temp_dir}/${binary_name}" "$binary_name" + download_release_asset "$archive_url" "${temp_dir}/${archive_name}" "$archive_name" download_release_asset "$checksum_url" "${temp_dir}/checksums.txt" "checksums" - - # Verify checksum + + # Verify archive checksum msg "Verifying checksum..." - (cd "$temp_dir" && grep "$binary_name" checksums.txt | shasum -a 256 --check --strict) \ + (cd "$temp_dir" && grep "$archive_name" checksums.txt | shasum -a 256 --check --strict) \ || err "Checksum verification failed" - - # Install binary - chmod +x "${temp_dir}/${binary_name}" + + # Extract archive + msg "Extracting archive..." + tar -xzf "${temp_dir}/${archive_name}" -C "$temp_dir" + + # Optional: verify attestation if cosign is available + if command -v cosign &>/dev/null && [[ -f "${temp_dir}/${BIN_NAME}-attestation.sigstore.json" ]]; then + msg "Verifying attestation with cosign..." + if cosign verify-blob-attestation \ + --bundle "${temp_dir}/${BIN_NAME}-attestation.sigstore.json" \ + --type https://slsa.dev/provenance/v1 \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --certificate-identity-regexp 'https://github.com/NVIDIA/aicr/.github/workflows/on-tag\.yaml@refs/tags/.*' \ + "${temp_dir}/${BIN_NAME}" 2>/dev/null; then + msg "Attestation verified: binary built by github.com/NVIDIA/aicr" + else + msg "Warning: attestation verification failed — cannot confirm this binary was built by the official CI pipeline" + fi + elif [[ -f "${temp_dir}/${BIN_NAME}-attestation.sigstore.json" ]]; then + msg "Tip: install cosign to verify binary attestation (https://docs.sigstore.dev/cosign/system_config/installation/)" + fi + + # Install binary and attestation + chmod +x "${temp_dir}/${BIN_NAME}" msg "Installing $BIN_NAME to $INSTALL_DIR" mkdir -p "$INSTALL_DIR" if [[ -w "$INSTALL_DIR" ]]; then - mv "${temp_dir}/${binary_name}" "${INSTALL_DIR}/${BIN_NAME}" + mv "${temp_dir}/${BIN_NAME}" "${INSTALL_DIR}/${BIN_NAME}" + [[ -f "${temp_dir}/${BIN_NAME}-attestation.sigstore.json" ]] && \ + mv "${temp_dir}/${BIN_NAME}-attestation.sigstore.json" "${INSTALL_DIR}/${BIN_NAME}-attestation.sigstore.json" else - sudo mv "${temp_dir}/${binary_name}" "${INSTALL_DIR}/${BIN_NAME}" + sudo mv "${temp_dir}/${BIN_NAME}" "${INSTALL_DIR}/${BIN_NAME}" + [[ -f "${temp_dir}/${BIN_NAME}-attestation.sigstore.json" ]] && \ + sudo mv "${temp_dir}/${BIN_NAME}-attestation.sigstore.json" "${INSTALL_DIR}/${BIN_NAME}-attestation.sigstore.json" fi - + # Verify installation msg "$BIN_NAME installed successfully!" "${BIN_NAME}" --version + [[ -f "${INSTALL_DIR}/${BIN_NAME}-attestation.sigstore.json" ]] && \ + msg "Attestation: ${INSTALL_DIR}/${BIN_NAME}-attestation.sigstore.json" } # Run main function