@calimero-network/mero-js 21.2.2 → 21.4.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.
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import type { SignedGroupOpenInvitation } from '../admin-api/admin-types.js';
17
17
  /** Core's `SIGNED_NAMESPACE_OP_SCHEMA_VERSION`. A node refuses any other value. */
18
- export declare const SIGNED_NAMESPACE_OP_SCHEMA_VERSION = 9;
18
+ export declare const SIGNED_NAMESPACE_OP_SCHEMA_VERSION = 10;
19
19
  /**
20
20
  * Borsh-encode a `SignedGroupOpenInvitation`.
21
21
  *
@@ -1,6 +1,6 @@
1
1
  import { concat, derivePublicKey, fromHex, hex, importSigningKey, u32le, u64le, } from '../crypto/internal.js';
2
2
  /** Core's `SIGNED_NAMESPACE_OP_SCHEMA_VERSION`. A node refuses any other value. */
3
- export const SIGNED_NAMESPACE_OP_SCHEMA_VERSION = 9;
3
+ export const SIGNED_NAMESPACE_OP_SCHEMA_VERSION = 10;
4
4
  /** Domain prefixed to the signable bytes; core's `NAMESPACE_GOVERNANCE_SIGN_DOMAIN`. */
5
5
  const NAMESPACE_SIGN_DOMAIN = new TextEncoder().encode('calimero.namespace.v1');
6
6
  /** `NamespaceOp::Root` is the first variant. */
@@ -20,7 +20,7 @@ const ROOT_OP = {
20
20
  KeyDelivery: 6,
21
21
  MemberJoinedOpen: 7,
22
22
  MemberJoinedAt: 8,
23
- NamespaceCreated: 9,
23
+ NamespaceCreatedV2: 9,
24
24
  MemberJoinedViaTeeAttestation: 10,
25
25
  };
26
26
  /** A required `[u8; 32]`, as JSON carries it: 32 byte values. */
