fix(security): revert broken padding auth — use explicit length + ct_eq #200
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| # Builds and publishes a multi-arch container image to GHCR. | |
| # | |
| # Triggers and resulting tags (CalVer-on-SemVer scheme, YYYY.0M0D.PATCH): | |
| # - push to main → :main, :sha-<short> | |
| # - pre-release tag → :X.Y.Z-rc.N, :testing, :sha-<short> | |
| # (e.g. 2026.510.0-rc.1; :latest and floating-version tags stay | |
| # on the prior final release) | |
| # - final tag (no -) → :X.Y.Z, :X.Y, :X, :latest, :stable, :sha-<short> | |
| # - pull_request → build-only verification on both arches (no push) | |
| # - workflow_dispatch → manual republish | |
| # | |
| # Architecture: separate native-runner jobs per platform (amd64 on | |
| # ubuntu-latest, arm64 on ubuntu-24.04-arm) push by digest, then a | |
| # merge job assembles the multi-arch manifest, signs it keyless with | |
| # cosign, and attaches a SLSA build-provenance attestation. This is | |
| # materially faster than QEMU emulation for a Rust workload. | |
| on: | |
| push: | |
| branches: [main] | |
| tags: ['[0-9]*'] | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| # Don't cancel a release in flight if a newer commit lands — the | |
| # in-flight build might be the one a user is about to pull. PRs are | |
| # safe to cancel on rebase. | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository_owner }}/gluco-hub | |
| # Deny-all by default. Each job below opts into the narrowest token scope | |
| # it needs — `build` only `packages: write` for digest push, `merge` adds | |
| # `id-token: write` for cosign keyless OIDC and `attestations: write` for | |
| # the SLSA build-provenance attestation. PR runs gate the actual push via | |
| # `if:` on the step, so even though the build job has `packages: write`, | |
| # nothing exercises it on `pull_request`. | |
| permissions: {} | |
| jobs: | |
| build: | |
| name: build (${{ matrix.platform }}) | |
| runs-on: ${{ matrix.runner }} | |
| permissions: | |
| contents: read | |
| packages: write # push image layers by digest (no-op on PRs) | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: linux/amd64 | |
| runner: ubuntu-latest | |
| - platform: linux/arm64 | |
| runner: ubuntu-24.04-arm | |
| steps: | |
| - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| - name: Compute env | |
| run: | | |
| echo "BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_ENV" | |
| # platform "linux/arm64" → "linux-arm64", used as artifact suffix | |
| echo "PLATFORM_PAIR=${PLATFORM//\//-}" >> "$GITHUB_ENV" | |
| env: | |
| PLATFORM: ${{ matrix.platform }} | |
| - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 | |
| - name: Log in to GHCR | |
| if: github.event_name != 'pull_request' | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # Only labels here — tags are computed in the merge job and applied | |
| # via `buildx imagetools create -t <tag> ...`. Per-arch builds push | |
| # by digest and have no human-readable tag of their own. | |
| - name: Compute image labels | |
| id: meta | |
| uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| labels: | | |
| org.opencontainers.image.licenses=AGPL-3.0-or-later | |
| - name: Build & push by digest | |
| id: build | |
| uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 | |
| with: | |
| context: . | |
| platforms: ${{ matrix.platform }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| build-args: | | |
| GLUCO_HUB_GIT_SHA=${{ github.sha }} | |
| BUILD_DATE=${{ env.BUILD_DATE }} | |
| # Per-platform cache scope so the two matrix jobs don't evict | |
| # each other. | |
| cache-from: type=gha,scope=${{ env.PLATFORM_PAIR }} | |
| cache-to: type=gha,mode=max,scope=${{ env.PLATFORM_PAIR }} | |
| # PRs build but don't push. Non-PR runs push by digest only — | |
| # the merge job assembles the multi-arch manifest with proper | |
| # tags. `name-canonical=true` ensures the digest is recorded | |
| # against the canonical image name. | |
| outputs: | | |
| ${{ github.event_name == 'pull_request' | |
| && 'type=cacheonly' | |
| || format('type=image,name={0}/{1},push-by-digest=true,name-canonical=true,push=true', env.REGISTRY, env.IMAGE_NAME) }} | |
| # Provenance default for public repos is mode=max since | |
| # build-push-action v6 — leaving implicit. SBOM must be opted | |
| # into explicitly. | |
| sbom: true | |
| - name: Export digest | |
| if: github.event_name != 'pull_request' | |
| run: | | |
| mkdir -p /tmp/digests | |
| digest="${{ steps.build.outputs.digest }}" | |
| touch "/tmp/digests/${digest#sha256:}" | |
| - name: Upload digest | |
| if: github.event_name != 'pull_request' | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| with: | |
| name: digest-${{ env.PLATFORM_PAIR }} | |
| path: /tmp/digests/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| scan: | |
| name: Container CVE scan | |
| needs: [build] | |
| if: github.event_name == 'push' || github.event_name == 'release' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| security-events: write | |
| steps: | |
| - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 | |
| - name: Build local amd64 image for scanning | |
| run: | | |
| docker build \ | |
| --build-arg GLUCO_HUB_GIT_SHA=${{ github.sha }} \ | |
| --build-arg BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) \ | |
| --platform linux/amd64 \ | |
| -t gluco-hub:scan \ | |
| . | |
| - name: Scan image with Grype | |
| id: grype | |
| uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0 | |
| with: | |
| image: gluco-hub:scan | |
| fail-build: true | |
| severity-cutoff: critical | |
| output-format: sarif | |
| - name: Upload SARIF to GitHub Security | |
| if: always() && steps.grype.outputs.sarif != '' | |
| uses: github/codeql-action/upload-sarif@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3.36.2 | |
| with: | |
| sarif_file: ${{ steps.grype.outputs.sarif }} | |
| category: grype | |
| merge: | |
| name: merge & publish manifest | |
| if: github.event_name != 'pull_request' | |
| needs: build | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write # push manifest list to GHCR | |
| id-token: write # cosign keyless OIDC + attestation signing | |
| attestations: write # GitHub Artifact Attestations | |
| steps: | |
| - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - name: Download digests | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 | |
| with: | |
| path: /tmp/digests | |
| pattern: digest-* | |
| merge-multiple: true | |
| - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 | |
| - name: Log in to GHCR | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # Channel model: | |
| # :main — stable; main-only pushes (release-cuts) | |
| # :testing — pre-release tags (v*-rc/alpha/beta) | |
| # :latest, :stable, floating — final tags only (v2026.510.0, no suffix) | |
| # `latest=false` disables auto-latest so :latest is set exclusively by | |
| # the explicit suffix-filtered raw rule below — no RC ever moves | |
| # :latest, :stable, :2026, or :2026.510. | |
| - name: Compute image metadata | |
| id: meta | |
| uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 | |
| # `buildx imagetools create` operates on the manifest list / OCI | |
| # index and rejects the default `manifest:` annotation prefix | |
| # ("manifest annotations are not supported yet"). Tell metadata- | |
| # action to emit `index:`-prefixed annotations instead, which is | |
| # the level imagetools is actually editing. | |
| env: | |
| DOCKER_METADATA_ANNOTATIONS_LEVELS: index | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| flavor: | | |
| latest=false | |
| labels: | | |
| org.opencontainers.image.description=LibreLink Up → HTTP / Nightscout bridge | |
| org.opencontainers.image.licenses=AGPL-3.0-or-later | |
| tags: | | |
| type=raw,value=main,enable={{is_default_branch}} | |
| type=raw,value=testing,enable=${{ startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '-') }} | |
| type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-') }} | |
| type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-') }} | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref, '-') }} | |
| type=semver,pattern={{major}},enable=${{ !contains(github.ref, '-') }} | |
| type=sha,format=short | |
| - name: Create manifest list & push | |
| working-directory: /tmp/digests | |
| # Build the argv as a bash array so values containing spaces or | |
| # shell-meaningful chars (e.g. "->", "--flag=") cannot be word-split | |
| # into stray flags — closes a CWE-88 argument-injection footgun. | |
| run: | | |
| set -euo pipefail | |
| args=() | |
| while IFS= read -r tag; do | |
| args+=(-t "$tag") | |
| done < <(jq -r '.tags[]' <<< "$DOCKER_METADATA_OUTPUT_JSON") | |
| while IFS= read -r ann; do | |
| args+=(--annotation "$ann") | |
| done < <(jq -r '.annotations[]? // empty' <<< "$DOCKER_METADATA_OUTPUT_JSON") | |
| for digest in *; do | |
| args+=("${REGISTRY}/${IMAGE_NAME}@sha256:${digest}") | |
| done | |
| docker buildx imagetools create "${args[@]}" | |
| - name: Inspect manifest | |
| run: | | |
| docker buildx imagetools inspect \ | |
| ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} | |
| - name: Resolve manifest digest | |
| id: manifest | |
| run: | | |
| digest=$(docker buildx imagetools inspect \ | |
| "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}" \ | |
| --format '{{json .Manifest.Digest}}' | tr -d '"') | |
| echo "digest=${digest}" >> "$GITHUB_OUTPUT" | |
| - name: Install cosign | |
| uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 | |
| # Sign the manifest-list digest. One signature covers every tag | |
| # pointing at the same digest — re-tagging doesn't invalidate it. | |
| - name: Sign image (keyless) | |
| env: | |
| IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| DIGEST: ${{ steps.manifest.outputs.digest }} | |
| run: | | |
| cosign sign --yes \ | |
| -a "repo=${{ github.repository }}" \ | |
| -a "workflow=${{ github.workflow }}" \ | |
| -a "ref=${{ github.sha }}" \ | |
| "${IMAGE}@${DIGEST}" | |
| # Pushes a Sigstore-signed SLSA provenance bundle to the registry, | |
| # discoverable in the GitHub UI and verifiable via: | |
| # gh attestation verify oci://ghcr.io/<owner>/gluco-hub:<tag> \ | |
| # --owner <owner> | |
| - name: Attest build provenance | |
| uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 | |
| with: | |
| subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| subject-digest: ${{ steps.manifest.outputs.digest }} | |
| push-to-registry: true |