@kungfu-tech/buildchain 3.0.5-alpha.8 → 3.0.6-alpha.0
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/contracts/auditable-demo-scenario-v1.schema.json +11 -0
- package/dist/site/buildchain-contract.json +42 -22
- package/dist/site/buildchain-site.json +73 -27
- package/dist/site/capability-registry.json +2 -2
- package/dist/site/cli-registry.json +5 -3
- package/dist/site/controller-registry.json +19 -3
- package/dist/site/kfd-claims.json +58 -14
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +7 -6
- package/dist/site/node-api-registry.json +335 -8
- package/dist/site/page-registry.json +63 -17
- package/dist/site/public-surface-audit.json +94 -11
- package/dist/site/publication-authority-registry.json +21 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +10 -10
- package/dist/site/workflow-registry.json +53 -4
- package/docs/MAP.md +4 -1
- package/docs/auditable-demo.md +19 -3
- package/docs/cli-reference.md +15 -4
- package/docs/dev-alpha-candidate-patrol.md +14 -5
- package/docs/dev-qualification-patrol.md +108 -0
- package/docs/node-api-reference.md +42 -9
- package/docs/release-propagation.md +93 -0
- package/docs/reusable-build-surface.md +44 -12
- package/docs/stable-candidate-patrol.md +5 -2
- package/package.json +1 -1
- package/packages/core/buildchain-publication-authority.js +1 -0
- package/packages/core/release-propagation-agent-entry.js +185 -0
- package/packages/core/release-propagation-push.js +293 -0
- package/packages/core/release-propagation-stage-evidence.js +36 -0
- package/packages/core/release-propagation-work.js +1 -1
- package/packages/core/release-propagation.js +15 -0
- package/scripts/artifact-signing-controller-core.mjs +525 -0
- package/scripts/artifact-signing-controller.mjs +302 -0
- package/scripts/artifact-signing-delegation.mjs +51 -3
- package/scripts/auditable-demo-platform.mjs +59 -1
- package/scripts/auditable-demo-transport-smoke.mjs +176 -0
- package/scripts/build-standalone-binary.mjs +3 -1
- package/scripts/buildchain-cli-help.mjs +1 -1
- package/scripts/check-inventory.mjs +1 -0
- package/scripts/compiler-cache-evidence.mjs +90 -7
- package/scripts/dev-alpha-candidate-patrol.mjs +80 -5
- package/scripts/dev-pr-auto-merge.mjs +4 -4
- package/scripts/dev-qualification-patrol.mjs +594 -0
- package/scripts/dispatch-artifact-signing-authority.mjs +312 -61
- package/scripts/generate-site-bundle.mjs +3 -0
- package/scripts/inspect-artifact-signing-requests.mjs +12 -0
- package/scripts/release-propagation.mjs +73 -0
- package/scripts/run-lifecycle-core.mjs +25 -0
- package/scripts/site-capability-metadata.mjs +1 -1
- package/scripts/stable-candidate-patrol.mjs +30 -4
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
RECEIPT_CONTRACT,
|
|
8
|
+
artifactSigningControlRequestOutputs,
|
|
9
|
+
assertArtifactSigningControlRequestContext,
|
|
10
|
+
digestDocument,
|
|
11
|
+
normalizeControllerStatus,
|
|
12
|
+
readArtifactSigningControlRequest,
|
|
13
|
+
required,
|
|
14
|
+
sealArtifactSigningControlRequest,
|
|
15
|
+
validateArtifactSigningControlRequest,
|
|
16
|
+
validateArtifactSigningControllerReceipt,
|
|
17
|
+
} from "./artifact-signing-controller-core.mjs";
|
|
18
|
+
import {
|
|
19
|
+
assertArtifactSigningDelegationContext,
|
|
20
|
+
artifactSigningDelegationOutputs,
|
|
21
|
+
createArtifactSigningDelegation,
|
|
22
|
+
readArtifactSigningDelegation,
|
|
23
|
+
} from "./artifact-signing-delegation.mjs";
|
|
24
|
+
import { writeGitHubOutputs } from "./build-contract-core.mjs";
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
artifactSigningCorrelation,
|
|
28
|
+
artifactSigningRequestRoot,
|
|
29
|
+
artifactSigningControlRequestOutputs,
|
|
30
|
+
assertArtifactSigningControlRequestContext,
|
|
31
|
+
createArtifactSigningControlRequest,
|
|
32
|
+
readArtifactSigningControlRequest,
|
|
33
|
+
sealArtifactSigningControlRequest,
|
|
34
|
+
validateArtifactSigningControlRequest,
|
|
35
|
+
validateArtifactSigningControllerReceipt,
|
|
36
|
+
} from "./artifact-signing-controller-core.mjs";
|
|
37
|
+
|
|
38
|
+
function authorityRuntimeShaFromEnvironment() {
|
|
39
|
+
return process.env.BUILDCHAIN_AUTHORITY_RUNTIME_SHA || "";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function settleArtifactSigningControl({
|
|
43
|
+
request,
|
|
44
|
+
authorityStatus = process.env.BUILDCHAIN_AUTHORITY_STATUS || "failed",
|
|
45
|
+
authorityRunId = process.env.BUILDCHAIN_AUTHORITY_RUN_ID || "",
|
|
46
|
+
authorityRuntimeSha = authorityRuntimeShaFromEnvironment(),
|
|
47
|
+
authorityRunUrl = process.env.BUILDCHAIN_AUTHORITY_RUN_URL || "",
|
|
48
|
+
authorityResultArtifact = process.env.BUILDCHAIN_SIGNING_RESULT_ARTIFACT ||
|
|
49
|
+
"",
|
|
50
|
+
authorityCorrelationId = process.env.BUILDCHAIN_AUTHORITY_CORRELATION_ID ||
|
|
51
|
+
"",
|
|
52
|
+
authorityConclusion = process.env.BUILDCHAIN_AUTHORITY_CONCLUSION || "",
|
|
53
|
+
controllerRepository = process.env.GITHUB_REPOSITORY,
|
|
54
|
+
controllerRunId = process.env.GITHUB_RUN_ID,
|
|
55
|
+
controllerRunAttempt = process.env.GITHUB_RUN_ATTEMPT || "1",
|
|
56
|
+
controllerJob = process.env.GITHUB_JOB || "artifact-signing-control",
|
|
57
|
+
controllerRunnerOs = process.env.RUNNER_OS || "Linux",
|
|
58
|
+
controllerStartedAt = process.env.BUILDCHAIN_CONTROLLER_STARTED_AT ||
|
|
59
|
+
new Date().toISOString(),
|
|
60
|
+
controllerCompletedAt = process.env.BUILDCHAIN_CONTROLLER_COMPLETED_AT ||
|
|
61
|
+
new Date().toISOString(),
|
|
62
|
+
receiptPath = process.env.BUILDCHAIN_SIGNING_CONTROLLER_RECEIPT_PATH,
|
|
63
|
+
delegationPath = process.env.BUILDCHAIN_SIGNING_DELEGATION_PATH,
|
|
64
|
+
} = {}) {
|
|
65
|
+
const control = validateArtifactSigningControlRequest(request);
|
|
66
|
+
const status = normalizeControllerStatus(authorityStatus);
|
|
67
|
+
const expectedStatus = control.request.count === 0 ? "skipped" : "succeeded";
|
|
68
|
+
const qualifying =
|
|
69
|
+
status === expectedStatus &&
|
|
70
|
+
(control.request.count === 0 || authorityConclusion === "success");
|
|
71
|
+
if (
|
|
72
|
+
authorityCorrelationId &&
|
|
73
|
+
authorityCorrelationId !== control.authority.correlationId
|
|
74
|
+
) {
|
|
75
|
+
throw new Error("authority correlation does not match control request");
|
|
76
|
+
}
|
|
77
|
+
if (
|
|
78
|
+
authorityResultArtifact &&
|
|
79
|
+
authorityResultArtifact !== control.authority.resultArtifact
|
|
80
|
+
) {
|
|
81
|
+
throw new Error("authority result artifact does not match control request");
|
|
82
|
+
}
|
|
83
|
+
const receipt = {
|
|
84
|
+
schemaVersion: 1,
|
|
85
|
+
contract: RECEIPT_CONTRACT,
|
|
86
|
+
requestDigest: control.digest,
|
|
87
|
+
source: control.source,
|
|
88
|
+
runtime: {
|
|
89
|
+
repository: control.runtime.repository,
|
|
90
|
+
sha: control.runtime.sha,
|
|
91
|
+
},
|
|
92
|
+
platform: control.platform,
|
|
93
|
+
request: control.request,
|
|
94
|
+
authority: {
|
|
95
|
+
repository: control.authority.repository,
|
|
96
|
+
runtimeSha: authorityRuntimeSha,
|
|
97
|
+
runId: control.request.count > 0 ? authorityRunId : "",
|
|
98
|
+
runUrl: control.request.count > 0 ? authorityRunUrl : "",
|
|
99
|
+
resultArtifact: control.request.count > 0 ? authorityResultArtifact : "",
|
|
100
|
+
correlationId: control.authority.correlationId,
|
|
101
|
+
conclusion:
|
|
102
|
+
control.request.count > 0
|
|
103
|
+
? authorityConclusion || status
|
|
104
|
+
: "not-required",
|
|
105
|
+
},
|
|
106
|
+
controller: {
|
|
107
|
+
repository: controllerRepository,
|
|
108
|
+
runId: controllerRunId,
|
|
109
|
+
runAttempt: Number(controllerRunAttempt),
|
|
110
|
+
job: controllerJob,
|
|
111
|
+
runnerOs: controllerRunnerOs,
|
|
112
|
+
startedAt: controllerStartedAt,
|
|
113
|
+
completedAt: controllerCompletedAt,
|
|
114
|
+
status,
|
|
115
|
+
},
|
|
116
|
+
qualifying,
|
|
117
|
+
};
|
|
118
|
+
receipt.digest = digestDocument(receipt);
|
|
119
|
+
const validatedReceipt = validateArtifactSigningControllerReceipt(receipt);
|
|
120
|
+
const resolvedReceiptPath = path.resolve(
|
|
121
|
+
required(receiptPath, "controller receipt path"),
|
|
122
|
+
);
|
|
123
|
+
fs.mkdirSync(path.dirname(resolvedReceiptPath), { recursive: true });
|
|
124
|
+
fs.writeFileSync(
|
|
125
|
+
resolvedReceiptPath,
|
|
126
|
+
`${JSON.stringify(validatedReceipt, null, 2)}\n`,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
let delegation = null;
|
|
130
|
+
if (validatedReceipt.qualifying) {
|
|
131
|
+
delegation = createArtifactSigningDelegation({
|
|
132
|
+
sourceRepository: control.source.repository,
|
|
133
|
+
sourceRunId: control.source.runId,
|
|
134
|
+
sourceRunAttempt: control.source.runAttempt,
|
|
135
|
+
sourceSha: control.source.sha,
|
|
136
|
+
sourceTreeSha: control.source.treeSha,
|
|
137
|
+
runtimeRepository: control.runtime.repository,
|
|
138
|
+
runtimeSha: control.runtime.sha,
|
|
139
|
+
platformId: control.platform.id,
|
|
140
|
+
platformName: control.platform.name,
|
|
141
|
+
requestCount: control.request.count,
|
|
142
|
+
requestArtifact: control.request.artifact,
|
|
143
|
+
requestRoot: control.request.root,
|
|
144
|
+
authorityRunId: validatedReceipt.authority.runId,
|
|
145
|
+
authorityRuntimeSha: validatedReceipt.authority.runtimeSha,
|
|
146
|
+
resultArtifact: validatedReceipt.authority.resultArtifact,
|
|
147
|
+
artifactName: control.artifact.name,
|
|
148
|
+
manifestArtifact: control.artifact.manifestArtifact,
|
|
149
|
+
diagnosticsArtifact: control.artifact.diagnosticsArtifact,
|
|
150
|
+
workingDirectory: control.workingDirectory,
|
|
151
|
+
controllerMode: "detached",
|
|
152
|
+
controllerReceiptDigest: validatedReceipt.digest,
|
|
153
|
+
});
|
|
154
|
+
const resolvedDelegationPath = path.resolve(
|
|
155
|
+
required(delegationPath, "delegation path"),
|
|
156
|
+
);
|
|
157
|
+
fs.mkdirSync(path.dirname(resolvedDelegationPath), { recursive: true });
|
|
158
|
+
fs.writeFileSync(
|
|
159
|
+
resolvedDelegationPath,
|
|
160
|
+
`${JSON.stringify(delegation, null, 2)}\n`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return { receipt: validatedReceipt, delegation };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function readArtifactSigningControllerReceipt(
|
|
167
|
+
inputPath = process.env.BUILDCHAIN_SIGNING_CONTROLLER_RECEIPT_PATH,
|
|
168
|
+
) {
|
|
169
|
+
const target = path.resolve(required(inputPath, "controller receipt path"));
|
|
170
|
+
return validateArtifactSigningControllerReceipt(
|
|
171
|
+
JSON.parse(fs.readFileSync(target, "utf8")),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function assertArtifactSigningControllerReceipt({
|
|
176
|
+
request,
|
|
177
|
+
receipt,
|
|
178
|
+
delegation,
|
|
179
|
+
}) {
|
|
180
|
+
const control = validateArtifactSigningControlRequest(request);
|
|
181
|
+
const result = validateArtifactSigningControllerReceipt(receipt);
|
|
182
|
+
const finalDelegation = assertArtifactSigningDelegationContext(delegation, {
|
|
183
|
+
sourceRepository: control.source.repository,
|
|
184
|
+
sourceRunId: control.source.runId,
|
|
185
|
+
sourceRunAttempt: control.source.runAttempt,
|
|
186
|
+
sourceSha: control.source.sha,
|
|
187
|
+
runtimeRepository: control.runtime.repository,
|
|
188
|
+
runtimeSha: control.runtime.sha,
|
|
189
|
+
platformId: control.platform.id,
|
|
190
|
+
});
|
|
191
|
+
const comparisons = [
|
|
192
|
+
[result.requestDigest, control.digest, "control request digest"],
|
|
193
|
+
[result.source.treeSha, control.source.treeSha, "source tree SHA"],
|
|
194
|
+
[result.request.root, control.request.root, "request root"],
|
|
195
|
+
[result.request.artifact, control.request.artifact, "request artifact"],
|
|
196
|
+
[
|
|
197
|
+
result.authority.correlationId,
|
|
198
|
+
control.authority.correlationId,
|
|
199
|
+
"correlation",
|
|
200
|
+
],
|
|
201
|
+
[
|
|
202
|
+
result.authority.runId,
|
|
203
|
+
finalDelegation.authority.runId,
|
|
204
|
+
"authority run ID",
|
|
205
|
+
],
|
|
206
|
+
[
|
|
207
|
+
result.authority.runtimeSha,
|
|
208
|
+
finalDelegation.authority.runtimeSha,
|
|
209
|
+
"authority runtime SHA",
|
|
210
|
+
],
|
|
211
|
+
[
|
|
212
|
+
result.authority.resultArtifact,
|
|
213
|
+
finalDelegation.authority.resultArtifact,
|
|
214
|
+
"authority result artifact",
|
|
215
|
+
],
|
|
216
|
+
[
|
|
217
|
+
finalDelegation.controller.receiptDigest,
|
|
218
|
+
result.digest,
|
|
219
|
+
"controller receipt digest",
|
|
220
|
+
],
|
|
221
|
+
[
|
|
222
|
+
finalDelegation.request.root,
|
|
223
|
+
result.request.root,
|
|
224
|
+
"delegation request root",
|
|
225
|
+
],
|
|
226
|
+
];
|
|
227
|
+
for (const [actual, expected, label] of comparisons) {
|
|
228
|
+
if (actual !== expected) {
|
|
229
|
+
throw new Error(`artifact signing controller ${label} mismatch`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (!result.qualifying) {
|
|
233
|
+
throw new Error("artifact signing controller receipt is not qualifying");
|
|
234
|
+
}
|
|
235
|
+
return { request: control, receipt: result, delegation: finalDelegation };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function expectedContext() {
|
|
239
|
+
return {
|
|
240
|
+
sourceRepository: process.env.BUILDCHAIN_EXPECTED_SOURCE_REPOSITORY || "",
|
|
241
|
+
sourceRunId: process.env.BUILDCHAIN_EXPECTED_SOURCE_RUN_ID || "",
|
|
242
|
+
sourceRunAttempt: process.env.BUILDCHAIN_EXPECTED_SOURCE_RUN_ATTEMPT || "",
|
|
243
|
+
sourceSha: process.env.BUILDCHAIN_EXPECTED_SOURCE_SHA || "",
|
|
244
|
+
sourceTreeSha: process.env.BUILDCHAIN_EXPECTED_SOURCE_TREE_SHA || "",
|
|
245
|
+
runtimeRepository: process.env.BUILDCHAIN_EXPECTED_RUNTIME_REPOSITORY || "",
|
|
246
|
+
runtimeSha: process.env.BUILDCHAIN_EXPECTED_RUNTIME_SHA || "",
|
|
247
|
+
platformId: process.env.BUILDCHAIN_EXPECTED_PLATFORM_ID || "",
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (
|
|
252
|
+
process.argv[1] &&
|
|
253
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
254
|
+
) {
|
|
255
|
+
try {
|
|
256
|
+
const mode = process.argv[2] || "seal";
|
|
257
|
+
if (mode === "seal") {
|
|
258
|
+
sealArtifactSigningControlRequest();
|
|
259
|
+
} else if (mode === "outputs") {
|
|
260
|
+
const request = assertArtifactSigningControlRequestContext(
|
|
261
|
+
readArtifactSigningControlRequest(),
|
|
262
|
+
expectedContext(),
|
|
263
|
+
);
|
|
264
|
+
writeGitHubOutputs(artifactSigningControlRequestOutputs(request));
|
|
265
|
+
} else if (mode === "settle") {
|
|
266
|
+
const request = assertArtifactSigningControlRequestContext(
|
|
267
|
+
readArtifactSigningControlRequest(),
|
|
268
|
+
expectedContext(),
|
|
269
|
+
);
|
|
270
|
+
const { receipt, delegation } = settleArtifactSigningControl({ request });
|
|
271
|
+
writeGitHubOutputs({
|
|
272
|
+
"controller-status": receipt.controller.status,
|
|
273
|
+
"controller-receipt-digest": receipt.digest,
|
|
274
|
+
"controller-qualifying": String(receipt.qualifying),
|
|
275
|
+
"delegation-created": String(Boolean(delegation)),
|
|
276
|
+
});
|
|
277
|
+
} else if (mode === "verify") {
|
|
278
|
+
const request = assertArtifactSigningControlRequestContext(
|
|
279
|
+
readArtifactSigningControlRequest(),
|
|
280
|
+
expectedContext(),
|
|
281
|
+
);
|
|
282
|
+
const checked = assertArtifactSigningControllerReceipt({
|
|
283
|
+
request,
|
|
284
|
+
receipt: readArtifactSigningControllerReceipt(),
|
|
285
|
+
delegation: readArtifactSigningDelegation(),
|
|
286
|
+
});
|
|
287
|
+
writeGitHubOutputs({
|
|
288
|
+
...artifactSigningDelegationOutputs(checked.delegation),
|
|
289
|
+
"request-root": checked.receipt.request.root,
|
|
290
|
+
"controller-receipt-digest": checked.receipt.digest,
|
|
291
|
+
"controller-status": checked.receipt.controller.status,
|
|
292
|
+
});
|
|
293
|
+
} else {
|
|
294
|
+
throw new Error(`unsupported artifact signing controller mode: ${mode}`);
|
|
295
|
+
}
|
|
296
|
+
} catch (error) {
|
|
297
|
+
console.error(
|
|
298
|
+
`::error::${String(error?.message || error).replace(/\r?\n/gu, "%0A")}`,
|
|
299
|
+
);
|
|
300
|
+
process.exitCode = 1;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -22,6 +22,14 @@ function optional(value, label) {
|
|
|
22
22
|
return normalized;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function sha256Root(value, label) {
|
|
26
|
+
const normalized = optional(value, label);
|
|
27
|
+
if (normalized && !/^sha256:[0-9a-f]{64}$/u.test(normalized)) {
|
|
28
|
+
throw new Error(`${label} must be a canonical sha256 root`);
|
|
29
|
+
}
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
function exactSha(value, label) {
|
|
26
34
|
const normalized = required(value, label);
|
|
27
35
|
if (!/^[0-9a-f]{40}$/u.test(normalized))
|
|
@@ -97,8 +105,13 @@ export function validateArtifactSigningDelegation(value) {
|
|
|
97
105
|
requestCount > 0
|
|
98
106
|
? required(value.request?.artifact, "request.artifact")
|
|
99
107
|
: optional(value.request?.artifact, "request.artifact"),
|
|
108
|
+
root: sha256Root(value.request?.root, "request.root"),
|
|
100
109
|
},
|
|
101
110
|
authority: {
|
|
111
|
+
runtimeSha:
|
|
112
|
+
requestCount > 0
|
|
113
|
+
? exactSha(value.authority?.runtimeSha, "authority.runtimeSha")
|
|
114
|
+
: optional(value.authority?.runtimeSha, "authority.runtimeSha"),
|
|
102
115
|
runId:
|
|
103
116
|
requestCount > 0
|
|
104
117
|
? required(value.authority?.runId, "authority.runId")
|
|
@@ -129,15 +142,32 @@ export function validateArtifactSigningDelegation(value) {
|
|
|
129
142
|
value.workingDirectory || ".",
|
|
130
143
|
"workingDirectory",
|
|
131
144
|
),
|
|
145
|
+
controller: {
|
|
146
|
+
mode: optional(value.controller?.mode, "controller.mode"),
|
|
147
|
+
receiptDigest: sha256Root(
|
|
148
|
+
value.controller?.receiptDigest,
|
|
149
|
+
"controller.receiptDigest",
|
|
150
|
+
),
|
|
151
|
+
},
|
|
132
152
|
};
|
|
133
153
|
if (
|
|
134
154
|
requestCount === 0 &&
|
|
135
|
-
(delegation.authority.
|
|
155
|
+
(delegation.authority.runtimeSha ||
|
|
156
|
+
delegation.authority.runId ||
|
|
157
|
+
delegation.authority.resultArtifact)
|
|
136
158
|
) {
|
|
137
159
|
throw new Error(
|
|
138
160
|
"unsigned delegation must not contain authority result coordinates",
|
|
139
161
|
);
|
|
140
162
|
}
|
|
163
|
+
if (
|
|
164
|
+
Boolean(delegation.controller.mode) !==
|
|
165
|
+
Boolean(delegation.controller.receiptDigest)
|
|
166
|
+
) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
"delegation controller mode and receipt digest must be provided together",
|
|
169
|
+
);
|
|
170
|
+
}
|
|
141
171
|
return delegation;
|
|
142
172
|
}
|
|
143
173
|
|
|
@@ -153,12 +183,17 @@ export function createArtifactSigningDelegation({
|
|
|
153
183
|
platformName = process.env.BUILDCHAIN_PLATFORM_NAME,
|
|
154
184
|
requestCount = process.env.BUILDCHAIN_SIGNING_REQUEST_COUNT || "0",
|
|
155
185
|
requestArtifact = process.env.BUILDCHAIN_SIGNING_REQUEST_ARTIFACT || "",
|
|
186
|
+
requestRoot = process.env.BUILDCHAIN_SIGNING_REQUEST_ROOT_DIGEST || "",
|
|
156
187
|
authorityRunId = process.env.BUILDCHAIN_AUTHORITY_RUN_ID || "",
|
|
188
|
+
authorityRuntimeSha = process.env.BUILDCHAIN_AUTHORITY_RUNTIME_SHA || "",
|
|
157
189
|
resultArtifact = process.env.BUILDCHAIN_SIGNING_RESULT_ARTIFACT || "",
|
|
158
190
|
artifactName = process.env.BUILDCHAIN_ARTIFACT_NAME,
|
|
159
191
|
manifestArtifact = process.env.BUILDCHAIN_MANIFEST_ARTIFACT_NAME,
|
|
160
192
|
diagnosticsArtifact = process.env.BUILDCHAIN_DIAGNOSTICS_ARTIFACT_NAME,
|
|
161
193
|
workingDirectory = process.env.BUILDCHAIN_SIGNING_CWD || ".",
|
|
194
|
+
controllerMode = process.env.BUILDCHAIN_SIGNING_CONTROLLER_MODE || "",
|
|
195
|
+
controllerReceiptDigest = process.env
|
|
196
|
+
.BUILDCHAIN_SIGNING_CONTROLLER_RECEIPT_DIGEST || "",
|
|
162
197
|
} = {}) {
|
|
163
198
|
return validateArtifactSigningDelegation({
|
|
164
199
|
schemaVersion: 1,
|
|
@@ -172,10 +207,22 @@ export function createArtifactSigningDelegation({
|
|
|
172
207
|
},
|
|
173
208
|
runtime: { repository: runtimeRepository, sha: runtimeSha },
|
|
174
209
|
platform: { id: platformId, name: platformName },
|
|
175
|
-
request: {
|
|
176
|
-
|
|
210
|
+
request: {
|
|
211
|
+
count: Number(requestCount),
|
|
212
|
+
artifact: requestArtifact,
|
|
213
|
+
root: requestRoot,
|
|
214
|
+
},
|
|
215
|
+
authority: {
|
|
216
|
+
runtimeSha: authorityRuntimeSha,
|
|
217
|
+
runId: authorityRunId,
|
|
218
|
+
resultArtifact,
|
|
219
|
+
},
|
|
177
220
|
artifact: { name: artifactName, manifestArtifact, diagnosticsArtifact },
|
|
178
221
|
workingDirectory,
|
|
222
|
+
controller: {
|
|
223
|
+
mode: controllerMode,
|
|
224
|
+
receiptDigest: controllerReceiptDigest,
|
|
225
|
+
},
|
|
179
226
|
});
|
|
180
227
|
}
|
|
181
228
|
|
|
@@ -205,6 +252,7 @@ export function artifactSigningDelegationOutputs(delegation) {
|
|
|
205
252
|
"request-count": String(value.request.count),
|
|
206
253
|
"request-artifact": value.request.artifact,
|
|
207
254
|
"authority-run-id": value.authority.runId,
|
|
255
|
+
"authority-runtime-sha": value.authority.runtimeSha,
|
|
208
256
|
"result-artifact": value.authority.resultArtifact,
|
|
209
257
|
"artifact-name": value.artifact.name,
|
|
210
258
|
"manifest-artifact-name": value.artifact.manifestArtifact,
|
|
@@ -25,6 +25,7 @@ const RENDITIONS = [
|
|
|
25
25
|
];
|
|
26
26
|
const STANDARD_MAX_SECONDS = 60;
|
|
27
27
|
const LONG_FORM_MAX_SECONDS = 180;
|
|
28
|
+
const MAX_EXECUTABLE_FILES = 32;
|
|
28
29
|
|
|
29
30
|
function durationPolicy(value = "standard") {
|
|
30
31
|
requireValue(value === "standard" || value === "long-form", "scenario duration class is invalid");
|
|
@@ -143,6 +144,54 @@ function validateArtifact(artifact) {
|
|
|
143
144
|
requireValue(Array.isArray(artifact.runtimeDependencies) && artifact.runtimeDependencies.length === 0, "scenario artifact must be standalone");
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
function validateTransportSmoke(smoke) {
|
|
148
|
+
exactKeys(smoke, ["argv", "timeoutSeconds", "expectedExitCodes", "stdoutIncludes"], [], "scenario.transportSmoke");
|
|
149
|
+
requireValue(Array.isArray(smoke.argv) && smoke.argv.length >= 1 && smoke.argv.length <= 64 && smoke.argv.every((item) => typeof item === "string" && !item.includes("\0") && item.length <= 512), "scenario transport smoke argv is invalid");
|
|
150
|
+
requireValue(Number.isInteger(smoke.timeoutSeconds) && smoke.timeoutSeconds >= 1 && smoke.timeoutSeconds <= STANDARD_MAX_SECONDS, "scenario transport smoke timeout is invalid");
|
|
151
|
+
requireValue(Array.isArray(smoke.expectedExitCodes) && smoke.expectedExitCodes.length >= 1 && smoke.expectedExitCodes.length <= 4 && smoke.expectedExitCodes.every((item) => Number.isInteger(item) && item >= 0 && item <= 255), "scenario transport smoke expected exits are invalid");
|
|
152
|
+
requireValue(Array.isArray(smoke.stdoutIncludes) && smoke.stdoutIncludes.length <= 32 && smoke.stdoutIncludes.every((item) => typeof item === "string" && item.length >= 1 && item.length <= 256), "scenario transport smoke stdout assertions are invalid");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function validateExecutableClosure({ artifactRoot, scenario, requireExecutable }) {
|
|
156
|
+
const root = path.resolve(artifactRoot);
|
|
157
|
+
const artifact = scenario.artifact;
|
|
158
|
+
const metadata = readJson(inside(root, artifact.metadataPath, "artifact metadata path"), "binary metadata");
|
|
159
|
+
requireValue(metadata.contract === artifact.metadataContract, "binary metadata contract mismatch");
|
|
160
|
+
requireValue((metadata.platformId || metadata.platform) === artifact.platformId, "binary metadata platform mismatch");
|
|
161
|
+
requireValue(JSON.stringify(metadata.runtimeDependencies) === JSON.stringify(artifact.runtimeDependencies), "binary metadata runtime dependency mismatch");
|
|
162
|
+
requireValue(Array.isArray(metadata.executableFiles) && metadata.executableFiles.length >= 1 && metadata.executableFiles.length <= MAX_EXECUTABLE_FILES, "binary metadata executableFiles must be a bounded non-empty array");
|
|
163
|
+
const declared = new Set();
|
|
164
|
+
const files = metadata.executableFiles.map((entry, index) => {
|
|
165
|
+
const label = `binary metadata executableFiles[${index}]`;
|
|
166
|
+
exactKeys(entry, ["path", "sha256"], [], label);
|
|
167
|
+
requireValue(!declared.has(entry.path), `${label}.path is repeated`);
|
|
168
|
+
declared.add(entry.path);
|
|
169
|
+
requireValue(/^[0-9a-f]{64}$/u.test(entry.sha256), `${label}.sha256 is invalid`);
|
|
170
|
+
const file = inside(root, entry.path, `${label}.path`);
|
|
171
|
+
const bytes = regular(file, label, 256 * 1024 * 1024);
|
|
172
|
+
requireValue(rootBytes(bytes) === `sha256:${entry.sha256}`, `${label} digest differs from exact artifact metadata`);
|
|
173
|
+
if (requireExecutable) requireValue((fs.statSync(file).mode & 0o111) !== 0, `${label} is not executable`);
|
|
174
|
+
return { path: entry.path, file, sha256: entry.sha256 };
|
|
175
|
+
});
|
|
176
|
+
requireValue(declared.has(artifact.binaryPath), "binary metadata executableFiles must include scenario.artifact.binaryPath");
|
|
177
|
+
const binary = files.find((entry) => entry.path === artifact.binaryPath);
|
|
178
|
+
const metadataBinarySha256 = String(metadata.sha256 || "").replace(/^sha256:/u, "");
|
|
179
|
+
requireValue(/^[0-9a-f]{64}$/u.test(metadataBinarySha256) && metadataBinarySha256 === binary.sha256, "binary digest differs from exact artifact metadata");
|
|
180
|
+
return { metadata, files };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function prepareArtifact({ artifactRoot, scenarioPath }) {
|
|
184
|
+
const scenario = validateScenario(readJson(path.resolve(scenarioPath), "scenario"));
|
|
185
|
+
const closure = validateExecutableClosure({ artifactRoot, scenario, requireExecutable: false });
|
|
186
|
+
for (const entry of closure.files) fs.chmodSync(entry.file, 0o755);
|
|
187
|
+
validateExecutableClosure({ artifactRoot, scenario, requireExecutable: true });
|
|
188
|
+
return {
|
|
189
|
+
executableFiles: closure.files.map(({ path: relative, sha256 }) => ({ path: relative, sha256 })),
|
|
190
|
+
metadataRoot: rootJson(closure.metadata),
|
|
191
|
+
authority: { classification: "artifact-mode-restoration", grants: [], nonAuthorities: NON_AUTHORITIES },
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
146
195
|
function validateExecution(execution) {
|
|
147
196
|
exactKeys(execution, ["deterministic", "network", "secrets", "totalTimeoutSeconds", "environment"], ["durationClass"], "scenario.execution");
|
|
148
197
|
requireValue(execution.deterministic === true && execution.network === "none" && execution.secrets === "none", "scenario execution must be deterministic, network-disabled, and secret-free");
|
|
@@ -185,7 +234,7 @@ function validateDemo(demo, index, demoIds, maximumSeconds) {
|
|
|
185
234
|
}
|
|
186
235
|
|
|
187
236
|
export function validateScenario(value) {
|
|
188
|
-
exactKeys(value, ["schema", "product", "artifact", "execution", "renditions", "demos", "publication", "authority"], [], "scenario");
|
|
237
|
+
exactKeys(value, ["schema", "product", "artifact", "execution", "renditions", "demos", "publication", "authority"], ["transportSmoke"], "scenario");
|
|
189
238
|
requireValue(value.schema === "buildchain.declarative-binary-demo/v1", "unsupported scenario schema");
|
|
190
239
|
validateProduct(value.product);
|
|
191
240
|
validateArtifact(value.artifact);
|
|
@@ -194,6 +243,7 @@ export function validateScenario(value) {
|
|
|
194
243
|
requireValue(Array.isArray(value.demos) && value.demos.length >= 1 && value.demos.length <= 8, "scenario requires 1 through 8 demos");
|
|
195
244
|
const demoIds = new Set();
|
|
196
245
|
value.demos.forEach((demo, index) => validateDemo(demo, index, demoIds, executionPolicy.maximumSeconds));
|
|
246
|
+
if (value.transportSmoke) validateTransportSmoke(value.transportSmoke);
|
|
197
247
|
exactKeys(value.publication, ["evidencePath", "readmePath", "marker"], [], "scenario.publication");
|
|
198
248
|
inside("/repository", value.publication.evidencePath, "scenario.publication.evidencePath");
|
|
199
249
|
inside("/repository", value.publication.readmePath, "scenario.publication.readmePath");
|
|
@@ -474,6 +524,14 @@ function main(argv = process.argv.slice(2)) {
|
|
|
474
524
|
process.stdout.write(stableJson(scenario.publication));
|
|
475
525
|
return;
|
|
476
526
|
}
|
|
527
|
+
if (command === "prepare-artifact") {
|
|
528
|
+
const result = prepareArtifact({
|
|
529
|
+
artifactRoot: path.resolve(args["artifact-root"]),
|
|
530
|
+
scenarioPath: path.resolve(args.scenario),
|
|
531
|
+
});
|
|
532
|
+
process.stdout.write(stableJson({ ok: true, ...result }));
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
477
535
|
if (command === "adapt") {
|
|
478
536
|
const result = adaptCapture({ artifactRoot: path.resolve(args["artifact-root"]), output: path.resolve(args.output) });
|
|
479
537
|
process.stdout.write(stableJson({ ok: true, ...result }));
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
prepareArtifact,
|
|
13
|
+
validateScenario,
|
|
14
|
+
} from "./auditable-demo-platform.mjs";
|
|
15
|
+
|
|
16
|
+
const NON_AUTHORITIES = [
|
|
17
|
+
"first-party-identity",
|
|
18
|
+
"system-identity",
|
|
19
|
+
"kfd-compliance",
|
|
20
|
+
"product-system-metadata",
|
|
21
|
+
"package-metadata",
|
|
22
|
+
"registry-history",
|
|
23
|
+
"scan-output",
|
|
24
|
+
"standalone-generation",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function fail(message) {
|
|
28
|
+
throw new Error(`auditable demo transport smoke: ${message}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function inside(root, relative, label) {
|
|
32
|
+
if (typeof relative !== "string" || !relative || path.isAbsolute(relative))
|
|
33
|
+
fail(`${label} must be relative`);
|
|
34
|
+
const resolvedRoot = path.resolve(root);
|
|
35
|
+
const resolved = path.resolve(resolvedRoot, relative);
|
|
36
|
+
if (
|
|
37
|
+
resolved === resolvedRoot ||
|
|
38
|
+
!resolved.startsWith(`${resolvedRoot}${path.sep}`)
|
|
39
|
+
)
|
|
40
|
+
fail(`${label} escapes its root`);
|
|
41
|
+
return resolved;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function removeExecuteBits(root) {
|
|
45
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
46
|
+
const file = path.join(root, entry.name);
|
|
47
|
+
if (entry.isDirectory()) removeExecuteBits(file);
|
|
48
|
+
else if (entry.isFile())
|
|
49
|
+
fs.chmodSync(file, fs.statSync(file).mode & ~0o111);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isolatedEnvironment(home, declared) {
|
|
54
|
+
const env = {
|
|
55
|
+
PATH: "/usr/local/bin:/usr/bin:/bin",
|
|
56
|
+
HOME: home,
|
|
57
|
+
XDG_CACHE_HOME: path.join(home, ".cache"),
|
|
58
|
+
XDG_CONFIG_HOME: path.join(home, ".config"),
|
|
59
|
+
XDG_DATA_HOME: path.join(home, ".local/share"),
|
|
60
|
+
XDG_STATE_HOME: path.join(home, ".local/state"),
|
|
61
|
+
LANG: "C.UTF-8",
|
|
62
|
+
LC_ALL: "C.UTF-8",
|
|
63
|
+
TZ: "UTC",
|
|
64
|
+
CI: "true",
|
|
65
|
+
TERM: "xterm-256color",
|
|
66
|
+
COLORTERM: "truecolor",
|
|
67
|
+
FORCE_COLOR: "3",
|
|
68
|
+
};
|
|
69
|
+
for (const [key, value] of Object.entries(declared)) {
|
|
70
|
+
if (Object.hasOwn(env, key))
|
|
71
|
+
fail(
|
|
72
|
+
`scenario environment may not override the isolated baseline: ${key}`,
|
|
73
|
+
);
|
|
74
|
+
env[key] = value;
|
|
75
|
+
}
|
|
76
|
+
return env;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function runTransportSmoke({ artifactRoot, scenarioPath }) {
|
|
80
|
+
if (process.platform === "win32")
|
|
81
|
+
fail("transport smoke currently requires a POSIX host");
|
|
82
|
+
const root = path.resolve(artifactRoot);
|
|
83
|
+
const scenario = validateScenario(
|
|
84
|
+
JSON.parse(fs.readFileSync(path.resolve(scenarioPath), "utf8")),
|
|
85
|
+
);
|
|
86
|
+
if (!scenario.transportSmoke) fail("scenario.transportSmoke is required");
|
|
87
|
+
const distributionRelative = path.posix.dirname(
|
|
88
|
+
scenario.artifact.metadataPath,
|
|
89
|
+
);
|
|
90
|
+
const sourceDistribution =
|
|
91
|
+
distributionRelative === "."
|
|
92
|
+
? root
|
|
93
|
+
: inside(root, distributionRelative, "distribution root");
|
|
94
|
+
const temporaryRoot = fs.mkdtempSync(
|
|
95
|
+
path.join(os.tmpdir(), "buildchain-demo-transport-"),
|
|
96
|
+
);
|
|
97
|
+
try {
|
|
98
|
+
const transportedRoot = path.join(temporaryRoot, "artifact");
|
|
99
|
+
const transportedDistribution =
|
|
100
|
+
distributionRelative === "."
|
|
101
|
+
? transportedRoot
|
|
102
|
+
: path.join(transportedRoot, ...distributionRelative.split("/"));
|
|
103
|
+
fs.mkdirSync(path.dirname(transportedDistribution), { recursive: true });
|
|
104
|
+
fs.cpSync(sourceDistribution, transportedDistribution, {
|
|
105
|
+
recursive: true,
|
|
106
|
+
dereference: false,
|
|
107
|
+
preserveTimestamps: true,
|
|
108
|
+
verbatimSymlinks: true,
|
|
109
|
+
});
|
|
110
|
+
removeExecuteBits(transportedDistribution);
|
|
111
|
+
const prepared = prepareArtifact({
|
|
112
|
+
artifactRoot: transportedRoot,
|
|
113
|
+
scenarioPath,
|
|
114
|
+
});
|
|
115
|
+
const binary = inside(
|
|
116
|
+
transportedRoot,
|
|
117
|
+
scenario.artifact.binaryPath,
|
|
118
|
+
"transport smoke binary",
|
|
119
|
+
);
|
|
120
|
+
const workspace = path.join(temporaryRoot, "workspace");
|
|
121
|
+
const home = path.join(temporaryRoot, "home");
|
|
122
|
+
fs.mkdirSync(workspace);
|
|
123
|
+
fs.mkdirSync(home);
|
|
124
|
+
const smoke = scenario.transportSmoke;
|
|
125
|
+
const result = spawnSync(binary, smoke.argv, {
|
|
126
|
+
cwd: workspace,
|
|
127
|
+
env: isolatedEnvironment(home, scenario.execution.environment),
|
|
128
|
+
encoding: "utf8",
|
|
129
|
+
timeout: smoke.timeoutSeconds * 1000,
|
|
130
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
131
|
+
});
|
|
132
|
+
const output = `${result.stdout || ""}${result.stderr || ""}`;
|
|
133
|
+
if (result.error) fail(`could not execute: ${result.error.message}`);
|
|
134
|
+
if (!smoke.expectedExitCodes.includes(result.status))
|
|
135
|
+
fail(`exited with ${result.status}; output: ${output.slice(-4096)}`);
|
|
136
|
+
for (const expected of smoke.stdoutIncludes)
|
|
137
|
+
if (!output.includes(expected)) fail(`output is missing: ${expected}`);
|
|
138
|
+
return {
|
|
139
|
+
schema: "buildchain.declarative-demo-transport-smoke/v1",
|
|
140
|
+
status: "passed",
|
|
141
|
+
executableFiles: prepared.executableFiles,
|
|
142
|
+
argv: smoke.argv,
|
|
143
|
+
exitCode: result.status,
|
|
144
|
+
outputRoot: `sha256:${crypto.createHash("sha256").update(output).digest("hex")}`,
|
|
145
|
+
authority: {
|
|
146
|
+
classification: "pre-upload-transport-diagnostic",
|
|
147
|
+
grants: [],
|
|
148
|
+
nonAuthorities: NON_AUTHORITIES,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
} finally {
|
|
152
|
+
fs.rmSync(temporaryRoot, { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parseArgs(argv) {
|
|
157
|
+
const values = {};
|
|
158
|
+
for (let index = 0; index < argv.length; index += 2)
|
|
159
|
+
values[argv[index].replace(/^--/u, "")] = argv[index + 1];
|
|
160
|
+
return values;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (
|
|
164
|
+
process.argv[1] &&
|
|
165
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
166
|
+
) {
|
|
167
|
+
try {
|
|
168
|
+
const args = parseArgs(process.argv.slice(2));
|
|
169
|
+
process.stdout.write(
|
|
170
|
+
`${JSON.stringify({ ok: true, ...runTransportSmoke({ artifactRoot: args["artifact-root"], scenarioPath: args.scenario }) }, null, 2)}\n`,
|
|
171
|
+
);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
174
|
+
process.exitCode = 1;
|
|
175
|
+
}
|
|
176
|
+
}
|