Add attestation support to increase release security (#2117)

* ci: Add sbom and attestation

* tools: Add verify-release.sh

* verify-release.sh: Set executable permission

* verify-release.sh: Put downloads into a gitignored directory

* tools: Make verify-release also download sbom

---------

Co-authored-by: OpenSauce04 <opensauce04@gmail.com>
This commit is contained in:
PabloMK7 2026-05-14 14:52:10 +02:00 committed by OpenSauce04
parent 26036ce2d3
commit 308a9b14ea
5 changed files with 423 additions and 9 deletions

View File

@ -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

View File

@ -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

View File

@ -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

3
.gitignore vendored
View File

@ -60,3 +60,6 @@ VULKAN_SDK/
# Version info files
GIT-COMMIT
GIT-TAG
# verify-release.sh downloads
verify/

194
tools/verify-release.sh Executable file
View File

@ -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 <owner/repo> <tag>
#
# 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 <owner/repo> <tag>"
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 "========================================"