@serve.zone/interfaces 32.47.0 → 32.49.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 +66 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/appstore/catalog.d.ts +300 -0
- package/dist_ts/appstore/catalog.js +544 -0
- package/dist_ts/appstore/index.d.ts +2 -0
- package/dist_ts/appstore/index.js +3 -1
- package/dist_ts/appstore/types.d.ts +9 -0
- package/dist_ts/appstore/types.js +1 -1
- package/dist_ts/appstore/versionconfig.d.ts +21 -0
- package/dist_ts/appstore/versionconfig.js +685 -0
- package/dist_ts/data/networkprojection.js +3 -3
- package/dist_ts/data/privatenetwork.d.ts +14 -1
- package/dist_ts/data/privatenetwork.js +30 -11
- package/dist_ts/private/keylist.d.ts +5 -0
- package/dist_ts/private/keylist.js +2 -0
- package/dist_ts/requests/appstore.d.ts +30 -1
- package/dist_ts/runtime.appstorecatalog.d.ts +46 -0
- package/dist_ts/runtime.appstorecatalog.js +142 -0
- package/dist_ts/runtime.d.ts +1 -0
- package/dist_ts/runtime.js +2 -1
- package/package.json +1 -1
- package/readme.md +65 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/appstore/catalog.ts +851 -0
- package/ts/appstore/index.ts +2 -0
- package/ts/appstore/types.ts +9 -0
- package/ts/appstore/versionconfig.ts +803 -0
- package/ts/data/networkprojection.ts +4 -2
- package/ts/data/privatenetwork.ts +34 -10
- package/ts/private/keylist.ts +6 -0
- package/ts/requests/appstore.ts +38 -0
- package/ts/runtime.appstorecatalog.ts +195 -0
- package/ts/runtime.ts +1 -0
|
@@ -666,7 +666,8 @@ export const validateRuntimeNetworkProjection = async (value: unknown): Promise<
|
|
|
666
666
|
}
|
|
667
667
|
const members = new Map(projection.memberships.map(member => [ownerKey(member), member])), aliases = new Set<string>();
|
|
668
668
|
for (const member of projection.memberships) {
|
|
669
|
-
composeServicePrivateNetworkDnsPolicy(member, member.attachments.map(attachment => networks.get(attachment.networkId))
|
|
669
|
+
composeServicePrivateNetworkDnsPolicy(member, member.attachments.map(attachment => networks.get(attachment.networkId)),
|
|
670
|
+
current.resolvers);
|
|
670
671
|
for (const attachment of member.attachments) for (const alias of attachment.aliases) {
|
|
671
672
|
const name = buildPrivateNetworkFqdn(networks.get(attachment.networkId), alias);
|
|
672
673
|
requireNetwork(!aliases.has(name)); aliases.add(name);
|
|
@@ -756,7 +757,8 @@ export const composeRuntimeNetworkProjectionDnsViews = async (
|
|
|
756
757
|
const members = new Map(projection.memberships.map(member => [ownerKey(member), member]));
|
|
757
758
|
return local.filter(endpoint => selected === null || selected.some(source => networkEqual(networkReferenceOf(endpoint.lease), source))).map(endpoint => {
|
|
758
759
|
const member = members.get(ownerKey(endpoint.lease))!;
|
|
759
|
-
const policy = composeServicePrivateNetworkDnsPolicy(member,
|
|
760
|
+
const policy = composeServicePrivateNetworkDnsPolicy(member,
|
|
761
|
+
member.attachments.map(attachment => networks.get(attachment.networkId)), projection.protectedAuthority.resolvers);
|
|
760
762
|
const records: IRuntimeNetworkDnsView['records'] = [];
|
|
761
763
|
for (const target of projection.memberships) {
|
|
762
764
|
if (target.organizationId !== member.organizationId) continue;
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
readDataElements, readDataProperty, readExactDataRecord,
|
|
3
3
|
} from '../private/dataproperties.js';
|
|
4
4
|
import { isPlausibleUnicastIpv4 } from '../private/ipv4.js';
|
|
5
|
+
import { runtimeNetworkAuthorityContract } from './networkauthority.js';
|
|
5
6
|
|
|
6
7
|
/** Cloudly owns these definitions. Membership never follows node placement. */
|
|
7
8
|
export interface IPrivateNetworkPolicy {
|
|
@@ -126,6 +127,13 @@ const ipv4 = (value: unknown): string => {
|
|
|
126
127
|
requireValue(isPlausibleUnicastIpv4(value));
|
|
127
128
|
return value;
|
|
128
129
|
};
|
|
130
|
+
const endpoint = (value: Record<string, unknown>): { address: string; port: number } => {
|
|
131
|
+
const port = counter(value.port);
|
|
132
|
+
requireValue(port <= 65535);
|
|
133
|
+
return { address: ipv4(value.address), port };
|
|
134
|
+
};
|
|
135
|
+
const byEndpoint = (left: { address: string; port: number }, right: { address: string; port: number }): number =>
|
|
136
|
+
left.address < right.address ? -1 : left.address > right.address ? 1 : left.port - right.port;
|
|
129
137
|
const publicSuffix = (value: unknown): string => {
|
|
130
138
|
requireValue(typeof value === 'string' && value.length <= 253 && value.endsWith('.'));
|
|
131
139
|
const labels = value.slice(0, -1).split('.');
|
|
@@ -141,12 +149,8 @@ export const snapshotPrivateNetworkPolicy = (value: unknown): IPrivateNetworkPol
|
|
|
141
149
|
requireValue(policy.connectivity === 'member-full-mesh');
|
|
142
150
|
if (policy.externalDns === null) return { connectivity: 'member-full-mesh', externalDns: null };
|
|
143
151
|
const external = object(policy.externalDns, ['upstreams', 'allowAnyPublicName', 'permittedSuffixes']);
|
|
144
|
-
const upstreams = array(external.upstreams, privateNetworkContract.maximumUpstreams,
|
|
145
|
-
|
|
146
|
-
const port = counter(upstream.port);
|
|
147
|
-
requireValue(port <= 65535);
|
|
148
|
-
return { address: ipv4(upstream.address), port };
|
|
149
|
-
});
|
|
152
|
+
const upstreams = array(external.upstreams, privateNetworkContract.maximumUpstreams,
|
|
153
|
+
entry => endpoint(object(entry, ['address', 'port'])));
|
|
150
154
|
requireValue(upstreams.length > 0 && new Set(upstreams.map(upstream => `${upstream.address}:${upstream.port}`)).size === upstreams.length);
|
|
151
155
|
requireValue(typeof external.allowAnyPublicName === 'boolean');
|
|
152
156
|
const permittedSuffixes = sorted(array(external.permittedSuffixes, privateNetworkContract.maximumPermittedSuffixes, publicSuffix));
|
|
@@ -223,6 +227,11 @@ export interface IServicePrivateNetworkDnsPolicy {
|
|
|
223
227
|
networkSuffixes: Array<{ networkId: string; networkRevision: number; dnsSuffix: string }>;
|
|
224
228
|
/** Empty or exactly the selected default network; never an ordered network union. */
|
|
225
229
|
searchDomains: string[];
|
|
230
|
+
/**
|
|
231
|
+
* The networks' intersection for an attached service. A service attached to no
|
|
232
|
+
* network forwards any public name to the authority's resolvers, or has none
|
|
233
|
+
* when the authority declares no resolver.
|
|
234
|
+
*/
|
|
226
235
|
externalDns: {
|
|
227
236
|
upstreams: Array<{ address: string; port: number }>;
|
|
228
237
|
allowAnyPublicName: boolean;
|
|
@@ -243,12 +252,23 @@ const minimalSuffixes = (values: string[]): string[] => {
|
|
|
243
252
|
* membership authentication, endpoint readiness, a DNS lease or a packet grant.
|
|
244
253
|
* The caller must fence these exact revisions before issuing runtime authority.
|
|
245
254
|
* An inactive/missing/extra network is rejected, never silently omitted.
|
|
255
|
+
*
|
|
256
|
+
* `resolvers` is the complete resolver list of the protected authority the
|
|
257
|
+
* membership is projected under. It decides external resolution only for a
|
|
258
|
+
* service attached to no private network: such a service forwards any public
|
|
259
|
+
* name to the first `maximumUpstreams` of those resolvers in address and port
|
|
260
|
+
* order, and an authority declaring none leaves it without external resolution.
|
|
261
|
+
* An attached service is decided by its networks alone, so a network denying
|
|
262
|
+
* external DNS keeps denying it whatever the authority declares.
|
|
246
263
|
*/
|
|
247
264
|
export const composeServicePrivateNetworkDnsPolicy = (
|
|
248
|
-
membershipValue: unknown, networksValue: unknown,
|
|
265
|
+
membershipValue: unknown, networksValue: unknown, resolversValue: unknown,
|
|
249
266
|
): IServicePrivateNetworkDnsPolicy => safe(() => {
|
|
250
267
|
const member = snapshotServicePrivateNetworks(membershipValue);
|
|
251
268
|
const networks = array(networksValue, privateNetworkContract.maximumAttachments, snapshotPrivateNetwork);
|
|
269
|
+
const resolvers = array(resolversValue, runtimeNetworkAuthorityContract.maximumResolvers,
|
|
270
|
+
entry => endpoint(object(entry, ['id', 'address', 'port']))).sort(byEndpoint);
|
|
271
|
+
requireValue(new Set(resolvers.map(resolver => `${resolver.address}:${resolver.port}`)).size === resolvers.length);
|
|
252
272
|
requireValue(networks.length === member.attachments.length
|
|
253
273
|
&& new Set(networks.map(network => network.id)).size === networks.length
|
|
254
274
|
&& new Set(networks.map(network => network.networkDnsKey)).size === networks.length
|
|
@@ -261,11 +281,15 @@ export const composeServicePrivateNetworkDnsPolicy = (
|
|
|
261
281
|
const searchDomains = member.defaultNetworkId === null ? []
|
|
262
282
|
: [networkSuffixes.find(network => network.networkId === member.defaultNetworkId)!.dnsSuffix];
|
|
263
283
|
let externalDns: IServicePrivateNetworkDnsPolicy['externalDns'] = null;
|
|
264
|
-
|
|
265
|
-
|
|
284
|
+
const policies: Array<NonNullable<IPrivateNetworkPolicy['externalDns']>> = [];
|
|
285
|
+
for (const network of ordered) if (network.policy.externalDns !== null) policies.push(network.policy.externalDns);
|
|
286
|
+
if (ordered.length === 0 && resolvers.length > 0) {
|
|
287
|
+
externalDns = { upstreams: resolvers.slice(0, privateNetworkContract.maximumUpstreams),
|
|
288
|
+
allowAnyPublicName: true, permittedSuffixes: [] };
|
|
289
|
+
} else if (ordered.length > 0 && policies.length === ordered.length) {
|
|
266
290
|
const upstreams = policies[0].upstreams.filter(upstream => policies.every(policy => policy.upstreams.some(
|
|
267
291
|
candidate => candidate.address === upstream.address && candidate.port === upstream.port,
|
|
268
|
-
))).sort(
|
|
292
|
+
))).sort(byEndpoint);
|
|
269
293
|
// null is the unrestricted public-name set during intersection, not denial.
|
|
270
294
|
let permittedSuffixes: string[] | null = null;
|
|
271
295
|
for (const policy of policies) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A key list that names every key of a record type, so a member added to the type cannot be
|
|
3
|
+
* forgotten by a validator built on the list: an incomplete list does not type-check.
|
|
4
|
+
*/
|
|
5
|
+
export type TCompleteKeyList<TRecord, TKeys extends readonly (keyof TRecord)[]> =
|
|
6
|
+
Exclude<keyof TRecord, TKeys[number]> extends never ? TKeys : never;
|
package/ts/requests/appstore.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { IService } from '../data/service.js';
|
|
|
4
4
|
import type {
|
|
5
5
|
IAppStoreApp,
|
|
6
6
|
IAppStoreAppMeta,
|
|
7
|
+
TAppStoreCatalogStatus,
|
|
7
8
|
IAppStoreInstallRequest,
|
|
8
9
|
IAppStoreVersionConfig,
|
|
9
10
|
IUpgradeableAppStoreService,
|
|
@@ -71,6 +72,43 @@ export interface IReq_Any_GetAppStoreTemplates extends plugins.typedrequestInter
|
|
|
71
72
|
};
|
|
72
73
|
response: {
|
|
73
74
|
apps: IAppStoreApp[];
|
|
75
|
+
/**
|
|
76
|
+
* The catalog state behind `apps`. Optional while consumers still serve a bundled catalog;
|
|
77
|
+
* a consumer that loads the catalog at runtime always answers it.
|
|
78
|
+
*/
|
|
79
|
+
catalogStatus?: TAppStoreCatalogStatus;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The App Store catalog's state: availability, the active catalog, the last attempt and refusal. */
|
|
84
|
+
export interface IReq_Any_GetAppStoreCatalogStatus extends plugins.typedrequestInterfaces.implementsTR<
|
|
85
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
86
|
+
IReq_Any_GetAppStoreCatalogStatus
|
|
87
|
+
> {
|
|
88
|
+
method: 'getAppStoreCatalogStatus';
|
|
89
|
+
request: {
|
|
90
|
+
identity: IIdentity;
|
|
91
|
+
};
|
|
92
|
+
response: {
|
|
93
|
+
status: TAppStoreCatalogStatus;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Runs one bounded catalog load attempt now, outside the refresh schedule, and answers the state
|
|
99
|
+
* after it. The attempt's outcome is `status.lastAttempt`; a refused or failed attempt keeps the
|
|
100
|
+
* active catalog in place and is not a request error.
|
|
101
|
+
*/
|
|
102
|
+
export interface IReq_Any_RefreshAppStoreCatalog extends plugins.typedrequestInterfaces.implementsTR<
|
|
103
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
104
|
+
IReq_Any_RefreshAppStoreCatalog
|
|
105
|
+
> {
|
|
106
|
+
method: 'refreshAppStoreCatalog';
|
|
107
|
+
request: {
|
|
108
|
+
identity: IIdentity;
|
|
109
|
+
};
|
|
110
|
+
response: {
|
|
111
|
+
status: TAppStoreCatalogStatus;
|
|
74
112
|
};
|
|
75
113
|
}
|
|
76
114
|
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ed25519 signing and verification of App Store catalog envelopes (Node-only).
|
|
3
|
+
*
|
|
4
|
+
* A consumer authenticates before it interprets: the envelope's digest and signature are checked
|
|
5
|
+
* before the catalog's contract and schema are read.
|
|
6
|
+
*/
|
|
7
|
+
import * as plugins from './plugins.runtime.js';
|
|
8
|
+
import type { TSha256Digest } from './data/immutableimage.js';
|
|
9
|
+
import { isSha256Digest } from './data/immutableimage.js';
|
|
10
|
+
import { canonicalizeStrictJson } from './private/canonicaljson.js';
|
|
11
|
+
import { readDataElements, readDataProperties } from './private/dataproperties.js';
|
|
12
|
+
import {
|
|
13
|
+
appStoreCatalogContract,
|
|
14
|
+
computeAppStoreCatalogDigest,
|
|
15
|
+
computeAppStoreCatalogSigningKeyId,
|
|
16
|
+
createAppStoreCatalogSignaturePayload,
|
|
17
|
+
findAppStoreCatalogConfigDigestMismatch,
|
|
18
|
+
isAppStoreCatalogEnvelope,
|
|
19
|
+
judgeAppStoreCatalogContract,
|
|
20
|
+
validateAppStoreCatalog,
|
|
21
|
+
validateAppStoreCatalogEnvelope,
|
|
22
|
+
validateAppStoreCatalogEnvelopeShape,
|
|
23
|
+
} from './appstore/catalog.js';
|
|
24
|
+
import type {
|
|
25
|
+
IAppStoreCatalog,
|
|
26
|
+
IAppStoreCatalogEnvelope,
|
|
27
|
+
IAppStoreCatalogRefusal,
|
|
28
|
+
IAppStoreCatalogSignature,
|
|
29
|
+
IAppStoreCatalogTrustedKey,
|
|
30
|
+
IAppStoreCatalogValidationOptions,
|
|
31
|
+
TAppStoreCatalogRefusalReason,
|
|
32
|
+
} from './appstore/catalog.js';
|
|
33
|
+
|
|
34
|
+
/** A catalog signing key. The private half is process-local; keep it in the publisher's secret store. */
|
|
35
|
+
export interface IAppStoreCatalogSigningKey {
|
|
36
|
+
keyId: TSha256Digest;
|
|
37
|
+
/** Raw 32-byte Ed25519 public key, lower-case hex. */
|
|
38
|
+
publicKey: string;
|
|
39
|
+
privateKey: plugins.crypto.KeyObject;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface IAppStoreCatalogVerificationOptions extends IAppStoreCatalogValidationOptions {
|
|
43
|
+
/** The keys this consumer trusts, shipped with its release. */
|
|
44
|
+
trustedKeys: readonly IAppStoreCatalogTrustedKey[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type TAppStoreCatalogVerification =
|
|
48
|
+
| { accepted: true; envelope: IAppStoreCatalogEnvelope; keyId: TSha256Digest }
|
|
49
|
+
| { accepted: false; refusal: IAppStoreCatalogRefusal };
|
|
50
|
+
|
|
51
|
+
const rawPublicKeyOf = (keyArg: plugins.crypto.KeyObject): string => {
|
|
52
|
+
const jwk = keyArg.export({ format: 'jwk' });
|
|
53
|
+
if (jwk.kty !== 'OKP' || jwk.crv !== 'Ed25519' || typeof jwk.x !== 'string') {
|
|
54
|
+
throw new Error('App Store catalog signing key must be an Ed25519 key');
|
|
55
|
+
}
|
|
56
|
+
return Buffer.from(jwk.x, 'base64url').toString('hex');
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const publicKeyObjectOf = (publicKeyArg: string): plugins.crypto.KeyObject =>
|
|
60
|
+
plugins.crypto.createPublicKey({
|
|
61
|
+
format: 'jwk',
|
|
62
|
+
key: { kty: 'OKP', crv: 'Ed25519', x: Buffer.from(publicKeyArg, 'hex').toString('base64url') },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/** A fresh Ed25519 catalog signing key with its key id. */
|
|
66
|
+
export const createAppStoreCatalogSigningKey = async (): Promise<IAppStoreCatalogSigningKey> => {
|
|
67
|
+
const pair = plugins.crypto.generateKeyPairSync('ed25519');
|
|
68
|
+
const publicKey = rawPublicKeyOf(pair.publicKey);
|
|
69
|
+
return { keyId: await computeAppStoreCatalogSigningKeyId(publicKey), publicKey, privateKey: pair.privateKey };
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Signs only a catalog this contract accepts, whose every `configDigest` matches its config.
|
|
74
|
+
*
|
|
75
|
+
* `optionsArg.supportedFeatureIds` should name every feature id the catalog uses, so the
|
|
76
|
+
* publisher judges every version config in full.
|
|
77
|
+
*/
|
|
78
|
+
export const signAppStoreCatalog = async (
|
|
79
|
+
catalogArg: IAppStoreCatalog,
|
|
80
|
+
signingKeysArg: readonly IAppStoreCatalogSigningKey[],
|
|
81
|
+
optionsArg: IAppStoreCatalogValidationOptions,
|
|
82
|
+
): Promise<IAppStoreCatalogEnvelope> => {
|
|
83
|
+
const errors = validateAppStoreCatalog(catalogArg, optionsArg);
|
|
84
|
+
if (errors.length > 0) {
|
|
85
|
+
throw new Error(`App Store catalog refused for signing: ${errors.join('; ')}`);
|
|
86
|
+
}
|
|
87
|
+
const mismatch = await findAppStoreCatalogConfigDigestMismatch(catalogArg);
|
|
88
|
+
if (mismatch !== null) throw new Error(`App Store catalog refused for signing: ${mismatch}`);
|
|
89
|
+
if (signingKeysArg.length === 0) throw new Error('App Store catalog needs at least one signing key');
|
|
90
|
+
const catalogDigest = await computeAppStoreCatalogDigest(catalogArg);
|
|
91
|
+
const payload = createAppStoreCatalogSignaturePayload(catalogDigest);
|
|
92
|
+
const signatures: IAppStoreCatalogSignature[] = [];
|
|
93
|
+
for (const key of signingKeysArg) {
|
|
94
|
+
const { privateKey } = key;
|
|
95
|
+
if (!(privateKey instanceof plugins.crypto.KeyObject) || privateKey.type !== 'private'
|
|
96
|
+
|| privateKey.asymmetricKeyType !== 'ed25519') {
|
|
97
|
+
throw new Error('App Store catalog signing key must be an Ed25519 private key');
|
|
98
|
+
}
|
|
99
|
+
const publicKey = rawPublicKeyOf(plugins.crypto.createPublicKey(privateKey));
|
|
100
|
+
if (publicKey !== key.publicKey || await computeAppStoreCatalogSigningKeyId(publicKey) !== key.keyId) {
|
|
101
|
+
throw new Error(`App Store catalog signing key ${key.keyId} does not match its private key`);
|
|
102
|
+
}
|
|
103
|
+
signatures.push({
|
|
104
|
+
keyId: key.keyId,
|
|
105
|
+
signature: plugins.crypto.sign(null, payload, privateKey).toString('hex'),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return { catalog: structuredClone(catalogArg), catalogDigest, signatures };
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const refuse = (reasonArg: TAppStoreCatalogRefusalReason, messageArg: string): TAppStoreCatalogVerification =>
|
|
112
|
+
({ accepted: false, refusal: { reason: reasonArg, message: messageArg } });
|
|
113
|
+
|
|
114
|
+
const failCanonicalInput = (reasonArg: string): never => {
|
|
115
|
+
throw new Error(reasonArg);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Judges an envelope a consumer fetched or an operator imported.
|
|
120
|
+
*
|
|
121
|
+
* In order: exact envelope shape, `catalogDigest` against the catalog, one valid signature by a
|
|
122
|
+
* trusted key (and no failing one), the contract identity, the catalog schema, every
|
|
123
|
+
* `configDigest`. The accepted envelope is an inert copy of the input. Succession against the
|
|
124
|
+
* active catalog is the caller's next step (`judgeAppStoreCatalogSuccession`).
|
|
125
|
+
*/
|
|
126
|
+
export const verifyAppStoreCatalogEnvelope = async (
|
|
127
|
+
envelopeArg: unknown,
|
|
128
|
+
optionsArg: IAppStoreCatalogVerificationOptions,
|
|
129
|
+
): Promise<TAppStoreCatalogVerification> => {
|
|
130
|
+
let inert: unknown;
|
|
131
|
+
try {
|
|
132
|
+
inert = JSON.parse(canonicalizeStrictJson(envelopeArg, failCanonicalInput, 'envelope'));
|
|
133
|
+
} catch (error) {
|
|
134
|
+
return refuse('schemaInvalid', error instanceof Error ? error.message : 'envelope is not canonical JSON');
|
|
135
|
+
}
|
|
136
|
+
const shapeErrors = validateAppStoreCatalogEnvelopeShape(inert);
|
|
137
|
+
const envelope = readDataProperties(inert);
|
|
138
|
+
const signatures = envelope === null
|
|
139
|
+
? null
|
|
140
|
+
: readDataElements(envelope.signatures, appStoreCatalogContract.maximumSignatures);
|
|
141
|
+
if (shapeErrors.length > 0 || envelope === null || signatures === null
|
|
142
|
+
|| typeof envelope.catalogDigest !== 'string' || !isSha256Digest(envelope.catalogDigest)) {
|
|
143
|
+
return refuse('schemaInvalid', shapeErrors.join('; ') || 'envelope must be an object');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (await computeAppStoreCatalogDigest(envelope.catalog) !== envelope.catalogDigest) {
|
|
147
|
+
return refuse('digestMismatch', 'catalogDigest does not match the catalog');
|
|
148
|
+
}
|
|
149
|
+
const trustedKeys = new Map<string, string>();
|
|
150
|
+
for (const trustedKey of optionsArg.trustedKeys) {
|
|
151
|
+
let keyId: TSha256Digest;
|
|
152
|
+
try {
|
|
153
|
+
keyId = await computeAppStoreCatalogSigningKeyId(trustedKey.publicKey);
|
|
154
|
+
} catch {
|
|
155
|
+
return refuse('signatureInvalid', `trusted key ${trustedKey.keyId} is not a raw Ed25519 public key`);
|
|
156
|
+
}
|
|
157
|
+
if (keyId !== trustedKey.keyId) {
|
|
158
|
+
return refuse('signatureInvalid', `trusted key ${trustedKey.keyId} does not match its public key`);
|
|
159
|
+
}
|
|
160
|
+
trustedKeys.set(keyId, trustedKey.publicKey);
|
|
161
|
+
}
|
|
162
|
+
const payload = createAppStoreCatalogSignaturePayload(envelope.catalogDigest);
|
|
163
|
+
let verifiedKeyId: TSha256Digest | null = null;
|
|
164
|
+
for (const signatureArg of signatures) {
|
|
165
|
+
const signature = readDataProperties(signatureArg);
|
|
166
|
+
if (signature === null || typeof signature.keyId !== 'string' || !isSha256Digest(signature.keyId)
|
|
167
|
+
|| typeof signature.signature !== 'string') {
|
|
168
|
+
return refuse('schemaInvalid', 'envelope.signatures entries must be objects');
|
|
169
|
+
}
|
|
170
|
+
const publicKey = trustedKeys.get(signature.keyId);
|
|
171
|
+
if (publicKey === undefined) continue;
|
|
172
|
+
const valid = plugins.crypto.verify(
|
|
173
|
+
null,
|
|
174
|
+
payload,
|
|
175
|
+
publicKeyObjectOf(publicKey),
|
|
176
|
+
Buffer.from(signature.signature, 'hex'),
|
|
177
|
+
);
|
|
178
|
+
if (!valid) {
|
|
179
|
+
return refuse('signatureInvalid', `signature by trusted key ${signature.keyId} does not verify`);
|
|
180
|
+
}
|
|
181
|
+
verifiedKeyId = signature.keyId;
|
|
182
|
+
}
|
|
183
|
+
if (verifiedKeyId === null) {
|
|
184
|
+
return refuse('signatureInvalid', 'no signature by a trusted key');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const contractRefusal = judgeAppStoreCatalogContract(envelope.catalog);
|
|
188
|
+
if (contractRefusal !== null) return { accepted: false, refusal: contractRefusal };
|
|
189
|
+
if (!isAppStoreCatalogEnvelope(inert, optionsArg)) {
|
|
190
|
+
return refuse('schemaInvalid', validateAppStoreCatalogEnvelope(inert, optionsArg).join('; '));
|
|
191
|
+
}
|
|
192
|
+
const mismatch = await findAppStoreCatalogConfigDigestMismatch(inert.catalog);
|
|
193
|
+
if (mismatch !== null) return refuse('digestMismatch', mismatch);
|
|
194
|
+
return { accepted: true, envelope: inert, keyId: verifiedKeyId };
|
|
195
|
+
};
|
package/ts/runtime.ts
CHANGED
|
@@ -46,6 +46,7 @@ export * from './runtime.corestore.js';
|
|
|
46
46
|
export * from './runtime.corestore.databaseexportpackage.js';
|
|
47
47
|
export * from './runtime.corestore.relay.js';
|
|
48
48
|
export * from './runtime.assignmentsecret.js';
|
|
49
|
+
export * from './runtime.appstorecatalog.js';
|
|
49
50
|
|
|
50
51
|
export interface ISealedResolvedSecretMaterialEntry {
|
|
51
52
|
secretVersionId: string;
|