From 8eafc81bf3a52a297d786927221454fbd657dbb4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 19:25:26 +0000 Subject: [PATCH 1/2] Initial plan From 0671495002e4fbc11a27b05473bab74d0079a3ea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 19:28:02 +0000 Subject: [PATCH 2/2] feat: add GPG signing workflow, scripts, docs, and .gpg-ignore Agent-Logs-Url: https://github.com/abooker30126/devsecops/sessions/2b123171-3b19-416d-a90f-5f48f86eda92 Co-authored-by: abooker30126 <31778876+abooker30126@users.noreply.github.com> --- .github/SECURITY_KEYS.md | 151 +++++++++++++++++++++++++++ .github/scripts/setup-gpg.sh | 65 ++++++++++++ .github/scripts/sign-files.sh | 73 +++++++++++++ .github/workflows/gpg-sign-files.yml | 107 +++++++++++++++++++ .gpg-ignore | 32 ++++++ 5 files changed, 428 insertions(+) create mode 100644 .github/SECURITY_KEYS.md create mode 100644 .github/scripts/setup-gpg.sh create mode 100644 .github/scripts/sign-files.sh create mode 100644 .github/workflows/gpg-sign-files.yml create mode 100644 .gpg-ignore diff --git a/.github/SECURITY_KEYS.md b/.github/SECURITY_KEYS.md new file mode 100644 index 0000000..5abda55 --- /dev/null +++ b/.github/SECURITY_KEYS.md @@ -0,0 +1,151 @@ +# Security Keys & GPG Signature Guide + +This document explains how to set up GPG signing for this repository, configure the required GitHub Secrets, and verify signatures created by the automated workflow. + +--- + +## Table of Contents + +1. [Prerequisites](#prerequisites) +2. [Generate a GPG Key](#generate-a-gpg-key) +3. [Configure GitHub Secrets](#configure-github-secrets) +4. [Local Git Signing Setup](#local-git-signing-setup) +5. [Verifying File Signatures](#verifying-file-signatures) +6. [Workflow Behaviour](#workflow-behaviour) + +--- + +## Prerequisites + +- GPG (GnuPG) installed locally — `gpg --version` +- Repository admin access to configure GitHub Secrets + +--- + +## Generate a GPG Key + +If you do not yet have a GPG key (or need a new one): + +```bash +gpg --full-generate-key +``` + +Select **RSA and RSA**, key size **4096**, and a suitable expiry. When prompted, enter your name and email. + +List your keys to find the key ID: + +```bash +gpg --list-secret-keys --keyid-format=long +``` + +Example output: + +``` +sec rsa4096/B732B308C0FE0BB3 2024-01-01 [SC] + C8040559438A554CAD747154B732B308C0FE0BB3 +uid [ultimate] Your Name +``` + +The key ID is `B732B308C0FE0BB3`. + +--- + +## Configure GitHub Secrets + +Two secrets are required by the workflow: + +| Secret name | Value | +|-------------------|-----------------------------------------------| +| `GPG_PRIVATE_KEY` | ASCII-armored exported private key (see below)| +| `GPG_PASSPHRASE` | Passphrase protecting the private key | + +### Export the private key + +```bash +gpg --armor --export-secret-keys B732B308C0FE0BB3 +``` + +Copy the full output (including the `-----BEGIN PGP PRIVATE KEY BLOCK-----` header and footer) and paste it as the value of the `GPG_PRIVATE_KEY` secret. + +### Add secrets to GitHub + +1. Open your repository on GitHub. +2. Go to **Settings → Secrets and variables → Actions**. +3. Click **New repository secret** for each secret above. + +--- + +## Local Git Signing Setup + +Run the provided helper script to configure your local environment: + +```bash +bash .github/scripts/setup-gpg.sh +``` + +Or configure manually: + +```bash +git config --global user.signingkey B732B308C0FE0BB3 +git config --global commit.gpgsign true +git config --global tag.gpgsign true +``` + +--- + +## Verifying File Signatures + +Each signed file will have a corresponding `.gpg.sig` file in the same directory. + +### Import the signer's public key + +```bash +gpg --import gpg-public-key.asc +``` + +Or fetch it directly from a key server: + +```bash +gpg --keyserver keyserver.ubuntu.com --recv-keys B732B308C0FE0BB3 +``` + +### Verify a single file + +```bash +gpg --verify path/to/file.sh.gpg.sig path/to/file.sh +``` + +A successful verification looks like: + +``` +gpg: Signature made Mon 01 Jan 2024 00:00:00 UTC +gpg: using RSA key B732B308C0FE0BB3 +gpg: Good signature from "Your Name " +``` + +### Verify all signatures in the repository + +```bash +find . -name '*.gpg.sig' | while read -r sig; do + original="${sig%.gpg.sig}" + echo "Verifying: $original" + gpg --verify "$sig" "$original" && echo " OK" || echo " FAILED" +done +``` + +--- + +## Workflow Behaviour + +The **GPG Sign Modified Files** workflow (`.github/workflows/gpg-sign-files.yml`) runs automatically on every pull request event (`opened`, `synchronize`, `reopened`). + +1. Identifies files modified in the PR that match these extensions: + - `.sh` — shell scripts + - `.py` — Python scripts + - `.yml` / `.yaml` — YAML configuration files + - `.tf` — Terraform files +2. Signs each matching file with a detached ASCII-armored GPG signature. +3. Commits the resulting `.gpg.sig` files back to the PR branch. +4. Posts a comment to the PR summarising which files were signed. + +Files and directories listed in [`.gpg-ignore`](../.gpg-ignore) at the repository root are excluded from signing. diff --git a/.github/scripts/setup-gpg.sh b/.github/scripts/setup-gpg.sh new file mode 100644 index 0000000..d7c8f00 --- /dev/null +++ b/.github/scripts/setup-gpg.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# setup-gpg.sh +# Helper script to configure a local GPG environment for code signing. +# Run this once on your local machine before signing commits or files. +# +# Usage: bash .github/scripts/setup-gpg.sh + +set -euo pipefail + +echo "=== GPG Local Setup ===" + +# ── 1. Check prerequisites ────────────────────────────────────────────────── +if ! command -v gpg &>/dev/null; then + echo "ERROR: gpg is not installed. Install it and re-run this script." >&2 + exit 1 +fi + +# ── 2. List existing secret keys ──────────────────────────────────────────── +echo "" +echo "Existing secret keys:" +gpg --list-secret-keys --keyid-format=long || true + +# ── 3. Optional: import a key from file ───────────────────────────────────── +if [[ -n "${GPG_KEY_FILE:-}" ]]; then + echo "" + echo "Importing key from: $GPG_KEY_FILE" + gpg --import "$GPG_KEY_FILE" +fi + +# ── 4. Select the signing key ──────────────────────────────────────────────── +echo "" +if [[ -z "${GPG_KEY_ID:-}" ]]; then + echo "Enter the GPG key ID you want to use for signing" + echo "(e.g. the 16-character hex ID shown above):" + read -r GPG_KEY_ID +fi + +if [[ -z "$GPG_KEY_ID" ]]; then + echo "ERROR: No GPG key ID provided." >&2 + exit 1 +fi + +echo "Using key: $GPG_KEY_ID" + +# ── 5. Configure git to sign commits with this key ────────────────────────── +git config --global user.signingkey "$GPG_KEY_ID" +git config --global commit.gpgsign true +git config --global tag.gpgsign true +echo "git configured to sign commits and tags with key $GPG_KEY_ID" + +# ── 6. Export the public key (for sharing / uploading to GitHub) ───────────── +PUB_KEY_FILE="gpg-public-key.asc" +gpg --armor --export "$GPG_KEY_ID" > "$PUB_KEY_FILE" +echo "" +echo "Public key exported to: $PUB_KEY_FILE" +echo "Upload this file's contents to GitHub → Settings → SSH and GPG keys." + +# ── 7. Export the private key (for adding to GitHub Secrets) ───────────────── +echo "" +echo "To export your private key for use as the GPG_PRIVATE_KEY GitHub Secret, run:" +echo " gpg --armor --export-secret-keys $GPG_KEY_ID" +echo "" +echo "Then add the output as the GPG_PRIVATE_KEY secret in your repository settings." +echo "" +echo "=== Setup complete ===" diff --git a/.github/scripts/sign-files.sh b/.github/scripts/sign-files.sh new file mode 100644 index 0000000..630d769 --- /dev/null +++ b/.github/scripts/sign-files.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# sign-files.sh +# Signs a single file with GPG and stores the detached signature alongside it. +# Usage: sign-files.sh +# +# Required environment variables: +# GPG_KEY_ID - Key ID to use for signing (set by the workflow) +# GPG_PASSPHRASE - Passphrase for the GPG private key + +set -euo pipefail + +FILE="${1:-}" + +if [[ -z "$FILE" ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +if [[ ! -f "$FILE" ]]; then + echo "File not found: $FILE" >&2 + exit 1 +fi + +# Check whether this file matches a .gpg-ignore pattern +if [[ -f ".gpg-ignore" ]]; then + while IFS= read -r pattern || [[ -n "$pattern" ]]; do + # Skip blank lines and comments + [[ -z "$pattern" || "$pattern" == \#* ]] && continue + # shellcheck disable=SC2053 + if [[ "$FILE" == $pattern ]]; then + echo "Skipping ignored file: $FILE" + exit 0 + fi + done < ".gpg-ignore" +fi + +SIG_FILE="${FILE}.gpg.sig" + +echo "Signing: $FILE" + +# Build the gpg signing command +GPG_SIGN_ARGS=( + --batch + --yes + --detach-sign + --armor + --output "$SIG_FILE" +) + +if [[ -n "${GPG_KEY_ID:-}" ]]; then + GPG_SIGN_ARGS+=(--local-user "$GPG_KEY_ID") +fi + +if [[ -n "${GPG_PASSPHRASE:-}" ]]; then + echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \ + "${GPG_SIGN_ARGS[@]}" "$FILE" +else + gpg "${GPG_SIGN_ARGS[@]}" "$FILE" +fi + +# Verify the signature was created successfully +if [[ -f "$SIG_FILE" ]]; then + echo "Signature created: $SIG_FILE" + if gpg --verify "$SIG_FILE" "$FILE"; then + echo "Signature verified successfully." + else + echo "Signature verification FAILED for $FILE" >&2 + exit 1 + fi +else + echo "ERROR: Signature file was not created for $FILE" >&2 + exit 1 +fi diff --git a/.github/workflows/gpg-sign-files.yml b/.github/workflows/gpg-sign-files.yml new file mode 100644 index 0000000..835fd33 --- /dev/null +++ b/.github/workflows/gpg-sign-files.yml @@ -0,0 +1,107 @@ +name: GPG Sign Modified Files + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: write + pull-requests: write + +jobs: + sign-files: + name: Sign Modified Files with GPG + runs-on: ubuntu-latest + + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install GPG + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq gnupg + + - name: Import GPG private key + env: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + echo "$GPG_PRIVATE_KEY" | gpg --batch --import + # Trust the imported key ultimately + KEY_ID=$(gpg --list-secret-keys --keyid-format=long \ + | grep '^sec' | head -n1 \ + | awk '{print $2}' | cut -d'/' -f2) + echo "${KEY_ID}:6:" | gpg --import-ownertrust + echo "GPG_KEY_ID=$KEY_ID" >> "$GITHUB_ENV" + + - name: Get list of modified files + id: changed-files + run: | + BASE_SHA=${{ github.event.pull_request.base.sha }} + HEAD_SHA=${{ github.event.pull_request.head.sha }} + CHANGED=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" \ + | grep -E '\.(sh|py|yml|yaml|tf)$' || true) + echo "Changed files matching target extensions:" + echo "$CHANGED" + # Store as newline-delimited list in an env file + { + echo "FILES<> "$GITHUB_ENV" + + - name: Sign modified files + if: env.FILES != '' + env: + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + chmod +x .github/scripts/sign-files.sh + echo "$FILES" | tr '\n' '\0' | xargs -0 -I{} \ + bash -c '[ -n "{}" ] && .github/scripts/sign-files.sh "{}"' + + - name: Commit signatures + if: env.FILES != '' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add '*.gpg.sig' + if git diff --cached --quiet; then + echo "No new or changed signatures to commit." + else + git commit -m "chore: add/update GPG signatures for modified files [skip ci]" + git push origin HEAD:${{ github.head_ref }} + fi + + - name: Post signature report to PR + if: env.FILES != '' + uses: actions/github-script@v7 + with: + script: | + const files = process.env.FILES.split('\n').filter(Boolean); + const sigLines = files.map(f => `- \`${f}\` → \`${f}.gpg.sig\``).join('\n'); + const body = [ + '## 🔏 GPG Signature Report', + '', + 'The following files were signed with GPG on this PR:', + '', + sigLines, + '', + 'To verify a signature locally, run:', + '```bash', + 'gpg --verify .gpg.sig ', + '```', + '', + 'See [`.github/SECURITY_KEYS.md`](.github/SECURITY_KEYS.md) for full verification instructions.', + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); diff --git a/.gpg-ignore b/.gpg-ignore new file mode 100644 index 0000000..7bea6ae --- /dev/null +++ b/.gpg-ignore @@ -0,0 +1,32 @@ +# .gpg-ignore +# Files and directories to skip during GPG signing. +# Supports shell glob patterns (matched against the file path from repo root). +# Lines starting with # are comments. + +# Dependency and vendor directories +node_modules/** +vendor/** +.venv/** +__pycache__/** + +# Signature files themselves (avoid signing signatures) +*.gpg.sig + +# Compiled / generated output +*.pyc +dist/** +build/** +*.tfstate +*.tfstate.backup +.terraform/** + +# OS and editor artefacts +.DS_Store +*.swp +*.swo +*~ + +# Secrets and local-only configs (should never be committed anyway) +*.env +.env.* +secrets/**