diff --git a/.github/workflows/_build-and-publish-image.yaml b/.github/workflows/_build-and-publish-image.yaml index fd6c4514..cdd09401 100644 --- a/.github/workflows/_build-and-publish-image.yaml +++ b/.github/workflows/_build-and-publish-image.yaml @@ -34,6 +34,8 @@ on: permissions: contents: write + id-token: write + attestations: write jobs: build: @@ -148,6 +150,50 @@ jobs: - name: Show build artifacts run: ls -lh build/ + - name: Generate SHA256SUMS + run: | + set -euo pipefail + shopt -s nullglob + cd build + efi=("${ARTIFACT_PREFIX}"_*.efi) sbom=("${ARTIFACT_PREFIX}"_*.sbom.cdx.json) + ((${#efi[@]} == 1 && ${#sbom[@]} == 1)) + sha256sum "${ARTIFACT_PREFIX}"_*.{efi,raw,tar.gz,qcow2,sbom.cdx.json} \ + portable_measurements.json >SHA256SUMS + printf 'EFI_FILE=build/%s\nSBOM_FILE=build/%s\n' "${efi[0]}" "${sbom[0]}" >>"$GITHUB_ENV" + + - name: Attest build provenance + id: attest-provenance + uses: actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d # v4.2.1 + with: + subject-checksums: build/SHA256SUMS + + - name: Export provenance bundle + env: + BUNDLE_PATH: ${{ steps.attest-provenance.outputs.bundle-path }} + run: cp "${BUNDLE_PATH}" build/provenance.sigstore.json + + - name: Attest SBOM + id: attest-sbom + uses: actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d # v4.2.1 + with: + subject-path: ${{ env.EFI_FILE }} + sbom-path: ${{ env.SBOM_FILE }} + + - name: Export SBOM bundle + env: + BUNDLE_PATH: ${{ steps.attest-sbom.outputs.bundle-path }} + run: cp "${BUNDLE_PATH}" build/sbom.sigstore.json + + - name: Verify attestations + env: + GH_TOKEN: ${{ github.token }} + run: | + gh attestation verify "${EFI_FILE}" --repo "${GITHUB_REPOSITORY}" \ + --bundle build/provenance.sigstore.json + gh attestation verify "${EFI_FILE}" --repo "${GITHUB_REPOSITORY}" \ + --bundle build/sbom.sigstore.json \ + --predicate-type https://cyclonedx.org/bom + - name: Install rclone run: | set -euo pipefail @@ -178,11 +224,13 @@ jobs: acl = private use_data_integrity_protections = false EOF - # Upload only the image artifacts and measurements. rclone copy -P --retries 3 --retries-sleep 20s --error-on-no-transfer \ --s3-upload-concurrency=8 --transfers=8 \ - --include "${ARTIFACT_PREFIX}_*.{efi,tar.gz,qcow2}" \ + --include "${ARTIFACT_PREFIX}_*.{efi,raw,tar.gz,qcow2}" \ + --include "${ARTIFACT_PREFIX}_*.sbom.cdx.json" \ --include "portable_measurements.json" \ + --include "SHA256SUMS" \ + --include "*.sigstore.json" \ build "r2:${R2_BUCKET}/${R2_PATH_PREFIX}/${VERSION_DIR}/" - name: Create draft GitHub Release @@ -207,18 +255,21 @@ jobs: echo "| ๐Ÿ”– **Commit** | [\`${COMMIT_SHORT}\`](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/commit/${HEAD_SHA}) |" echo "| ๐Ÿ“… **Built** | ${BUILT} |" echo "" - echo "### ๐Ÿ’ฟ Images" + echo "### ๐Ÿ’ฟ Artifacts" echo "" echo "| Target | File | Size |" echo "|---|---|--:|" - for f in build/"${ARTIFACT_PREFIX}"_*.efi build/"${ARTIFACT_PREFIX}"_*.tar.gz build/"${ARTIFACT_PREFIX}"_*.qcow2; do + for f in build/"${ARTIFACT_PREFIX}"_*.efi build/"${ARTIFACT_PREFIX}"_*.raw build/"${ARTIFACT_PREFIX}"_*.tar.gz build/"${ARTIFACT_PREFIX}"_*.qcow2 \ + build/"${ARTIFACT_PREFIX}"_*.sbom.cdx.json; do n="$(basename "$f")" sz="$(du -h "$f" | cut -f1)" case "$n" in - *.efi) t="๐Ÿง UKI (EFI)" ;; - *.tar.gz) t="โ˜๏ธ GCP disk" ;; - *.qcow2) t="๐Ÿงช QEMU qcow2" ;; - *) t="๐Ÿ“„ artifact" ;; + *.efi) t="๐Ÿง UKI (EFI)" ;; + *.raw) t="๐Ÿ’ฝ Raw disk" ;; + *.tar.gz) t="โ˜๏ธ GCP disk" ;; + *.qcow2) t="๐Ÿงช QEMU qcow2" ;; + *.cdx.json) t="๐Ÿงพ SBOM (CycloneDX)" ;; + *) t="๐Ÿ“„ artifact" ;; esac echo "| ${t} | [\`${n}\`](${R2_PUBLIC_BASE_URL}/${R2_PATH_PREFIX}/${VERSION_DIR}/${n}) | ${sz} |" done @@ -228,6 +279,15 @@ jobs: echo ">" echo "> ๐Ÿ“– Details: [attestation walkthrough](https://github.com/flashbots/flashbots-images/blob/main/modules/flashbox/flashbox-l1/readme.md#attestation-walkthrough) ยท [attested-tls-proxy](https://github.com/flashbots/attested-tls-proxy)" echo "" + echo "### ๐Ÿ” Verify build and SBOM" + echo "" + echo "Checksums and Sigstore verification bundles are attached." + echo "" + echo '```bash' + echo "gh attestation verify --repo ${GITHUB_REPOSITORY} --bundle provenance.sigstore.json" + echo "gh attestation verify --repo ${GITHUB_REPOSITORY} --bundle sbom.sigstore.json --predicate-type https://cyclonedx.org/bom" + echo '```' + echo "" echo "### ๐Ÿ” Reproduce the image hashes" echo "" echo "1. Clone and check out this release:" @@ -256,10 +316,10 @@ jobs: echo ' ```' } > release-notes.md - # Attach the measurements and the .efi UKI; the larger tar.gz/qcow2 - # artifacts stay in R2 and are linked in the release body. assets=() - for f in build/portable_measurements.json build/"${ARTIFACT_PREFIX}"_*.efi; do + for f in build/portable_measurements.json build/SHA256SUMS \ + build/provenance.sigstore.json build/sbom.sigstore.json \ + build/"${ARTIFACT_PREFIX}"_*.efi; do [ -e "$f" ] && assets+=("$f") done diff --git a/.github/workflows/flashbox-l1.yaml b/.github/workflows/flashbox-l1.yaml index bf23837c..91d684b6 100644 --- a/.github/workflows/flashbox-l1.yaml +++ b/.github/workflows/flashbox-l1.yaml @@ -35,6 +35,8 @@ jobs: release: permissions: contents: write + id-token: write + attestations: write uses: ./.github/workflows/_build-and-publish-image.yaml with: image-id: flashbox-l1 diff --git a/flake.nix b/flake.nix index fe62d1b9..574b8551 100644 --- a/flake.nix +++ b/flake.nix @@ -88,6 +88,7 @@ qemu-utils parted jq + syft reprepro systemd bash diff --git a/modules/flashbox/flashbox-l1/mkosi.build b/modules/flashbox/flashbox-l1/mkosi.build index dd7c1b70..d507a4ae 100755 --- a/modules/flashbox/flashbox-l1/mkosi.build +++ b/modules/flashbox/flashbox-l1/mkosi.build @@ -11,7 +11,7 @@ LIGHTHOUSE_BUILD_CMD=" export SOURCE_DATE_EPOCH=\$(git log -1 --pretty=%ct) export RUSTFLAGS='-C link-arg=-Wl,--build-id=none -C metadata= --remap-path-prefix \$(pwd)=. -L /usr/lib/x86_64-linux-gnu -l z -l zstd -l snappy' - cargo build --bin lighthouse \ + cargo auditable build --bin lighthouse \ --features gnosis,slasher-lmdb,slasher-mdbx,slasher-redb,jemalloc-unprefixed \ --profile release \ --locked \ diff --git a/scripts/build_rust_package.sh b/scripts/build_rust_package.sh index 57f1d232..049e2129 100755 --- a/scripts/build_rust_package.sh +++ b/scripts/build_rust_package.sh @@ -21,6 +21,7 @@ build_rust_package() { # Clone the repository local build_dir="$BUILDROOT/build/$package" + local source_url=$git_url mkdir -p "$build_dir" set +x # don't leak github token into logs echo "Cloning ${git_url}" @@ -38,6 +39,12 @@ build_rust_package() { local git_describe=$( git -C "$build_dir" describe --always --long --tags ) printf "${git_describe#$package/}" > "$BUILDDIR/$package.git" + mkdir -p "${ARTIFACTDIR:?}/sbom" + local repository=${source_url#*://*/} + local vcs_url=$(printf 'git+%s@%s' "$source_url" "$(git -C "$build_dir" rev-parse HEAD)" | jq -sRr @uri) + printf 'pkg:generic/%s/%s@%s?arch=%s&vcs_url=%s\n' "${repository%/*}" "$package" "$version" "$DISTRIBUTION_ARCHITECTURE" "$vcs_url" \ + > "$ARTIFACTDIR/sbom/$package.sbom" + # If binary is cached, skip compilation if [ -n "${extra_features}" ]; then local cached_binary="$BUILDDIR/${package}-${git_describe#${package}/}-${extra_features}/${package}" @@ -45,7 +52,7 @@ build_rust_package() { local cached_binary="$BUILDDIR/${package}-${git_describe#${package}/}/${package}" fi local cached_binary="${cached_binary//,/-}" - if [ -f "$cached_binary" ]; then + if [ -f "$cached_binary" ] && grep -aFq .dep-v0 "$cached_binary"; then echo "Using cached binary for $package version $version" if [ -n "${extra_features}" ]; then echo "| \`$package\` | \`$version\` (\`$git_describe\`, features: ${extra_features}) | reused from cache | \`$( du -sh $cached_binary | cut -f1 )\` | |" ">> $BUILDDIR/manifest.md" @@ -83,7 +90,7 @@ build_rust_package() { CARGO_TERM_COLOR='never' cd '/build/$package' cargo fetch - cargo build --release --frozen ${extra_features:+--features $extra_features} --package $cargo_package + cargo auditable build --release --frozen ${extra_features:+--features $extra_features} --package $cargo_package " local seconds=$(( $( date +%s ) - ts )) local duration=$( printf "%dm%ds" $(( seconds / 60 )) $(( seconds % 60 )) ) diff --git a/scripts/make_git_package.sh b/scripts/make_git_package.sh index d5f196f7..977fb93e 100644 --- a/scripts/make_git_package.sh +++ b/scripts/make_git_package.sh @@ -20,6 +20,7 @@ make_git_package() { # Clone the repository local build_dir="$BUILDROOT/build/$package" + local source_url=$git_url set +x # don't leak github token into logs echo "Cloning ${git_url}" if [ -f "$BUILDDIR/.ghtoken" ]; then @@ -36,6 +37,12 @@ make_git_package() { local git_describe=$( git -C "$build_dir" describe --always --long --tags ) printf "${git_describe#$package/}" > "$BUILDDIR/$package.git" + mkdir -p "${ARTIFACTDIR:?}/sbom" + local repository=${source_url#*://*/} + local vcs_url=$(printf 'git+%s@%s' "$source_url" "$(git -C "$build_dir" rev-parse HEAD)" | jq -sRr @uri) + printf 'pkg:generic/%s/%s@%s?arch=%s&vcs_url=%s\n' "${repository%/*}" "$package" "$version" "$DISTRIBUTION_ARCHITECTURE" "$vcs_url" \ + > "$ARTIFACTDIR/sbom/$package.sbom" + local env_hash=$( { # We hash the build_cmd into the cache dir to catch diff --git a/shared/mkosi.conf b/shared/mkosi.conf index eb8c057a..df26dd7c 100644 --- a/shared/mkosi.conf +++ b/shared/mkosi.conf @@ -48,6 +48,7 @@ BuildPackages=build-essential git curl cmake + cargo-auditable pkg-config clang flex diff --git a/shared/mkosi.postinst.d/95-prepare-sbom.sh b/shared/mkosi.postinst.d/95-prepare-sbom.sh new file mode 100755 index 00000000..04b1a57f --- /dev/null +++ b/shared/mkosi.postinst.d/95-prepare-sbom.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Record package metadata before it's removed by CleanPackageMetadata +set -euo pipefail +shopt -s nullglob + +stage="${OUTPUTDIR:?}/.sbom-root" + +# Copy dpkg metadata +mkdir -p "$stage/var/lib/dpkg" "$stage/etc" "$stage/usr/lib/sbom" +cp "${BUILDROOT:?}/var/lib/dpkg/status" "$stage/var/lib/dpkg/status" +cp -L "$BUILDROOT/etc/os-release" "$stage/etc/os-release" +if [[ -d "$BUILDROOT/usr/share/doc" ]]; then + (cd "$BUILDROOT" && find usr/share/doc -maxdepth 2 -name copyright \ + -exec cp --parents -t "$stage" {} +) +fi + +# Create SBOM metadata for deb packages installed manually +debroot=$(mktemp -d "${PACKAGEDIR:?}/.sbom.XXXXXX") +trap 'rm -r "$debroot"' EXIT +for deb in "${PACKAGEDIR:?}"/*.deb; do + package=$(dpkg-deb -f "$deb" Package) + if [[ $(dpkg-query --admindir="$stage/var/lib/dpkg" -W -f='${Status} ${Version}' \ + "$package" 2>/dev/null) == "install ok installed $(dpkg-deb -f "$deb" Version)" ]]; then + ln "$deb" "$debroot/" + sed -i "/^Package: $package$/,/^$/d" "$stage/var/lib/dpkg/status" + fi +done +SYFT_CHECK_FOR_APP_UPDATE=false SYFT_FILE_METADATA_SELECTION=none \ + syft -q scan "dir:$debroot" --override-default-catalogers deb-archive-cataloger \ + -o syft-json >"$stage/usr/lib/sbom/local-debs.syft.json" +SYFT_CHECK_FOR_APP_UPDATE=false SYFT_FILE_METADATA_SELECTION=none \ + syft -q scan "dir:$BUILDROOT" --override-default-catalogers \ + go-module-binary-cataloger,cargo-auditable-binary-cataloger \ + -o syft-json >"$stage/usr/lib/sbom/binaries.syft.json" + +# Add metadata for packages installed from source +if [[ -d "${ARTIFACTDIR:?}/sbom" ]]; then + cp -r "$ARTIFACTDIR/sbom/." "$stage/usr/lib/sbom" +fi diff --git a/shared/mkosi.postoutput.d/95-gen-sbom.sh b/shared/mkosi.postoutput.d/95-gen-sbom.sh new file mode 100755 index 00000000..99353771 --- /dev/null +++ b/shared/mkosi.postoutput.d/95-gen-sbom.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +root="$OUTPUTDIR/.sbom-root" +out="$OUTPUTDIR/${IMAGE_ID}_${IMAGE_VERSION}" + +# Bind the SBOM to the primary image artifact +image="$out.efi" +[[ -f "$out.raw" ]] && image="$out.raw" +sha=$(sha256sum "$image" | cut -d' ' -f1) + +export SYFT_CHECK_FOR_APP_UPDATE=false +export SYFT_FILE_METADATA_SELECTION=none +syft scan "dir:$root" \ + --select-catalogers +sbom-cataloger \ + --source-name "$IMAGE_ID" \ + --source-version "$IMAGE_VERSION" \ + -o cyclonedx-json | + jq --arg file "${image##*/}" --arg sha "$sha" ' + del(.serialNumber) + | del(.components[].properties[]? | select(.name == "syft:cpe23")) + | .metadata.timestamp = "1970-01-01T00:00:00Z" + | .metadata.component += { + "bom-ref": "root", "type": "operating-system", + hashes: [{alg: "SHA-256", content: $sha}], + properties: [{name: "sbom:image-file", value: $file}]}' \ + >"$out.sbom.cdx.json" + +rm -r "$root"