@serve.zone/interfaces 23.2.0 → 24.0.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.
@@ -0,0 +1,582 @@
1
+ import type { TSha256Digest, TImmutableContainerPlatform } from './data/index.js';
2
+ import { isSha256Digest } from './data/immutableimage.js';
3
+
4
+ export const workloadInitReleaseContract = Object.freeze({
5
+ packageName: '@serve.zone/workloadinit',
6
+ sourceRepository: 'https://code.foss.global/serve.zone/workloadinit.git',
7
+ imageRegistry: 'code.foss.global',
8
+ imageRepository: 'serve.zone/workloadinit',
9
+ imageIndexMediaType: 'application/vnd.oci.image.index.v1+json',
10
+ statementType: 'https://in-toto.io/Statement/v1',
11
+ predicateType: 'https://slsa.dev/provenance/v1',
12
+ buildType: 'https://serve.zone/buildtypes/workloadinit-gitzone-tsdocker/v1',
13
+ builderId: 'https://serve.zone/builders/workloadinit-manual-release/v1',
14
+ signatureFormat: 'cosign-dsse-bundle-v0.3',
15
+ signatureAlgorithm: 'ecdsa-p256-sha256',
16
+ } as const);
17
+
18
+ export interface IWorkloadInitBuildToolVersionsV1 {
19
+ dockerBuildx: string;
20
+ gitzone: string;
21
+ tsdocker: string;
22
+ tsrust: string;
23
+ }
24
+
25
+ export interface IWorkloadInitReleaseIdentityV1 {
26
+ packageName: typeof workloadInitReleaseContract.packageName;
27
+ version: string;
28
+ sourceRepository: typeof workloadInitReleaseContract.sourceRepository;
29
+ sourceRevision: string;
30
+ sourceRef: string;
31
+ sourceDirty: false;
32
+ imageRegistry: typeof workloadInitReleaseContract.imageRegistry;
33
+ imageRepository: typeof workloadInitReleaseContract.imageRepository;
34
+ imageTag: string;
35
+ }
36
+
37
+ export interface IWorkloadInitReleaseAttestationStatementV1 {
38
+ _type: typeof workloadInitReleaseContract.statementType;
39
+ subject: [{
40
+ name: string;
41
+ digest: {
42
+ sha256: string;
43
+ };
44
+ }];
45
+ predicateType: typeof workloadInitReleaseContract.predicateType;
46
+ predicate: {
47
+ buildDefinition: {
48
+ buildType: typeof workloadInitReleaseContract.buildType;
49
+ externalParameters: {
50
+ artifact: {
51
+ mediaType: typeof workloadInitReleaseContract.imageIndexMediaType;
52
+ platforms: ['linux/amd64', 'linux/arm64'];
53
+ repository: string;
54
+ tag: string;
55
+ };
56
+ source: {
57
+ repository: typeof workloadInitReleaseContract.sourceRepository;
58
+ tag: string;
59
+ dirty: false;
60
+ };
61
+ };
62
+ resolvedDependencies: [{
63
+ uri: string;
64
+ digest: {
65
+ gitCommit: string;
66
+ };
67
+ }];
68
+ };
69
+ runDetails: {
70
+ builder: {
71
+ id: typeof workloadInitReleaseContract.builderId;
72
+ version: IWorkloadInitBuildToolVersionsV1;
73
+ };
74
+ };
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Cloudly-created summary of a private DSSE verification. Shape and digest
80
+ * validation do not independently prove the signature or its trust policy.
81
+ */
82
+ export interface IWorkloadInitCloudlyVerificationSummaryV1 {
83
+ signatureFormat: typeof workloadInitReleaseContract.signatureFormat;
84
+ signatureAlgorithm: typeof workloadInitReleaseContract.signatureAlgorithm;
85
+ signerKeyId: TSha256Digest;
86
+ detachedBundleDigest: TSha256Digest;
87
+ statementDigest: TSha256Digest;
88
+ subjectImageIndexDigest: TSha256Digest;
89
+ builderId: typeof workloadInitReleaseContract.builderId;
90
+ policyGeneration: number;
91
+ policyDigest: TSha256Digest;
92
+ verifiedAt: number;
93
+ }
94
+
95
+ export interface IWorkloadInitApprovedArtifactV1 {
96
+ platform: TImmutableContainerPlatform;
97
+ workloadInitPlatformManifestDigest: TSha256Digest;
98
+ workloadInitExecutableDigest: TSha256Digest;
99
+ }
100
+
101
+ export interface IWorkloadInitReleaseApprovalV1 {
102
+ schemaVersion: 1;
103
+ release: IWorkloadInitReleaseIdentityV1;
104
+ imageIndexMediaType: typeof workloadInitReleaseContract.imageIndexMediaType;
105
+ workloadInitImageIndexDigest: TSha256Digest;
106
+ artifacts: [IWorkloadInitApprovedArtifactV1, IWorkloadInitApprovedArtifactV1];
107
+ verificationSummary: IWorkloadInitCloudlyVerificationSummaryV1;
108
+ approvedAt: number;
109
+ approvalDigest: TSha256Digest;
110
+ }
111
+
112
+ export interface IWorkloadInitApprovalAuthorityReferenceV1 {
113
+ authorityVersion: 1;
114
+ authorityGeneration: number;
115
+ approvalDigest: TSha256Digest;
116
+ }
117
+
118
+ export interface IWorkloadInitActiveApprovalAuthorityV1
119
+ extends IWorkloadInitApprovalAuthorityReferenceV1 {
120
+ approval: IWorkloadInitReleaseApprovalV1;
121
+ }
122
+
123
+ const semanticVersionRegex = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/;
124
+ const gitRevisionRegex = /^[a-f0-9]{40}$/;
125
+ const bareSha256Regex = /^[a-f0-9]{64}$/;
126
+ const toolVersionRegex = /^[A-Za-z0-9][A-Za-z0-9.+_-]{0,63}$/;
127
+
128
+ const isRecord = (valueArg: unknown): valueArg is Record<string, unknown> => (
129
+ Boolean(valueArg) && typeof valueArg === 'object' && !Array.isArray(valueArg)
130
+ );
131
+
132
+ const hasExactKeys = (
133
+ valueArg: Record<string, unknown>,
134
+ keysArg: string[],
135
+ ): boolean => JSON.stringify(Object.keys(valueArg).sort()) === JSON.stringify([...keysArg].sort());
136
+
137
+ const isPositiveSafeInteger = (valueArg: unknown): valueArg is number => (
138
+ Number.isSafeInteger(valueArg) && (valueArg as number) > 0
139
+ );
140
+
141
+ const computeSha256 = async (inputArg: Uint8Array): Promise<TSha256Digest> => {
142
+ const digest = new Uint8Array(await globalThis.crypto.subtle.digest(
143
+ 'SHA-256',
144
+ new Uint8Array(inputArg),
145
+ ));
146
+ return `sha256:${[...digest]
147
+ .map((byteArg) => byteArg.toString(16).padStart(2, '0'))
148
+ .join('')}` as TSha256Digest;
149
+ };
150
+
151
+ export const validateWorkloadInitReleaseIdentity = (
152
+ releaseArg: unknown,
153
+ ): string[] => {
154
+ if (!isRecord(releaseArg)
155
+ || !hasExactKeys(releaseArg, [
156
+ 'packageName',
157
+ 'version',
158
+ 'sourceRepository',
159
+ 'sourceRevision',
160
+ 'sourceRef',
161
+ 'sourceDirty',
162
+ 'imageRegistry',
163
+ 'imageRepository',
164
+ 'imageTag',
165
+ ])) {
166
+ return ['WorkloadInit release identity must use its exact schema'];
167
+ }
168
+ const errors: string[] = [];
169
+ if (releaseArg.packageName !== workloadInitReleaseContract.packageName
170
+ || releaseArg.sourceRepository !== workloadInitReleaseContract.sourceRepository
171
+ || releaseArg.imageRegistry !== workloadInitReleaseContract.imageRegistry
172
+ || releaseArg.imageRepository !== workloadInitReleaseContract.imageRepository) {
173
+ errors.push('WorkloadInit release identity must use the canonical package and repositories');
174
+ }
175
+ if (typeof releaseArg.version !== 'string' || !semanticVersionRegex.test(releaseArg.version)) {
176
+ errors.push('WorkloadInit release version must be canonical stable semver');
177
+ } else if (releaseArg.sourceRef !== `refs/tags/v${releaseArg.version}`
178
+ || releaseArg.imageTag !== `v${releaseArg.version}`) {
179
+ errors.push('WorkloadInit release source ref and image tag must match its version');
180
+ }
181
+ if (typeof releaseArg.sourceRevision !== 'string'
182
+ || !gitRevisionRegex.test(releaseArg.sourceRevision)
183
+ || releaseArg.sourceDirty !== false) {
184
+ errors.push('WorkloadInit release source must be an exact clean Git revision');
185
+ }
186
+ return errors;
187
+ };
188
+
189
+ const canonicalToolVersions = (
190
+ versionsArg: IWorkloadInitBuildToolVersionsV1,
191
+ ): IWorkloadInitBuildToolVersionsV1 => ({
192
+ dockerBuildx: versionsArg.dockerBuildx,
193
+ gitzone: versionsArg.gitzone,
194
+ tsdocker: versionsArg.tsdocker,
195
+ tsrust: versionsArg.tsrust,
196
+ });
197
+
198
+ export const createWorkloadInitReleaseAttestationStatement = (inputArg: {
199
+ release: IWorkloadInitReleaseIdentityV1;
200
+ workloadInitImageIndexDigest: TSha256Digest;
201
+ toolVersions: IWorkloadInitBuildToolVersionsV1;
202
+ }): IWorkloadInitReleaseAttestationStatementV1 => ({
203
+ _type: workloadInitReleaseContract.statementType,
204
+ subject: [{
205
+ name: `${inputArg.release.imageRegistry}/${inputArg.release.imageRepository}:${
206
+ inputArg.release.imageTag
207
+ }`,
208
+ digest: {
209
+ sha256: inputArg.workloadInitImageIndexDigest.slice('sha256:'.length),
210
+ },
211
+ }],
212
+ predicateType: workloadInitReleaseContract.predicateType,
213
+ predicate: {
214
+ buildDefinition: {
215
+ buildType: workloadInitReleaseContract.buildType,
216
+ externalParameters: {
217
+ artifact: {
218
+ mediaType: workloadInitReleaseContract.imageIndexMediaType,
219
+ platforms: ['linux/amd64', 'linux/arm64'],
220
+ repository: `${inputArg.release.imageRegistry}/${inputArg.release.imageRepository}`,
221
+ tag: inputArg.release.imageTag,
222
+ },
223
+ source: {
224
+ repository: inputArg.release.sourceRepository,
225
+ tag: inputArg.release.sourceRef,
226
+ dirty: inputArg.release.sourceDirty,
227
+ },
228
+ },
229
+ resolvedDependencies: [{
230
+ uri: `git+${inputArg.release.sourceRepository}@${inputArg.release.sourceRef}`,
231
+ digest: {
232
+ gitCommit: inputArg.release.sourceRevision,
233
+ },
234
+ }],
235
+ },
236
+ runDetails: {
237
+ builder: {
238
+ id: workloadInitReleaseContract.builderId,
239
+ version: canonicalToolVersions(inputArg.toolVersions),
240
+ },
241
+ },
242
+ },
243
+ });
244
+
245
+ export const createWorkloadInitReleaseAttestationPayload = (
246
+ statementArg: IWorkloadInitReleaseAttestationStatementV1,
247
+ ): Uint8Array => new TextEncoder().encode(JSON.stringify(statementArg));
248
+
249
+ export const computeWorkloadInitReleaseAttestationStatementDigest = async (
250
+ statementArg: IWorkloadInitReleaseAttestationStatementV1,
251
+ ): Promise<TSha256Digest> => computeSha256(
252
+ createWorkloadInitReleaseAttestationPayload(statementArg),
253
+ );
254
+
255
+ export const validateWorkloadInitReleaseAttestationStatement = (
256
+ statementArg: unknown,
257
+ ): string[] => {
258
+ try {
259
+ if (!isRecord(statementArg)
260
+ || !hasExactKeys(statementArg, ['_type', 'subject', 'predicateType', 'predicate'])) {
261
+ return ['WorkloadInit release attestation statement must use its exact schema'];
262
+ }
263
+ const errors: string[] = [];
264
+ if (statementArg._type !== workloadInitReleaseContract.statementType
265
+ || statementArg.predicateType !== workloadInitReleaseContract.predicateType) {
266
+ errors.push('WorkloadInit release attestation types must be canonical');
267
+ }
268
+ if (!Array.isArray(statementArg.subject)
269
+ || statementArg.subject.length !== 1
270
+ || !isRecord(statementArg.subject[0])
271
+ || !hasExactKeys(statementArg.subject[0], ['name', 'digest'])
272
+ || !isRecord(statementArg.subject[0].digest)
273
+ || !hasExactKeys(statementArg.subject[0].digest, ['sha256'])
274
+ || typeof statementArg.subject[0].digest.sha256 !== 'string'
275
+ || !bareSha256Regex.test(statementArg.subject[0].digest.sha256)) {
276
+ errors.push('WorkloadInit release attestation must have one canonical root-index subject');
277
+ }
278
+ if (!isRecord(statementArg.predicate)
279
+ || !hasExactKeys(statementArg.predicate, ['buildDefinition', 'runDetails'])) {
280
+ return [...errors, 'WorkloadInit release attestation predicate must use its exact schema'];
281
+ }
282
+ const buildDefinition = statementArg.predicate.buildDefinition;
283
+ const runDetails = statementArg.predicate.runDetails;
284
+ if (!isRecord(buildDefinition)
285
+ || !hasExactKeys(buildDefinition, [
286
+ 'buildType',
287
+ 'externalParameters',
288
+ 'resolvedDependencies',
289
+ ])
290
+ || buildDefinition.buildType !== workloadInitReleaseContract.buildType
291
+ || !isRecord(buildDefinition.externalParameters)
292
+ || !hasExactKeys(buildDefinition.externalParameters, ['artifact', 'source'])
293
+ || !Array.isArray(buildDefinition.resolvedDependencies)
294
+ || buildDefinition.resolvedDependencies.length !== 1
295
+ || !isRecord(runDetails)
296
+ || !hasExactKeys(runDetails, ['builder'])
297
+ || !isRecord(runDetails.builder)
298
+ || !hasExactKeys(runDetails.builder, ['id', 'version'])
299
+ || runDetails.builder.id !== workloadInitReleaseContract.builderId) {
300
+ return [...errors, 'WorkloadInit release attestation build definition must be canonical'];
301
+ }
302
+ const artifact = buildDefinition.externalParameters.artifact;
303
+ const source = buildDefinition.externalParameters.source;
304
+ const dependency = buildDefinition.resolvedDependencies[0];
305
+ const toolVersions = runDetails.builder.version;
306
+ if (!isRecord(artifact)
307
+ || !hasExactKeys(artifact, ['mediaType', 'platforms', 'repository', 'tag'])
308
+ || artifact.mediaType !== workloadInitReleaseContract.imageIndexMediaType
309
+ || JSON.stringify(artifact.platforms) !== JSON.stringify(['linux/amd64', 'linux/arm64'])
310
+ || artifact.repository !== `${workloadInitReleaseContract.imageRegistry}/${
311
+ workloadInitReleaseContract.imageRepository
312
+ }`
313
+ || typeof artifact.tag !== 'string'
314
+ || !/^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/.test(artifact.tag)) {
315
+ errors.push('WorkloadInit release attestation artifact must be canonical and version-tagged');
316
+ }
317
+ if (!isRecord(source)
318
+ || !hasExactKeys(source, ['repository', 'tag', 'dirty'])
319
+ || source.repository !== workloadInitReleaseContract.sourceRepository
320
+ || source.dirty !== false
321
+ || typeof source.tag !== 'string'
322
+ || source.tag !== `refs/tags/${artifact && isRecord(artifact) ? artifact.tag : ''}`) {
323
+ errors.push('WorkloadInit release attestation source must match its clean version tag');
324
+ }
325
+ if (!isRecord(dependency)
326
+ || !hasExactKeys(dependency, ['uri', 'digest'])
327
+ || !isRecord(dependency.digest)
328
+ || !hasExactKeys(dependency.digest, ['gitCommit'])
329
+ || typeof dependency.digest.gitCommit !== 'string'
330
+ || !gitRevisionRegex.test(dependency.digest.gitCommit)
331
+ || dependency.uri !== `git+${workloadInitReleaseContract.sourceRepository}@${source && isRecord(source)
332
+ ? source.tag
333
+ : ''}`) {
334
+ errors.push('WorkloadInit release attestation source dependency must be canonical');
335
+ }
336
+ if (!isRecord(toolVersions)
337
+ || !hasExactKeys(toolVersions, ['dockerBuildx', 'gitzone', 'tsdocker', 'tsrust'])
338
+ || Object.values(toolVersions).some((valueArg) => (
339
+ typeof valueArg !== 'string' || !toolVersionRegex.test(valueArg)
340
+ ))) {
341
+ errors.push('WorkloadInit release attestation tool versions must be canonical');
342
+ }
343
+ if (Array.isArray(statementArg.subject)
344
+ && isRecord(statementArg.subject[0])
345
+ && isRecord(artifact)
346
+ && statementArg.subject[0].name
347
+ !== `${artifact.repository as string}:${artifact.tag as string}`) {
348
+ errors.push('WorkloadInit release attestation subject name must match the versioned image');
349
+ }
350
+ return errors;
351
+ } catch {
352
+ return ['WorkloadInit release attestation statement must be safely inspectable'];
353
+ }
354
+ };
355
+
356
+ export const validateWorkloadInitCloudlyVerificationSummary = (
357
+ summaryArg: unknown,
358
+ ): string[] => {
359
+ if (!isRecord(summaryArg)
360
+ || !hasExactKeys(summaryArg, [
361
+ 'signatureFormat',
362
+ 'signatureAlgorithm',
363
+ 'signerKeyId',
364
+ 'detachedBundleDigest',
365
+ 'statementDigest',
366
+ 'subjectImageIndexDigest',
367
+ 'builderId',
368
+ 'policyGeneration',
369
+ 'policyDigest',
370
+ 'verifiedAt',
371
+ ])) {
372
+ return ['WorkloadInit Cloudly verification summary must use its exact schema'];
373
+ }
374
+ const errors: string[] = [];
375
+ if (summaryArg.signatureFormat !== workloadInitReleaseContract.signatureFormat
376
+ || summaryArg.signatureAlgorithm !== workloadInitReleaseContract.signatureAlgorithm
377
+ || summaryArg.builderId !== workloadInitReleaseContract.builderId) {
378
+ errors.push('WorkloadInit Cloudly verification summary policy identifiers must be canonical');
379
+ }
380
+ for (const field of [
381
+ 'signerKeyId',
382
+ 'detachedBundleDigest',
383
+ 'statementDigest',
384
+ 'subjectImageIndexDigest',
385
+ 'policyDigest',
386
+ ] as const) {
387
+ if (typeof summaryArg[field] !== 'string' || !isSha256Digest(summaryArg[field])) {
388
+ errors.push(`WorkloadInit Cloudly verification summary ${field} must be canonical`);
389
+ }
390
+ }
391
+ if (!isPositiveSafeInteger(summaryArg.policyGeneration)
392
+ || !isPositiveSafeInteger(summaryArg.verifiedAt)) {
393
+ errors.push('WorkloadInit Cloudly verification summary generation and time must be positive');
394
+ }
395
+ return errors;
396
+ };
397
+
398
+ const canonicalReleaseIdentity = (
399
+ releaseArg: IWorkloadInitReleaseIdentityV1,
400
+ ): IWorkloadInitReleaseIdentityV1 => ({
401
+ packageName: releaseArg.packageName,
402
+ version: releaseArg.version,
403
+ sourceRepository: releaseArg.sourceRepository,
404
+ sourceRevision: releaseArg.sourceRevision,
405
+ sourceRef: releaseArg.sourceRef,
406
+ sourceDirty: releaseArg.sourceDirty,
407
+ imageRegistry: releaseArg.imageRegistry,
408
+ imageRepository: releaseArg.imageRepository,
409
+ imageTag: releaseArg.imageTag,
410
+ });
411
+
412
+ const canonicalApprovedArtifact = (
413
+ artifactArg: IWorkloadInitApprovedArtifactV1,
414
+ ): IWorkloadInitApprovedArtifactV1 => ({
415
+ platform: artifactArg.platform,
416
+ workloadInitPlatformManifestDigest: artifactArg.workloadInitPlatformManifestDigest,
417
+ workloadInitExecutableDigest: artifactArg.workloadInitExecutableDigest,
418
+ });
419
+
420
+ const canonicalVerificationSummary = (
421
+ summaryArg: IWorkloadInitCloudlyVerificationSummaryV1,
422
+ ): IWorkloadInitCloudlyVerificationSummaryV1 => ({
423
+ signatureFormat: summaryArg.signatureFormat,
424
+ signatureAlgorithm: summaryArg.signatureAlgorithm,
425
+ signerKeyId: summaryArg.signerKeyId,
426
+ detachedBundleDigest: summaryArg.detachedBundleDigest,
427
+ statementDigest: summaryArg.statementDigest,
428
+ subjectImageIndexDigest: summaryArg.subjectImageIndexDigest,
429
+ builderId: summaryArg.builderId,
430
+ policyGeneration: summaryArg.policyGeneration,
431
+ policyDigest: summaryArg.policyDigest,
432
+ verifiedAt: summaryArg.verifiedAt,
433
+ });
434
+
435
+ export const createWorkloadInitReleaseApprovalDigestInput = (
436
+ approvalArg: IWorkloadInitReleaseApprovalV1,
437
+ ): string => JSON.stringify({
438
+ schemaVersion: approvalArg.schemaVersion,
439
+ purpose: 'serve.zone/workloadinit-release-approval',
440
+ release: canonicalReleaseIdentity(approvalArg.release),
441
+ imageIndexMediaType: approvalArg.imageIndexMediaType,
442
+ workloadInitImageIndexDigest: approvalArg.workloadInitImageIndexDigest,
443
+ artifacts: approvalArg.artifacts.map(canonicalApprovedArtifact),
444
+ verificationSummary: canonicalVerificationSummary(approvalArg.verificationSummary),
445
+ approvedAt: approvalArg.approvedAt,
446
+ });
447
+
448
+ export const computeWorkloadInitReleaseApprovalDigest = async (
449
+ approvalArg: IWorkloadInitReleaseApprovalV1,
450
+ ): Promise<TSha256Digest> => computeSha256(
451
+ new TextEncoder().encode(createWorkloadInitReleaseApprovalDigestInput(approvalArg)),
452
+ );
453
+
454
+ export const validateWorkloadInitReleaseApproval = async (
455
+ approvalArg: unknown,
456
+ ): Promise<string[]> => {
457
+ try {
458
+ if (!isRecord(approvalArg)
459
+ || !hasExactKeys(approvalArg, [
460
+ 'schemaVersion',
461
+ 'release',
462
+ 'imageIndexMediaType',
463
+ 'workloadInitImageIndexDigest',
464
+ 'artifacts',
465
+ 'verificationSummary',
466
+ 'approvedAt',
467
+ 'approvalDigest',
468
+ ])) {
469
+ return ['WorkloadInit release approval must use its exact schema'];
470
+ }
471
+ const errors = [
472
+ ...validateWorkloadInitReleaseIdentity(approvalArg.release),
473
+ ...validateWorkloadInitCloudlyVerificationSummary(approvalArg.verificationSummary),
474
+ ];
475
+ if (approvalArg.schemaVersion !== 1
476
+ || approvalArg.imageIndexMediaType !== workloadInitReleaseContract.imageIndexMediaType
477
+ || typeof approvalArg.workloadInitImageIndexDigest !== 'string'
478
+ || !isSha256Digest(approvalArg.workloadInitImageIndexDigest)
479
+ || !isPositiveSafeInteger(approvalArg.approvedAt)
480
+ || typeof approvalArg.approvalDigest !== 'string'
481
+ || !isSha256Digest(approvalArg.approvalDigest)) {
482
+ errors.push('WorkloadInit release approval authority fields must be canonical');
483
+ }
484
+ if (!Array.isArray(approvalArg.artifacts)
485
+ || approvalArg.artifacts.length !== 2
486
+ || approvalArg.artifacts.map((artifactArg) => (
487
+ isRecord(artifactArg) ? artifactArg.platform : undefined
488
+ )).join(',') !== 'linux/amd64,linux/arm64') {
489
+ errors.push('WorkloadInit release approval must contain amd64 then arm64 artifacts');
490
+ } else {
491
+ const manifestDigests = new Set<string>();
492
+ for (const [index, artifactArg] of approvalArg.artifacts.entries()) {
493
+ if (!isRecord(artifactArg)
494
+ || !hasExactKeys(artifactArg, [
495
+ 'platform',
496
+ 'workloadInitPlatformManifestDigest',
497
+ 'workloadInitExecutableDigest',
498
+ ])
499
+ || typeof artifactArg.workloadInitPlatformManifestDigest !== 'string'
500
+ || !isSha256Digest(artifactArg.workloadInitPlatformManifestDigest)
501
+ || typeof artifactArg.workloadInitExecutableDigest !== 'string'
502
+ || !isSha256Digest(artifactArg.workloadInitExecutableDigest)) {
503
+ errors.push(`WorkloadInit release approval artifacts[${index}] must be canonical`);
504
+ } else {
505
+ manifestDigests.add(artifactArg.workloadInitPlatformManifestDigest);
506
+ }
507
+ }
508
+ if (manifestDigests.size !== approvalArg.artifacts.length) {
509
+ errors.push('WorkloadInit release approval platform manifest digests must be unique');
510
+ }
511
+ }
512
+ if (isRecord(approvalArg.verificationSummary)) {
513
+ if (approvalArg.verificationSummary.subjectImageIndexDigest
514
+ !== approvalArg.workloadInitImageIndexDigest) {
515
+ errors.push('WorkloadInit release approval subject must match its image index');
516
+ }
517
+ if (isPositiveSafeInteger(approvalArg.verificationSummary.verifiedAt)
518
+ && isPositiveSafeInteger(approvalArg.approvedAt)
519
+ && approvalArg.verificationSummary.verifiedAt > approvalArg.approvedAt) {
520
+ errors.push('WorkloadInit release approval cannot predate verification');
521
+ }
522
+ }
523
+ if (errors.length === 0
524
+ && approvalArg.approvalDigest !== await computeWorkloadInitReleaseApprovalDigest(
525
+ approvalArg as unknown as IWorkloadInitReleaseApprovalV1,
526
+ )) {
527
+ errors.push('WorkloadInit release approval digest does not match');
528
+ }
529
+ return errors;
530
+ } catch {
531
+ return ['WorkloadInit release approval must be safely inspectable'];
532
+ }
533
+ };
534
+
535
+ export const validateWorkloadInitApprovalAuthorityReference = (
536
+ referenceArg: unknown,
537
+ ): string[] => {
538
+ if (!isRecord(referenceArg)
539
+ || !hasExactKeys(referenceArg, [
540
+ 'authorityVersion',
541
+ 'authorityGeneration',
542
+ 'approvalDigest',
543
+ ])) {
544
+ return ['WorkloadInit approval authority reference must use its exact schema'];
545
+ }
546
+ const errors: string[] = [];
547
+ if (referenceArg.authorityVersion !== 1
548
+ || !isPositiveSafeInteger(referenceArg.authorityGeneration)
549
+ || typeof referenceArg.approvalDigest !== 'string'
550
+ || !isSha256Digest(referenceArg.approvalDigest)) {
551
+ errors.push('WorkloadInit approval authority reference fields must be canonical');
552
+ }
553
+ return errors;
554
+ };
555
+
556
+ export const validateWorkloadInitActiveApprovalAuthority = async (
557
+ authorityArg: unknown,
558
+ ): Promise<string[]> => {
559
+ if (!isRecord(authorityArg)
560
+ || !hasExactKeys(authorityArg, [
561
+ 'authorityVersion',
562
+ 'authorityGeneration',
563
+ 'approvalDigest',
564
+ 'approval',
565
+ ])) {
566
+ return ['active WorkloadInit approval authority must use its exact schema'];
567
+ }
568
+ const reference = {
569
+ authorityVersion: authorityArg.authorityVersion,
570
+ authorityGeneration: authorityArg.authorityGeneration,
571
+ approvalDigest: authorityArg.approvalDigest,
572
+ };
573
+ const errors = [
574
+ ...validateWorkloadInitApprovalAuthorityReference(reference),
575
+ ...await validateWorkloadInitReleaseApproval(authorityArg.approval),
576
+ ];
577
+ if (isRecord(authorityArg.approval)
578
+ && authorityArg.approvalDigest !== authorityArg.approval.approvalDigest) {
579
+ errors.push('active WorkloadInit authority must reference its embedded approval');
580
+ }
581
+ return errors;
582
+ };