@kungfu-tech/buildchain 2.13.0-alpha.0 → 2.13.0-alpha.2
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 +13 -0
- package/dist/site/buildchain-contract.json +32 -17
- package/dist/site/buildchain-site.json +28 -23
- package/dist/site/controller-registry.json +14 -2
- package/dist/site/kfd-claims.json +16 -5
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +5 -5
- package/dist/site/node-api-registry.json +3 -3
- package/dist/site/page-registry.json +19 -14
- package/dist/site/public-surface-audit.json +20 -7
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +9 -9
- package/dist/site/workflow-registry.json +18 -5
- package/docs/install.md +3 -1
- package/docs/publication-authority.md +26 -0
- package/docs/publish-transaction.md +1 -1
- package/docs/release-candidate.md +19 -0
- package/docs/release-governance.md +10 -1
- package/docs/stable-candidate-patrol.md +9 -0
- package/package.json +1 -1
- package/packages/core/index.js +6 -0
- package/packages/core/publication-authority.js +281 -12
- package/scripts/assemble-publication-artifact-admission.mjs +10 -0
- package/scripts/assemble-self-publication-admission.mjs +10 -1
- package/scripts/stable-candidate-qualification.mjs +12 -3
|
@@ -16,6 +16,10 @@ export const PUBLICATION_CONTROL_PLANE_AUDIT_CONTRACT =
|
|
|
16
16
|
"kungfu-buildchain-publication-control-plane-audit";
|
|
17
17
|
export const PUBLICATION_GATE_DECISION_CONTRACT =
|
|
18
18
|
"kungfu-buildchain-publication-gate-decision";
|
|
19
|
+
export const CONSUMER_PUBLICATION_DECISION_CONTRACT =
|
|
20
|
+
"kungfu-buildchain-consumer-publication-decision";
|
|
21
|
+
export const PUBLICATION_QUALIFICATION_RECEIPT_CONTRACT =
|
|
22
|
+
"kungfu-buildchain-publication-qualification-receipt";
|
|
19
23
|
export const PUBLICATION_ARTIFACT_MANIFEST_SET_CONTRACT =
|
|
20
24
|
"kungfu-buildchain-publication-artifact-manifest-set";
|
|
21
25
|
|
|
@@ -225,6 +229,16 @@ export function createPublicationControlPlaneAudit({
|
|
|
225
229
|
}
|
|
226
230
|
|
|
227
231
|
export function createPublicationAdmission(input = {}) {
|
|
232
|
+
const qualificationRequired = input.qualification?.required === true;
|
|
233
|
+
const qualification = {
|
|
234
|
+
required: qualificationRequired,
|
|
235
|
+
predicateId: qualificationRequired
|
|
236
|
+
? requiredString(input.qualification?.predicateId, "qualification.predicateId")
|
|
237
|
+
: "",
|
|
238
|
+
predicateDigest: qualificationRequired
|
|
239
|
+
? normalizeDigest(input.qualification?.predicateDigest, "qualification.predicateDigest")
|
|
240
|
+
: "",
|
|
241
|
+
};
|
|
228
242
|
const payload = {
|
|
229
243
|
schemaVersion: 1,
|
|
230
244
|
contract: PUBLICATION_ADMISSION_CONTRACT,
|
|
@@ -236,6 +250,10 @@ export function createPublicationAdmission(input = {}) {
|
|
|
236
250
|
runtimeSha: normalizeGitSha(input.runtimeSha, "runtimeSha"),
|
|
237
251
|
contractDigest: normalizeDigest(input.contractDigest, "contractDigest"),
|
|
238
252
|
policyDigest: normalizeDigest(input.policyDigest, "policyDigest"),
|
|
253
|
+
gateRegistryDigest: normalizeDigest(
|
|
254
|
+
input.gateRegistryDigest || input.policyDigest,
|
|
255
|
+
"gateRegistryDigest",
|
|
256
|
+
),
|
|
239
257
|
controllerReceiptDigest: normalizeDigest(input.controllerReceiptDigest, "controllerReceiptDigest"),
|
|
240
258
|
runnerProvenanceDigest: normalizeDigest(input.runnerProvenanceDigest, "runnerProvenanceDigest"),
|
|
241
259
|
controlPlaneAuditDigest: normalizeDigest(input.controlPlaneAuditDigest, "controlPlaneAuditDigest"),
|
|
@@ -249,6 +267,7 @@ export function createPublicationAdmission(input = {}) {
|
|
|
249
267
|
nonce: requiredString(input.nonce, "nonce"),
|
|
250
268
|
issuedAt: new Date(parseTime(input.issuedAt, "issuedAt")).toISOString(),
|
|
251
269
|
expiresAt: new Date(parseTime(input.expiresAt, "expiresAt")).toISOString(),
|
|
270
|
+
qualification,
|
|
252
271
|
};
|
|
253
272
|
return { ...payload, admissionDigest: publicationAuthorityDigest(payload) };
|
|
254
273
|
}
|
|
@@ -381,8 +400,7 @@ export function createPublicationArtifactManifestSet({
|
|
|
381
400
|
return { ...payload, manifestSetDigest: publicationAuthorityDigest(payload) };
|
|
382
401
|
}
|
|
383
402
|
|
|
384
|
-
function
|
|
385
|
-
const passportSourceSha = passport.source?.headSha || passport.source?.sha;
|
|
403
|
+
export function publicationGateAggregateBindings(gateAggregate) {
|
|
386
404
|
if (gateAggregate?.contract === "buildchain.shifu-gate-aggregate/v1") {
|
|
387
405
|
const { digest, ...payload } = gateAggregate;
|
|
388
406
|
const actualDigest = sha256Json(payload);
|
|
@@ -392,12 +410,17 @@ function validateGateAggregate(gateAggregate, { admission, passport }) {
|
|
|
392
410
|
if (gateAggregate.status !== "pass" || gateAggregate.qualifying !== true) {
|
|
393
411
|
throw new Error("gate aggregate is not qualifying");
|
|
394
412
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
413
|
+
const registryDigest = normalizeDigest(
|
|
414
|
+
gateAggregate.registry?.digest,
|
|
415
|
+
"gateAggregate.registry.digest",
|
|
416
|
+
);
|
|
399
417
|
const policyDigest = normalizeDigest(gateAggregate.matrixDigest, "gateAggregate.matrixDigest");
|
|
400
|
-
return {
|
|
418
|
+
return {
|
|
419
|
+
gateAggregateDigest: actualDigest,
|
|
420
|
+
policyDigest,
|
|
421
|
+
registryDigest,
|
|
422
|
+
sourceSha: normalizeGitSha(gateAggregate.sourceSha, "gateAggregate.sourceSha"),
|
|
423
|
+
};
|
|
401
424
|
}
|
|
402
425
|
if (gateAggregate?.contract !== PUBLICATION_GATE_DECISION_CONTRACT) {
|
|
403
426
|
throw new Error("gate evidence must be a Shifu Gate aggregate or explicit publication Gate decision");
|
|
@@ -410,9 +433,6 @@ function validateGateAggregate(gateAggregate, { admission, passport }) {
|
|
|
410
433
|
if (gateAggregate.required !== false) {
|
|
411
434
|
throw new Error("a required Gate policy must supply a qualifying Shifu Gate aggregate");
|
|
412
435
|
}
|
|
413
|
-
if (![admission.sourceSha, passportSourceSha].includes(gateAggregate.sourceSha)) {
|
|
414
|
-
throw new Error("publication Gate decision source SHA mismatch");
|
|
415
|
-
}
|
|
416
436
|
const normalizedPolicy = {
|
|
417
437
|
profile: requiredString(gateAggregate.profile, "gateDecision.profile"),
|
|
418
438
|
required: false,
|
|
@@ -423,7 +443,21 @@ function validateGateAggregate(gateAggregate, { admission, passport }) {
|
|
|
423
443
|
if (normalizeDigest(gateAggregate.policyDigest, "gateDecision.policyDigest") !== policyDigest) {
|
|
424
444
|
throw new Error("publication Gate policy digest mismatch");
|
|
425
445
|
}
|
|
426
|
-
return {
|
|
446
|
+
return {
|
|
447
|
+
gateAggregateDigest: actualDigest,
|
|
448
|
+
policyDigest,
|
|
449
|
+
registryDigest: policyDigest,
|
|
450
|
+
sourceSha: normalizeGitSha(gateAggregate.sourceSha, "gateDecision.sourceSha"),
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function validateGateAggregate(gateAggregate, { admission, passport }) {
|
|
455
|
+
const bindings = publicationGateAggregateBindings(gateAggregate);
|
|
456
|
+
const passportSourceSha = passport.source?.headSha || passport.source?.sha;
|
|
457
|
+
if (![admission.sourceSha, passportSourceSha].includes(bindings.sourceSha)) {
|
|
458
|
+
throw new Error("gate aggregate source SHA mismatch");
|
|
459
|
+
}
|
|
460
|
+
return bindings;
|
|
427
461
|
}
|
|
428
462
|
|
|
429
463
|
function validatePublicationEvidence(publicationEvidence, admission) {
|
|
@@ -458,6 +492,7 @@ function validatePublicationEvidence(publicationEvidence, admission) {
|
|
|
458
492
|
contractDigest,
|
|
459
493
|
gateAggregateDigest: gate.gateAggregateDigest,
|
|
460
494
|
policyDigest: gate.policyDigest,
|
|
495
|
+
gateRegistryDigest: gate.registryDigest,
|
|
461
496
|
artifactDigest: candidate.candidateDigest,
|
|
462
497
|
evidenceDigest: publicationAuthorityDigest({
|
|
463
498
|
sourceTreeSha: candidate.sourceTreeSha,
|
|
@@ -551,6 +586,7 @@ function validatePublicationEvidence(publicationEvidence, admission) {
|
|
|
551
586
|
contractDigest,
|
|
552
587
|
gateAggregateDigest: gate.gateAggregateDigest,
|
|
553
588
|
policyDigest: gate.policyDigest,
|
|
589
|
+
gateRegistryDigest: gate.registryDigest,
|
|
554
590
|
artifactDigest: manifestSet.manifestSetDigest,
|
|
555
591
|
evidenceDigest: publicationAuthorityDigest({
|
|
556
592
|
passportCandidateHash: passport.candidateHash,
|
|
@@ -712,7 +748,7 @@ export function verifyPublicationAdmission({
|
|
|
712
748
|
for (const key of ["sourceSha", "runtimeSha"]) {
|
|
713
749
|
normalizeGitSha(admission[key], `admission.${key}`);
|
|
714
750
|
}
|
|
715
|
-
for (const key of ["contractDigest", "policyDigest", "controllerReceiptDigest", "gateAggregateDigest", "artifactDigest"]) {
|
|
751
|
+
for (const key of ["contractDigest", "policyDigest", "controllerReceiptDigest", "gateAggregateDigest", "artifactDigest", ...(admission.gateRegistryDigest ? ["gateRegistryDigest"] : [])]) {
|
|
716
752
|
normalizeDigest(admission[key], `admission.${key}`);
|
|
717
753
|
}
|
|
718
754
|
|
|
@@ -726,6 +762,13 @@ export function verifyPublicationAdmission({
|
|
|
726
762
|
expectedDigest: admission.controlPlaneAuditDigest,
|
|
727
763
|
});
|
|
728
764
|
const evidence = validatePublicationEvidence(publicationEvidence, admission);
|
|
765
|
+
if (
|
|
766
|
+
admission.gateRegistryDigest &&
|
|
767
|
+
normalizeDigest(admission.gateRegistryDigest, "admission.gateRegistryDigest") !==
|
|
768
|
+
evidence.gateRegistryDigest
|
|
769
|
+
) {
|
|
770
|
+
throw new Error("Gate registry evidence binding mismatch");
|
|
771
|
+
}
|
|
729
772
|
const { admissionDigest: suppliedAdmissionDigest, producerDecision: _ignored, ...admissionPayload } = admission;
|
|
730
773
|
const actualAdmissionDigest = publicationAuthorityDigest(admissionPayload);
|
|
731
774
|
if (normalizeDigest(suppliedAdmissionDigest, "admission.admissionDigest") !== actualAdmissionDigest) {
|
|
@@ -744,6 +787,8 @@ export function verifyPublicationAdmission({
|
|
|
744
787
|
runtimeSha: normalizeGitSha(admission.runtimeSha, "admission.runtimeSha"),
|
|
745
788
|
contractDigest: normalizeDigest(admission.contractDigest, "admission.contractDigest"),
|
|
746
789
|
policyDigest: normalizeDigest(admission.policyDigest, "admission.policyDigest"),
|
|
790
|
+
authorityRegistryDigest: actualRegistryDigest,
|
|
791
|
+
gateRegistryDigest: evidence.gateRegistryDigest,
|
|
747
792
|
controllerReceiptDigest: normalizeDigest(admission.controllerReceiptDigest, "admission.controllerReceiptDigest"),
|
|
748
793
|
product: admission.product,
|
|
749
794
|
target: admission.target,
|
|
@@ -758,7 +803,231 @@ export function verifyPublicationAdmission({
|
|
|
758
803
|
gateAggregateDigest: normalizeDigest(admission.gateAggregateDigest, "admission.gateAggregateDigest"),
|
|
759
804
|
environment,
|
|
760
805
|
nonce,
|
|
806
|
+
issuedAt: admission.issuedAt,
|
|
761
807
|
expiresAt: admission.expiresAt,
|
|
808
|
+
qualification: admission.qualification?.required === true
|
|
809
|
+
? {
|
|
810
|
+
required: true,
|
|
811
|
+
predicateId: requiredString(
|
|
812
|
+
admission.qualification.predicateId,
|
|
813
|
+
"admission.qualification.predicateId",
|
|
814
|
+
),
|
|
815
|
+
predicateDigest: normalizeDigest(
|
|
816
|
+
admission.qualification.predicateDigest,
|
|
817
|
+
"admission.qualification.predicateDigest",
|
|
818
|
+
),
|
|
819
|
+
}
|
|
820
|
+
: { required: false, predicateId: "", predicateDigest: "" },
|
|
762
821
|
};
|
|
763
822
|
return { ...capability, capabilityDigest: publicationAuthorityDigest(capability) };
|
|
764
823
|
}
|
|
824
|
+
|
|
825
|
+
function validatePublicationCapability(capability, now = new Date()) {
|
|
826
|
+
if (capability?.contract !== PUBLICATION_CAPABILITY_CONTRACT) {
|
|
827
|
+
throw new Error("publication capability contract mismatch");
|
|
828
|
+
}
|
|
829
|
+
const { capabilityDigest: supplied, ...payload } = capability;
|
|
830
|
+
const actual = publicationAuthorityDigest(payload);
|
|
831
|
+
if (normalizeDigest(supplied, "capability.capabilityDigest") !== actual) {
|
|
832
|
+
throw new Error("publication capability digest mismatch");
|
|
833
|
+
}
|
|
834
|
+
if (capability.decision !== "allow") {
|
|
835
|
+
throw new Error("publication capability did not allow provider mutation");
|
|
836
|
+
}
|
|
837
|
+
const nowMs = now instanceof Date ? now.getTime() : parseTime(now, "now");
|
|
838
|
+
if (parseTime(capability.expiresAt, "capability.expiresAt") <= nowMs) {
|
|
839
|
+
throw new Error("publication capability is stale");
|
|
840
|
+
}
|
|
841
|
+
return actual;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function validateCapabilityGateAggregate(capability, gateAggregate) {
|
|
845
|
+
const gate = publicationGateAggregateBindings(gateAggregate);
|
|
846
|
+
if (gate.gateAggregateDigest !== normalizeDigest(capability.gateAggregateDigest, "capability.gateAggregateDigest")) {
|
|
847
|
+
throw new Error("publication qualification Gate aggregate binding mismatch");
|
|
848
|
+
}
|
|
849
|
+
if (gate.policyDigest !== normalizeDigest(capability.policyDigest, "capability.policyDigest")) {
|
|
850
|
+
throw new Error("publication qualification Gate policy binding mismatch");
|
|
851
|
+
}
|
|
852
|
+
if (gate.registryDigest !== normalizeDigest(capability.gateRegistryDigest, "capability.gateRegistryDigest")) {
|
|
853
|
+
throw new Error("publication qualification Gate registry binding mismatch");
|
|
854
|
+
}
|
|
855
|
+
if (gate.sourceSha !== normalizeGitSha(capability.sourceSha, "capability.sourceSha")) {
|
|
856
|
+
throw new Error("publication qualification Gate source binding mismatch");
|
|
857
|
+
}
|
|
858
|
+
return gate;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
export function createConsumerPublicationDecision({
|
|
862
|
+
capability,
|
|
863
|
+
gateAggregate,
|
|
864
|
+
decision,
|
|
865
|
+
predicateId,
|
|
866
|
+
predicateDigest,
|
|
867
|
+
evidence = {},
|
|
868
|
+
now = new Date(),
|
|
869
|
+
} = {}) {
|
|
870
|
+
const capabilityDigest = validatePublicationCapability(capability, now);
|
|
871
|
+
const gate = validateCapabilityGateAggregate(capability, gateAggregate);
|
|
872
|
+
if (capability.qualification?.required !== true) {
|
|
873
|
+
throw new Error("publication capability did not require consumer qualification");
|
|
874
|
+
}
|
|
875
|
+
const normalizedPredicateId = requiredString(predicateId, "predicateId");
|
|
876
|
+
const normalizedPredicateDigest = normalizeDigest(predicateDigest, "predicateDigest");
|
|
877
|
+
if (normalizedPredicateId !== capability.qualification.predicateId) {
|
|
878
|
+
throw new Error("consumer publication predicate id mismatch");
|
|
879
|
+
}
|
|
880
|
+
if (normalizedPredicateDigest !== capability.qualification.predicateDigest) {
|
|
881
|
+
throw new Error("consumer publication predicate digest mismatch");
|
|
882
|
+
}
|
|
883
|
+
const normalizedDecision = requiredString(decision, "decision");
|
|
884
|
+
if (!["allow", "deny"].includes(normalizedDecision)) {
|
|
885
|
+
throw new Error("consumer publication decision must be allow or deny");
|
|
886
|
+
}
|
|
887
|
+
const payload = {
|
|
888
|
+
schemaVersion: 1,
|
|
889
|
+
contract: CONSUMER_PUBLICATION_DECISION_CONTRACT,
|
|
890
|
+
decision: normalizedDecision,
|
|
891
|
+
predicateId: normalizedPredicateId,
|
|
892
|
+
predicateDigest: normalizedPredicateDigest,
|
|
893
|
+
capabilityDigest,
|
|
894
|
+
gateAggregateDigest: gate.gateAggregateDigest,
|
|
895
|
+
sourceSha: capability.sourceSha,
|
|
896
|
+
artifactDigest: capability.artifactDigest,
|
|
897
|
+
version: capability.version,
|
|
898
|
+
channel: capability.channel,
|
|
899
|
+
nonce: capability.nonce,
|
|
900
|
+
evidence,
|
|
901
|
+
};
|
|
902
|
+
return { ...payload, decisionDigest: publicationAuthorityDigest(payload) };
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
export function createPublicationQualificationReceipt({
|
|
906
|
+
capability,
|
|
907
|
+
gateAggregate,
|
|
908
|
+
consumerDecision,
|
|
909
|
+
now = new Date(),
|
|
910
|
+
} = {}) {
|
|
911
|
+
const capabilityDigest = validatePublicationCapability(capability, now);
|
|
912
|
+
const gate = validateCapabilityGateAggregate(capability, gateAggregate);
|
|
913
|
+
if (capability.qualification?.required !== true) {
|
|
914
|
+
throw new Error("publication capability did not require consumer qualification");
|
|
915
|
+
}
|
|
916
|
+
if (consumerDecision?.contract !== CONSUMER_PUBLICATION_DECISION_CONTRACT) {
|
|
917
|
+
throw new Error("consumer publication decision contract mismatch");
|
|
918
|
+
}
|
|
919
|
+
const { decisionDigest: suppliedDecisionDigest, ...decisionPayload } = consumerDecision;
|
|
920
|
+
const decisionDigest = publicationAuthorityDigest(decisionPayload);
|
|
921
|
+
if (normalizeDigest(suppliedDecisionDigest, "consumerDecision.decisionDigest") !== decisionDigest) {
|
|
922
|
+
throw new Error("consumer publication decision digest mismatch");
|
|
923
|
+
}
|
|
924
|
+
if (consumerDecision.decision !== "allow") {
|
|
925
|
+
throw new Error("consumer publication predicate did not qualify");
|
|
926
|
+
}
|
|
927
|
+
const exactBindings = {
|
|
928
|
+
predicateId: capability.qualification.predicateId,
|
|
929
|
+
predicateDigest: capability.qualification.predicateDigest,
|
|
930
|
+
capabilityDigest,
|
|
931
|
+
gateAggregateDigest: gate.gateAggregateDigest,
|
|
932
|
+
sourceSha: capability.sourceSha,
|
|
933
|
+
artifactDigest: capability.artifactDigest,
|
|
934
|
+
version: capability.version,
|
|
935
|
+
channel: capability.channel,
|
|
936
|
+
nonce: capability.nonce,
|
|
937
|
+
};
|
|
938
|
+
for (const [key, expected] of Object.entries(exactBindings)) {
|
|
939
|
+
if (String(consumerDecision[key] || "") !== String(expected)) {
|
|
940
|
+
throw new Error(`consumer publication decision ${key} binding mismatch`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
const payload = {
|
|
944
|
+
schemaVersion: 1,
|
|
945
|
+
contract: PUBLICATION_QUALIFICATION_RECEIPT_CONTRACT,
|
|
946
|
+
decision: "allow",
|
|
947
|
+
...exactBindings,
|
|
948
|
+
authorityRegistryDigest: capability.authorityRegistryDigest,
|
|
949
|
+
gateRegistryDigest: gate.registryDigest,
|
|
950
|
+
policyDigest: gate.policyDigest,
|
|
951
|
+
runtimeSha: capability.runtimeSha,
|
|
952
|
+
contractDigest: capability.contractDigest,
|
|
953
|
+
controllerReceiptDigest: capability.controllerReceiptDigest,
|
|
954
|
+
runnerProvenanceDigest: capability.runnerProvenanceDigest,
|
|
955
|
+
controlPlaneAuditDigest: capability.controlPlaneAuditDigest,
|
|
956
|
+
publicationEvidenceDigest: capability.publicationEvidenceDigest,
|
|
957
|
+
product: capability.product,
|
|
958
|
+
target: capability.target,
|
|
959
|
+
issuedAt: capability.issuedAt,
|
|
960
|
+
expiresAt: capability.expiresAt,
|
|
961
|
+
consumerDecisionDigest: decisionDigest,
|
|
962
|
+
};
|
|
963
|
+
return { ...payload, receiptDigest: publicationAuthorityDigest(payload) };
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
export function verifyPublicationQualificationReceipt({
|
|
967
|
+
receipt,
|
|
968
|
+
capability,
|
|
969
|
+
gateAggregate,
|
|
970
|
+
expected = {},
|
|
971
|
+
usedNonces = [],
|
|
972
|
+
now = new Date(),
|
|
973
|
+
} = {}) {
|
|
974
|
+
const capabilityDigest = validatePublicationCapability(capability, now);
|
|
975
|
+
const gate = validateCapabilityGateAggregate(capability, gateAggregate);
|
|
976
|
+
if (capability.qualification?.required !== true) {
|
|
977
|
+
throw new Error("publication capability did not require consumer qualification");
|
|
978
|
+
}
|
|
979
|
+
if (receipt?.contract !== PUBLICATION_QUALIFICATION_RECEIPT_CONTRACT) {
|
|
980
|
+
throw new Error("publication qualification receipt contract mismatch");
|
|
981
|
+
}
|
|
982
|
+
if (receipt.decision !== "allow") {
|
|
983
|
+
throw new Error("publication qualification receipt did not allow provider mutation");
|
|
984
|
+
}
|
|
985
|
+
const exactBindings = {
|
|
986
|
+
predicateId: capability.qualification.predicateId,
|
|
987
|
+
predicateDigest: capability.qualification.predicateDigest,
|
|
988
|
+
capabilityDigest,
|
|
989
|
+
gateAggregateDigest: gate.gateAggregateDigest,
|
|
990
|
+
authorityRegistryDigest: capability.authorityRegistryDigest,
|
|
991
|
+
gateRegistryDigest: gate.registryDigest,
|
|
992
|
+
policyDigest: gate.policyDigest,
|
|
993
|
+
sourceSha: capability.sourceSha,
|
|
994
|
+
runtimeSha: capability.runtimeSha,
|
|
995
|
+
contractDigest: capability.contractDigest,
|
|
996
|
+
controllerReceiptDigest: capability.controllerReceiptDigest,
|
|
997
|
+
runnerProvenanceDigest: capability.runnerProvenanceDigest,
|
|
998
|
+
controlPlaneAuditDigest: capability.controlPlaneAuditDigest,
|
|
999
|
+
publicationEvidenceDigest: capability.publicationEvidenceDigest,
|
|
1000
|
+
artifactDigest: capability.artifactDigest,
|
|
1001
|
+
product: capability.product,
|
|
1002
|
+
target: capability.target,
|
|
1003
|
+
version: capability.version,
|
|
1004
|
+
channel: capability.channel,
|
|
1005
|
+
nonce: capability.nonce,
|
|
1006
|
+
issuedAt: capability.issuedAt,
|
|
1007
|
+
expiresAt: capability.expiresAt,
|
|
1008
|
+
};
|
|
1009
|
+
for (const [key, value] of Object.entries(exactBindings)) {
|
|
1010
|
+
if (String(receipt[key] || "") !== String(value)) {
|
|
1011
|
+
throw new Error(`publication qualification receipt ${key} binding mismatch`);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
normalizeDigest(
|
|
1015
|
+
receipt.consumerDecisionDigest,
|
|
1016
|
+
"receipt.consumerDecisionDigest",
|
|
1017
|
+
);
|
|
1018
|
+
const { receiptDigest: suppliedReceiptDigest, ...receiptPayload } = receipt;
|
|
1019
|
+
const receiptDigest = publicationAuthorityDigest(receiptPayload);
|
|
1020
|
+
if (normalizeDigest(suppliedReceiptDigest, "receipt.receiptDigest") !== receiptDigest) {
|
|
1021
|
+
throw new Error("publication qualification receipt digest mismatch");
|
|
1022
|
+
}
|
|
1023
|
+
const nonce = requiredString(receipt.nonce, "receipt.nonce");
|
|
1024
|
+
if (new Set(usedNonces.map(String)).has(nonce)) {
|
|
1025
|
+
throw new Error("publication qualification receipt nonce was replayed");
|
|
1026
|
+
}
|
|
1027
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
1028
|
+
if (value !== undefined && String(receipt[key] || "") !== String(value)) {
|
|
1029
|
+
throw new Error(`publication qualification receipt ${key} binding mismatch`);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return { ok: true, receiptDigest, nonce };
|
|
1033
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
createPublicationAdmission,
|
|
9
9
|
createPublicationGateDecision,
|
|
10
10
|
createRunnerProvenance,
|
|
11
|
+
publicationGateAggregateBindings,
|
|
11
12
|
} from "../packages/core/publication-authority.js";
|
|
12
13
|
import { buildPublicationArtifactCandidate } from "./publication-artifact-candidate.mjs";
|
|
13
14
|
|
|
@@ -126,6 +127,8 @@ async function main() {
|
|
|
126
127
|
isolation: "github-hosted-single-job",
|
|
127
128
|
});
|
|
128
129
|
const issuedAt = new Date();
|
|
130
|
+
const gateBindings = publicationGateAggregateBindings(gateAggregate);
|
|
131
|
+
const qualificationRequired = process.env.BUILDCHAIN_CONSUMER_QUALIFICATION_REQUIRED === "true";
|
|
129
132
|
const admission = createPublicationAdmission({
|
|
130
133
|
registryDigest: registry.registryDigest,
|
|
131
134
|
workflowPath:
|
|
@@ -137,6 +140,7 @@ async function main() {
|
|
|
137
140
|
runtimeSha,
|
|
138
141
|
contractDigest: controllerReceipt.runtime?.contractDigest,
|
|
139
142
|
policyDigest: gateAggregate.policyDigest,
|
|
143
|
+
gateRegistryDigest: gateBindings.registryDigest,
|
|
140
144
|
controllerReceiptDigest: controllerReceipt.digest,
|
|
141
145
|
runnerProvenanceDigest: runnerProvenance.receiptDigest,
|
|
142
146
|
controlPlaneAuditDigest: controlPlaneAudit.receiptDigest,
|
|
@@ -150,6 +154,11 @@ async function main() {
|
|
|
150
154
|
nonce: `${required("GITHUB_RUN_ID")}:${required("GITHUB_RUN_ATTEMPT")}:${sourceSha}:paper`,
|
|
151
155
|
issuedAt: issuedAt.toISOString(),
|
|
152
156
|
expiresAt: new Date(issuedAt.getTime() + 10 * 60 * 1000).toISOString(),
|
|
157
|
+
qualification: {
|
|
158
|
+
required: qualificationRequired,
|
|
159
|
+
predicateId: process.env.BUILDCHAIN_CONSUMER_PREDICATE_ID || "",
|
|
160
|
+
predicateDigest: process.env.BUILDCHAIN_CONSUMER_PREDICATE_DIGEST || "",
|
|
161
|
+
},
|
|
153
162
|
});
|
|
154
163
|
const bindingNames = [
|
|
155
164
|
"repository",
|
|
@@ -158,6 +167,7 @@ async function main() {
|
|
|
158
167
|
"runtimeSha",
|
|
159
168
|
"contractDigest",
|
|
160
169
|
"policyDigest",
|
|
170
|
+
"gateRegistryDigest",
|
|
161
171
|
"controllerReceiptDigest",
|
|
162
172
|
"gateAggregateDigest",
|
|
163
173
|
"environment",
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
createPublicationArtifactManifestSet,
|
|
10
10
|
createPublicationGateDecision,
|
|
11
11
|
createRunnerProvenance,
|
|
12
|
+
publicationGateAggregateBindings,
|
|
12
13
|
} from "../packages/core/publication-authority.js";
|
|
13
14
|
|
|
14
15
|
function required(name) {
|
|
@@ -148,6 +149,8 @@ async function main() {
|
|
|
148
149
|
const packageJson = JSON.parse(fs.readFileSync(path.join(runtimeRoot, "package.json"), "utf8"));
|
|
149
150
|
const publicationVersion = required("BUILDCHAIN_PUBLICATION_VERSION");
|
|
150
151
|
const issuedAt = new Date();
|
|
152
|
+
const gateBindings = publicationGateAggregateBindings(gateAggregate);
|
|
153
|
+
const qualificationRequired = process.env.BUILDCHAIN_CONSUMER_QUALIFICATION_REQUIRED === "true";
|
|
151
154
|
const admission = createPublicationAdmission({
|
|
152
155
|
registryDigest: registry.registryDigest,
|
|
153
156
|
workflowPath: required("BUILDCHAIN_AUTHORITY_WORKFLOW_PATH"),
|
|
@@ -157,6 +160,7 @@ async function main() {
|
|
|
157
160
|
runtimeSha,
|
|
158
161
|
contractDigest: controllerReceipt.runtime?.contractDigest,
|
|
159
162
|
policyDigest: gateAggregate.policyDigest,
|
|
163
|
+
gateRegistryDigest: gateBindings.registryDigest,
|
|
160
164
|
controllerReceiptDigest: controllerReceipt.digest,
|
|
161
165
|
runnerProvenanceDigest: runnerProvenance.receiptDigest,
|
|
162
166
|
controlPlaneAuditDigest: controlPlaneAudit.receiptDigest,
|
|
@@ -170,9 +174,14 @@ async function main() {
|
|
|
170
174
|
nonce: `${required("GITHUB_RUN_ID")}:${required("GITHUB_RUN_ATTEMPT")}:${sourceSha}`,
|
|
171
175
|
issuedAt: issuedAt.toISOString(),
|
|
172
176
|
expiresAt: new Date(issuedAt.getTime() + 10 * 60 * 1000).toISOString(),
|
|
177
|
+
qualification: {
|
|
178
|
+
required: qualificationRequired,
|
|
179
|
+
predicateId: process.env.BUILDCHAIN_CONSUMER_PREDICATE_ID || "",
|
|
180
|
+
predicateDigest: process.env.BUILDCHAIN_CONSUMER_PREDICATE_DIGEST || "",
|
|
181
|
+
},
|
|
173
182
|
});
|
|
174
183
|
const bindingNames = [
|
|
175
|
-
"repository", "publisherWorkflowPath", "sourceSha", "runtimeSha", "contractDigest", "policyDigest",
|
|
184
|
+
"repository", "publisherWorkflowPath", "sourceSha", "runtimeSha", "contractDigest", "policyDigest", "gateRegistryDigest",
|
|
176
185
|
"controllerReceiptDigest", "gateAggregateDigest", "environment", "product", "target", "version", "channel",
|
|
177
186
|
"artifactDigest",
|
|
178
187
|
];
|
|
@@ -33,6 +33,14 @@ function repository(value) {
|
|
|
33
33
|
return normalized;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function optionalSha(value, label) {
|
|
37
|
+
const normalized = text(value);
|
|
38
|
+
if (normalized && !/^[0-9a-f]{40}$/i.test(normalized)) {
|
|
39
|
+
throw new Error(`${label} must be an exact 40-character commit SHA, got ${normalized}`);
|
|
40
|
+
}
|
|
41
|
+
return normalized;
|
|
42
|
+
}
|
|
43
|
+
|
|
36
44
|
export function normalizeStableCandidateQualificationOptions(options = {}) {
|
|
37
45
|
const candidateSha = text(options.candidateSha ?? process.env.BUILDCHAIN_QUALIFICATION_CANDIDATE_SHA);
|
|
38
46
|
if (!/^[0-9a-f]{40}$/i.test(candidateSha)) {
|
|
@@ -47,6 +55,7 @@ export function normalizeStableCandidateQualificationOptions(options = {}) {
|
|
|
47
55
|
canaryWorkflowFile: text(options.canaryWorkflowFile ?? process.env.BUILDCHAIN_QUALIFICATION_CANARY_WORKFLOW_FILE) || DEFAULTS.canaryWorkflowFile,
|
|
48
56
|
canaryWorkflowName: text(options.canaryWorkflowName ?? process.env.BUILDCHAIN_QUALIFICATION_CANARY_WORKFLOW_NAME) || DEFAULTS.canaryWorkflowName,
|
|
49
57
|
canaryStatusContext: text(options.canaryStatusContext ?? process.env.BUILDCHAIN_QUALIFICATION_CANARY_STATUS_CONTEXT) || DEFAULTS.canaryStatusContext,
|
|
58
|
+
canaryRef: optionalSha(options.canaryRef ?? process.env.BUILDCHAIN_QUALIFICATION_CANARY_REF, "canary ref"),
|
|
50
59
|
pollAttempts: integer(options.pollAttempts ?? process.env.BUILDCHAIN_QUALIFICATION_POLL_ATTEMPTS, DEFAULTS.pollAttempts),
|
|
51
60
|
pollIntervalMs: integer(options.pollIntervalMs ?? process.env.BUILDCHAIN_QUALIFICATION_POLL_INTERVAL_MS, DEFAULTS.pollIntervalMs),
|
|
52
61
|
dryRun: bool(options.dryRun ?? process.env.BUILDCHAIN_QUALIFICATION_DRY_RUN, false),
|
|
@@ -108,13 +117,13 @@ export async function runStableCandidateQualification(optionsInput = {}, clientI
|
|
|
108
117
|
let status = await client.findCommitStatus(options.repository, options.candidateSha, options.canaryStatusContext);
|
|
109
118
|
let canary = { state: status?.state === "success" ? "existing" : "pending", run: undefined };
|
|
110
119
|
if (status?.state !== "success") {
|
|
111
|
-
const
|
|
120
|
+
const canaryRef = options.canaryRef || await client.defaultBranch(options.canaryRepository);
|
|
112
121
|
canary = await ensureWorkflowEvidence({
|
|
113
122
|
client,
|
|
114
123
|
repository: options.canaryRepository,
|
|
115
124
|
workflowFile: options.canaryWorkflowFile,
|
|
116
125
|
workflowName: options.canaryWorkflowName,
|
|
117
|
-
ref:
|
|
126
|
+
ref: canaryRef,
|
|
118
127
|
headSha: options.candidateSha,
|
|
119
128
|
runName: `${options.canaryWorkflowName} / ${options.candidateSha}`,
|
|
120
129
|
options,
|
|
@@ -137,7 +146,7 @@ export async function runStableCandidateQualification(optionsInput = {}, clientI
|
|
|
137
146
|
dryRun: options.dryRun,
|
|
138
147
|
candidate,
|
|
139
148
|
build: { state: build.state, url: build.run?.html_url || "" },
|
|
140
|
-
canary: { state: canary.state, url: canary.run?.html_url || status?.target_url || "" },
|
|
149
|
+
canary: { state: canary.state, ref: options.canaryRef || "default-branch", url: canary.run?.html_url || status?.target_url || "" },
|
|
141
150
|
attestation: { context: options.canaryStatusContext, state: options.dryRun ? "planned" : status?.state || "" },
|
|
142
151
|
};
|
|
143
152
|
}
|