From db2f57fdd2223c2d2bf8f38b57f88897819cda32 Mon Sep 17 00:00:00 2001 From: Vladimir Dubrovin <3proxy@3proxy.ru> Date: Fri, 21 Aug 2026 11:07:54 +0300 Subject: [PATCH] Sign packages, publish checksums, attest before upload, narrow permissions GPG signing of rpm packages and detached signatures for deb packages and for the SHA256SUMS files, using GPG_PRIVATE_KEY / GPG_PASSPHRASE secrets. Checksums are published as release assets. Build provenance is attested before the assets are uploaded, so a failed attestation does not leave unattested files published. Workflow permissions are read-only, write permissions are requested per job only where they are used. --- .github/workflows/build-rpm-arm64.yml | 61 +++++++++++++++++++------ .github/workflows/build-rpm-armhf.yml | 61 +++++++++++++++++++------ .github/workflows/build-rpm-x86-64.yml | 63 ++++++++++++++++++++------ .github/workflows/build-watcom.yml | 30 +++++++++--- .github/workflows/build-win32.yml | 30 +++++++++--- .github/workflows/build-win64.yml | 30 +++++++++--- .github/workflows/build-winarm64.yml | 30 +++++++++--- .github/workflows/docker.yml | 11 +++-- SECURITY.md | 42 +++++++++++++++++ 9 files changed, 287 insertions(+), 71 deletions(-) diff --git a/.github/workflows/build-rpm-arm64.yml b/.github/workflows/build-rpm-arm64.yml index f975b9d..7a56621 100644 --- a/.github/workflows/build-rpm-arm64.yml +++ b/.github/workflows/build-rpm-arm64.yml @@ -6,12 +6,14 @@ on: workflow_dispatch: permissions: - contents: write - id-token: write - attestations: write + contents: read jobs: ci: + permissions: + contents: write + id-token: write + attestations: write name: "${{ matrix.target }}" strategy: matrix: @@ -51,12 +53,6 @@ jobs: with: name: "3proxy-${{ env.RELEASE }}-arm64.rpm" path: "*.rpm" - - name: Upload rpm to release - if: github.event_name == 'release' - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ github.event.release.tag_name }} - run: gh release upload "$TAG" 3proxy-${{ env.RELEASE }}.arm64.rpm - name: debbuild run: | ret=`pwd` @@ -77,12 +73,45 @@ jobs: with: name: "3proxy-${{ env.RELEASE }}-arm64.deb" path: "*.deb" - - name: Upload deb to release + - name: Import signing key if: github.event_name == 'release' env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ github.event.release.tag_name }} - run: gh release upload "$TAG" 3proxy-${{ env.RELEASE }}.arm64.deb + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + if [ -z "$GPG_PRIVATE_KEY" ]; then echo "GPG_PRIVATE_KEY is not set"; exit 1; fi + mkdir -p ~/.gnupg && chmod 700 ~/.gnupg + printf 'allow-loopback-pinentry\ndefault-cache-ttl 7200\nmax-cache-ttl 7200\n' > ~/.gnupg/gpg-agent.conf + gpgconf --kill gpg-agent || true + printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import + KEYID=$(gpg --list-secret-keys --with-colons | awk -F: '/^sec:/{print $5; exit}') + echo "GPG_KEYID=$KEYID" >> $GITHUB_ENV + echo prime > /tmp/prime.txt + gpg --batch --yes --pinentry-mode loopback --passphrase "$GPG_PASSPHRASE" \ + -u "$KEYID" --detach-sign -o /dev/null /tmp/prime.txt + rm -f /tmp/prime.txt + - name: Sign rpm + if: github.event_name == 'release' + run: | + echo "%_gpg_name $GPG_KEYID" > ~/.rpmmacros + rpm --addsign *.rpm + for f in *.rpm; do + sig=$(rpm -qp --qf '%{RSAHEADER:pgpsig}' "$f" 2>/dev/null) + case "$sig" in ""|"(none)") echo "$f is not signed"; exit 1 ;; esac + echo "$f: $sig" + done + - name: Checksums and detached signatures + if: github.event_name == 'release' + env: + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + sha256sum *.rpm *.deb > SHA256SUMS-arm64 + for f in *.deb SHA256SUMS-arm64; do + gpg --batch --yes --pinentry-mode loopback --passphrase "$GPG_PASSPHRASE" \ + -u "$GPG_KEYID" --armor --detach-sign "$f" + done + sha256sum -c SHA256SUMS-arm64 + gpg --verify SHA256SUMS-arm64.asc SHA256SUMS-arm64 - name: Attest build provenance if: github.event_name == 'release' uses: actions/attest-build-provenance@v2 @@ -90,3 +119,9 @@ jobs: subject-path: | *.rpm *.deb + - name: Upload to release + if: github.event_name == 'release' + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.event.release.tag_name }} + run: gh release upload "$TAG" *.rpm *.deb *.deb.asc SHA256SUMS-arm64 SHA256SUMS-arm64.asc diff --git a/.github/workflows/build-rpm-armhf.yml b/.github/workflows/build-rpm-armhf.yml index fe3291f..28668ae 100644 --- a/.github/workflows/build-rpm-armhf.yml +++ b/.github/workflows/build-rpm-armhf.yml @@ -6,12 +6,14 @@ on: workflow_dispatch: permissions: - contents: write - id-token: write - attestations: write + contents: read jobs: ci: + permissions: + contents: write + id-token: write + attestations: write name: "${{ matrix.target }}" strategy: matrix: @@ -78,12 +80,6 @@ jobs: with: name: "3proxy-${{ env.RELEASE }}-arm.rpm" path: "*.rpm" - - name: Upload rpm to release - if: github.event_name == 'release' - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ github.event.release.tag_name }} - run: gh release upload "$TAG" 3proxy-${{ env.RELEASE }}.arm.rpm - name: debbuild run: | ret=`pwd` @@ -110,12 +106,45 @@ jobs: with: name: "3proxy-${{ env.RELEASE }}-arm.deb" path: "*.deb" - - name: Upload deb to release + - name: Import signing key if: github.event_name == 'release' env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ github.event.release.tag_name }} - run: gh release upload "$TAG" 3proxy-${{ env.RELEASE }}.arm.deb + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + if [ -z "$GPG_PRIVATE_KEY" ]; then echo "GPG_PRIVATE_KEY is not set"; exit 1; fi + mkdir -p ~/.gnupg && chmod 700 ~/.gnupg + printf 'allow-loopback-pinentry\ndefault-cache-ttl 7200\nmax-cache-ttl 7200\n' > ~/.gnupg/gpg-agent.conf + gpgconf --kill gpg-agent || true + printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import + KEYID=$(gpg --list-secret-keys --with-colons | awk -F: '/^sec:/{print $5; exit}') + echo "GPG_KEYID=$KEYID" >> $GITHUB_ENV + echo prime > /tmp/prime.txt + gpg --batch --yes --pinentry-mode loopback --passphrase "$GPG_PASSPHRASE" \ + -u "$KEYID" --detach-sign -o /dev/null /tmp/prime.txt + rm -f /tmp/prime.txt + - name: Sign rpm + if: github.event_name == 'release' + run: | + echo "%_gpg_name $GPG_KEYID" > ~/.rpmmacros + rpm --addsign *.rpm + for f in *.rpm; do + sig=$(rpm -qp --qf '%{RSAHEADER:pgpsig}' "$f" 2>/dev/null) + case "$sig" in ""|"(none)") echo "$f is not signed"; exit 1 ;; esac + echo "$f: $sig" + done + - name: Checksums and detached signatures + if: github.event_name == 'release' + env: + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + sha256sum *.rpm *.deb > SHA256SUMS-arm + for f in *.deb SHA256SUMS-arm; do + gpg --batch --yes --pinentry-mode loopback --passphrase "$GPG_PASSPHRASE" \ + -u "$GPG_KEYID" --armor --detach-sign "$f" + done + sha256sum -c SHA256SUMS-arm + gpg --verify SHA256SUMS-arm.asc SHA256SUMS-arm - name: Attest build provenance if: github.event_name == 'release' uses: actions/attest-build-provenance@v2 @@ -123,3 +152,9 @@ jobs: subject-path: | *.rpm *.deb + - name: Upload to release + if: github.event_name == 'release' + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.event.release.tag_name }} + run: gh release upload "$TAG" *.rpm *.deb *.deb.asc SHA256SUMS-arm SHA256SUMS-arm.asc diff --git a/.github/workflows/build-rpm-x86-64.yml b/.github/workflows/build-rpm-x86-64.yml index 2e19720..24a9fa3 100644 --- a/.github/workflows/build-rpm-x86-64.yml +++ b/.github/workflows/build-rpm-x86-64.yml @@ -6,12 +6,14 @@ on: workflow_dispatch: permissions: - contents: write - id-token: write - attestations: write + contents: read jobs: ci: + permissions: + contents: write + id-token: write + attestations: write name: "${{ matrix.target }}" strategy: matrix: @@ -51,12 +53,6 @@ jobs: with: name: "3proxy-${{ env.RELEASE }}-x86_64.rpm" path: "*.rpm" - - name: Upload rpm to release - if: github.event_name == 'release' - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ github.event.release.tag_name }} - run: gh release upload "$TAG" 3proxy-${{ env.RELEASE }}.x86_64.rpm - name: debbuild run: | ret=`pwd` @@ -77,13 +73,46 @@ jobs: with: name: "3proxy-${{ env.RELEASE }}-x86_64.deb" path: "*.deb" - - name: Upload deb to release + + - name: Import signing key if: github.event_name == 'release' env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ github.event.release.tag_name }} - run: gh release upload "$TAG" 3proxy-${{ env.RELEASE }}.x86_64.deb - + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + if [ -z "$GPG_PRIVATE_KEY" ]; then echo "GPG_PRIVATE_KEY is not set"; exit 1; fi + mkdir -p ~/.gnupg && chmod 700 ~/.gnupg + printf 'allow-loopback-pinentry\ndefault-cache-ttl 7200\nmax-cache-ttl 7200\n' > ~/.gnupg/gpg-agent.conf + gpgconf --kill gpg-agent || true + printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import + KEYID=$(gpg --list-secret-keys --with-colons | awk -F: '/^sec:/{print $5; exit}') + echo "GPG_KEYID=$KEYID" >> $GITHUB_ENV + echo prime > /tmp/prime.txt + gpg --batch --yes --pinentry-mode loopback --passphrase "$GPG_PASSPHRASE" \ + -u "$KEYID" --detach-sign -o /dev/null /tmp/prime.txt + rm -f /tmp/prime.txt + - name: Sign rpm + if: github.event_name == 'release' + run: | + echo "%_gpg_name $GPG_KEYID" > ~/.rpmmacros + rpm --addsign *.rpm + for f in *.rpm; do + sig=$(rpm -qp --qf '%{RSAHEADER:pgpsig}' "$f" 2>/dev/null) + case "$sig" in ""|"(none)") echo "$f is not signed"; exit 1 ;; esac + echo "$f: $sig" + done + - name: Checksums and detached signatures + if: github.event_name == 'release' + env: + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + sha256sum *.rpm *.deb > SHA256SUMS-x86_64 + for f in *.deb SHA256SUMS-x86_64; do + gpg --batch --yes --pinentry-mode loopback --passphrase "$GPG_PASSPHRASE" \ + -u "$GPG_KEYID" --armor --detach-sign "$f" + done + sha256sum -c SHA256SUMS-x86_64 + gpg --verify SHA256SUMS-x86_64.asc SHA256SUMS-x86_64 - name: Attest build provenance if: github.event_name == 'release' uses: actions/attest-build-provenance@v2 @@ -91,3 +120,9 @@ jobs: subject-path: | *.rpm *.deb + - name: Upload to release + if: github.event_name == 'release' + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.event.release.tag_name }} + run: gh release upload "$TAG" *.rpm *.deb *.deb.asc SHA256SUMS-x86_64 SHA256SUMS-x86_64.asc diff --git a/.github/workflows/build-watcom.yml b/.github/workflows/build-watcom.yml index 5b17146..70d74a4 100644 --- a/.github/workflows/build-watcom.yml +++ b/.github/workflows/build-watcom.yml @@ -6,12 +6,14 @@ on: workflow_dispatch: permissions: - contents: write - id-token: write - attestations: write + contents: read jobs: ci: + permissions: + contents: write + id-token: write + attestations: write name: "${{ matrix.target }}" strategy: matrix: @@ -88,16 +90,30 @@ jobs: if: github.event_name == 'release' shell: pwsh run: Compress-Archive -Path dist/* -DestinationPath 3proxy-${{ env.RELEASE }}-lite.zip - - name: Upload to release + - name: Checksums and detached signature if: github.event_name == 'release' shell: bash env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ github.event.release.tag_name }} - run: gh release upload "$TAG" 3proxy-${{ env.RELEASE }}-lite.zip + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + if [ -z "$GPG_PRIVATE_KEY" ]; then echo "GPG_PRIVATE_KEY is not set"; exit 1; fi + printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import + KEYID=$(gpg --list-secret-keys --with-colons | awk -F: '/^sec:/{print $5; exit}') + sha256sum *.zip > SHA256SUMS-win-lite + gpg --batch --yes --pinentry-mode loopback --passphrase "$GPG_PASSPHRASE" \ + -u "$KEYID" --armor --detach-sign SHA256SUMS-win-lite + sha256sum -c SHA256SUMS-win-lite - name: Attest build provenance if: github.event_name == 'release' uses: actions/attest-build-provenance@v2 with: subject-path: | *.zip + - name: Upload to release + if: github.event_name == 'release' + shell: bash + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.event.release.tag_name }} + run: gh release upload "$TAG" *.zip SHA256SUMS-win-lite SHA256SUMS-win-lite.asc diff --git a/.github/workflows/build-win32.yml b/.github/workflows/build-win32.yml index 141283b..458a746 100644 --- a/.github/workflows/build-win32.yml +++ b/.github/workflows/build-win32.yml @@ -6,12 +6,14 @@ on: workflow_dispatch: permissions: - contents: write - id-token: write - attestations: write + contents: read jobs: ci: + permissions: + contents: write + id-token: write + attestations: write name: "${{ matrix.target }}" strategy: matrix: @@ -103,16 +105,30 @@ jobs: if: github.event_name == 'release' shell: pwsh run: Compress-Archive -Path dist/* -DestinationPath 3proxy-${{ env.RELEASE }}-x86.zip - - name: Upload to release + - name: Checksums and detached signature if: github.event_name == 'release' shell: bash env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ github.event.release.tag_name }} - run: gh release upload "$TAG" 3proxy-${{ env.RELEASE }}-x86.zip + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + if [ -z "$GPG_PRIVATE_KEY" ]; then echo "GPG_PRIVATE_KEY is not set"; exit 1; fi + printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import + KEYID=$(gpg --list-secret-keys --with-colons | awk -F: '/^sec:/{print $5; exit}') + sha256sum *.zip > SHA256SUMS-win-x86 + gpg --batch --yes --pinentry-mode loopback --passphrase "$GPG_PASSPHRASE" \ + -u "$KEYID" --armor --detach-sign SHA256SUMS-win-x86 + sha256sum -c SHA256SUMS-win-x86 - name: Attest build provenance if: github.event_name == 'release' uses: actions/attest-build-provenance@v2 with: subject-path: | *.zip + - name: Upload to release + if: github.event_name == 'release' + shell: bash + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.event.release.tag_name }} + run: gh release upload "$TAG" *.zip SHA256SUMS-win-x86 SHA256SUMS-win-x86.asc diff --git a/.github/workflows/build-win64.yml b/.github/workflows/build-win64.yml index aeb51c2..676301c 100644 --- a/.github/workflows/build-win64.yml +++ b/.github/workflows/build-win64.yml @@ -6,12 +6,14 @@ on: workflow_dispatch: permissions: - contents: write - id-token: write - attestations: write + contents: read jobs: ci: + permissions: + contents: write + id-token: write + attestations: write name: "${{ matrix.target }}" strategy: matrix: @@ -104,16 +106,30 @@ jobs: if: github.event_name == 'release' shell: pwsh run: Compress-Archive -Path dist/* -DestinationPath 3proxy-${{ env.RELEASE }}-x64.zip - - name: Upload to release + - name: Checksums and detached signature if: github.event_name == 'release' shell: bash env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ github.event.release.tag_name }} - run: gh release upload "$TAG" 3proxy-${{ env.RELEASE }}-x64.zip + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + if [ -z "$GPG_PRIVATE_KEY" ]; then echo "GPG_PRIVATE_KEY is not set"; exit 1; fi + printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import + KEYID=$(gpg --list-secret-keys --with-colons | awk -F: '/^sec:/{print $5; exit}') + sha256sum *.zip > SHA256SUMS-win-x64 + gpg --batch --yes --pinentry-mode loopback --passphrase "$GPG_PASSPHRASE" \ + -u "$KEYID" --armor --detach-sign SHA256SUMS-win-x64 + sha256sum -c SHA256SUMS-win-x64 - name: Attest build provenance if: github.event_name == 'release' uses: actions/attest-build-provenance@v2 with: subject-path: | *.zip + - name: Upload to release + if: github.event_name == 'release' + shell: bash + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.event.release.tag_name }} + run: gh release upload "$TAG" *.zip SHA256SUMS-win-x64 SHA256SUMS-win-x64.asc diff --git a/.github/workflows/build-winarm64.yml b/.github/workflows/build-winarm64.yml index 600967a..28ab904 100644 --- a/.github/workflows/build-winarm64.yml +++ b/.github/workflows/build-winarm64.yml @@ -6,12 +6,14 @@ on: workflow_dispatch: permissions: - contents: write - id-token: write - attestations: write + contents: read jobs: ci: + permissions: + contents: write + id-token: write + attestations: write name: "${{ matrix.target }}" strategy: matrix: @@ -103,16 +105,30 @@ jobs: if: github.event_name == 'release' shell: pwsh run: Compress-Archive -Path dist/* -DestinationPath 3proxy-${{ env.RELEASE }}-arm64.zip - - name: Upload to release + - name: Checksums and detached signature if: github.event_name == 'release' shell: bash env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ github.event.release.tag_name }} - run: gh release upload "$TAG" 3proxy-${{ env.RELEASE }}-arm64.zip + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + if [ -z "$GPG_PRIVATE_KEY" ]; then echo "GPG_PRIVATE_KEY is not set"; exit 1; fi + printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import + KEYID=$(gpg --list-secret-keys --with-colons | awk -F: '/^sec:/{print $5; exit}') + sha256sum *.zip > SHA256SUMS-win-arm64 + gpg --batch --yes --pinentry-mode loopback --passphrase "$GPG_PASSPHRASE" \ + -u "$KEYID" --armor --detach-sign SHA256SUMS-win-arm64 + sha256sum -c SHA256SUMS-win-arm64 - name: Attest build provenance if: github.event_name == 'release' uses: actions/attest-build-provenance@v2 with: subject-path: | *.zip + - name: Upload to release + if: github.event_name == 'release' + shell: bash + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.event.release.tag_name }} + run: gh release upload "$TAG" *.zip SHA256SUMS-win-arm64 SHA256SUMS-win-arm64.asc diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index adbb129..b1390d2 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -7,9 +7,6 @@ on: permissions: contents: read - packages: write - id-token: write - attestations: write env: DOCKERHUB_IMAGE: docker.io/3proxy/3proxy @@ -18,6 +15,9 @@ env: jobs: build: name: ${{ matrix.image }} ${{ matrix.platform }} + permissions: + contents: read + packages: write runs-on: ${{ matrix.runner }} strategy: fail-fast: false @@ -81,6 +81,11 @@ jobs: publish: name: Publish ${{ matrix.image }} + permissions: + contents: read + packages: write + id-token: write + attestations: write runs-on: ubuntu-latest needs: build strategy: diff --git a/SECURITY.md b/SECURITY.md index 3e04ca6..784994f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,3 +12,45 @@ Report to 3proxy@3proxy.org or via [GitHub security reporting](https://github.com/3proxy/3proxy/security) For High/Critical patched version is released within 2 weeks + +## Verifying downloads + +Release binaries are published with SHA256 checksums, an OpenPGP signature and +a GitHub build provenance attestation. + +The release signing key is `3proxy-release-key.asc` in the root of this +repository. Import it once: + +``` +gpg --import 3proxy-release-key.asc +``` + +Checksums and the checksum file signature: + +``` +gpg --verify SHA256SUMS-x86_64.asc SHA256SUMS-x86_64 +sha256sum -c SHA256SUMS-x86_64 +``` + +RPM packages are signed, the signature is checked by rpm itself: + +``` +sudo rpm --import 3proxy-release-key.asc +rpm -K 3proxy-0.9.9.x86_64.rpm +``` + +DEB packages are published with a detached signature: + +``` +gpg --verify 3proxy-0.9.9.x86_64.deb.asc 3proxy-0.9.9.x86_64.deb +``` + +Build provenance (which workflow, commit and runner produced the file) is +verified with the GitHub CLI: + +``` +gh attestation verify 3proxy-0.9.9.x86_64.rpm --owner 3proxy +gh attestation verify oci://docker.io/3proxy/3proxy:lts --owner 3proxy +``` + +Windows binaries are Authenticode signed in addition to the above.