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.
- package/package.json +1 -1
- package/patches/@earendil-works+pi-coding-agent+0.84.1.patch +321 -9
- package/src/config/moat.ts +115 -22
- package/src/engine/errors.ts +7 -0
- package/src/mcp/catalog.ts +41 -0
- package/src/providers/phala/aci-verifier/VENDORED.md +54 -30
- package/src/providers/phala/aci-verifier/crypto.ts +35 -27
- package/src/providers/phala/aci-verifier/digest.ts +31 -95
- package/src/providers/phala/aci-verifier/e2ee-channel.ts +3 -1
- package/src/providers/phala/aci-verifier/errors.ts +2 -21
- package/src/providers/phala/aci-verifier/index.ts +29 -35
- package/src/providers/phala/aci-verifier/jcs.ts +8 -3
- package/src/providers/phala/aci-verifier/receipt.ts +96 -88
- package/src/providers/phala/aci-verifier/report.ts +62 -85
- package/src/providers/phala/aci-verifier/session.ts +40 -0
- package/src/providers/phala/aci-verifier/types.ts +90 -80
- package/src/providers/phalaSeal.ts +8 -9
- package/src/providers/phala/reportBinding.ts +0 -193
package/src/mcp/catalog.ts
CHANGED
|
@@ -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:
|
|
1
|
+
# Vendored: the ACI verifier
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- **`
|
|
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
|
-
##
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
21
|
-
(Node ≥ 22) these are all native — **no polyfills needed** (unlike
|
|
22
|
-
which bridges them via `react-native-quick-crypto`).
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
4
|
-
*
|
|
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
|
|
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.
|
|
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.
|
|
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 (
|
|
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
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
|
10
|
+
import { AciFormatError } from './errors';
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* the
|
|
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
|
-
|
|
36
|
-
workloadKeysetDigest: string,
|
|
27
|
+
keysetDigest: string,
|
|
37
28
|
nonce: string | null | undefined,
|
|
38
|
-
):
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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` (§
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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
|
-
|
|
54
|
-
workloadKeysetDigest: string,
|
|
48
|
+
keysetDigest: string,
|
|
55
49
|
nonce: string | null | undefined,
|
|
56
50
|
): Promise<string> {
|
|
57
|
-
return sha256Hex(
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
* @
|
|
2
|
+
* @phala/aci-verifier — a zero-dependency ACI verifier for the browser and node.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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 (
|
|
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
|
|
28
|
-
export {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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 (§
|
|
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
|
|
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
|
-
//
|
|
49
|
+
// Receipt verification (§9.3)
|
|
54
50
|
export {
|
|
55
51
|
verifyReceipt,
|
|
56
52
|
findEvent,
|
|
57
53
|
hashBody,
|
|
58
54
|
checkRequestBodyHash,
|
|
59
|
-
|
|
60
|
-
checkResponseCleartextHash,
|
|
55
|
+
checkResponseBodyHash,
|
|
61
56
|
} from './receipt';
|
|
62
57
|
|
|
63
|
-
//
|
|
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
|
|
63
|
+
export { AciError, AciFormatError } from './errors';
|
|
69
64
|
|
|
70
65
|
// Wire & result types
|
|
71
66
|
export type {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
ReceiptSigningKey,
|
|
67
|
+
KeysetKey,
|
|
68
|
+
TlsKeyPin,
|
|
75
69
|
WorkloadKeyset,
|
|
76
|
-
|
|
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
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
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
|
}
|