Round-3 PR re-review follow-ups: LOW — CODEOWNERS missed /scripts/build-plugins-index.mjs and /wrangler.toml. Both can change the bytes that get signed without touching the sign step. Replaced the per-file enumeration with /scripts/ catch-all and added wrangler.toml. HIGH — workflow comment said the post-sign verify step "catches a buggy or tampered sign-script run". The "tampered" claim was wrong: any attacker who can edit sign-plugins-index.mjs in a PR can edit the verify step (and the embedded pubkey) in the same diff. Re-stated as "catches accidental regressions only — CODEOWNERS is what stops adversarial edits". Branch protection on main has been enabled separately via gh API (force-push + deletion blocked, PR review required, CODEOWNERS enforced; bypass for repo owner and github-actions[bot] so the auto-publish workflow keeps working).
153 lines
6.1 KiB
YAML
153 lines
6.1 KiB
YAML
name: Refresh registry-worker cache
|
|
|
|
# On every push that changes any registry content, regenerate the two
|
|
# in-repo indexes the registry-worker ingests:
|
|
# plugins-index.json — daemon-shaped flat plugins array (signed)
|
|
# registry-index.json — dict-shaped dashboard payload (unsigned)
|
|
# then poke the worker's forced-refresh endpoint so it pulls both
|
|
# (3 subrequests total, regardless of registry size — fits Workers
|
|
# Free's 50-subrequest budget) and stores them.
|
|
#
|
|
# Without this, dashboard + daemon would have to wait for the next
|
|
# 02:00 UTC cron tick to see content changes (up to ~24h delay).
|
|
#
|
|
# === SECURITY MODEL ===
|
|
#
|
|
# REGISTRY_PRIVATE_KEY (Ed25519 PKCS#8) is the trust root for every
|
|
# `librefang plugin install <name>` worldwide. Threats and mitigations:
|
|
#
|
|
# 1. Compromised maintainer pushes a malicious plugins-index.json
|
|
# directly to main. Mitigation: GitHub branch protection on `main`
|
|
# requires PR review (admin-configured); .github/CODEOWNERS forces
|
|
# sign-script and committed-artefact changes through the registry
|
|
# owner.
|
|
#
|
|
# 2. Malicious PR adds a step that exfiltrates the secret. Mitigation:
|
|
# .github/CODEOWNERS owns this file; can't be modified without
|
|
# registry-owner approval. Action versions are SHA-pinned to block
|
|
# action-supply-chain attacks (transitive `uses:` swap).
|
|
#
|
|
# 3. Sign step writes garbage. Mitigation: post-sign verify step
|
|
# validates the signature against the corresponding pubkey before
|
|
# committing — a malformed sign-script run is caught here, not
|
|
# after publish.
|
|
#
|
|
# 4. Worker secret REGISTRY_REFRESH_TOKEN leaked. Mitigation: worker
|
|
# no longer holds signing material (PR #4600), so a leaked token
|
|
# lets an attacker trigger refreshes against existing committed
|
|
# bytes — they can't substitute attacker-supplied bytes for the
|
|
# worker to sign.
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
paths:
|
|
- 'plugins/**'
|
|
- 'agents/**'
|
|
- 'skills/**'
|
|
- 'hands/**'
|
|
- 'channels/**'
|
|
- 'providers/**'
|
|
- 'workflows/**'
|
|
- 'mcp/**'
|
|
- 'scripts/build-plugins-index.mjs'
|
|
- 'scripts/build-registry-index.mjs'
|
|
- 'scripts/sign-plugins-index.mjs'
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
contents: write # commit regenerated index files back
|
|
|
|
jobs:
|
|
refresh:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
# SHA-pinned to block action-supply-chain swaps. Update with care
|
|
# (read the diff between current and target SHA upstream).
|
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
|
|
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
|
with:
|
|
node-version: '20'
|
|
|
|
- name: Rebuild indexes
|
|
run: |
|
|
node scripts/build-plugins-index.mjs
|
|
node scripts/build-registry-index.mjs
|
|
|
|
# REGISTRY_PRIVATE_KEY is scoped to ONLY this step's env so prior
|
|
# build steps and later trigger steps cannot read it. Keep this
|
|
# narrow; do NOT lift the env to the job level.
|
|
- name: Sign plugins-index.json
|
|
env:
|
|
REGISTRY_PRIVATE_KEY: ${{ secrets.REGISTRY_PRIVATE_KEY }}
|
|
run: node scripts/sign-plugins-index.mjs
|
|
|
|
# Defense in depth: verify the signature against the committed
|
|
# pubkey before the .sig hits main. This catches an *accidental*
|
|
# sign-script regression — a malformed signature, an env-leak that
|
|
# produces zero bytes, an off-by-one in the JSON canonicalization.
|
|
# It does NOT defend against an adversary, because an attacker
|
|
# who can edit sign-plugins-index.mjs in a PR can edit this verify
|
|
# step and the embedded pubkey in the same diff. The CODEOWNERS
|
|
# gate on `/scripts/` and `/.github/workflows/` is what stops
|
|
# adversarial edits, not this step.
|
|
- name: Verify signature against committed pubkey
|
|
env:
|
|
REGISTRY_PUBLIC_KEY: ClGa0Ucap8NdrKAy1rw9Tt6A9I8eg4zJ53+xIuKMuq0=
|
|
run: |
|
|
node -e '
|
|
const c = require("crypto"), fs = require("fs");
|
|
const idx = fs.readFileSync("plugins-index.json");
|
|
const sig = Buffer.from(
|
|
fs.readFileSync("plugins-index.json.sig", "utf8").trim(),
|
|
"base64",
|
|
);
|
|
const pub = Buffer.from(process.env.REGISTRY_PUBLIC_KEY, "base64");
|
|
if (pub.length !== 32) {
|
|
console.error("REGISTRY_PUBLIC_KEY is not a raw 32-byte Ed25519 pubkey");
|
|
process.exit(1);
|
|
}
|
|
const spki = Buffer.concat([
|
|
Buffer.from("302a300506032b6570032100", "hex"),
|
|
pub,
|
|
]);
|
|
const k = c.createPublicKey({ key: spki, format: "der", type: "spki" });
|
|
if (!c.verify(null, idx, k, sig)) {
|
|
console.error("plugins-index.json.sig does NOT verify against the committed pubkey");
|
|
process.exit(1);
|
|
}
|
|
console.log("Signature verifies OK against committed pubkey.");
|
|
'
|
|
|
|
- name: Commit regenerated indexes if changed
|
|
run: |
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
git add plugins-index.json plugins-index.json.sig registry-index.json
|
|
if git diff --cached --quiet; then
|
|
echo "indexes already up-to-date"
|
|
else
|
|
git commit -m "chore: regenerate registry indexes"
|
|
git push
|
|
fi
|
|
|
|
- name: Trigger worker refresh
|
|
env:
|
|
REGISTRY_REFRESH_TOKEN: ${{ secrets.REGISTRY_REFRESH_TOKEN }}
|
|
run: |
|
|
if [ -z "$REGISTRY_REFRESH_TOKEN" ]; then
|
|
echo "::error::REGISTRY_REFRESH_TOKEN secret is not set on this repo"
|
|
exit 1
|
|
fi
|
|
response=$(curl -fsS -X POST \
|
|
-H "Authorization: Bearer $REGISTRY_REFRESH_TOKEN" \
|
|
-w "\nHTTP_CODE:%{http_code}" \
|
|
https://stats.librefang.ai/api/registry/refresh)
|
|
echo "$response"
|
|
code=$(echo "$response" | grep -oE 'HTTP_CODE:[0-9]+' | cut -d: -f2)
|
|
if [ "$code" != "200" ]; then
|
|
echo "::error::worker returned $code"
|
|
exit 1
|
|
fi
|