privateer-agent 0.12.19 → 0.12.21

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.
@@ -375,6 +375,47 @@ export const MCP_CATALOG: CatalogEntry[] = [
375
375
  hosted: false,
376
376
  docsUrl: "https://dev.epicgames.com/documentation/unreal-engine/unreal-mcp-in-unreal-editor",
377
377
  },
378
+ {
379
+ // Godot ships no MCP server of its own. This is the "Godot MCP Native" editor
380
+ // plugin (MIT, Godot Asset Library) — chosen because it serves streamable HTTP
381
+ // from INSIDE the editor process, which is the same shape as the Unreal entry
382
+ // above and needs nothing installed on the machine outside Godot itself.
383
+ //
384
+ // Deliberately NOT one of the npx-launched Godot servers: those shell out to the
385
+ // `godot` BINARY headlessly (a second, non-interactive copy of the project rather
386
+ // than the editor the user is looking at), and every one of them is configured by
387
+ // a GODOT_PATH env var — a filesystem path, which is neither a token nor a
388
+ // placeholder arg, so this catalog has no `needs` that could ask for it honestly.
389
+ //
390
+ // Two things differ from Unreal, and they are the only two:
391
+ // 1. THE PLUGIN IS NOT PART OF THE ENGINE. Install "Godot MCP Native" from the
392
+ // Asset Library and enable it in Project → Project Settings → Plugins. Until
393
+ // that is done the port is simply closed, which looks identical to "Godot is
394
+ // not running" — so the setup guide below matters more here than it does for
395
+ // Unreal, where the plugin ships with 5.8.
396
+ // 2. ITS BIND ADDRESS IS NOT DOCUMENTED UPSTREAM. Unreal's is (loopback, plus an
397
+ // Origin check); this one's is not, so the honest claim is only that WE dial
398
+ // 127.0.0.1 — never that the editor cannot be reached from the LAN. Its own
399
+ // `auth_enabled` is off by default (user://mcp_settings.cfg), which is the
400
+ // knob to turn on when the machine sits on a network you don't trust.
401
+ //
402
+ // Everything else matches Unreal: nothing to authorize on the wire we use (hence
403
+ // localHttp + auth:"none"), it answers only while the editor is open, and the port
404
+ // is editable (`http_port`) — hence needs:"url", confirm the endpoint.
405
+ id: "godot",
406
+ name: "godot",
407
+ label: "Godot Engine",
408
+ blurb: "Drive the Godot editor — scenes, nodes, scripts, resources.",
409
+ transport: "http",
410
+ url: "http://127.0.0.1:9080/mcp",
411
+ localHttp: true,
412
+ needs: "url",
413
+ // Same reason as Unreal: hostedCapable() derives its answer from `oauth`, so
414
+ // http-with-no-auth would otherwise read as hostable. An enclave cannot reach a
415
+ // loopback port on the user's desk.
416
+ hosted: false,
417
+ docsUrl: "https://github.com/yurineko73/Godot-MCP-Native",
418
+ },
378
419
  ];
379
420
 
