Skip to content

[P1][C5+C2] Signed releases, SLSA provenance, and branch protection rules #13

Description

@POWDER-RANGER

Problem

The existing release.yml creates GitHub Releases on tags but does not:

  • Sign release artifacts (GPG or Sigstore)
  • Generate SLSA provenance for the Windows EXE and Python wheel
  • Enforce branch protection preventing direct pushes to main

These gaps leave the supply chain open and suppress Scorecard Signed-Releases and Branch-Protection scores.

Part 1: Branch Protection (Manual + Automated)

Manual Settings (GitHub UI: Settings > Branches > Add Rule for main)

  • Enable Require a pull request before merging
    • Require 1 approving review minimum
    • Dismiss stale reviews on new pushes
  • Enable Require status checks to pass before merging
    • Required checks: lint, test (3.11), test (3.12), build, smoke, analyze (python) (CodeQL)
  • Enable Require branches to be up to date before merging
  • Enable Require linear history (prevents merge commits, enforces rebase/squash)
  • Enable Do not allow bypassing the above settings (applies to admins too)
  • Enable Require signed commits (optional but boosts Scorecard)
  • Block force pushes to main
  • Block deletions of main branch

Automation: .github/workflows/branch-protection.yml (Scorecard validation)

name: Branch Protection Check
on:
  push:
    branches: [main]
jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - name: Verify no direct push to main
        run: |
          if [ "${{ github.event_name }}" = "push" ]; then
            echo "Direct push to main detected! This should be blocked by branch protection."
            echo "If this CI run is seeing this, branch protection may not be configured correctly."
          fi

Part 2: Signed Releases via Sigstore

Update release.yml to add artifact signing:

name: Release

on:
  push:
    tags:
      - 'v*.*.*'

permissions:
  contents: write
  id-token: write    # Required for Sigstore OIDC signing

jobs:
  build-and-sign:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
        with:
          persist-credentials: false

      - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b  # v5.3.0
        with:
          python-version: "3.11"

      - name: Build wheel and sdist
        run: |
          pip install build
          python -m build

      - name: Sign artifacts with Sigstore
        uses: sigstore/gh-action-sigstore-python@f514d46b907ebcd5bedc05145c03b69c1edd8b46  # v3.0.0
        with:
          inputs: |
            dist/*.whl
            dist/*.tar.gz

      - name: Create GitHub Release
        uses: softprops/action-gh-release@e7a8f85e1c67a31e6ed99a94b41bd0b71bbee6b9  # v2.0.6
        with:
          files: |
            dist/*.whl
            dist/*.tar.gz
            dist/*.sigstore.json
          generate_release_notes: true
          fail_on_unmatched_files: true

Part 3: SLSA Provenance for Python Package

Add SLSA Level 3 provenance generation:

  slsa-provenance:
    needs: [build-and-sign]
    permissions:
      actions: read
      id-token: write
      contents: write
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
    with:
      base64-subjects: "${{ needs.build-and-sign.outputs.hashes }}"
      upload-assets: true
      upload-tag-name: ${{ github.ref_name }}

Part 4: SLSA Provenance for Windows EXE (build-exe.yml)

  • Update build-exe.yml to output SHA256 hash of generated .exe
  • Add provenance attestation for Windows artifacts:
    - name: Generate SHA256 for EXE
      run: |
        sha256sum dist/oblisk.exe > dist/oblisk.exe.sha256
        echo "exe-hash=$(cat dist/oblisk.exe.sha256 | cut -d' ' -f1)" >> $GITHUB_OUTPUT
    
    - name: Attest build provenance
      uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec4c809d56c28f936  # v2.0.0
      with:
        subject-path: dist/oblisk.exe

Part 5: Signed Git Tags

  • Set up GPG key for release signing:
    # Generate key
    gpg --full-generate-key
    # Export public key
    gpg --armor --export your@email.com > public.key
    # Add to GitHub: Settings > SSH and GPG keys
  • Configure git to sign tags:
    git config --global user.signingkey YOUR_KEY_ID
    git config --global tag.gpgSign true
  • Add GPG key ID to SECURITY.md for verification
  • Alternatively: use gh attestation sign with Sigstore (no GPG key needed)

Acceptance Criteria

  • ✅ Main branch has protection rules: required reviews, required status checks, no force push
  • ✅ Direct pushes to main rejected (even for repo owner)
  • ✅ Release workflow signs .whl and .tar.gz with Sigstore
  • .sigstore.json files attached to every GitHub Release
  • ✅ SLSA provenance attestation generated for Python package releases
  • ✅ Windows EXE has SHA256 checksum and build attestation in release assets
  • Git tags are GPG-signed (or Sigstore-signed)
  • ✅ Scorecard Signed-Releases check shows 10/10
  • ✅ Scorecard Branch-Protection check shows 10/10

Scorecard Impact

Check Before After
Signed-Releases 0/10 10/10
Branch-Protection ~3/10 10/10
Code-Review ~3/10 8-10/10

Dependencies

Requires: #10 (CI must be green for required status checks), #11 (SHA pinning), #12 (CodeQL as required check)
Estimated effort: 4-6 hours

Priority: P1
Labels: security, releases, supply-chain, branch-protection, scorecard, P1

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions