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
40 changes: 35 additions & 5 deletions layers/artifact-delivery/server/utils/checks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ArtifactFile, CheckResult, ResolvedSource } from '../schemas/contracts'
import type { ArtifactSourceFile } from './github-source'
import { digestHex } from './encoding'
import { ARTIFACT_POLICY_VERSION } from './state'

/**
* 2026-10-07: the check became advisory. It still reports every finding, but
Expand All @@ -14,13 +15,39 @@ const PATH_POLICY_VERSION = '1'
const CREDENTIAL_MATERIAL_VERSION = '1'
const EXECUTABLE_FILES_VERSION = '1'

const CURRENT_ARTIFACT_CHECKS = new Map([
/** The checks a statement under one policy carries, by check name. */
export type ArtifactCheckSet = ReadonlyMap<string, { version: string, required: boolean }>

const CURRENT_ARTIFACT_CHECKS: ArtifactCheckSet = new Map([
['path-policy', { version: PATH_POLICY_VERSION, required: true }],
['agent-skills-spec', { version: AGENT_SKILLS_CHECK_VERSION, required: false }],
['credential-material', { version: CREDENTIAL_MATERIAL_VERSION, required: true }],
['executable-files', { version: EXECUTABLE_FILES_VERSION, required: false }],
])

/**
* Every policy the artifact signer signs, with the checks its statements carry.
*
* The deploy updates the signer before the site, and a failed smoke rolls the
* site back alone. In both windows the running site stages statements under
* the policy before the signer's, so the signer signs that one too. When you
* bump `ARTIFACT_POLICY_VERSION`, replace the previous entry with the policy
* you bumped from. Leave it out only when the bump closes a safety gap, so the
* signer refuses the old policy at once.
*
* The site never reuses a build under another policy, so a statement signed
* under the previous one only finishes a run already in flight.
*/
export const SIGNABLE_ARTIFACT_POLICIES: ReadonlyMap<string, ArtifactCheckSet> = new Map([
[ARTIFACT_POLICY_VERSION, CURRENT_ARTIFACT_CHECKS],
['2026-08-20.1', new Map([
['path-policy', { version: '1', required: true }],
['agent-skills-spec', { version: '2026-08-20', required: true }],
['credential-material', { version: '1', required: true }],
['executable-files', { version: '1', required: false }],
])],
])

export interface CheckedArtifactSource {
files: ArtifactFile[]
checkResults: CheckResult[]
Expand Down Expand Up @@ -151,20 +178,23 @@ export async function checkArtifactSource(
}
}

