Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 81 additions & 45 deletions apps/server/src/provider/prime/PrimeAgentDistributionVerifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ const VERSION = "1.0.0";
function syntheticPublication(
channel: "preview" | "stable" = "preview",
policies = { preview: 1, build: 1, promotion: 1 },
recipeRevision = 1,
): PrimePublicationFixture & { readonly verified: VerifiedPrimePublication } {
const buildId = `pylon-build-g${SOURCE_COMMIT.slice(0, 12)}-r${recipeRevision}`;
const artifactBytes = new Map([
[`pylon-prime-agent-${VERSION}.tgz`, Buffer.from("synthetic-root-tarball-v1")],
[`pylon-prime-agent-ai-${VERSION}.tgz`, Buffer.from("synthetic-ai-tarball-v1")],
Expand Down Expand Up @@ -93,12 +95,12 @@ function syntheticPublication(
tree: SOURCE_TREE,
},
build: {
id: BUILD_ID,
recipeRevision: 1,
id: buildId,
recipeRevision,
node: "22.23.2",
npm: "11.10.1",
lockfile: { file: "package-lock.json", sha256: "e".repeat(64) },
assetBaseUrl: `${PRIME_DISTRIBUTION_REPOSITORY_URL}/releases/download/${BUILD_ID}`,
assetBaseUrl: `${PRIME_DISTRIBUTION_REPOSITORY_URL}/releases/download/${buildId}`,
},
package: {
name: "prime-agent",
Expand All @@ -123,9 +125,9 @@ function syntheticPublication(
sequence: 7,
workflowRunId: "9001",
build: {
tag: BUILD_ID,
id: BUILD_ID,
recipeRevision: 1,
tag: buildId,
id: buildId,
recipeRevision,
source: releaseManifest.source,
releaseManifest: {
file: PRIME_RELEASE_MANIFEST,
Expand All @@ -140,7 +142,7 @@ function syntheticPublication(
channel: "stable",
repository: PRIME_DISTRIBUTION_REPOSITORY_URL,
sequence: 3,
tag: `pylon-stable-000003-g${SOURCE_COMMIT.slice(0, 12)}-r1`,
tag: `pylon-stable-000003-g${SOURCE_COMMIT.slice(0, 12)}-r${recipeRevision}`,
history: {
highWater: 2,
previous: {
Expand All @@ -154,9 +156,9 @@ function syntheticPublication(
sequence: 7,
workflowRunId: "9001",
},
previewTag: BUILD_ID,
id: BUILD_ID,
recipeRevision: 1,
previewTag: buildId,
id: buildId,
recipeRevision,
publicationPolicyRevision: policies.build,
source: releaseManifest.source,
releaseManifest: {
Expand Down Expand Up @@ -199,10 +201,10 @@ function syntheticPublication(
channel,
sequenceEpoch: 1,
sequence: channel === "preview" ? 7 : 3,
buildId: BUILD_ID,
buildId: buildId,
sourceCommit: SOURCE_COMMIT,
sourceTree: SOURCE_TREE,
recipeRevision: 1,
recipeRevision,
rootAsset: rootAsset.file,
rootSha256: rootAsset.sha256,
packageVersion: VERSION,
Expand Down Expand Up @@ -480,6 +482,25 @@ describe("Pylon Prime publication verification", () => {
);
});

it.each(["preview", "stable"] as const)(
"verifies both frozen recipes for %s and rejects unknown recipes",
async (channel) => {
const policies = { preview: 4, build: 4, promotion: 4 };
for (const revision of [1, 2]) {
const fixture = syntheticPublication(channel, policies, revision);
await expect(verifyPrimePublicationFixture(fixture, validVerification)).resolves.toEqual(
fixture.verified,
);
}
await expect(
verifyPrimePublicationFixture(
syntheticPublication(channel, policies, 3),
validVerification,
),
).rejects.toThrow(/recipe/u);
},
);

it("retains immutable historical and current workflow policies", () => {
expect(Object.isFrozen(PRIME_PUBLICATION_POLICIES)).toBe(true);
expect(PRIME_PUBLICATION_POLICIES.every(Object.isFrozen)).toBe(true);
Expand All @@ -505,6 +526,13 @@ describe("Pylon Prime publication verification", () => {
stableWorkflowPath: PRIME_STABLE_WORKFLOW,
stableWorkflowSha256: "96e4f3ccd889a17a391f00b4398d132a4234b5bc337045fedd3605f4eb74c331",
},
{
publicationPolicyRevision: 4,
previewWorkflowPath: PRIME_PREVIEW_WORKFLOW,
previewWorkflowSha256: "8ad08abd8004b7205b04dc3cd9f7cc0e720649c9c6814431622f5b4dfb6e4c32",
stableWorkflowPath: PRIME_STABLE_WORKFLOW,
stableWorkflowSha256: "52c72e078b08c0a2774ab3dc512283c46ab31c4a87738d87007f55f955662703",
},
]);
});

Expand All @@ -515,6 +543,9 @@ describe("Pylon Prime publication verification", () => {
{ preview: 3, build: 3, promotion: 3 },
{ preview: 1, build: 1, promotion: 3 },
{ preview: 2, build: 2, promotion: 3 },
{ preview: 4, build: 4, promotion: 4 },
{ preview: 1, build: 1, promotion: 4 },
{ preview: 3, build: 3, promotion: 4 },
])("verifies independent build and promotion policies: %j", async (policies) => {
const fixture = syntheticPublication("stable", policies);
const verifySourcePolicy = vi.fn(validVerification.verifySourcePolicy);
Expand Down Expand Up @@ -546,9 +577,9 @@ describe("Pylon Prime publication verification", () => {
});

it.each([
{ preview: 4, build: 3, promotion: 3 },
{ preview: 3, build: 4, promotion: 3 },
{ preview: 3, build: 3, promotion: 4 },
{ preview: 5, build: 3, promotion: 3 },
{ preview: 3, build: 5, promotion: 3 },
{ preview: 3, build: 3, promotion: 5 },
])("rejects an unknown policy revision in any manifest position: %j", async (policies) => {
await expect(
verifyPrimePublicationFixture(syntheticPublication("stable", policies), validVerification),
Expand All @@ -566,36 +597,41 @@ describe("Pylon Prime publication verification", () => {
).rejects.toThrow(/Stable manifest does not bind the exact verified preview/u);
});

it.each([1, 2, 3])("checks actual fetched workflow bytes under policy %i", async (revision) => {
const fixture = syntheticPublication("preview", {
preview: revision,
build: revision,
promotion: revision,
});
const harness = makeNetworkHarness({
channel: "preview",
candidates: 1,
key: `policy-bytes-${revision}`,
fixture,
});
const { verifySourcePolicy: _injectedSourcePolicy, ...dependencies } =
harness.dependencyShape();
const workflowUrl = `https://raw.githubusercontent.com/${PRIME_DISTRIBUTION_REPOSITORY}/${SOURCE_COMMIT}/${PRIME_PREVIEW_WORKFLOW}`;
const fetchBytes = vi.fn(async (url: string, maximumBytes: number) => {
if (url === workflowUrl) return Buffer.from("altered publication workflow");
return dependencies.fetchBytes(url, maximumBytes);
});
const load = makeLatestPrimePublicationLoader({
...dependencies,
fetchJson: async (url, maximumBytes) =>
url.endsWith(`/git/commits/${SOURCE_COMMIT}`)
? { sha: SOURCE_COMMIT, tree: { sha: SOURCE_TREE } }
: dependencies.fetchJson(url, maximumBytes),
fetchBytes,
});
await expect(load("preview")).rejects.toThrow(/No exact signed preview publication verified/u);
expect(fetchBytes.mock.calls.some(([url]) => url === workflowUrl)).toBe(true);
});
it.each([1, 2, 3, 4])(
"checks actual fetched workflow bytes under policy %i",
async (revision) => {
const fixture = syntheticPublication("preview", {
preview: revision,
build: revision,
promotion: revision,
});
const harness = makeNetworkHarness({
channel: "preview",
candidates: 1,
key: `policy-bytes-${revision}`,
fixture,
});
const { verifySourcePolicy: _injectedSourcePolicy, ...dependencies } =
harness.dependencyShape();
const workflowUrl = `https://raw.githubusercontent.com/${PRIME_DISTRIBUTION_REPOSITORY}/${SOURCE_COMMIT}/${PRIME_PREVIEW_WORKFLOW}`;
const fetchBytes = vi.fn(async (url: string, maximumBytes: number) => {
if (url === workflowUrl) return Buffer.from("altered publication workflow");
return dependencies.fetchBytes(url, maximumBytes);
});
const load = makeLatestPrimePublicationLoader({
...dependencies,
fetchJson: async (url, maximumBytes) =>
url.endsWith(`/git/commits/${SOURCE_COMMIT}`)
? { sha: SOURCE_COMMIT, tree: { sha: SOURCE_TREE } }
: dependencies.fetchJson(url, maximumBytes),
fetchBytes,
});
await expect(load("preview")).rejects.toThrow(
/No exact signed preview publication verified/u,
);
expect(fetchBytes.mock.calls.some(([url]) => url === workflowUrl)).toBe(true);
},
);

it("fails closed for tarball, manifest, attestation, source, recipe, and stable history tampering", async () => {
const fixture = syntheticPublication();
Expand Down
26 changes: 20 additions & 6 deletions apps/server/src/provider/prime/PrimeAgentDistributionVerifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,10 @@ export const PRIME_RELEASE_RECIPE = Object.freeze({
npmVersion: "11.10.1",
minimumNodeVersion: "22.8.0",
});
export const PRIME_RELEASE_RECIPES = Object.freeze([
PRIME_RELEASE_RECIPE,
Object.freeze({ ...PRIME_RELEASE_RECIPE, recipeRevision: 2 }),
]);
export const PRIME_PUBLICATION_SCHEMA_SOURCE = Object.freeze({
commit: "f4d9ef03b529faf2e07031c8b7cd703363316ae5",
tree: "b9a14b389aa64f54527008fb4d6119a7c57c2b58",
Expand All @@ -400,6 +404,13 @@ export const PRIME_PUBLICATION_POLICIES = Object.freeze([
stableWorkflowPath: PRIME_STABLE_WORKFLOW,
stableWorkflowSha256: "96e4f3ccd889a17a391f00b4398d132a4234b5bc337045fedd3605f4eb74c331",
}),
Object.freeze({
publicationPolicyRevision: 4,
previewWorkflowPath: PRIME_PREVIEW_WORKFLOW,
previewWorkflowSha256: "8ad08abd8004b7205b04dc3cd9f7cc0e720649c9c6814431622f5b4dfb6e4c32",
stableWorkflowPath: PRIME_STABLE_WORKFLOW,
stableWorkflowSha256: "52c72e078b08c0a2774ab3dc512283c46ab31c4a87738d87007f55f955662703",
}),
]);

function publicationPolicyFor(revision: number) {
Expand Down Expand Up @@ -472,14 +483,17 @@ function parseReleaseManifest(bytes: Buffer): ReleaseManifest {
"Pylon Prime release manifest",
MAX_MANIFEST_BYTES,
decodeReleaseManifest,
// Recipe 1 preserves object field order; its exact original bytes remain signed subjects.
// Release recipes preserve field order; the original bytes remain signed subjects.
"preserved",
);
const recipe = PRIME_RELEASE_RECIPES.find(
(entry) => entry.recipeRevision === manifest.build.recipeRevision,
);
if (
manifest.build.recipeRevision !== PRIME_RELEASE_RECIPE.recipeRevision ||
manifest.build.node !== PRIME_RELEASE_RECIPE.nodeVersion ||
manifest.build.npm !== PRIME_RELEASE_RECIPE.npmVersion ||
manifest.package.minimumNode !== PRIME_RELEASE_RECIPE.minimumNodeVersion ||
!recipe ||
manifest.build.node !== recipe.nodeVersion ||
manifest.build.npm !== recipe.npmVersion ||
manifest.package.minimumNode !== recipe.minimumNodeVersion ||
manifest.build.id !==
`pylon-build-g${manifest.source.commit.slice(0, 12)}-r${manifest.build.recipeRevision}` ||
manifest.build.assetBaseUrl !==
Expand Down Expand Up @@ -576,7 +590,7 @@ function parseStableManifest(bytes: Buffer): StableManifest {
Number(/^pylon-stable-([0-9]{6})-/.exec(manifest.history.previous.tag)?.[1]) !==
manifest.sequence - 1) ||
manifest.build.previewTag !== manifest.build.id ||
manifest.build.recipeRevision !== PRIME_RELEASE_RECIPE.recipeRevision
!PRIME_RELEASE_RECIPES.some((entry) => entry.recipeRevision === manifest.build.recipeRevision)
) {
throw new Error("Stable manifest is not an exact closed Pylon build receipt.");
}
Expand Down
Loading
Loading