380
421
  export function catalogEntry(id: string): CatalogEntry | undefined {
@@ -1,39 +1,63 @@
1
- # Vendored: `@dstack/aci-verifier`
1
+ # Vendored: the ACI verifier
2
2
 
3
- Faithful copy of the zero-dependency TypeScript ACI verifier from
3
+ Copy of the zero-dependency TypeScript ACI verifier from
4
4
  [Dstack-TEE/private-ai-gateway](https://github.com/Dstack-TEE/private-ai-gateway)
5
5
  (`clients/verifier-ts/src`), Apache-2.0. It is `private: true` upstream (not on
6
6
  npm), so it is vendored here rather than installed.
7
7
 
8
- Provides the pieces `PhalaProvider` needs:
9
- - **`verifyReportBinding`** (`report.ts`) §10.1 checks 2–6 (crypto binding of the
10
- attestation report to the attested keyset for a supplied nonce). NOT the hardware
11
- TDX quote (check 1) that is layered on with `@phala/dcap-qvl` in the provider.
12
- - **`openE2eeChannel`** (`e2ee-channel.ts`) — the ACI E2EE channel:
8
+ Current drop: commit `1a044e960fbec8ab20f38524bc93aa0ced83d5b0` — the `aci/1`
9
+ protocol, which is what `inference.phala.com` actually serves as of 2026-08-24.
10
+
11
+ Provides the pieces the Phala sealed transport needs:
12
+ - **`verifyReportBinding`** (`report.ts`) — §9.1 checks 2–3: the served keyset
13
+ canonicalizes to the digest that the attestation statement for our nonce hashes
14
+ into `report_data`, and the keyset has not expired. NOT the hardware TDX quote
15
+ (check 1) — that is layered on with `@phala/dcap-qvl` in `../../phalaSeal.ts`.
16
+ - **`openE2eeChannel`** (`e2ee-channel.ts`) — the E2EE v2 channel:
13
17
  `x25519-aes-256-gcm-hkdf-sha256`, per-field seal/open, `X-E2EE-*` headers.
14
18
 
15
- ## Local adaptation (the only change from upstream)
16
- - Relative import specifiers had their `.js` extension stripped (`'./jcs.js'`
17
- `'./jcs'`) so Metro + TS (`moduleResolution: bundler`) resolve to the `.ts` files.
19
+ ## Protocol note: what `aci/1` changed
20
+ The previous drop verified a *keyset endorsement*: the keyset carried a
21
+ `workload_identity` key, that key signed the keyset digest, and `workload_id` was
22
+ the digest of the identity key. `aci/1` removes all three. The keyset is now bound
23
+ straight into `report_data` — the statement is
24
+ `{"keyset_digest":…,"nonce":…,"purpose":"aci.report_data.v1"}` — so the hardware
25
+ quote is the only signature over it, and per-key custody (`evidence.key_custody`,
26
+ the dstack-KMS chain) is explicitly policy/caller territory (§9.1 checks 5–6),
27
+ which we do not check.
28
+
29
+ Practical consequence: a client written against the old shape does not degrade,
30
+ it *crashes* — `workload_keyset.workload_identity` is simply absent. That is what
31
+ broke sealed `phala/*` turns with `sealed shim: Cannot read properties of
32
+ undefined (reading 'public_key')` before this drop.
33
+
34
+ ## Local adaptations (the only changes from upstream)
35
+ 1. Relative import specifiers had their `.js` extension stripped (`'./jcs.js'` →
36
+ `'./jcs'`) so Metro + TS (`moduleResolution: bundler`) resolve to the `.ts` files.
37
+ 2. `jcs.ts` is kept as its own module. Upstream folded JCS into `crypto.ts` as a
38
+ sort-and-`JSON.stringify` helper; ours is the stricter RFC 8785 implementation
39
+ from the earlier drop (it *rejects* non-integer numbers rather than
40
+ mis-serializing them), and the E2EE AAD builders depend on it. `digest.ts`,
41
+ `receipt.ts`, and `session.ts` therefore import `jcsBytes` from `./jcs`
42
+ instead of `./crypto`.
43
+ 3. `report.ts` carries only `verifyReportBinding`. Upstream's `verifyQuote` and
44
+ `verifyComposeMeasurement` are omitted: `../../phalaSeal.ts` owns the quote (it
45
+ also gates TCB status and pins the measurements) and `../measurements.ts` owns
46
+ the event-log replay across all four RTMRs, not just RTMR3. Omitting them keeps
47
+ this tree dependency-free and `@phala/dcap-qvl` off every startup's import path.
48
+ 4. `e2ee.ts` and `e2ee-channel.ts` are carried forward from the earlier drop.
49
+ Upstream moved E2EE out of the verifier package ("specified by §6 but not
50
+ constructed by this verifier"); the wire format itself is unchanged and still
51
+ specified in `spec/e2ee-v2.md`, and the gateway still advertises
52
+ `supported_e2ee_versions: ["2"]`.
53
+ 5. `transcript.ts` (upstream's one-call `verifyService` + verdict rendering) is not
54
+ vendored — `phalaSeal.ts` composes its own verdict and enclave identity.
18
55
 
19
56
  Everything else is byte-for-byte upstream. The crypto runs on `globalThis.crypto`
20
- (Web Crypto: X25519, HKDF, AES-GCM, Ed25519, `getRandomValues`). In privateer-agent
21
- (Node ≥ 22) these are all native — **no polyfills needed** (unlike the treeview RN app,
22
- which bridges them via `react-native-quick-crypto`). Re-pull from upstream to update;
23
- re-apply only the `.js`-extension strip.
24
-
25
- ## What lives OUTSIDE this directory (and why)
26
- `../reportBinding.ts` `verifyAciReportBinding`, the algorithm dispatch for §10.1
27
- checks 2–6. Callers use it instead of importing `verifyReportBinding` from here.
28
-
29
- Upstream's verifier is Web-Crypto-only, so it throws `UnsupportedAlgorithmError` on
30
- an `ecdsa-secp256k1` keyset endorsement — which §4.3 explicitly permits alongside
31
- ed25519, and which the deployed `inference.phala.com` gateway actually uses. Rather
32
- than patch this tree (and re-patch it on every re-pull), the dispatch sits outside:
33
- ed25519 delegates here verbatim, secp256k1 takes a parallel path over `@noble/curves`,
34
- and any other algorithm still throws. Nothing here changed, so the re-pull recipe
35
- above stays exactly the `.js`-extension strip.
36
-
37
- If upstream ever adds secp256k1 (or a check 7) to `report.ts`, collapse
38
- `reportBinding.ts` back to a straight re-export — `tests/phalaReportBinding.test.ts`
39
- pins the behaviour either way.
57
+ (Web Crypto: X25519, HKDF, AES-GCM, Ed25519, SHA-384, `getRandomValues`). In
58
+ privateer-agent (Node ≥ 22) these are all native — **no polyfills needed** (unlike
59
+ the treeview RN app, which bridges them via `react-native-quick-crypto`).
60
+
61
+ Re-pull recipe: copy `clients/verifier-ts/src/*.ts`, strip the `.js` extensions,
62
+ then re-apply adaptations 2–5. `tests/phalaReportBinding.test.ts` pins the binding
63
+ behaviour against a real report captured from the live gateway.
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Cryptographic primitives, all via the Web Crypto API (`globalThis.crypto`) so
3
- * the same code runs in browsers and in Node 20+ with no third-party deps.
4
- * Only SHA-256 and Ed25519 verification are needed for Level 1.
3
+ * the same code runs in browsers and in Node 20+ with no dependencies. ACI's
4
+ * only signature algorithm is Ed25519 and its only hash is SHA-256 (spec
5
+ * Appendix B) — both are in Web Crypto, so nothing needs injecting.
5
6
  */
6
7
 
7
- import { AciFormatError, UnsupportedAlgorithmError } from './errors';
8
+ import { AciFormatError } from './errors';
8
9
 
9
10
  const subtle = globalThis.crypto.subtle;
10
11
 
@@ -23,20 +24,45 @@ export function fromHex(hex: string): Uint8Array {
23
24
  }
24
25
  const out = new Uint8Array(h.length / 2);
25
26
  for (let i = 0; i < out.length; i++) {
26
- const byte = Number.parseInt(h.substr(i * 2, 2), 16);
27
+ const byte = Number.parseInt(h.slice(i * 2, i * 2 + 2), 16);
27
28
  if (Number.isNaN(byte)) {
28
- throw new AciFormatError(`invalid hex at offset ${i * 2}: "${h.substr(i * 2, 2)}"`);
29
+ throw new AciFormatError(`invalid hex at offset ${i * 2}: "${h.slice(i * 2, i * 2 + 2)}"`);
29
30
  }
30
31
  out[i] = byte;
31
32
  }
32
33
  return out;
33
34
  }
34
35
 
36
+ /** Encode bytes as standard base64 (RFC 4648 §4, with padding) — the `_b64` field form (Appendix A). */
37
+ export function toBase64(bytes: Uint8Array): string {
38
+ let bin = '';
39
+ for (const b of bytes) bin += String.fromCharCode(b);
40
+ return btoa(bin);
41
+ }
42
+
43
+ /** Decode standard base64 to the exact underlying bytes. */
44
+ export function fromBase64(b64: string): Uint8Array {
45
+ let bin: string;
46
+ try {
47
+ bin = atob(b64);
48
+ } catch {
49
+ throw new AciFormatError('invalid base64');
50
+ }
51
+ const out = new Uint8Array(bin.length);
52
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
53
+ return out;
54
+ }
55
+
35
56
  /** SHA-256 of the given bytes. */
36
57
  export async function sha256(bytes: Uint8Array): Promise<Uint8Array> {
37
58
  return new Uint8Array(await subtle.digest('SHA-256', bytes as BufferSource));
38
59
  }
39
60
 
61
+ /** SHA-384 of the given bytes — the dstack RTMR replay hash (§9.1 policy). */
62
+ export async function sha384(bytes: Uint8Array): Promise<Uint8Array> {
63
+ return new Uint8Array(await subtle.digest('SHA-384', bytes as BufferSource));
64
+ }
65
+
40
66
  /** Lowercase-hex SHA-256 of the given bytes. */
41
67
  export async function sha256Hex(bytes: Uint8Array): Promise<string> {
42
68
  return toHex(await sha256(bytes));
@@ -44,16 +70,16 @@ export async function sha256Hex(bytes: Uint8Array): Promise<string> {
44
70
 
45
71
  /**
46
72
  * `sha256:<lowercase-hex>` digest string of the given bytes — the ACI digest
47
- * form (§3) used for `workload_id`, keyset digests, and body hashes.
73
+ * form (Appendix A) used for keyset digests, body hashes, and session ids.
48
74
  */
49
75
  export async function sha256Prefixed(bytes: Uint8Array): Promise<string> {
50
76
  return 'sha256:' + (await sha256Hex(bytes));
51
77
  }
52
78
 
53
79
  /**
54
- * Verify an Ed25519 signature (RFC 8032, §4.3/§8.5) over `message`.
55
- * `publicKeyRaw` is the 32-byte raw key; `signature` the 64-byte value.
56
- * Returns false on a bad signature or malformed key — never throws for those.
80
+ * Verify an Ed25519 signature (RFC 8032) over `message`. `publicKeyRaw` is the
81
+ * 32-byte raw key; `signature` the 64-byte value. Returns false on a bad
82
+ * signature or malformed key — never throws for those.
57
83
  */
58
84
  export async function verifyEd25519(
59
85
  publicKeyRaw: Uint8Array,
@@ -75,21 +101,3 @@ export async function verifyEd25519(
75
101
  return false;
76
102
  }
77
103
  }
78
-
79
- /**
80
- * Verify a signature by ACI signature `algo`, dispatching on the algorithm the
81
- * attested keyset entry declares. Only `ed25519` is verifiable here; every other
82
- * algorithm (including `ecdsa-secp256k1`) raises {@link UnsupportedAlgorithmError}.
83
- */
84
- export async function verifySignature(
85
- algo: string,
86
- publicKeyRaw: Uint8Array,
87
- signature: Uint8Array,
88
- message: Uint8Array,
89
- context: string,
90
- ): Promise<boolean> {
91
- if (algo === 'ed25519') {
92
- return verifyEd25519(publicKeyRaw, signature, message);
93
- }
94
- throw new UnsupportedAlgorithmError(algo, context);
95
- }
@@ -1,116 +1,52 @@
1
1
  /**
2
- * The ACI digest and canonical-signing-bytes constructions (§4.1, §4.2, §4.3,
3
- * §4.4, §4.7, §8.5, §9.2). Each returns the exact bytes/strings the spec pins in
4
- * spec/test-vectors.md, so they double as the byte-for-byte reference.
2
+ * The ACI digest constructions (Appendix A, §3.1, §3.2). Artifacts the service builds
3
+ * are hashed as the exact served bytes; the attestation statement is the one
4
+ * report payload a verifier constructs itself, as a fixed byte template whose
5
+ * inputs are restricted so no JSON escaping is ever needed.
5
6
  */
6
7
 
7
8
  import { jcsBytes } from './jcs';
8
- import type { JcsValue } from './jcs';
9
9
  import { sha256Hex, sha256Prefixed } from './crypto';
10
- import type { PublicKey, WorkloadKeyset, Receipt, SessionRecord } from './types';
10
+ import { AciFormatError } from './errors';
11
11
 
12
- /**
13
- * `workload_id` — the stable name of a workload (§4.1):
14
- * `"sha256:" || hex(sha256(JCS(public_key)))`.
15
- */
16
- export async function computeWorkloadId(publicKey: PublicKey): Promise<string> {
17
- return sha256Prefixed(jcsBytes({ algo: publicKey.algo, public_key: publicKey.public_key }));
18
- }
12
+ const DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
13
+ const NONCE_RE = /^[0-9a-f]{64}$/;
19
14
 
20
- /**
21
- * `workload_keyset_digest` (§4.2): `"sha256:" || hex(sha256(JCS(keyset)))`,
22
- * over the whole keyset object as given.
23
- */
24
- export async function computeKeysetDigest(keyset: WorkloadKeyset): Promise<string> {
25
- return sha256Prefixed(jcsBytes(keyset as JcsValue));
15
+ /** `workload_keyset_digest` (§3.1): sha256 over the keyset's JCS form. */
16
+ export async function computeKeysetDigest(keyset: unknown): Promise<string> {
17
+ return sha256Prefixed(jcsBytes(keyset));
26
18
  }
27
19
 
28
20
  /**
29
- * The attestation statement (§4.4) whose JCS is hashed into `report_data`.
30
- * `nonce` is the request's decoded value, or JSON `null` when the query
31
- * parameter was omitted (never the string `"null"`); pass `undefined`/`null` for
32
- * the omitted case.
21
+ * The exact attestation-statement bytes 3.2) for a keyset digest and the
22
+ * nonce the client sent `null`/`undefined` when the query parameter was
23
+ * omitted, which puts the JSON literal `null` in the template. Inputs outside
24
+ * the spec-pinned formats throw {@link AciFormatError}.
33
25
  */
34
26
  export function attestationStatement(
35
- workloadId: string,
36
- workloadKeysetDigest: string,
27
+ keysetDigest: string,
37
28
  nonce: string | null | undefined,
38
- ): JcsValue {
39
- return {
40
- purpose: 'aci.report_data.v1',
41
- workload_id: workloadId,
42
- workload_keyset_digest: workloadKeysetDigest,
43
- nonce: nonce ?? null,
44
- };
29
+ ): Uint8Array {
30
+ if (!DIGEST_RE.test(keysetDigest)) {
31
+ throw new AciFormatError(`keyset digest is not sha256:<64-hex>: "${keysetDigest}"`);
32
+ }
33
+ if (nonce != null && !NONCE_RE.test(nonce)) {
34
+ throw new AciFormatError('nonce must be exactly 64 lowercase hex characters (§3.2)');
35
+ }
36
+ const noncePart = nonce == null ? 'null' : `"${nonce}"`;
37
+ return new TextEncoder().encode(
38
+ `{"keyset_digest":"${keysetDigest}","nonce":${noncePart},"purpose":"aci.report_data.v1"}`,
39
+ );
45
40
  }
46
41
 
47
42
  /**
48
- * `report_data` (§4.4): `hex(sha256(JCS(attestation_statement)))` the raw
49
- * 32-byte digest as lowercase hex, with no `sha256:` prefix (it names a bare
50
- * report-data slot, not an ACI digest string).
43
+ * `report_data` (§3.2): SHA-256 of the attestation statement, as bare lowercase
44
+ * hex (it fills a report-data slot, not an ACI digest string). The TEE places
45
+ * these 32 bytes zero-padded to 64 in the quote's report-data field.
51
46
  */
52
47
  export async function computeReportData(
53
- workloadId: string,
54
- workloadKeysetDigest: string,
48
+ keysetDigest: string,
55
49
  nonce: string | null | undefined,
56
50
  ): Promise<string> {
57
- return sha256Hex(jcsBytes(attestationStatement(workloadId, workloadKeysetDigest, nonce)));
58
- }
59
-
60
- /** JCS bytes of the keyset endorsement payload (§4.3), signed by the identity key. */
61
- export function keysetEndorsementPayload(workloadKeysetDigest: string): Uint8Array {
62
- return jcsBytes({
63
- purpose: 'aci.keyset.endorsement.v1',
64
- workload_keyset_digest: workloadKeysetDigest,
65
- });
66
- }
67
-
68
- /** JCS bytes of the keyset revocation payload (§4.7), signed by the identity key. */
69
- export function keysetRevocationPayload(workloadKeysetDigest: string): Uint8Array {
70
- return jcsBytes({
71
- purpose: 'aci.keyset.revocation.v1',
72
- workload_keyset_digest: workloadKeysetDigest,
73
- });
74
- }
75
-
76
- /**
77
- * Canonical bytes a receipt signature covers (§8.5): the JCS of the whole
78
- * receipt with only `signature.value` removed (`algo` and `key_id`, and any
79
- * other signature fields, are retained). Unknown top-level fields and events are
80
- * preserved by canonicalizing the object as given (§3.2).
81
- */
82
- export function receiptSigningBytes(receipt: Receipt): Uint8Array {
83
- const { value: _omitted, ...signatureWithoutValue } = receipt.signature;
84
- const forSigning: JcsValue = {
85
- ...(receipt as unknown as { [k: string]: JcsValue }),
86
- signature: signatureWithoutValue as unknown as JcsValue,
87
- };
88
- return jcsBytes(forSigning);
89
- }
90
-
91
- /**
92
- * The content-addressing material for a session id (§9.2). The wire record omits
93
- * absent optional fields; the material restores `endpoint`, `identity`, and
94
- * `evidence.digest` as JSON `null`, and timestamps / raw evidence bytes are
95
- * excluded entirely.
96
- */
97
- export function sessionMaterial(record: SessionRecord): JcsValue {
98
- return {
99
- upstream_name: record.upstream_name,
100
- endpoint: record.endpoint ?? null,
101
- verifier_id: record.verifier_id,
102
- identity: record.identity ?? null,
103
- channel_binding: record.channel_binding,
104
- claims: record.claims,
105
- evidence_digest: record.evidence?.digest ?? null,
106
- };
107
- }
108
-
109
- /**
110
- * `session_id` (§9.2): `"as_" || hex(sha256(JCS(material)))`. Recomputing this
111
- * from a fetched record and comparing it to the id the signed receipt committed
112
- * to is what makes the session tamper-evident — there is no session signature.
113
- */
114
- export async function computeSessionId(record: SessionRecord): Promise<string> {
115
- return 'as_' + (await sha256Hex(jcsBytes(sessionMaterial(record))));
51
+ return sha256Hex(attestationStatement(keysetDigest, nonce));
116
52
  }
@@ -41,7 +41,9 @@ export async function openE2eeChannel(
41
41
  if (!verification.ok || verification.workloadKeysetDigest !== report.workload_keyset_digest) {
42
42
  throw new Error('openE2eeChannel: report is not verified — call verifyReportBinding and check .ok');
43
43
  }
44
- const keys = (report.attestation.workload_keyset.e2ee_public_keys ?? []) as Array<{
44
+ // Read the keys off the ESTABLISHED keyset the object whose JCS the
45
+ // verifier hashed to the digest the quote signed — not the report's copy.
46
+ const keys = (verification.keyset?.e2ee_public_keys ?? []) as Array<{
45
47
  algo: string;
46
48
  public_key: string;
47
49
  }>;
@@ -2,8 +2,7 @@
2
2
  * Errors raised by the verifier for conditions that are *not* ordinary
3
3
  * verification failures. A failed check (bad signature, wrong hash) is reported
4
4
  * as `ok: false` in the result objects — never thrown — so callers cannot ignore
5
- * it by forgetting a try/catch. These errors mean "the input is malformed or the
6
- * algorithm is outside this verifier's Level 1 / Web Crypto scope".
5
+ * it by forgetting a try/catch. These errors mean "the input is malformed".
7
6
  */
8
7
 
9
8
  /** Base class for every error this package throws. */
@@ -14,28 +13,10 @@ export class AciError extends Error {
14
13
  }
15
14
  }
16
15
 
17
- /** A JCS input violated the ACI subset (e.g. a non-integer number) or a hex/field value would not parse. */
16
+ /** An input value would not parse (hex, base64, JSON) or violates a spec-pinned format. */
18
17
  export class AciFormatError extends AciError {
19
18
  constructor(message: string) {
20
19
  super(message);
21
20
  this.name = 'AciFormatError';
22
21
  }
23
22
  }
24
-
25
- /**
26
- * A signature or identity algorithm that ACI defines but this Web-Crypto-only
27
- * verifier cannot check. `ecdsa-secp256k1` is the expected case: the curve is
28
- * absent from the Web Crypto API, so verify it against the reference
29
- * implementation or a Level 2 verifier profile instead.
30
- */
31
- export class UnsupportedAlgorithmError extends AciError {
32
- readonly algorithm: string;
33
- constructor(algorithm: string, context: string) {
34
- super(
35
- `unsupported algorithm "${algorithm}" for ${context}: this verifier supports only ed25519 via the Web Crypto API. ` +
36
- `secp256k1 is out of scope — verify it against the reference implementation or a Level 2 profile.`,
37
- );
38
- this.name = 'UnsupportedAlgorithmError';
39
- this.algorithm = algorithm;
40
- }
41
- }
@@ -1,43 +1,39 @@
1
1
  /**
2
- * @dstack/aci-verifier — a zero-dependency ACI Level 1 verifier.
2
+ * @phala/aci-verifier — a zero-dependency ACI verifier for the browser and node.
3
3
  *
4
- * Level 1 (receipt verification, §10.2) is fully implemented against an
5
- * established keyset. {@link verifyReportBinding} adds the cryptographic-binding
6
- * checks of Level 2 (§10.1 checks 2–6); the hardware quote, key custody, and
7
- * provenance checks (§10.1 checks 1, 7–10) are verifier-profile territory and
8
- * out of scope here. All crypto is Web Crypto (Ed25519, SHA-256); `ecdsa-secp256k1`
9
- * is unsupported (not in the Web Crypto API) and raises a clear error.
4
+ * Report binding (§9.1 checks 2–3) establishes the workload keyset: the served
5
+ * keyset canonicalizes to the digest the attestation statement hashes into
6
+ * `report_data`, which the hardware quote signs. Everything downstream — the
7
+ * E2EE key we seal to, receipt signing keys, TLS pins — is a member of that one
8
+ * quote-bound object, so nothing else needs its own signature. Receipt
9
+ * verification (§9.3) runs against an established keyset. All crypto here is
10
+ * Web Crypto (Ed25519, SHA-256/384); the quote itself is ../../phalaSeal.ts.
10
11
  */
11
12
 
12
- // Canonicalization (§3)
13
+ // Canonicalization (Appendix A)
13
14
  export { canonicalize, jcsBytes } from './jcs';
14
15
  export type { JcsValue } from './jcs';
15
16
 
16
17
  // Crypto primitives (Web Crypto only)
17
18
  export {
18
19
  sha256,
20
+ sha384,
19
21
  sha256Hex,
20
22
  sha256Prefixed,
21
23
  verifyEd25519,
22
- verifySignature,
23
24
  toHex,
24
25
  fromHex,
26
+ toBase64,
27
+ fromBase64,
25
28
  } from './crypto';
26
29
 
27
- // Digest & canonical-signing-bytes constructions (§4, §8.5, §9.2)
28
- export {
29
- computeWorkloadId,
30
- computeKeysetDigest,
31
- attestationStatement,
32
- computeReportData,
33
- keysetEndorsementPayload,
34
- keysetRevocationPayload,
35
- receiptSigningBytes,
36
- sessionMaterial,
37
- computeSessionId,
38
- } from './digest';
30
+ // Digest constructions (Appendix A, §3.1, §3.2)
31
+ export { computeKeysetDigest, attestationStatement, computeReportData } from './digest';
32
+
33
+ // Attested sessions: content addressing and evidence (§8, §9.3)
34
+ export { computeSessionId, checkSessionApiVersion, checkSessionEvidence } from './session';
39
35
 
40
- // E2EE AAD builders (§7.3)
36
+ // E2EE v2 AAD builders (spec/e2ee-v2.md §6)
41
37
  export {
42
38
  requestAad,
43
39
  requestAadString,
@@ -46,39 +42,37 @@ export {
46
42
  } from './e2ee';
47
43
  export type { AadCommon } from './e2ee';
48
44
 
49
- // E2EE channel to a verified workload — encrypt requests, decrypt replies (§7)
45
+ // E2EE v2 channel to a verified workload — encrypt requests, decrypt replies
50
46
  export { openE2eeChannel } from './e2ee-channel';
51
47
  export type { E2eeChannel } from './e2ee-channel';
52
48
 
53
- // Level 1 receipt verification (§10.2)
49
+ // Receipt verification (§9.3)
54
50
  export {
55
51
  verifyReceipt,
56
52
  findEvent,
57
53
  hashBody,
58
54
  checkRequestBodyHash,
59
- checkResponseWireHash,
60
- checkResponseCleartextHash,
55
+ checkResponseBodyHash,
61
56
  } from './receipt';
62
57
 
63
- // Level 2 report-binding checks 10.1 checks 2–6, no hardware quote)
58
+ // Report binding (§9.1 checks 2–3)
64
59
  export { verifyReportBinding } from './report';
65
60
  export type { ReportBindingOptions } from './report';
66
61
 
67
62
  // Errors
68
- export { AciError, AciFormatError, UnsupportedAlgorithmError } from './errors';
63
+ export { AciError, AciFormatError } from './errors';
69
64
 
70
65
  // Wire & result types
71
66
  export type {
72
- PublicKey,
73
- WorkloadIdentity,
74
- ReceiptSigningKey,
67
+ KeysetKey,
68
+ TlsKeyPin,
75
69
  WorkloadKeyset,
76
- ReceiptSignature,
77
- ReceiptEvent,
78
- Receipt,
79
- Endorsement,
70
+ SourceProvenance,
80
71
  Attestation,
81
72
  AttestationReport,
73
+ ReceiptEnvelope,
74
+ ReceiptEvent,
75
+ ReceiptPayload,
82
76
  SessionEvidence,
83
77
  SessionRecord,
84
78
  Check,
@@ -63,7 +63,12 @@ function serializeObject(obj: { [key: string]: JcsValue | undefined }): string {
63
63
  return out + '}';
64
64
  }
65
65
 
66
- /** Canonicalize and encode to UTF-8 bytes — the form fed to SHA-256 and signatures. */
67
- export function jcsBytes(value: JcsValue): Uint8Array {
68
- return new TextEncoder().encode(canonicalize(value));
66
+ /**
67
+ * Canonicalize and encode to UTF-8 bytes — the form fed to SHA-256 and
68
+ * signatures. Takes `unknown` because the values that get canonicalized are
69
+ * parsed server JSON (a keyset, a receipt document); anything outside the ACI
70
+ * subset is rejected by {@link canonicalize} rather than mis-serialized.
71
+ */
72
+ export function jcsBytes(value: unknown): Uint8Array {
73
+ return new TextEncoder().encode(canonicalize(value as JcsValue));
69
74
  }