export function checksBlockArtifact(checks: CheckResult[]): boolean {
export function checksBlockArtifact(checks: CheckResult[], checkSet: ArtifactCheckSet = CURRENT_ARTIFACT_CHECKS): boolean {
const byName = new Map(checks.map(check => [check.name, check]))
if (byName.size !== checks.length)
return true
for (const [name, expected] of CURRENT_ARTIFACT_CHECKS) {
for (const [name, expected] of checkSet) {
const check = byName.get(name)
if (!check || check.version !== expected.version || check.required !== expected.required)
return true
}
return checks.some(check => check.required && (check.outcome === 'fail' || check.outcome === 'error'))
}

export function checksPermitSigning(checks: CheckResult[]): boolean {
return !checksBlockArtifact(checks)
/** Whether checks permit signing under one policy. The current policy is the default. */
export function checksPermitSigning(checks: CheckResult[], policyVersion: string = ARTIFACT_POLICY_VERSION): boolean {
const checkSet = SIGNABLE_ARTIFACT_POLICIES.get(policyVersion)
return checkSet !== undefined
&& !checksBlockArtifact(checks, checkSet)
&& checks.every(check => !check.required || check.outcome === 'pass')
}

Expand Down
3 changes: 3 additions & 0 deletions layers/artifact-delivery/server/utils/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import { canonicalJson, digestHex } from './encoding'
* Bump it when a change to loading, packaging, or checks changes what one
* commit produces. A ready build signed under another version is never reused,
* so the bump also makes every commit load from GitHub once more.
*
* The signer also signs the policy before this one, so a deploy window fails
* no run. Update `SIGNABLE_ARTIFACT_POLICIES` in `checks.ts` with each bump.
*/
export const ARTIFACT_POLICY_VERSION = '2026-10-07.1'

Expand Down
58 changes: 46 additions & 12 deletions test/unit/artifact-signer.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ResolvedSource } from '../../layers/artifact-delivery/server/schemas/contracts'
import type { CheckResult, ResolvedSource } from '../../layers/artifact-delivery/server/schemas/contracts'
import type { ArtifactSignerBindings } from '../../workers/artifact-signer/src/handler'
import { describe, expect, it, vi } from 'vitest'
import { artifactR2Key } from '../../layers/artifact-delivery/server/utils/artifact-storage'
Expand All @@ -7,7 +7,9 @@ import {
createAttestationStatement,
encodeAttestationStatement,
} from '../../layers/artifact-delivery/server/utils/attestation'
import { SIGNABLE_ARTIFACT_POLICIES } from '../../layers/artifact-delivery/server/utils/checks'
import { bytesToBase64Url, digestHex } from '../../layers/artifact-delivery/server/utils/encoding'
import { ARTIFACT_POLICY_VERSION } from '../../layers/artifact-delivery/server/utils/state'
import {
ARTIFACT_SIGNER_MAX_REQUEST_BYTES,
handleArtifactSignerRequest,
Expand Down Expand Up @@ -154,6 +156,33 @@ describe('artifact signing Worker', () => {
fixture.close()
})

// The deploy updates the signer before the site, and a failed smoke rolls
// the site back alone. In both windows the running site stages statements
// under the policy before the signer's. The signer refused every one, so a
// deploy that bumped the policy failed each run in its window.
it('signs a statement the site staged under the previous policy', async () => {
const [policyVersion, checkSet] = [...SIGNABLE_ARTIFACT_POLICIES].find(([version]) => version !== ARTIFACT_POLICY_VERSION)!
const fixture = await createSignerFixture({
policyVersion,
checks: [...checkSet].map(([name, check]) => ({ name, version: check.version, outcome: 'pass' as const, required: check.required })),
})

const response = await handleArtifactSignerRequest(attestRequest(fixture.artifactId), fixture.bindings, () => NOW)

expect(response.status).toBe(200)
fixture.close()
})

it('rejects current check results under the previous policy', async () => {
const [policyVersion] = [...SIGNABLE_ARTIFACT_POLICIES].find(([version]) => version !== ARTIFACT_POLICY_VERSION)!
const fixture = await createSignerFixture({ policyVersion })

const response = await handleArtifactSignerRequest(attestRequest(fixture.artifactId), fixture.bindings, () => NOW)

expect(await readCode(response)).toBe('CHECKS_BLOCKED')
fixture.close()
})

it.each([
['size', { sizeOffset: 1 }],
['stored digest', { storedDigest: 'f'.repeat(64) }],
Expand Down Expand Up @@ -235,6 +264,8 @@ interface SignerFixtureOptions {
body?: Uint8Array
}
privateArtifact?: boolean
policyVersion?: string
checks?: CheckResult[]
}

async function createSignerFixture(options: SignerFixtureOptions = {}) {
Expand All @@ -255,21 +286,24 @@ async function createSignerFixture(options: SignerFixtureOptions = {}) {
const r2Key = options.privateArtifact
? `v1/private/account/${RESOLUTION_ID}.bin`
: artifactR2Key(contentSha256)
const checks = [
const checks: CheckResult[] = options.checks ?? [
{ name: 'path-policy', version: '1', outcome: options.checkOutcome ?? 'pass', required: true },
{ name: 'agent-skills-spec', version: '2026-10-07', outcome: 'pass', required: false },
{ name: 'credential-material', version: '1', outcome: 'pass', required: true },
{ name: 'executable-files', version: '1', outcome: 'pass', required: false },
] as const
const statement = encodeAttestationStatement(createAttestationStatement({
artifactId,
createdAt: new Date((NOW - 60) * 1000).toISOString(),
source: options.privateArtifact ? { ...source, visibility: 'private' } : source,
contentSha256,
contentBytes: artifactBytes.byteLength,
files: [{ path: 'SKILL.md', mode: 420, size: 1, sha256: 'a'.repeat(64) }],
checkResults: checks.map(check => ({ ...check })),
}))
]
const statement = encodeAttestationStatement({
...createAttestationStatement({
artifactId,
createdAt: new Date((NOW - 60) * 1000).toISOString(),
source: options.privateArtifact ? { ...source, visibility: 'private' } : source,
contentSha256,
contentBytes: artifactBytes.byteLength,
files: [{ path: 'SKILL.md', mode: 420, size: 1, sha256: 'a'.repeat(64) }],
checkResults: checks.map(check => ({ ...check })),
}),
...(options.policyVersion ? { policyVersion: options.policyVersion } : {}),
})
sqlite.raw.prepare(
`INSERT INTO artifact_resolutions (
id, request_fingerprint, state, state_version,
Expand Down
12 changes: 9 additions & 3 deletions workers/artifact-signer/src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from '../../../layers/artifact-delivery/server/schemas/contracts'
import { artifactR2Key } from '../../../layers/artifact-delivery/server/utils/artifact-storage'
import { createAttestationSignaturePayload, encodeAttestationStatement } from '../../../layers/artifact-delivery/server/utils/attestation'
import { checksPermitSigning } from '../../../layers/artifact-delivery/server/utils/checks'
import { checksPermitSigning, SIGNABLE_ARTIFACT_POLICIES } from '../../../layers/artifact-delivery/server/utils/checks'
import { base64ToBytes, bytesToBase64Url, canonicalJson, digestHex } from '../../../layers/artifact-delivery/server/utils/encoding'
import { ARTIFACT_POLICY_VERSION } from '../../../layers/artifact-delivery/server/utils/state'

Expand Down Expand Up @@ -211,9 +211,15 @@ function validateSigningRow(row: SigningRow, maximumAgeSeconds: number, now: num
) {
return failures.artifactIdMismatch()
}
// Checks count under the policy the statement names, when the signer signs
// it. An unknown policy counts under the current one and fails below.
const stagedPolicy = artifactAttestationStatementSchema.safeParse(parseJson(row.attestation_statement_json))
const policyVersion = stagedPolicy.success && SIGNABLE_ARTIFACT_POLICIES.has(stagedPolicy.data.policyVersion)
? stagedPolicy.data.policyVersion
: ARTIFACT_POLICY_VERSION
const checkResultsValue = parseJson(row.check_results_json)
const checkResults = z.array(checkResultSchema).min(1).max(100).safeParse(checkResultsValue)
if (!checkResults.success || !checksPermitSigning(checkResults.data))
if (!checkResults.success || !checksPermitSigning(checkResults.data, policyVersion))
return failures.checksBlocked()
const checkAge = now - row.updated_at
if (checkAge < 0 || checkAge > maximumAgeSeconds)
Expand All @@ -226,7 +232,7 @@ function validateSigningRow(row: SigningRow, maximumAgeSeconds: number, now: num
const statement = artifactAttestationStatementSchema.safeParse(statementValue)
if (!statement.success)
return failures.statementChanged()
if (statement.data.policyVersion !== ARTIFACT_POLICY_VERSION)
if (!SIGNABLE_ARTIFACT_POLICIES.has(statement.data.policyVersion))
return failures.policyOutdated()
if (row.attestation_statement_json !== encodeAttestationStatement(statement.data))
return failures.statementChanged()
Expand Down