# 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-anonymize 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}"