Files
clawsec/skills/clawsec-suite/scripts/generate_checksums_json.mjs
T
davida-ps 5ee8587b1e Integration/signing work (#20)
* ci: sign advisory feed and checksums in workflows

* feat(clawsec-suite): add verifier-side signature and checksum enforcement

Implements cryptographic verification for advisory feed loading:

- Ed25519 detached signature verification for feed.json
- Supports raw base64 and JSON-wrapped signature formats
- Pinned public key at advisories/feed-signing-public.pem

- SHA-256 checksum manifest (checksums.json) verification
- Signed checksums.json.sig prevents partial artifact substitution
- Verifies feed.json, feed.json.sig, and public key against manifest

- Remote feed: returns null on verification failure (triggers fallback)
- Local feed: throws on verification failure (hard fail)
- No silent bypass of verification

- CLAWSEC_ALLOW_UNSIGNED_FEED=1 temporarily bypasses verification
- Warning logged when bypass mode is enabled
- Intended for transition period only

- guarded_skill_install without --version matches any advisory for skill
- Encourages explicit version specification

- scripts/sign_detached_ed25519.mjs - signing utility
- scripts/verify_detached_ed25519.mjs - verification utility
- scripts/generate_checksums_json.mjs - checksum manifest generator
- test/feed_verification.test.mjs - 14 verification tests
- test/guarded_install.test.mjs - 6 install flow tests

- hooks/.../lib/feed.mjs - full rewrite with verification
- hooks/.../handler.ts - verification options integration
- scripts/guarded_skill_install.mjs - verification integration
- skill.json - v0.0.9, new SBOM entries, openssl requirement
- SKILL.md - signed install flow, env vars documentation
- HOOK.md - new environment variables
- ci.yml - added verification test job

Refs: fail-closed verification, Ed25519 signatures, checksum manifests

* fix: update action versions in CI workflows for improved stability

* chore(clawsec-suite): bump version to 0.0.10

* feat: enhance security measures in asset deployment and add changelog for version history

* feat: add dry-run signing for advisory artifacts and generate checksums

* fix: enhance error handling in loadRemoteFeed for security policy violations

* feat: implement Ed25519 signing and verification for advisory artifacts and checksums

* feat: implement signing and verification for advisory artifacts and checksums in workflows

* feat: update dry-run signing key generation to use Ed25519 algorithm

* feat: update Ed25519 signing and verification to use -rawin flag for compatibility

* feat: add public key copying to advisory directory and implement safe basename extraction for URLs

* feat: remove Product Hunt promotion section from README and Home page
2026-02-12 18:49:34 +02:00

86 lines
2.1 KiB
JavaScript

#!/usr/bin/env node
import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
const DEFAULT_FILES = ["feed-signing-public.pem", "feed.json", "feed.json.sig"];
function usage() {
process.stderr.write(
[
"Usage:",
" node scripts/generate_checksums_json.mjs --out advisories/checksums.json [--base advisories] [--file feed.json --file feed.json.sig ...]",
"",
"Defaults:",
" --base <dirname(--out)>",
` --file ${DEFAULT_FILES.join(" --file ")}`,
"",
].join("\n"),
);
}
function parseArgs(argv) {
const parsed = { files: [] };
for (let i = 0; i < argv.length; i += 1) {
const token = argv[i];
if (token === "--out") {
parsed.outPath = argv[++i];
} else if (token === "--base") {
parsed.baseDir = argv[++i];
} else if (token === "--file") {
parsed.files.push(argv[++i]);
} else if (token === "-h" || token === "--help") {
parsed.help = true;
} else {
throw new Error(`Unknown argument: ${token}`);
}
}
return parsed;
}
function sha256Hex(buffer) {
return crypto.createHash("sha256").update(buffer).digest("hex");
}
async function main() {
const { outPath, baseDir, files, help } = parseArgs(process.argv.slice(2));
if (help) {
usage();
process.exit(0);
}
if (!outPath) {
usage();
throw new Error("Missing required argument: --out");
}
const resolvedBase = path.resolve(baseDir ?? path.dirname(outPath));
const fileList = files.length > 0 ? files : DEFAULT_FILES;
const checksums = {};
for (const relativePath of [...fileList].sort((a, b) => a.localeCompare(b))) {
const absolutePath = path.resolve(resolvedBase, relativePath);
const content = await fs.readFile(absolutePath);
checksums[relativePath] = sha256Hex(content);
}
const payload = {
schema_version: "1.0",
algorithm: "sha256",
files: checksums,
};
await fs.writeFile(`${outPath}`, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
process.stdout.write(`Wrote ${outPath}\n`);
}
main().catch((error) => {
process.stderr.write(`${String(error)}\n`);
process.exit(1);
});