upgrade to v0.2.0 vaultmesh artifact
This commit is contained in:
@@ -2,37 +2,7 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { hashBlake3Hex, hashSha256Hex } from "../lib/hash.js";
|
||||
import { readHead } from "../lib/ledger.js";
|
||||
import { keyIdFromPublicHex, verifyMessage } from "../lib/keys.js";
|
||||
|
||||
type ReceiptEnvelope = {
|
||||
receipt_version: "1";
|
||||
created_at: string;
|
||||
cwd: string;
|
||||
user: string;
|
||||
hostname: string;
|
||||
argv: string[];
|
||||
reason: string;
|
||||
lock_file: string | null;
|
||||
lock_started_at: string | null;
|
||||
force: boolean;
|
||||
plan_file: string | null;
|
||||
plan_sha256: string | null;
|
||||
plan_blake3: string | null;
|
||||
target: { id: number; name: string; ip?: string | null };
|
||||
request: unknown;
|
||||
response: unknown;
|
||||
prev_blake3: string | null;
|
||||
hash_alg: "blake3+sha256";
|
||||
blake3: string;
|
||||
sha256: string;
|
||||
sig_alg?: "ed25519";
|
||||
signer_pub?: string;
|
||||
signer_kid?: string;
|
||||
signed_at?: string;
|
||||
signature?: string;
|
||||
};
|
||||
|
||||
type ReceiptBody = Omit<ReceiptEnvelope, "hash_alg" | "blake3" | "sha256">;
|
||||
import { verifyMessage } from "../lib/keys.js";
|
||||
|
||||
function mustExist(p: string, label: string) {
|
||||
if (!fs.existsSync(p)) {
|
||||
@@ -42,16 +12,31 @@ function mustExist(p: string, label: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function stripHashes(env: ReceiptEnvelope): ReceiptBody {
|
||||
type ReceiptEnvelope = Record<string, unknown> & {
|
||||
hash_alg?: string;
|
||||
blake3?: string;
|
||||
sha256?: string;
|
||||
prev_blake3?: string | null;
|
||||
|
||||
plan_file?: string | null;
|
||||
plan_sha256?: string | null;
|
||||
plan_blake3?: string | null;
|
||||
|
||||
sig_alg?: string;
|
||||
signer_pub?: string;
|
||||
signature?: string;
|
||||
signed_at?: string;
|
||||
};
|
||||
|
||||
function stripForHash(env: ReceiptEnvelope) {
|
||||
const {
|
||||
hash_alg: _hash_alg,
|
||||
blake3: _blake3,
|
||||
sha256: _sha256,
|
||||
sig_alg: _sig_alg,
|
||||
signer_pub: _signer_pub,
|
||||
signer_kid: _signer_kid,
|
||||
signed_at: _signed_at,
|
||||
signature: _signature,
|
||||
signed_at: _signed_at,
|
||||
...body
|
||||
} = env;
|
||||
return body;
|
||||
@@ -65,95 +50,69 @@ export async function verifyReceipt(
|
||||
mustExist(abs, "Receipt");
|
||||
|
||||
const raw = fs.readFileSync(abs, "utf8");
|
||||
let env: ReceiptEnvelope;
|
||||
try {
|
||||
env = JSON.parse(raw) as ReceiptEnvelope;
|
||||
} catch {
|
||||
const err = new Error("Receipt is not valid JSON");
|
||||
(err as { exitCode?: number }).exitCode = 2;
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (env.hash_alg !== "blake3+sha256" || !env.blake3 || !env.sha256) {
|
||||
const err = new Error(
|
||||
"Receipt missing required hash fields (hash_alg/blake3/sha256)"
|
||||
);
|
||||
(err as { exitCode?: number }).exitCode = 2;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const body = stripHashes(env);
|
||||
const blake3 = hashBlake3Hex(body);
|
||||
const sha256 = hashSha256Hex(body);
|
||||
const env = JSON.parse(raw) as ReceiptEnvelope;
|
||||
|
||||
const failures: string[] = [];
|
||||
if (blake3 !== env.blake3) {
|
||||
failures.push(`BLAKE3 mismatch (expected ${env.blake3}, got ${blake3})`);
|
||||
}
|
||||
if (sha256 !== env.sha256) {
|
||||
failures.push(`SHA256 mismatch (expected ${env.sha256}, got ${sha256})`);
|
||||
|
||||
if (env.hash_alg !== "blake3+sha256") {
|
||||
failures.push(`hash_alg mismatch (got ${String(env.hash_alg)})`);
|
||||
}
|
||||
|
||||
if (opts.plan && env.plan_file) {
|
||||
const planAbs = path.isAbsolute(env.plan_file)
|
||||
? env.plan_file
|
||||
: path.join(process.cwd(), env.plan_file);
|
||||
try {
|
||||
mustExist(planAbs, "Plan file referenced by receipt");
|
||||
const planRaw = fs.readFileSync(planAbs, "utf8");
|
||||
const planObj = JSON.parse(planRaw) as unknown;
|
||||
const planSha = hashSha256Hex(planObj);
|
||||
const planB3 = hashBlake3Hex(planObj);
|
||||
if (!env.plan_sha256 || !env.plan_blake3) {
|
||||
failures.push("Receipt is missing plan hash fields");
|
||||
const body = stripForHash(env);
|
||||
const blake3_calc = hashBlake3Hex(body);
|
||||
const sha256_calc = hashSha256Hex(body);
|
||||
|
||||
if (env.blake3 !== blake3_calc) failures.push("blake3 mismatch");
|
||||
if (env.sha256 !== sha256_calc) failures.push("sha256 mismatch");
|
||||
|
||||
if (opts.plan) {
|
||||
const planFile = env.plan_file ? String(env.plan_file) : "";
|
||||
const planSha = env.plan_sha256 ? String(env.plan_sha256) : "";
|
||||
const planBlake = env.plan_blake3 ? String(env.plan_blake3) : "";
|
||||
|
||||
if (!planFile) {
|
||||
failures.push("receipt has no plan_file");
|
||||
} else {
|
||||
const planAbs = path.resolve(planFile);
|
||||
if (!fs.existsSync(planAbs)) {
|
||||
failures.push(`plan file not found: ${planFile}`);
|
||||
} else {
|
||||
if (env.plan_sha256 !== planSha) {
|
||||
failures.push(
|
||||
`Plan SHA256 mismatch (expected ${env.plan_sha256}, got ${planSha})`
|
||||
);
|
||||
}
|
||||
if (env.plan_blake3 !== planB3) {
|
||||
failures.push(
|
||||
`Plan BLAKE3 mismatch (expected ${env.plan_blake3}, got ${planB3})`
|
||||
);
|
||||
try {
|
||||
const planRaw = fs.readFileSync(planAbs, "utf8");
|
||||
const plan = JSON.parse(planRaw) as unknown;
|
||||
const sha = hashSha256Hex(plan);
|
||||
const b3 = hashBlake3Hex(plan);
|
||||
if (planSha && sha !== planSha) failures.push("plan_sha256 mismatch");
|
||||
if (planBlake && b3 !== planBlake) failures.push("plan_blake3 mismatch");
|
||||
} catch (e) {
|
||||
failures.push(`failed to read/parse plan: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
failures.push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.head) {
|
||||
const head = readHead();
|
||||
if (!head) {
|
||||
failures.push("HEAD.json missing");
|
||||
failures.push("HEAD.json not found or invalid");
|
||||
} else {
|
||||
const rel = path.relative(process.cwd(), abs) || abs;
|
||||
if (head.file === rel && head.blake3 !== env.blake3) {
|
||||
failures.push(
|
||||
`HEAD blake3 mismatch (HEAD=${head.blake3}, receipt=${env.blake3})`
|
||||
);
|
||||
if (head.file !== rel) {
|
||||
failures.push(`receipt is not HEAD.file (HEAD.file=${head.file})`);
|
||||
}
|
||||
if (head.blake3 !== env.blake3) {
|
||||
failures.push("HEAD.blake3 mismatch");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.sig) {
|
||||
if (!env.signature || !env.signer_pub || env.sig_alg !== "ed25519") {
|
||||
const err = new Error("Receipt signature fields missing or invalid");
|
||||
(err as { exitCode?: number }).exitCode = 2;
|
||||
throw err;
|
||||
}
|
||||
const msg = new TextEncoder().encode(env.blake3);
|
||||
const ok = await verifyMessage(msg, env.signature, env.signer_pub);
|
||||
if (!ok) failures.push("Signature verification failed");
|
||||
if (env.signer_kid) {
|
||||
const kid = keyIdFromPublicHex(env.signer_pub);
|
||||
if (env.signer_kid !== kid) {
|
||||
failures.push(
|
||||
`Signer key id mismatch (expected ${env.signer_kid}, got ${kid})`
|
||||
);
|
||||
}
|
||||
if (env.sig_alg !== "ed25519" || !env.signature || !env.signer_pub) {
|
||||
failures.push("signature fields missing (need sig_alg, signer_pub, signature)");
|
||||
} else {
|
||||
const msg = new TextEncoder().encode(String(env.blake3 ?? ""));
|
||||
const ok = await verifyMessage(msg, String(env.signature), String(env.signer_pub));
|
||||
if (!ok) failures.push("signature verification failed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,10 +122,8 @@ export async function verifyReceipt(
|
||||
throw err;
|
||||
}
|
||||
|
||||
console.log("OK receipt verified");
|
||||
console.log(`file: ${abs}`);
|
||||
console.log(`blake3: ${env.blake3}`);
|
||||
console.log(`sha256: ${env.sha256}`);
|
||||
if (env.prev_blake3) console.log(`prev_blake3: ${env.prev_blake3}`);
|
||||
if (env.plan_file) console.log(`plan: ${env.plan_file}`);
|
||||
console.log(`OK receipt ${receiptPath}`);
|
||||
console.log(`blake3=${env.blake3}`);
|
||||
console.log(`sha256=${env.sha256}`);
|
||||
if (opts.sig && env.signature) console.log(`sig=ok signer_pub=${env.signer_pub}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user