Skip to content

feat: Verdict reproducibility (discussion #56): An identical re-scan … #89

feat: Verdict reproducibility (discussion #56): An identical re-scan …

feat: Verdict reproducibility (discussion #56): An identical re-scan … #89

Workflow file for this run

name: ci
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
jobs:
# Fail the build if go.sum
# integrity is off, or if vendor/ has drifted from go.mod/go.sum (e.g. the dep
# was bumped but `go mod vendor` wasn't re-run and committed).
vendor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod # tracks the toolchain in go.mod, no hardcoding
- name: verify module checksums against go.sum
run: go mod verify
- name: vendor/ is in sync with go.mod / go.sum
run: |
go mod tidy
go mod vendor
if ! git diff --exit-code -- go.mod go.sum vendor/; then
echo "::error::go.mod/go.sum/vendor are out of date. Run 'go mod tidy && go mod vendor' and commit the result."
exit 1
fi
# Runs on every push and PR.
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history + tags so `git describe` works
- uses: actions/setup-go@v5
with:
go-version: "1.22"
- run: go vet ./...
- run: go build ./...
- run: go test ./...
# Builds release artifacts as hardened static-PIE binaries (PIE + full RELRO,
# via the external linker; netgo+osusergo keep them static and portable). No
# UPX: it strips PIE/RELRO, trips antivirus, and hurts reproducibility for a
# marginal size win on an on-demand CLI (#30). On a v* tag it also creates the
# GitHub Release; otherwise artifacts are just uploaded for inspection.
release:
needs: test
runs-on: ubuntu-latest
permissions:
contents: write # required to create the Release and upload assets
strategy:
matrix:
goarch: [amd64, arm64]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # tags needed for version stamping
- uses: actions/setup-go@v5
with:
go-version: "1.22"
- name: derive version
id: ver
run: |
VERSION="$(git describe --tags --always --dirty 2>/dev/null || echo dev)"
COMMIT="$(git rev-parse --short=12 HEAD 2>/dev/null || true)"
DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "commit=$COMMIT" >> "$GITHUB_OUTPUT"
echo "date=$DATE" >> "$GITHUB_OUTPUT"
echo "Building $VERSION ($COMMIT)"
- name: install C toolchain (external linker → PIE + full RELRO)
run: |
sudo apt-get update
sudo apt-get install -y gcc binutils
if [ "${{ matrix.goarch }}" = "arm64" ]; then
sudo apt-get install -y gcc-aarch64-linux-gnu libc6-dev-arm64-cross
fi
- name: build hardened static binary (PIE + full RELRO, version-stamped)
env:
# CGO is enabled only to drive the external linker (needed for
# DT_BIND_NOW / full RELRO). netgo+osusergo force Go's pure resolver
# and user lookup, so the result stays fully static and portable —
# no glibc NSS at runtime. -static-pie gives a position-independent
# static ELF (ET_DYN) that satisfies namcap's PIE and FULL RELRO
# checks (#30).
CGO_ENABLED: "1"
GOOS: linux
GOARCH: ${{ matrix.goarch }}
CC: ${{ matrix.goarch == 'arm64' && 'aarch64-linux-gnu-gcc' || 'gcc' }}
PKG: github.com/manticore-projects/aurscan/internal/version
run: |
go build -trimpath -buildmode=pie -tags 'netgo osusergo' \
-ldflags="-s -w -linkmode=external \
-extldflags '-static-pie -Wl,-z,relro -Wl,-z,now' \
-X ${PKG}.Version=${{ steps.ver.outputs.version }} \
-X ${PKG}.Commit=${{ steps.ver.outputs.commit }} \
-X ${PKG}.Date=${{ steps.ver.outputs.date }}" \
-o aurscan-linux-${{ matrix.goarch }} ./cmd/aurscan
- name: verify hardening (PIE + full RELRO + static)
run: |
bin=aurscan-linux-${{ matrix.goarch }}
readelf -h "$bin" | grep -q 'DYN (Position-Independent' || { echo "::error::$bin is not PIE (ET_DYN)"; exit 1; }
readelf -d "$bin" | grep -qi 'BIND_NOW' || { echo "::error::$bin lacks full RELRO (no BIND_NOW)"; exit 1; }
file "$bin" | grep -q 'static-pie' || { echo "::error::$bin is not static-pie"; exit 1; }
echo "$bin: PIE + full RELRO + static-pie — ok"
- name: smoke test (amd64 only — the runner is amd64)
if: matrix.goarch == 'amd64'
run: ./aurscan-linux-amd64 --version
- name: upload artifact
uses: actions/upload-artifact@v4
with:
name: aurscan-linux-${{ matrix.goarch }}
path: aurscan-linux-${{ matrix.goarch }}
- name: attach to release
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v2
with:
files: aurscan-linux-${{ matrix.goarch }}
# Derive the signing key from the imported secret material instead of
# trusting GPG_KEY_ID to match (the previous "No secret key" failure), prints a
# secret-free diagnostic, and fails clearly if GPG_PRIVATE_KEY carried no secret.
sign:
needs: release
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: download built binaries
uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: generate SHA256SUMS
working-directory: dist
run: |
sha256sum aurscan-linux-amd64 aurscan-linux-arm64 > SHA256SUMS
echo "---- SHA256SUMS ----"; cat SHA256SUMS
- name: import signing key
env:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
run: |
if [ -z "$GPG_PRIVATE_KEY" ]; then
echo "::error::GPG_PRIVATE_KEY secret is not set"; exit 1
fi
export GNUPGHOME="$(mktemp -d)"
echo "GNUPGHOME=$GNUPGHOME" >> "$GITHUB_ENV"
echo "allow-loopback-pinentry" > "$GNUPGHOME/gpg-agent.conf"
gpgconf --kill gpg-agent || true
printf '%s\n' "$GPG_PRIVATE_KEY" | gpg --batch --import
# Fail loudly if the export contained no SECRET key (e.g. a public key
# was exported by mistake) — this is the usual root of "No secret key".
if [ "$(gpg --list-secret-keys --with-colons | grep -c '^sec')" -eq 0 ]; then
echo "::error::GPG_PRIVATE_KEY imported no secret key. Re-export with"
echo "::error:: gpg --armor --export-secret-keys <KEYID> (or --export-secret-subkeys <SUBKEYID>!)"
exit 1
fi
# Secret-free diagnostic: which key(s) and capabilities did we get?
echo "---- imported secret keys ----"
gpg --list-secret-keys --keyid-format=long | grep -E 'sec|ssb|uid'
# Resolve the signing key. If GPG_KEY_ID is set it must match an
# imported secret key; otherwise auto-derive the first secret key's
# primary fingerprint (gpg then auto-selects its signing subkey).
want='${{ secrets.GPG_KEY_ID }}'
if [ -n "$want" ]; then
if ! gpg --list-secret-keys --with-colons | grep -qiE ":(${want#0x}):"; then
echo "::error::GPG_KEY_ID ($want) does not match any imported secret key (see list above)."
echo "::error::Fix GPG_KEY_ID, or unset it to auto-select the imported key."
exit 1
fi
key="$want"
else
key="$(gpg --list-secret-keys --with-colons | awk -F: '$1=="fpr"{print $10; exit}')"
fi
echo "SIGN_KEY=$key" >> "$GITHUB_ENV"
echo "Will sign with: $key"
- name: sign SHA256SUMS (detached, armored)
working-directory: dist
env:
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
run: |
gpg --batch --yes --pinentry-mode loopback \
${GPG_PASSPHRASE:+--passphrase "$GPG_PASSPHRASE"} \
--local-user "$SIGN_KEY" \
--armor --detach-sign --output SHA256SUMS.asc SHA256SUMS
gpg --verify SHA256SUMS.asc SHA256SUMS
- name: attach checksums + signature to the release
uses: softprops/action-gh-release@v2
with:
files: |
dist/SHA256SUMS
dist/SHA256SUMS.asc