@serve.zone/interfaces 32.8.0 → 32.10.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 +12 -0
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/data/index.d.ts +1 -0
- package/dist_ts/data/index.js +2 -1
- package/dist_ts/data/runtimestorageclaim.d.ts +44 -0
- package/dist_ts/data/runtimestorageclaim.js +129 -0
- package/dist_ts/data/secret.d.ts +10 -3
- package/dist_ts/data/secret.js +3 -1
- package/dist_ts/requests/runtimesession.d.ts +23 -0
- package/dist_ts/runtime.assignmentsecret.d.ts +151 -0
- package/dist_ts/runtime.assignmentsecret.js +427 -0
- package/dist_ts/runtime.d.ts +1 -0
- package/dist_ts/runtime.js +2 -1
- package/package.json +1 -1
- package/readme.md +86 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/index.ts +1 -0
- package/ts/data/runtimestorageclaim.ts +162 -0
- package/ts/data/secret.ts +16 -2
- package/ts/requests/runtimesession.ts +35 -0
- package/ts/runtime.assignmentsecret.ts +581 -0
- package/ts/runtime.ts +1 -0
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type {
|
|
3
|
+
IActiveSecretRecipientMetadata, IResolvedSecretManifest, IResolvedSecretManifestReference,
|
|
4
|
+
IRuntimeAssignment, IRuntimeAssignmentReference, IRuntimeAuthorityReference, IRuntimeConfig,
|
|
5
|
+
IRuntimeSessionBinding, TImmutableContainerPlatform, TSecretRecipientMetadata, TSha256Digest,
|
|
6
|
+
} from './data/index.js';
|
|
7
|
+
import {
|
|
8
|
+
bindRuntimeConfigToAssignment, getRuntimeAssignmentReference, isSha256Digest,
|
|
9
|
+
snapshotRuntimeAssignment, snapshotRuntimeSessionBinding, validateRuntimeAssignment,
|
|
10
|
+
validateResolvedSecretManifest, validateResolvedSecretManifestReference,
|
|
11
|
+
validateSecretRecipientMetadata, validateSecretRecipientSet, verifyResolvedSecretManifestDigest,
|
|
12
|
+
verifySecretEnvelopeContext,
|
|
13
|
+
} from './data/index.js';
|
|
14
|
+
import { secretValueLimits } from './data/secret.js';
|
|
15
|
+
import {
|
|
16
|
+
validateWorkloadInitActiveApprovalAuthority, validateWorkloadInitApprovalAuthorityReference,
|
|
17
|
+
type IWorkloadInitActiveApprovalAuthority, type IWorkloadInitApprovalAuthorityReference,
|
|
18
|
+
type IWorkloadInitApprovedArtifact,
|
|
19
|
+
} from './runtime.workloadinit.js';
|
|
20
|
+
import { canonicalizeStrictJson, createCanonicalJsonSha256Hex } from './private/canonicaljson.js';
|
|
21
|
+
import { isClusterNodeId } from './private/nodeidentifier.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Content contracts only. Authentication of the physical peer, current session,
|
|
25
|
+
* node credential and slot, recipient-generation CAS, accepted manifest/version
|
|
26
|
+
* retention, challenge consumption and material issuance are Cloudly-owned.
|
|
27
|
+
*/
|
|
28
|
+
export const runtimeAssignmentSecretContract = Object.freeze({
|
|
29
|
+
descriptorDomain: 'serve.zone/runtime-assignment-secret-authority',
|
|
30
|
+
maximumRequestBytes: 327_680,
|
|
31
|
+
maximumMaterialBytes: 192 * 1024 * 1024,
|
|
32
|
+
error: 'Runtime assignment secret contract rejected.',
|
|
33
|
+
} as const);
|
|
34
|
+
|
|
35
|
+
const fail = (): never => { throw new Error(runtimeAssignmentSecretContract.error); };
|
|
36
|
+
function requireValue(conditionArg: unknown): asserts conditionArg {
|
|
37
|
+
if (!conditionArg) fail();
|
|
38
|
+
}
|
|
39
|
+
const exact = (valueArg: unknown, keysArg: readonly string[]): Record<string, unknown> => {
|
|
40
|
+
requireValue(valueArg !== null && typeof valueArg === 'object' && !Array.isArray(valueArg));
|
|
41
|
+
const keys = Object.keys(valueArg);
|
|
42
|
+
requireValue(keys.length === keysArg.length && keysArg.every((keyArg) => keys.includes(keyArg)));
|
|
43
|
+
return valueArg as Record<string, unknown>;
|
|
44
|
+
};
|
|
45
|
+
const positive = (valueArg: unknown): number => {
|
|
46
|
+
requireValue(Number.isSafeInteger(valueArg) && (valueArg as number) > 0);
|
|
47
|
+
return valueArg as number;
|
|
48
|
+
};
|
|
49
|
+
const nonnegative = (valueArg: unknown): number => {
|
|
50
|
+
requireValue(Number.isSafeInteger(valueArg) && (valueArg as number) >= 0);
|
|
51
|
+
return valueArg as number;
|
|
52
|
+
};
|
|
53
|
+
const identifier = (valueArg: unknown): string => {
|
|
54
|
+
requireValue(typeof valueArg === 'string' && /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/.test(valueArg));
|
|
55
|
+
return valueArg;
|
|
56
|
+
};
|
|
57
|
+
const digest = (valueArg: unknown): TSha256Digest => {
|
|
58
|
+
requireValue(typeof valueArg === 'string' && isSha256Digest(valueArg));
|
|
59
|
+
return valueArg;
|
|
60
|
+
};
|
|
61
|
+
const canonical = (valueArg: unknown): string => canonicalizeStrictJson(valueArg, fail);
|
|
62
|
+
const capture = (valueArg: unknown, maximumArg: number = runtimeAssignmentSecretContract.maximumRequestBytes): unknown => {
|
|
63
|
+
const serialized = canonical(valueArg);
|
|
64
|
+
requireValue(new TextEncoder().encode(serialized).byteLength <= maximumArg);
|
|
65
|
+
return JSON.parse(serialized) as unknown;
|
|
66
|
+
};
|
|
67
|
+
const same = (leftArg: unknown, rightArg: unknown): boolean => canonical(leftArg) === canonical(rightArg);
|
|
68
|
+
const snapshot = <T>(callbackArg: () => T): T => {
|
|
69
|
+
try { return callbackArg(); } catch { return fail(); }
|
|
70
|
+
};
|
|
71
|
+
const session = (valueArg: unknown): IRuntimeSessionBinding =>
|
|
72
|
+
snapshotRuntimeSessionBinding(valueArg);
|
|
73
|
+
const assignmentReference = (valueArg: unknown): IRuntimeAssignmentReference => {
|
|
74
|
+
const value = exact(valueArg, ['assignmentId', 'generation', 'digest']);
|
|
75
|
+
return { assignmentId: identifier(value.assignmentId), generation: positive(value.generation), digest: digest(value.digest) };
|
|
76
|
+
};
|
|
77
|
+
const authorityReference = (valueArg: unknown): IRuntimeAuthorityReference => {
|
|
78
|
+
const value = exact(valueArg, ['id', 'generation', 'digest']);
|
|
79
|
+
return { id: identifier(value.id), generation: positive(value.generation), digest: digest(value.digest) };
|
|
80
|
+
};
|
|
81
|
+
const manifestReference = (manifestArg: IResolvedSecretManifest): IResolvedSecretManifestReference => ({
|
|
82
|
+
manifestId: manifestArg.id, manifestDigest: manifestArg.digest, clusterId: manifestArg.clusterId,
|
|
83
|
+
imageRolloutId: manifestArg.imageRolloutId, imageRolloutGeneration: manifestArg.imageRolloutGeneration,
|
|
84
|
+
requestedImageDigest: manifestArg.requestedImageDigest, invocationDigest: manifestArg.invocationDigest,
|
|
85
|
+
secretRolloutId: manifestArg.secretRolloutId, secretRolloutGeneration: manifestArg.secretRolloutGeneration,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export interface IRuntimeAssignmentSecretLauncher {
|
|
89
|
+
approval: IWorkloadInitApprovalAuthorityReference;
|
|
90
|
+
artifact: IWorkloadInitApprovedArtifact;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Value-free descriptor hashed BEFORE the assignment is sealed. Its manifest is
|
|
95
|
+
* already independently hashed; neither digest contains the assignment digest.
|
|
96
|
+
*/
|
|
97
|
+
export interface IRuntimeAssignmentSecretAuthority extends IRuntimeAuthorityReference {
|
|
98
|
+
organizationId: string;
|
|
99
|
+
serviceId: string;
|
|
100
|
+
platform: TImmutableContainerPlatform;
|
|
101
|
+
manifest: IResolvedSecretManifestReference;
|
|
102
|
+
workloadInit: IRuntimeAssignmentSecretLauncher | null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const launcher = (valueArg: unknown, platformArg: TImmutableContainerPlatform): IRuntimeAssignmentSecretLauncher => {
|
|
106
|
+
const value = exact(valueArg, ['approval', 'artifact']);
|
|
107
|
+
requireValue(validateWorkloadInitApprovalAuthorityReference(value.approval).length === 0);
|
|
108
|
+
const artifact = exact(value.artifact, [
|
|
109
|
+
'platform', 'workloadInitPlatformManifestDigest', 'workloadInitExecutableDigest',
|
|
110
|
+
]);
|
|
111
|
+
requireValue(artifact.platform === platformArg);
|
|
112
|
+
digest(artifact.workloadInitPlatformManifestDigest);
|
|
113
|
+
digest(artifact.workloadInitExecutableDigest);
|
|
114
|
+
return valueArg as IRuntimeAssignmentSecretLauncher;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const snapshotRuntimeAssignmentSecretAuthority = (valueArg: unknown): IRuntimeAssignmentSecretAuthority =>
|
|
118
|
+
snapshot(() => {
|
|
119
|
+
const value = exact(capture(valueArg), [
|
|
120
|
+
'id', 'generation', 'digest', 'organizationId', 'serviceId', 'platform', 'manifest', 'workloadInit',
|
|
121
|
+
]);
|
|
122
|
+
const reference = authorityReference({
|
|
123
|
+
id: value.id, generation: value.generation, digest: value.digest,
|
|
124
|
+
});
|
|
125
|
+
requireValue(value.platform === 'linux/amd64' || value.platform === 'linux/arm64');
|
|
126
|
+
const manifest = value.manifest;
|
|
127
|
+
requireValue(validateResolvedSecretManifestReference(manifest, (manifest as IResolvedSecretManifestReference)?.clusterId).length === 0);
|
|
128
|
+
requireValue(reference.id === (manifest as IResolvedSecretManifestReference).manifestId
|
|
129
|
+
&& reference.generation === (manifest as IResolvedSecretManifestReference).secretRolloutGeneration);
|
|
130
|
+
if (value.workloadInit !== null) launcher(value.workloadInit, value.platform);
|
|
131
|
+
return value as unknown as IRuntimeAssignmentSecretAuthority;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
export const computeRuntimeAssignmentSecretAuthorityDigest = async (
|
|
135
|
+
authorityArg: IRuntimeAssignmentSecretAuthority,
|
|
136
|
+
): Promise<TSha256Digest> => {
|
|
137
|
+
const authority = snapshotRuntimeAssignmentSecretAuthority(authorityArg);
|
|
138
|
+
const { digest: _digest, ...payload } = authority;
|
|
139
|
+
return `sha256:${await createCanonicalJsonSha256Hex(canonical({
|
|
140
|
+
domain: runtimeAssignmentSecretContract.descriptorDomain, payload,
|
|
141
|
+
}), fail)}`;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const validateRuntimeAssignmentSecretAuthority = async (valueArg: unknown): Promise<string[]> => {
|
|
145
|
+
try {
|
|
146
|
+
const value = snapshotRuntimeAssignmentSecretAuthority(valueArg);
|
|
147
|
+
return value.digest === await computeRuntimeAssignmentSecretAuthorityDigest(value)
|
|
148
|
+
? [] : [runtimeAssignmentSecretContract.error];
|
|
149
|
+
} catch { return [runtimeAssignmentSecretContract.error]; }
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/** Producer creates this before assignment digest computation. */
|
|
153
|
+
export const createRuntimeAssignmentSecretAuthority = async (
|
|
154
|
+
manifestArg: unknown, platformArg: TImmutableContainerPlatform,
|
|
155
|
+
workloadInitArg: IRuntimeAssignmentSecretLauncher | null,
|
|
156
|
+
): Promise<IRuntimeAssignmentSecretAuthority> => {
|
|
157
|
+
try {
|
|
158
|
+
const manifest = capture(manifestArg) as IResolvedSecretManifest;
|
|
159
|
+
requireValue(validateResolvedSecretManifest(manifest).length === 0
|
|
160
|
+
&& await verifyResolvedSecretManifestDigest(manifest)
|
|
161
|
+
&& (platformArg === 'linux/amd64' || platformArg === 'linux/arm64')
|
|
162
|
+
&& (manifest.entries.some((entryArg) => entryArg.delivery.type === 'launcher-environment')
|
|
163
|
+
? workloadInitArg !== null : workloadInitArg === null));
|
|
164
|
+
const workloadInit = workloadInitArg === null
|
|
165
|
+
? null : launcher(capture(workloadInitArg), platformArg);
|
|
166
|
+
const authority: IRuntimeAssignmentSecretAuthority = {
|
|
167
|
+
id: manifest.id, generation: manifest.secretRolloutGeneration,
|
|
168
|
+
digest: 'sha256:0000000000000000000000000000000000000000000000000000000000000000',
|
|
169
|
+
organizationId: manifest.organizationId, serviceId: manifest.serviceId,
|
|
170
|
+
platform: platformArg, manifest: manifestReference(manifest), workloadInit,
|
|
171
|
+
};
|
|
172
|
+
authority.digest = await computeRuntimeAssignmentSecretAuthorityDigest(authority);
|
|
173
|
+
return authority;
|
|
174
|
+
} catch { return fail(); }
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/** Trusted manifest/config are required: the reference alone is no authorization. */
|
|
178
|
+
export const bindRuntimeAssignmentSecretAuthority = async (
|
|
179
|
+
authorityArg: unknown, manifestArg: unknown, assignmentArg: unknown, configArg: unknown,
|
|
180
|
+
trustedClusterIdArg: string,
|
|
181
|
+
): Promise<IRuntimeAssignmentSecretAuthority> => {
|
|
182
|
+
try {
|
|
183
|
+
const authority = snapshotRuntimeAssignmentSecretAuthority(authorityArg);
|
|
184
|
+
const assignment = snapshotRuntimeAssignment(assignmentArg);
|
|
185
|
+
const manifest = capture(manifestArg) as IResolvedSecretManifest;
|
|
186
|
+
requireValue((await validateRuntimeAssignmentSecretAuthority(authority)).length === 0
|
|
187
|
+
&& (await validateRuntimeAssignment(assignment)).length === 0
|
|
188
|
+
&& validateResolvedSecretManifest(manifest).length === 0
|
|
189
|
+
&& await verifyResolvedSecretManifestDigest(manifest));
|
|
190
|
+
const { config } = await bindRuntimeConfigToAssignment(configArg, assignment);
|
|
191
|
+
requireValue(assignment.disposition === 'run' && assignment.generation === 1
|
|
192
|
+
&& assignment.previous === null && assignment.workload.secrets !== null
|
|
193
|
+
&& same(assignment.workload.secrets, {
|
|
194
|
+
id: authority.id, generation: authority.generation, digest: authority.digest,
|
|
195
|
+
})
|
|
196
|
+
&& same(authority.manifest, manifestReference(manifest))
|
|
197
|
+
&& authority.organizationId === assignment.organizationId
|
|
198
|
+
&& authority.serviceId === assignment.serviceId
|
|
199
|
+
&& authority.platform === assignment.workload.platform
|
|
200
|
+
&& identifier(trustedClusterIdArg) === manifest.clusterId
|
|
201
|
+
&& manifest.organizationId === assignment.organizationId
|
|
202
|
+
&& manifest.serviceId === assignment.serviceId
|
|
203
|
+
&& manifest.requestedImageDigest === assignment.workload.imageDigest
|
|
204
|
+
&& manifest.invocationDigest === config.image.invocation.digest
|
|
205
|
+
&& (manifest.entries.some((entryArg) => entryArg.delivery.type === 'launcher-environment')
|
|
206
|
+
? authority.workloadInit !== null : authority.workloadInit === null));
|
|
207
|
+
return authority;
|
|
208
|
+
} catch { return fail(); }
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export interface IRuntimeSecretRecipientState {
|
|
212
|
+
nodeId: string;
|
|
213
|
+
generation: number;
|
|
214
|
+
recipients: TSecretRecipientMetadata[];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface IGetRuntimeSecretRecipientStateRequest { session: IRuntimeSessionBinding }
|
|
218
|
+
export interface IGetRuntimeSecretRecipientStateResponse { state: IRuntimeSecretRecipientState }
|
|
219
|
+
export interface IBeginRuntimeSecretRecipientEnrollmentRequest {
|
|
220
|
+
session: IRuntimeSessionBinding;
|
|
221
|
+
proposedRecipientKeyId: string;
|
|
222
|
+
proposedPublicKey: string;
|
|
223
|
+
expectedGeneration: number;
|
|
224
|
+
}
|
|
225
|
+
export interface IRuntimeSecretRecipientEnrollmentChallenge {
|
|
226
|
+
session: IRuntimeSessionBinding;
|
|
227
|
+
enrollmentId: string;
|
|
228
|
+
recipientKeyId: string;
|
|
229
|
+
publicKey: string;
|
|
230
|
+
generation: number;
|
|
231
|
+
expiresAt: number;
|
|
232
|
+
envelope: plugins.smartcrypto.IX25519EnvelopeV1;
|
|
233
|
+
}
|
|
234
|
+
export interface IBeginRuntimeSecretRecipientEnrollmentResponse {
|
|
235
|
+
challenge: IRuntimeSecretRecipientEnrollmentChallenge;
|
|
236
|
+
}
|
|
237
|
+
export interface ICompleteRuntimeSecretRecipientEnrollmentRequest {
|
|
238
|
+
session: IRuntimeSessionBinding;
|
|
239
|
+
enrollmentId: string;
|
|
240
|
+
expectedGeneration: number;
|
|
241
|
+
/** Canonical unpadded base64url encoding of the opened 32-byte challenge. */
|
|
242
|
+
response: string;
|
|
243
|
+
}
|
|
244
|
+
export interface ICompleteRuntimeSecretRecipientEnrollmentResponse {
|
|
245
|
+
recipient: IActiveSecretRecipientMetadata;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const raw32ByteBase64Url = /^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/;
|
|
249
|
+
export const snapshotRuntimeSecretRecipientState = (valueArg: unknown): IRuntimeSecretRecipientState =>
|
|
250
|
+
snapshot(() => {
|
|
251
|
+
const state = exact(capture(valueArg), ['nodeId', 'generation', 'recipients']);
|
|
252
|
+
requireValue(isClusterNodeId(state.nodeId));
|
|
253
|
+
const generation = nonnegative(state.generation);
|
|
254
|
+
requireValue(Array.isArray(state.recipients));
|
|
255
|
+
requireValue(generation === 0
|
|
256
|
+
? state.recipients.length === 0
|
|
257
|
+
: validateSecretRecipientSet(state.recipients).length === 0
|
|
258
|
+
&& state.recipients.some((recipientArg: TSecretRecipientMetadata) =>
|
|
259
|
+
recipientArg.lifecycleState === 'active' && recipientArg.generation === generation));
|
|
260
|
+
return state as unknown as IRuntimeSecretRecipientState;
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
export const bindGetRuntimeSecretRecipientStateRequest = (
|
|
264
|
+
valueArg: unknown, trustedSessionArg: unknown,
|
|
265
|
+
): IGetRuntimeSecretRecipientStateRequest => snapshot(() => {
|
|
266
|
+
const value = exact(capture(valueArg), ['session']);
|
|
267
|
+
const trusted = session(trustedSessionArg);
|
|
268
|
+
requireValue(same(session(value.session), trusted));
|
|
269
|
+
return { session: trusted };
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
export const bindGetRuntimeSecretRecipientStateResponse = (
|
|
273
|
+
valueArg: unknown, requestArg: unknown,
|
|
274
|
+
): IGetRuntimeSecretRecipientStateResponse => snapshot(() => {
|
|
275
|
+
const request = exact(capture(requestArg), ['session']);
|
|
276
|
+
const value = exact(capture(valueArg), ['state']);
|
|
277
|
+
const state = snapshotRuntimeSecretRecipientState(value.state);
|
|
278
|
+
requireValue(state.nodeId === session(request.session).nodeId);
|
|
279
|
+
return { state };
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
export const bindBeginRuntimeSecretRecipientEnrollmentRequest = (
|
|
283
|
+
valueArg: unknown, trustedSessionArg: unknown, currentStateArg: unknown,
|
|
284
|
+
): IBeginRuntimeSecretRecipientEnrollmentRequest => snapshot(() => {
|
|
285
|
+
const value = exact(capture(valueArg), [
|
|
286
|
+
'session', 'proposedRecipientKeyId', 'proposedPublicKey', 'expectedGeneration',
|
|
287
|
+
]);
|
|
288
|
+
const trusted = session(trustedSessionArg);
|
|
289
|
+
const state = snapshotRuntimeSecretRecipientState(currentStateArg);
|
|
290
|
+
requireValue(same(session(value.session), trusted) && state.nodeId === trusted.nodeId
|
|
291
|
+
&& nonnegative(value.expectedGeneration) === state.generation
|
|
292
|
+
&& raw32ByteBase64Url.test(value.proposedPublicKey as string));
|
|
293
|
+
return {
|
|
294
|
+
session: trusted, proposedRecipientKeyId: identifier(value.proposedRecipientKeyId),
|
|
295
|
+
proposedPublicKey: value.proposedPublicKey as string, expectedGeneration: state.generation,
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
/** Use the existing SmartCrypto X25519 envelope and one-time challenge proof. */
|
|
300
|
+
export const createRuntimeSecretRecipientEnrollmentChallengeContext = (
|
|
301
|
+
challengeArg: Omit<IRuntimeSecretRecipientEnrollmentChallenge, 'envelope'>,
|
|
302
|
+
): Uint8Array => new TextEncoder().encode(canonical({
|
|
303
|
+
purpose: 'serve.zone/runtime-secret-recipient-enrollment-challenge',
|
|
304
|
+
session: challengeArg.session,
|
|
305
|
+
enrollmentId: challengeArg.enrollmentId,
|
|
306
|
+
recipientKeyId: challengeArg.recipientKeyId,
|
|
307
|
+
publicKey: challengeArg.publicKey,
|
|
308
|
+
generation: challengeArg.generation,
|
|
309
|
+
expiresAt: challengeArg.expiresAt,
|
|
310
|
+
}));
|
|
311
|
+
|
|
312
|
+
export const bindBeginRuntimeSecretRecipientEnrollmentResponse = async (
|
|
313
|
+
valueArg: unknown, requestArg: unknown, trustedSessionArg: unknown, nowArg: number,
|
|
314
|
+
): Promise<IBeginRuntimeSecretRecipientEnrollmentResponse> => {
|
|
315
|
+
try {
|
|
316
|
+
const value = exact(capture(valueArg), ['challenge']);
|
|
317
|
+
const challenge = exact(value.challenge, [
|
|
318
|
+
'session', 'enrollmentId', 'recipientKeyId', 'publicKey', 'generation', 'expiresAt', 'envelope',
|
|
319
|
+
]);
|
|
320
|
+
const request = exact(capture(requestArg), [
|
|
321
|
+
'session', 'proposedRecipientKeyId', 'proposedPublicKey', 'expectedGeneration',
|
|
322
|
+
]);
|
|
323
|
+
const trusted = session(trustedSessionArg);
|
|
324
|
+
requireValue(same(session(request.session), trusted) && same(session(challenge.session), trusted)
|
|
325
|
+
&& identifier(challenge.enrollmentId).length > 0
|
|
326
|
+
&& challenge.recipientKeyId === identifier(request.proposedRecipientKeyId)
|
|
327
|
+
&& challenge.publicKey === request.proposedPublicKey
|
|
328
|
+
&& typeof challenge.publicKey === 'string' && raw32ByteBase64Url.test(challenge.publicKey)
|
|
329
|
+
&& positive(challenge.generation) === nonnegative(request.expectedGeneration) + 1
|
|
330
|
+
&& positive(challenge.expiresAt) > positive(nowArg));
|
|
331
|
+
const envelope = plugins.smartcrypto.parseX25519Envelope(challenge.envelope);
|
|
332
|
+
requireValue(envelope.recipientKeyId === challenge.recipientKeyId
|
|
333
|
+
&& raw32ByteBase64Url.test(envelope.ciphertext));
|
|
334
|
+
const captured = { ...challenge, session: trusted, envelope } as IRuntimeSecretRecipientEnrollmentChallenge;
|
|
335
|
+
requireValue(await verifySecretEnvelopeContext(
|
|
336
|
+
envelope, createRuntimeSecretRecipientEnrollmentChallengeContext(captured),
|
|
337
|
+
));
|
|
338
|
+
return { challenge: captured };
|
|
339
|
+
} catch { return fail(); }
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
export const bindCompleteRuntimeSecretRecipientEnrollmentRequest = async (
|
|
343
|
+
valueArg: unknown, challengeArg: unknown, trustedSessionArg: unknown, nowArg: number,
|
|
344
|
+
): Promise<ICompleteRuntimeSecretRecipientEnrollmentRequest> => {
|
|
345
|
+
try {
|
|
346
|
+
const value = exact(capture(valueArg), ['session', 'enrollmentId', 'expectedGeneration', 'response']);
|
|
347
|
+
const challenge = exact(capture(challengeArg), [
|
|
348
|
+
'session', 'enrollmentId', 'recipientKeyId', 'publicKey', 'generation', 'expiresAt', 'envelope',
|
|
349
|
+
]);
|
|
350
|
+
const trusted = session(trustedSessionArg);
|
|
351
|
+
requireValue(same(session(value.session), trusted) && same(session(challenge.session), trusted)
|
|
352
|
+
&& identifier(value.enrollmentId) === identifier(challenge.enrollmentId)
|
|
353
|
+
&& identifier(challenge.recipientKeyId).length > 0
|
|
354
|
+
&& nonnegative(value.expectedGeneration) + 1 === positive(challenge.generation)
|
|
355
|
+
&& positive(challenge.expiresAt) > positive(nowArg)
|
|
356
|
+
&& typeof value.response === 'string' && raw32ByteBase64Url.test(value.response));
|
|
357
|
+
const envelope = plugins.smartcrypto.parseX25519Envelope(challenge.envelope);
|
|
358
|
+
requireValue(envelope.recipientKeyId === challenge.recipientKeyId
|
|
359
|
+
&& typeof challenge.publicKey === 'string' && raw32ByteBase64Url.test(challenge.publicKey)
|
|
360
|
+
&& raw32ByteBase64Url.test(envelope.ciphertext)
|
|
361
|
+
&& await verifySecretEnvelopeContext(
|
|
362
|
+
envelope,
|
|
363
|
+
createRuntimeSecretRecipientEnrollmentChallengeContext(
|
|
364
|
+
challenge as unknown as IRuntimeSecretRecipientEnrollmentChallenge),
|
|
365
|
+
));
|
|
366
|
+
return value as unknown as ICompleteRuntimeSecretRecipientEnrollmentRequest;
|
|
367
|
+
} catch { return fail(); }
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
export const bindCompleteRuntimeSecretRecipientEnrollmentResponse = (
|
|
371
|
+
valueArg: unknown, challengeArg: unknown,
|
|
372
|
+
): ICompleteRuntimeSecretRecipientEnrollmentResponse => snapshot(() => {
|
|
373
|
+
const value = exact(capture(valueArg), ['recipient']);
|
|
374
|
+
const challenge = exact(capture(challengeArg), [
|
|
375
|
+
'session', 'enrollmentId', 'recipientKeyId', 'publicKey', 'generation', 'expiresAt', 'envelope',
|
|
376
|
+
]);
|
|
377
|
+
requireValue(validateSecretRecipientMetadata(value.recipient).length === 0);
|
|
378
|
+
const recipient = value.recipient as IActiveSecretRecipientMetadata;
|
|
379
|
+
requireValue(recipient.lifecycleState === 'active' && recipient.recipientKeyId === challenge.recipientKeyId
|
|
380
|
+
&& recipient.publicKey === challenge.publicKey && recipient.generation === challenge.generation);
|
|
381
|
+
return { recipient };
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
export interface IGetRuntimeAssignmentSecretMaterialRequest {
|
|
385
|
+
session: IRuntimeSessionBinding;
|
|
386
|
+
/** Exact current generation-one run assignment, never a historical receipt. */
|
|
387
|
+
assignment: IRuntimeAssignmentReference;
|
|
388
|
+
secrets: IRuntimeAuthorityReference;
|
|
389
|
+
expectedRecipientKeyId: string;
|
|
390
|
+
expectedRecipientGeneration: number;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export interface ISealedRuntimeAssignmentSecretMaterialEntry {
|
|
394
|
+
secretVersionId: string;
|
|
395
|
+
envelope: plugins.smartcrypto.IX25519EnvelopeV1;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Sensitive ephemeral sealed material; exclude the complete body from hooks and logs. */
|
|
399
|
+
export interface ISealedRuntimeAssignmentSecretMaterial {
|
|
400
|
+
assignment: IRuntimeAssignmentReference;
|
|
401
|
+
nodeId: string;
|
|
402
|
+
secrets: IRuntimeAuthorityReference;
|
|
403
|
+
recipientKeyId: string;
|
|
404
|
+
recipientGeneration: number;
|
|
405
|
+
authority: IRuntimeAssignmentSecretAuthority;
|
|
406
|
+
manifest: IResolvedSecretManifest;
|
|
407
|
+
/** Full approved immutable launcher authority, exactly when an entry needs it. */
|
|
408
|
+
workloadInit: IWorkloadInitActiveApprovalAuthority | null;
|
|
409
|
+
/** Sorted exact one-to-one coverage of the pinned manifest's versions. */
|
|
410
|
+
entries: ISealedRuntimeAssignmentSecretMaterialEntry[];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export interface IGetRuntimeAssignmentSecretMaterialResponse {
|
|
414
|
+
material: ISealedRuntimeAssignmentSecretMaterial;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* The descriptor was hashed before assignment seal. This context binds each
|
|
419
|
+
* envelope to the final run digest, node and recipient generation afterwards.
|
|
420
|
+
* Use SmartCrypto sealX25519Envelope with these exact bytes.
|
|
421
|
+
*/
|
|
422
|
+
export const createRuntimeAssignmentSecretMaterialEnvelopeContext = (contextArg: {
|
|
423
|
+
recipientKeyId: string;
|
|
424
|
+
recipientGeneration: number;
|
|
425
|
+
organizationId: string;
|
|
426
|
+
clusterId: string;
|
|
427
|
+
serviceId: string;
|
|
428
|
+
nodeId: string;
|
|
429
|
+
assignment: IRuntimeAssignmentReference;
|
|
430
|
+
authorityDigest: TSha256Digest;
|
|
431
|
+
manifestId: string;
|
|
432
|
+
manifestDigest: TSha256Digest;
|
|
433
|
+
imageRolloutId: string;
|
|
434
|
+
imageRolloutGeneration: number;
|
|
435
|
+
requestedImageDigest: TSha256Digest;
|
|
436
|
+
invocationDigest: TSha256Digest;
|
|
437
|
+
secretRolloutId: string;
|
|
438
|
+
secretRolloutGeneration: number;
|
|
439
|
+
secretId: string;
|
|
440
|
+
secretVersionId: string;
|
|
441
|
+
}): Uint8Array => new TextEncoder().encode(canonical({
|
|
442
|
+
purpose: 'serve.zone/runtime-assignment-secret-material',
|
|
443
|
+
recipientKeyId: contextArg.recipientKeyId,
|
|
444
|
+
recipientGeneration: contextArg.recipientGeneration,
|
|
445
|
+
organizationId: contextArg.organizationId,
|
|
446
|
+
clusterId: contextArg.clusterId,
|
|
447
|
+
serviceId: contextArg.serviceId,
|
|
448
|
+
nodeId: contextArg.nodeId,
|
|
449
|
+
assignment: contextArg.assignment,
|
|
450
|
+
authorityDigest: contextArg.authorityDigest,
|
|
451
|
+
manifestId: contextArg.manifestId,
|
|
452
|
+
manifestDigest: contextArg.manifestDigest,
|
|
453
|
+
imageRolloutId: contextArg.imageRolloutId,
|
|
454
|
+
imageRolloutGeneration: contextArg.imageRolloutGeneration,
|
|
455
|
+
requestedImageDigest: contextArg.requestedImageDigest,
|
|
456
|
+
invocationDigest: contextArg.invocationDigest,
|
|
457
|
+
secretRolloutId: contextArg.secretRolloutId,
|
|
458
|
+
secretRolloutGeneration: contextArg.secretRolloutGeneration,
|
|
459
|
+
secretId: contextArg.secretId,
|
|
460
|
+
secretVersionId: contextArg.secretVersionId,
|
|
461
|
+
}));
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* trustedSessionArg MUST be the current authenticated connection's server-owned
|
|
465
|
+
* binding; assignmentArg MUST be its current durable run. Content checks here
|
|
466
|
+
* do not prove either source, recipient possession, version issuance policy or
|
|
467
|
+
* the native workload's mount ownership.
|
|
468
|
+
*/
|
|
469
|
+
export const bindGetRuntimeAssignmentSecretMaterialRequest = async (
|
|
470
|
+
valueArg: unknown, assignmentArg: unknown, trustedSessionArg: unknown,
|
|
471
|
+
activeRecipientArg: unknown,
|
|
472
|
+
): Promise<IGetRuntimeAssignmentSecretMaterialRequest> => {
|
|
473
|
+
try {
|
|
474
|
+
const value = exact(capture(valueArg), [
|
|
475
|
+
'session', 'assignment', 'secrets', 'expectedRecipientKeyId', 'expectedRecipientGeneration',
|
|
476
|
+
]);
|
|
477
|
+
const assignment = snapshotRuntimeAssignment(assignmentArg);
|
|
478
|
+
const trusted = session(trustedSessionArg);
|
|
479
|
+
const recipient = activeRecipientArg as IActiveSecretRecipientMetadata;
|
|
480
|
+
requireValue((await validateRuntimeAssignment(assignment)).length === 0
|
|
481
|
+
&& validateSecretRecipientMetadata(recipient).length === 0
|
|
482
|
+
&& recipient.lifecycleState === 'active'
|
|
483
|
+
&& same(session(value.session), trusted)
|
|
484
|
+
&& trusted.nodeId === assignment.nodeId
|
|
485
|
+
&& trusted.runtimeNamespace === assignment.runtimeNamespace
|
|
486
|
+
&& same(trusted.controller, assignment.controller)
|
|
487
|
+
&& assignment.generation === 1 && assignment.previous === null
|
|
488
|
+
&& assignment.disposition === 'run' && assignment.workload.secrets !== null
|
|
489
|
+
&& same(assignmentReference(value.assignment), getRuntimeAssignmentReference(assignment))
|
|
490
|
+
&& same(authorityReference(value.secrets), assignment.workload.secrets)
|
|
491
|
+
&& identifier(value.expectedRecipientKeyId) === recipient.recipientKeyId
|
|
492
|
+
&& positive(value.expectedRecipientGeneration) === recipient.generation);
|
|
493
|
+
return {
|
|
494
|
+
session: trusted, assignment: getRuntimeAssignmentReference(assignment),
|
|
495
|
+
secrets: assignment.workload.secrets,
|
|
496
|
+
expectedRecipientKeyId: recipient.recipientKeyId,
|
|
497
|
+
expectedRecipientGeneration: recipient.generation,
|
|
498
|
+
};
|
|
499
|
+
} catch { return fail(); }
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Verify one complete response without opening a private key. The caller must
|
|
504
|
+
* supply its authenticated current session, current assignment, pinned config,
|
|
505
|
+
* enrolled cluster and active recipient from their respective trusted owners.
|
|
506
|
+
*/
|
|
507
|
+
export const bindGetRuntimeAssignmentSecretMaterialResponse = async (
|
|
508
|
+
valueArg: unknown, requestArg: unknown, assignmentArg: unknown, configArg: unknown,
|
|
509
|
+
trustedSessionArg: unknown, trustedClusterIdArg: string, activeRecipientArg: unknown,
|
|
510
|
+
): Promise<IGetRuntimeAssignmentSecretMaterialResponse> => {
|
|
511
|
+
try {
|
|
512
|
+
const value = exact(capture(valueArg, runtimeAssignmentSecretContract.maximumMaterialBytes), ['material']);
|
|
513
|
+
const material = exact(value.material, [
|
|
514
|
+
'assignment', 'nodeId', 'secrets', 'recipientKeyId', 'recipientGeneration',
|
|
515
|
+
'authority', 'manifest', 'workloadInit', 'entries',
|
|
516
|
+
]);
|
|
517
|
+
const request = await bindGetRuntimeAssignmentSecretMaterialRequest(
|
|
518
|
+
requestArg, assignmentArg, trustedSessionArg, activeRecipientArg,
|
|
519
|
+
);
|
|
520
|
+
const assignment = snapshotRuntimeAssignment(assignmentArg);
|
|
521
|
+
const authority = await bindRuntimeAssignmentSecretAuthority(
|
|
522
|
+
material.authority, material.manifest, assignment, configArg, trustedClusterIdArg,
|
|
523
|
+
);
|
|
524
|
+
const manifest = material.manifest as IResolvedSecretManifest;
|
|
525
|
+
requireValue(identifier(trustedClusterIdArg) === manifest.clusterId
|
|
526
|
+
&& material.nodeId === assignment.nodeId
|
|
527
|
+
&& same(assignmentReference(material.assignment), request.assignment)
|
|
528
|
+
&& same(authorityReference(material.secrets), request.secrets)
|
|
529
|
+
&& material.recipientKeyId === request.expectedRecipientKeyId
|
|
530
|
+
&& material.recipientGeneration === request.expectedRecipientGeneration
|
|
531
|
+
&& Array.isArray(material.entries)
|
|
532
|
+
&& material.entries.length === manifest.entries.length);
|
|
533
|
+
if (authority.workloadInit === null) {
|
|
534
|
+
requireValue(material.workloadInit === null);
|
|
535
|
+
} else {
|
|
536
|
+
requireValue((await validateWorkloadInitActiveApprovalAuthority(material.workloadInit)).length === 0);
|
|
537
|
+
const approval = material.workloadInit as IWorkloadInitActiveApprovalAuthority;
|
|
538
|
+
requireValue(same({
|
|
539
|
+
authorityGeneration: approval.authorityGeneration, approvalDigest: approval.approvalDigest,
|
|
540
|
+
}, authority.workloadInit.approval)
|
|
541
|
+
&& approval.approval.artifacts.some((artifactArg) =>
|
|
542
|
+
same(artifactArg, authority.workloadInit!.artifact)));
|
|
543
|
+
}
|
|
544
|
+
const expectedIds = manifest.entries.map((entryArg) => entryArg.secretVersionId).sort();
|
|
545
|
+
const actualIds = material.entries.map((entryArg: unknown) =>
|
|
546
|
+
exact(entryArg, ['secretVersionId', 'envelope']).secretVersionId);
|
|
547
|
+
requireValue(actualIds.every((idArg: unknown) => typeof idArg === 'string')
|
|
548
|
+
&& same(actualIds, expectedIds));
|
|
549
|
+
const byVersion = new Map(manifest.entries.map((entryArg) => [entryArg.secretVersionId, entryArg]));
|
|
550
|
+
for (const entryValue of material.entries) {
|
|
551
|
+
const entry = exact(entryValue, ['secretVersionId', 'envelope']);
|
|
552
|
+
const manifestEntry = byVersion.get(entry.secretVersionId as string);
|
|
553
|
+
requireValue(manifestEntry);
|
|
554
|
+
const envelope = plugins.smartcrypto.parseX25519Envelope(entry.envelope);
|
|
555
|
+
requireValue(envelope.recipientKeyId === request.expectedRecipientKeyId
|
|
556
|
+
&& envelope.ciphertext.length <= Math.ceil(secretValueLimits.maximumBytes * 4 / 3));
|
|
557
|
+
const context = createRuntimeAssignmentSecretMaterialEnvelopeContext({
|
|
558
|
+
recipientKeyId: request.expectedRecipientKeyId,
|
|
559
|
+
recipientGeneration: request.expectedRecipientGeneration,
|
|
560
|
+
organizationId: manifest.organizationId,
|
|
561
|
+
clusterId: manifest.clusterId,
|
|
562
|
+
serviceId: manifest.serviceId,
|
|
563
|
+
nodeId: assignment.nodeId,
|
|
564
|
+
assignment: request.assignment,
|
|
565
|
+
authorityDigest: authority.digest,
|
|
566
|
+
manifestId: manifest.id,
|
|
567
|
+
manifestDigest: manifest.digest,
|
|
568
|
+
imageRolloutId: manifest.imageRolloutId,
|
|
569
|
+
imageRolloutGeneration: manifest.imageRolloutGeneration,
|
|
570
|
+
requestedImageDigest: manifest.requestedImageDigest,
|
|
571
|
+
invocationDigest: manifest.invocationDigest,
|
|
572
|
+
secretRolloutId: manifest.secretRolloutId,
|
|
573
|
+
secretRolloutGeneration: manifest.secretRolloutGeneration,
|
|
574
|
+
secretId: manifestEntry.secretId,
|
|
575
|
+
secretVersionId: manifestEntry.secretVersionId,
|
|
576
|
+
});
|
|
577
|
+
requireValue(await verifySecretEnvelopeContext(envelope, context));
|
|
578
|
+
}
|
|
579
|
+
return { material: material as unknown as ISealedRuntimeAssignmentSecretMaterial };
|
|
580
|
+
} catch { return fail(); }
|
|
581
|
+
};
|
package/ts/runtime.ts
CHANGED
|
@@ -45,6 +45,7 @@ export * from './runtime.networkprojection.js';
|
|
|
45
45
|
export * from './runtime.corestore.js';
|
|
46
46
|
export * from './runtime.corestore.databaseexportpackage.js';
|
|
47
47
|
export * from './runtime.corestore.relay.js';
|
|
48
|
+
export * from './runtime.assignmentsecret.js';
|
|
48
49
|
|
|
49
50
|
export interface ISealedResolvedSecretMaterialEntry {
|
|
50
51
|
secretVersionId: string;
|