@@ -2,3 +2,6 @@ export { HANDSHAKE_PATH, SEALED_CONTENT_TYPE, SEALED_PATH, SealedTransportError,
2
2
  export type { AttestedSealedFetchOptions, SealedFetchOptions, VerifyTransportQuote } from './sealed.js';
3
3
  export { createQuoteVerifier, trustedMeasurementsFromReleases } from './verify.js';
4
4
  export type { DcapCollateral, DcapVerifiedReport, DcapVerify, PublishedMrtds, QuoteVerifier, QuoteVerifierOptions, TrustedMeasurements, } from './verify.js';
5
+ export { DEFAULT_RELEASE_MIRROR, NODE_RELEASE_SIGNER, cloudNodeReleaseUrl, createSignedReleaseSealedFetch, createSignedReleaseVerifier, fetchNodeRelease, fetchNodeReleaseVersion, nodeReleaseVersionFromImage, trustSignedRelease, verifySignedNodeRelease, } from './release.js';
6
+ export type { SignedNodeRelease, SignedReleaseSealedFetchOptions, SignedReleaseVerifierOptions, TrustSignedReleaseOptions, VerifySignedNodeReleaseOptions, } from './release.js';
7
+ export type { SignerIdentity } from './sigstore.js';
@@ -1,2 +1,3 @@
1
1
  export { HANDSHAKE_PATH, SEALED_CONTENT_TYPE, SEALED_PATH, SealedTransportError, StaleTransportKeyError, createAttestedSealedFetch, createSealedFetch, fetchAttestedTransportKey, transportKeyBinding, } from './sealed.js';
2
2
  export { createQuoteVerifier, trustedMeasurementsFromReleases } from './verify.js';
3
+ export { DEFAULT_RELEASE_MIRROR, NODE_RELEASE_SIGNER, cloudNodeReleaseUrl, createSignedReleaseSealedFetch, createSignedReleaseVerifier, fetchNodeRelease, fetchNodeReleaseVersion, nodeReleaseVersionFromImage, trustSignedRelease, verifySignedNodeRelease, } from './release.js';
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Trusting a TEE node's image by its signed release, fetched at run time from
3
+ * anywhere, instead of shipping the release's `published-mrtds.json` with the
4
+ * app.
5
+ *
6
+ * Each mero-tee node release publishes `published-mrtds.json` (the
7
+ * measurements of its image) with a cosign keyless bundle, signed by the
8
+ * mero-tee release workflow on GitHub Actions. {@link verifySignedNodeRelease}
9
+ * checks that signature here, against the Sigstore trust root embedded in this
10
+ * package, so the file can come from any source: the cloud's mirror
11
+ * (`GET https://cloud.calimero.network/api/tee/node-releases/{version}`), a
12
+ * mirror of your own, or a file bundled with the app. None of them is
13
+ * trusted; a file whose signature does not verify is refused.
14
+ *
15
+ * What is trusted:
16
+ *
17
+ * - the signer: the node release workflow of `calimero-network/mero-tee` on
18
+ * `master` ({@link NODE_RELEASE_SIGNER}), the identity core pins as
19
+ * `NODE_RELEASE_IDENTITY`;
20
+ * - the Sigstore public-good trust root: Fulcio's CAs and Rekor's key;
21
+ * - the minimum release you pass, below which no release is accepted.
22
+ *
23
+ * What is not: the transport, the server that serves the release, and the
24
+ * relay's choice of release. A relay can present any genuinely signed release
25
+ * at or above the minimum, and no other; its quote must then match that
26
+ * release's measurements exactly, so it cannot claim one release and run
27
+ * another.
28
+ */
29
+ import { type SignerIdentity } from './sigstore.js';
30
+ import { type DcapVerify, type PublishedMrtds, type QuoteVerifier, type QuoteVerifierOptions } from './verify.js';
31
+ /** The workflow that signs every mero-tee node release, and the issuer of its identity. */
32
+ export declare const NODE_RELEASE_SIGNER: Readonly<SignerIdentity>;
33
+ /**
34
+ * The public mirror of signed node releases, used when no other source is
35
+ * given. Only a transport: what it serves is verified, so it can fail to
36
+ * answer but cannot make a release trusted.
37
+ */
38
+ export declare const DEFAULT_RELEASE_MIRROR = "https://cloud.calimero.network";
39
+ /**
40
+ * A node release as the servers hand it out, wrapped as `{"data": ...}`: the
41
+ * release's `published-mrtds.json`, byte for byte, and its cosign bundle.
42
+ * Unverified until {@link verifySignedNodeRelease} says otherwise.
43
+ */
44
+ export interface SignedNodeRelease {
45
+ /** The release, e.g. `2.3.87`. It must be the signed file's `tag`. */
46
+ version: string;
47
+ /** `published-mrtds.json`, exactly as signed. */
48
+ publishedMrtds: string;
49
+ /** `published-mrtds.json.bundle.json`, as JSON text. */
50
+ bundle: string;
51
+ }
52
+ export interface VerifySignedNodeReleaseOptions {
53
+ /** Require the file's `tag` to be this release. */
54
+ expectedVersion?: string;
55
+ /** The certificate identity required. Defaults to {@link NODE_RELEASE_SIGNER}'s. */
56
+ identity?: string;
57
+ /** The OIDC issuer required. Defaults to {@link NODE_RELEASE_SIGNER}'s. */
58
+ issuer?: string;
59
+ /** The release's detached `published-mrtds.json.sig`, when you have it; it must be the bundle's. */
60
+ signature?: string;
61
+ }
62
+ /**
63
+ * Verify a node release's `published-mrtds.json` against its cosign bundle,
64
+ * as `cosign verify-blob` would, and return the file parsed. Rejects, with the
65
+ * reason, unless:
66
+ *
67
+ * - the signature over the exact bytes verifies with the bundle's certificate;
68
+ * - that certificate chains to a Fulcio CA in the Sigstore trust root, and it
69
+ * and the CA were valid when Rekor logged the signature;
70
+ * - it names the node release workflow and GitHub Actions as its issuer;
71
+ * - Rekor's signed entry timestamp verifies, for an entry that is this
72
+ * signature over this file by this certificate;
73
+ * - the file is a node release (`role: "node"`), and `expectedVersion`, when
74
+ * given, is its `tag`.
75
+ *
76
+ * Nothing is read from the clock: the certificate is checked at the time Rekor
77
+ * logged the signature, so a release that verifies once always does.
78
+ */
79
+ export declare function verifySignedNodeRelease(publishedMrtds: string | Uint8Array, bundle: string | object, options?: VerifySignedNodeReleaseOptions): Promise<PublishedMrtds>;
80
+ /**
81
+ * Fetch a {@link SignedNodeRelease} from a server that wraps it as
82
+ * `{"data": ...}`, such as the cloud's {@link cloudNodeReleaseUrl}. It is
83
+ * only fetched: pass it to
84
+ * {@link trustSignedRelease}, or {@link verifySignedNodeRelease}, before using it.
85
+ */
86
+ export declare function fetchNodeRelease(url: string, options?: {
87
+ fetch?: typeof fetch;
88
+ }): Promise<SignedNodeRelease>;
89
+ /**
90
+ * The release a mero-tee node image was built from, read from its name, e.g.
91
+ * `merotee-ubuntu-questing-25-10-locked-read-only-2-3-87` is `2.3.87`. Throws
92
+ * for any other name, including the suffixed images of unreleased builds.
93
+ */
94
+ export declare function nodeReleaseVersionFromImage(osImage: string): string;
95
+ /**
96
+ * The release the TEE node at `baseUrl` says it runs, from the image name its
97
+ * `GET /admin-api/tee/info` reports. The node's own claim, and not trusted:
98
+ * it only picks which signed release to fetch, and the quote must then match
99
+ * that release's measurements.
100
+ */
101
+ export declare function fetchNodeReleaseVersion(baseUrl: string, options?: {
102
+ fetch?: typeof fetch;
103
+ }): Promise<string>;
104
+ /** Where the cloud at `cloudUrl` (e.g. `https://cloud.calimero.network`) mirrors node release `version`. */
105
+ export declare function cloudNodeReleaseUrl(cloudUrl: string, version: string): string;
106
+ export interface TrustSignedReleaseOptions {
107
+ /** The release, from any source; it is verified here. */
108
+ release: SignedNodeRelease;
109
+ /** The image profile to trust, e.g. `locked-read-only`. */
110
+ profile: string;
111
+ /**
112
+ * The oldest release accepted, e.g. `2.3.87`. Required: every release ever
113
+ * signed stays validly signed, so without a floor a relay could present an
114
+ * old release, with a flaw since fixed, and a quote from that old image
115
+ * would pass. Raise it when a release must no longer be trusted.
116
+ */
117
+ minReleaseVersion: string;
118
+ /** Override the signer; defaults to {@link NODE_RELEASE_SIGNER}. */
119
+ identity?: string;
120
+ issuer?: string;
121
+ }
122
+ /**
123
+ * The {@link QuoteVerifierOptions} that trust one profile of a signed node
124
+ * release, from an untrusted source: its signature is verified, its signed
125
+ * `tag` must be `release.version` and no older than `minReleaseVersion`, and
126
+ * then it is read as {@link trustedMeasurementsFromReleases} reads a bundled
127
+ * file.
128
+ *
129
+ * ```ts
130
+ * const version = await fetchNodeReleaseVersion(relayUrl);
131
+ * const release = await fetchNodeRelease(cloudNodeReleaseUrl(DEFAULT_RELEASE_MIRROR, version));
132
+ * const verifier = createQuoteVerifier({
133
+ * dcapVerify,
134
+ * ...(await trustSignedRelease({ release, profile: 'locked-read-only', minReleaseVersion: '2.3.87' })),
135
+ * });
136
+ * ```
137
+ */
138
+ export declare function trustSignedRelease(options: TrustSignedReleaseOptions): Promise<Required<Pick<QuoteVerifierOptions, 'allowedMeasurements' | 'allowedTcbStatuses'>>>;
139
+ export interface SignedReleaseVerifierOptions extends Omit<TrustSignedReleaseOptions, 'release'> {
140
+ /** DCAP verification: `verify` from `@phala/dcap-qvl`. */
141
+ dcapVerify: DcapVerify;
142
+ /**
143
+ * Where to get the node's release, asked again each time the node is
144
+ * attested (a restarted node may run a newer one). Untrusted: it is
145
+ * verified, and the quote must match it.
146
+ */
147
+ release: () => Promise<SignedNodeRelease>;
148
+ /** As {@link QuoteVerifierOptions.fetchCollateral}. */
149
+ fetchCollateral?: QuoteVerifierOptions['fetchCollateral'];
150
+ /** The current time in milliseconds, for the quote's collateral; defaults to `Date.now`. For tests. */
151
+ now?: () => number;
152
+ }
153
+ /**
154
+ * A {@link createQuoteVerifier} whose trusted image is the signed release the
155
+ * node says it runs. On each attestation it gets the release, checks it with
156
+ * {@link trustSignedRelease}, and verifies the quote against that release's
157
+ * measurements only.
158
+ *
159
+ * The node's choice of release is the one thing taken from it, and it is
160
+ * bounded: only a genuinely signed release at or above `minReleaseVersion`
161
+ * passes, and the quote must then be of exactly that release's image. Use it
162
+ * as `connectCloud`'s `seal`, or with `createAttestedSealedFetch`;
163
+ * {@link createSignedReleaseSealedFetch} does the latter for you.
164
+ */
165
+ export declare function createSignedReleaseVerifier(options: SignedReleaseVerifierOptions): QuoteVerifier;
166
+ export interface SignedReleaseSealedFetchOptions extends Omit<SignedReleaseVerifierOptions, 'release'> {
167
+ /** The TEE node's (the relay's) base URL. */
168
+ baseUrl: string;
169
+ /**
170
+ * Where to get the release the node says it runs, the version read from its
171
+ * `GET /admin-api/tee/info` each time it is attested. A string is the base
172
+ * URL of a mirror serving {@link cloudNodeReleaseUrl}'s path, defaulting to
173
+ * {@link DEFAULT_RELEASE_MIRROR}; a function gets the version and returns
174
+ * the release, e.g. from a file bundled with the app. Whichever it is, it is
175
+ * not trusted: the release is verified. `/tee/info` is read in the clear,
176
+ * which a node with `[server.sealed] required` refuses: for one, pass a
177
+ * function that ignores `version` and returns the release you expect.
178
+ */
179
+ releaseSource?: string | ((context: {
180
+ baseUrl: string;
181
+ version: string;
182
+ fetch: typeof fetch;
183
+ }) => Promise<SignedNodeRelease>);
184
+ /** Also require the node to run this application with this bytecode hash. */
185
+ applicationId?: string;
186
+ applicationHash?: string;
187
+ /** Fetch for the release, the attestation and the envelopes. Defaults to global `fetch`. */
188
+ fetch?: typeof fetch;
189
+ }
190
+ /**
191
+ * {@link createAttestedSealedFetch} for a TEE node known only by URL, trusting
192
+ * whatever signed release at or above `minReleaseVersion` it runs: it reads
193
+ * the release the node names from its `/tee/info`, gets that release from
194
+ * `releaseSource` (the public mirror unless you say otherwise), verifies its
195
+ * signature, builds the quote verifier from its measurements, attests the node
196
+ * and seals everything to it.
197
+ *
198
+ * ```ts
199
+ * import { verify as dcapVerify } from '@phala/dcap-qvl';
200
+ *
201
+ * const relay = new RelayClient({
202
+ * relayUrl,
203
+ * fetch: createSignedReleaseSealedFetch({
204
+ * baseUrl: relayUrl,
205
+ * dcapVerify,
206
+ * profile: 'locked-read-only',
207
+ * minReleaseVersion: '2.3.87',
208
+ * }),
209
+ * ...
210
+ * });
211
+ * ```
212
+ */
213
+ export declare function createSignedReleaseSealedFetch(options: SignedReleaseSealedFetchOptions): typeof fetch;
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Trusting a TEE node's image by its signed release, fetched at run time from
3
+ * anywhere, instead of shipping the release's `published-mrtds.json` with the
4
+ * app.
5
+ *
6
+ * Each mero-tee node release publishes `published-mrtds.json` (the
7
+ * measurements of its image) with a cosign keyless bundle, signed by the
8
+ * mero-tee release workflow on GitHub Actions. {@link verifySignedNodeRelease}
9
+ * checks that signature here, against the Sigstore trust root embedded in this
10
+ * package, so the file can come from any source: the cloud's mirror
11
+ * (`GET https://cloud.calimero.network/api/tee/node-releases/{version}`), a
12
+ * mirror of your own, or a file bundled with the app. None of them is
13
+ * trusted; a file whose signature does not verify is refused.
14
+ *
15
+ * What is trusted:
16
+ *
17
+ * - the signer: the node release workflow of `calimero-network/mero-tee` on
18
+ * `master` ({@link NODE_RELEASE_SIGNER}), the identity core pins as
19
+ * `NODE_RELEASE_IDENTITY`;
20
+ * - the Sigstore public-good trust root: Fulcio's CAs and Rekor's key;
21
+ * - the minimum release you pass, below which no release is accepted.
22
+ *
23
+ * What is not: the transport, the server that serves the release, and the
24
+ * relay's choice of release. A relay can present any genuinely signed release
25
+ * at or above the minimum, and no other; its quote must then match that
26
+ * release's measurements exactly, so it cannot claim one release and run
27
+ * another.
28
+ */
29
+ import { createAttestedSealedFetch } from './sealed.js';
30
+ import { verifyCosignBundle } from './sigstore.js';
31
+ import { createQuoteVerifier, trustedMeasurementsFromReleases, } from './verify.js';
32
+ /** The workflow that signs every mero-tee node release, and the issuer of its identity. */
33
+ export const NODE_RELEASE_SIGNER = Object.freeze({
34
+ identity: 'https://github.com/calimero-network/mero-tee/.github/workflows/release-node-image-gcp.yaml@refs/heads/master',
35
+ issuer: 'https://token.actions.githubusercontent.com',
36
+ });
37
+ const MEASUREMENTS = ['mrtd', 'rtmr0', 'rtmr1', 'rtmr2', 'rtmr3'];
38
+ /**
39
+ * The public mirror of signed node releases, used when no other source is
40
+ * given. Only a transport: what it serves is verified, so it can fail to
41
+ * answer but cannot make a release trusted.
42
+ */
43
+ export const DEFAULT_RELEASE_MIRROR = 'https://cloud.calimero.network';
44
+ /**
45
+ * Verify a node release's `published-mrtds.json` against its cosign bundle,
46
+ * as `cosign verify-blob` would, and return the file parsed. Rejects, with the
47
+ * reason, unless:
48
+ *
49
+ * - the signature over the exact bytes verifies with the bundle's certificate;
50
+ * - that certificate chains to a Fulcio CA in the Sigstore trust root, and it
51
+ * and the CA were valid when Rekor logged the signature;
52
+ * - it names the node release workflow and GitHub Actions as its issuer;
53
+ * - Rekor's signed entry timestamp verifies, for an entry that is this
54
+ * signature over this file by this certificate;
55
+ * - the file is a node release (`role: "node"`), and `expectedVersion`, when
56
+ * given, is its `tag`.
57
+ *
58
+ * Nothing is read from the clock: the certificate is checked at the time Rekor
59
+ * logged the signature, so a release that verifies once always does.
60
+ */
61
+ export async function verifySignedNodeRelease(publishedMrtds, bundle, options = {}) {
62
+ const body = typeof publishedMrtds === 'string' ? new TextEncoder().encode(publishedMrtds) : publishedMrtds;
63
+ await verifyCosignBundle(body, bundle, {
64
+ identity: options.identity ?? NODE_RELEASE_SIGNER.identity,
65
+ issuer: options.issuer ?? NODE_RELEASE_SIGNER.issuer,
66
+ }, options.signature);
67
+ let release;
68
+ try {
69
+ release = JSON.parse(new TextDecoder().decode(body));
70
+ }
71
+ catch {
72
+ throw new Error('The signed release is not JSON');
73
+ }
74
+ if (release?.role !== 'node') {
75
+ throw new Error(`The signed release is a ${JSON.stringify(release?.role)} release, not a node one`);
76
+ }
77
+ // The workflow signs other files too, some with a `role` and `profiles` of
78
+ // their own (`release-provenance.json`): the signature covers bytes, not a
79
+ // file name, so check this is the measurements file.
80
+ const profiles = Object.values(release.profiles ?? {});
81
+ const measured = (image) => MEASUREMENTS.every((register) => /^[0-9a-f]{96}$/i.test(String(image?.[register])));
82
+ if (typeof release.tag !== 'string' || profiles.length === 0 || !profiles.every(measured)) {
83
+ throw new Error("The signed file is not a release's published-mrtds.json: it lists no measured profiles");
84
+ }
85
+ if (options.expectedVersion !== undefined && release.tag !== options.expectedVersion) {
86
+ throw new Error(`The signed release is ${release.tag}, not ${options.expectedVersion}`);
87
+ }
88
+ return release;
89
+ }
90
+ /**
91
+ * Fetch a {@link SignedNodeRelease} from a server that wraps it as
92
+ * `{"data": ...}`, such as the cloud's {@link cloudNodeReleaseUrl}. It is
93
+ * only fetched: pass it to
94
+ * {@link trustSignedRelease}, or {@link verifySignedNodeRelease}, before using it.
95
+ */
96
+ export async function fetchNodeRelease(url, options = {}) {
97
+ const response = await (options.fetch ?? fetch)(url, { headers: { accept: 'application/json' } });
98
+ if (!response.ok)
99
+ throw new Error(`Fetching the node release from ${url} failed: HTTP ${response.status}`);
100
+ let release;
101
+ try {
102
+ release = (await response.json())?.data;
103
+ }
104
+ catch {
105
+ throw new Error(`${url} did not answer with JSON`);
106
+ }
107
+ if (typeof release?.version !== 'string' ||
108
+ typeof release.publishedMrtds !== 'string' ||
109
+ typeof release.bundle !== 'string') {
110
+ throw new Error(`${url} did not answer with a signed node release`);
111
+ }
112
+ return { version: release.version, publishedMrtds: release.publishedMrtds, bundle: release.bundle };
113
+ }
114
+ /**
115
+ * The release a mero-tee node image was built from, read from its name, e.g.
116
+ * `merotee-ubuntu-questing-25-10-locked-read-only-2-3-87` is `2.3.87`. Throws
117
+ * for any other name, including the suffixed images of unreleased builds.
118
+ */
119
+ export function nodeReleaseVersionFromImage(osImage) {
120
+ const match = /^merotee-.+-(\d+)-(\d+)-(\d+)$/.exec(osImage);
121
+ if (!match)
122
+ throw new Error(`${JSON.stringify(osImage)} is not the image of a mero-tee node release`);
123
+ return `${Number(match[1])}.${Number(match[2])}.${Number(match[3])}`;
124
+ }
125
+ /**
126
+ * The release the TEE node at `baseUrl` says it runs, from the image name its
127
+ * `GET /admin-api/tee/info` reports. The node's own claim, and not trusted:
128
+ * it only picks which signed release to fetch, and the quote must then match
129
+ * that release's measurements.
130
+ */
131
+ export async function fetchNodeReleaseVersion(baseUrl, options = {}) {
132
+ const url = `${baseUrl.replace(/\/+$/, '')}/admin-api/tee/info`;
133
+ const response = await (options.fetch ?? fetch)(url, { headers: { accept: 'application/json' } });
134
+ if (!response.ok)
135
+ throw new Error(`Fetching ${url} failed: HTTP ${response.status}`);
136
+ let osImage;
137
+ try {
138
+ osImage = (await response.json())?.data?.osImage;
139
+ }
140
+ catch {
141
+ throw new Error(`${url} did not answer with JSON`);
142
+ }
143
+ if (typeof osImage !== 'string')
144
+ throw new Error(`${url} did not name the node's image`);
145
+ return nodeReleaseVersionFromImage(osImage);
146
+ }
147
+ /** Where the cloud at `cloudUrl` (e.g. `https://cloud.calimero.network`) mirrors node release `version`. */
148
+ export function cloudNodeReleaseUrl(cloudUrl, version) {
149
+ return `${cloudUrl.replace(/\/+$/, '')}/api/tee/node-releases/${releaseVersion(version, 'version')}`;
150
+ }
151
+ /**
152
+ * The {@link QuoteVerifierOptions} that trust one profile of a signed node
153
+ * release, from an untrusted source: its signature is verified, its signed
154
+ * `tag` must be `release.version` and no older than `minReleaseVersion`, and
155
+ * then it is read as {@link trustedMeasurementsFromReleases} reads a bundled
156
+ * file.
157
+ *
158
+ * ```ts
159
+ * const version = await fetchNodeReleaseVersion(relayUrl);
160
+ * const release = await fetchNodeRelease(cloudNodeReleaseUrl(DEFAULT_RELEASE_MIRROR, version));
161
+ * const verifier = createQuoteVerifier({
162
+ * dcapVerify,
163
+ * ...(await trustSignedRelease({ release, profile: 'locked-read-only', minReleaseVersion: '2.3.87' })),
164
+ * });
165
+ * ```
166
+ */
167
+ export async function trustSignedRelease(options) {
168
+ const minimum = releaseVersion(options.minReleaseVersion, 'minReleaseVersion');
169
+ const version = releaseVersion(options.release?.version, 'The release version');
170
+ const published = await verifySignedNodeRelease(options.release.publishedMrtds, options.release.bundle, {
171
+ expectedVersion: version,
172
+ identity: options.identity,
173
+ issuer: options.issuer,
174
+ });
175
+ if (compareReleaseVersions(version, minimum) < 0) {
176
+ throw new Error(`Release ${version} is older than the minimum trusted, ${minimum}`);
177
+ }
178
+ return trustedMeasurementsFromReleases([published], { profile: options.profile });
179
+ }
180
+ /**
181
+ * A {@link createQuoteVerifier} whose trusted image is the signed release the
182
+ * node says it runs. On each attestation it gets the release, checks it with
183
+ * {@link trustSignedRelease}, and verifies the quote against that release's
184
+ * measurements only.
185
+ *
186
+ * The node's choice of release is the one thing taken from it, and it is
187
+ * bounded: only a genuinely signed release at or above `minReleaseVersion`
188
+ * passes, and the quote must then be of exactly that release's image. Use it
189
+ * as `connectCloud`'s `seal`, or with `createAttestedSealedFetch`;
190
+ * {@link createSignedReleaseSealedFetch} does the latter for you.
191
+ */
192
+ export function createSignedReleaseVerifier(options) {
193
+ releaseVersion(options.minReleaseVersion, 'minReleaseVersion');
194
+ const verifier = async (args) => {
195
+ const trusted = await trustSignedRelease({
196
+ release: await options.release(),
197
+ profile: options.profile,
198
+ minReleaseVersion: options.minReleaseVersion,
199
+ identity: options.identity,
200
+ issuer: options.issuer,
201
+ });
202
+ return createQuoteVerifier({
203
+ dcapVerify: options.dcapVerify,
204
+ fetchCollateral: options.fetchCollateral,
205
+ now: options.now,
206
+ ...trusted,
207
+ })(args);
208
+ };
209
+ return Object.assign(verifier, { includeCollateral: true });
210
+ }
211
+ /**
212
+ * {@link createAttestedSealedFetch} for a TEE node known only by URL, trusting
213
+ * whatever signed release at or above `minReleaseVersion` it runs: it reads
214
+ * the release the node names from its `/tee/info`, gets that release from
215
+ * `releaseSource` (the public mirror unless you say otherwise), verifies its
216
+ * signature, builds the quote verifier from its measurements, attests the node
217
+ * and seals everything to it.
218
+ *
219
+ * ```ts
220
+ * import { verify as dcapVerify } from '@phala/dcap-qvl';
221
+ *
222
+ * const relay = new RelayClient({
223
+ * relayUrl,
224
+ * fetch: createSignedReleaseSealedFetch({
225
+ * baseUrl: relayUrl,
226
+ * dcapVerify,
227
+ * profile: 'locked-read-only',
228
+ * minReleaseVersion: '2.3.87',
229
+ * }),
230
+ * ...
231
+ * });
232
+ * ```
233
+ */
234
+ export function createSignedReleaseSealedFetch(options) {
235
+ const { baseUrl, releaseSource, applicationId, applicationHash, fetch: givenFetch, ...verifierOptions } = options;
236
+ const baseFetch = givenFetch ?? ((input, init) => fetch(input, init));
237
+ const release = async () => {
238
+ const version = await fetchNodeReleaseVersion(baseUrl, { fetch: baseFetch });
239
+ return typeof releaseSource === 'function'
240
+ ? releaseSource({ baseUrl, version, fetch: baseFetch })
241
+ : fetchNodeRelease(cloudNodeReleaseUrl(releaseSource ?? DEFAULT_RELEASE_MIRROR, version), { fetch: baseFetch });
242
+ };
243
+ return createAttestedSealedFetch({
244
+ baseUrl,
245
+ verify: createSignedReleaseVerifier({ ...verifierOptions, release }),
246
+ applicationId,
247
+ applicationHash,
248
+ fetch: baseFetch,
249
+ });
250
+ }
251
+ const RELEASE_VERSION = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
252
+ /** `value` if it is a release version (`2.3.87`, `2.3.88-rc.1`); throws otherwise. */
253
+ function releaseVersion(value, what) {
254
+ const match = typeof value === 'string' ? RELEASE_VERSION.exec(value) : null;
255
+ const pre = match?.[4];
256
+ if (!match || (pre !== undefined && pre.split('.').some((id) => id === ''))) {
257
+ throw new Error(`${what} is ${JSON.stringify(value)}, not a release version like 2.3.87`);
258
+ }
259
+ return value;
260
+ }
261
+ /**
262
+ * Order two release versions as semver does, and as core's
263
+ * `compare_release_versions`: numerically by `X.Y.Z`, a pre-release before its
264
+ * release, build metadata ignored.
265
+ */
266
+ function compareReleaseVersions(a, b) {
267
+ const [aCore, aPre] = splitVersion(a);
268
+ const [bCore, bPre] = splitVersion(b);
269
+ for (let i = 0; i < 3; i += 1) {
270
+ if (aCore[i] !== bCore[i])
271
+ return aCore[i] < bCore[i] ? -1 : 1;
272
+ }
273
+ if (aPre === undefined || bPre === undefined)
274
+ return aPre === bPre ? 0 : aPre === undefined ? 1 : -1;
275
+ const aIds = aPre.split('.');
276
+ const bIds = bPre.split('.');
277
+ for (let i = 0; i < Math.max(aIds.length, bIds.length); i += 1) {
278
+ const [x, y] = [aIds[i], bIds[i]];
279
+ if (x === undefined || y === undefined)
280
+ return x === undefined ? -1 : 1;
281
+ const [xNum, yNum] = [/^\d+$/.test(x), /^\d+$/.test(y)];
282
+ const order = xNum && yNum ? Math.sign(Number(x) - Number(y)) : xNum !== yNum ? (xNum ? -1 : 1) : x < y ? -1 : x > y ? 1 : 0;
283
+ if (order !== 0)
284
+ return order;
285
+ }
286
+ return 0;
287
+ }
288
+ function splitVersion(version) {
289
+ const match = RELEASE_VERSION.exec(version);
290
+ return [[Number(match[1]), Number(match[2]), Number(match[3])], match[4]];
291
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The parts of the Sigstore public-good trust root that verifying a mero-tee
3
+ * release's cosign bundle needs: the Fulcio certificate authorities that issue
4
+ * the release workflow's short-lived signing certificates, and the Rekor log
5
+ * key that signs the bundle's signed entry timestamp. Each with the window in
6
+ * which the trust root says it was in use.
7
+ *
8
+ * Copied from the `trusted_root.json` that Sigstore publishes through its TUF
9
+ * repository (https://tuf-repo-cdn.sigstore.dev, target `trusted_root.json`),
10
+ * as shipped by the `sigstore` Python client 4.5.0 in
11
+ * `sigstore/_store/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev/trusted_root.json`.
12
+ * Embedded rather than fetched: fetching it at run time would trust whoever
13
+ * serves it, the very thing this check exists to avoid.
14
+ *
15
+ * Left out, because a legacy cosign bundle never uses them: the CT logs (the
16
+ * certificate's SCT is not checked here, as core does not either), the
17
+ * timestamp authority, and the Rekor v2 log `log2025-1.rekor.sigstore.dev`,
18
+ * whose Ed25519 key signs checkpoints rather than signed entry timestamps. A
19
+ * bundle that names any log but those below is refused.
20
+ *
21
+ * To refresh it when Sigstore rotates a key or adds a CA: take the current
22
+ * `trusted_root.json` from a TUF client (`python -m sigstore` or `cosign
23
+ * initialize` both keep one, verified against the TUF root), and copy each
24
+ * `certificateAuthorities[].certChain.certificates[].rawBytes` with its
25
+ * `validFor`, and each ECDSA `tlogs[]` entry's `publicKey.rawBytes`,
26
+ * `publicKey.validFor` and `logId.keyId` (as hex). The tests check each log id
27
+ * is the SHA-256 of its key and each chain verifies.
28
+ */
29
+ /** A Fulcio CA: its chain from the certificate that issues leaves up to the root, DER as base64. */
30
+ export interface FulcioAuthority {
31
+ uri: string;
32
+ /** When the trust root says this CA issued certificates. No `end`: it still does. */
33
+ validFor: {
34
+ start: string;
35
+ end?: string;
36
+ };
37
+ certChain: string[];
38
+ }
39
+ /** A Rekor log whose signed entry timestamps are accepted. */
40
+ export interface RekorLog {
41
+ baseUrl: string;
42
+ /** The log id a bundle names it by: the SHA-256 of its public key, hex. */
43
+ logId: string;
44
+ /** Its public key: DER SubjectPublicKeyInfo, base64. ECDSA P-256 with SHA-256. */
45
+ publicKey: string;
46
+ validFor: {
47
+ start: string;
48
+ end?: string;
49
+ };
50
+ }
51
+ export declare const FULCIO_AUTHORITIES: readonly FulcioAuthority[];
52
+ export declare const REKOR_LOGS: readonly RekorLog[];