From 6156cd5bd834e0f50f4e4eda732c4493f0f9a775 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Thu, 28 May 2026 23:14:01 +0200 Subject: [PATCH] ci: sign release bundles with FAI_SIGNING_KEY Drop-in from fai/platform .forgejo/workflow-templates/ sign-bundle-keypair.yml. Triggers on v*.*.* tag push: builds wasm32-wasip2, fetches the fai CLI from get.fai.flemming.ai, packs the bundle, ECDSA P-256 signs it against the org-level Forgejo secret FAI_SIGNING_KEY, round-trip-verifies against infra/cosign/official.pub on fai/platform main, and attaches bundle + .sig to the Forgejo Release via the API. Hubs configured with require_signatures: true now accept this module's releases. Without the secret configured the workflow fails fast with the setup instructions in stderr. Signed-off-by: flemming-it --- .forgejo/workflows/sign.yml | 223 ++++++++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 .forgejo/workflows/sign.yml diff --git a/.forgejo/workflows/sign.yml b/.forgejo/workflows/sign.yml new file mode 100644 index 0000000..f30db6b --- /dev/null +++ b/.forgejo/workflows/sign.yml @@ -0,0 +1,223 @@ +# F∆I module bundle signing workflow (cosign key-pinning model). +# +# Drop-in template for any module repo under fai-modules/. +# Triggers on tag push (vX.Y.Z) — builds the module, packs the +# .fai bundle, signs the bundle bytes with the Flemming.AI +# signing key (ECDSA P-256), and attaches bundle + .sig sidecar +# to the matching Forgejo Release. +# +# The hub-side verifier ships in 0.12.0+: when an operator sets +# `require_signatures: true`, install downloads the .sig sidecar +# from `.sig` and verifies it against the pinned store +# public key (built-in for `bundled` / `official`, per-store +# config for everything else). +# +# Adopt by copying this file to `.forgejo/workflows/sign.yml` in +# the module repo and replacing the MODULE_NAME placeholder. +# Add the signing key to Forgejo secrets as FAI_SIGNING_KEY (PEM, +# unencrypted, ECDSA P-256). Rotation is a 5-minute key-roll + +# binary re-release; see infra/cosign/README.md. + +name: sign-bundle + +on: + push: + tags: + - 'v*.*.*' + +env: + CARGO_TERM_COLOR: always + RUST_TOOLCHAIN: "1.86" + CARGO_NET_GIT_FETCH_WITH_CLI: "true" + +jobs: + sign: + runs-on: ubuntu-latest + permissions: + contents: write # attach signature to release + + env: + # The canonical module name as written in module.yaml. + # Used in the bundle filename + wasm path. + MODULE_NAME: text-extract + + steps: + # See fai/platform CI for background on the manual external + # checkout: the DinD runner cannot resolve forgejo:3000, so + # we clone via the external URL with $GITHUB_TOKEN. + - name: Checkout via external URL + run: | + set -eu + mkdir -p "$GITHUB_WORKSPACE" + cd "$GITHUB_WORKSPACE" + git init -q + git remote add origin \ + "https://x-access-token:${GITHUB_TOKEN}@git.flemming.ai/${GITHUB_REPOSITORY}.git" + git fetch --depth=1 origin "$GITHUB_SHA" + git checkout -q FETCH_HEAD + + # The fai-module-sdk dependency lives in another Forgejo org + # behind REQUIRE_SIGNIN_VIEW. MODULE_SDK_PAT (org-level + # secret, read-only on fai/module-sdk) authenticates cargo's + # git fetch via insteadOf. + - name: Configure git URL rewrite for SDK fetch + env: + SDK_PAT: ${{ secrets.MODULE_SDK_PAT }} + run: | + git config --global \ + "url.https://x-access-token:${SDK_PAT}@git.flemming.ai/.insteadOf" \ + "https://git.flemming.ai/" + + - name: Install system dependencies + run: | + apt-get update -qq + apt-get install -y --no-install-recommends \ + curl \ + ca-certificates \ + build-essential \ + pkg-config \ + libssl-dev \ + git \ + jq \ + openssl + + - name: Install Rust toolchain + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y \ + --profile minimal \ + --default-toolchain "$RUST_TOOLCHAIN" \ + --target wasm32-wasip2 + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + + - name: Build module (wasm32-wasip2) + run: cargo build --release --target wasm32-wasip2 + + - name: Install fai CLI + # Pinned to the production channel; signing requires the + # `fai pack` command. Override FAI_VERSION when bundle + # format compatibility matters. + run: | + curl -fsSL https://get.fai.flemming.ai | sh -s -- --no-bootstrap + # The installer drops fai at $HOME/.local/bin + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + export PATH="$HOME/.local/bin:$PATH" + fai --version + + - name: Stage bundle inputs + # The wasm artefact is named after the Cargo package + # (underscored), which may differ from the module name + # in module.yaml (dashed). Resolve it from Cargo.toml so + # the template stays agnostic to the project's naming + # convention. + run: | + set -eu + CARGO_NAME=$(grep -E '^name *= *"' Cargo.toml \ + | head -1 | sed 's/.*"\(.*\)".*/\1/') + mkdir -p staging + cp module.yaml staging/ + cp "target/wasm32-wasip2/release/${CARGO_NAME}.wasm" staging/module.wasm + if [ -f sbom.cdx.json ]; then cp sbom.cdx.json staging/; fi + if [ -f MODULE.md ]; then cp MODULE.md staging/; fi + if [ -f MODULE.de.md ]; then cp MODULE.de.md staging/; fi + + - name: Pack bundle + id: pack + run: | + export PATH="$HOME/.local/bin:$PATH" + BUNDLE="${{ env.MODULE_NAME }}-${{ github.ref_name }}.fai" + fai pack staging --output "$BUNDLE" + echo "bundle=$BUNDLE" >> "$GITHUB_OUTPUT" + ls -la "$BUNDLE" + + - name: Sign bundle (Flemming.AI signing key) + env: + FAI_SIGNING_KEY_PEM: ${{ secrets.FAI_SIGNING_KEY }} + run: | + if [ -z "${FAI_SIGNING_KEY_PEM:-}" ]; then + cat >&2 <<'EOF' + + FAI_SIGNING_KEY secret is empty or undefined. + + This workflow signs module bundles with the Flemming.AI + module-signing key (ECDSA P-256, PKCS#8 PEM, unencrypted). + The matching public key is pinned in the fai-hub binary at + infra/cosign/official.pub — hubs that require_signatures + will reject anything not signed with this key. + + To configure: + 1. Forgejo -> fai-modules org -> Settings -> Secrets + 2. Add secret FAI_SIGNING_KEY (org-level recommended) + 3. Paste the unencrypted PEM (BEGIN PRIVATE KEY ...) + The encrypted source-of-truth lives on Stefan's Mac at + ~/.fai-secrets/flemming-ai-signing.key (passphrase in + mSecure under "F∆I Platform — Module Signing Key (Private)"). + + See fai/platform infra/cosign/OPERATOR-HANDOFF.md for the + full setup runbook and rotation process. + EOF + exit 1 + fi + umask 077 + echo "$FAI_SIGNING_KEY_PEM" > /tmp/signing-key.pem + openssl dgst -sha256 \ + -sign /tmp/signing-key.pem \ + -out "${{ steps.pack.outputs.bundle }}.sig.raw" \ + "${{ steps.pack.outputs.bundle }}" + base64 < "${{ steps.pack.outputs.bundle }}.sig.raw" \ + > "${{ steps.pack.outputs.bundle }}.sig" + rm "${{ steps.pack.outputs.bundle }}.sig.raw" + shred -u /tmp/signing-key.pem || rm -f /tmp/signing-key.pem + ls -la "${{ steps.pack.outputs.bundle }}.sig" + + - name: Round-trip verify + # Sanity-check: the freshly-signed bundle must verify + # against the well-known public key before we publish it. + run: | + curl -fsSL https://git.flemming.ai/fai/platform/raw/branch/main/infra/cosign/official.pub \ + -o /tmp/official.pub + openssl dgst -sha256 \ + -verify /tmp/official.pub \ + -signature <(base64 -d < "${{ steps.pack.outputs.bundle }}.sig") \ + "${{ steps.pack.outputs.bundle }}" + + - name: Attach bundle + signature to Forgejo Release + run: | + set -eu + BUNDLE="${{ steps.pack.outputs.bundle }}" + SIG="$BUNDLE.sig" + REL_JSON=$(curl -sS \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + "https://git.flemming.ai/api/v1/repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}") + REL_ID=$(echo "$REL_JSON" | jq -r '.id // empty') + if [ -z "$REL_ID" ] || [ "$REL_ID" = "null" ]; then + REL_ID=$(curl -sS -X POST \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + -H "Content-Type: application/json" \ + "https://git.flemming.ai/api/v1/repos/${GITHUB_REPOSITORY}/releases" \ + -d "{\"tag_name\":\"${GITHUB_REF_NAME}\",\"name\":\"${GITHUB_REF_NAME}\",\"draft\":false,\"prerelease\":false,\"body\":\"Auto-generated by sign-bundle workflow\"}" \ + | jq -r '.id') + fi + upload_one() { + local asset_path="$1" asset + asset=$(basename "$asset_path") + EXISTING=$(curl -sS \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + "https://git.flemming.ai/api/v1/repos/${GITHUB_REPOSITORY}/releases/${REL_ID}/assets" \ + | jq -r ".[] | select(.name==\"${asset}\") | .id") + if [ -n "$EXISTING" ]; then + curl -sS -X DELETE \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + "https://git.flemming.ai/api/v1/repos/${GITHUB_REPOSITORY}/releases/${REL_ID}/assets/${EXISTING}" \ + || true + fi + curl -sS -X POST \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + -F "attachment=@${asset_path}" \ + "https://git.flemming.ai/api/v1/repos/${GITHUB_REPOSITORY}/releases/${REL_ID}/assets?name=${asset}" \ + > /dev/null + echo "uploaded $asset" + } + upload_one "$BUNDLE" + upload_one "$SIG" + echo "release ${GITHUB_REF_NAME} ready: https://git.flemming.ai/${GITHUB_REPOSITORY}/releases/tag/${GITHUB_REF_NAME}"