@calimero-network/mero-js 19.27.1 → 20.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin-api/admin-types.d.ts +5 -2
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +3 -3
- package/dist/index.cjs +73 -6
- package/dist/index.cjs.map +2 -2
- package/dist/index.mjs +73 -6
- package/dist/index.mjs.map +2 -2
- package/dist/sealed/index.d.ts +2 -2
- package/dist/sealed/index.js +1 -1
- package/dist/sealed/verify.d.ts +69 -6
- package/dist/sealed/verify.js +97 -8
- package/package.json +1 -1
package/dist/sealed/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { HANDSHAKE_PATH, SEALED_CONTENT_TYPE, SEALED_PATH, SealedTransportError, StaleTransportKeyError, createAttestedSealedFetch, createSealedFetch, fetchAttestedTransportKey, transportKeyBinding, } from './sealed.js';
|
|
2
2
|
export type { AttestedSealedFetchOptions, SealedFetchOptions, VerifyTransportQuote } from './sealed.js';
|
|
3
|
-
export { createQuoteVerifier } from './verify.js';
|
|
4
|
-
export type { DcapCollateral, DcapVerifiedReport, DcapVerify, QuoteVerifier, QuoteVerifierOptions, } from './verify.js';
|
|
3
|
+
export { createQuoteVerifier, trustedMeasurementsFromReleases } from './verify.js';
|
|
4
|
+
export type { DcapCollateral, DcapVerifiedReport, DcapVerify, PublishedMrtds, QuoteVerifier, QuoteVerifierOptions, TrustedMeasurements, } from './verify.js';
|
package/dist/sealed/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { HANDSHAKE_PATH, SEALED_CONTENT_TYPE, SEALED_PATH, SealedTransportError, StaleTransportKeyError, createAttestedSealedFetch, createSealedFetch, fetchAttestedTransportKey, transportKeyBinding, } from './sealed.js';
|
|
2
|
-
export { createQuoteVerifier } from './verify.js';
|
|
2
|
+
export { createQuoteVerifier, trustedMeasurementsFromReleases } from './verify.js';
|
package/dist/sealed/verify.d.ts
CHANGED
|
@@ -49,15 +49,33 @@ export interface DcapVerifiedReport {
|
|
|
49
49
|
* `@phala/dcap-qvl` has exactly this shape.
|
|
50
50
|
*/
|
|
51
51
|
export type DcapVerify = (quote: Uint8Array, collateral: DcapCollateral, nowSecs: number) => DcapVerifiedReport;
|
|
52
|
+
/**
|
|
53
|
+
* One image's measurements, all five registers, hex. A quote matches it only
|
|
54
|
+
* when every register does, so measurements of different images never combine.
|
|
55
|
+
*/
|
|
56
|
+
export interface TrustedMeasurements {
|
|
57
|
+
mrtd: string;
|
|
58
|
+
rtmr0: string;
|
|
59
|
+
rtmr1: string;
|
|
60
|
+
rtmr2: string;
|
|
61
|
+
rtmr3: string;
|
|
62
|
+
}
|
|
52
63
|
export interface QuoteVerifierOptions {
|
|
53
64
|
/** DCAP verification: `verify` from `@phala/dcap-qvl`. */
|
|
54
65
|
dcapVerify: DcapVerify;
|
|
55
66
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
67
|
+
* The images trusted, each by all of its measurements. The way to say which
|
|
68
|
+
* code may read the traffic: build it with {@link trustedMeasurementsFromReleases}
|
|
69
|
+
* from the `published-mrtds.json` of the releases you trust.
|
|
70
|
+
*/
|
|
71
|
+
allowedMeasurements?: TrustedMeasurements[];
|
|
72
|
+
/**
|
|
73
|
+
* MRTDs (hex) accepted. On its own this does not name an image: on GCP the
|
|
74
|
+
* MRTD measures the platform's TD firmware, which every image shares, and
|
|
75
|
+
* the image is in RTMR1–3. So a verifier needs either
|
|
76
|
+
* {@link allowedMeasurements}, or this with all of `allowedRtmr1`–`3`.
|
|
59
77
|
*/
|
|
60
|
-
allowedMrtd
|
|
78
|
+
allowedMrtd?: string[];
|
|
61
79
|
/** RTMR allowlists (hex); a register is not checked when its list is left out. */
|
|
62
80
|
allowedRtmr0?: string[];
|
|
63
81
|
allowedRtmr1?: string[];
|
|
@@ -90,7 +108,8 @@ export type QuoteVerifier = VerifyTransportQuote & {
|
|
|
90
108
|
* now;
|
|
91
109
|
* - the platform's TCB status is one allowed (`UpToDate` by default), and
|
|
92
110
|
* never `Revoked`;
|
|
93
|
-
* -
|
|
111
|
+
* - its measurements are those of a trusted image: one of
|
|
112
|
+
* `allowedMeasurements` in full, and the MRTD and any RTMRs listed;
|
|
94
113
|
* - its report data is the nonce followed by the binding the client computed.
|
|
95
114
|
*
|
|
96
115
|
* Any failure rejects with the reason; nothing is trusted from a quote that
|
|
@@ -99,7 +118,10 @@ export type QuoteVerifier = VerifyTransportQuote & {
|
|
|
99
118
|
* ```ts
|
|
100
119
|
* import { verify as dcapVerify } from '@phala/dcap-qvl';
|
|
101
120
|
*
|
|
102
|
-
* const verifier = createQuoteVerifier({
|
|
121
|
+
* const verifier = createQuoteVerifier({
|
|
122
|
+
* dcapVerify,
|
|
123
|
+
* ...trustedMeasurementsFromReleases([publishedMrtds], { profile: 'locked-read-only' }),
|
|
124
|
+
* });
|
|
103
125
|
* const fetch = createSealedFetch({
|
|
104
126
|
* baseUrl,
|
|
105
127
|
* transportPublicKey: () => fetchAttestedTransportKey(admin, verifier),
|
|
@@ -107,3 +129,44 @@ export type QuoteVerifier = VerifyTransportQuote & {
|
|
|
107
129
|
* ```
|
|
108
130
|
*/
|
|
109
131
|
export declare function createQuoteVerifier(options: QuoteVerifierOptions): QuoteVerifier;
|
|
132
|
+
/**
|
|
133
|
+
* A mero-tee node release's `published-mrtds.json`: the measurements the
|
|
134
|
+
* release workflow observed booting each profile of its image on TDX, signed
|
|
135
|
+
* with the release. Only the fields read here are typed.
|
|
136
|
+
*/
|
|
137
|
+
export interface PublishedMrtds {
|
|
138
|
+
role?: string;
|
|
139
|
+
tag?: string;
|
|
140
|
+
profiles: Record<string, {
|
|
141
|
+
mrtd: string;
|
|
142
|
+
rtmr0: string;
|
|
143
|
+
rtmr1: string;
|
|
144
|
+
rtmr2: string;
|
|
145
|
+
rtmr3: string;
|
|
146
|
+
allowed_tcb_statuses?: string[];
|
|
147
|
+
}>;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The {@link QuoteVerifierOptions} that trust exactly one profile of the given
|
|
151
|
+
* releases: each release's image by all of its measurements, and only the TCB
|
|
152
|
+
* statuses every one of those releases accepts.
|
|
153
|
+
*
|
|
154
|
+
* Pass every release a node you talk to may run. During a rollout that is the
|
|
155
|
+
* old one and the new one; drop the old one once no node runs it.
|
|
156
|
+
*
|
|
157
|
+
* ```ts
|
|
158
|
+
* import published from './trusted/mero-tee-v2.3.78.published-mrtds.json';
|
|
159
|
+
*
|
|
160
|
+
* const verifier = createQuoteVerifier({
|
|
161
|
+
* dcapVerify,
|
|
162
|
+
* ...trustedMeasurementsFromReleases([published], { profile: 'locked-read-only' }),
|
|
163
|
+
* });
|
|
164
|
+
* ```
|
|
165
|
+
*
|
|
166
|
+
* Where the file comes from is the trust decision. Shipping it with the app
|
|
167
|
+
* (checked out of a release you verified) trusts that release; fetching it at
|
|
168
|
+
* run time trusts whoever serves it.
|
|
169
|
+
*/
|
|
170
|
+
export declare function trustedMeasurementsFromReleases(releases: PublishedMrtds[], { profile }: {
|
|
171
|
+
profile: string;
|
|
172
|
+
}): Required<Pick<QuoteVerifierOptions, 'allowedMeasurements' | 'allowedTcbStatuses'>>;
|
package/dist/sealed/verify.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { hex } from '../crypto/internal.js';
|
|
20
20
|
const MEASUREMENT_HEX = /^[0-9a-f]{96}$/;
|
|
21
|
+
const REGISTERS = ['mrtd', 'rtmr0', 'rtmr1', 'rtmr2', 'rtmr3'];
|
|
21
22
|
/**
|
|
22
23
|
* A verifier that checks a TEE node's quote here, in the page, and accepts it
|
|
23
24
|
* only when every check passes:
|
|
@@ -26,7 +27,8 @@ const MEASUREMENT_HEX = /^[0-9a-f]{96}$/;
|
|
|
26
27
|
* now;
|
|
27
28
|
* - the platform's TCB status is one allowed (`UpToDate` by default), and
|
|
28
29
|
* never `Revoked`;
|
|
29
|
-
* -
|
|
30
|
+
* - its measurements are those of a trusted image: one of
|
|
31
|
+
* `allowedMeasurements` in full, and the MRTD and any RTMRs listed;
|
|
30
32
|
* - its report data is the nonce followed by the binding the client computed.
|
|
31
33
|
*
|
|
32
34
|
* Any failure rejects with the reason; nothing is trusted from a quote that
|
|
@@ -35,7 +37,10 @@ const MEASUREMENT_HEX = /^[0-9a-f]{96}$/;
|
|
|
35
37
|
* ```ts
|
|
36
38
|
* import { verify as dcapVerify } from '@phala/dcap-qvl';
|
|
37
39
|
*
|
|
38
|
-
* const verifier = createQuoteVerifier({
|
|
40
|
+
* const verifier = createQuoteVerifier({
|
|
41
|
+
* dcapVerify,
|
|
42
|
+
* ...trustedMeasurementsFromReleases([publishedMrtds], { profile: 'locked-read-only' }),
|
|
43
|
+
* });
|
|
39
44
|
* const fetch = createSealedFetch({
|
|
40
45
|
* baseUrl,
|
|
41
46
|
* transportPublicKey: () => fetchAttestedTransportKey(admin, verifier),
|
|
@@ -43,16 +48,29 @@ const MEASUREMENT_HEX = /^[0-9a-f]{96}$/;
|
|
|
43
48
|
* ```
|
|
44
49
|
*/
|
|
45
50
|
export function createQuoteVerifier(options) {
|
|
46
|
-
const allowedMrtd = measurements(options.allowedMrtd, 'allowedMrtd');
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
const allowedMrtd = measurements(options.allowedMrtd ?? [], 'allowedMrtd');
|
|
52
|
+
const allowedImages = (options.allowedMeasurements ?? []).map((image, index) => {
|
|
53
|
+
const label = `allowedMeasurements[${index}]`;
|
|
54
|
+
return REGISTERS.map((register) => measurements([image?.[register] ?? ''], `${label}.${register}`)[0]).join();
|
|
55
|
+
});
|
|
50
56
|
const allowedRtmr = [
|
|
51
57
|
measurements(options.allowedRtmr0 ?? [], 'allowedRtmr0'),
|
|
52
58
|
measurements(options.allowedRtmr1 ?? [], 'allowedRtmr1'),
|
|
53
59
|
measurements(options.allowedRtmr2 ?? [], 'allowedRtmr2'),
|
|
54
60
|
measurements(options.allowedRtmr3 ?? [], 'allowedRtmr3'),
|
|
55
61
|
];
|
|
62
|
+
if (options.allowedMeasurements !== undefined && allowedImages.length === 0) {
|
|
63
|
+
throw new Error('allowedMeasurements is empty, so no quote would ever be accepted');
|
|
64
|
+
}
|
|
65
|
+
if (allowedImages.length === 0) {
|
|
66
|
+
if (allowedMrtd.length === 0) {
|
|
67
|
+
throw new Error('No image is trusted: give allowedMeasurements, or allowedMrtd with allowedRtmr1-3');
|
|
68
|
+
}
|
|
69
|
+
if (allowedRtmr.slice(1).some((list) => list.length === 0)) {
|
|
70
|
+
throw new Error('allowedMrtd alone does not name an image: the MRTD measures the TD firmware, which ' +
|
|
71
|
+
'images share, and the image is in RTMR1-3. Give allowedMeasurements, or allowedRtmr1-3 too');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
56
74
|
const allowedStatuses = options.allowedTcbStatuses ?? ['UpToDate'];
|
|
57
75
|
const now = options.now ?? Date.now;
|
|
58
76
|
const verifier = async ({ quoteB64, nonce, reportDataSuffix, collateral, }) => {
|
|
@@ -72,8 +90,12 @@ export function createQuoteVerifier(options) {
|
|
|
72
90
|
throw new Error(`The platform's TCB status is ${verified.status}, which is not accepted`);
|
|
73
91
|
}
|
|
74
92
|
const td = tdReport(verified.report);
|
|
75
|
-
|
|
76
|
-
|
|
93
|
+
const measured = [td.mrTd, td.rtMr0, td.rtMr1, td.rtMr2, td.rtMr3].map((register) => hex(register));
|
|
94
|
+
if (allowedImages.length > 0 && !allowedImages.includes(measured.join())) {
|
|
95
|
+
throw new Error(`The measurements (MRTD ${measured[0]}, RTMR0-3 ${measured.slice(1).join(', ')}) are not an image this verifier trusts`);
|
|
96
|
+
}
|
|
97
|
+
if (allowedMrtd.length > 0 && !allowedMrtd.includes(measured[0])) {
|
|
98
|
+
throw new Error(`MRTD ${measured[0]} is not an image this verifier trusts`);
|
|
77
99
|
}
|
|
78
100
|
const registers = [td.rtMr0, td.rtMr1, td.rtMr2, td.rtMr3];
|
|
79
101
|
registers.forEach((register, index) => {
|
|
@@ -114,3 +136,70 @@ function fromBase64(value) {
|
|
|
114
136
|
bytes[i] = binary.charCodeAt(i);
|
|
115
137
|
return bytes;
|
|
116
138
|
}
|
|
139
|
+
/** TCB statuses as DCAP reports them; the release lists them in lower case. */
|
|
140
|
+
const TCB_STATUSES = [
|
|
141
|
+
'UpToDate',
|
|
142
|
+
'SWHardeningNeeded',
|
|
143
|
+
'ConfigurationNeeded',
|
|
144
|
+
'ConfigurationAndSWHardeningNeeded',
|
|
145
|
+
'OutOfDate',
|
|
146
|
+
'OutOfDateConfigurationNeeded',
|
|
147
|
+
];
|
|
148
|
+
/**
|
|
149
|
+
* The {@link QuoteVerifierOptions} that trust exactly one profile of the given
|
|
150
|
+
* releases: each release's image by all of its measurements, and only the TCB
|
|
151
|
+
* statuses every one of those releases accepts.
|
|
152
|
+
*
|
|
153
|
+
* Pass every release a node you talk to may run. During a rollout that is the
|
|
154
|
+
* old one and the new one; drop the old one once no node runs it.
|
|
155
|
+
*
|
|
156
|
+
* ```ts
|
|
157
|
+
* import published from './trusted/mero-tee-v2.3.78.published-mrtds.json';
|
|
158
|
+
*
|
|
159
|
+
* const verifier = createQuoteVerifier({
|
|
160
|
+
* dcapVerify,
|
|
161
|
+
* ...trustedMeasurementsFromReleases([published], { profile: 'locked-read-only' }),
|
|
162
|
+
* });
|
|
163
|
+
* ```
|
|
164
|
+
*
|
|
165
|
+
* Where the file comes from is the trust decision. Shipping it with the app
|
|
166
|
+
* (checked out of a release you verified) trusts that release; fetching it at
|
|
167
|
+
* run time trusts whoever serves it.
|
|
168
|
+
*/
|
|
169
|
+
export function trustedMeasurementsFromReleases(releases, { profile }) {
|
|
170
|
+
if (releases.length === 0) {
|
|
171
|
+
throw new Error('No release given, so no image would be trusted');
|
|
172
|
+
}
|
|
173
|
+
let statuses;
|
|
174
|
+
const allowedMeasurements = releases.map((release, index) => {
|
|
175
|
+
const name = release?.tag ?? `releases[${index}]`;
|
|
176
|
+
if (release?.role !== undefined && release.role !== 'node') {
|
|
177
|
+
throw new Error(`${name} lists the measurements of a ${release.role}, not of a node image`);
|
|
178
|
+
}
|
|
179
|
+
const image = release?.profiles?.[profile];
|
|
180
|
+
if (!image) {
|
|
181
|
+
const known = Object.keys(release?.profiles ?? {}).join(', ') || 'none';
|
|
182
|
+
throw new Error(`${name} has no profile ${JSON.stringify(profile)} (it has: ${known})`);
|
|
183
|
+
}
|
|
184
|
+
const accepted = (image.allowed_tcb_statuses ?? ['uptodate']).map((status) => tcbStatus(status, name));
|
|
185
|
+
statuses = statuses === undefined ? accepted : statuses.filter((status) => accepted.includes(status));
|
|
186
|
+
return {
|
|
187
|
+
mrtd: image.mrtd,
|
|
188
|
+
rtmr0: image.rtmr0,
|
|
189
|
+
rtmr1: image.rtmr1,
|
|
190
|
+
rtmr2: image.rtmr2,
|
|
191
|
+
rtmr3: image.rtmr3,
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
if (!statuses || statuses.length === 0) {
|
|
195
|
+
throw new Error('The releases given accept no TCB status in common');
|
|
196
|
+
}
|
|
197
|
+
return { allowedMeasurements, allowedTcbStatuses: statuses };
|
|
198
|
+
}
|
|
199
|
+
function tcbStatus(status, release) {
|
|
200
|
+
const known = TCB_STATUSES.find((candidate) => candidate.toLowerCase() === String(status).toLowerCase());
|
|
201
|
+
if (!known) {
|
|
202
|
+
throw new Error(`${release} accepts TCB status ${JSON.stringify(status)}, which this verifier does not know`);
|
|
203
|
+
}
|
|
204
|
+
return known;
|
|
205
|
+
}
|