fix(release): complete protected publish tooling

This commit is contained in:
Peter Steinberger
2026-07-15 10:37:45 +01:00
parent 3fccaec469
commit dfa246e6fd
6 changed files with 279 additions and 13 deletions

View File

@@ -201,6 +201,12 @@ type NpmProvenanceStatement = {
repository?: string;
};
};
resolvedDependencies?: Array<{
digest?: {
gitCommit?: string;
};
uri?: string;
}>;
};
runDetails?: {
builder?: {
@@ -277,6 +283,10 @@ export function verifyNpmRegistrySignatures(params: {
function resolveNpmProvenanceVerificationPolicy(
statement: NpmProvenanceStatement,
version: string,
expectedWorkflow?: {
ref?: string;
sha?: string;
},
): NpmProvenanceVerificationPolicy {
const parsedVersion = parseReleaseVersion(version);
if (parsedVersion === null) {
@@ -285,9 +295,37 @@ function resolveNpmProvenanceVerificationPolicy(
const workflow = statement.predicate?.buildDefinition?.externalParameters?.workflow;
const workflowRef = workflow?.ref;
const expectedReleaseRef = `refs/heads/release/${parsedVersion.baseVersion}`;
const protectedReleasePublishMatch =
/^refs\/tags\/release-publish\/([a-f0-9]{12})-[1-9][0-9]*$/u.exec(workflowRef ?? "");
let protectedReleasePublishTrusted = false;
if (protectedReleasePublishMatch) {
const expectedRef = expectedWorkflow?.ref;
const expectedSha = expectedWorkflow?.sha;
if (
expectedRef !== workflowRef ||
!/^[a-f0-9]{40}$/u.test(expectedSha ?? "") ||
expectedSha?.slice(0, 12) !== protectedReleasePublishMatch[1]
) {
throw new Error(
"npm provenance SHA-pinned release-publish ref does not match the approved workflow ref and SHA.",
);
}
const expectedDependencyUri = `git+${NPM_PROVENANCE_REPOSITORY}@${workflowRef}`;
protectedReleasePublishTrusted =
statement.predicate?.buildDefinition?.resolvedDependencies?.some(
(dependency) =>
dependency.uri === expectedDependencyUri && dependency.digest?.gitCommit === expectedSha,
) === true;
if (!protectedReleasePublishTrusted) {
throw new Error(
"npm provenance does not bind the approved SHA-pinned release-publish ref to its workflow revision.",
);
}
}
const isTrustedRef =
workflowRef === "refs/heads/main" ||
workflowRef === expectedReleaseRef ||
protectedReleasePublishTrusted ||
(parsedVersion.channel === "alpha" &&
/^refs\/heads\/tideclaw\/alpha\/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$/u.test(
workflowRef ?? "",
@@ -320,6 +358,8 @@ async function verifySigstoreNpmProvenanceBundle(
export async function verifyNpmProvenanceAttestation(params: {
attestations: NpmRegistryAttestation[];
expectedWorkflowRef?: string;
expectedWorkflowSha?: string;
integrity: string;
packageName: string;
verifyBundle?: VerifyNpmProvenanceBundle;
@@ -353,7 +393,10 @@ export async function verifyNpmProvenanceAttestation(params: {
) {
let policy: NpmProvenanceVerificationPolicy;
try {
policy = resolveNpmProvenanceVerificationPolicy(statement, params.version);
policy = resolveNpmProvenanceVerificationPolicy(statement, params.version, {
ref: params.expectedWorkflowRef,
sha: params.expectedWorkflowSha,
});
} catch (error) {
policyError = error;
continue;
@@ -1156,6 +1199,8 @@ async function verifyPublishedRegistryProvenanceOnce(version: string): Promise<v
version,
integrity,
attestations,
expectedWorkflowRef: process.env.OPENCLAW_NPM_EXPECTED_WORKFLOW_REF,
expectedWorkflowSha: process.env.OPENCLAW_NPM_EXPECTED_WORKFLOW_SHA,
});
console.log(
`openclaw-npm-postpublish-verify: registry signature and provenance attestation verified (${version})`,