@serve.zone/interfaces 25.1.1 → 25.3.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 +19 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/platform/index.d.ts +3 -1
- package/dist_ts/platform/index.js +4 -2
- package/dist_ts/platform/objectstorageretention.d.ts +63 -0
- package/dist_ts/platform/objectstorageretention.js +255 -0
- package/dist_ts/platform/types.d.ts +3 -0
- package/dist_ts/plugins.runtime.d.ts +2 -0
- package/dist_ts/plugins.runtime.js +4 -0
- package/dist_ts/requests/config.d.ts +3 -0
- package/dist_ts/runtime.cloudlylegacydeploymentsettlement.d.ts +394 -0
- package/dist_ts/runtime.cloudlylegacydeploymentsettlement.golden.d.ts +35 -0
- package/dist_ts/runtime.cloudlylegacydeploymentsettlement.golden.js +315 -0
- package/dist_ts/runtime.cloudlylegacydeploymentsettlement.js +1378 -0
- package/dist_ts/runtime.corestore.d.ts +204 -0
- package/dist_ts/runtime.corestore.js +620 -0
- package/dist_ts/runtime.d.ts +2 -0
- package/dist_ts/runtime.js +3 -1
- package/dist_ts/runtime.workloadinit.d.ts +1 -1
- package/package.json +5 -1
- package/readme.md +54 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/platform/index.ts +3 -0
- package/ts/platform/objectstorageretention.ts +372 -0
- package/ts/platform/types.ts +3 -0
- package/ts/plugins.runtime.ts +4 -0
- package/ts/requests/config.ts +3 -0
- package/ts/runtime.cloudlylegacydeploymentsettlement.golden.ts +421 -0
- package/ts/runtime.cloudlylegacydeploymentsettlement.ts +2884 -0
- package/ts/runtime.corestore.ts +909 -0
- package/ts/runtime.ts +2 -0
- package/ts/runtime.workloadinit.ts +1 -1
|
@@ -0,0 +1,909 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type {
|
|
3
|
+
IActiveSecretRecipientMetadata,
|
|
4
|
+
IIdentityCredential,
|
|
5
|
+
ISecretVersionReference,
|
|
6
|
+
} from './data/index.js';
|
|
7
|
+
import { isSha256Digest } from './data/immutableimage.js';
|
|
8
|
+
import {
|
|
9
|
+
validateSecretRecipientMetadata,
|
|
10
|
+
verifySecretEnvelopeContext,
|
|
11
|
+
} from './data/secret.js';
|
|
12
|
+
import type { ISecretEnvelopeAdmissionBindingV1 } from './runtime.js';
|
|
13
|
+
import type {
|
|
14
|
+
IObjectStorageRetentionEvidenceExpectationV1,
|
|
15
|
+
IObjectStorageRetentionEvidenceV1,
|
|
16
|
+
} from './platform/objectstorageretention.js';
|
|
17
|
+
import { validateObjectStorageRetentionEvidence } from './platform/objectstorageretention.js';
|
|
18
|
+
|
|
19
|
+
export type TCorestoreCredentialCapability = 'database' | 'objectstorage';
|
|
20
|
+
|
|
21
|
+
export const corestoreProviderConfigIds = Object.freeze({
|
|
22
|
+
database: 'cloudly-corestore-database',
|
|
23
|
+
objectstorage: 'cloudly-corestore-objectstorage',
|
|
24
|
+
} as const);
|
|
25
|
+
export const corestoreControlUrl = 'http://corestore:3000' as const;
|
|
26
|
+
export const corestoreCredentialManagementScope = 'platform:cloudly-corestore' as const;
|
|
27
|
+
export const corestoreControlCredentialKey = 'CORESTORE_API_TOKEN' as const;
|
|
28
|
+
export const corestoreCredentialEnvironment = 'production' as const;
|
|
29
|
+
|
|
30
|
+
export const corestoreCredentialRuntimeLimits = Object.freeze({
|
|
31
|
+
maximumIdentifierLength: 200,
|
|
32
|
+
maximumPublicationGrants: 256,
|
|
33
|
+
maximumGrantLifetimeMs: 24 * 60 * 60 * 1000,
|
|
34
|
+
maximumCredentialStringBytes: 8 * 1024,
|
|
35
|
+
maximumSealedMaterialBytes: 64 * 1024,
|
|
36
|
+
minimumControlTokenBytes: 32,
|
|
37
|
+
maximumControlTokenBytes: 4096,
|
|
38
|
+
maximumSealedControlCredentialBytes: 12 * 1024,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const getCorestoreProviderConfigId = (
|
|
42
|
+
capabilityArg: TCorestoreCredentialCapability,
|
|
43
|
+
): typeof corestoreProviderConfigIds[TCorestoreCredentialCapability] => {
|
|
44
|
+
if (capabilityArg !== 'database' && capabilityArg !== 'objectstorage') {
|
|
45
|
+
throw new Error('corestore capability must be database or objectstorage');
|
|
46
|
+
}
|
|
47
|
+
return corestoreProviderConfigIds[capabilityArg];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export interface ICorestoreControlCredentialPlaintextV1 {
|
|
51
|
+
schemaVersion: 1;
|
|
52
|
+
key: typeof corestoreControlCredentialKey;
|
|
53
|
+
environment: typeof corestoreCredentialEnvironment;
|
|
54
|
+
value: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ICorestoreControlCredentialMaterialV1 {
|
|
58
|
+
schemaVersion: 1;
|
|
59
|
+
requestId: string;
|
|
60
|
+
clusterId: string;
|
|
61
|
+
capability: TCorestoreCredentialCapability;
|
|
62
|
+
providerConfigId: typeof corestoreProviderConfigIds[TCorestoreCredentialCapability];
|
|
63
|
+
controlUrl: typeof corestoreControlUrl;
|
|
64
|
+
managementScope: typeof corestoreCredentialManagementScope;
|
|
65
|
+
key: typeof corestoreControlCredentialKey;
|
|
66
|
+
environment: typeof corestoreCredentialEnvironment;
|
|
67
|
+
recipientKeyId: string;
|
|
68
|
+
recipientGeneration: number;
|
|
69
|
+
envelope: plugins.smartcrypto.IX25519EnvelopeV1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface IReq_GetCorestoreControlCredentialMaterial
|
|
73
|
+
extends plugins.typedrequestInterfaces.implementsTR<
|
|
74
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
75
|
+
IReq_GetCorestoreControlCredentialMaterial
|
|
76
|
+
> {
|
|
77
|
+
method: 'getCorestoreControlCredentialMaterial';
|
|
78
|
+
request: {
|
|
79
|
+
identity: IIdentityCredential;
|
|
80
|
+
requestId: string;
|
|
81
|
+
capability: TCorestoreCredentialCapability;
|
|
82
|
+
expectedRecipientKeyId: string;
|
|
83
|
+
expectedRecipientGeneration: number;
|
|
84
|
+
};
|
|
85
|
+
response: {
|
|
86
|
+
material: ICorestoreControlCredentialMaterialV1;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface ICorestoreControlCredentialMaterialExpectationV1 {
|
|
91
|
+
requestId: string;
|
|
92
|
+
clusterId: string;
|
|
93
|
+
capability: TCorestoreCredentialCapability;
|
|
94
|
+
expectedRecipientKeyId: string;
|
|
95
|
+
expectedRecipientGeneration: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ICorestoreControlCredentialMaterialContextInputV1 {
|
|
99
|
+
requestId: string;
|
|
100
|
+
clusterId: string;
|
|
101
|
+
capability: TCorestoreCredentialCapability;
|
|
102
|
+
recipientKeyId: string;
|
|
103
|
+
recipientGeneration: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const identifierPattern = /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/;
|
|
107
|
+
const bareSha256Pattern = /^[a-f0-9]{64}$/;
|
|
108
|
+
const environmentKeyPattern = /^[A-Z_][A-Z0-9_]{0,127}$/;
|
|
109
|
+
|
|
110
|
+
const isRecord = (valueArg: unknown): valueArg is Record<string, unknown> => (
|
|
111
|
+
Boolean(valueArg)
|
|
112
|
+
&& typeof valueArg === 'object'
|
|
113
|
+
&& !Array.isArray(valueArg)
|
|
114
|
+
&& (Object.getPrototypeOf(valueArg) === Object.prototype
|
|
115
|
+
|| Object.getPrototypeOf(valueArg) === null)
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const hasExactKeys = (
|
|
119
|
+
valueArg: Record<string, unknown>,
|
|
120
|
+
keysArg: readonly string[],
|
|
121
|
+
): boolean => {
|
|
122
|
+
const actual = Object.keys(valueArg).sort();
|
|
123
|
+
const expected = [...keysArg].sort();
|
|
124
|
+
return actual.length === expected.length
|
|
125
|
+
&& actual.every((keyArg, indexArg) => keyArg === expected[indexArg]);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const hasKeys = (
|
|
129
|
+
valueArg: Record<string, unknown>,
|
|
130
|
+
requiredArg: readonly string[],
|
|
131
|
+
optionalArg: readonly string[],
|
|
132
|
+
): boolean => requiredArg.every((keyArg) => Object.hasOwn(valueArg, keyArg))
|
|
133
|
+
&& Object.keys(valueArg).every((keyArg) => requiredArg.includes(keyArg)
|
|
134
|
+
|| optionalArg.includes(keyArg));
|
|
135
|
+
|
|
136
|
+
const isIdentifier = (valueArg: unknown): valueArg is string => (
|
|
137
|
+
typeof valueArg === 'string' && identifierPattern.test(valueArg)
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const isPositiveSafeInteger = (valueArg: unknown): valueArg is number => (
|
|
141
|
+
Number.isSafeInteger(valueArg) && (valueArg as number) > 0
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const isNonNegativeSafeInteger = (valueArg: unknown): valueArg is number => (
|
|
145
|
+
Number.isSafeInteger(valueArg) && (valueArg as number) >= 0 && !Object.is(valueArg, -0)
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const isJwtIdentity = (valueArg: unknown): valueArg is IIdentityCredential => (
|
|
149
|
+
isRecord(valueArg)
|
|
150
|
+
&& hasExactKeys(valueArg, ['jwt'])
|
|
151
|
+
&& typeof valueArg.jwt === 'string'
|
|
152
|
+
&& valueArg.jwt.length > 0
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const utf8ByteLength = (valueArg: string): number => new TextEncoder().encode(valueArg).byteLength;
|
|
156
|
+
|
|
157
|
+
const isCorestoreControlToken = (valueArg: unknown): valueArg is string => {
|
|
158
|
+
if (typeof valueArg !== 'string' || valueArg.length === 0 || /\s/u.test(valueArg)) return false;
|
|
159
|
+
const encoded = new TextEncoder().encode(valueArg);
|
|
160
|
+
let roundTrip: string;
|
|
161
|
+
try {
|
|
162
|
+
roundTrip = new TextDecoder('utf-8', { fatal: true }).decode(encoded);
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
return roundTrip === valueArg
|
|
167
|
+
&& encoded.byteLength >= corestoreCredentialRuntimeLimits.minimumControlTokenBytes
|
|
168
|
+
&& encoded.byteLength <= corestoreCredentialRuntimeLimits.maximumControlTokenBytes;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export const validateGetCorestoreControlCredentialMaterialRequest = (
|
|
172
|
+
requestArg: unknown,
|
|
173
|
+
activeRecipientArg: IActiveSecretRecipientMetadata,
|
|
174
|
+
): string[] => {
|
|
175
|
+
if (!isRecord(requestArg)
|
|
176
|
+
|| !hasExactKeys(requestArg, [
|
|
177
|
+
'identity',
|
|
178
|
+
'requestId',
|
|
179
|
+
'capability',
|
|
180
|
+
'expectedRecipientKeyId',
|
|
181
|
+
'expectedRecipientGeneration',
|
|
182
|
+
])) {
|
|
183
|
+
return ['corestore control credential request must use its exact schema'];
|
|
184
|
+
}
|
|
185
|
+
const errors: string[] = [];
|
|
186
|
+
if (!isJwtIdentity(requestArg.identity)) {
|
|
187
|
+
errors.push('corestore control credential request requires only a JWT identity');
|
|
188
|
+
}
|
|
189
|
+
if (!isIdentifier(requestArg.requestId)
|
|
190
|
+
|| !isIdentifier(requestArg.expectedRecipientKeyId)) {
|
|
191
|
+
errors.push('corestore control credential request identifiers must be canonical');
|
|
192
|
+
}
|
|
193
|
+
if (requestArg.capability !== 'database' && requestArg.capability !== 'objectstorage') {
|
|
194
|
+
errors.push('corestore control credential capability must be database or objectstorage');
|
|
195
|
+
}
|
|
196
|
+
if (!isPositiveSafeInteger(requestArg.expectedRecipientGeneration)) {
|
|
197
|
+
errors.push('corestore control credential recipient generation must be positive');
|
|
198
|
+
}
|
|
199
|
+
if (validateSecretRecipientMetadata(activeRecipientArg).length > 0
|
|
200
|
+
|| activeRecipientArg.lifecycleState !== 'active'
|
|
201
|
+
|| requestArg.expectedRecipientKeyId !== activeRecipientArg.recipientKeyId
|
|
202
|
+
|| requestArg.expectedRecipientGeneration !== activeRecipientArg.generation) {
|
|
203
|
+
errors.push('corestore control credential request must target the active recipient');
|
|
204
|
+
}
|
|
205
|
+
return errors;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export const createCorestoreControlCredentialMaterialEnvelopeContext = (
|
|
209
|
+
materialArg: ICorestoreControlCredentialMaterialContextInputV1,
|
|
210
|
+
): Uint8Array => new TextEncoder().encode(JSON.stringify({
|
|
211
|
+
schemaVersion: 1,
|
|
212
|
+
purpose: 'serve.zone/corestore-control-credential-material',
|
|
213
|
+
requestId: materialArg.requestId,
|
|
214
|
+
clusterId: materialArg.clusterId,
|
|
215
|
+
capability: materialArg.capability,
|
|
216
|
+
providerConfigId: getCorestoreProviderConfigId(materialArg.capability),
|
|
217
|
+
controlUrl: corestoreControlUrl,
|
|
218
|
+
managementScope: corestoreCredentialManagementScope,
|
|
219
|
+
key: corestoreControlCredentialKey,
|
|
220
|
+
environment: corestoreCredentialEnvironment,
|
|
221
|
+
recipientKeyId: materialArg.recipientKeyId,
|
|
222
|
+
recipientGeneration: materialArg.recipientGeneration,
|
|
223
|
+
}));
|
|
224
|
+
|
|
225
|
+
export const validateCorestoreControlCredentialPlaintext = (
|
|
226
|
+
plaintextArg: unknown,
|
|
227
|
+
): string[] => {
|
|
228
|
+
if (!isRecord(plaintextArg)
|
|
229
|
+
|| !hasExactKeys(plaintextArg, ['schemaVersion', 'key', 'environment', 'value'])) {
|
|
230
|
+
return ['corestore control credential plaintext must use its exact schema'];
|
|
231
|
+
}
|
|
232
|
+
const errors: string[] = [];
|
|
233
|
+
if (plaintextArg.schemaVersion !== 1
|
|
234
|
+
|| plaintextArg.key !== corestoreControlCredentialKey
|
|
235
|
+
|| plaintextArg.environment !== corestoreCredentialEnvironment) {
|
|
236
|
+
errors.push('corestore control credential plaintext constants must be canonical');
|
|
237
|
+
}
|
|
238
|
+
if (!isCorestoreControlToken(plaintextArg.value)) {
|
|
239
|
+
errors.push('corestore control credential value must be 32-4096 UTF-8 bytes without whitespace');
|
|
240
|
+
}
|
|
241
|
+
return errors;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
export const validateCorestoreControlCredentialMaterial = async (
|
|
245
|
+
materialArg: unknown,
|
|
246
|
+
expectedArg: ICorestoreControlCredentialMaterialExpectationV1,
|
|
247
|
+
): Promise<string[]> => {
|
|
248
|
+
if (!isRecord(materialArg)
|
|
249
|
+
|| !hasExactKeys(materialArg, [
|
|
250
|
+
'schemaVersion',
|
|
251
|
+
'requestId',
|
|
252
|
+
'clusterId',
|
|
253
|
+
'capability',
|
|
254
|
+
'providerConfigId',
|
|
255
|
+
'controlUrl',
|
|
256
|
+
'managementScope',
|
|
257
|
+
'key',
|
|
258
|
+
'environment',
|
|
259
|
+
'recipientKeyId',
|
|
260
|
+
'recipientGeneration',
|
|
261
|
+
'envelope',
|
|
262
|
+
])) {
|
|
263
|
+
return ['corestore control credential material must use its exact schema'];
|
|
264
|
+
}
|
|
265
|
+
const errors: string[] = [];
|
|
266
|
+
if (materialArg.schemaVersion !== 1
|
|
267
|
+
|| !isIdentifier(materialArg.requestId)
|
|
268
|
+
|| !isIdentifier(materialArg.clusterId)
|
|
269
|
+
|| !isIdentifier(materialArg.recipientKeyId)
|
|
270
|
+
|| !isPositiveSafeInteger(materialArg.recipientGeneration)) {
|
|
271
|
+
errors.push('corestore control credential material metadata must be canonical');
|
|
272
|
+
}
|
|
273
|
+
if (materialArg.capability !== 'database' && materialArg.capability !== 'objectstorage') {
|
|
274
|
+
errors.push('corestore control credential material capability must be canonical');
|
|
275
|
+
} else if (materialArg.providerConfigId !== getCorestoreProviderConfigId(materialArg.capability)) {
|
|
276
|
+
errors.push('corestore control credential provider must be derived from capability');
|
|
277
|
+
}
|
|
278
|
+
if (materialArg.controlUrl !== corestoreControlUrl
|
|
279
|
+
|| materialArg.managementScope !== corestoreCredentialManagementScope
|
|
280
|
+
|| materialArg.key !== corestoreControlCredentialKey
|
|
281
|
+
|| materialArg.environment !== corestoreCredentialEnvironment) {
|
|
282
|
+
errors.push('corestore control credential material constants must be canonical');
|
|
283
|
+
}
|
|
284
|
+
if (materialArg.requestId !== expectedArg.requestId
|
|
285
|
+
|| materialArg.clusterId !== expectedArg.clusterId
|
|
286
|
+
|| materialArg.capability !== expectedArg.capability
|
|
287
|
+
|| materialArg.recipientKeyId !== expectedArg.expectedRecipientKeyId
|
|
288
|
+
|| materialArg.recipientGeneration !== expectedArg.expectedRecipientGeneration) {
|
|
289
|
+
errors.push('corestore control credential material does not match its request authority');
|
|
290
|
+
}
|
|
291
|
+
let envelope: plugins.smartcrypto.IX25519EnvelopeV1 | undefined;
|
|
292
|
+
try {
|
|
293
|
+
envelope = plugins.smartcrypto.parseX25519Envelope(materialArg.envelope);
|
|
294
|
+
if (envelope.ciphertext.length
|
|
295
|
+
> Math.ceil(corestoreCredentialRuntimeLimits.maximumSealedControlCredentialBytes * 4 / 3)) {
|
|
296
|
+
errors.push('corestore control credential envelope exceeds its byte limit');
|
|
297
|
+
}
|
|
298
|
+
} catch {
|
|
299
|
+
errors.push('corestore control credential envelope must be strict');
|
|
300
|
+
}
|
|
301
|
+
if (envelope) {
|
|
302
|
+
if (envelope.recipientKeyId !== materialArg.recipientKeyId) {
|
|
303
|
+
errors.push('corestore control credential envelope recipient must match');
|
|
304
|
+
} else if (errors.length === 0 && !await verifySecretEnvelopeContext(
|
|
305
|
+
envelope,
|
|
306
|
+
createCorestoreControlCredentialMaterialEnvelopeContext(
|
|
307
|
+
materialArg as unknown as ICorestoreControlCredentialMaterialContextInputV1,
|
|
308
|
+
),
|
|
309
|
+
)) {
|
|
310
|
+
errors.push('corestore control credential envelope context must match');
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return errors;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
export interface ICorestoreCredentialPublicationGrantV1 {
|
|
317
|
+
schemaVersion: 1;
|
|
318
|
+
operationId: string;
|
|
319
|
+
serviceId: string;
|
|
320
|
+
bindingId: string;
|
|
321
|
+
capability: TCorestoreCredentialCapability;
|
|
322
|
+
reconciliationGeneration: number;
|
|
323
|
+
expectedTargetSecretsRevision: number;
|
|
324
|
+
bindingRequestDigest: string;
|
|
325
|
+
issuedAt: number;
|
|
326
|
+
expiresAt: number;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export const validateCorestoreCredentialPublicationGrant = (
|
|
330
|
+
grantArg: unknown,
|
|
331
|
+
trustedNowArg?: number,
|
|
332
|
+
): string[] => {
|
|
333
|
+
if (!isRecord(grantArg)
|
|
334
|
+
|| !hasExactKeys(grantArg, [
|
|
335
|
+
'schemaVersion',
|
|
336
|
+
'operationId',
|
|
337
|
+
'serviceId',
|
|
338
|
+
'bindingId',
|
|
339
|
+
'capability',
|
|
340
|
+
'reconciliationGeneration',
|
|
341
|
+
'expectedTargetSecretsRevision',
|
|
342
|
+
'bindingRequestDigest',
|
|
343
|
+
'issuedAt',
|
|
344
|
+
'expiresAt',
|
|
345
|
+
])) {
|
|
346
|
+
return ['corestore credential publication grant must use its exact schema'];
|
|
347
|
+
}
|
|
348
|
+
const errors: string[] = [];
|
|
349
|
+
if (grantArg.schemaVersion !== 1) {
|
|
350
|
+
errors.push('corestore credential publication grant schemaVersion must be 1');
|
|
351
|
+
}
|
|
352
|
+
if (!isIdentifier(grantArg.operationId)
|
|
353
|
+
|| !isIdentifier(grantArg.serviceId)
|
|
354
|
+
|| !isIdentifier(grantArg.bindingId)) {
|
|
355
|
+
errors.push('corestore credential publication grant identifiers must be canonical');
|
|
356
|
+
}
|
|
357
|
+
if (grantArg.capability !== 'database' && grantArg.capability !== 'objectstorage') {
|
|
358
|
+
errors.push('corestore credential publication grant capability must be canonical');
|
|
359
|
+
}
|
|
360
|
+
if (!isPositiveSafeInteger(grantArg.reconciliationGeneration)
|
|
361
|
+
|| !isNonNegativeSafeInteger(grantArg.expectedTargetSecretsRevision)) {
|
|
362
|
+
errors.push('corestore credential publication grant revisions must be canonical');
|
|
363
|
+
}
|
|
364
|
+
if (typeof grantArg.bindingRequestDigest !== 'string'
|
|
365
|
+
|| !bareSha256Pattern.test(grantArg.bindingRequestDigest)) {
|
|
366
|
+
errors.push('corestore credential publication grant digest must be bare lowercase 64-hex');
|
|
367
|
+
}
|
|
368
|
+
if (!isPositiveSafeInteger(grantArg.issuedAt)
|
|
369
|
+
|| !isPositiveSafeInteger(grantArg.expiresAt)
|
|
370
|
+
|| (grantArg.expiresAt as number) <= (grantArg.issuedAt as number)
|
|
371
|
+
|| (grantArg.expiresAt as number) - (grantArg.issuedAt as number)
|
|
372
|
+
> corestoreCredentialRuntimeLimits.maximumGrantLifetimeMs) {
|
|
373
|
+
errors.push('corestore credential publication grant lifetime must be positive and bounded');
|
|
374
|
+
}
|
|
375
|
+
if (trustedNowArg !== undefined
|
|
376
|
+
&& (!isPositiveSafeInteger(trustedNowArg)
|
|
377
|
+
|| trustedNowArg < (grantArg.issuedAt as number)
|
|
378
|
+
|| trustedNowArg >= (grantArg.expiresAt as number))) {
|
|
379
|
+
errors.push('corestore credential publication grant must be currently valid');
|
|
380
|
+
}
|
|
381
|
+
return errors;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
export const corestoreCredentialPublicationGrantsEqual = (
|
|
385
|
+
leftArg: unknown,
|
|
386
|
+
rightArg: unknown,
|
|
387
|
+
): boolean => validateCorestoreCredentialPublicationGrant(leftArg).length === 0
|
|
388
|
+
&& validateCorestoreCredentialPublicationGrant(rightArg).length === 0
|
|
389
|
+
&& (leftArg as ICorestoreCredentialPublicationGrantV1).schemaVersion
|
|
390
|
+
=== (rightArg as ICorestoreCredentialPublicationGrantV1).schemaVersion
|
|
391
|
+
&& (leftArg as ICorestoreCredentialPublicationGrantV1).operationId
|
|
392
|
+
=== (rightArg as ICorestoreCredentialPublicationGrantV1).operationId
|
|
393
|
+
&& (leftArg as ICorestoreCredentialPublicationGrantV1).serviceId
|
|
394
|
+
=== (rightArg as ICorestoreCredentialPublicationGrantV1).serviceId
|
|
395
|
+
&& (leftArg as ICorestoreCredentialPublicationGrantV1).bindingId
|
|
396
|
+
=== (rightArg as ICorestoreCredentialPublicationGrantV1).bindingId
|
|
397
|
+
&& (leftArg as ICorestoreCredentialPublicationGrantV1).capability
|
|
398
|
+
=== (rightArg as ICorestoreCredentialPublicationGrantV1).capability
|
|
399
|
+
&& (leftArg as ICorestoreCredentialPublicationGrantV1).reconciliationGeneration
|
|
400
|
+
=== (rightArg as ICorestoreCredentialPublicationGrantV1).reconciliationGeneration
|
|
401
|
+
&& (leftArg as ICorestoreCredentialPublicationGrantV1).expectedTargetSecretsRevision
|
|
402
|
+
=== (rightArg as ICorestoreCredentialPublicationGrantV1).expectedTargetSecretsRevision
|
|
403
|
+
&& (leftArg as ICorestoreCredentialPublicationGrantV1).bindingRequestDigest
|
|
404
|
+
=== (rightArg as ICorestoreCredentialPublicationGrantV1).bindingRequestDigest
|
|
405
|
+
&& (leftArg as ICorestoreCredentialPublicationGrantV1).issuedAt
|
|
406
|
+
=== (rightArg as ICorestoreCredentialPublicationGrantV1).issuedAt
|
|
407
|
+
&& (leftArg as ICorestoreCredentialPublicationGrantV1).expiresAt
|
|
408
|
+
=== (rightArg as ICorestoreCredentialPublicationGrantV1).expiresAt;
|
|
409
|
+
|
|
410
|
+
const comparePublicationGrants = (
|
|
411
|
+
leftArg: ICorestoreCredentialPublicationGrantV1,
|
|
412
|
+
rightArg: ICorestoreCredentialPublicationGrantV1,
|
|
413
|
+
): number => leftArg.bindingId < rightArg.bindingId
|
|
414
|
+
? -1
|
|
415
|
+
: leftArg.bindingId > rightArg.bindingId
|
|
416
|
+
? 1
|
|
417
|
+
: leftArg.operationId < rightArg.operationId
|
|
418
|
+
? -1
|
|
419
|
+
: leftArg.operationId > rightArg.operationId
|
|
420
|
+
? 1
|
|
421
|
+
: 0;
|
|
422
|
+
|
|
423
|
+
export const validateCorestoreCredentialPublicationGrants = (
|
|
424
|
+
grantsArg: unknown,
|
|
425
|
+
trustedNowArg?: number,
|
|
426
|
+
): string[] => {
|
|
427
|
+
if (!Array.isArray(grantsArg)
|
|
428
|
+
|| grantsArg.length > corestoreCredentialRuntimeLimits.maximumPublicationGrants) {
|
|
429
|
+
return ['corestore credential publication grants must be a bounded array'];
|
|
430
|
+
}
|
|
431
|
+
const errors: string[] = [];
|
|
432
|
+
let previous: ICorestoreCredentialPublicationGrantV1 | undefined;
|
|
433
|
+
for (const [index, grantArg] of grantsArg.entries()) {
|
|
434
|
+
const grantErrors = validateCorestoreCredentialPublicationGrant(grantArg, trustedNowArg);
|
|
435
|
+
errors.push(...grantErrors.map((errorArg) => `publication grants[${index}] ${errorArg}`));
|
|
436
|
+
if (grantErrors.length === 0) {
|
|
437
|
+
const grant = grantArg as ICorestoreCredentialPublicationGrantV1;
|
|
438
|
+
if (previous && comparePublicationGrants(previous, grant) >= 0) {
|
|
439
|
+
errors.push('corestore credential publication grants must be unique and sorted by bindingId then operationId');
|
|
440
|
+
}
|
|
441
|
+
previous = grant;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return errors;
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
export const corestoreDatabaseCredentialKeys = Object.freeze([
|
|
448
|
+
'MONGODB_URI',
|
|
449
|
+
'MONGODB_URL',
|
|
450
|
+
'MONGO_URL',
|
|
451
|
+
'MONGODB_HOST',
|
|
452
|
+
'MONGODB_PORT',
|
|
453
|
+
'MONGODB_DATABASE',
|
|
454
|
+
'MONGO_DBNAME',
|
|
455
|
+
'MONGODB_USERNAME',
|
|
456
|
+
'MONGO_DBUSER',
|
|
457
|
+
'MONGODB_PASSWORD',
|
|
458
|
+
'MONGO_DBPASS',
|
|
459
|
+
] as const);
|
|
460
|
+
export const corestoreObjectStorageCredentialKeys = Object.freeze([
|
|
461
|
+
'accessKeyId',
|
|
462
|
+
'secretAccessKey',
|
|
463
|
+
] as const);
|
|
464
|
+
|
|
465
|
+
export type TCorestoreDatabaseCredentialKey = typeof corestoreDatabaseCredentialKeys[number];
|
|
466
|
+
export type TCorestoreObjectStorageCredentialKey =
|
|
467
|
+
typeof corestoreObjectStorageCredentialKeys[number];
|
|
468
|
+
export type TCorestoreCredentialKey =
|
|
469
|
+
| TCorestoreDatabaseCredentialKey
|
|
470
|
+
| TCorestoreObjectStorageCredentialKey;
|
|
471
|
+
|
|
472
|
+
export type TCorestoreDatabaseCredentialValuesV1 = {
|
|
473
|
+
[TKey in TCorestoreDatabaseCredentialKey]: string;
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
export interface ICorestoreDatabaseCredentialPlaintextMaterialV1 {
|
|
477
|
+
schemaVersion: 1;
|
|
478
|
+
capability: 'database';
|
|
479
|
+
values: TCorestoreDatabaseCredentialValuesV1;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export interface ICorestoreObjectStorageCredentialPlaintextMaterialV1 {
|
|
483
|
+
schemaVersion: 1;
|
|
484
|
+
capability: 'objectstorage';
|
|
485
|
+
values: {
|
|
486
|
+
accessKeyId: string;
|
|
487
|
+
secretAccessKey: string;
|
|
488
|
+
};
|
|
489
|
+
retention?: IObjectStorageRetentionEvidenceV1;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export type TCorestoreCredentialPlaintextMaterialV1 =
|
|
493
|
+
| ICorestoreDatabaseCredentialPlaintextMaterialV1
|
|
494
|
+
| ICorestoreObjectStorageCredentialPlaintextMaterialV1;
|
|
495
|
+
|
|
496
|
+
const readCredentialString = (valueArg: unknown): boolean => typeof valueArg === 'string'
|
|
497
|
+
&& valueArg.length > 0
|
|
498
|
+
&& utf8ByteLength(valueArg) <= corestoreCredentialRuntimeLimits.maximumCredentialStringBytes
|
|
499
|
+
&& !valueArg.includes('\0');
|
|
500
|
+
|
|
501
|
+
export const validateCorestoreDatabaseCredentialMaterial = (
|
|
502
|
+
materialArg: unknown,
|
|
503
|
+
): string[] => {
|
|
504
|
+
if (!isRecord(materialArg)
|
|
505
|
+
|| !hasExactKeys(materialArg, ['schemaVersion', 'capability', 'values'])
|
|
506
|
+
|| materialArg.schemaVersion !== 1
|
|
507
|
+
|| materialArg.capability !== 'database'
|
|
508
|
+
|| !isRecord(materialArg.values)
|
|
509
|
+
|| !hasExactKeys(materialArg.values, corestoreDatabaseCredentialKeys)) {
|
|
510
|
+
return ['corestore database credential material must use its exact schema'];
|
|
511
|
+
}
|
|
512
|
+
const values = materialArg.values;
|
|
513
|
+
const errors: string[] = [];
|
|
514
|
+
if (corestoreDatabaseCredentialKeys.some((keyArg) => !readCredentialString(values[keyArg]))) {
|
|
515
|
+
errors.push('corestore database credential values must be bounded non-empty strings');
|
|
516
|
+
}
|
|
517
|
+
if (values.MONGODB_URI !== values.MONGODB_URL
|
|
518
|
+
|| values.MONGODB_URI !== values.MONGO_URL) {
|
|
519
|
+
errors.push('corestore database URI aliases must be equal');
|
|
520
|
+
}
|
|
521
|
+
if (values.MONGODB_DATABASE !== values.MONGO_DBNAME) {
|
|
522
|
+
errors.push('corestore database name aliases must be equal');
|
|
523
|
+
}
|
|
524
|
+
if (values.MONGODB_USERNAME !== values.MONGO_DBUSER) {
|
|
525
|
+
errors.push('corestore database username aliases must be equal');
|
|
526
|
+
}
|
|
527
|
+
if (values.MONGODB_PASSWORD !== values.MONGO_DBPASS) {
|
|
528
|
+
errors.push('corestore database password aliases must be equal');
|
|
529
|
+
}
|
|
530
|
+
if (typeof values.MONGODB_PORT !== 'string'
|
|
531
|
+
|| !/^[1-9][0-9]{0,4}$/.test(values.MONGODB_PORT)
|
|
532
|
+
|| Number(values.MONGODB_PORT) > 65535) {
|
|
533
|
+
errors.push('corestore database port must be canonical');
|
|
534
|
+
}
|
|
535
|
+
return errors;
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
export const validateCorestoreObjectStorageCredentialMaterial = async (
|
|
539
|
+
materialArg: unknown,
|
|
540
|
+
retentionExpectationArg?: IObjectStorageRetentionEvidenceExpectationV1,
|
|
541
|
+
): Promise<string[]> => {
|
|
542
|
+
if (!isRecord(materialArg)
|
|
543
|
+
|| !hasKeys(materialArg, ['schemaVersion', 'capability', 'values'], ['retention'])
|
|
544
|
+
|| materialArg.schemaVersion !== 1
|
|
545
|
+
|| materialArg.capability !== 'objectstorage'
|
|
546
|
+
|| !isRecord(materialArg.values)
|
|
547
|
+
|| !hasExactKeys(materialArg.values, corestoreObjectStorageCredentialKeys)) {
|
|
548
|
+
return ['corestore objectstorage credential material must use its exact schema'];
|
|
549
|
+
}
|
|
550
|
+
const errors: string[] = [];
|
|
551
|
+
if (!readCredentialString(materialArg.values.accessKeyId)
|
|
552
|
+
|| !readCredentialString(materialArg.values.secretAccessKey)) {
|
|
553
|
+
errors.push('corestore objectstorage credential values must be bounded non-empty strings');
|
|
554
|
+
}
|
|
555
|
+
if (materialArg.retention !== undefined) {
|
|
556
|
+
if (!retentionExpectationArg) {
|
|
557
|
+
errors.push('corestore objectstorage retention evidence requires trusted binding authority');
|
|
558
|
+
} else {
|
|
559
|
+
errors.push(...await validateObjectStorageRetentionEvidence(
|
|
560
|
+
materialArg.retention,
|
|
561
|
+
retentionExpectationArg,
|
|
562
|
+
));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return errors;
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
export const validateCorestoreCredentialPlaintextMaterial = async (
|
|
569
|
+
materialArg: unknown,
|
|
570
|
+
retentionExpectationArg?: IObjectStorageRetentionEvidenceExpectationV1,
|
|
571
|
+
): Promise<string[]> => isRecord(materialArg) && materialArg.capability === 'database'
|
|
572
|
+
? validateCorestoreDatabaseCredentialMaterial(materialArg)
|
|
573
|
+
: validateCorestoreObjectStorageCredentialMaterial(materialArg, retentionExpectationArg);
|
|
574
|
+
|
|
575
|
+
export interface IPublishCorestoreCredentialMaterialRequestV1 {
|
|
576
|
+
identity: IIdentityCredential;
|
|
577
|
+
mutationId: string;
|
|
578
|
+
grant: ICorestoreCredentialPublicationGrantV1;
|
|
579
|
+
expectedIngressRecipientKeyId: string;
|
|
580
|
+
expectedIngressRecipientGeneration: number;
|
|
581
|
+
envelope: plugins.smartcrypto.IX25519EnvelopeV1;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export interface ICorestoreCredentialSecretVersionReferenceV1
|
|
585
|
+
extends ISecretVersionReference {
|
|
586
|
+
key: TCorestoreCredentialKey;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export interface ICorestoreCredentialPublicationCoverageV1 {
|
|
590
|
+
keys: TCorestoreCredentialKey[];
|
|
591
|
+
versionReferences: ICorestoreCredentialSecretVersionReferenceV1[];
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export interface ICorestoreCredentialPublicationReceiptV1 {
|
|
595
|
+
schemaVersion: 1;
|
|
596
|
+
mutationId: string;
|
|
597
|
+
organizationId: string;
|
|
598
|
+
clusterId: string;
|
|
599
|
+
serviceId: string;
|
|
600
|
+
bindingId: string;
|
|
601
|
+
capability: TCorestoreCredentialCapability;
|
|
602
|
+
grant: ICorestoreCredentialPublicationGrantV1;
|
|
603
|
+
coverage: ICorestoreCredentialPublicationCoverageV1;
|
|
604
|
+
expectedTargetSecretsRevision: number;
|
|
605
|
+
targetSecretsRevision: number;
|
|
606
|
+
ingressAdmissionBinding: ISecretEnvelopeAdmissionBindingV1;
|
|
607
|
+
acceptedAt: number;
|
|
608
|
+
replayed: boolean;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export interface IReq_PublishCorestoreCredentialMaterial
|
|
612
|
+
extends plugins.typedrequestInterfaces.implementsTR<
|
|
613
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
614
|
+
IReq_PublishCorestoreCredentialMaterial
|
|
615
|
+
> {
|
|
616
|
+
method: 'publishCorestoreCredentialMaterial';
|
|
617
|
+
request: IPublishCorestoreCredentialMaterialRequestV1;
|
|
618
|
+
response: {
|
|
619
|
+
receipt: ICorestoreCredentialPublicationReceiptV1;
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export const createPublishCorestoreCredentialMaterialEnvelopeContext = (
|
|
624
|
+
requestArg: Omit<IPublishCorestoreCredentialMaterialRequestV1, 'identity' | 'envelope'>,
|
|
625
|
+
): Uint8Array => new TextEncoder().encode(JSON.stringify({
|
|
626
|
+
schemaVersion: 1,
|
|
627
|
+
purpose: 'serve.zone/publish-corestore-credential-material',
|
|
628
|
+
mutationId: requestArg.mutationId,
|
|
629
|
+
grant: {
|
|
630
|
+
schemaVersion: requestArg.grant.schemaVersion,
|
|
631
|
+
operationId: requestArg.grant.operationId,
|
|
632
|
+
serviceId: requestArg.grant.serviceId,
|
|
633
|
+
bindingId: requestArg.grant.bindingId,
|
|
634
|
+
capability: requestArg.grant.capability,
|
|
635
|
+
reconciliationGeneration: requestArg.grant.reconciliationGeneration,
|
|
636
|
+
expectedTargetSecretsRevision: requestArg.grant.expectedTargetSecretsRevision,
|
|
637
|
+
bindingRequestDigest: requestArg.grant.bindingRequestDigest,
|
|
638
|
+
issuedAt: requestArg.grant.issuedAt,
|
|
639
|
+
expiresAt: requestArg.grant.expiresAt,
|
|
640
|
+
},
|
|
641
|
+
expectedIngressRecipientKeyId: requestArg.expectedIngressRecipientKeyId,
|
|
642
|
+
expectedIngressRecipientGeneration: requestArg.expectedIngressRecipientGeneration,
|
|
643
|
+
}));
|
|
644
|
+
|
|
645
|
+
export const validatePublishCorestoreCredentialMaterialRequest = async (
|
|
646
|
+
requestArg: unknown,
|
|
647
|
+
expectedGrantArg: ICorestoreCredentialPublicationGrantV1,
|
|
648
|
+
activeRecipientArg: IActiveSecretRecipientMetadata,
|
|
649
|
+
trustedNowArg: number,
|
|
650
|
+
): Promise<string[]> => {
|
|
651
|
+
if (!isRecord(requestArg)
|
|
652
|
+
|| !hasExactKeys(requestArg, [
|
|
653
|
+
'identity',
|
|
654
|
+
'mutationId',
|
|
655
|
+
'grant',
|
|
656
|
+
'expectedIngressRecipientKeyId',
|
|
657
|
+
'expectedIngressRecipientGeneration',
|
|
658
|
+
'envelope',
|
|
659
|
+
])) {
|
|
660
|
+
return ['publish corestore credential request must use its exact schema'];
|
|
661
|
+
}
|
|
662
|
+
const errors = validateCorestoreCredentialPublicationGrant(requestArg.grant, trustedNowArg);
|
|
663
|
+
if (!corestoreCredentialPublicationGrantsEqual(requestArg.grant, expectedGrantArg)) {
|
|
664
|
+
errors.push('publish corestore credential request grant must match current server authority');
|
|
665
|
+
}
|
|
666
|
+
if (!isJwtIdentity(requestArg.identity)) {
|
|
667
|
+
errors.push('publish corestore credential request requires only a JWT identity');
|
|
668
|
+
}
|
|
669
|
+
if (!isIdentifier(requestArg.mutationId)
|
|
670
|
+
|| !isIdentifier(requestArg.expectedIngressRecipientKeyId)
|
|
671
|
+
|| !isPositiveSafeInteger(requestArg.expectedIngressRecipientGeneration)) {
|
|
672
|
+
errors.push('publish corestore credential request recipient and mutation must be canonical');
|
|
673
|
+
}
|
|
674
|
+
let envelope: plugins.smartcrypto.IX25519EnvelopeV1 | undefined;
|
|
675
|
+
try {
|
|
676
|
+
envelope = plugins.smartcrypto.parseX25519Envelope(requestArg.envelope);
|
|
677
|
+
if (envelope.ciphertext.length
|
|
678
|
+
> Math.ceil(corestoreCredentialRuntimeLimits.maximumSealedMaterialBytes * 4 / 3)) {
|
|
679
|
+
errors.push('publish corestore credential envelope exceeds its byte limit');
|
|
680
|
+
}
|
|
681
|
+
} catch {
|
|
682
|
+
errors.push('publish corestore credential envelope must be strict');
|
|
683
|
+
}
|
|
684
|
+
if (validateSecretRecipientMetadata(activeRecipientArg).length > 0
|
|
685
|
+
|| activeRecipientArg.lifecycleState !== 'active'
|
|
686
|
+
|| activeRecipientArg.recipientKeyId !== requestArg.expectedIngressRecipientKeyId
|
|
687
|
+
|| activeRecipientArg.generation !== requestArg.expectedIngressRecipientGeneration) {
|
|
688
|
+
errors.push('publish corestore credential request must target the active ingress recipient');
|
|
689
|
+
}
|
|
690
|
+
if (envelope) {
|
|
691
|
+
if (envelope.recipientKeyId !== requestArg.expectedIngressRecipientKeyId) {
|
|
692
|
+
errors.push('publish corestore credential envelope recipient must match');
|
|
693
|
+
} else if (errors.length === 0 && !await verifySecretEnvelopeContext(
|
|
694
|
+
envelope,
|
|
695
|
+
createPublishCorestoreCredentialMaterialEnvelopeContext(
|
|
696
|
+
requestArg as unknown as Omit<
|
|
697
|
+
IPublishCorestoreCredentialMaterialRequestV1,
|
|
698
|
+
'identity' | 'envelope'
|
|
699
|
+
>,
|
|
700
|
+
),
|
|
701
|
+
)) {
|
|
702
|
+
errors.push('publish corestore credential envelope context must match');
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return errors;
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
const expectedCoverageKeys = (
|
|
709
|
+
capabilityArg: TCorestoreCredentialCapability,
|
|
710
|
+
): readonly TCorestoreCredentialKey[] => capabilityArg === 'database'
|
|
711
|
+
? corestoreDatabaseCredentialKeys
|
|
712
|
+
: corestoreObjectStorageCredentialKeys;
|
|
713
|
+
|
|
714
|
+
export interface ICorestoreCredentialPublicationReceiptExpectationV1 {
|
|
715
|
+
request: IPublishCorestoreCredentialMaterialRequestV1;
|
|
716
|
+
organizationId: string;
|
|
717
|
+
clusterId: string;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const computeRuntimeSha256 = async (valueArg: Uint8Array): Promise<string> => {
|
|
721
|
+
const digest = new Uint8Array(await globalThis.crypto.subtle.digest(
|
|
722
|
+
'SHA-256',
|
|
723
|
+
new Uint8Array(valueArg),
|
|
724
|
+
));
|
|
725
|
+
return [...digest].map((byteArg) => byteArg.toString(16).padStart(2, '0')).join('');
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
const verifyPublicationAdmissionBinding = async (
|
|
729
|
+
bindingArg: ISecretEnvelopeAdmissionBindingV1,
|
|
730
|
+
envelopeArg: unknown,
|
|
731
|
+
contextArg: Uint8Array,
|
|
732
|
+
): Promise<boolean> => {
|
|
733
|
+
try {
|
|
734
|
+
const envelope = plugins.smartcrypto.parseX25519Envelope(envelopeArg);
|
|
735
|
+
if (envelope.recipientKeyId !== bindingArg.recipientKeyId
|
|
736
|
+
|| !await verifySecretEnvelopeContext(envelope, contextArg)) {
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
const envelopeDigestInput = JSON.stringify({
|
|
740
|
+
schemaVersion: envelope.schemaVersion,
|
|
741
|
+
profile: envelope.profile,
|
|
742
|
+
recipientKeyId: envelope.recipientKeyId,
|
|
743
|
+
ephemeralPublicKey: envelope.ephemeralPublicKey,
|
|
744
|
+
nonce: envelope.nonce,
|
|
745
|
+
ciphertext: envelope.ciphertext,
|
|
746
|
+
tag: envelope.tag,
|
|
747
|
+
contextDigest: envelope.contextDigest,
|
|
748
|
+
});
|
|
749
|
+
return bindingArg.envelopeDigest
|
|
750
|
+
=== `sha256:${await computeRuntimeSha256(new TextEncoder().encode(envelopeDigestInput))}`
|
|
751
|
+
&& bindingArg.requestContextDigest
|
|
752
|
+
=== `sha256:${await computeRuntimeSha256(contextArg)}`;
|
|
753
|
+
} catch {
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
export const validateCorestoreCredentialPublicationReceipt = async (
|
|
759
|
+
receiptArg: unknown,
|
|
760
|
+
expectedArg: ICorestoreCredentialPublicationReceiptExpectationV1,
|
|
761
|
+
): Promise<string[]> => {
|
|
762
|
+
if (!isRecord(receiptArg)
|
|
763
|
+
|| !hasExactKeys(receiptArg, [
|
|
764
|
+
'schemaVersion',
|
|
765
|
+
'mutationId',
|
|
766
|
+
'organizationId',
|
|
767
|
+
'clusterId',
|
|
768
|
+
'serviceId',
|
|
769
|
+
'bindingId',
|
|
770
|
+
'capability',
|
|
771
|
+
'grant',
|
|
772
|
+
'coverage',
|
|
773
|
+
'expectedTargetSecretsRevision',
|
|
774
|
+
'targetSecretsRevision',
|
|
775
|
+
'ingressAdmissionBinding',
|
|
776
|
+
'acceptedAt',
|
|
777
|
+
'replayed',
|
|
778
|
+
])) {
|
|
779
|
+
return ['corestore credential publication receipt must use its exact schema'];
|
|
780
|
+
}
|
|
781
|
+
const errors = validateCorestoreCredentialPublicationGrant(receiptArg.grant);
|
|
782
|
+
if (receiptArg.schemaVersion !== 1
|
|
783
|
+
|| !isIdentifier(receiptArg.mutationId)
|
|
784
|
+
|| !isIdentifier(receiptArg.organizationId)
|
|
785
|
+
|| !isIdentifier(receiptArg.clusterId)
|
|
786
|
+
|| !isIdentifier(receiptArg.serviceId)
|
|
787
|
+
|| !isIdentifier(receiptArg.bindingId)) {
|
|
788
|
+
errors.push('corestore credential publication receipt identifiers must be canonical');
|
|
789
|
+
}
|
|
790
|
+
const grant = receiptArg.grant as ICorestoreCredentialPublicationGrantV1;
|
|
791
|
+
if ((receiptArg.capability !== 'database' && receiptArg.capability !== 'objectstorage')
|
|
792
|
+
|| receiptArg.serviceId !== grant.serviceId
|
|
793
|
+
|| receiptArg.bindingId !== grant.bindingId
|
|
794
|
+
|| receiptArg.capability !== grant.capability
|
|
795
|
+
|| receiptArg.expectedTargetSecretsRevision !== grant.expectedTargetSecretsRevision) {
|
|
796
|
+
errors.push('corestore credential publication receipt must exactly bind its grant');
|
|
797
|
+
}
|
|
798
|
+
if (receiptArg.mutationId !== expectedArg.request.mutationId
|
|
799
|
+
|| receiptArg.organizationId !== expectedArg.organizationId
|
|
800
|
+
|| receiptArg.clusterId !== expectedArg.clusterId
|
|
801
|
+
|| !corestoreCredentialPublicationGrantsEqual(receiptArg.grant, expectedArg.request.grant)) {
|
|
802
|
+
errors.push('corestore credential publication receipt must match its trusted request scope');
|
|
803
|
+
}
|
|
804
|
+
if (!isRecord(receiptArg.coverage)
|
|
805
|
+
|| !hasExactKeys(receiptArg.coverage, ['keys', 'versionReferences'])
|
|
806
|
+
|| !Array.isArray(receiptArg.coverage.keys)
|
|
807
|
+
|| !Array.isArray(receiptArg.coverage.versionReferences)) {
|
|
808
|
+
errors.push('corestore credential publication coverage must use its exact schema');
|
|
809
|
+
} else if (receiptArg.capability === 'database' || receiptArg.capability === 'objectstorage') {
|
|
810
|
+
const requiredKeys = expectedCoverageKeys(receiptArg.capability);
|
|
811
|
+
if (JSON.stringify(receiptArg.coverage.keys) !== JSON.stringify(requiredKeys)
|
|
812
|
+
|| receiptArg.coverage.versionReferences.length !== requiredKeys.length) {
|
|
813
|
+
errors.push('corestore credential publication coverage must be exact');
|
|
814
|
+
} else {
|
|
815
|
+
const secretIds = new Set<string>();
|
|
816
|
+
const secretVersionIds = new Set<string>();
|
|
817
|
+
for (const [index, referenceArg] of receiptArg.coverage.versionReferences.entries()) {
|
|
818
|
+
if (!isRecord(referenceArg)
|
|
819
|
+
|| !hasExactKeys(referenceArg, ['key', 'secretId', 'secretVersionId'])
|
|
820
|
+
|| referenceArg.key !== requiredKeys[index]
|
|
821
|
+
|| !isIdentifier(referenceArg.secretId)
|
|
822
|
+
|| !isIdentifier(referenceArg.secretVersionId)) {
|
|
823
|
+
errors.push('corestore credential publication version references must exactly cover keys');
|
|
824
|
+
break;
|
|
825
|
+
}
|
|
826
|
+
if (secretIds.has(referenceArg.secretId as string)
|
|
827
|
+
|| secretVersionIds.has(referenceArg.secretVersionId as string)) {
|
|
828
|
+
errors.push('corestore credential publication version references must be unique');
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
secretIds.add(referenceArg.secretId as string);
|
|
832
|
+
secretVersionIds.add(referenceArg.secretVersionId as string);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
const referenceCount = isRecord(receiptArg.coverage)
|
|
837
|
+
&& Array.isArray(receiptArg.coverage.versionReferences)
|
|
838
|
+
? receiptArg.coverage.versionReferences.length
|
|
839
|
+
: 0;
|
|
840
|
+
if (!isNonNegativeSafeInteger(receiptArg.expectedTargetSecretsRevision)
|
|
841
|
+
|| !isNonNegativeSafeInteger(receiptArg.targetSecretsRevision)
|
|
842
|
+
|| receiptArg.targetSecretsRevision !== receiptArg.expectedTargetSecretsRevision + referenceCount) {
|
|
843
|
+
errors.push('corestore credential publication target Secrets revision must exactly cover created versions');
|
|
844
|
+
}
|
|
845
|
+
const admission = receiptArg.ingressAdmissionBinding;
|
|
846
|
+
if (!isRecord(admission)
|
|
847
|
+
|| !hasExactKeys(admission, [
|
|
848
|
+
'schemaVersion',
|
|
849
|
+
'recipientKeyId',
|
|
850
|
+
'recipientGeneration',
|
|
851
|
+
'envelopeDigest',
|
|
852
|
+
'requestContextDigest',
|
|
853
|
+
])
|
|
854
|
+
|| admission.schemaVersion !== 1
|
|
855
|
+
|| !isIdentifier(admission.recipientKeyId)
|
|
856
|
+
|| !isPositiveSafeInteger(admission.recipientGeneration)
|
|
857
|
+
|| typeof admission.envelopeDigest !== 'string'
|
|
858
|
+
|| !isSha256Digest(admission.envelopeDigest)
|
|
859
|
+
|| typeof admission.requestContextDigest !== 'string'
|
|
860
|
+
|| !isSha256Digest(admission.requestContextDigest)) {
|
|
861
|
+
errors.push('corestore credential publication ingress admission binding must be canonical');
|
|
862
|
+
} else if (admission.recipientKeyId !== expectedArg.request.expectedIngressRecipientKeyId
|
|
863
|
+
|| admission.recipientGeneration !== expectedArg.request.expectedIngressRecipientGeneration
|
|
864
|
+
|| !await verifyPublicationAdmissionBinding(
|
|
865
|
+
admission as unknown as ISecretEnvelopeAdmissionBindingV1,
|
|
866
|
+
expectedArg.request.envelope,
|
|
867
|
+
createPublishCorestoreCredentialMaterialEnvelopeContext(expectedArg.request),
|
|
868
|
+
)) {
|
|
869
|
+
errors.push('corestore credential publication ingress admission binding must match the request');
|
|
870
|
+
}
|
|
871
|
+
if (!isPositiveSafeInteger(receiptArg.acceptedAt)
|
|
872
|
+
|| receiptArg.acceptedAt < grant.issuedAt
|
|
873
|
+
|| receiptArg.acceptedAt >= grant.expiresAt
|
|
874
|
+
|| typeof receiptArg.replayed !== 'boolean') {
|
|
875
|
+
errors.push('corestore credential publication receipt acceptance must be canonical');
|
|
876
|
+
}
|
|
877
|
+
return errors;
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
export const corestoreCredentialPublicationErrorMetadata = Object.freeze({
|
|
881
|
+
INVALID_IDENTITY: Object.freeze({ retryable: false }),
|
|
882
|
+
INVALID_REQUEST: Object.freeze({ retryable: false }),
|
|
883
|
+
GRANT_EXPIRED: Object.freeze({ retryable: false }),
|
|
884
|
+
GRANT_SCOPE_MISMATCH: Object.freeze({ retryable: false }),
|
|
885
|
+
GRANT_REVISION_MISMATCH: Object.freeze({ retryable: true }),
|
|
886
|
+
INGRESS_RECIPIENT_MISMATCH: Object.freeze({ retryable: true }),
|
|
887
|
+
ENVELOPE_INVALID: Object.freeze({ retryable: false }),
|
|
888
|
+
MATERIAL_INVALID: Object.freeze({ retryable: false }),
|
|
889
|
+
SECRETS_REVISION_CONFLICT: Object.freeze({ retryable: true }),
|
|
890
|
+
REPLAY_CONFLICT: Object.freeze({ retryable: false }),
|
|
891
|
+
INTERNAL_ERROR: Object.freeze({ retryable: true }),
|
|
892
|
+
} as const);
|
|
893
|
+
|
|
894
|
+
export type TCorestoreCredentialPublicationErrorCode =
|
|
895
|
+
keyof typeof corestoreCredentialPublicationErrorMetadata;
|
|
896
|
+
|
|
897
|
+
export interface ICorestoreCredentialPublicationErrorMetadataV1 {
|
|
898
|
+
schemaVersion: 1;
|
|
899
|
+
code: TCorestoreCredentialPublicationErrorCode;
|
|
900
|
+
retryable: boolean;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
export const getCorestoreCredentialPublicationErrorMetadata = (
|
|
904
|
+
codeArg: TCorestoreCredentialPublicationErrorCode,
|
|
905
|
+
): ICorestoreCredentialPublicationErrorMetadataV1 => ({
|
|
906
|
+
schemaVersion: 1,
|
|
907
|
+
code: codeArg,
|
|
908
|
+
retryable: corestoreCredentialPublicationErrorMetadata[codeArg].retryable,
|
|
909
|
+
});
|