feat: @crudy/pgmorbac ships the SQL of each signed pgmorbac release
prepack downloads the release whose version matches package.json, verifies the Ed25519 manifest against the pinned release key and the zip against its sha256, and only then extracts the control file and scripts into sql/. No SQL is committed here.
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
// Verifies a signed pgmorbac release and extracts its SQL. Pure functions, so
|
||||
// the checks are tested without a network or the real signing key.
|
||||
import { createHash } from 'node:crypto';
|
||||
import { importSPKI, jwtVerify } from 'jose';
|
||||
import { unzipSync } from 'fflate';
|
||||
|
||||
export const AUDIENCE = 'pgmorbac-release';
|
||||
|
||||
export function releaseFiles(version) {
|
||||
const stem = `pgmorbac-${version}`;
|
||||
return { zip: `${stem}.zip`, manifest: `${stem}.manifest.jwt` };
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws unless the manifest is signed by publicKeyPem for this audience and
|
||||
* version, and names the zip with its exact sha256.
|
||||
*/
|
||||
export async function verifyRelease({ zip, manifestJwt, publicKeyPem, version }) {
|
||||
const key = await importSPKI(publicKeyPem.trim(), 'EdDSA');
|
||||
const { payload } = await jwtVerify(manifestJwt.trim(), key, { algorithms: ['EdDSA'], audience: AUDIENCE });
|
||||
const manifest = payload.manifest;
|
||||
if (manifest?.name !== 'pgmorbac' || manifest.version !== version) {
|
||||
throw new Error(`manifest is for ${manifest?.name}@${manifest?.version}, expected pgmorbac@${version}`);
|
||||
}
|
||||
const expected = manifest.artifacts?.find((a) => a.name === releaseFiles(version).zip)?.sha256;
|
||||
const actual = createHash('sha256').update(zip).digest('hex');
|
||||
if (!expected || expected !== actual) throw new Error(`zip sha256 ${actual} does not match the signed manifest`);
|
||||
}
|
||||
|
||||
/**
|
||||
* The control file, install script and upgrade scripts of a verified release
|
||||
* zip, as { name: bytes }.
|
||||
*/
|
||||
export function extractSql(zip, version) {
|
||||
const prefix = `pgmorbac-${version}/`;
|
||||
const wanted = new RegExp(`^(pgmorbac\\.control|pgmorbac--${version.replace(/\./g, '\\.')}\\.sql|pgmorbac--\\d+\\.\\d+\\.\\d+--\\d+\\.\\d+\\.\\d+\\.sql)$`);
|
||||
const out = {};
|
||||
for (const [path, bytes] of Object.entries(unzipSync(zip))) {
|
||||
if (!path.startsWith(prefix)) continue;
|
||||
const name = path.slice(prefix.length);
|
||||
if (wanted.test(name)) out[name] = bytes;
|
||||
}
|
||||
if (!out['pgmorbac.control'] || !out[`pgmorbac--${version}.sql`]) {
|
||||
throw new Error(`release zip lacks pgmorbac.control or pgmorbac--${version}.sql`);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user