From 048776d422eb996f05e284ba8cfbcdaf34083c13 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Tue, 21 Jul 2026 22:32:42 +0900 Subject: [PATCH] fix(release): trust extended-stable npm provenance (#112301) --- scripts/openclaw-npm-postpublish-verify.ts | 7 ++ test/openclaw-npm-postpublish-verify.test.ts | 90 +++++++++++++++++++- 2 files changed, 93 insertions(+), 4 deletions(-) diff --git a/scripts/openclaw-npm-postpublish-verify.ts b/scripts/openclaw-npm-postpublish-verify.ts index ad73491f0f55..f75defb8b5a8 100644 --- a/scripts/openclaw-npm-postpublish-verify.ts +++ b/scripts/openclaw-npm-postpublish-verify.ts @@ -295,6 +295,12 @@ function resolveNpmProvenanceVerificationPolicy( const workflow = statement.predicate?.buildDefinition?.externalParameters?.workflow; const workflowRef = workflow?.ref; const expectedReleaseRef = `refs/heads/release/${parsedVersion.baseVersion}`; + // A month's final patch >=33 releases stay on its canonical .33 maintenance branch. + const isExpectedExtendedStableRef = + parsedVersion.channel === "stable" && + parsedVersion.correctionNumber === undefined && + parsedVersion.patch >= 33 && + workflowRef === `refs/heads/extended-stable/${parsedVersion.year}.${parsedVersion.month}.33`; const protectedReleasePublishMatch = /^refs\/tags\/release-publish\/([a-f0-9]{12})-[1-9][0-9]*$/u.exec(workflowRef ?? ""); let protectedReleasePublishTrusted = false; @@ -325,6 +331,7 @@ function resolveNpmProvenanceVerificationPolicy( const isTrustedRef = workflowRef === "refs/heads/main" || workflowRef === expectedReleaseRef || + isExpectedExtendedStableRef || protectedReleasePublishTrusted || (parsedVersion.channel === "alpha" && /^refs\/heads\/tideclaw\/alpha\/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$/u.test( diff --git a/test/openclaw-npm-postpublish-verify.test.ts b/test/openclaw-npm-postpublish-verify.test.ts index 5c616654f997..a238bd067416 100644 --- a/test/openclaw-npm-postpublish-verify.test.ts +++ b/test/openclaw-npm-postpublish-verify.test.ts @@ -98,10 +98,10 @@ describe("npm registry provenance verification", () => { const packageName = "openclaw"; const version = "2026.3.23"; const integrity = `sha512-${Buffer.from("registry integrity", "utf8").toString("base64")}`; - const provenancePayload = { + const buildProvenancePayload = (releaseVersion: string, workflowRef: string) => ({ subject: [ { - name: `pkg:npm/${packageName}@${version}`, + name: `pkg:npm/${packageName}@${releaseVersion}`, digest: { sha512: Buffer.from(integrity.slice("sha512-".length), "base64").toString("hex"), }, @@ -113,7 +113,7 @@ describe("npm registry provenance verification", () => { workflow: { repository: "https://github.com/openclaw/openclaw", path: ".github/workflows/openclaw-npm-release.yml", - ref: "refs/heads/release/2026.3.23", + ref: workflowRef, }, }, }, @@ -123,7 +123,8 @@ describe("npm registry provenance verification", () => { }, }, }, - }; + }); + const provenancePayload = buildProvenancePayload(version, "refs/heads/release/2026.3.23"); it("fetches npm registry JSON with bounded response handling", async () => { const fetchImpl = vi.fn(async (_url: string, init?: RequestInit) => { @@ -362,6 +363,87 @@ describe("npm registry provenance verification", () => { ).rejects.toThrow("does not match"); }); + it.each([ + ["2026.6.33", "refs/heads/extended-stable/2026.6.33"], + ["2026.6.34", "refs/heads/extended-stable/2026.6.33"], + ])( + "trusts canonical extended-stable provenance for %s", + async (extendedStableVersion, workflowRef) => { + let verificationPolicy: + | { + certificateIdentityURI: string; + certificateIssuer: string; + } + | undefined; + + await expect( + verifyNpmProvenanceAttestation({ + packageName, + version: extendedStableVersion, + integrity, + attestations: [ + { + predicateType: "https://slsa.dev/provenance/v1", + bundle: { + dsseEnvelope: { + payload: Buffer.from( + JSON.stringify(buildProvenancePayload(extendedStableVersion, workflowRef)), + "utf8", + ).toString("base64"), + }, + }, + }, + ], + verifyBundle: async (_bundle, policy) => { + verificationPolicy = policy; + }, + }), + ).resolves.toBeUndefined(); + expect(verificationPolicy).toEqual({ + certificateIssuer: "https://token.actions.githubusercontent.com", + certificateIdentityURI: `https://github.com/openclaw/openclaw/.github/workflows/openclaw-npm-release.yml@${workflowRef}`, + }); + }, + ); + + it.each([ + ["later patch on a noncanonical branch", "2026.6.34", "refs/heads/extended-stable/2026.6.34"], + ["patch below 33", "2026.6.32", "refs/heads/extended-stable/2026.6.33"], + ["correction suffix", "2026.6.33-1", "refs/heads/extended-stable/2026.6.33"], + ])( + "rejects extended-stable provenance for %s", + async (_label, extendedStableVersion, workflowRef) => { + let verificationCalls = 0; + + await expect( + verifyNpmProvenanceAttestation({ + packageName, + version: extendedStableVersion, + integrity, + attestations: [ + { + predicateType: "https://slsa.dev/provenance/v1", + bundle: { + dsseEnvelope: { + payload: Buffer.from( + JSON.stringify(buildProvenancePayload(extendedStableVersion, workflowRef)), + "utf8", + ).toString("base64"), + }, + }, + }, + ], + verifyBundle: async () => { + verificationCalls += 1; + }, + }), + ).rejects.toThrow( + `does not bind ${extendedStableVersion} to the trusted OpenClaw GitHub release workflow`, + ); + expect(verificationCalls).toBe(0); + }, + ); + it("rejects matching provenance from an untrusted source before Sigstore verification", async () => { let verificationCalls = 0;