diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aee56e3d..a77ba209 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,7 +55,105 @@ jobs: env: GORELEASER_CURRENT_TAG: ${{ github.ref_name }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - + + - name: Set up uv for nvsec + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + version: "0.11.31" + + - name: Install nvsec + run: | + uv python install 3.13 + uv venv --python 3.13 --seed "$RUNNER_TEMP/nvsec-venv" + "$RUNNER_TEMP/nvsec-venv/bin/pip" install "nvsec==2.9.2" \ + --index-url "https://gitlab-master.nvidia.com/api/v4/projects/25923/packages/pypi/simple" \ + --extra-index-url "https://urm.nvidia.com/artifactory/api/pypi/sw-cftt-pypi-local/simple" + echo "$RUNNER_TEMP/nvsec-venv/bin" >> "$GITHUB_PATH" + + - name: Sign and verify DEB and RPM packages + run: | + scripts/sign-linux-packages.sh \ + dist \ + deployments/packages/keys/fleet-intelligence.pub.asc + env: + NVSEC_SSA_CLIENT_ID: ${{ secrets.NVSEC_SSA_CLIENT_ID }} + NVSEC_SSA_CLIENT_SECRET: ${{ secrets.NVSEC_SSA_CLIENT_SECRET }} + NVSEC_LINUX_PACKAGE_SSA_SCOPE: ${{ vars.NVSEC_LINUX_PACKAGE_SSA_SCOPE || 'SIGNING_LINUX_PACKAGE_FLEET_INTELLIGENCE' }} + NVSEC_LINUX_PACKAGE_JOB_TYPE: ${{ vars.NVSEC_LINUX_PACKAGE_JOB_TYPE || 'LINUX_PACKAGE_FLEET_INTELLIGENCE' }} + NSPECT_ID: ${{ vars.NSPECT_ID }} + + - name: Refresh checksums for signed release artifacts + run: | + scripts/refresh-checksums.sh \ + dist/checksums.txt \ + deployments/packages/keys/fleet-intelligence.pub.asc \ + scripts/verify-linux-package-signature.sh + + - name: Sign and verify checksum manifest + run: | + scripts/sign-checksum-manifest.sh \ + dist/checksums.txt \ + deployments/packages/keys/fleet-intelligence.pub.asc + env: + NVSEC_SSA_CLIENT_ID: ${{ secrets.NVSEC_SSA_CLIENT_ID }} + NVSEC_SSA_CLIENT_SECRET: ${{ secrets.NVSEC_SSA_CLIENT_SECRET }} + NVSEC_CHECKSUM_SSA_SCOPE: ${{ vars.NVSEC_CHECKSUM_SSA_SCOPE || 'SIGNING_FLEET_INTELLIGENCE' }} + NVSEC_CHECKSUM_JOB_TYPE: ${{ vars.NVSEC_CHECKSUM_JOB_TYPE || 'FLEET_INTELLIGENCE' }} + NSPECT_ID: ${{ vars.NSPECT_ID }} + + - name: Replace signed assets in draft release + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9.0.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require("fs"); + const path = require("path"); + const tag = context.ref.replace("refs/tags/", ""); + const releases = await github.paginate(github.rest.repos.listReleases, { + owner: context.repo.owner, + repo: context.repo.repo, + per_page: 100, + }); + const release = releases.find((candidate) => candidate.tag_name === tag); + if (!release) { + throw new Error(`Draft release not found for tag ${tag}`); + } + if (!release.draft) { + throw new Error("Refusing to replace assets in a published release"); + } + + const files = fs.readdirSync("dist") + .filter((name) => + name.endsWith(".deb") || + name.endsWith(".rpm") || + name === "checksums.txt.asc" || + name === "checksums.txt" + ) + .map((name) => path.join("dist", name)); + files.push( + "deployments/packages/keys/fleet-intelligence.pub.asc", + "scripts/verify-linux-package-signature.sh", + ); + + const existing = new Map(release.assets.map((asset) => [asset.name, asset.id])); + for (const file of files) { + const name = path.basename(file); + const existingID = existing.get(name); + if (existingID) { + await github.rest.repos.deleteReleaseAsset({ + owner: context.repo.owner, + repo: context.repo.repo, + asset_id: existingID, + }); + } + await github.rest.repos.uploadReleaseAsset({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: release.id, + name, + data: fs.readFileSync(file), + }); + } - name: Get version and commit info id: version run: | @@ -170,7 +268,7 @@ jobs: "sw-dgxc-gpuhealth-generic-local/KITMAKER/release/fleetint/linux-x86_64/${{ steps.version.outputs.version }}/" \ --flat=true \ --target-props "component_name=fleetint;branch=${{ steps.version.outputs.branch }};os=linux;arch=x86_64;platform=linux-x86_64;version=${{ steps.version.outputs.version }};changelist=${{ steps.version.outputs.commit }};source=${{ steps.version.outputs.source }};category=toolkit" - + - name: Upload sbsa tarball to Artifactory if: steps.version.outputs.is_prerelease == 'false' run: | @@ -179,7 +277,32 @@ jobs: "sw-dgxc-gpuhealth-generic-local/KITMAKER/release/fleetint/linux-sbsa/${{ steps.version.outputs.version }}/" \ --flat=true \ --target-props "component_name=fleetint;branch=${{ steps.version.outputs.branch }};os=linux;arch=sbsa;platform=linux-sbsa;version=${{ steps.version.outputs.version }};changelist=${{ steps.version.outputs.commit }};source=${{ steps.version.outputs.source }};category=toolkit" - + + - name: Publish verified GitHub release + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9.0.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const tag = context.ref.replace("refs/tags/", ""); + const releases = await github.paginate(github.rest.repos.listReleases, { + owner: context.repo.owner, + repo: context.repo.repo, + per_page: 100, + }); + const release = releases.find((candidate) => candidate.tag_name === tag); + if (!release) { + throw new Error(`Draft release not found for tag ${tag}`); + } + if (!release.draft) { + throw new Error("Release is already published"); + } + await github.rest.repos.updateRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: release.id, + draft: false, + }); + - name: Summary run: | echo "### Release Build Complete" >> $GITHUB_STEP_SUMMARY @@ -195,6 +318,8 @@ jobs: echo "repo: oci://ghcr.io/${{ steps.agent_image_ghcr.outputs.owner_lc }}/charts" >> $GITHUB_STEP_SUMMARY echo "chart: fleet-intelligence-agent:${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY + echo "**Linux packages and checksum manifest:** verified with Fleet Intelligence key \`087199B3\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY if [[ "${{ steps.version.outputs.is_prerelease }}" == "true" ]]; then echo "GitHub release created (pre-release)" >> $GITHUB_STEP_SUMMARY diff --git a/.goreleaser.yaml b/.goreleaser.yaml index b2d8b8de..e4b58b03 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -151,6 +151,10 @@ release: github: owner: NVIDIA name: fleet-intelligence-agent + # Keep unsigned GoReleaser output private until the release workflow replaces + # DEB/RPM assets with verified 3S-signed versions, refreshes checksums, and + # adds a verified detached signature for the final checksum manifest. + draft: true header: | ## NVIDIA Fleet Intelligence Agent v{{.Version}} @@ -173,11 +177,19 @@ release: ```bash sha256sum -c checksums.txt ``` + + Confirm that `fleet-intelligence.pub.asc` has fingerprint + `FE0C 8B74 CA66 357C 13BE 197D CCE3 C963 0871 99B3`, then authenticate + the checksum manifest: + ```bash + gpg --show-keys --fingerprint fleet-intelligence.pub.asc + gpg --import fleet-intelligence.pub.asc + gpg --verify checksums.txt.asc checksums.txt + ``` ### Support For issues and questions, please visit: https://github.com/NVIDIA/fleet-intelligence-agent/issues - draft: false prerelease: auto # Auto-detect pre-release based on version (e.g., v1.0.0-beta) mode: replace # Replace existing release if it exists replace_existing_artifacts: true # Handle reruns where GitHub assets with same name already exist diff --git a/deployments/packages/keys/fleet-intelligence.pub.asc b/deployments/packages/keys/fleet-intelligence.pub.asc new file mode 100644 index 00000000..3099aceb --- /dev/null +++ b/deployments/packages/keys/fleet-intelligence.pub.asc @@ -0,0 +1,29 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGpjlKgBEADpuUQEvYXpO4hC8noK3fHFllG/6D8bzlgwGhApux+/oPJrc1wn +Whcikthv91gRGI9SORGzQiIzhH9IGZNYUHwD3vituPD+TeG6zli6HV3AlB1b7iRx +xtYwaZb7IIyiduzKStxX/vjdRuB3zMSVbIwIIsH8Vm1UEqyBemJ/CMSQZtKU5q33 +mKlWCKs1q/N2ebY9aoHiFTGRXDBaSDm5prHBeydH1J9O2I+LR2Yt4gEv3mYGFSnL +uAQO71rDcyxf4AHNp/ajRjzaBqVnOfYnJrEMFmcgOfMbQ+0+mjH8ZuzZav2Au+p4 +v8dbIycfsVrLmvoeeX5ognllc8yKZk0NbWdzf303DC1COPpDAJIMCPHPgjFMQI1w +2lInQgzCu+0NuvSsi6DZS58JfM06CTIBlkyAFYajBLERdsc5wsqwFFJA6sft+aiR +Ot3H5/5Ha9b1rds90mikLYoRkL7lh2Da0mP6dWsl2ojuWkol7MJ4SvE8ahLrxCxN +rNENYGnwusbNV5HSVVyBKjRDhW01rs/Dra6V7VnZjFtY/TW5XkRPCtAawGO5EgGE +O8xDHUXZ8zGUc9LkHtoPUKj0AXOvKwyUc40Hd09CRQUUZ6cL/H5m70VHiJ0zP0xg +vm2XTtAnib2zDxgp0SIBhUy/Li/tj07BNe4sNi03RF0PTCHC8r6wo7s4/wARAQAB +tDhOVklESUEgRmxlZXQgSW50ZWxsaWdlbmNlIDxmbGVldGludEBleGNoYW5nZS5u +dmlkaWEuY29tPokCRgQTAQgAMBYhBP4Mi3TKZjV8E74ZfczjyWMIcZmzBQJqY5So +AhsvAgsJAxUKCAIWAgIeBQIXgAAKCRDM48ljCHGZs1xtEADky/CSLyEmXwyf6RH8 +FdeNdfV6fbgOx7xfbL8s7NpdT09O/aryWpZOczl9MWQfO33PE7qa5AEpOqg4Iph3 +yfcJA2OVuQ+mEKtagn9s0B0CAKbj4rP3YQs5mQvy1vGMba3+5tzJfdVuMZaqK3QK +6AdHk5zUH+Ptly28gHXH+G+XYvP0e0xlDzImVgcTA5M9WQF8AMO6BME1WLDD9Ewt +hvNxWKLAnzpEg582KOegV4+Vg+h8hV192sEpY9Y7rsGlSAzr/hoxDQP7F45LZfba +xqZqOzGdS5xr8PvsfZEWW7ibT7VaGQizAEHgE1xYgEyPD9RaB6rOGz2ewltRRAwH +vcMXJgwABUudoCjmFkDoMMwkXcYGr31y0O1KHh8gbmc6FmGY/0Sb3Wh7c1T4JxZy +APZGjIsU33vhLx6BpJOaV7iFLIQ5xgB6lQCix5ps5QeO31QYn/eTlSkZW6j6uXPG +Jj6MUGjq0hnlWrDCA5J2XjZjQ38Pa+dI+Hjat0bWR+nTTnUUo3Ybrn43rpOAM5jN +lmJHZ8R0q5KV7tVh6zQBMSJ1cFBPDJoF9eUvEM8uiE1FlvtajkWrcnwuVpVSCHod +twcRgdpoq9Y/rdgaAuhd/cLCypBumZSoJtYhwX/GP0LeT6Wk4/6awACoMPLahrsa +8N/pBfOGFmtZpcUq0GLV0j+ipw== +=uZuZ +-----END PGP PUBLIC KEY BLOCK----- diff --git a/docs/install-deb.md b/docs/install-deb.md index 6ea3e246..1bc8ba4b 100644 --- a/docs/install-deb.md +++ b/docs/install-deb.md @@ -46,7 +46,9 @@ After adding the CUDA repository, package dependencies (`datacenter-gpu-manager- ## Install package -Download the package from [Latest stable release](https://github.com/NVIDIA/fleet-intelligence-agent/releases/latest), then install: +Download the package from the +[latest stable release](https://github.com/NVIDIA/fleet-intelligence-agent/releases/latest), +then install it: ```bash # Ubuntu (x86_64) @@ -63,6 +65,32 @@ sudo fleetint --version systemctl status fleetintd ``` +## Optional: Verify package authenticity + +To verify a release before installation, download the package, +`fleet-intelligence.pub.asc`, `checksums.txt`, `checksums.txt.asc`, and +`verify-linux-package-signature.sh` from the same release. + +Confirm that the public key fingerprint is: + +```text +FE0C 8B74 CA66 357C 13BE 197D CCE3 C963 0871 99B3 +``` + +Then verify the checksum manifest, package digest, and embedded package +signature: + +```bash +gpg --show-keys --fingerprint fleet-intelligence.pub.asc +gpg --import fleet-intelligence.pub.asc +gpg --verify checksums.txt.asc checksums.txt +sha256sum -c --ignore-missing checksums.txt +chmod 755 verify-linux-package-signature.sh +./verify-linux-package-signature.sh \ + fleetint_VERSION_amd64.deb \ + fleet-intelligence.pub.asc +``` + ## Update Install the newer package version: @@ -87,4 +115,4 @@ sudo apt purge fleetint # Also removes configuration files References: - DCGM: - Fabric Manager: -- NVAT (`nvattest`/`corelib`): \ No newline at end of file +- NVAT (`nvattest`/`corelib`): diff --git a/docs/install-rpm.md b/docs/install-rpm.md index ce32f4ff..b612f733 100644 --- a/docs/install-rpm.md +++ b/docs/install-rpm.md @@ -71,7 +71,9 @@ After adding the CUDA repository, package dependencies (`datacenter-gpu-manager- ## Install package -Download the package from [Latest stable release](https://github.com/NVIDIA/fleet-intelligence-agent/releases/latest), then install: +Download the package from the +[latest stable release](https://github.com/NVIDIA/fleet-intelligence-agent/releases/latest), +then install it: ```bash # RHEL/Rocky/AlmaLinux/Amazon Linux (x86_64) @@ -88,6 +90,32 @@ sudo fleetint --version systemctl status fleetintd ``` +## Optional: Verify package authenticity + +To verify a release before installation, download the package, +`fleet-intelligence.pub.asc`, `checksums.txt`, `checksums.txt.asc`, and +`verify-linux-package-signature.sh` from the same release. + +Confirm that the public key fingerprint is: + +```text +FE0C 8B74 CA66 357C 13BE 197D CCE3 C963 0871 99B3 +``` + +Then verify the checksum manifest, package digest, and embedded package +signature: + +```bash +gpg --show-keys --fingerprint fleet-intelligence.pub.asc +gpg --import fleet-intelligence.pub.asc +gpg --verify checksums.txt.asc checksums.txt +sha256sum -c --ignore-missing checksums.txt +chmod 755 verify-linux-package-signature.sh +./verify-linux-package-signature.sh \ + fleetint-VERSION-1.x86_64.rpm \ + fleet-intelligence.pub.asc +``` + ## Update Install the newer package version: @@ -111,4 +139,4 @@ sudo dnf remove fleetint References: - DCGM: - Fabric Manager: -- NVAT (`nvattest`/`corelib`): \ No newline at end of file +- NVAT (`nvattest`/`corelib`): diff --git a/scripts/refresh-checksums.sh b/scripts/refresh-checksums.sh new file mode 100755 index 00000000..69bbebdc --- /dev/null +++ b/scripts/refresh-checksums.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +if [[ $# -lt 1 ]]; then + echo "Usage: $0 [extra-artifact ...]" >&2 + exit 1 +fi + +checksum_file="$1" +dist_dir="$(cd "$(dirname "$checksum_file")" && pwd)" +checksum_file="$dist_dir/$(basename "$checksum_file")" + +[[ -s "$checksum_file" ]] || { + echo "Checksum file not found or empty: $checksum_file" >&2 + exit 1 +} + +if command -v sha256sum >/dev/null 2>&1; then + sha256_file() { + sha256sum "$1" | awk '{print $1}' + } +elif command -v shasum >/dev/null 2>&1; then + sha256_file() { + shasum -a 256 "$1" | awk '{print $1}' + } +else + echo "sha256sum or shasum is required" >&2 + exit 1 +fi + +updated_file="$(mktemp "$dist_dir/.checksums.txt.XXXXXX")" +cleanup() { + rm -f "$updated_file" +} +trap cleanup EXIT + +artifact_count=0 +while read -r _ artifact_name extra || [[ -n "${artifact_name:-}" ]]; do + [[ -z "${extra:-}" ]] || { + echo "Unsupported checksum line for an artifact containing whitespace: $artifact_name $extra" >&2 + exit 1 + } + artifact_name="${artifact_name#\*}" + [[ "$artifact_name" != */* && -n "$artifact_name" ]] || { + echo "Unsafe artifact name in checksum file: $artifact_name" >&2 + exit 1 + } + + artifact_path="$dist_dir/$artifact_name" + [[ -f "$artifact_path" ]] || { + echo "Artifact listed in checksum file is missing: $artifact_path" >&2 + exit 1 + } + + printf '%s %s\n' "$(sha256_file "$artifact_path")" "$artifact_name" >> "$updated_file" + artifact_count=$((artifact_count + 1)) +done < "$checksum_file" + +for extra_artifact in "${@:2}"; do + [[ -f "$extra_artifact" ]] || { + echo "Extra checksum artifact is missing: $extra_artifact" >&2 + exit 1 + } + artifact_name="$(basename "$extra_artifact")" + [[ "$artifact_name" != *" "* ]] || { + echo "Extra artifact name contains unsupported whitespace: $artifact_name" >&2 + exit 1 + } + if awk '{ print $2 }' "$updated_file" | grep -Fxq "$artifact_name"; then + continue + fi + printf '%s %s\n' "$(sha256_file "$extra_artifact")" "$artifact_name" >> "$updated_file" + artifact_count=$((artifact_count + 1)) +done + +(( artifact_count > 0 )) || { + echo "No artifacts were found in $checksum_file" >&2 + exit 1 +} + +chmod 0644 "$updated_file" +mv "$updated_file" "$checksum_file" +trap - EXIT + +echo "Refreshed SHA-256 checksums for $artifact_count release artifacts" diff --git a/scripts/sign-checksum-manifest.sh b/scripts/sign-checksum-manifest.sh new file mode 100755 index 00000000..2510636f --- /dev/null +++ b/scripts/sign-checksum-manifest.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +readonly EXPECTED_FINGERPRINT="FE0C8B74CA66357C13BE197DCCE3C963087199B3" + +if [[ $# -ne 2 ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +checksums_file="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" +public_key="$(cd "$(dirname "$2")" && pwd)/$(basename "$2")" +nvsec_bin="${NVSEC_BIN:-nvsec}" +job_type="${NVSEC_CHECKSUM_JOB_TYPE:-FLEET_INTELLIGENCE}" +scope="${NVSEC_CHECKSUM_SSA_SCOPE:-SIGNING_FLEET_INTELLIGENCE}" +signature_name="$(basename "$checksums_file").asc" + +: "${NVSEC_SSA_CLIENT_ID:?NVSEC_SSA_CLIENT_ID is required}" +: "${NVSEC_SSA_CLIENT_SECRET:?NVSEC_SSA_CLIENT_SECRET is required}" +command -v "$nvsec_bin" >/dev/null 2>&1 || { echo "nvsec is required" >&2; exit 1; } +command -v gpg >/dev/null 2>&1 || { echo "gpg is required" >&2; exit 1; } +command -v gpgv >/dev/null 2>&1 || { echo "gpgv is required" >&2; exit 1; } +[[ -s "$checksums_file" ]] || { echo "Checksum file not found or empty: $checksums_file" >&2; exit 1; } +[[ -s "$public_key" ]] || { echo "Public key not found or empty: $public_key" >&2; exit 1; } + +work_dir="$(mktemp -d "${RUNNER_TEMP:-/tmp}/fleetint-checksum-sign.XXXXXX")" +trap 'rm -rf "$work_dir"' EXIT + +args=( + 3s submit + --job_type "$job_type" + --description "Fleet Intelligence ${GITHUB_REF_NAME:-snapshot} checksum manifest" + --input_file "$checksums_file" + --download + --print_log + --auth ssa + --scope "$scope" + --timeout 600 + --result_dir "$work_dir" + --result_filename "$signature_name" +) +[[ -z "${NSPECT_ID:-}" ]] || args+=(--nspect_id "$NSPECT_ID") + +echo "Signing $(basename "$checksums_file") with 3S job $job_type" +"$nvsec_bin" "${args[@]}" + +signature="$work_dir/$signature_name" +[[ -s "$signature" ]] || { + echo "3S did not return a detached checksum signature" >&2 + find "$work_dir" -maxdepth 1 -name 'sign_r*.log' -type f -exec tail -n 100 {} \; >&2 + exit 1 +} + +key_home="$work_dir/key-home" +keyring="$work_dir/fleet-intelligence.gpg" +status_file="$work_dir/gpg-status" +mkdir -m 0700 "$key_home" + +fingerprint="$( + gpg --batch --homedir "$key_home" --show-keys --with-colons "$public_key" | + awk -F: '$1 == "fpr" { print $10; exit }' +)" +[[ "$fingerprint" == "$EXPECTED_FINGERPRINT" ]] || { + echo "Unexpected Fleet Intelligence key fingerprint: $fingerprint" >&2 + exit 1 +} + +gpg --batch --homedir "$key_home" --yes --dearmor --output "$keyring" "$public_key" +gpgv --status-fd 1 --keyring "$keyring" "$signature" "$checksums_file" > "$status_file" 2>&1 +grep -q "^\[GNUPG:\] VALIDSIG $EXPECTED_FINGERPRINT " "$status_file" || { + echo "Checksum signature was not made by the expected Fleet Intelligence key" >&2 + cat "$status_file" >&2 + exit 1 +} + +install -m 0644 "$signature" "$checksums_file.asc" +echo "Created verified detached signature $checksums_file.asc" diff --git a/scripts/sign-linux-packages.sh b/scripts/sign-linux-packages.sh new file mode 100755 index 00000000..d582feb3 --- /dev/null +++ b/scripts/sign-linux-packages.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +if [[ $# -ne 2 ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +dist_dir="$(cd "$1" && pwd)" +public_key="$(cd "$(dirname "$2")" && pwd)/$(basename "$2")" +nvsec_bin="${NVSEC_BIN:-nvsec}" +job_type="${NVSEC_LINUX_PACKAGE_JOB_TYPE:-LINUX_PACKAGE_FLEET_INTELLIGENCE}" +scope="${NVSEC_LINUX_PACKAGE_SSA_SCOPE:-SIGNING_LINUX_PACKAGE_FLEET_INTELLIGENCE}" + +: "${NVSEC_SSA_CLIENT_ID:?NVSEC_SSA_CLIENT_ID is required}" +: "${NVSEC_SSA_CLIENT_SECRET:?NVSEC_SSA_CLIENT_SECRET is required}" +command -v "$nvsec_bin" >/dev/null 2>&1 || { echo "nvsec is required" >&2; exit 1; } +[[ -s "$public_key" ]] || { echo "Public key not found or empty: $public_key" >&2; exit 1; } + +shopt -s nullglob +packages=("$dist_dir"/*.deb "$dist_dir"/*.rpm) +shopt -u nullglob +(( ${#packages[@]} > 0 )) || { + echo "No DEB or RPM packages found in $dist_dir" >&2 + exit 1 +} + +work_dir="$(mktemp -d "${RUNNER_TEMP:-/tmp}/fleetint-package-sign.XXXXXX")" +cleanup() { + case "$work_dir" in + "${RUNNER_TEMP:-/tmp}"/fleetint-package-sign.*) rm -rf "$work_dir" ;; + *) echo "Refusing to remove unexpected temporary directory: $work_dir" >&2 ;; + esac +} +trap cleanup EXIT + +for package in "${packages[@]}"; do + package_name="$(basename "$package")" + result_dir="$work_dir/$package_name" + mkdir -p "$result_dir" + + args=( + 3s submit + --job_type "$job_type" + --description "fleetint ${GITHUB_REF_NAME:-snapshot} package signing: $package_name" + --input_file "$package" + --download + --print_log + --auth ssa + --scope "$scope" + --timeout 600 + --result_dir "$result_dir" + --result_filename "$package_name" + ) + [[ -z "${NSPECT_ID:-}" ]] || args+=(--nspect_id "$NSPECT_ID") + + echo "Signing $package_name with 3S job $job_type" + "$nvsec_bin" "${args[@]}" + + signed_package="$result_dir/$package_name" + [[ -s "$signed_package" ]] || { + echo "3S did not return a signed package for $package_name" >&2 + find "$result_dir" -maxdepth 1 -name 'sign_r*.log' -type f -exec tail -n 100 {} \; >&2 + exit 1 + } + + "$(dirname "$0")/verify-linux-package-signature.sh" "$signed_package" "$public_key" + + replacement="$dist_dir/.$package_name.signed" + install -m 0644 "$signed_package" "$replacement" + mv "$replacement" "$package" + echo "Replaced $package_name with its verified signed package" +done + +echo "Signed and verified ${#packages[@]} Linux packages" diff --git a/scripts/verify-linux-package-signature.sh b/scripts/verify-linux-package-signature.sh new file mode 100755 index 00000000..a0bfadf7 --- /dev/null +++ b/scripts/verify-linux-package-signature.sh @@ -0,0 +1,149 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +readonly EXPECTED_FINGERPRINT="FE0C8B74CA66357C13BE197DCCE3C963087199B3" + +if [[ $# -ne 2 ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +package="$1" +public_key="$2" + +[[ -s "$package" ]] || { echo "Package not found or empty: $package" >&2; exit 1; } +[[ -s "$public_key" ]] || { echo "Public key not found or empty: $public_key" >&2; exit 1; } + +work_dir="$(mktemp -d "${RUNNER_TEMP:-/tmp}/fleetint-package-verify.XXXXXX")" +cleanup() { + case "$work_dir" in + "${RUNNER_TEMP:-/tmp}"/fleetint-package-verify.*) rm -rf "$work_dir" ;; + *) echo "Refusing to remove unexpected temporary directory: $work_dir" >&2 ;; + esac +} +trap cleanup EXIT + +verify_key_fingerprint() { + command -v gpg >/dev/null 2>&1 || { echo "gpg is required" >&2; exit 1; } + + local keycheck_home="$work_dir/keycheck" + local actual_fingerprint + mkdir -m 0700 "$keycheck_home" + actual_fingerprint="$( + gpg --batch --homedir "$keycheck_home" --show-keys --with-colons "$public_key" | + awk -F: '$1 == "fpr" { print $10; exit }' + )" + [[ "$actual_fingerprint" == "$EXPECTED_FINGERPRINT" ]] || { + echo "Unexpected Fleet Intelligence signing-key fingerprint: $actual_fingerprint" >&2 + exit 1 + } +} + +verify_deb() { + for command in ar awk gpg md5sum sha1sum; do + command -v "$command" >/dev/null 2>&1 || { + echo "$command is required to verify DEB signatures" >&2 + exit 1 + } + done + + local gpg_home="$work_dir/gnupg" + local signature_file="$work_dir/_gpgbuilder" + local status_file="$work_dir/gpg-status" + local manifest_file="$work_dir/manifest" + mkdir -m 0700 "$gpg_home" + + ar p "$package" _gpgbuilder > "$signature_file" + gpg --batch --homedir "$gpg_home" --quiet --import "$public_key" + gpg --batch --homedir "$gpg_home" --status-fd 1 --verify "$signature_file" > "$status_file" 2>&1 + grep -q "^\\[GNUPG:\\] VALIDSIG $EXPECTED_FINGERPRINT " "$status_file" || { + echo "DEB signature was not made by the expected Fleet Intelligence key" >&2 + cat "$status_file" >&2 + exit 1 + } + + awk ' + /^Files:[[:space:]]*$/ { in_files = 1; next } + /^-----BEGIN PGP SIGNATURE-----$/ { in_files = 0 } + in_files && NF == 4 { print $1, $2, $3, $4 } + ' "$signature_file" > "$manifest_file" + [[ -s "$manifest_file" ]] || { + echo "DEB signature does not contain a package-member manifest" >&2 + exit 1 + } + + require_signed_member() { + local member_pattern="$1" + local member_description="$2" + awk -v pattern="$member_pattern" '$4 ~ pattern { found = 1 } END { exit !found }' "$manifest_file" || { + echo "DEB signature does not cover required member: $member_description" >&2 + exit 1 + } + } + require_signed_member '^debian-binary$' 'debian-binary' + require_signed_member '^control[.]tar([.](gz|xz|zst|bz2|lzma))?$' 'control.tar.*' + require_signed_member '^data[.]tar([.](gz|xz|zst|bz2|lzma))?$' 'data.tar.*' + + while read -r expected_md5 expected_sha1 expected_size member_name; do + [[ "$member_name" != */* && "$member_name" != "_gpgbuilder" ]] || { + echo "Unsafe DEB member in signed manifest: $member_name" >&2 + exit 1 + } + + local member_file="$work_dir/member" + ar p "$package" "$member_name" > "$member_file" + local actual_md5 actual_sha1 actual_size + actual_md5="$(md5sum "$member_file" | awk '{print $1}')" + actual_sha1="$(sha1sum "$member_file" | awk '{print $1}')" + actual_size="$(wc -c < "$member_file" | tr -d '[:space:]')" + + [[ "$actual_md5" == "$expected_md5" ]] || { + echo "DEB member MD5 mismatch: $member_name" >&2 + exit 1 + } + [[ "$actual_sha1" == "$expected_sha1" ]] || { + echo "DEB member SHA-1 mismatch: $member_name" >&2 + exit 1 + } + [[ "$actual_size" == "$expected_size" ]] || { + echo "DEB member size mismatch: $member_name" >&2 + exit 1 + } + done < "$manifest_file" +} + +verify_rpm() { + command -v rpm >/dev/null 2>&1 || { + echo "rpm is required to verify RPM signatures" >&2 + exit 1 + } + + local rpm_db="$work_dir/rpmdb" + local rpm_status="$work_dir/rpm-status" + mkdir -p "$rpm_db" + rpm --dbpath "$rpm_db" --initdb + rpm --dbpath "$rpm_db" --import "$public_key" + rpm --dbpath "$rpm_db" --checksig --verbose "$package" | tee "$rpm_status" + + grep -Eiq "Signature, key ID 087199b3: OK" "$rpm_status" || { + echo "RPM signature was not made by the expected Fleet Intelligence key" >&2 + exit 1 + } + if grep -Eiq "NOT OK|NOKEY|NOTTRUSTED|BAD" "$rpm_status"; then + echo "RPM signature or digest validation failed" >&2 + exit 1 + fi +} + +verify_key_fingerprint +case "$package" in + *.deb) verify_deb ;; + *.rpm) verify_rpm ;; + *) echo "Unsupported package format: $package" >&2; exit 1 ;; +esac + +echo "Verified $(basename "$package") with Fleet Intelligence key $EXPECTED_FINGERPRINT"