@serve.zone/interfaces 23.1.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 +17 -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 +61 -25
- package/dist_ts/runtime.js +256 -93
- 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 +48 -13
- 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 +399 -137
- 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,52 +622,96 @@ export const validateSecretRecipientEnrollmentState = (
|
|
|
481
622
|
}
|
|
482
623
|
};
|
|
483
624
|
|
|
484
|
-
export interface
|
|
485
|
-
|
|
486
|
-
nodeName: string;
|
|
487
|
-
platform: TImmutableContainerPlatform;
|
|
488
|
-
}
|
|
625
|
+
export interface ICoreflowSecretRuntimeNodeTargetV2
|
|
626
|
+
extends IClusterRuntimeTargetV1 {}
|
|
489
627
|
|
|
490
|
-
export interface
|
|
491
|
-
extends
|
|
628
|
+
export interface ICoreflowSecretRuntimeNodeEvidenceV2
|
|
629
|
+
extends ICoreflowSecretRuntimeNodeTargetV2 {
|
|
492
630
|
workloadInitPlatformManifestDigest: TSha256Digest;
|
|
493
631
|
workloadInitExecutableDigest: TSha256Digest;
|
|
494
632
|
}
|
|
495
633
|
|
|
496
|
-
export interface
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
workloadInitExecutableDigest: TSha256Digest;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
export interface ICoreflowSecretRuntimeRegistrationExpectationV1 {
|
|
503
|
-
expectationVersion: 1;
|
|
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
|
-
|
|
512
|
-
|
|
643
|
+
/** Dedicated TypedSocket tag carrying ICoreflowSecretRuntimeRegistrationV2. */
|
|
644
|
+
export const coreflowSecretRuntimeRegistrationTagId =
|
|
645
|
+
'coreflowSecretRuntimeRegistration' as const;
|
|
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
|
+
|
|
661
|
+
/** Cluster scope is derived exclusively from the verified cluster JWT. */
|
|
662
|
+
export interface IReq_GetCoreflowSecretRuntimeRegistrationExpectation
|
|
663
|
+
extends plugins.typedrequestInterfaces.implementsTR<
|
|
664
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
665
|
+
IReq_GetCoreflowSecretRuntimeRegistrationExpectation
|
|
666
|
+
> {
|
|
667
|
+
method: 'getCoreflowSecretRuntimeRegistrationExpectation';
|
|
668
|
+
request: {
|
|
669
|
+
identity: IIdentityCredential;
|
|
670
|
+
};
|
|
671
|
+
response: TCoreflowSecretRuntimeRegistrationExpectationResponseV2;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
export const validateGetCoreflowSecretRuntimeRegistrationExpectationRequest = (
|
|
675
|
+
requestArg: unknown,
|
|
676
|
+
): string[] => {
|
|
677
|
+
try {
|
|
678
|
+
if (!isRuntimeRecord(requestArg)
|
|
679
|
+
|| !hasExactRuntimeKeys(requestArg, ['identity'])
|
|
680
|
+
|| !isRuntimeRecord(requestArg.identity)
|
|
681
|
+
|| !hasExactRuntimeKeys(requestArg.identity, ['jwt'])
|
|
682
|
+
|| typeof requestArg.identity.jwt !== 'string'
|
|
683
|
+
|| requestArg.identity.jwt.length === 0) {
|
|
684
|
+
return ['secret runtime registration expectation request must contain only a JWT identity'];
|
|
685
|
+
}
|
|
686
|
+
return [];
|
|
687
|
+
} catch {
|
|
688
|
+
return ['secret runtime registration expectation request must be safely inspectable'];
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
export interface ICoreflowSecretRuntimeRegistrationV2 {
|
|
693
|
+
registrationVersion: 2;
|
|
694
|
+
expectationDigest: TSha256Digest;
|
|
513
695
|
reporterSessionId: string;
|
|
514
696
|
registeredAt: number;
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
697
|
+
targetGeneration: number;
|
|
698
|
+
targetSetDigest: TSha256Digest;
|
|
699
|
+
targets: ICoreflowSecretRuntimeNodeTargetV2[];
|
|
700
|
+
nodeEvidence: ICoreflowSecretRuntimeNodeEvidenceV2[];
|
|
518
701
|
capabilities: ICoreflowRuntimeCapabilities;
|
|
519
702
|
activeRecipient: IActiveSecretRecipientMetadata;
|
|
703
|
+
workloadInitAuthority: IWorkloadInitApprovalAuthorityReferenceV1;
|
|
520
704
|
workloadInitImageIndexDigest: TSha256Digest;
|
|
521
|
-
workloadInitApprovedAt: number;
|
|
522
705
|
}
|
|
523
706
|
|
|
524
707
|
const compareRuntimeTargets = (
|
|
525
|
-
leftArg:
|
|
526
|
-
rightArg:
|
|
708
|
+
leftArg: ICoreflowSecretRuntimeNodeTargetV2,
|
|
709
|
+
rightArg: ICoreflowSecretRuntimeNodeTargetV2,
|
|
527
710
|
): number => {
|
|
528
711
|
for (const [left, right] of [
|
|
529
|
-
[leftArg.
|
|
712
|
+
[leftArg.cloudlyNodeId, rightArg.cloudlyNodeId],
|
|
713
|
+
[leftArg.swarmClusterId, rightArg.swarmClusterId],
|
|
714
|
+
[leftArg.swarmNodeId, rightArg.swarmNodeId],
|
|
530
715
|
[leftArg.nodeName, rightArg.nodeName],
|
|
531
716
|
[leftArg.platform, rightArg.platform],
|
|
532
717
|
]) {
|
|
@@ -537,24 +722,50 @@ const compareRuntimeTargets = (
|
|
|
537
722
|
};
|
|
538
723
|
|
|
539
724
|
const canonicalRuntimeTarget = (
|
|
540
|
-
targetArg:
|
|
541
|
-
):
|
|
542
|
-
|
|
725
|
+
targetArg: ICoreflowSecretRuntimeNodeTargetV2,
|
|
726
|
+
): ICoreflowSecretRuntimeNodeTargetV2 => ({
|
|
727
|
+
cloudlyNodeId: targetArg.cloudlyNodeId,
|
|
728
|
+
swarmClusterId: targetArg.swarmClusterId,
|
|
729
|
+
swarmNodeId: targetArg.swarmNodeId,
|
|
543
730
|
nodeName: targetArg.nodeName,
|
|
544
731
|
platform: targetArg.platform,
|
|
545
732
|
});
|
|
546
733
|
|
|
547
|
-
|
|
548
|
-
|
|
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,
|
|
549
747
|
): string => JSON.stringify({
|
|
550
|
-
schemaVersion:
|
|
551
|
-
|
|
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),
|
|
552
763
|
});
|
|
553
764
|
|
|
554
|
-
export const
|
|
555
|
-
|
|
765
|
+
export const computeCoreflowSecretRuntimeRegistrationExpectationDigest = async (
|
|
766
|
+
expectationArg: ICoreflowSecretRuntimeRegistrationExpectationV2,
|
|
556
767
|
): Promise<TSha256Digest> => computeRuntimeSha256(
|
|
557
|
-
|
|
768
|
+
createCoreflowSecretRuntimeRegistrationExpectationDigestInput(expectationArg),
|
|
558
769
|
);
|
|
559
770
|
|
|
560
771
|
const validateRuntimeTargets = (
|
|
@@ -567,178 +778,219 @@ const validateRuntimeTargets = (
|
|
|
567
778
|
return [`${pathArg} must be a non-empty bounded array`];
|
|
568
779
|
}
|
|
569
780
|
const errors: string[] = [];
|
|
570
|
-
let previous:
|
|
571
|
-
const
|
|
781
|
+
let previous: ICoreflowSecretRuntimeNodeTargetV2 | undefined;
|
|
782
|
+
const cloudlyNodeIds = new Set<string>();
|
|
783
|
+
const swarmNodeScopes = new Set<string>();
|
|
572
784
|
const nodeNames = new Set<string>();
|
|
573
785
|
for (const [index, targetArg] of targetsArg.entries()) {
|
|
574
786
|
if (!isRuntimeRecord(targetArg)
|
|
575
|
-
|| !hasExactRuntimeKeys(targetArg, [
|
|
787
|
+
|| !hasExactRuntimeKeys(targetArg, [
|
|
788
|
+
'cloudlyNodeId',
|
|
789
|
+
'swarmClusterId',
|
|
790
|
+
'swarmNodeId',
|
|
791
|
+
'nodeName',
|
|
792
|
+
'platform',
|
|
793
|
+
])) {
|
|
576
794
|
errors.push(`${pathArg}[${index}] must use its exact schema`);
|
|
577
795
|
continue;
|
|
578
796
|
}
|
|
579
|
-
if (!isRuntimeIdentifier(targetArg.
|
|
797
|
+
if (!isRuntimeIdentifier(targetArg.cloudlyNodeId)
|
|
798
|
+
|| !isRuntimeIdentifier(targetArg.swarmClusterId)
|
|
799
|
+
|| !isRuntimeIdentifier(targetArg.swarmNodeId)
|
|
800
|
+
|| !isRuntimeIdentifier(targetArg.nodeName)) {
|
|
580
801
|
errors.push(`${pathArg}[${index}] node identity must be canonical`);
|
|
581
802
|
}
|
|
582
803
|
if (!runtimePlatforms.has(targetArg.platform as TImmutableContainerPlatform)) {
|
|
583
804
|
errors.push(`${pathArg}[${index}].platform must be supported`);
|
|
584
805
|
}
|
|
585
|
-
if (isRuntimeIdentifier(targetArg.
|
|
806
|
+
if (isRuntimeIdentifier(targetArg.cloudlyNodeId)
|
|
807
|
+
&& isRuntimeIdentifier(targetArg.swarmClusterId)
|
|
808
|
+
&& isRuntimeIdentifier(targetArg.swarmNodeId)
|
|
586
809
|
&& isRuntimeIdentifier(targetArg.nodeName)
|
|
587
810
|
&& runtimePlatforms.has(targetArg.platform as TImmutableContainerPlatform)) {
|
|
588
|
-
const target = targetArg as unknown as
|
|
811
|
+
const target = targetArg as unknown as ICoreflowSecretRuntimeNodeTargetV2;
|
|
589
812
|
if (previous && compareRuntimeTargets(previous, target) >= 0) {
|
|
590
|
-
errors.push(`${pathArg} must be uniquely sorted by
|
|
813
|
+
errors.push(`${pathArg} must be uniquely sorted by runtime identity`);
|
|
591
814
|
}
|
|
592
815
|
previous = target;
|
|
593
|
-
|
|
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
|
+
}
|
|
594
823
|
if (nodeNames.has(target.nodeName)) errors.push(`${pathArg} nodeNames must be unique`);
|
|
595
|
-
|
|
824
|
+
cloudlyNodeIds.add(target.cloudlyNodeId);
|
|
825
|
+
swarmNodeScopes.add(swarmNodeScope);
|
|
596
826
|
nodeNames.add(target.nodeName);
|
|
597
827
|
}
|
|
598
828
|
}
|
|
599
829
|
return errors;
|
|
600
830
|
};
|
|
601
831
|
|
|
602
|
-
const validateRuntimeArtifacts = (
|
|
603
|
-
artifactsArg: unknown,
|
|
604
|
-
pathArg: string,
|
|
605
|
-
): string[] => {
|
|
606
|
-
if (!Array.isArray(artifactsArg)
|
|
607
|
-
|| artifactsArg.length === 0
|
|
608
|
-
|| artifactsArg.length > runtimePlatforms.size) {
|
|
609
|
-
return [`${pathArg} must be a non-empty bounded array`];
|
|
610
|
-
}
|
|
611
|
-
const errors: string[] = [];
|
|
612
|
-
const platforms: string[] = [];
|
|
613
|
-
for (const [index, artifactArg] of artifactsArg.entries()) {
|
|
614
|
-
if (!isRuntimeRecord(artifactArg)
|
|
615
|
-
|| !hasExactRuntimeKeys(artifactArg, [
|
|
616
|
-
'platform',
|
|
617
|
-
'workloadInitPlatformManifestDigest',
|
|
618
|
-
'workloadInitExecutableDigest',
|
|
619
|
-
])) {
|
|
620
|
-
errors.push(`${pathArg}[${index}] must use its exact schema`);
|
|
621
|
-
continue;
|
|
622
|
-
}
|
|
623
|
-
platforms.push(artifactArg.platform as string);
|
|
624
|
-
if (!runtimePlatforms.has(artifactArg.platform as TImmutableContainerPlatform)) {
|
|
625
|
-
errors.push(`${pathArg}[${index}].platform must be supported`);
|
|
626
|
-
}
|
|
627
|
-
if (typeof artifactArg.workloadInitPlatformManifestDigest !== 'string'
|
|
628
|
-
|| !isSha256Digest(artifactArg.workloadInitPlatformManifestDigest)
|
|
629
|
-
|| typeof artifactArg.workloadInitExecutableDigest !== 'string'
|
|
630
|
-
|| !isSha256Digest(artifactArg.workloadInitExecutableDigest)) {
|
|
631
|
-
errors.push(`${pathArg}[${index}] digests must be canonical`);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
if (new Set(platforms).size !== platforms.length
|
|
635
|
-
|| JSON.stringify(platforms) !== JSON.stringify([...platforms].sort())) {
|
|
636
|
-
errors.push(`${pathArg} must be uniquely sorted by platform`);
|
|
637
|
-
}
|
|
638
|
-
return errors;
|
|
639
|
-
};
|
|
640
|
-
|
|
641
832
|
const activeRecipientFingerprint = (recipientArg: IActiveSecretRecipientMetadata): string => (
|
|
642
|
-
JSON.stringify(
|
|
643
|
-
schemaVersion: recipientArg.schemaVersion,
|
|
644
|
-
recipientKeyId: recipientArg.recipientKeyId,
|
|
645
|
-
publicKey: recipientArg.publicKey,
|
|
646
|
-
generation: recipientArg.generation,
|
|
647
|
-
activatedAt: recipientArg.activatedAt,
|
|
648
|
-
lifecycleState: recipientArg.lifecycleState,
|
|
649
|
-
})
|
|
833
|
+
JSON.stringify(canonicalActiveRecipient(recipientArg))
|
|
650
834
|
);
|
|
651
835
|
|
|
652
836
|
/**
|
|
653
|
-
*
|
|
654
|
-
*
|
|
837
|
+
* Validates only a Cloudly-created expectation. It does not establish the
|
|
838
|
+
* private DSSE trust decision or Spark manager consensus that produced it.
|
|
655
839
|
*/
|
|
656
|
-
export const
|
|
657
|
-
registrationArg: unknown,
|
|
840
|
+
export const validateCoreflowSecretRuntimeRegistrationExpectation = async (
|
|
658
841
|
expectationArg: unknown,
|
|
842
|
+
trustedNowArg: number,
|
|
659
843
|
): Promise<string[]> => {
|
|
660
844
|
try {
|
|
661
845
|
const errors: string[] = [];
|
|
662
846
|
if (!isRuntimeRecord(expectationArg)
|
|
663
847
|
|| !hasExactRuntimeKeys(expectationArg, [
|
|
664
848
|
'expectationVersion',
|
|
849
|
+
'expectationDigest',
|
|
665
850
|
'reporterSessionId',
|
|
666
|
-
'
|
|
667
|
-
'
|
|
668
|
-
'workloadInitArtifacts',
|
|
851
|
+
'targetAuthority',
|
|
852
|
+
'workloadInitAuthority',
|
|
669
853
|
'activeRecipient',
|
|
670
854
|
])) {
|
|
671
855
|
return ['secret runtime registration expectation must use its exact schema'];
|
|
672
856
|
}
|
|
673
|
-
if (expectationArg.expectationVersion !==
|
|
674
|
-
errors.push('secret runtime registration expectationVersion must be
|
|
857
|
+
if (expectationArg.expectationVersion !== 2) {
|
|
858
|
+
errors.push('secret runtime registration expectationVersion must be 2');
|
|
675
859
|
}
|
|
676
860
|
if (!isRuntimeIdentifier(expectationArg.reporterSessionId)) {
|
|
677
861
|
errors.push('expected secret runtime reporterSessionId must be canonical');
|
|
678
862
|
}
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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');
|
|
688
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
|
+
));
|
|
689
884
|
const expectedRecipientErrors = validateSecretRecipientMetadata(expectationArg.activeRecipient);
|
|
690
885
|
if (expectedRecipientErrors.length > 0
|
|
691
886
|
|| !isRuntimeRecord(expectationArg.activeRecipient)
|
|
692
887
|
|| expectationArg.activeRecipient.lifecycleState !== 'active') {
|
|
693
888
|
errors.push('expected secret recipient must be canonical and active');
|
|
694
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;
|
|
695
937
|
if (!isRuntimeRecord(registrationArg)
|
|
696
938
|
|| !hasExactRuntimeKeys(registrationArg, [
|
|
697
939
|
'registrationVersion',
|
|
940
|
+
'expectationDigest',
|
|
698
941
|
'reporterSessionId',
|
|
699
942
|
'registeredAt',
|
|
943
|
+
'targetGeneration',
|
|
944
|
+
'targetSetDigest',
|
|
700
945
|
'targets',
|
|
701
|
-
'nodeSetDigest',
|
|
702
946
|
'nodeEvidence',
|
|
703
947
|
'capabilities',
|
|
704
948
|
'activeRecipient',
|
|
949
|
+
'workloadInitAuthority',
|
|
705
950
|
'workloadInitImageIndexDigest',
|
|
706
|
-
'workloadInitApprovedAt',
|
|
707
951
|
])) {
|
|
708
952
|
errors.push('secret runtime registration must use its exact schema');
|
|
709
953
|
return errors;
|
|
710
954
|
}
|
|
711
|
-
if (registrationArg.registrationVersion !==
|
|
712
|
-
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');
|
|
713
960
|
}
|
|
714
961
|
if (!isRuntimeIdentifier(registrationArg.reporterSessionId)) {
|
|
715
962
|
errors.push('secret runtime reporterSessionId must be canonical');
|
|
716
963
|
} else if (registrationArg.reporterSessionId !== expectationArg.reporterSessionId) {
|
|
717
964
|
errors.push('secret runtime reporterSessionId does not match the live session');
|
|
718
965
|
}
|
|
719
|
-
if (!isPositiveSafeInteger(registrationArg.registeredAt)
|
|
720
|
-
|
|
721
|
-
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');
|
|
722
968
|
}
|
|
723
969
|
const registrationTargetErrors = validateRuntimeTargets(
|
|
724
970
|
registrationArg.targets,
|
|
725
971
|
'registration targets',
|
|
726
972
|
);
|
|
727
973
|
errors.push(...registrationTargetErrors);
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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');
|
|
731
978
|
}
|
|
732
979
|
if (typeof registrationArg.workloadInitImageIndexDigest !== 'string'
|
|
733
980
|
|| !isSha256Digest(registrationArg.workloadInitImageIndexDigest)) {
|
|
734
981
|
errors.push('secret runtime WorkloadInit image index digest must be canonical');
|
|
735
982
|
}
|
|
736
983
|
const registrationRecipientErrors = validateSecretRecipientMetadata(registrationArg.activeRecipient);
|
|
984
|
+
const expectedRecipientErrors = validateSecretRecipientMetadata(expectationArg.activeRecipient);
|
|
737
985
|
if (registrationRecipientErrors.length > 0
|
|
738
986
|
|| !isRuntimeRecord(registrationArg.activeRecipient)
|
|
739
987
|
|| registrationArg.activeRecipient.lifecycleState !== 'active') {
|
|
740
988
|
errors.push('secret runtime registration recipient must be canonical and active');
|
|
741
989
|
}
|
|
990
|
+
const registrationAuthorityErrors = validateWorkloadInitApprovalAuthorityReference(
|
|
991
|
+
registrationArg.workloadInitAuthority,
|
|
992
|
+
);
|
|
993
|
+
errors.push(...registrationAuthorityErrors);
|
|
742
994
|
if (!isRuntimeRecord(registrationArg.capabilities)
|
|
743
995
|
|| !hasOnlyRuntimeKeys(registrationArg.capabilities, [
|
|
744
996
|
'immutableImageDeploymentVersion',
|
|
@@ -764,12 +1016,14 @@ export const validateCoreflowSecretRuntimeRegistration = async (
|
|
|
764
1016
|
|| registrationArg.nodeEvidence.length > maximumRuntimeNodes) {
|
|
765
1017
|
errors.push('secret runtime node evidence must be a non-empty bounded array');
|
|
766
1018
|
} else {
|
|
767
|
-
const evidenceTargets:
|
|
768
|
-
const evidenceArtifacts:
|
|
1019
|
+
const evidenceTargets: ICoreflowSecretRuntimeNodeTargetV2[] = [];
|
|
1020
|
+
const evidenceArtifacts: IWorkloadInitApprovedArtifactV1[] = [];
|
|
769
1021
|
for (const [index, evidenceArg] of registrationArg.nodeEvidence.entries()) {
|
|
770
1022
|
if (!isRuntimeRecord(evidenceArg)
|
|
771
1023
|
|| !hasExactRuntimeKeys(evidenceArg, [
|
|
772
|
-
'
|
|
1024
|
+
'cloudlyNodeId',
|
|
1025
|
+
'swarmClusterId',
|
|
1026
|
+
'swarmNodeId',
|
|
773
1027
|
'nodeName',
|
|
774
1028
|
'platform',
|
|
775
1029
|
'workloadInitPlatformManifestDigest',
|
|
@@ -779,7 +1033,9 @@ export const validateCoreflowSecretRuntimeRegistration = async (
|
|
|
779
1033
|
continue;
|
|
780
1034
|
}
|
|
781
1035
|
evidenceTargets.push({
|
|
782
|
-
|
|
1036
|
+
cloudlyNodeId: evidenceArg.cloudlyNodeId as string,
|
|
1037
|
+
swarmClusterId: evidenceArg.swarmClusterId as string,
|
|
1038
|
+
swarmNodeId: evidenceArg.swarmNodeId as string,
|
|
783
1039
|
nodeName: evidenceArg.nodeName as string,
|
|
784
1040
|
platform: evidenceArg.platform as TImmutableContainerPlatform,
|
|
785
1041
|
});
|
|
@@ -802,8 +1058,9 @@ export const validateCoreflowSecretRuntimeRegistration = async (
|
|
|
802
1058
|
}
|
|
803
1059
|
const expectedArtifacts = new Map<
|
|
804
1060
|
TImmutableContainerPlatform,
|
|
805
|
-
|
|
806
|
-
>((expectationArg.
|
|
1061
|
+
IWorkloadInitApprovedArtifactV1
|
|
1062
|
+
>(((expectationArg.workloadInitAuthority as unknown as IWorkloadInitActiveApprovalAuthorityV1)
|
|
1063
|
+
.approval.artifacts)
|
|
807
1064
|
.map((artifactArg) => [artifactArg.platform, artifactArg]));
|
|
808
1065
|
for (const [index, artifactArg] of evidenceArtifacts.entries()) {
|
|
809
1066
|
const expectedArtifact = expectedArtifacts.get(artifactArg.platform);
|
|
@@ -816,21 +1073,26 @@ export const validateCoreflowSecretRuntimeRegistration = async (
|
|
|
816
1073
|
}
|
|
817
1074
|
}
|
|
818
1075
|
}
|
|
819
|
-
if (JSON.stringify(registrationArg.targets) !== JSON.stringify(
|
|
1076
|
+
if (JSON.stringify(registrationArg.targets) !== JSON.stringify(targetAuthority.targets)) {
|
|
820
1077
|
errors.push('secret runtime registration must exactly cover authoritative target nodes');
|
|
821
1078
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
expectationArg.targets as ICoreflowSecretRuntimeNodeTargetV1[],
|
|
825
|
-
);
|
|
826
|
-
if (registrationArg.nodeSetDigest !== expectedNodeSetDigest) {
|
|
827
|
-
errors.push('secret runtime nodeSetDigest does not match authoritative target nodes');
|
|
828
|
-
}
|
|
829
|
-
}
|
|
1079
|
+
const workloadInitAuthority = expectationArg.workloadInitAuthority as
|
|
1080
|
+
IWorkloadInitActiveApprovalAuthorityV1;
|
|
830
1081
|
if (registrationArg.workloadInitImageIndexDigest
|
|
831
|
-
!==
|
|
1082
|
+
!== workloadInitAuthority.approval.workloadInitImageIndexDigest) {
|
|
832
1083
|
errors.push('secret runtime WorkloadInit image index is not approved');
|
|
833
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
|
+
}
|
|
834
1096
|
if (registrationRecipientErrors.length === 0 && expectedRecipientErrors.length === 0
|
|
835
1097
|
&& activeRecipientFingerprint(
|
|
836
1098
|
registrationArg.activeRecipient as unknown as IActiveSecretRecipientMetadata,
|