@kungfu-tech/buildchain 2.12.6 → 2.12.7-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/actions/promote-buildchain-ref/README.md +7 -3
- package/bin/buildchain.mjs +78 -0
- package/dist/site/agent-index.json +1 -0
- package/dist/site/artifact-schemas.json +1 -0
- package/dist/site/buildchain-contract.json +137 -37
- package/dist/site/buildchain-site.json +68 -12
- package/dist/site/capability-registry.json +6 -5
- package/dist/site/cli-registry.json +36 -0
- package/dist/site/controller-registry.json +104 -13
- package/dist/site/kfd-claims.json +251 -15
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +15 -1
- package/dist/site/node-api-registry.json +43 -4
- package/dist/site/page-registry.json +54 -7
- package/dist/site/public-surface-audit.json +150 -17
- package/dist/site/publication-authority-registry.json +754 -0
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +4 -0
- package/dist/site/site-manifest.json +14 -5
- package/dist/site/workflow-registry.json +108 -10
- package/docs/MAP.md +2 -1
- package/docs/publication-artifacts.md +12 -10
- package/docs/publication-authority.md +204 -0
- package/package.json +5 -1
- package/packages/core/buildchain-kfd-claims.js +5 -0
- package/packages/core/buildchain-publication-authority.js +79 -0
- package/packages/core/controller-evidence.js +7 -7
- package/packages/core/index.js +28 -0
- package/packages/core/publication-authority.js +725 -0
- package/packages/core/publication-control-plane-audit.js +133 -0
- package/scripts/assemble-self-publication-admission.mjs +182 -0
- package/scripts/audit-publication-control-plane.mjs +406 -0
- package/scripts/check-inventory.mjs +20 -2
- package/scripts/generate-site-bundle.mjs +16 -0
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { validateControllerReceipt } from "./controller-evidence.js";
|
|
4
|
+
import { sha256Json, validateReleaseCandidatePassport } from "./release-candidate.js";
|
|
5
|
+
|
|
6
|
+
export const PUBLICATION_AUTHORITY_REGISTRY_CONTRACT =
|
|
7
|
+
"kungfu-buildchain-publication-authority-registry";
|
|
8
|
+
export const PUBLICATION_ADMISSION_CONTRACT =
|
|
9
|
+
"kungfu-buildchain-publication-admission";
|
|
10
|
+
export const PUBLICATION_CAPABILITY_CONTRACT =
|
|
11
|
+
"kungfu-buildchain-publication-capability";
|
|
12
|
+
export const RUNNER_PROVENANCE_CONTRACT =
|
|
13
|
+
"kungfu-buildchain-runner-provenance";
|
|
14
|
+
export const PUBLICATION_CONTROL_PLANE_AUDIT_CONTRACT =
|
|
15
|
+
"kungfu-buildchain-publication-control-plane-audit";
|
|
16
|
+
export const PUBLICATION_GATE_DECISION_CONTRACT =
|
|
17
|
+
"kungfu-buildchain-publication-gate-decision";
|
|
18
|
+
export const PUBLICATION_ARTIFACT_MANIFEST_SET_CONTRACT =
|
|
19
|
+
"kungfu-buildchain-publication-artifact-manifest-set";
|
|
20
|
+
|
|
21
|
+
export const PUBLICATION_AUTHORITY_CLASSES = Object.freeze([
|
|
22
|
+
"product-publication",
|
|
23
|
+
"evidence-publication",
|
|
24
|
+
"governance-write",
|
|
25
|
+
"non-publication-oidc",
|
|
26
|
+
"dry-run-only",
|
|
27
|
+
"retired-deny",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
export const RUNNER_PROVENANCE_CLASSES = Object.freeze([
|
|
31
|
+
"ephemeral",
|
|
32
|
+
"reimaged",
|
|
33
|
+
"persistent-measured",
|
|
34
|
+
"unqualified",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const QUALIFIED_RUNNER_CLASSES = new Set([
|
|
38
|
+
"ephemeral",
|
|
39
|
+
"reimaged",
|
|
40
|
+
"persistent-measured",
|
|
41
|
+
]);
|
|
42
|
+
const REQUIRED_CONTROL_PLANE_FACTS = Object.freeze([
|
|
43
|
+
"actions-policy",
|
|
44
|
+
"branch-policy",
|
|
45
|
+
"environment-policy",
|
|
46
|
+
"oidc-policy",
|
|
47
|
+
"publisher-policy",
|
|
48
|
+
"runner-policy",
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
function stableJson(value) {
|
|
52
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
53
|
+
if (value && typeof value === "object") {
|
|
54
|
+
return `{${Object.keys(value)
|
|
55
|
+
.sort()
|
|
56
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
|
|
57
|
+
.join(",")}}`;
|
|
58
|
+
}
|
|
59
|
+
return JSON.stringify(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function publicationAuthorityDigest(value) {
|
|
63
|
+
return crypto.createHash("sha256").update(stableJson(value)).digest("hex");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function requiredString(value, label) {
|
|
67
|
+
const normalized = String(value || "").trim();
|
|
68
|
+
if (!normalized) throw new Error(`${label} must be a non-empty string`);
|
|
69
|
+
return normalized;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeDigest(value, label) {
|
|
73
|
+
const normalized = requiredString(value, label).replace(/^sha256:/, "");
|
|
74
|
+
if (!/^[0-9a-f]{64}$/i.test(normalized)) {
|
|
75
|
+
throw new Error(`${label} must be a sha256 digest`);
|
|
76
|
+
}
|
|
77
|
+
return normalized.toLowerCase();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeGitSha(value, label) {
|
|
81
|
+
const normalized = requiredString(value, label).toLowerCase();
|
|
82
|
+
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(normalized)) {
|
|
83
|
+
throw new Error(`${label} must be a 40- or 64-character Git commit SHA`);
|
|
84
|
+
}
|
|
85
|
+
return normalized;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseTime(value, label) {
|
|
89
|
+
const time = Date.parse(requiredString(value, label));
|
|
90
|
+
if (!Number.isFinite(time)) throw new Error(`${label} must be an ISO timestamp`);
|
|
91
|
+
return time;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function detectPublicationAuthoritySignals(workflowText = "") {
|
|
95
|
+
const text = String(workflowText);
|
|
96
|
+
const signals = [];
|
|
97
|
+
const rules = [
|
|
98
|
+
["write-all", /permissions\s*:\s*write-all|permissions\s*:\s*\{[^}]*write-all/i],
|
|
99
|
+
["write-permission", /^\s*(?:contents|packages|deployments|checks|issues|pull-requests|actions)\s*:\s*write\s*$/im],
|
|
100
|
+
["oidc", /^\s*id-token\s*:\s*write\s*$/im],
|
|
101
|
+
["environment", /^\s*environment\s*:/im],
|
|
102
|
+
["npm-publish", /\bnpm\s+(?:stage\s+)?publish\b/i],
|
|
103
|
+
["github-release", /\bgh\s+release\b|repos\.createRelease|repos\.updateRelease/i],
|
|
104
|
+
["git-push", /\bgit\s+push\b/i],
|
|
105
|
+
["cloud-credential", /configure-aws-credentials|google-github-actions\/auth|azure\/login/i],
|
|
106
|
+
];
|
|
107
|
+
for (const [id, pattern] of rules) if (pattern.test(text)) signals.push(id);
|
|
108
|
+
return signals;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function createPublicationAuthorityRegistry({ descriptors = [], workflows = [] } = {}) {
|
|
112
|
+
const byPath = new Map();
|
|
113
|
+
for (const descriptor of descriptors) {
|
|
114
|
+
const workflowPath = requiredString(descriptor.workflowPath, "descriptor.workflowPath");
|
|
115
|
+
const authorityClass = requiredString(descriptor.authorityClass, `${workflowPath}.authorityClass`);
|
|
116
|
+
if (!PUBLICATION_AUTHORITY_CLASSES.includes(authorityClass)) {
|
|
117
|
+
throw new Error(`${workflowPath} has unknown publication authority class: ${authorityClass}`);
|
|
118
|
+
}
|
|
119
|
+
if (byPath.has(workflowPath)) throw new Error(`duplicate publication authority descriptor: ${workflowPath}`);
|
|
120
|
+
byPath.set(workflowPath, {
|
|
121
|
+
workflowPath,
|
|
122
|
+
authorityClass,
|
|
123
|
+
publicationCapable: descriptor.publicationCapable === true,
|
|
124
|
+
capabilityIds: [...new Set((descriptor.capabilityIds || []).map(String))].sort(),
|
|
125
|
+
credentialMode: String(descriptor.credentialMode || "none"),
|
|
126
|
+
publisherWorkflowMode: String(descriptor.publisherWorkflowMode || "fixed"),
|
|
127
|
+
publisherWorkflowPath: String(descriptor.publisherWorkflowPath || workflowPath),
|
|
128
|
+
environment: String(descriptor.environment || ""),
|
|
129
|
+
environmentMode: String(descriptor.environmentMode || "fixed"),
|
|
130
|
+
runnerPolicy: String(descriptor.runnerPolicy || "unqualified"),
|
|
131
|
+
notes: String(descriptor.notes || ""),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const workflowFacts = workflows.map((workflow) => {
|
|
136
|
+
const workflowPath = requiredString(workflow.path, "workflow.path");
|
|
137
|
+
const signals = detectPublicationAuthoritySignals(workflow.text);
|
|
138
|
+
const descriptor = byPath.get(workflowPath);
|
|
139
|
+
if (signals.length > 0 && !descriptor) {
|
|
140
|
+
throw new Error(`authority-bearing workflow is not classified: ${workflowPath} (${signals.join(", ")})`);
|
|
141
|
+
}
|
|
142
|
+
return { path: workflowPath, signals, classified: Boolean(descriptor) };
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const entries = [...byPath.values()].sort((left, right) => left.workflowPath.localeCompare(right.workflowPath));
|
|
146
|
+
const registry = {
|
|
147
|
+
schemaVersion: 1,
|
|
148
|
+
contract: PUBLICATION_AUTHORITY_REGISTRY_CONTRACT,
|
|
149
|
+
defaultDecision: "deny",
|
|
150
|
+
entries,
|
|
151
|
+
workflowFacts: workflowFacts.sort((left, right) => left.path.localeCompare(right.path)),
|
|
152
|
+
};
|
|
153
|
+
return { ...registry, registryDigest: publicationAuthorityDigest(registry) };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function createRunnerProvenance({
|
|
157
|
+
runnerClass,
|
|
158
|
+
os,
|
|
159
|
+
architecture,
|
|
160
|
+
imageDigest,
|
|
161
|
+
measurementDigest,
|
|
162
|
+
baselineDigest = "",
|
|
163
|
+
toolchainDigest = "",
|
|
164
|
+
cacheContractDigest = "",
|
|
165
|
+
taskIsolationDigest = "",
|
|
166
|
+
cleanBaselineProven = false,
|
|
167
|
+
isolation = "",
|
|
168
|
+
} = {}) {
|
|
169
|
+
const normalizedClass = requiredString(runnerClass, "runnerClass");
|
|
170
|
+
if (!RUNNER_PROVENANCE_CLASSES.includes(normalizedClass)) {
|
|
171
|
+
throw new Error(`unknown runner provenance class: ${normalizedClass}`);
|
|
172
|
+
}
|
|
173
|
+
const measuredFloor = [baselineDigest, toolchainDigest, cacheContractDigest, taskIsolationDigest].every(Boolean);
|
|
174
|
+
const qualificationStatus = normalizedClass === "ephemeral"
|
|
175
|
+
? (String(isolation).trim() ? "qualifying" : "unqualified")
|
|
176
|
+
: ["reimaged", "persistent-measured"].includes(normalizedClass)
|
|
177
|
+
? (cleanBaselineProven === true && measuredFloor ? "qualifying" : "unqualified")
|
|
178
|
+
: "unqualified";
|
|
179
|
+
const receipt = {
|
|
180
|
+
schemaVersion: 1,
|
|
181
|
+
contract: RUNNER_PROVENANCE_CONTRACT,
|
|
182
|
+
runnerClass: normalizedClass,
|
|
183
|
+
os: requiredString(os, "os"),
|
|
184
|
+
architecture: requiredString(architecture, "architecture"),
|
|
185
|
+
imageDigest: normalizeDigest(imageDigest, "imageDigest"),
|
|
186
|
+
measurementDigest: normalizeDigest(measurementDigest, "measurementDigest"),
|
|
187
|
+
baselineDigest: baselineDigest ? normalizeDigest(baselineDigest, "baselineDigest") : "",
|
|
188
|
+
toolchainDigest: toolchainDigest ? normalizeDigest(toolchainDigest, "toolchainDigest") : "",
|
|
189
|
+
cacheContractDigest: cacheContractDigest ? normalizeDigest(cacheContractDigest, "cacheContractDigest") : "",
|
|
190
|
+
taskIsolationDigest: taskIsolationDigest ? normalizeDigest(taskIsolationDigest, "taskIsolationDigest") : "",
|
|
191
|
+
cleanBaselineProven: cleanBaselineProven === true,
|
|
192
|
+
qualificationStatus,
|
|
193
|
+
isolation: String(isolation),
|
|
194
|
+
};
|
|
195
|
+
return { ...receipt, receiptDigest: publicationAuthorityDigest(receipt) };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function createPublicationControlPlaneAudit({
|
|
199
|
+
repository,
|
|
200
|
+
workflowPath,
|
|
201
|
+
publisherWorkflowPath,
|
|
202
|
+
environment,
|
|
203
|
+
facts = [],
|
|
204
|
+
observedAt,
|
|
205
|
+
expiresAt,
|
|
206
|
+
} = {}) {
|
|
207
|
+
const normalizedFacts = facts.map((fact, index) => ({
|
|
208
|
+
id: requiredString(fact.id, `facts[${index}].id`),
|
|
209
|
+
status: requiredString(fact.status, `facts[${index}].status`),
|
|
210
|
+
digest: normalizeDigest(fact.digest, `facts[${index}].digest`),
|
|
211
|
+
}));
|
|
212
|
+
const receipt = {
|
|
213
|
+
schemaVersion: 1,
|
|
214
|
+
contract: PUBLICATION_CONTROL_PLANE_AUDIT_CONTRACT,
|
|
215
|
+
repository: requiredString(repository, "repository"),
|
|
216
|
+
workflowPath: requiredString(workflowPath, "workflowPath"),
|
|
217
|
+
publisherWorkflowPath: requiredString(publisherWorkflowPath || workflowPath, "publisherWorkflowPath"),
|
|
218
|
+
environment: requiredString(environment, "environment"),
|
|
219
|
+
observedAt: new Date(parseTime(observedAt, "observedAt")).toISOString(),
|
|
220
|
+
expiresAt: new Date(parseTime(expiresAt, "expiresAt")).toISOString(),
|
|
221
|
+
facts: normalizedFacts.sort((left, right) => left.id.localeCompare(right.id)),
|
|
222
|
+
};
|
|
223
|
+
return { ...receipt, receiptDigest: publicationAuthorityDigest(receipt) };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function createPublicationAdmission(input = {}) {
|
|
227
|
+
const payload = {
|
|
228
|
+
schemaVersion: 1,
|
|
229
|
+
contract: PUBLICATION_ADMISSION_CONTRACT,
|
|
230
|
+
registryDigest: normalizeDigest(input.registryDigest, "registryDigest"),
|
|
231
|
+
workflowPath: requiredString(input.workflowPath, "workflowPath"),
|
|
232
|
+
publisherWorkflowPath: requiredString(input.publisherWorkflowPath || input.workflowPath, "publisherWorkflowPath"),
|
|
233
|
+
repository: requiredString(input.repository, "repository"),
|
|
234
|
+
sourceSha: normalizeGitSha(input.sourceSha, "sourceSha"),
|
|
235
|
+
runtimeSha: normalizeGitSha(input.runtimeSha, "runtimeSha"),
|
|
236
|
+
contractDigest: normalizeDigest(input.contractDigest, "contractDigest"),
|
|
237
|
+
policyDigest: normalizeDigest(input.policyDigest, "policyDigest"),
|
|
238
|
+
controllerReceiptDigest: normalizeDigest(input.controllerReceiptDigest, "controllerReceiptDigest"),
|
|
239
|
+
runnerProvenanceDigest: normalizeDigest(input.runnerProvenanceDigest, "runnerProvenanceDigest"),
|
|
240
|
+
controlPlaneAuditDigest: normalizeDigest(input.controlPlaneAuditDigest, "controlPlaneAuditDigest"),
|
|
241
|
+
gateAggregateDigest: normalizeDigest(input.gateAggregateDigest, "gateAggregateDigest"),
|
|
242
|
+
environment: requiredString(input.environment, "environment"),
|
|
243
|
+
product: requiredString(input.product, "product"),
|
|
244
|
+
target: requiredString(input.target, "target"),
|
|
245
|
+
version: requiredString(input.version, "version"),
|
|
246
|
+
channel: requiredString(input.channel, "channel"),
|
|
247
|
+
artifactDigest: normalizeDigest(input.artifactDigest, "artifactDigest"),
|
|
248
|
+
nonce: requiredString(input.nonce, "nonce"),
|
|
249
|
+
issuedAt: new Date(parseTime(input.issuedAt, "issuedAt")).toISOString(),
|
|
250
|
+
expiresAt: new Date(parseTime(input.expiresAt, "expiresAt")).toISOString(),
|
|
251
|
+
};
|
|
252
|
+
return { ...payload, admissionDigest: publicationAuthorityDigest(payload) };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function createPublicationGateDecision({
|
|
256
|
+
sourceSha,
|
|
257
|
+
profile,
|
|
258
|
+
required = false,
|
|
259
|
+
rationale,
|
|
260
|
+
policy = {},
|
|
261
|
+
} = {}) {
|
|
262
|
+
const normalizedPolicy = {
|
|
263
|
+
profile: requiredString(profile, "profile"),
|
|
264
|
+
required: required === true,
|
|
265
|
+
rationale: requiredString(rationale, "rationale"),
|
|
266
|
+
policy,
|
|
267
|
+
};
|
|
268
|
+
const payload = {
|
|
269
|
+
schemaVersion: 1,
|
|
270
|
+
contract: PUBLICATION_GATE_DECISION_CONTRACT,
|
|
271
|
+
sourceSha: normalizeGitSha(sourceSha, "sourceSha"),
|
|
272
|
+
policyDigest: publicationAuthorityDigest(normalizedPolicy),
|
|
273
|
+
...normalizedPolicy,
|
|
274
|
+
};
|
|
275
|
+
return { ...payload, digest: publicationAuthorityDigest(payload) };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function createPublicationArtifactManifestSet({
|
|
279
|
+
repository,
|
|
280
|
+
sourceSha,
|
|
281
|
+
sourceTreeSha,
|
|
282
|
+
manifests = [],
|
|
283
|
+
payloads = [],
|
|
284
|
+
} = {}) {
|
|
285
|
+
const normalizedRepository = requiredString(repository, "repository");
|
|
286
|
+
const normalizedSourceSha = normalizeGitSha(sourceSha, "sourceSha");
|
|
287
|
+
const normalizedSourceTreeSha = normalizeGitSha(sourceTreeSha, "sourceTreeSha");
|
|
288
|
+
if (!Array.isArray(manifests) || manifests.length === 0) {
|
|
289
|
+
throw new Error("publication artifact manifest set requires at least one manifest");
|
|
290
|
+
}
|
|
291
|
+
if (!Array.isArray(payloads) || payloads.length !== manifests.length) {
|
|
292
|
+
throw new Error("publication artifact payload set must exactly match the manifest set");
|
|
293
|
+
}
|
|
294
|
+
const payloadMap = new Map();
|
|
295
|
+
for (const [index, payload] of payloads.entries()) {
|
|
296
|
+
const artifactName = requiredString(payload?.artifactName, `artifactPayloads[${index}].artifactName`);
|
|
297
|
+
if (payloadMap.has(artifactName)) throw new Error(`duplicate publication artifact payload: ${artifactName}`);
|
|
298
|
+
if (!Array.isArray(payload.files)) throw new Error(`artifactPayloads[${index}].files must be an array`);
|
|
299
|
+
payloadMap.set(artifactName, payload.files);
|
|
300
|
+
}
|
|
301
|
+
const artifacts = manifests.map((manifest, index) => {
|
|
302
|
+
if (manifest?.contract !== "kungfu-buildchain-artifact") {
|
|
303
|
+
throw new Error(`artifactManifests[${index}] contract mismatch`);
|
|
304
|
+
}
|
|
305
|
+
if (manifest.git?.repository !== normalizedRepository) {
|
|
306
|
+
throw new Error(`artifactManifests[${index}] repository mismatch`);
|
|
307
|
+
}
|
|
308
|
+
if (manifest.git?.sha !== normalizedSourceSha) {
|
|
309
|
+
throw new Error(`artifactManifests[${index}] source SHA mismatch`);
|
|
310
|
+
}
|
|
311
|
+
if (manifest.expectedArtifacts?.ok !== true) {
|
|
312
|
+
throw new Error(`artifactManifests[${index}] expected artifacts did not qualify`);
|
|
313
|
+
}
|
|
314
|
+
const artifactName = requiredString(manifest.artifactName, `artifactManifests[${index}].artifactName`);
|
|
315
|
+
if (manifest.summary?.contract !== "kungfu-buildchain-artifact-summary") {
|
|
316
|
+
throw new Error(`artifactManifests[${index}] summary contract mismatch`);
|
|
317
|
+
}
|
|
318
|
+
if (manifest.summary?.artifactName !== artifactName || manifest.summary?.platform?.id !== manifest.platform?.id) {
|
|
319
|
+
throw new Error(`artifactManifests[${index}] summary identity mismatch`);
|
|
320
|
+
}
|
|
321
|
+
if (!Array.isArray(manifest.files)) throw new Error(`artifactManifests[${index}].files must be an array`);
|
|
322
|
+
const declaredFiles = manifest.files.map((file, fileIndex) => {
|
|
323
|
+
const filePath = requiredString(file?.path, `artifactManifests[${index}].files[${fileIndex}].path`);
|
|
324
|
+
if (filePath.startsWith("/") || filePath.includes("\\") || filePath.split("/").includes("..")) {
|
|
325
|
+
throw new Error(`artifactManifests[${index}] contains an unsafe file path`);
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
path: filePath,
|
|
329
|
+
size: Number(file?.size),
|
|
330
|
+
sha256: normalizeDigest(file?.sha256, `artifactManifests[${index}].files[${fileIndex}].sha256`),
|
|
331
|
+
};
|
|
332
|
+
});
|
|
333
|
+
if (new Set(declaredFiles.map((file) => file.path)).size !== declaredFiles.length) {
|
|
334
|
+
throw new Error(`artifactManifests[${index}] contains duplicate file paths`);
|
|
335
|
+
}
|
|
336
|
+
if (declaredFiles.some((file) => !Number.isSafeInteger(file.size) || file.size < 0)) {
|
|
337
|
+
throw new Error(`artifactManifests[${index}] contains an invalid file size`);
|
|
338
|
+
}
|
|
339
|
+
const totalBytes = declaredFiles.reduce((sum, file) => sum + file.size, 0);
|
|
340
|
+
const contentHash = crypto.createHash("sha256");
|
|
341
|
+
for (const file of declaredFiles) contentHash.update(`${file.path}\0${file.size}\0${file.sha256}\n`);
|
|
342
|
+
const contentDigest = contentHash.digest("hex");
|
|
343
|
+
if (manifest.summary.fileCount !== declaredFiles.length || manifest.summary.totalBytes !== totalBytes) {
|
|
344
|
+
throw new Error(`artifactManifests[${index}] summary counts mismatch`);
|
|
345
|
+
}
|
|
346
|
+
if (normalizeDigest(manifest.summary.digest, `artifactManifests[${index}].summary.digest`) !== contentDigest) {
|
|
347
|
+
throw new Error(`artifactManifests[${index}] summary digest mismatch`);
|
|
348
|
+
}
|
|
349
|
+
const declaredPayloadFiles = declaredFiles.filter((file) => !file.path.startsWith(".buildchain/"));
|
|
350
|
+
if (declaredPayloadFiles.length === 0) {
|
|
351
|
+
throw new Error(`artifactManifests[${index}] declares no independently verifiable product payload files`);
|
|
352
|
+
}
|
|
353
|
+
const actualFiles = payloadMap.get(artifactName);
|
|
354
|
+
if (!actualFiles) throw new Error(`publication artifact payload is missing: ${artifactName}`);
|
|
355
|
+
const normalizedActualFiles = actualFiles.map((file, fileIndex) => ({
|
|
356
|
+
path: requiredString(file?.path, `artifactPayloads[${index}].files[${fileIndex}].path`),
|
|
357
|
+
size: Number(file?.size),
|
|
358
|
+
sha256: normalizeDigest(file?.sha256, `artifactPayloads[${index}].files[${fileIndex}].sha256`),
|
|
359
|
+
}));
|
|
360
|
+
const byPath = (left, right) => left.path.localeCompare(right.path);
|
|
361
|
+
if (JSON.stringify([...declaredPayloadFiles].sort(byPath)) !== JSON.stringify([...normalizedActualFiles].sort(byPath))) {
|
|
362
|
+
throw new Error(`publication artifact payload bytes do not match manifest: ${artifactName}`);
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
artifactName,
|
|
366
|
+
platformId: requiredString(manifest.platform?.id, `artifactManifests[${index}].platform.id`),
|
|
367
|
+
manifestDigest: publicationAuthorityDigest(manifest),
|
|
368
|
+
contentDigest,
|
|
369
|
+
productPayloadDigest: publicationAuthorityDigest([...normalizedActualFiles].sort(byPath)),
|
|
370
|
+
};
|
|
371
|
+
}).sort((left, right) => left.artifactName.localeCompare(right.artifactName));
|
|
372
|
+
const payload = {
|
|
373
|
+
schemaVersion: 1,
|
|
374
|
+
contract: PUBLICATION_ARTIFACT_MANIFEST_SET_CONTRACT,
|
|
375
|
+
repository: normalizedRepository,
|
|
376
|
+
sourceSha: normalizedSourceSha,
|
|
377
|
+
sourceTreeSha: normalizedSourceTreeSha,
|
|
378
|
+
artifacts,
|
|
379
|
+
};
|
|
380
|
+
return { ...payload, manifestSetDigest: publicationAuthorityDigest(payload) };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function validateGateAggregate(gateAggregate, { admission, passport }) {
|
|
384
|
+
if (gateAggregate?.contract === "buildchain.shifu-gate-aggregate/v1") {
|
|
385
|
+
const { digest, ...payload } = gateAggregate;
|
|
386
|
+
const actualDigest = sha256Json(payload);
|
|
387
|
+
if (normalizeDigest(digest, "gateAggregate.digest") !== actualDigest) {
|
|
388
|
+
throw new Error("gate aggregate digest mismatch");
|
|
389
|
+
}
|
|
390
|
+
if (gateAggregate.status !== "pass" || gateAggregate.qualifying !== true) {
|
|
391
|
+
throw new Error("gate aggregate is not qualifying");
|
|
392
|
+
}
|
|
393
|
+
if (![admission.sourceSha, passport.source?.headSha].includes(gateAggregate.sourceSha)) {
|
|
394
|
+
throw new Error("gate aggregate source SHA mismatch");
|
|
395
|
+
}
|
|
396
|
+
normalizeDigest(gateAggregate.registry?.digest, "gateAggregate.registry.digest");
|
|
397
|
+
const policyDigest = normalizeDigest(gateAggregate.matrixDigest, "gateAggregate.matrixDigest");
|
|
398
|
+
return { gateAggregateDigest: actualDigest, policyDigest };
|
|
399
|
+
}
|
|
400
|
+
if (gateAggregate?.contract !== PUBLICATION_GATE_DECISION_CONTRACT) {
|
|
401
|
+
throw new Error("gate evidence must be a Shifu Gate aggregate or explicit publication Gate decision");
|
|
402
|
+
}
|
|
403
|
+
const { digest, ...payload } = gateAggregate;
|
|
404
|
+
const actualDigest = publicationAuthorityDigest(payload);
|
|
405
|
+
if (normalizeDigest(digest, "gateDecision.digest") !== actualDigest) {
|
|
406
|
+
throw new Error("publication Gate decision digest mismatch");
|
|
407
|
+
}
|
|
408
|
+
if (gateAggregate.required !== false) {
|
|
409
|
+
throw new Error("a required Gate policy must supply a qualifying Shifu Gate aggregate");
|
|
410
|
+
}
|
|
411
|
+
if (![admission.sourceSha, passport.source?.headSha].includes(gateAggregate.sourceSha)) {
|
|
412
|
+
throw new Error("publication Gate decision source SHA mismatch");
|
|
413
|
+
}
|
|
414
|
+
const normalizedPolicy = {
|
|
415
|
+
profile: requiredString(gateAggregate.profile, "gateDecision.profile"),
|
|
416
|
+
required: false,
|
|
417
|
+
rationale: requiredString(gateAggregate.rationale, "gateDecision.rationale"),
|
|
418
|
+
policy: gateAggregate.policy || {},
|
|
419
|
+
};
|
|
420
|
+
const policyDigest = publicationAuthorityDigest(normalizedPolicy);
|
|
421
|
+
if (normalizeDigest(gateAggregate.policyDigest, "gateDecision.policyDigest") !== policyDigest) {
|
|
422
|
+
throw new Error("publication Gate policy digest mismatch");
|
|
423
|
+
}
|
|
424
|
+
return { gateAggregateDigest: actualDigest, policyDigest };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function validatePublicationEvidence(publicationEvidence, admission) {
|
|
428
|
+
if (!publicationEvidence || typeof publicationEvidence !== "object" || Array.isArray(publicationEvidence)) {
|
|
429
|
+
throw new Error("independent publication evidence is required");
|
|
430
|
+
}
|
|
431
|
+
const passport = publicationEvidence.releaseCandidatePassport;
|
|
432
|
+
const buildSummary = publicationEvidence.buildSummary;
|
|
433
|
+
const passportValidation = validateReleaseCandidatePassport({
|
|
434
|
+
passport,
|
|
435
|
+
buildSummary,
|
|
436
|
+
repository: admission.repository,
|
|
437
|
+
targetChannel: admission.channel,
|
|
438
|
+
});
|
|
439
|
+
if (!passportValidation.ok) {
|
|
440
|
+
throw new Error(`release-candidate passport did not qualify: ${passportValidation.errors.join("; ")}`);
|
|
441
|
+
}
|
|
442
|
+
const candidateHash = sha256Json({
|
|
443
|
+
repository: passport.repository,
|
|
444
|
+
target: passport.target,
|
|
445
|
+
source: passport.source,
|
|
446
|
+
platformMatrix: passport.platformMatrix,
|
|
447
|
+
buildchain: passport.buildchain,
|
|
448
|
+
...(passport.gateProfileEvidence ? { gateProfileEvidence: passport.gateProfileEvidence } : {}),
|
|
449
|
+
...(passport.controllerReceipts ? { controllerReceipts: passport.controllerReceipts } : {}),
|
|
450
|
+
});
|
|
451
|
+
if (normalizeDigest(passport.candidateHash, "releaseCandidatePassport.candidateHash") !== candidateHash) {
|
|
452
|
+
throw new Error("release-candidate passport candidate hash mismatch");
|
|
453
|
+
}
|
|
454
|
+
const sourceTreeSha = normalizeGitSha(publicationEvidence.sourceTreeSha, "publicationEvidence.sourceTreeSha");
|
|
455
|
+
if (passport.source?.treeHash !== sourceTreeSha) {
|
|
456
|
+
throw new Error("release-candidate source tree does not match the admitted source commit");
|
|
457
|
+
}
|
|
458
|
+
const controllerReceipt = publicationEvidence.controllerReceipt;
|
|
459
|
+
const controllerValidation = validateControllerReceipt(controllerReceipt, {
|
|
460
|
+
expectedSourceSha: passport.source?.headSha || "",
|
|
461
|
+
expectedRuntimeSha: passport.buildchain?.sha || "",
|
|
462
|
+
});
|
|
463
|
+
if (!controllerValidation.ok || !controllerValidation.qualifying) {
|
|
464
|
+
throw new Error(`controller receipt did not qualify: ${controllerValidation.issues.join("; ")}`);
|
|
465
|
+
}
|
|
466
|
+
const reference = (passport.controllerReceipts || []).find(
|
|
467
|
+
(entry) => entry.controllerId === controllerReceipt.controller?.id,
|
|
468
|
+
);
|
|
469
|
+
if (!reference || reference.receiptDigest !== controllerReceipt.digest) {
|
|
470
|
+
throw new Error("release-candidate controller receipt reference mismatch");
|
|
471
|
+
}
|
|
472
|
+
const controllerReceiptDigest = normalizeDigest(controllerReceipt.digest, "controllerReceipt.digest");
|
|
473
|
+
if (controllerReceiptDigest !== normalizeDigest(admission.controllerReceiptDigest, "admission.controllerReceiptDigest")) {
|
|
474
|
+
throw new Error("controller receipt evidence binding mismatch");
|
|
475
|
+
}
|
|
476
|
+
const contractDigest = normalizeDigest(controllerReceipt.runtime?.contractDigest, "controllerReceipt.runtime.contractDigest");
|
|
477
|
+
if (contractDigest !== normalizeDigest(admission.contractDigest, "admission.contractDigest")) {
|
|
478
|
+
throw new Error("runtime contract evidence binding mismatch");
|
|
479
|
+
}
|
|
480
|
+
const gate = validateGateAggregate(publicationEvidence.gateAggregate, { admission, passport });
|
|
481
|
+
if (passport.gateProfileEvidence?.digest && passport.gateProfileEvidence.digest !== `sha256:${gate.gateAggregateDigest}`) {
|
|
482
|
+
throw new Error("release-candidate passport Gate evidence binding mismatch");
|
|
483
|
+
}
|
|
484
|
+
if (gate.gateAggregateDigest !== normalizeDigest(admission.gateAggregateDigest, "admission.gateAggregateDigest")) {
|
|
485
|
+
throw new Error("Gate aggregate evidence binding mismatch");
|
|
486
|
+
}
|
|
487
|
+
if (gate.policyDigest !== normalizeDigest(admission.policyDigest, "admission.policyDigest")) {
|
|
488
|
+
throw new Error("Gate policy evidence binding mismatch");
|
|
489
|
+
}
|
|
490
|
+
const manifestSet = createPublicationArtifactManifestSet({
|
|
491
|
+
repository: admission.repository,
|
|
492
|
+
sourceSha: passport.source?.headSha,
|
|
493
|
+
sourceTreeSha,
|
|
494
|
+
manifests: publicationEvidence.artifactManifests,
|
|
495
|
+
payloads: publicationEvidence.artifactPayloads,
|
|
496
|
+
});
|
|
497
|
+
const passportPlatforms = (passport.platformMatrix || [])
|
|
498
|
+
.map((entry) => `${entry.platformId}\0${entry.artifactName}`)
|
|
499
|
+
.sort();
|
|
500
|
+
const manifestPlatforms = manifestSet.artifacts
|
|
501
|
+
.map((entry) => `${entry.platformId}\0${entry.artifactName}`)
|
|
502
|
+
.sort();
|
|
503
|
+
if (JSON.stringify(passportPlatforms) !== JSON.stringify(manifestPlatforms)) {
|
|
504
|
+
throw new Error("artifact manifest set does not match the release-candidate platform matrix");
|
|
505
|
+
}
|
|
506
|
+
if (manifestSet.manifestSetDigest !== normalizeDigest(admission.artifactDigest, "admission.artifactDigest")) {
|
|
507
|
+
throw new Error("artifact manifest evidence binding mismatch");
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
sourceTreeSha,
|
|
511
|
+
controllerReceiptDigest,
|
|
512
|
+
contractDigest,
|
|
513
|
+
gateAggregateDigest: gate.gateAggregateDigest,
|
|
514
|
+
policyDigest: gate.policyDigest,
|
|
515
|
+
artifactDigest: manifestSet.manifestSetDigest,
|
|
516
|
+
evidenceDigest: publicationAuthorityDigest({
|
|
517
|
+
passportCandidateHash: passport.candidateHash,
|
|
518
|
+
sourceTreeSha,
|
|
519
|
+
controllerReceiptDigest,
|
|
520
|
+
gateAggregateDigest: gate.gateAggregateDigest,
|
|
521
|
+
artifactDigest: manifestSet.manifestSetDigest,
|
|
522
|
+
}),
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function validateRunnerProvenance(receipt, expectedDigest) {
|
|
527
|
+
if (receipt?.contract !== RUNNER_PROVENANCE_CONTRACT) throw new Error("runner provenance contract mismatch");
|
|
528
|
+
if (!QUALIFIED_RUNNER_CLASSES.has(receipt.runnerClass)) {
|
|
529
|
+
throw new Error(`runner provenance is not qualified: ${receipt.runnerClass || "unknown"}`);
|
|
530
|
+
}
|
|
531
|
+
if (receipt.qualificationStatus !== "qualifying") {
|
|
532
|
+
throw new Error(`runner provenance qualification floor was not met: ${receipt.runnerClass}`);
|
|
533
|
+
}
|
|
534
|
+
const { receiptDigest: supplied, ...payload } = receipt;
|
|
535
|
+
const actual = publicationAuthorityDigest(payload);
|
|
536
|
+
if (normalizeDigest(supplied, "runnerProvenance.receiptDigest") !== actual) {
|
|
537
|
+
throw new Error("runner provenance digest mismatch");
|
|
538
|
+
}
|
|
539
|
+
if (expectedDigest && normalizeDigest(expectedDigest, "runnerProvenanceDigest") !== actual) {
|
|
540
|
+
throw new Error("runner provenance binding mismatch");
|
|
541
|
+
}
|
|
542
|
+
return actual;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function validateControlPlaneAudit(receipt, {
|
|
546
|
+
repository,
|
|
547
|
+
workflowPath,
|
|
548
|
+
publisherWorkflowPath,
|
|
549
|
+
environment,
|
|
550
|
+
now,
|
|
551
|
+
expectedDigest,
|
|
552
|
+
}) {
|
|
553
|
+
if (receipt?.contract !== PUBLICATION_CONTROL_PLANE_AUDIT_CONTRACT) {
|
|
554
|
+
throw new Error("publication control-plane audit contract mismatch");
|
|
555
|
+
}
|
|
556
|
+
if (receipt.workflowPath !== workflowPath) throw new Error("control-plane workflow binding mismatch");
|
|
557
|
+
if (receipt.publisherWorkflowPath !== publisherWorkflowPath) {
|
|
558
|
+
throw new Error("control-plane publisher workflow binding mismatch");
|
|
559
|
+
}
|
|
560
|
+
if (receipt.repository !== repository) throw new Error("control-plane repository binding mismatch");
|
|
561
|
+
if (receipt.environment !== environment) throw new Error("control-plane environment binding mismatch");
|
|
562
|
+
if (parseTime(receipt.observedAt, "controlPlaneAudit.observedAt") > now) {
|
|
563
|
+
throw new Error("control-plane audit is from the future");
|
|
564
|
+
}
|
|
565
|
+
if (parseTime(receipt.expiresAt, "controlPlaneAudit.expiresAt") <= now) {
|
|
566
|
+
throw new Error("control-plane audit is stale");
|
|
567
|
+
}
|
|
568
|
+
const facts = new Map((receipt.facts || []).map((fact) => [fact.id, fact]));
|
|
569
|
+
for (const id of REQUIRED_CONTROL_PLANE_FACTS) {
|
|
570
|
+
const fact = facts.get(id);
|
|
571
|
+
if (!fact) throw new Error(`control-plane audit is missing required fact: ${id}`);
|
|
572
|
+
if (fact.status !== "pass") throw new Error(`control-plane audit fact did not pass: ${id}`);
|
|
573
|
+
normalizeDigest(fact.digest, `controlPlaneAudit.${id}.digest`);
|
|
574
|
+
}
|
|
575
|
+
const { receiptDigest: supplied, ...payload } = receipt;
|
|
576
|
+
const actual = publicationAuthorityDigest(payload);
|
|
577
|
+
if (normalizeDigest(supplied, "controlPlaneAudit.receiptDigest") !== actual) {
|
|
578
|
+
throw new Error("control-plane audit digest mismatch");
|
|
579
|
+
}
|
|
580
|
+
if (expectedDigest && normalizeDigest(expectedDigest, "controlPlaneAuditDigest") !== actual) {
|
|
581
|
+
throw new Error("control-plane audit binding mismatch");
|
|
582
|
+
}
|
|
583
|
+
return actual;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export function verifyPublicationAdmission({
|
|
587
|
+
admission,
|
|
588
|
+
registry,
|
|
589
|
+
runnerProvenance,
|
|
590
|
+
controlPlaneAudit,
|
|
591
|
+
publicationEvidence,
|
|
592
|
+
expected = {},
|
|
593
|
+
usedNonces = [],
|
|
594
|
+
now = new Date(),
|
|
595
|
+
} = {}) {
|
|
596
|
+
if (admission?.contract !== PUBLICATION_ADMISSION_CONTRACT) {
|
|
597
|
+
throw new Error("publication admission contract mismatch");
|
|
598
|
+
}
|
|
599
|
+
if (registry?.contract !== PUBLICATION_AUTHORITY_REGISTRY_CONTRACT) {
|
|
600
|
+
throw new Error("publication authority registry contract mismatch");
|
|
601
|
+
}
|
|
602
|
+
const { registryDigest: suppliedRegistryDigest, ...registryPayload } = registry;
|
|
603
|
+
const actualRegistryDigest = publicationAuthorityDigest(registryPayload);
|
|
604
|
+
if (normalizeDigest(suppliedRegistryDigest, "registry.registryDigest") !== actualRegistryDigest) {
|
|
605
|
+
throw new Error("publication authority registry digest mismatch");
|
|
606
|
+
}
|
|
607
|
+
if (normalizeDigest(admission.registryDigest, "admission.registryDigest") !== actualRegistryDigest) {
|
|
608
|
+
throw new Error("publication authority registry binding mismatch");
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const workflowPath = requiredString(admission.workflowPath, "admission.workflowPath");
|
|
612
|
+
const descriptor = registry.entries?.find((entry) => entry.workflowPath === workflowPath);
|
|
613
|
+
if (!descriptor) throw new Error(`unknown publication workflow: ${workflowPath}`);
|
|
614
|
+
if (!descriptor.publicationCapable || descriptor.authorityClass !== "product-publication") {
|
|
615
|
+
throw new Error(`workflow is not product-publication capable: ${workflowPath}`);
|
|
616
|
+
}
|
|
617
|
+
const publisherWorkflowMode = descriptor.publisherWorkflowMode || "fixed";
|
|
618
|
+
if (!["fixed", "caller-bound"].includes(publisherWorkflowMode)) {
|
|
619
|
+
throw new Error(`unsupported publisher workflow mode: ${publisherWorkflowMode}`);
|
|
620
|
+
}
|
|
621
|
+
const publisherWorkflowPath = publisherWorkflowMode === "caller-bound"
|
|
622
|
+
? requiredString(admission.publisherWorkflowPath, "admission.publisherWorkflowPath")
|
|
623
|
+
: requiredString(descriptor.publisherWorkflowPath || workflowPath, "descriptor.publisherWorkflowPath");
|
|
624
|
+
if (publisherWorkflowMode === "fixed" && admission.publisherWorkflowPath !== publisherWorkflowPath) {
|
|
625
|
+
throw new Error("publication admission publisher workflow binding mismatch");
|
|
626
|
+
}
|
|
627
|
+
const environmentMode = descriptor.environmentMode || "fixed";
|
|
628
|
+
if (!["fixed", "caller-bound"].includes(environmentMode)) {
|
|
629
|
+
throw new Error(`unsupported publication environment mode: ${environmentMode}`);
|
|
630
|
+
}
|
|
631
|
+
const environment = environmentMode === "caller-bound"
|
|
632
|
+
? requiredString(admission.environment, "admission.environment")
|
|
633
|
+
: descriptor.environment;
|
|
634
|
+
if (!environment) throw new Error("publication authority descriptor has no protected environment");
|
|
635
|
+
if (environmentMode === "fixed" && admission.environment !== environment) {
|
|
636
|
+
throw new Error("publication admission environment binding mismatch");
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const nowMs = now instanceof Date ? now.getTime() : parseTime(now, "now");
|
|
640
|
+
const issuedAt = parseTime(admission.issuedAt, "admission.issuedAt");
|
|
641
|
+
const expiresAt = parseTime(admission.expiresAt, "admission.expiresAt");
|
|
642
|
+
if (issuedAt > nowMs) throw new Error("publication admission is from the future");
|
|
643
|
+
if (expiresAt <= nowMs) throw new Error("publication admission is stale");
|
|
644
|
+
if (expiresAt - issuedAt > 15 * 60 * 1000) throw new Error("publication admission lifetime exceeds 15 minutes");
|
|
645
|
+
const nonce = requiredString(admission.nonce, "admission.nonce");
|
|
646
|
+
if (new Set(usedNonces.map(String)).has(nonce)) throw new Error("publication admission nonce was replayed");
|
|
647
|
+
|
|
648
|
+
const bindings = [
|
|
649
|
+
"repository",
|
|
650
|
+
"publisherWorkflowPath",
|
|
651
|
+
"sourceSha",
|
|
652
|
+
"runtimeSha",
|
|
653
|
+
"contractDigest",
|
|
654
|
+
"policyDigest",
|
|
655
|
+
"controllerReceiptDigest",
|
|
656
|
+
"gateAggregateDigest",
|
|
657
|
+
"environment",
|
|
658
|
+
"product",
|
|
659
|
+
"target",
|
|
660
|
+
"version",
|
|
661
|
+
"channel",
|
|
662
|
+
"artifactDigest",
|
|
663
|
+
];
|
|
664
|
+
for (const key of bindings) {
|
|
665
|
+
const actual = requiredString(admission[key], `admission.${key}`);
|
|
666
|
+
if (expected[key] === undefined) {
|
|
667
|
+
throw new Error(`publication admission expected ${key} binding is required`);
|
|
668
|
+
}
|
|
669
|
+
if (String(expected[key]) !== actual) {
|
|
670
|
+
throw new Error(`publication admission ${key} binding mismatch`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
for (const key of ["sourceSha", "runtimeSha"]) {
|
|
674
|
+
normalizeGitSha(admission[key], `admission.${key}`);
|
|
675
|
+
}
|
|
676
|
+
for (const key of ["contractDigest", "policyDigest", "controllerReceiptDigest", "gateAggregateDigest", "artifactDigest"]) {
|
|
677
|
+
normalizeDigest(admission[key], `admission.${key}`);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const runnerDigest = validateRunnerProvenance(runnerProvenance, admission.runnerProvenanceDigest);
|
|
681
|
+
const controlPlaneDigest = validateControlPlaneAudit(controlPlaneAudit, {
|
|
682
|
+
repository: admission.repository,
|
|
683
|
+
workflowPath,
|
|
684
|
+
publisherWorkflowPath,
|
|
685
|
+
environment,
|
|
686
|
+
now: nowMs,
|
|
687
|
+
expectedDigest: admission.controlPlaneAuditDigest,
|
|
688
|
+
});
|
|
689
|
+
const evidence = validatePublicationEvidence(publicationEvidence, admission);
|
|
690
|
+
const { admissionDigest: suppliedAdmissionDigest, producerDecision: _ignored, ...admissionPayload } = admission;
|
|
691
|
+
const actualAdmissionDigest = publicationAuthorityDigest(admissionPayload);
|
|
692
|
+
if (normalizeDigest(suppliedAdmissionDigest, "admission.admissionDigest") !== actualAdmissionDigest) {
|
|
693
|
+
throw new Error("publication admission digest mismatch");
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const capability = {
|
|
697
|
+
schemaVersion: 1,
|
|
698
|
+
contract: PUBLICATION_CAPABILITY_CONTRACT,
|
|
699
|
+
decision: "allow",
|
|
700
|
+
workflowPath,
|
|
701
|
+
publisherWorkflowPath,
|
|
702
|
+
capabilityIds: descriptor.capabilityIds,
|
|
703
|
+
repository: admission.repository,
|
|
704
|
+
sourceSha: normalizeGitSha(admission.sourceSha, "admission.sourceSha"),
|
|
705
|
+
runtimeSha: normalizeGitSha(admission.runtimeSha, "admission.runtimeSha"),
|
|
706
|
+
contractDigest: normalizeDigest(admission.contractDigest, "admission.contractDigest"),
|
|
707
|
+
policyDigest: normalizeDigest(admission.policyDigest, "admission.policyDigest"),
|
|
708
|
+
controllerReceiptDigest: normalizeDigest(admission.controllerReceiptDigest, "admission.controllerReceiptDigest"),
|
|
709
|
+
product: admission.product,
|
|
710
|
+
target: admission.target,
|
|
711
|
+
version: admission.version,
|
|
712
|
+
channel: admission.channel,
|
|
713
|
+
artifactDigest: normalizeDigest(admission.artifactDigest, "admission.artifactDigest"),
|
|
714
|
+
sourceTreeSha: evidence.sourceTreeSha,
|
|
715
|
+
publicationEvidenceDigest: evidence.evidenceDigest,
|
|
716
|
+
admissionDigest: actualAdmissionDigest,
|
|
717
|
+
runnerProvenanceDigest: runnerDigest,
|
|
718
|
+
controlPlaneAuditDigest: controlPlaneDigest,
|
|
719
|
+
gateAggregateDigest: normalizeDigest(admission.gateAggregateDigest, "admission.gateAggregateDigest"),
|
|
720
|
+
environment,
|
|
721
|
+
nonce,
|
|
722
|
+
expiresAt: admission.expiresAt,
|
|
723
|
+
};
|
|
724
|
+
return { ...capability, capabilityDigest: publicationAuthorityDigest(capability) };
|
|
725
|
+
}
|