From 87492a8d6bf695f0f2a71c3fc6b29260ede3d04a Mon Sep 17 00:00:00 2001 From: Tim Nunamaker Date: Wed, 22 Jul 2026 18:36:18 -0500 Subject: [PATCH] fix(installer): transitional artifact identity allowlist for org migration The connectors-latest index is now signed by this repository's publish-connector-release-index.yml workflow, but the artifacts it references still carry Sigstore bundles from the pre-migration vana-com/data-connectors workflow. Verifying both against the single hardcoded identity deterministically fails for every consumer. - index verification stays strict: PDP-Connect identity only - artifact verification tries TRANSITIONAL_ARTIFACT_CERTIFICATE_IDENTITIES in order (PDP-Connect, then the historic vana-com identity) and fails only after all attempts, reporting every attempted identity - tests: vana-com fallback success, all-identity failure message, strict index verification Retirement: once every referenced artifact is re-published with PDP-Connect bundles, shrink the allowlist back to one entry. Signed-off-by: Tim Nunamaker --- packages/connector-installer-core/index.mjs | 37 +++++--- .../connector-installer-core/index.test.mjs | 93 +++++++++++++++++++ 2 files changed, 119 insertions(+), 11 deletions(-) create mode 100644 packages/connector-installer-core/index.test.mjs diff --git a/packages/connector-installer-core/index.mjs b/packages/connector-installer-core/index.mjs index 50e7e94..304e73c 100644 --- a/packages/connector-installer-core/index.mjs +++ b/packages/connector-installer-core/index.mjs @@ -20,6 +20,12 @@ export const DEFAULT_SIGSTORE_CERTIFICATE_ISSUER = "https://token.actions.githubusercontent.com"; export const DEFAULT_SIGSTORE_CERTIFICATE_IDENTITY = "https://github.com/PDP-Connect/data-connectors/.github/workflows/publish-connector-release-index.yml@refs/heads/main"; +// Transitional only: remove the vana-com identity after every referenced artifact +// has been re-published from PDP-Connect and shrink this list back to one identity. +export const TRANSITIONAL_ARTIFACT_CERTIFICATE_IDENTITIES = [ + DEFAULT_SIGSTORE_CERTIFICATE_IDENTITY, + "https://github.com/vana-com/data-connectors/.github/workflows/publish-connector-release-index.yml@refs/heads/main", +]; export function readJson(path) { return JSON.parse(readFileSync(path, "utf8")); @@ -182,12 +188,14 @@ function resolveBundleUrl(subjectUrl, signature) { return `${subjectUrl}.sigstore.json`; } -async function verifyRemoteSignature({ +export async function verifyRemoteSignature({ payloadBuffer, subjectLabel, subjectUrl, signature, allowUnsignedRemote = false, + certificateIdentities = [DEFAULT_SIGSTORE_CERTIFICATE_IDENTITY], + verifyBundle = verifySigstoreBundle, }) { const normalizedSignature = normalizeSignature(signature); if (!normalizedSignature) { @@ -209,18 +217,24 @@ async function verifyRemoteSignature({ const bundleBuffer = await fetchBinary(bundleUrl); const bundle = JSON.parse(bundleBuffer.toString("utf8")); - try { - await verifySigstoreBundle(bundle, payloadBuffer, { - certificateIssuer: DEFAULT_SIGSTORE_CERTIFICATE_ISSUER, - certificateIdentityURI: DEFAULT_SIGSTORE_CERTIFICATE_IDENTITY, - }); - } catch (error) { - throw new Error( - `${subjectLabel} signature verification failed: ${error instanceof Error ? error.message : String(error)}` - ); + const verificationErrors = []; + for (const certificateIdentityURI of certificateIdentities) { + try { + await verifyBundle(bundle, payloadBuffer, { + certificateIssuer: DEFAULT_SIGSTORE_CERTIFICATE_ISSUER, + certificateIdentityURI, + }); + return true; + } catch (error) { + verificationErrors.push(error instanceof Error ? error.message : String(error)); + } } - return true; + throw new Error( + `${subjectLabel} signature verification failed: attempted certificate identities ${certificateIdentities.join( + ", " + )}; ${verificationErrors.join("; ")}` + ); } function ensureInside(baseDir, relativePath) { @@ -464,6 +478,7 @@ async function fetchArtifactForEntry(indexSource, entry) { subjectLabel: `Connector artifact ${resolvedEntry.connectorId}@${resolvedEntry.version}`, subjectUrl: resolvedEntry.artifactUrl, signature: resolvedEntry.artifactSignature, + certificateIdentities: TRANSITIONAL_ARTIFACT_CERTIFICATE_IDENTITIES, }); return artifactBuffer; } diff --git a/packages/connector-installer-core/index.test.mjs b/packages/connector-installer-core/index.test.mjs new file mode 100644 index 0000000..86d8055 --- /dev/null +++ b/packages/connector-installer-core/index.test.mjs @@ -0,0 +1,93 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + DEFAULT_SIGSTORE_CERTIFICATE_IDENTITY, + TRANSITIONAL_ARTIFACT_CERTIFICATE_IDENTITIES, + verifyRemoteSignature, +} from "./index.mjs"; + +async function withSigstoreBundle(fn) { + const originalFetch = globalThis.fetch; + globalThis.fetch = async () => new Response(JSON.stringify({})); + try { + await fn(); + } finally { + globalThis.fetch = originalFetch; + } +} + +function signature() { + return { + type: "sigstoreBundle", + bundleUrl: "https://example.test/artifact.tgz.sigstore.json", + }; +} + +test("artifact verification accepts the second transitional certificate identity", async () => { + await withSigstoreBundle(async () => { + const attempted = []; + + await verifyRemoteSignature({ + payloadBuffer: Buffer.from("artifact"), + subjectLabel: "Connector artifact example@1.0.0", + subjectUrl: "https://example.test/artifact.tgz", + signature: signature(), + certificateIdentities: TRANSITIONAL_ARTIFACT_CERTIFICATE_IDENTITIES, + verifyBundle: async (_bundle, _payload, options) => { + attempted.push(options.certificateIdentityURI); + if ( + options.certificateIdentityURI === + TRANSITIONAL_ARTIFACT_CERTIFICATE_IDENTITIES[1] + ) { + return; + } + throw new Error("certificate identity does not match PDP-Connect"); + }, + }); + + assert.deepEqual(attempted, TRANSITIONAL_ARTIFACT_CERTIFICATE_IDENTITIES); + }); +}); + +test("artifact verification reports every attempted transitional certificate identity", async () => { + await withSigstoreBundle(async () => { + await assert.rejects( + verifyRemoteSignature({ + payloadBuffer: Buffer.from("artifact"), + subjectLabel: "Connector artifact example@1.0.0", + subjectUrl: "https://example.test/artifact.tgz", + signature: signature(), + certificateIdentities: TRANSITIONAL_ARTIFACT_CERTIFICATE_IDENTITIES, + verifyBundle: async () => { + throw new Error("certificate identity does not match"); + }, + }), + (error) => { + assert.match(error.message, /Connector artifact example@1\.0\.0 signature verification failed:/); + for (const identity of TRANSITIONAL_ARTIFACT_CERTIFICATE_IDENTITIES) { + assert.match(error.message, new RegExp(identity.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); + } + return true; + } + ); + }); +}); + +test("index verification remains strict to the PDP-Connect certificate identity", async () => { + await withSigstoreBundle(async () => { + const attempted = []; + + await verifyRemoteSignature({ + payloadBuffer: Buffer.from("index"), + subjectLabel: "Connector index", + subjectUrl: "https://example.test/connector-index.json", + signature: signature(), + verifyBundle: async (_bundle, _payload, options) => { + attempted.push(options.certificateIdentityURI); + }, + }); + + assert.deepEqual(attempted, [DEFAULT_SIGSTORE_CERTIFICATE_IDENTITY]); + }); +});