@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.
- package/changelog.md +9 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/appstore/types.d.ts +2 -2
- package/dist_ts/appstore/types.js +1 -1
- package/dist_ts/data/clusternode.d.ts +85 -0
- package/dist_ts/data/clusternode.js +355 -1
- package/dist_ts/data/secret.d.ts +51 -4
- package/dist_ts/data/secret.js +326 -10
- package/dist_ts/requests/secret.d.ts +33 -6
- package/dist_ts/requests/secret.js +198 -1
- package/dist_ts/runtime.d.ts +52 -29
- package/dist_ts/runtime.js +239 -94
- package/dist_ts/runtime.workloadinit.d.ts +129 -0
- package/dist_ts/runtime.workloadinit.js +405 -0
- package/package.json +1 -1
- package/readme.md +45 -17
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/appstore/types.ts +2 -2
- package/ts/data/clusternode.ts +513 -0
- package/ts/data/secret.ts +402 -16
- package/ts/requests/secret.ts +248 -6
- package/ts/runtime.ts +366 -141
- package/ts/runtime.workloadinit.ts +582 -0
package/ts/runtime.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import type {
|
|
3
3
|
IActiveSecretRecipientMetadata,
|
|
4
|
+
IClusterRuntimeTargetSetReadyV1,
|
|
5
|
+
IClusterRuntimeTargetV1,
|
|
4
6
|
IClusterSecretDeploymentState,
|
|
5
7
|
ICoreflowRuntimeCapabilities,
|
|
6
8
|
IIdentityCredential,
|
|
@@ -21,7 +23,22 @@ import {
|
|
|
21
23
|
verifySecretEnvelopeContext,
|
|
22
24
|
verifyResolvedSecretManifestDigest,
|
|
23
25
|
} from './data/secret.js';
|
|
26
|
+
import {
|
|
27
|
+
isClusterRuntimeTargetSetFresh,
|
|
28
|
+
validateClusterRuntimeTargetSet,
|
|
29
|
+
} from './data/clusternode.js';
|
|
24
30
|
import { isSha256Digest } from './data/immutableimage.js';
|
|
31
|
+
import type {
|
|
32
|
+
IWorkloadInitActiveApprovalAuthorityV1,
|
|
33
|
+
IWorkloadInitApprovalAuthorityReferenceV1,
|
|
34
|
+
IWorkloadInitApprovedArtifactV1,
|
|
35
|
+
} from './runtime.workloadinit.js';
|
|
36
|
+
import {
|
|
37
|
+
validateWorkloadInitActiveApprovalAuthority,
|
|
38
|
+
validateWorkloadInitApprovalAuthorityReference,
|
|
39
|
+
} from './runtime.workloadinit.js';
|
|
40
|
+
|
|
41
|
+
export * from './runtime.workloadinit.js';
|
|
25
42
|
|
|
26
43
|
export interface ISealedResolvedSecretMaterialEntry {
|
|
27
44
|
secretVersionId: string;
|
|
@@ -402,6 +419,130 @@ const computeRuntimeSha256 = async (inputArg: string): Promise<TSha256Digest> =>
|
|
|
402
419
|
.join('')}` as TSha256Digest;
|
|
403
420
|
};
|
|
404
421
|
|
|
422
|
+
const computeRuntimeSha256Bytes = async (
|
|
423
|
+
inputArg: Uint8Array,
|
|
424
|
+
): Promise<TSha256Digest> => {
|
|
425
|
+
const digest = new Uint8Array(await globalThis.crypto.subtle.digest(
|
|
426
|
+
'SHA-256',
|
|
427
|
+
new Uint8Array(inputArg),
|
|
428
|
+
));
|
|
429
|
+
return `sha256:${[...digest]
|
|
430
|
+
.map((byteArg) => byteArg.toString(16).padStart(2, '0'))
|
|
431
|
+
.join('')}` as TSha256Digest;
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Reproducible binding persisted inside a trusted Cloudly mutation receipt.
|
|
436
|
+
* This value alone does not prove that Cloudly admitted the envelope.
|
|
437
|
+
*/
|
|
438
|
+
export interface ISecretEnvelopeAdmissionBindingV1 {
|
|
439
|
+
schemaVersion: 1;
|
|
440
|
+
recipientKeyId: string;
|
|
441
|
+
recipientGeneration: number;
|
|
442
|
+
envelopeDigest: TSha256Digest;
|
|
443
|
+
requestContextDigest: TSha256Digest;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export const createSecretEnvelopeDigestInput = (
|
|
447
|
+
envelopeArg: unknown,
|
|
448
|
+
): string => {
|
|
449
|
+
const envelope = plugins.smartcrypto.parseX25519Envelope(envelopeArg);
|
|
450
|
+
return JSON.stringify({
|
|
451
|
+
schemaVersion: envelope.schemaVersion,
|
|
452
|
+
profile: envelope.profile,
|
|
453
|
+
recipientKeyId: envelope.recipientKeyId,
|
|
454
|
+
ephemeralPublicKey: envelope.ephemeralPublicKey,
|
|
455
|
+
nonce: envelope.nonce,
|
|
456
|
+
ciphertext: envelope.ciphertext,
|
|
457
|
+
tag: envelope.tag,
|
|
458
|
+
contextDigest: envelope.contextDigest,
|
|
459
|
+
});
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
export const computeSecretEnvelopeDigest = async (
|
|
463
|
+
envelopeArg: unknown,
|
|
464
|
+
): Promise<TSha256Digest> => computeRuntimeSha256(createSecretEnvelopeDigestInput(envelopeArg));
|
|
465
|
+
|
|
466
|
+
export const computeSecretRequestContextDigest = async (
|
|
467
|
+
contextArg: Uint8Array,
|
|
468
|
+
): Promise<TSha256Digest> => {
|
|
469
|
+
if (!(contextArg instanceof Uint8Array)) {
|
|
470
|
+
throw new Error('secret request context must be bytes');
|
|
471
|
+
}
|
|
472
|
+
return computeRuntimeSha256Bytes(contextArg);
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
export const validateSecretEnvelopeAdmissionBinding = (
|
|
476
|
+
bindingArg: unknown,
|
|
477
|
+
): string[] => {
|
|
478
|
+
if (!isRuntimeRecord(bindingArg)
|
|
479
|
+
|| !hasExactRuntimeKeys(bindingArg, [
|
|
480
|
+
'schemaVersion',
|
|
481
|
+
'recipientKeyId',
|
|
482
|
+
'recipientGeneration',
|
|
483
|
+
'envelopeDigest',
|
|
484
|
+
'requestContextDigest',
|
|
485
|
+
])) {
|
|
486
|
+
return ['secret envelope admission binding must use its exact schema'];
|
|
487
|
+
}
|
|
488
|
+
const errors: string[] = [];
|
|
489
|
+
if (bindingArg.schemaVersion !== 1) {
|
|
490
|
+
errors.push('secret envelope admission binding schemaVersion must be 1');
|
|
491
|
+
}
|
|
492
|
+
if (!isRuntimeIdentifier(bindingArg.recipientKeyId)
|
|
493
|
+
|| !isPositiveSafeInteger(bindingArg.recipientGeneration)) {
|
|
494
|
+
errors.push('secret envelope admission binding recipient must be canonical');
|
|
495
|
+
}
|
|
496
|
+
if (typeof bindingArg.envelopeDigest !== 'string'
|
|
497
|
+
|| !isSha256Digest(bindingArg.envelopeDigest)
|
|
498
|
+
|| typeof bindingArg.requestContextDigest !== 'string'
|
|
499
|
+
|| !isSha256Digest(bindingArg.requestContextDigest)) {
|
|
500
|
+
errors.push('secret envelope admission binding digests must be canonical');
|
|
501
|
+
}
|
|
502
|
+
return errors;
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
export const createSecretEnvelopeAdmissionBinding = async (
|
|
506
|
+
recipientArg: IActiveSecretRecipientMetadata,
|
|
507
|
+
envelopeArg: unknown,
|
|
508
|
+
requestContextArg: Uint8Array,
|
|
509
|
+
): Promise<ISecretEnvelopeAdmissionBindingV1> => {
|
|
510
|
+
if (validateSecretRecipientMetadata(recipientArg).length > 0
|
|
511
|
+
|| recipientArg.lifecycleState !== 'active') {
|
|
512
|
+
throw new Error('secret envelope admission requires the active recipient');
|
|
513
|
+
}
|
|
514
|
+
const envelope = plugins.smartcrypto.parseX25519Envelope(envelopeArg);
|
|
515
|
+
if (envelope.recipientKeyId !== recipientArg.recipientKeyId
|
|
516
|
+
|| !await verifySecretEnvelopeContext(envelope, requestContextArg)) {
|
|
517
|
+
throw new Error('secret envelope admission binding does not match recipient and context');
|
|
518
|
+
}
|
|
519
|
+
return {
|
|
520
|
+
schemaVersion: 1,
|
|
521
|
+
recipientKeyId: recipientArg.recipientKeyId,
|
|
522
|
+
recipientGeneration: recipientArg.generation,
|
|
523
|
+
envelopeDigest: await computeSecretEnvelopeDigest(envelope),
|
|
524
|
+
requestContextDigest: await computeSecretRequestContextDigest(requestContextArg),
|
|
525
|
+
};
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
export const verifySecretEnvelopeAdmissionBinding = async (
|
|
529
|
+
bindingArg: unknown,
|
|
530
|
+
envelopeArg: unknown,
|
|
531
|
+
requestContextArg: Uint8Array,
|
|
532
|
+
): Promise<boolean> => {
|
|
533
|
+
try {
|
|
534
|
+
if (validateSecretEnvelopeAdmissionBinding(bindingArg).length > 0) return false;
|
|
535
|
+
const binding = bindingArg as ISecretEnvelopeAdmissionBindingV1;
|
|
536
|
+
const envelope = plugins.smartcrypto.parseX25519Envelope(envelopeArg);
|
|
537
|
+
return envelope.recipientKeyId === binding.recipientKeyId
|
|
538
|
+
&& await verifySecretEnvelopeContext(envelope, requestContextArg)
|
|
539
|
+
&& binding.envelopeDigest === await computeSecretEnvelopeDigest(envelope)
|
|
540
|
+
&& binding.requestContextDigest === await computeSecretRequestContextDigest(requestContextArg);
|
|
541
|
+
} catch {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
|
|
405
546
|
export type TSecretRecipientEnrollmentStateV1 =
|
|
406
547
|
| {
|
|
407
548
|
schemaVersion: 1;
|
|
@@ -481,37 +622,42 @@ export const validateSecretRecipientEnrollmentState = (
|
|
|
481
622
|
}
|
|
482
623
|
};
|
|
483
624
|
|
|
484
|
-
export interface
|
|
485
|
-
|
|
486
|
-
nodeName: string;
|
|
487
|
-
platform: TImmutableContainerPlatform;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
export interface ICoreflowSecretRuntimeNodeEvidenceV1
|
|
491
|
-
extends ICoreflowSecretRuntimeNodeTargetV1 {
|
|
492
|
-
workloadInitPlatformManifestDigest: TSha256Digest;
|
|
493
|
-
workloadInitExecutableDigest: TSha256Digest;
|
|
494
|
-
}
|
|
625
|
+
export interface ICoreflowSecretRuntimeNodeTargetV2
|
|
626
|
+
extends IClusterRuntimeTargetV1 {}
|
|
495
627
|
|
|
496
|
-
export interface
|
|
497
|
-
|
|
628
|
+
export interface ICoreflowSecretRuntimeNodeEvidenceV2
|
|
629
|
+
extends ICoreflowSecretRuntimeNodeTargetV2 {
|
|
498
630
|
workloadInitPlatformManifestDigest: TSha256Digest;
|
|
499
631
|
workloadInitExecutableDigest: TSha256Digest;
|
|
500
632
|
}
|
|
501
633
|
|
|
502
|
-
export interface
|
|
503
|
-
expectationVersion:
|
|
634
|
+
export interface ICoreflowSecretRuntimeRegistrationExpectationV2 {
|
|
635
|
+
expectationVersion: 2;
|
|
636
|
+
expectationDigest: TSha256Digest;
|
|
504
637
|
reporterSessionId: string;
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
workloadInitArtifacts: ICoreflowSecretRuntimeArtifactV1[];
|
|
638
|
+
targetAuthority: IClusterRuntimeTargetSetReadyV1;
|
|
639
|
+
workloadInitAuthority: IWorkloadInitActiveApprovalAuthorityV1;
|
|
508
640
|
activeRecipient: IActiveSecretRecipientMetadata;
|
|
509
641
|
}
|
|
510
642
|
|
|
511
|
-
/** Dedicated TypedSocket tag carrying
|
|
643
|
+
/** Dedicated TypedSocket tag carrying ICoreflowSecretRuntimeRegistrationV2. */
|
|
512
644
|
export const coreflowSecretRuntimeRegistrationTagId =
|
|
513
645
|
'coreflowSecretRuntimeRegistration' as const;
|
|
514
646
|
|
|
647
|
+
export type TCoreflowSecretRuntimeRegistrationExpectationResponseV2 =
|
|
648
|
+
| {
|
|
649
|
+
status: 'available';
|
|
650
|
+
expectation: ICoreflowSecretRuntimeRegistrationExpectationV2;
|
|
651
|
+
}
|
|
652
|
+
| {
|
|
653
|
+
status: 'unavailable';
|
|
654
|
+
reason:
|
|
655
|
+
| 'recipient-unavailable'
|
|
656
|
+
| 'workloadinit-unconfigured'
|
|
657
|
+
| 'workloadinit-revoked'
|
|
658
|
+
| 'targets-unavailable';
|
|
659
|
+
};
|
|
660
|
+
|
|
515
661
|
/** Cluster scope is derived exclusively from the verified cluster JWT. */
|
|
516
662
|
export interface IReq_GetCoreflowSecretRuntimeRegistrationExpectation
|
|
517
663
|
extends plugins.typedrequestInterfaces.implementsTR<
|
|
@@ -522,9 +668,7 @@ extends plugins.typedrequestInterfaces.implementsTR<
|
|
|
522
668
|
request: {
|
|
523
669
|
identity: IIdentityCredential;
|
|
524
670
|
};
|
|
525
|
-
response:
|
|
526
|
-
expectation: ICoreflowSecretRuntimeRegistrationExpectationV1;
|
|
527
|
-
};
|
|
671
|
+
response: TCoreflowSecretRuntimeRegistrationExpectationResponseV2;
|
|
528
672
|
}
|
|
529
673
|
|
|
530
674
|
export const validateGetCoreflowSecretRuntimeRegistrationExpectationRequest = (
|
|
@@ -545,25 +689,29 @@ export const validateGetCoreflowSecretRuntimeRegistrationExpectationRequest = (
|
|
|
545
689
|
}
|
|
546
690
|
};
|
|
547
691
|
|
|
548
|
-
export interface
|
|
549
|
-
registrationVersion:
|
|
692
|
+
export interface ICoreflowSecretRuntimeRegistrationV2 {
|
|
693
|
+
registrationVersion: 2;
|
|
694
|
+
expectationDigest: TSha256Digest;
|
|
550
695
|
reporterSessionId: string;
|
|
551
696
|
registeredAt: number;
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
697
|
+
targetGeneration: number;
|
|
698
|
+
targetSetDigest: TSha256Digest;
|
|
699
|
+
targets: ICoreflowSecretRuntimeNodeTargetV2[];
|
|
700
|
+
nodeEvidence: ICoreflowSecretRuntimeNodeEvidenceV2[];
|
|
555
701
|
capabilities: ICoreflowRuntimeCapabilities;
|
|
556
702
|
activeRecipient: IActiveSecretRecipientMetadata;
|
|
703
|
+
workloadInitAuthority: IWorkloadInitApprovalAuthorityReferenceV1;
|
|
557
704
|
workloadInitImageIndexDigest: TSha256Digest;
|
|
558
|
-
workloadInitApprovedAt: number;
|
|
559
705
|
}
|
|
560
706
|
|
|
561
707
|
const compareRuntimeTargets = (
|
|
562
|
-
leftArg:
|
|
563
|
-
rightArg:
|
|
708
|
+
leftArg: ICoreflowSecretRuntimeNodeTargetV2,
|
|
709
|
+
rightArg: ICoreflowSecretRuntimeNodeTargetV2,
|
|
564
710
|
): number => {
|
|
565
711
|
for (const [left, right] of [
|
|
566
|
-
[leftArg.
|
|
712
|
+
[leftArg.cloudlyNodeId, rightArg.cloudlyNodeId],
|
|
713
|
+
[leftArg.swarmClusterId, rightArg.swarmClusterId],
|
|
714
|
+
[leftArg.swarmNodeId, rightArg.swarmNodeId],
|
|
567
715
|
[leftArg.nodeName, rightArg.nodeName],
|
|
568
716
|
[leftArg.platform, rightArg.platform],
|
|
569
717
|
]) {
|
|
@@ -574,24 +722,50 @@ const compareRuntimeTargets = (
|
|
|
574
722
|
};
|
|
575
723
|
|
|
576
724
|
const canonicalRuntimeTarget = (
|
|
577
|
-
targetArg:
|
|
578
|
-
):
|
|
579
|
-
|
|
725
|
+
targetArg: ICoreflowSecretRuntimeNodeTargetV2,
|
|
726
|
+
): ICoreflowSecretRuntimeNodeTargetV2 => ({
|
|
727
|
+
cloudlyNodeId: targetArg.cloudlyNodeId,
|
|
728
|
+
swarmClusterId: targetArg.swarmClusterId,
|
|
729
|
+
swarmNodeId: targetArg.swarmNodeId,
|
|
580
730
|
nodeName: targetArg.nodeName,
|
|
581
731
|
platform: targetArg.platform,
|
|
582
732
|
});
|
|
583
733
|
|
|
584
|
-
|
|
585
|
-
|
|
734
|
+
const canonicalActiveRecipient = (
|
|
735
|
+
recipientArg: IActiveSecretRecipientMetadata,
|
|
736
|
+
): IActiveSecretRecipientMetadata => ({
|
|
737
|
+
schemaVersion: recipientArg.schemaVersion,
|
|
738
|
+
recipientKeyId: recipientArg.recipientKeyId,
|
|
739
|
+
publicKey: recipientArg.publicKey,
|
|
740
|
+
generation: recipientArg.generation,
|
|
741
|
+
activatedAt: recipientArg.activatedAt,
|
|
742
|
+
lifecycleState: recipientArg.lifecycleState,
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
export const createCoreflowSecretRuntimeRegistrationExpectationDigestInput = (
|
|
746
|
+
expectationArg: ICoreflowSecretRuntimeRegistrationExpectationV2,
|
|
586
747
|
): string => JSON.stringify({
|
|
587
|
-
schemaVersion:
|
|
588
|
-
|
|
748
|
+
schemaVersion: 2,
|
|
749
|
+
purpose: 'serve.zone/coreflow-secret-runtime-expectation',
|
|
750
|
+
reporterSessionId: expectationArg.reporterSessionId,
|
|
751
|
+
targetAuthority: {
|
|
752
|
+
cloudlyClusterId: expectationArg.targetAuthority.cloudlyClusterId,
|
|
753
|
+
swarmClusterId: expectationArg.targetAuthority.swarmClusterId,
|
|
754
|
+
generation: expectationArg.targetAuthority.generation,
|
|
755
|
+
targetSetDigest: expectationArg.targetAuthority.targetSetDigest,
|
|
756
|
+
},
|
|
757
|
+
workloadInitAuthority: {
|
|
758
|
+
authorityVersion: expectationArg.workloadInitAuthority.authorityVersion,
|
|
759
|
+
authorityGeneration: expectationArg.workloadInitAuthority.authorityGeneration,
|
|
760
|
+
approvalDigest: expectationArg.workloadInitAuthority.approvalDigest,
|
|
761
|
+
},
|
|
762
|
+
activeRecipient: canonicalActiveRecipient(expectationArg.activeRecipient),
|
|
589
763
|
});
|
|
590
764
|
|
|
591
|
-
export const
|
|
592
|
-
|
|
765
|
+
export const computeCoreflowSecretRuntimeRegistrationExpectationDigest = async (
|
|
766
|
+
expectationArg: ICoreflowSecretRuntimeRegistrationExpectationV2,
|
|
593
767
|
): Promise<TSha256Digest> => computeRuntimeSha256(
|
|
594
|
-
|
|
768
|
+
createCoreflowSecretRuntimeRegistrationExpectationDigestInput(expectationArg),
|
|
595
769
|
);
|
|
596
770
|
|
|
597
771
|
const validateRuntimeTargets = (
|
|
@@ -604,178 +778,219 @@ const validateRuntimeTargets = (
|
|
|
604
778
|
return [`${pathArg} must be a non-empty bounded array`];
|
|
605
779
|
}
|
|
606
780
|
const errors: string[] = [];
|
|
607
|
-
let previous:
|
|
608
|
-
const
|
|
781
|
+
let previous: ICoreflowSecretRuntimeNodeTargetV2 | undefined;
|
|
782
|
+
const cloudlyNodeIds = new Set<string>();
|
|
783
|
+
const swarmNodeScopes = new Set<string>();
|
|
609
784
|
const nodeNames = new Set<string>();
|
|
610
785
|
for (const [index, targetArg] of targetsArg.entries()) {
|
|
611
786
|
if (!isRuntimeRecord(targetArg)
|
|
612
|
-
|| !hasExactRuntimeKeys(targetArg, [
|
|
787
|
+
|| !hasExactRuntimeKeys(targetArg, [
|
|
788
|
+
'cloudlyNodeId',
|
|
789
|
+
'swarmClusterId',
|
|
790
|
+
'swarmNodeId',
|
|
791
|
+
'nodeName',
|
|
792
|
+
'platform',
|
|
793
|
+
])) {
|
|
613
794
|
errors.push(`${pathArg}[${index}] must use its exact schema`);
|
|
614
795
|
continue;
|
|
615
796
|
}
|
|
616
|
-
if (!isRuntimeIdentifier(targetArg.
|
|
797
|
+
if (!isRuntimeIdentifier(targetArg.cloudlyNodeId)
|
|
798
|
+
|| !isRuntimeIdentifier(targetArg.swarmClusterId)
|
|
799
|
+
|| !isRuntimeIdentifier(targetArg.swarmNodeId)
|
|
800
|
+
|| !isRuntimeIdentifier(targetArg.nodeName)) {
|
|
617
801
|
errors.push(`${pathArg}[${index}] node identity must be canonical`);
|
|
618
802
|
}
|
|
619
803
|
if (!runtimePlatforms.has(targetArg.platform as TImmutableContainerPlatform)) {
|
|
620
804
|
errors.push(`${pathArg}[${index}].platform must be supported`);
|
|
621
805
|
}
|
|
622
|
-
if (isRuntimeIdentifier(targetArg.
|
|
806
|
+
if (isRuntimeIdentifier(targetArg.cloudlyNodeId)
|
|
807
|
+
&& isRuntimeIdentifier(targetArg.swarmClusterId)
|
|
808
|
+
&& isRuntimeIdentifier(targetArg.swarmNodeId)
|
|
623
809
|
&& isRuntimeIdentifier(targetArg.nodeName)
|
|
624
810
|
&& runtimePlatforms.has(targetArg.platform as TImmutableContainerPlatform)) {
|
|
625
|
-
const target = targetArg as unknown as
|
|
811
|
+
const target = targetArg as unknown as ICoreflowSecretRuntimeNodeTargetV2;
|
|
626
812
|
if (previous && compareRuntimeTargets(previous, target) >= 0) {
|
|
627
|
-
errors.push(`${pathArg} must be uniquely sorted by
|
|
813
|
+
errors.push(`${pathArg} must be uniquely sorted by runtime identity`);
|
|
628
814
|
}
|
|
629
815
|
previous = target;
|
|
630
|
-
|
|
816
|
+
const swarmNodeScope = `${target.swarmClusterId}\0${target.swarmNodeId}`;
|
|
817
|
+
if (cloudlyNodeIds.has(target.cloudlyNodeId)) {
|
|
818
|
+
errors.push(`${pathArg} cloudlyNodeIds must be unique`);
|
|
819
|
+
}
|
|
820
|
+
if (swarmNodeScopes.has(swarmNodeScope)) {
|
|
821
|
+
errors.push(`${pathArg} scoped swarmNodeIds must be unique`);
|
|
822
|
+
}
|
|
631
823
|
if (nodeNames.has(target.nodeName)) errors.push(`${pathArg} nodeNames must be unique`);
|
|
632
|
-
|
|
824
|
+
cloudlyNodeIds.add(target.cloudlyNodeId);
|
|
825
|
+
swarmNodeScopes.add(swarmNodeScope);
|
|
633
826
|
nodeNames.add(target.nodeName);
|
|
634
827
|
}
|
|
635
828
|
}
|
|
636
829
|
return errors;
|
|
637
830
|
};
|
|
638
831
|
|
|
639
|
-
const validateRuntimeArtifacts = (
|
|
640
|
-
artifactsArg: unknown,
|
|
641
|
-
pathArg: string,
|
|
642
|
-
): string[] => {
|
|
643
|
-
if (!Array.isArray(artifactsArg)
|
|
644
|
-
|| artifactsArg.length === 0
|
|
645
|
-
|| artifactsArg.length > runtimePlatforms.size) {
|
|
646
|
-
return [`${pathArg} must be a non-empty bounded array`];
|
|
647
|
-
}
|
|
648
|
-
const errors: string[] = [];
|
|
649
|
-
const platforms: string[] = [];
|
|
650
|
-
for (const [index, artifactArg] of artifactsArg.entries()) {
|
|
651
|
-
if (!isRuntimeRecord(artifactArg)
|
|
652
|
-
|| !hasExactRuntimeKeys(artifactArg, [
|
|
653
|
-
'platform',
|
|
654
|
-
'workloadInitPlatformManifestDigest',
|
|
655
|
-
'workloadInitExecutableDigest',
|
|
656
|
-
])) {
|
|
657
|
-
errors.push(`${pathArg}[${index}] must use its exact schema`);
|
|
658
|
-
continue;
|
|
659
|
-
}
|
|
660
|
-
platforms.push(artifactArg.platform as string);
|
|
661
|
-
if (!runtimePlatforms.has(artifactArg.platform as TImmutableContainerPlatform)) {
|
|
662
|
-
errors.push(`${pathArg}[${index}].platform must be supported`);
|
|
663
|
-
}
|
|
664
|
-
if (typeof artifactArg.workloadInitPlatformManifestDigest !== 'string'
|
|
665
|
-
|| !isSha256Digest(artifactArg.workloadInitPlatformManifestDigest)
|
|
666
|
-
|| typeof artifactArg.workloadInitExecutableDigest !== 'string'
|
|
667
|
-
|| !isSha256Digest(artifactArg.workloadInitExecutableDigest)) {
|
|
668
|
-
errors.push(`${pathArg}[${index}] digests must be canonical`);
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
if (new Set(platforms).size !== platforms.length
|
|
672
|
-
|| JSON.stringify(platforms) !== JSON.stringify([...platforms].sort())) {
|
|
673
|
-
errors.push(`${pathArg} must be uniquely sorted by platform`);
|
|
674
|
-
}
|
|
675
|
-
return errors;
|
|
676
|
-
};
|
|
677
|
-
|
|
678
832
|
const activeRecipientFingerprint = (recipientArg: IActiveSecretRecipientMetadata): string => (
|
|
679
|
-
JSON.stringify(
|
|
680
|
-
schemaVersion: recipientArg.schemaVersion,
|
|
681
|
-
recipientKeyId: recipientArg.recipientKeyId,
|
|
682
|
-
publicKey: recipientArg.publicKey,
|
|
683
|
-
generation: recipientArg.generation,
|
|
684
|
-
activatedAt: recipientArg.activatedAt,
|
|
685
|
-
lifecycleState: recipientArg.lifecycleState,
|
|
686
|
-
})
|
|
833
|
+
JSON.stringify(canonicalActiveRecipient(recipientArg))
|
|
687
834
|
);
|
|
688
835
|
|
|
689
836
|
/**
|
|
690
|
-
*
|
|
691
|
-
*
|
|
837
|
+
* Validates only a Cloudly-created expectation. It does not establish the
|
|
838
|
+
* private DSSE trust decision or Spark manager consensus that produced it.
|
|
692
839
|
*/
|
|
693
|
-
export const
|
|
694
|
-
registrationArg: unknown,
|
|
840
|
+
export const validateCoreflowSecretRuntimeRegistrationExpectation = async (
|
|
695
841
|
expectationArg: unknown,
|
|
842
|
+
trustedNowArg: number,
|
|
696
843
|
): Promise<string[]> => {
|
|
697
844
|
try {
|
|
698
845
|
const errors: string[] = [];
|
|
699
846
|
if (!isRuntimeRecord(expectationArg)
|
|
700
847
|
|| !hasExactRuntimeKeys(expectationArg, [
|
|
701
848
|
'expectationVersion',
|
|
849
|
+
'expectationDigest',
|
|
702
850
|
'reporterSessionId',
|
|
703
|
-
'
|
|
704
|
-
'
|
|
705
|
-
'workloadInitArtifacts',
|
|
851
|
+
'targetAuthority',
|
|
852
|
+
'workloadInitAuthority',
|
|
706
853
|
'activeRecipient',
|
|
707
854
|
])) {
|
|
708
855
|
return ['secret runtime registration expectation must use its exact schema'];
|
|
709
856
|
}
|
|
710
|
-
if (expectationArg.expectationVersion !==
|
|
711
|
-
errors.push('secret runtime registration expectationVersion must be
|
|
857
|
+
if (expectationArg.expectationVersion !== 2) {
|
|
858
|
+
errors.push('secret runtime registration expectationVersion must be 2');
|
|
712
859
|
}
|
|
713
860
|
if (!isRuntimeIdentifier(expectationArg.reporterSessionId)) {
|
|
714
861
|
errors.push('expected secret runtime reporterSessionId must be canonical');
|
|
715
862
|
}
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
863
|
+
const targetAuthorityErrors = await validateClusterRuntimeTargetSet(
|
|
864
|
+
expectationArg.targetAuthority,
|
|
865
|
+
);
|
|
866
|
+
errors.push(...targetAuthorityErrors.map((errorArg) => `expected ${errorArg}`));
|
|
867
|
+
if (targetAuthorityErrors.length === 0
|
|
868
|
+
&& !isClusterRuntimeTargetSetFresh(
|
|
869
|
+
expectationArg.targetAuthority as IClusterRuntimeTargetSetReadyV1,
|
|
870
|
+
trustedNowArg,
|
|
871
|
+
)) {
|
|
872
|
+
errors.push('expected cluster runtime target authority must be fresh');
|
|
725
873
|
}
|
|
874
|
+
if (isRuntimeRecord(expectationArg.targetAuthority)
|
|
875
|
+
&& expectationArg.targetAuthority.state === 'ready') {
|
|
876
|
+
errors.push(...validateRuntimeTargets(
|
|
877
|
+
expectationArg.targetAuthority.targets,
|
|
878
|
+
'expected targets',
|
|
879
|
+
));
|
|
880
|
+
}
|
|
881
|
+
errors.push(...await validateWorkloadInitActiveApprovalAuthority(
|
|
882
|
+
expectationArg.workloadInitAuthority,
|
|
883
|
+
));
|
|
726
884
|
const expectedRecipientErrors = validateSecretRecipientMetadata(expectationArg.activeRecipient);
|
|
727
885
|
if (expectedRecipientErrors.length > 0
|
|
728
886
|
|| !isRuntimeRecord(expectationArg.activeRecipient)
|
|
729
887
|
|| expectationArg.activeRecipient.lifecycleState !== 'active') {
|
|
730
888
|
errors.push('expected secret recipient must be canonical and active');
|
|
731
889
|
}
|
|
890
|
+
if (isRuntimeRecord(expectationArg.workloadInitAuthority)
|
|
891
|
+
&& isRuntimeRecord(expectationArg.workloadInitAuthority.approval)
|
|
892
|
+
&& Array.isArray(expectationArg.workloadInitAuthority.approval.artifacts)
|
|
893
|
+
&& isRuntimeRecord(expectationArg.targetAuthority)
|
|
894
|
+
&& Array.isArray(expectationArg.targetAuthority.targets)) {
|
|
895
|
+
const approvedPlatforms = new Set(
|
|
896
|
+
expectationArg.workloadInitAuthority.approval.artifacts.map((artifactArg) => (
|
|
897
|
+
isRuntimeRecord(artifactArg) ? artifactArg.platform : undefined
|
|
898
|
+
)),
|
|
899
|
+
);
|
|
900
|
+
if (expectationArg.targetAuthority.targets.some((targetArg) => (
|
|
901
|
+
!isRuntimeRecord(targetArg) || !approvedPlatforms.has(targetArg.platform)
|
|
902
|
+
))) {
|
|
903
|
+
errors.push('expected target platforms must all have approved WorkloadInit artifacts');
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
if (typeof expectationArg.expectationDigest !== 'string'
|
|
907
|
+
|| !isSha256Digest(expectationArg.expectationDigest)) {
|
|
908
|
+
errors.push('secret runtime expectationDigest must be canonical');
|
|
909
|
+
} else if (errors.length === 0
|
|
910
|
+
&& expectationArg.expectationDigest
|
|
911
|
+
!== await computeCoreflowSecretRuntimeRegistrationExpectationDigest(
|
|
912
|
+
expectationArg as unknown as ICoreflowSecretRuntimeRegistrationExpectationV2,
|
|
913
|
+
)) {
|
|
914
|
+
errors.push('secret runtime expectationDigest does not match');
|
|
915
|
+
}
|
|
916
|
+
return errors;
|
|
917
|
+
} catch {
|
|
918
|
+
return ['secret runtime registration expectation must be safely inspectable'];
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* Consumers discard this registration on transport disconnect, tag
|
|
924
|
+
* removal/replacement, reporter-session change, or any expectation change.
|
|
925
|
+
*/
|
|
926
|
+
export const validateCoreflowSecretRuntimeRegistration = async (
|
|
927
|
+
registrationArg: unknown,
|
|
928
|
+
expectationArg: unknown,
|
|
929
|
+
trustedNowArg: number,
|
|
930
|
+
): Promise<string[]> => {
|
|
931
|
+
try {
|
|
932
|
+
const errors = await validateCoreflowSecretRuntimeRegistrationExpectation(
|
|
933
|
+
expectationArg,
|
|
934
|
+
trustedNowArg,
|
|
935
|
+
);
|
|
936
|
+
if (errors.length > 0 || !isRuntimeRecord(expectationArg)) return errors;
|
|
732
937
|
if (!isRuntimeRecord(registrationArg)
|
|
733
938
|
|| !hasExactRuntimeKeys(registrationArg, [
|
|
734
939
|
'registrationVersion',
|
|
940
|
+
'expectationDigest',
|
|
735
941
|
'reporterSessionId',
|
|
736
942
|
'registeredAt',
|
|
943
|
+
'targetGeneration',
|
|
944
|
+
'targetSetDigest',
|
|
737
945
|
'targets',
|
|
738
|
-
'nodeSetDigest',
|
|
739
946
|
'nodeEvidence',
|
|
740
947
|
'capabilities',
|
|
741
948
|
'activeRecipient',
|
|
949
|
+
'workloadInitAuthority',
|
|
742
950
|
'workloadInitImageIndexDigest',
|
|
743
|
-
'workloadInitApprovedAt',
|
|
744
951
|
])) {
|
|
745
952
|
errors.push('secret runtime registration must use its exact schema');
|
|
746
953
|
return errors;
|
|
747
954
|
}
|
|
748
|
-
if (registrationArg.registrationVersion !==
|
|
749
|
-
errors.push('secret runtime registrationVersion must be
|
|
955
|
+
if (registrationArg.registrationVersion !== 2) {
|
|
956
|
+
errors.push('secret runtime registrationVersion must be 2');
|
|
957
|
+
}
|
|
958
|
+
if (registrationArg.expectationDigest !== expectationArg.expectationDigest) {
|
|
959
|
+
errors.push('secret runtime registration expectationDigest does not match');
|
|
750
960
|
}
|
|
751
961
|
if (!isRuntimeIdentifier(registrationArg.reporterSessionId)) {
|
|
752
962
|
errors.push('secret runtime reporterSessionId must be canonical');
|
|
753
963
|
} else if (registrationArg.reporterSessionId !== expectationArg.reporterSessionId) {
|
|
754
964
|
errors.push('secret runtime reporterSessionId does not match the live session');
|
|
755
965
|
}
|
|
756
|
-
if (!isPositiveSafeInteger(registrationArg.registeredAt)
|
|
757
|
-
|
|
758
|
-
errors.push('secret runtime registration timestamps must be positive integers');
|
|
966
|
+
if (!isPositiveSafeInteger(registrationArg.registeredAt)) {
|
|
967
|
+
errors.push('secret runtime registration timestamp must be positive');
|
|
759
968
|
}
|
|
760
969
|
const registrationTargetErrors = validateRuntimeTargets(
|
|
761
970
|
registrationArg.targets,
|
|
762
971
|
'registration targets',
|
|
763
972
|
);
|
|
764
973
|
errors.push(...registrationTargetErrors);
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
974
|
+
const targetAuthority = expectationArg.targetAuthority as unknown as IClusterRuntimeTargetSetReadyV1;
|
|
975
|
+
if (registrationArg.targetGeneration !== targetAuthority.generation
|
|
976
|
+
|| registrationArg.targetSetDigest !== targetAuthority.targetSetDigest) {
|
|
977
|
+
errors.push('secret runtime registration target authority does not match');
|
|
768
978
|
}
|
|
769
979
|
if (typeof registrationArg.workloadInitImageIndexDigest !== 'string'
|
|
770
980
|
|| !isSha256Digest(registrationArg.workloadInitImageIndexDigest)) {
|
|
771
981
|
errors.push('secret runtime WorkloadInit image index digest must be canonical');
|
|
772
982
|
}
|
|
773
983
|
const registrationRecipientErrors = validateSecretRecipientMetadata(registrationArg.activeRecipient);
|
|
984
|
+
const expectedRecipientErrors = validateSecretRecipientMetadata(expectationArg.activeRecipient);
|
|
774
985
|
if (registrationRecipientErrors.length > 0
|
|
775
986
|
|| !isRuntimeRecord(registrationArg.activeRecipient)
|
|
776
987
|
|| registrationArg.activeRecipient.lifecycleState !== 'active') {
|
|
777
988
|
errors.push('secret runtime registration recipient must be canonical and active');
|
|
778
989
|
}
|
|
990
|
+
const registrationAuthorityErrors = validateWorkloadInitApprovalAuthorityReference(
|
|
991
|
+
registrationArg.workloadInitAuthority,
|
|
992
|
+
);
|
|
993
|
+
errors.push(...registrationAuthorityErrors);
|
|
779
994
|
if (!isRuntimeRecord(registrationArg.capabilities)
|
|
780
995
|
|| !hasOnlyRuntimeKeys(registrationArg.capabilities, [
|
|
781
996
|
'immutableImageDeploymentVersion',
|
|
@@ -801,12 +1016,14 @@ export const validateCoreflowSecretRuntimeRegistration = async (
|
|
|
801
1016
|
|| registrationArg.nodeEvidence.length > maximumRuntimeNodes) {
|
|
802
1017
|
errors.push('secret runtime node evidence must be a non-empty bounded array');
|
|
803
1018
|
} else {
|
|
804
|
-
const evidenceTargets:
|
|
805
|
-
const evidenceArtifacts:
|
|
1019
|
+
const evidenceTargets: ICoreflowSecretRuntimeNodeTargetV2[] = [];
|
|
1020
|
+
const evidenceArtifacts: IWorkloadInitApprovedArtifactV1[] = [];
|
|
806
1021
|
for (const [index, evidenceArg] of registrationArg.nodeEvidence.entries()) {
|
|
807
1022
|
if (!isRuntimeRecord(evidenceArg)
|
|
808
1023
|
|| !hasExactRuntimeKeys(evidenceArg, [
|
|
809
|
-
'
|
|
1024
|
+
'cloudlyNodeId',
|
|
1025
|
+
'swarmClusterId',
|
|
1026
|
+
'swarmNodeId',
|
|
810
1027
|
'nodeName',
|
|
811
1028
|
'platform',
|
|
812
1029
|
'workloadInitPlatformManifestDigest',
|
|
@@ -816,7 +1033,9 @@ export const validateCoreflowSecretRuntimeRegistration = async (
|
|
|
816
1033
|
continue;
|
|
817
1034
|
}
|
|
818
1035
|
evidenceTargets.push({
|
|
819
|
-
|
|
1036
|
+
cloudlyNodeId: evidenceArg.cloudlyNodeId as string,
|
|
1037
|
+
swarmClusterId: evidenceArg.swarmClusterId as string,
|
|
1038
|
+
swarmNodeId: evidenceArg.swarmNodeId as string,
|
|
820
1039
|
nodeName: evidenceArg.nodeName as string,
|
|
821
1040
|
platform: evidenceArg.platform as TImmutableContainerPlatform,
|
|
822
1041
|
});
|
|
@@ -839,8 +1058,9 @@ export const validateCoreflowSecretRuntimeRegistration = async (
|
|
|
839
1058
|
}
|
|
840
1059
|
const expectedArtifacts = new Map<
|
|
841
1060
|
TImmutableContainerPlatform,
|
|
842
|
-
|
|
843
|
-
>((expectationArg.
|
|
1061
|
+
IWorkloadInitApprovedArtifactV1
|
|
1062
|
+
>(((expectationArg.workloadInitAuthority as unknown as IWorkloadInitActiveApprovalAuthorityV1)
|
|
1063
|
+
.approval.artifacts)
|
|
844
1064
|
.map((artifactArg) => [artifactArg.platform, artifactArg]));
|
|
845
1065
|
for (const [index, artifactArg] of evidenceArtifacts.entries()) {
|
|
846
1066
|
const expectedArtifact = expectedArtifacts.get(artifactArg.platform);
|
|
@@ -853,21 +1073,26 @@ export const validateCoreflowSecretRuntimeRegistration = async (
|
|
|
853
1073
|
}
|
|
854
1074
|
}
|
|
855
1075
|
}
|
|
856
|
-
if (JSON.stringify(registrationArg.targets) !== JSON.stringify(
|
|
1076
|
+
if (JSON.stringify(registrationArg.targets) !== JSON.stringify(targetAuthority.targets)) {
|
|
857
1077
|
errors.push('secret runtime registration must exactly cover authoritative target nodes');
|
|
858
1078
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
expectationArg.targets as ICoreflowSecretRuntimeNodeTargetV1[],
|
|
862
|
-
);
|
|
863
|
-
if (registrationArg.nodeSetDigest !== expectedNodeSetDigest) {
|
|
864
|
-
errors.push('secret runtime nodeSetDigest does not match authoritative target nodes');
|
|
865
|
-
}
|
|
866
|
-
}
|
|
1079
|
+
const workloadInitAuthority = expectationArg.workloadInitAuthority as
|
|
1080
|
+
IWorkloadInitActiveApprovalAuthorityV1;
|
|
867
1081
|
if (registrationArg.workloadInitImageIndexDigest
|
|
868
|
-
!==
|
|
1082
|
+
!== workloadInitAuthority.approval.workloadInitImageIndexDigest) {
|
|
869
1083
|
errors.push('secret runtime WorkloadInit image index is not approved');
|
|
870
1084
|
}
|
|
1085
|
+
if (isRuntimeRecord(registrationArg.workloadInitAuthority)
|
|
1086
|
+
&& (
|
|
1087
|
+
registrationArg.workloadInitAuthority.authorityVersion
|
|
1088
|
+
!== workloadInitAuthority.authorityVersion
|
|
1089
|
+
|| registrationArg.workloadInitAuthority.authorityGeneration
|
|
1090
|
+
!== workloadInitAuthority.authorityGeneration
|
|
1091
|
+
|| registrationArg.workloadInitAuthority.approvalDigest
|
|
1092
|
+
!== workloadInitAuthority.approvalDigest
|
|
1093
|
+
)) {
|
|
1094
|
+
errors.push('secret runtime WorkloadInit authority does not match');
|
|
1095
|
+
}
|
|
871
1096
|
if (registrationRecipientErrors.length === 0 && expectedRecipientErrors.length === 0
|
|
872
1097
|
&& activeRecipientFingerprint(
|
|
873
1098
|
registrationArg.activeRecipient as unknown as IActiveSecretRecipientMetadata,
|