diff --git a/.ci/docker.sh b/.ci/docker.sh index d4df835c4..1747f8832 100755 --- a/.ci/docker.sh +++ b/.ci/docker.sh @@ -14,4 +14,7 @@ echo "Tag name is: $TAG_NAME" docker build -f docker/azahar-room/Dockerfile -t azahar-room:$TAG_NAME . mkdir -p build -docker save azahar-room:$TAG_NAME > build/azahar-room-$TAG_NAME.dockerimage +FILENAME="azahar-room-$TAG_NAME.dockerimage" +docker save azahar-room:$TAG_NAME > build/$FILENAME + +echo "DOCKER_IMAGE_PATH=artifacts/$FILENAME" >> $GITHUB_ENV \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0201aa3b0..75d552f1d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,6 +7,11 @@ on: pull_request: branches: [ master ] +permissions: + id-token: write + contents: read + attestations: write + jobs: source: if: ${{ !github.head_ref }} @@ -17,11 +22,26 @@ jobs: submodules: recursive - name: Pack run: ./.ci/source.sh + - name: Generate SBOM + if: ${{ github.ref_type == 'tag' }} + uses: anchore/sbom-action@v0 + with: + path: ./ + format: spdx-json + output-file: artifacts/source.spdx.json + upload-artifact: false - name: Upload uses: actions/upload-artifact@v4 with: name: source path: artifacts/ + - name: Attest artifacts + if: ${{ github.ref_type == 'tag' }} + uses: actions/attest@v4 + with: + subject-path: | + artifacts/*.tar.xz + sbom-path: artifacts/source.spdx.json linux-x86_64: runs-on: ubuntu-latest @@ -39,13 +59,14 @@ jobs: OS: linux TARGET: ${{ matrix.target }} SHOULD_RUN: ${{ (matrix.target != 'appimage-wayland' || github.ref_type == 'tag') }} + CACHE_ENABLED: ${{ github.ref_type != 'tag' }} steps: - uses: actions/checkout@v4 if: ${{ env.SHOULD_RUN == 'true' }} with: submodules: recursive - name: Set up cache - if: ${{ env.SHOULD_RUN == 'true' }} + if: ${{ env.SHOULD_RUN == 'true' && env.CACHE_ENABLED == 'true' }} uses: actions/cache@v4 with: path: ${{ env.CCACHE_DIR }} @@ -64,12 +85,27 @@ jobs: if: ${{ matrix.target == 'appimage-wayland' && env.SHOULD_RUN == 'true' }} run: | mv artifacts/azahar.AppImage artifacts/azahar-wayland.AppImage + - name: Generate SBOM + if: ${{ contains(matrix.target, 'appimage') && github.ref_type == 'tag' && env.SHOULD_RUN == 'true' }} + uses: anchore/sbom-action@v0 + with: + path: build/ + format: spdx-json + output-file: artifacts/linux-x86_64-${{ matrix.target }}.spdx.json + upload-artifact: false - name: Upload if: ${{ contains(matrix.target, 'appimage') && env.SHOULD_RUN == 'true' }} uses: actions/upload-artifact@v4 with: name: ${{ github.job }}-${{ matrix.target }} path: artifacts/ + - name: Attest artifacts + if: ${{ contains(matrix.target, 'appimage') && github.ref_type == 'tag' && env.SHOULD_RUN == 'true' }} + uses: actions/attest@v4 + with: + subject-path: | + artifacts/*.AppImage + sbom-path: artifacts/linux-x86_64-${{ matrix.target }}.spdx.json linux-arm64: runs-on: ubuntu-24.04-arm @@ -106,12 +142,14 @@ jobs: CCACHE_DIR: ${{ github.workspace }}/.ccache CCACHE_COMPILERCHECK: content CCACHE_SLOPPINESS: time_macros + CACHE_ENABLED: ${{ github.ref_type != 'tag' }} OS: macos steps: - uses: actions/checkout@v4 with: submodules: recursive - name: Set up cache + if: ${{ env.CACHE_ENABLED == 'true' }} uses: actions/cache@v4 with: path: ${{ env.CCACHE_DIR }} @@ -136,11 +174,26 @@ jobs: env: PACK_INDIVIDUALLY: 1 run: ./.ci/pack.sh + - name: Generate SBOM + if: ${{ github.ref_type == 'tag' }} + uses: anchore/sbom-action@v0 + with: + path: build/ + format: spdx-json + output-file: artifacts/macos.spdx.json + upload-artifact: false - name: Upload uses: actions/upload-artifact@v4 with: name: ${{ env.OS }} path: artifacts/ + - name: Attest artifacts + if: ${{ github.ref_type == 'tag' }} + uses: actions/attest@v4 + with: + subject-path: | + artifacts/*.zip + sbom-path: artifacts/macos.spdx.json windows: runs-on: windows-latest @@ -155,6 +208,7 @@ jobs: CCACHE_DIR: ${{ github.workspace }}/.ccache CCACHE_COMPILERCHECK: content CCACHE_SLOPPINESS: time_macros + CACHE_ENABLED: ${{ github.ref_type != 'tag' }} OS: windows TARGET: ${{ matrix.target }} steps: @@ -162,6 +216,7 @@ jobs: with: submodules: recursive - name: Set up cache + if: ${{ env.CACHE_ENABLED == 'true' }} uses: actions/cache@v4 with: path: ${{ env.CCACHE_DIR }} @@ -214,11 +269,27 @@ jobs: shell: cmd - name: Pack run: ./.ci/pack.sh + - name: Generate SBOM + if: ${{ github.ref_type == 'tag' }} + uses: anchore/sbom-action@v0 + with: + path: build/ + format: spdx-json + output-file: artifacts/windows-${{ matrix.target }}.spdx.json + upload-artifact: false - name: Upload uses: actions/upload-artifact@v4 with: name: ${{ env.OS }}-${{ env.TARGET }} path: artifacts/ + - name: Attest artifacts + if: ${{ github.ref_type == 'tag' }} + uses: actions/attest@v4 + with: + subject-path: | + artifacts/*.zip + artifacts/*.exe + sbom-path: artifacts/windows-${{ matrix.target }}.spdx.json android: runs-on: ubuntu-latest @@ -230,6 +301,7 @@ jobs: CCACHE_DIR: ${{ github.workspace }}/.ccache CCACHE_COMPILERCHECK: content CCACHE_SLOPPINESS: time_macros + CACHE_ENABLED: ${{ github.ref_type != 'tag' }} OS: android TARGET: ${{ matrix.target }} SHOULD_RUN: ${{ (matrix.target == 'vanilla' || github.ref_type == 'tag') }} @@ -239,7 +311,7 @@ jobs: with: submodules: recursive - name: Set up cache - if: ${{ env.SHOULD_RUN == 'true' }} + if: ${{ env.SHOULD_RUN == 'true' && env.CACHE_ENABLED == 'true' }} uses: actions/cache@v4 with: path: | @@ -278,12 +350,28 @@ jobs: working-directory: src/android/app env: UNPACKED: 1 + - name: Generate SBOM + if: ${{ github.ref_type == 'tag' }} + uses: anchore/sbom-action@v0 + with: + path: src/android + format: spdx-json + output-file: src/android/app/artifacts/android-${{ matrix.target }}.spdx.json + upload-artifact: false - name: Upload if: ${{ env.SHOULD_RUN == 'true' }} uses: actions/upload-artifact@v4 with: name: ${{ env.OS }}-${{ env.TARGET }} path: src/android/app/artifacts/ + - name: Attest artifacts + if: ${{ github.ref_type == 'tag' }} + uses: actions/attest@v4 + with: + subject-path: | + src/android/app/artifacts/*.apk + src/android/app/artifacts/*.aab + sbom-path: src/android/app/artifacts/android-${{ matrix.target }}.spdx.json docker: runs-on: ubuntu-latest @@ -303,8 +391,23 @@ jobs: run: | mkdir -p artifacts mv build/*.dockerimage artifacts/ + - name: Generate SBOM + if: ${{ github.ref_type == 'tag' }} + uses: anchore/sbom-action@v0 + with: + image: ${{ env.DOCKER_IMAGE_PATH }} + format: spdx-json + output-file: artifacts/docker-room.spdx.json + upload-artifact: false - name: Upload uses: actions/upload-artifact@v4 with: name: docker path: artifacts/ + - name: Attest artifacts + if: ${{ github.ref_type == 'tag' }} + uses: actions/attest@v4 + with: + subject-path: | + artifacts/*.dockerimage + sbom-path: artifacts/docker-room.spdx.json diff --git a/.github/workflows/libretro.yml b/.github/workflows/libretro.yml index 7739a0e8a..10c137327 100644 --- a/.github/workflows/libretro.yml +++ b/.github/workflows/libretro.yml @@ -11,6 +11,11 @@ on: env: CORE_ARGS: -DENABLE_LIBRETRO=ON +permissions: + id-token: write + contents: read + attestations: write + jobs: android: runs-on: ubuntu-22.04 @@ -48,11 +53,29 @@ jobs: llvm-strip -s $BUILD_DIR/$EXTRA_PATH/azahar_libretro.* - name: Pack run: ./.ci/libretro-pack.sh + - name: Generate SBOM + if: ${{ github.ref_type == 'tag' }} + uses: anchore/sbom-action@v0 + with: + path: build/ + format: spdx-json + output-file: libretro-android.spdx.json + upload-artifact: false - name: Upload uses: actions/upload-artifact@v4 with: name: ${{ env.OS }}-${{ env.TARGET }} - path: ./*.zip + path: | + ./*.zip + ./*.spdx.json + - name: Attest artifacts + if: ${{ github.ref_type == 'tag' }} + uses: actions/attest@v4 + with: + subject-path: | + ./*.zip + sbom-path: libretro-android.spdx.json + linux: runs-on: ubuntu-22.04 env: @@ -76,11 +99,29 @@ jobs: llvm-strip -s $BUILD_DIR/$EXTRA_PATH/azahar_libretro.* - name: Pack run: ./.ci/libretro-pack.sh + - name: Generate SBOM + if: ${{ github.ref_type == 'tag' }} + uses: anchore/sbom-action@v0 + with: + path: build/ + format: spdx-json + output-file: libretro-linux.spdx.json + upload-artifact: false - name: Upload uses: actions/upload-artifact@v4 with: name: ${{ env.OS }}-${{ env.TARGET }} - path: ./*.zip + path: | + ./*.zip + ./*.spdx.json + - name: Attest artifacts + if: ${{ github.ref_type == 'tag' }} + uses: actions/attest@v4 + with: + subject-path: | + ./*.zip + sbom-path: libretro-linux.spdx.json + windows: runs-on: ubuntu-latest env: @@ -108,11 +149,28 @@ jobs: x86_64-w64-mingw32.static-strip -s $BUILD_DIR/$EXTRA_PATH/azahar_libretro.*" - name: Pack run: ./.ci/libretro-pack.sh + - name: Generate SBOM + if: ${{ github.ref_type == 'tag' }} + uses: anchore/sbom-action@v0 + with: + path: build/ + format: spdx-json + output-file: libretro-windows.spdx.json + upload-artifact: false - name: Upload uses: actions/upload-artifact@v4 with: name: ${{ env.OS }}-${{ env.TARGET }} - path: ./*.zip + path: | + ./*.zip + ./*.spdx.json + - name: Attest artifacts + if: ${{ github.ref_type == 'tag' }} + uses: actions/attest@v4 + with: + subject-path: | + ./*.zip + sbom-path: libretro-windows.spdx.json macos: runs-on: macos-26 strategy: @@ -137,11 +195,29 @@ jobs: strip -x $BUILD_DIR/$EXTRA_PATH/azahar_libretro.* - name: Pack run: ./.ci/libretro-pack.sh + - name: Generate SBOM + if: ${{ github.ref_type == 'tag' }} + uses: anchore/sbom-action@v0 + with: + path: build/ + format: spdx-json + output-file: libretro-macos-${{ matrix.target }}.spdx.json + upload-artifact: false - name: Upload uses: actions/upload-artifact@v4 with: name: ${{ env.OS }}-${{ env.TARGET }} - path: ./*.zip + path: | + ./*.zip + ./*.spdx.json + - name: Attest artifacts + if: ${{ github.ref_type == 'tag' }} + uses: actions/attest@v4 + with: + subject-path: | + ./*.zip + sbom-path: libretro-macos-${{ matrix.target }}.spdx.json + ios: runs-on: macos-26 env: @@ -161,11 +237,29 @@ jobs: strip -x $BUILD_DIR/$EXTRA_PATH/azahar_libretro.* - name: Pack run: ./.ci/libretro-pack.sh + - name: Generate SBOM + if: ${{ github.ref_type == 'tag' }} + uses: anchore/sbom-action@v0 + with: + path: build/ + format: spdx-json + output-file: libretro-ios.spdx.json + upload-artifact: false - name: Upload uses: actions/upload-artifact@v4 with: name: ${{ env.OS }}-${{ env.TARGET }} - path: ./*.zip + path: | + ./*.zip + ./*.spdx.json + - name: Attest artifacts + if: ${{ github.ref_type == 'tag' }} + uses: actions/attest@v4 + with: + subject-path: | + ./*.zip + sbom-path: libretro-ios.spdx.json + tvos: runs-on: macos-26 env: @@ -185,8 +279,25 @@ jobs: strip -x $BUILD_DIR/$EXTRA_PATH/azahar_libretro.* - name: Pack run: ./.ci/libretro-pack.sh + - name: Generate SBOM + if: ${{ github.ref_type == 'tag' }} + uses: anchore/sbom-action@v0 + with: + path: build/ + format: spdx-json + output-file: libretro-tvos.spdx.json + upload-artifact: false - name: Upload uses: actions/upload-artifact@v4 with: name: ${{ env.OS }}-${{ env.TARGET }} - path: ./*.zip + path: | + ./*.zip + ./*.spdx.json + - name: Attest artifacts + if: ${{ github.ref_type == 'tag' }} + uses: actions/attest@v4 + with: + subject-path: | + ./*.zip + sbom-path: libretro-tvos.spdx.json diff --git a/.gitignore b/.gitignore index 3a44642e1..b234d6bc5 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,6 @@ VULKAN_SDK/ # Version info files GIT-COMMIT GIT-TAG + +# verify-release.sh downloads +verify/ \ No newline at end of file diff --git a/tools/verify-release.sh b/tools/verify-release.sh new file mode 100755 index 000000000..7d3e7b040 --- /dev/null +++ b/tools/verify-release.sh @@ -0,0 +1,194 @@ +#!/usr/bin/env bash + +# Copyright Citra Emulator Project / Azahar Emulator Project +# Licensed under GPLv2 or any later version +# Refer to the license.txt file included. + +set -euo pipefail + +# Usage: +# ./verify-release.sh +# +# Example: +# ./verify-release.sh azahar-emu/azahar 2126.0 +# +# Behavior: +# - Downloads all release assets +# - Verifies asset is published in the release +# - Verifies SPDX attestations for every asset +# - Extracts SPDX SBOMs +# +# Notes: +# - Requires installation of the GitHub CLI (gh) and jq tools. +# - Draft release support requires authentication with permission +# to view the draft release. +# - gh release verify-asset currently does NOT support draft releases. + +if [[ $# -ne 2 ]]; then + echo "Usage: $0 " + exit 1 +fi + +command -v gh >/dev/null 2>&1 || { + echo "ERROR: GitHub CLI (gh) is not installed or not in PATH" + exit 1 +} + +command -v jq >/dev/null 2>&1 || { + echo "ERROR: jq is not installed or not in PATH" + exit 1 +} + +REPO="$1" +TAG="$2" + +echo "==> Fetching release metadata" + +IS_DRAFT=$( + gh release view "$TAG" \ + --repo "$REPO" \ + --json isDraft \ + --jq '.isDraft' +) + +WORKDIR="verify/release-${TAG}" +SBOMSUBDIR="sbom" + +rm -rf "$WORKDIR" +mkdir -p "$WORKDIR" +cd "$WORKDIR" +mkdir -p "$SBOMSUBDIR" + + +echo +echo "==> Downloading release assets" + +gh release download "$TAG" \ + --repo "$REPO" + +echo +echo "==> Fetching asset list" + +ASSETS=() + +while IFS= read -r asset; do + ASSETS+=("$asset") +done < <( + gh release view "$TAG" \ + --repo "$REPO" \ + --json assets \ + --jq '.assets[].name' +) + +echo +echo "==> Release type: $( + [[ "$IS_DRAFT" == "true" ]] && echo "draft" || echo "published" +)" + +echo +echo "==> Verifying assets" + +for asset in "${ASSETS[@]}"; do + # Skip attestation files themselves + if [[ "$asset" == *.intoto.jsonl ]]; then + continue + fi + + if [[ ! -f "$asset" ]]; then + echo "ERROR: Missing downloaded asset: $asset" + exit 1 + fi + + echo + echo "========================================" + echo "Asset: $asset" + echo "========================================" + + echo "1/3 Release asset verification" + + if [[ "$IS_DRAFT" != "true" ]]; then + gh release verify-asset "$TAG" "$asset" \ + --repo "$REPO" + echo + else + echo "SKIPPED (draft releases unsupported)" + echo + fi + + echo "2/3 Attestation verification" + + if [[ "$asset" == *.sha256sum ]]; then + echo "SKIPPED (sha256sum does not need verification)" + echo "SKIPPED (no SPDX SBOM extraction)" + else + gh attestation verify "$asset" \ + --repo "$REPO" \ + --predicate-type https://spdx.dev/Document + + echo + echo "3/3 SBOM extraction" + + BASE_NAME="$(basename "$asset")" + SBOM_FILE="${SBOMSUBDIR}/${BASE_NAME}.spdx.json" + + # gh attestation download does not currently support + # specifying the output file, nor it allows piping the + # output. For that reason, we need to find the .jsonl + # in the current directory. + + # Exclude any existing .jsonl files from find + # (failsafe, should not happen) + BEFORE_JSONL="$(find . -maxdepth 1 -name '*.jsonl' -print)" + + gh attestation download "$asset" \ + --repo "$REPO" \ + >/dev/null + + ATTESTATION_FILE="" + + while IFS= read -r file; do + FOUND=false + + while IFS= read -r oldfile; do + if [[ "$file" == "$oldfile" ]]; then + FOUND=true + break + fi + done <<< "$BEFORE_JSONL" + + # Only consider new jsonl files + if [[ "$FOUND" == "false" ]]; then + ATTESTATION_FILE="$file" + break + fi + done < <(find . -maxdepth 1 -name '*.jsonl' -print) + + if [[ -z "$ATTESTATION_FILE" ]]; then + echo "ERROR: Could not locate downloaded attestation jsonl" + exit 1 + fi + + # Extract and decode the SBOM from the jsonl + jq -r ' + .dsseEnvelope.payload + ' "$ATTESTATION_FILE" | + while IFS= read -r payload; do + echo "$payload" | base64 -d + done | + jq '.predicate' \ + > "$SBOM_FILE" + + rm -f "$ATTESTATION_FILE" + + echo "Saved SBOM: $SBOM_FILE" + fi + + echo + echo "OK: $asset" +done + +echo +echo "========================================" +echo "All assets verified successfully" +echo "SBOMs saved in: $WORKDIR/$SBOMSUBDIR" +echo "========================================" \ No newline at end of file