@calimero-network/mero-js 19.25.0 → 19.26.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-client.d.ts +4 -0
- package/dist/admin-api/admin-client.js +4 -0
- package/dist/admin-api/admin-types.d.ts +18 -0
- package/dist/admin-api/admin-types.js +0 -2
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +101 -5
- package/dist/index.cjs.map +4 -4
- package/dist/index.mjs +101 -5
- package/dist/index.mjs.map +4 -4
- package/dist/sealed/index.d.ts +2 -0
- package/dist/sealed/index.js +1 -0
- package/dist/sealed/noise.js +11 -3
- package/dist/sealed/sealed.d.ts +14 -6
- package/dist/sealed/sealed.js +8 -1
- package/dist/sealed/verify.d.ts +109 -0
- package/dist/sealed/verify.js +116 -0
- package/package.json +2 -1
package/dist/sealed/index.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { HANDSHAKE_PATH, SEALED_CONTENT_TYPE, SEALED_PATH, SealedTransportError, StaleTransportKeyError, createSealedFetch, fetchAttestedTransportKey, transportKeyBinding, } from './sealed.js';
|
|
2
2
|
export type { SealedFetchOptions, VerifyTransportQuote } from './sealed.js';
|
|
3
|
+
export { createQuoteVerifier } from './verify.js';
|
|
4
|
+
export type { DcapCollateral, DcapVerifiedReport, DcapVerify, QuoteVerifier, QuoteVerifierOptions, } from './verify.js';
|
package/dist/sealed/index.js
CHANGED
package/dist/sealed/noise.js
CHANGED
|
@@ -94,9 +94,17 @@ export async function initiate(remoteStatic, prologue, fixedEphemeral) {
|
|
|
94
94
|
}
|
|
95
95
|
export async function x25519KeyPair(secret) {
|
|
96
96
|
if (!secret) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
let pair;
|
|
98
|
+
try {
|
|
99
|
+
pair = (await crypto.subtle.generateKey({ name: 'X25519' }, true, [
|
|
100
|
+
'deriveBits',
|
|
101
|
+
]));
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
throw new Error('Sealed transport needs WebCrypto with X25519, which this runtime lacks ' +
|
|
105
|
+
`(${error?.name ?? 'no crypto.subtle'}). Current browsers and Node have ` +
|
|
106
|
+
'it; in React Native, install react-native-quick-crypto as the global crypto.');
|
|
107
|
+
}
|
|
100
108
|
const publicKey = new Uint8Array(await crypto.subtle.exportKey('raw', pair.publicKey));
|
|
101
109
|
return { privateKey: pair.privateKey, publicKey };
|
|
102
110
|
}
|
package/dist/sealed/sealed.d.ts
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
*/
|
|
38
38
|
import type { TeeAttestRequest, TeeAttestResponseData } from '../admin-api/admin-types.js';
|
|
39
39
|
import { initiate } from './noise.js';
|
|
40
|
+
import type { DcapCollateral } from './verify.js';
|
|
40
41
|
/** Where a session is opened, relative to the node's base URL. */
|
|
41
42
|
export declare const HANDSHAKE_PATH = "/sealed/v2/handshake";
|
|
42
43
|
/** Where sealed requests are posted, relative to the node's base URL. */
|
|
@@ -68,18 +69,25 @@ export declare class SealedTransportError extends Error {
|
|
|
68
69
|
export declare function transportKeyBinding(inner: Uint8Array, transportKey: Uint8Array): Promise<Uint8Array>;
|
|
69
70
|
export interface VerifyTransportQuote {
|
|
70
71
|
/**
|
|
71
|
-
* Verify `quoteB64`
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
72
|
+
* Verify `quoteB64` — never by asking the node being attested — and check
|
|
73
|
+
* that its report data is `nonce || reportDataSuffix` (both hex). Resolve
|
|
74
|
+
* true only when the quote is genuine, its measurements are ones you accept,
|
|
75
|
+
* and the report data matches. {@link createQuoteVerifier} does all of this
|
|
76
|
+
* in the page. `collateral` is what the node sent with the quote, when the
|
|
77
|
+
* verifier asked for it.
|
|
77
78
|
*/
|
|
78
79
|
(args: {
|
|
79
80
|
quoteB64: string;
|
|
80
81
|
nonce: string;
|
|
81
82
|
reportDataSuffix: string;
|
|
83
|
+
collateral?: DcapCollateral;
|
|
82
84
|
}): Promise<boolean>;
|
|
85
|
+
/**
|
|
86
|
+
* Ask the node for the collateral its quote verifies against, and pass it
|
|
87
|
+
* in as `collateral`. Off unless set, because a node that predates the field
|
|
88
|
+
* refuses the request.
|
|
89
|
+
*/
|
|
90
|
+
readonly includeCollateral?: boolean;
|
|
83
91
|
}
|
|
84
92
|
/**
|
|
85
93
|
* Ask a node for its transport key and accept it only if a quote you verified
|
package/dist/sealed/sealed.js
CHANGED
|
@@ -109,6 +109,7 @@ export async function fetchAttestedTransportKey(admin, verify, options = {}) {
|
|
|
109
109
|
nonce,
|
|
110
110
|
applicationId: options.applicationId,
|
|
111
111
|
bindTransportKey: true,
|
|
112
|
+
...(verify.includeCollateral ? { includeCollateral: true } : {}),
|
|
112
113
|
});
|
|
113
114
|
if (!attested.transportPublicKey) {
|
|
114
115
|
throw new Error('The node did not report a transport key; it predates sealed transport');
|
|
@@ -118,7 +119,13 @@ export async function fetchAttestedTransportKey(admin, verify, options = {}) {
|
|
|
118
119
|
? fromHex(options.applicationHash, 'applicationHash', 32)
|
|
119
120
|
: new Uint8Array(32);
|
|
120
121
|
const reportDataSuffix = hex(await transportKeyBinding(inner, transportKey));
|
|
121
|
-
|
|
122
|
+
const verified = await verify({
|
|
123
|
+
quoteB64: attested.quoteB64,
|
|
124
|
+
nonce,
|
|
125
|
+
reportDataSuffix,
|
|
126
|
+
collateral: attested.collateral,
|
|
127
|
+
});
|
|
128
|
+
if (!verified) {
|
|
122
129
|
throw new Error('The attestation did not verify, so its transport key is not trusted');
|
|
123
130
|
}
|
|
124
131
|
return transportKey;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifying a TEE node's quote in the page, before trusting the key it binds.
|
|
3
|
+
*
|
|
4
|
+
* A quote proves what runs in a TD only once it has been checked: its
|
|
5
|
+
* signatures against Intel's root, the TCB status of the platform, the
|
|
6
|
+
* measurements of the image, and that its report data is the nonce and the
|
|
7
|
+
* binding this client asked for. {@link createQuoteVerifier} does all of that
|
|
8
|
+
* here, with nothing but the node: the node hands over the Intel-signed
|
|
9
|
+
* collateral with its quote (`includeCollateral`), and a node cannot forge
|
|
10
|
+
* collateral, only choose which valid collateral to serve.
|
|
11
|
+
*
|
|
12
|
+
* The DCAP verification itself — parsing the quote, checking its signature
|
|
13
|
+
* chain and appraising its TCB level against the collateral — comes from a
|
|
14
|
+
* library you pass in, normally `verify` from `@phala/dcap-qvl`, a pure
|
|
15
|
+
* JavaScript implementation that runs in browsers, Node and React Native. It
|
|
16
|
+
* is passed in rather than bundled so this SDK keeps no runtime dependencies,
|
|
17
|
+
* and applications that never talk to a TEE node pay nothing for it.
|
|
18
|
+
*/
|
|
19
|
+
import type { VerifyTransportQuote } from './sealed.js';
|
|
20
|
+
/**
|
|
21
|
+
* The collateral a quote is verified against — TCB info, QE identity, CRLs and
|
|
22
|
+
* their chains — in dcap-qvl's form: byte fields are hex strings or byte
|
|
23
|
+
* arrays. What `/admin-api/tee/attest` returns with `includeCollateral`, and
|
|
24
|
+
* what `@phala/dcap-qvl`'s `getCollateral` fetches.
|
|
25
|
+
*/
|
|
26
|
+
export interface DcapCollateral {
|
|
27
|
+
pck_crl_issuer_chain: string;
|
|
28
|
+
root_ca_crl: number[] | string;
|
|
29
|
+
pck_crl: number[] | string;
|
|
30
|
+
tcb_info_issuer_chain: string;
|
|
31
|
+
tcb_info: string;
|
|
32
|
+
tcb_info_signature: number[] | string;
|
|
33
|
+
qe_identity_issuer_chain: string;
|
|
34
|
+
qe_identity: string;
|
|
35
|
+
qe_identity_signature: number[] | string;
|
|
36
|
+
}
|
|
37
|
+
/** What a DCAP verifier returns for a quote whose signature chain verified. */
|
|
38
|
+
export interface DcapVerifiedReport {
|
|
39
|
+
status: string;
|
|
40
|
+
advisory_ids?: string[];
|
|
41
|
+
report: {
|
|
42
|
+
type: string;
|
|
43
|
+
data: unknown;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Verify a quote's signature chain against `collateral` as of `nowSecs`, and
|
|
48
|
+
* appraise its TCB level. Throws when it does not verify. `verify` from
|
|
49
|
+
* `@phala/dcap-qvl` has exactly this shape.
|
|
50
|
+
*/
|
|
51
|
+
export type DcapVerify = (quote: Uint8Array, collateral: DcapCollateral, nowSecs: number) => DcapVerifiedReport;
|
|
52
|
+
export interface QuoteVerifierOptions {
|
|
53
|
+
/** DCAP verification: `verify` from `@phala/dcap-qvl`. */
|
|
54
|
+
dcapVerify: DcapVerify;
|
|
55
|
+
/**
|
|
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.
|
|
59
|
+
*/
|
|
60
|
+
allowedMrtd: string[];
|
|
61
|
+
/** RTMR allowlists (hex); a register is not checked when its list is left out. */
|
|
62
|
+
allowedRtmr0?: string[];
|
|
63
|
+
allowedRtmr1?: string[];
|
|
64
|
+
allowedRtmr2?: string[];
|
|
65
|
+
allowedRtmr3?: string[];
|
|
66
|
+
/**
|
|
67
|
+
* TCB statuses accepted. Defaults to `['UpToDate']`. `Revoked` is refused
|
|
68
|
+
* even when listed.
|
|
69
|
+
*/
|
|
70
|
+
allowedTcbStatuses?: string[];
|
|
71
|
+
/**
|
|
72
|
+
* Where to get collateral when the node sends none (it predates
|
|
73
|
+
* `includeCollateral`): for example
|
|
74
|
+
* `(quote) => getCollateral(PHALA_PCCS_URL, quote)` from `@phala/dcap-qvl`.
|
|
75
|
+
* Without it, such a node's quote is refused.
|
|
76
|
+
*/
|
|
77
|
+
fetchCollateral?: (quote: Uint8Array) => Promise<DcapCollateral>;
|
|
78
|
+
/** The current time in milliseconds; defaults to `Date.now`. For tests. */
|
|
79
|
+
now?: () => number;
|
|
80
|
+
}
|
|
81
|
+
/** A verifier for {@link fetchAttestedTransportKey}, which asks the node for collateral. */
|
|
82
|
+
export type QuoteVerifier = VerifyTransportQuote & {
|
|
83
|
+
readonly includeCollateral: true;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* A verifier that checks a TEE node's quote here, in the page, and accepts it
|
|
87
|
+
* only when every check passes:
|
|
88
|
+
*
|
|
89
|
+
* - its signatures verify against Intel's root, with collateral that is valid
|
|
90
|
+
* now;
|
|
91
|
+
* - the platform's TCB status is one allowed (`UpToDate` by default), and
|
|
92
|
+
* never `Revoked`;
|
|
93
|
+
* - the MRTD, and any RTMRs given, are ones allowed;
|
|
94
|
+
* - its report data is the nonce followed by the binding the client computed.
|
|
95
|
+
*
|
|
96
|
+
* Any failure rejects with the reason; nothing is trusted from a quote that
|
|
97
|
+
* does not pass. Mock quotes never pass: they carry no signature.
|
|
98
|
+
*
|
|
99
|
+
* ```ts
|
|
100
|
+
* import { verify as dcapVerify } from '@phala/dcap-qvl';
|
|
101
|
+
*
|
|
102
|
+
* const verifier = createQuoteVerifier({ dcapVerify, allowedMrtd: [trustedMrtd] });
|
|
103
|
+
* const fetch = createSealedFetch({
|
|
104
|
+
* baseUrl,
|
|
105
|
+
* transportPublicKey: () => fetchAttestedTransportKey(admin, verifier),
|
|
106
|
+
* });
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export declare function createQuoteVerifier(options: QuoteVerifierOptions): QuoteVerifier;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifying a TEE node's quote in the page, before trusting the key it binds.
|
|
3
|
+
*
|
|
4
|
+
* A quote proves what runs in a TD only once it has been checked: its
|
|
5
|
+
* signatures against Intel's root, the TCB status of the platform, the
|
|
6
|
+
* measurements of the image, and that its report data is the nonce and the
|
|
7
|
+
* binding this client asked for. {@link createQuoteVerifier} does all of that
|
|
8
|
+
* here, with nothing but the node: the node hands over the Intel-signed
|
|
9
|
+
* collateral with its quote (`includeCollateral`), and a node cannot forge
|
|
10
|
+
* collateral, only choose which valid collateral to serve.
|
|
11
|
+
*
|
|
12
|
+
* The DCAP verification itself — parsing the quote, checking its signature
|
|
13
|
+
* chain and appraising its TCB level against the collateral — comes from a
|
|
14
|
+
* library you pass in, normally `verify` from `@phala/dcap-qvl`, a pure
|
|
15
|
+
* JavaScript implementation that runs in browsers, Node and React Native. It
|
|
16
|
+
* is passed in rather than bundled so this SDK keeps no runtime dependencies,
|
|
17
|
+
* and applications that never talk to a TEE node pay nothing for it.
|
|
18
|
+
*/
|
|
19
|
+
import { hex } from '../crypto/internal.js';
|
|
20
|
+
const MEASUREMENT_HEX = /^[0-9a-f]{96}$/;
|
|
21
|
+
/**
|
|
22
|
+
* A verifier that checks a TEE node's quote here, in the page, and accepts it
|
|
23
|
+
* only when every check passes:
|
|
24
|
+
*
|
|
25
|
+
* - its signatures verify against Intel's root, with collateral that is valid
|
|
26
|
+
* now;
|
|
27
|
+
* - the platform's TCB status is one allowed (`UpToDate` by default), and
|
|
28
|
+
* never `Revoked`;
|
|
29
|
+
* - the MRTD, and any RTMRs given, are ones allowed;
|
|
30
|
+
* - its report data is the nonce followed by the binding the client computed.
|
|
31
|
+
*
|
|
32
|
+
* Any failure rejects with the reason; nothing is trusted from a quote that
|
|
33
|
+
* does not pass. Mock quotes never pass: they carry no signature.
|
|
34
|
+
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* import { verify as dcapVerify } from '@phala/dcap-qvl';
|
|
37
|
+
*
|
|
38
|
+
* const verifier = createQuoteVerifier({ dcapVerify, allowedMrtd: [trustedMrtd] });
|
|
39
|
+
* const fetch = createSealedFetch({
|
|
40
|
+
* baseUrl,
|
|
41
|
+
* transportPublicKey: () => fetchAttestedTransportKey(admin, verifier),
|
|
42
|
+
* });
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
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
|
+
}
|
|
50
|
+
const allowedRtmr = [
|
|
51
|
+
measurements(options.allowedRtmr0 ?? [], 'allowedRtmr0'),
|
|
52
|
+
measurements(options.allowedRtmr1 ?? [], 'allowedRtmr1'),
|
|
53
|
+
measurements(options.allowedRtmr2 ?? [], 'allowedRtmr2'),
|
|
54
|
+
measurements(options.allowedRtmr3 ?? [], 'allowedRtmr3'),
|
|
55
|
+
];
|
|
56
|
+
const allowedStatuses = options.allowedTcbStatuses ?? ['UpToDate'];
|
|
57
|
+
const now = options.now ?? Date.now;
|
|
58
|
+
const verifier = async ({ quoteB64, nonce, reportDataSuffix, collateral, }) => {
|
|
59
|
+
const quote = fromBase64(quoteB64);
|
|
60
|
+
const against = collateral ?? (await options.fetchCollateral?.(quote));
|
|
61
|
+
if (!against) {
|
|
62
|
+
throw new Error('The node sent no collateral for its quote, and no fetchCollateral was given to get it');
|
|
63
|
+
}
|
|
64
|
+
let verified;
|
|
65
|
+
try {
|
|
66
|
+
verified = options.dcapVerify(quote, against, Math.floor(now() / 1000));
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
throw new Error(`The quote did not verify: ${error?.message ?? String(error)}`);
|
|
70
|
+
}
|
|
71
|
+
if (verified.status === 'Revoked' || !allowedStatuses.includes(verified.status)) {
|
|
72
|
+
throw new Error(`The platform's TCB status is ${verified.status}, which is not accepted`);
|
|
73
|
+
}
|
|
74
|
+
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`);
|
|
77
|
+
}
|
|
78
|
+
const registers = [td.rtMr0, td.rtMr1, td.rtMr2, td.rtMr3];
|
|
79
|
+
registers.forEach((register, index) => {
|
|
80
|
+
const allowed = allowedRtmr[index];
|
|
81
|
+
if (allowed.length > 0 && !allowed.includes(hex(register))) {
|
|
82
|
+
throw new Error(`RTMR${index} ${hex(register)} is not one this verifier trusts`);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
if (hex(td.reportData) !== `${nonce}${reportDataSuffix}`.toLowerCase()) {
|
|
86
|
+
throw new Error('The quote does not commit to this nonce and binding');
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
};
|
|
90
|
+
return Object.assign(verifier, { includeCollateral: true });
|
|
91
|
+
}
|
|
92
|
+
function measurements(values, label) {
|
|
93
|
+
return values.map((value) => {
|
|
94
|
+
const normalized = value.trim().toLowerCase();
|
|
95
|
+
if (!MEASUREMENT_HEX.test(normalized)) {
|
|
96
|
+
throw new Error(`${label} holds ${JSON.stringify(value)}, which is not a 48-byte hex measurement`);
|
|
97
|
+
}
|
|
98
|
+
return normalized;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/** The TD report of a TDX quote (1.0 or 1.5); anything else is refused. */
|
|
102
|
+
function tdReport(report) {
|
|
103
|
+
const data = report.data;
|
|
104
|
+
if (report.type === 'td10' && data)
|
|
105
|
+
return data;
|
|
106
|
+
if (report.type === 'td15' && data?.base)
|
|
107
|
+
return data.base;
|
|
108
|
+
throw new Error(`The quote is a ${report.type} quote, not a TDX one`);
|
|
109
|
+
}
|
|
110
|
+
function fromBase64(value) {
|
|
111
|
+
const binary = atob(value);
|
|
112
|
+
const bytes = new Uint8Array(binary.length);
|
|
113
|
+
for (let i = 0; i < binary.length; i += 1)
|
|
114
|
+
bytes[i] = binary.charCodeAt(i);
|
|
115
|
+
return bytes;
|
|
116
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@calimero-network/mero-js",
|
|
3
|
-
"version": "19.
|
|
3
|
+
"version": "19.26.0",
|
|
4
4
|
"description": "Pure JavaScript SDK for Calimero",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"types": "./dist/index.d.ts",
|
|
18
18
|
"sideEffects": false,
|
|
19
19
|
"devDependencies": {
|
|
20
|
+
"@phala/dcap-qvl": "0.5.3",
|
|
20
21
|
"@semantic-release/changelog": "^6.0.3",
|
|
21
22
|
"@semantic-release/commit-analyzer": "^13.0.0",
|
|
22
23
|
"@semantic-release/exec": "^7.0.0",
|