@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.
@@ -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';
@@ -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';
@@ -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
- * MRTDs (hex) of the images trusted. Required: the MRTD names the image a TD
57
- * booted, and a quote that proves only "some genuine TD" proves nothing
58
- * about which code reads the traffic.
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: string[];
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
- * - the MRTD, and any RTMRs given, are ones allowed;
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({ dcapVerify, allowedMrtd: [trustedMrtd] });
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'>>;
@@ -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
- * - the MRTD, and any RTMRs given, are ones allowed;
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({ dcapVerify, allowedMrtd: [trustedMrtd] });
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
- if (allowedMrtd.length === 0) {
48
- throw new Error('allowedMrtd is empty, so no quote would ever be accepted');
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
- if (!allowedMrtd.includes(hex(td.mrTd))) {
76
- throw new Error(`MRTD ${hex(td.mrTd)} is not an image this verifier trusts`);
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calimero-network/mero-js",
3
- "version": "19.27.1",
3
+ "version": "20.0.0",
4
4
  "description": "Pure JavaScript SDK for Calimero",
5
5
  "type": "module",
6
6
  "exports": {