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.
@@ -1,139 +1,149 @@
1
1
  /**
2
2
  * Wire shapes for the ACI artifacts this verifier reads, plus the result types
3
- * it returns. These mirror spec/aci.md §4, §5, §8, §9; only the fields the
3
+ * it returns. These mirror spec/aci.md §3, §4, §7, §8; only the fields the
4
4
  * verifier touches are typed precisely, with an index signature left open so
5
- * unknown extension fields (§3.2) survive canonicalization untouched.
5
+ * extension fields (Appendix B) are visible to callers.
6
6
  */
7
7
 
8
- import type { JcsValue } from './jcs';
9
-
10
- /** A public key object: `{ algo, public_key }` (§4.1). */
11
- export interface PublicKey {
8
+ /** A keyed public-key entry (§3.1) — receipt signing and E2EE keys. */
9
+ export interface KeysetKey {
10
+ key_id: string;
12
11
  algo: string;
13
12
  public_key: string;
14
- [key: string]: JcsValue | undefined;
13
+ [key: string]: unknown;
15
14
  }
16
15
 
17
- /** The workload identity4.2): the identity public key plus an optional subject. */
18
- export interface WorkloadIdentity {
19
- public_key: PublicKey;
20
- subject?: string | null;
21
- [key: string]: JcsValue | undefined;
16
+ /** A TLS pin entry 3.1): the certificate SPKI digest, optionally domain-scoped. */
17
+ export interface TlsKeyPin {
18
+ spki_sha256: string;
19
+ domain?: string;
20
+ [key: string]: unknown;
22
21
  }
23
22
 
24
- /** A receipt signing key entry (§4.2). */
25
- export interface ReceiptSigningKey {
26
- key_id: string;
27
- algo: string;
28
- public_key: string;
29
- [key: string]: JcsValue | undefined;
30
- }
31
-
32
- /** The workload keyset (§4.2). Digested with JCS to yield `workload_keyset_digest`. */
23
+ /**
24
+ * The workload keyset (§3.1) — the unit of workload identity. It travels as
25
+ * `workload_keyset`; its digest is the SHA-256 of the JCS form of the
26
+ * parsed object (Appendix A).
27
+ */
33
28
  export interface WorkloadKeyset {
34
- workload_identity: WorkloadIdentity;
35
- keyset_epoch: { version: number; not_after: number; [key: string]: JcsValue | undefined };
36
- receipt_signing_keys: ReceiptSigningKey[];
37
- e2ee_public_keys?: JcsValue[];
38
- tls_public_keys?: JcsValue[];
39
- [key: string]: JcsValue | undefined;
29
+ subject?: string | null;
30
+ not_after: number;
31
+ receipt_signing_keys: KeysetKey[];
32
+ e2ee_public_keys: KeysetKey[];
33
+ tls_public_keys?: TlsKeyPin[];
34
+ [key: string]: unknown;
40
35
  }
41
36
 
42
- /** A receipt signature block 8.2). `value` is dropped for canonical signing bytes (§8.5). */
43
- export interface ReceiptSignature {
44
- algo: string;
45
- key_id: string;
46
- value: string;
47
- [key: string]: JcsValue | undefined;
37
+ /** Source provenance4.1); each field is `null` when unknown. */
38
+ export interface SourceProvenance {
39
+ repo_url?: string | null;
40
+ repo_commit?: string | null;
41
+ image_digest?: string | null;
42
+ image_provenance?: unknown;
43
+ [key: string]: unknown;
48
44
  }
49
45
 
50
- /** A single receipt event8.3). Only `seq`/`type` are fixed; other fields are type-specific. */
51
- export interface ReceiptEvent {
52
- seq: number;
53
- type: string;
54
- [key: string]: JcsValue | undefined;
46
+ /** The `attestation` object of a report 4.1). `evidence` is policy-defined (§4.2). */
47
+ export interface Attestation {
48
+ tee_type: string;
49
+ workload_keyset: unknown;
50
+ report_data: string;
51
+ source_provenance?: SourceProvenance | null;
52
+ evidence?: unknown;
53
+ [key: string]: unknown;
55
54
  }
56
55
 
57
- /** An inference receipt8.2). */
58
- export interface Receipt {
56
+ /** An attestation report4.1). */
57
+ export interface AttestationReport {
59
58
  api_version: string;
60
- receipt_id: string;
61
- workload_id: string;
62
59
  workload_keyset_digest: string;
63
- event_log: ReceiptEvent[];
64
- signature: ReceiptSignature;
65
- [key: string]: JcsValue | undefined;
60
+ attestation: Attestation;
61
+ service_capabilities?: { supported_e2ee_versions?: string[]; [key: string]: unknown };
62
+ [key: string]: unknown;
66
63
  }
67
64
 
68
- /** The keyset endorsement / revocation signature block (§4.3, §5.1). */
69
- export interface Endorsement {
70
- algo: string;
71
- value: string;
72
- [key: string]: JcsValue | undefined;
65
+ /**
66
+ * The receipt document served by `GET /v1/aci/receipts/{id}` (§7.2): the
67
+ * §7.3 payload members plus `key_id` and `signature`. The signature covers
68
+ * JCS(document minus `signature`).
69
+ */
70
+ export interface ReceiptEnvelope {
71
+ key_id: string;
72
+ signature: string;
73
+ [key: string]: unknown;
73
74
  }
74
75
 
75
- /** The `attestation` object of a report 5.1); only the fields Level 1 reads are typed. */
76
- export interface Attestation {
77
- workload_keyset: WorkloadKeyset;
78
- report_data: string;
79
- keyset_endorsement: Endorsement;
80
- freshness?: { fetched_at?: number; stale_after?: number; [key: string]: JcsValue | undefined };
81
- [key: string]: JcsValue | undefined;
76
+ /** A receipt event7.3): `type` plus type-specific fields; order is array order. */
77
+ export interface ReceiptEvent {
78
+ type: string;
79
+ body_hash?: string;
80
+ [key: string]: unknown;
82
81
  }
83
82
 
84
- /** An attestation report5.1). */
85
- export interface AttestationReport {
83
+ /** The receipt payload the envelope signs 7.3). */
84
+ export interface ReceiptPayload {
86
85
  api_version: string;
87
- workload_id: string;
86
+ receipt_id: string;
87
+ chat_id?: string | null;
88
+ model?: string | null;
88
89
  workload_keyset_digest: string;
89
- attestation: Attestation;
90
- [key: string]: JcsValue | undefined;
90
+ endpoint: string;
91
+ method: string;
92
+ served_at: number;
93
+ event_log: ReceiptEvent[];
94
+ [key: string]: unknown;
91
95
  }
92
96
 
93
- /** A verifier-provided evidence block on a session record (§9.2). */
97
+ /** A session evidence block (§8.2): a base64 data URI plus the digest of its decoded bytes. */
94
98
  export interface SessionEvidence {
95
- digest?: string | null;
99
+ digest: string;
96
100
  data?: string;
97
- [key: string]: JcsValue | undefined;
101
+ [key: string]: unknown;
98
102
  }
99
103
 
100
104
  /**
101
- * An attested session record (§9.2). The `session_id` is recomputed from the
102
- * named fields; absent optional fields (`endpoint`, `identity`, `evidence.digest`)
103
- * are restored as JSON `null` in the content-addressing material.
105
+ * An attested session record (§8.2). Its id is the SHA-256 of the JCS form
106
+ * of the parsed document ({@link computeSessionId}).
104
107
  */
105
108
  export interface SessionRecord {
109
+ api_version: string;
106
110
  upstream_name: string;
107
111
  endpoint?: string | null;
108
112
  verifier_id: string;
109
- identity?: JcsValue;
110
- channel_binding: JcsValue[];
111
- claims: JcsValue;
112
- evidence?: SessionEvidence | null;
113
- [key: string]: JcsValue | undefined;
113
+ established_at: number;
114
+ expires_at: number;
115
+ identity?: unknown;
116
+ channel_binding: unknown[];
117
+ claims: unknown;
118
+ evidence: SessionEvidence;
119
+ [key: string]: unknown;
114
120
  }
115
121
 
116
122
  /** Outcome of one named verification check. */
117
123
  export interface Check {
118
- /** Stable machine-readable id, e.g. `signature`, `workload_id`. */
124
+ /** Stable machine-readable id, e.g. `signature`, `report_data`. */
119
125
  name: string;
120
126
  ok: boolean;
121
127
  /** Human-readable detail, present when the check fails. */
122
128
  detail?: string;
123
129
  }
124
130
 
125
- /** Result of {@link verifyReceipt}: overall pass plus the individual §10.2 checks. */
131
+ /** Result of {@link verifyReceipt}: overall pass plus the individual §9.3 checks. */
126
132
  export interface ReceiptVerification {
127
133
  ok: boolean;
128
134
  checks: Check[];
135
+ /** The receipt document read as its §7.3 payload members. */
136
+ payload?: ReceiptPayload;
129
137
  }
130
138
 
131
- /** Result of {@link verifyReportBinding}: overall pass, the §10.1 checks, and the derived identity. */
139
+ /**
140
+ * Result of {@link verifyReportBinding}: overall pass, the checks, and the
141
+ * keyset established from the report — the digest is recomputed over the
142
+ * served keyset object's JCS form (§3.1).
143
+ */
132
144
  export interface ReportVerification {
133
145
  ok: boolean;
134
146
  checks: Check[];
135
- /** `workload_id` recomputed from the report's keyset (§4.1). */
136
- workloadId: string;
137
- /** `workload_keyset_digest` recomputed from the report's keyset (§4.2). */
138
- workloadKeysetDigest: string;
147
+ workloadKeysetDigest?: string;
148
+ keyset?: WorkloadKeyset;
139
149
  }
@@ -8,13 +8,16 @@
8
8
  // (`${server}/api/sealed/phala`, treeview/server/routes/sealed.js) injects PHALA_API_KEY
9
9
  // and forwards ciphertext — it can't read prompts or responses.
10
10
  //
11
- // Crypto is the vendored @dstack/aci-verifier (./phala/aci-verifier), pure Web Crypto
11
+ // Crypto is the vendored aci-verifier (./phala/aci-verifier), pure Web Crypto
12
12
  // (X25519/HKDF/AES-GCM/Ed25519). Node ≥ 22 provides all of it on globalThis.crypto —
13
13
  // no polyfills, unlike the RN app.
14
14
  //
15
15
  // Two-layer attestation, fail-secure:
16
- // (1) verifyAciReportBinding — the report's crypto binding (keyset digest,
17
- // report_data == statement(nonce), endorsement sig). Self-attesting alone.
16
+ // (1) verifyReportBinding — the report's crypto binding (§9.1 checks 2–3): the
17
+ // served keyset canonicalizes to the digest that the attestation statement for
18
+ // OUR nonce hashes into report_data, and the keyset has not expired. Every key
19
+ // we go on to use — the X25519 key we seal to above all — is a member of that
20
+ // one object, so the binding covers them without a separate signature.
18
21
  // (2) verifyHardwareQuote — the hardware root: @phala/dcap-qvl verifies the TDX quote
19
22
  // against Intel collateral and binds the quote's report_data to (1)'s statement
20
23
  // digest. requireQuote defaults TRUE; PRIVATEER_PHALA_REQUIRE_QUOTE=0 drops it
@@ -23,17 +26,13 @@
23
26
  import type { Report } from "@phala/dcap-qvl";
24
27
  import {
25
28
  openE2eeChannel,
29
+ verifyReportBinding,
26
30
  toHex,
27
31
  fromHex,
28
32
  type AttestationReport,
29
33
  type ReportVerification,
30
34
  type E2eeChannel,
31
35
  } from "./phala/aci-verifier/index.ts";
32
- // Not the vendored verifyReportBinding directly: the deployed gateway signs its
33
- // keyset endorsement with ecdsa-secp256k1, which upstream's Web-Crypto-only verifier
34
- // refuses. This wrapper delegates ed25519 to it unchanged and adds the secp256k1 arm
35
- // the spec allows (§4.3), leaving aci-verifier/ pristine for re-pulls.
36
- import { verifyAciReportBinding } from "./phala/reportBinding.ts";
37
36
  import {
38
37
  parseEventLog,
39
38
  appIdentityFrom,
@@ -153,7 +152,7 @@ async function establishAttestation(): Promise<VerifiedAttestation> {
153
152
  if (!res.ok) throw new Error(`phala attestation HTTP ${res.status}`);
154
153
  const report = (await res.json()) as AttestationReport;
155
154
 
156
- const verification = await verifyAciReportBinding(report, nonce);
155
+ const verification = await verifyReportBinding(report, nonce);
157
156
  if (!verification.ok) {
158
157
  const failed = verification.checks.filter((c) => !c.ok).map((c) => c.name).join(", ");
159
158
  throw new Error(`phala attestation binding failed: ${failed}`);
@@ -1,193 +0,0 @@
1
- // Algorithm dispatch for the ACI report-binding checks (§10.1 checks 2–6).
2
- //
3
- // The ACI spec (§4.3) allows the keyset endorsement to be signed with EITHER
4
- // `ed25519` OR `ecdsa-secp256k1` — the former because "every primitive in it is
5
- // available in the Web Crypto API", the latter for "clients in the EVM/dstack
6
- // ecosystem". Upstream's reference TS verifier implements only the Web Crypto half:
7
- // `verifySignature` throws `UnsupportedAlgorithmError` on secp256k1
8
- // (aci-verifier/crypto.ts), and `verifyReportBinding` propagates that.
9
- //
10
- // The deployed gateway (inference.phala.com) signs with `ecdsa-secp256k1`, so the
11
- // vendored verifier can never attest it — a limit of upstream's CLIENT, not of the
12
- // spec or the gateway. Verified live 2026-07-31: attestation fetched, endorsement
13
- // rejected with UnsupportedAlgorithmError.
14
- //
15
- // This module owns the dispatch so `aci-verifier/` stays byte-for-byte upstream
16
- // (see its VENDORED.md — only the `.js`-extension strip diverges, and re-pulls stay
17
- // mechanical):
18
- // ed25519 → delegate to the vendored verifyReportBinding, verbatim
19
- // ecdsa-secp256k1 → the same checks 2–6, with check 5 done over @noble/curves
20
- // anything else → still throws (never a silent pass)
21
- //
22
- // The secp256k1 path deliberately mirrors report.ts check-for-check, in the same
23
- // order and with the same check names, so a caller cannot tell which path ran.
24
-
25
- import { secp256k1 } from "@noble/curves/secp256k1.js";
26
- import {
27
- verifyReportBinding,
28
- computeWorkloadId,
29
- computeKeysetDigest,
30
- computeReportData,
31
- keysetEndorsementPayload,
32
- sha256,
33
- fromHex,
34
- UnsupportedAlgorithmError,
35
- type AttestationReport,
36
- type Check,
37
- type ReportVerification,
38
- type ReportBindingOptions,
39
- } from "./aci-verifier/index.ts";
40
-
41
- const ED25519 = "ed25519";
42
- const SECP256K1 = "ecdsa-secp256k1";
43
-
44
- /**
45
- * Verify a report's cryptographic bindings for `nonce` (§10.1 checks 2–6),
46
- * dispatching on the algorithm the attested identity key declares. Drop-in
47
- * replacement for the vendored `verifyReportBinding`: same arguments, same result
48
- * shape, same "a failed check is `ok: false`, never thrown" contract.
49
- *
50
- * Like upstream, this is the crypto-binding half only — compose it with a hardware
51
- * quote verifier (phalaSeal.ts `verifyHardwareQuote`) for Level 2.
52
- */
53
- export async function verifyAciReportBinding(
54
- report: AttestationReport,
55
- nonce: string | null | undefined,
56
- options: ReportBindingOptions = {},
57
- ): Promise<ReportVerification> {
58
- const algo = report.attestation.workload_keyset.workload_identity.public_key.algo;
59
- if (algo === ED25519) return verifyReportBinding(report, nonce, options);
60
- if (algo !== SECP256K1) {
61
- // Same fail-closed posture as upstream: an algorithm we cannot check is a
62
- // refusal, not a pass.
63
- throw new UnsupportedAlgorithmError(algo, "keyset endorsement (§4.3)");
64
- }
65
- return verifySecp256k1ReportBinding(report, nonce, options);
66
- }
67
-
68
- async function verifySecp256k1ReportBinding(
69
- report: AttestationReport,
70
- nonce: string | null | undefined,
71
- options: ReportBindingOptions,
72
- ): Promise<ReportVerification> {
73
- const now = options.now ?? Math.floor(Date.now() / 1000);
74
- const checks: Check[] = [];
75
-
76
- const keyset = report.attestation.workload_keyset;
77
- const identityKey = keyset.workload_identity.public_key;
78
-
79
- // Check 2: workload_id == digest of the identity public key in the report's keyset.
80
- const workloadId = await computeWorkloadId(identityKey);
81
- pushEqual(checks, "workload_id", report.workload_id, workloadId);
82
-
83
- // Check 3: workload_keyset_digest == digest of the report's keyset.
84
- const workloadKeysetDigest = await computeKeysetDigest(keyset);
85
- pushEqual(checks, "workload_keyset_digest", report.workload_keyset_digest, workloadKeysetDigest);
86
-
87
- // Check 4 (binding half): report_data == the §4.4 statement digest for this nonce.
88
- // The hardware-evidence-binds-report_data half is verifyHardwareQuote's job.
89
- const expectedReportData = await computeReportData(workloadId, workloadKeysetDigest, nonce);
90
- pushEqual(checks, "report_data", report.attestation.report_data, expectedReportData);
91
-
92
- // Check 5: keyset endorsement verifies under the identity key, algo matching.
93
- const endorsement = report.attestation.keyset_endorsement;
94
- if (endorsement.algo !== identityKey.algo) {
95
- checks.push({
96
- name: "keyset_endorsement",
97
- ok: false,
98
- detail: `endorsement.algo "${endorsement.algo}" != identity key algo "${identityKey.algo}"`,
99
- });
100
- } else {
101
- const ok = await verifySecp256k1(
102
- identityKey.public_key,
103
- endorsement.value,
104
- keysetEndorsementPayload(workloadKeysetDigest),
105
- );
106
- checks.push({
107
- name: "keyset_endorsement",
108
- ok,
109
- ...(ok ? {} : { detail: "endorsement signature failed under identity key" }),
110
- });
111
- }
112
-
113
- // Check 6: freshness. Nonce binding is check 4; here bound the epoch and, when
114
- // the profile trusts it, the declared validity window.
115
- const notAfter = keyset.keyset_epoch.not_after;
116
- const epochOk = now < notAfter;
117
- checks.push({
118
- name: "keyset_epoch.not_after",
119
- ok: epochOk,
120
- ...(epochOk ? {} : { detail: `now ${now} >= not_after ${notAfter}` }),
121
- });
122
- if (options.trustPlatformClock) {
123
- const freshness = report.attestation.freshness;
124
- const fetchedAt = freshness?.fetched_at;
125
- const staleAfter = freshness?.stale_after;
126
- const windowOk =
127
- typeof fetchedAt === "number" &&
128
- typeof staleAfter === "number" &&
129
- fetchedAt <= now &&
130
- now < staleAfter;
131
- checks.push({
132
- name: "freshness_window",
133
- ok: windowOk,
134
- ...(windowOk ? {} : { detail: `now ${now} outside [${fetchedAt}, ${staleAfter})` }),
135
- });
136
- }
137
-
138
- return { ok: checks.every((c) => c.ok), checks, workloadId, workloadKeysetDigest };
139
- }
140
-
141
- /**
142
- * §4.3 secp256k1 endorsement: a 64-byte `r || s` signature over
143
- * `sha256(payload bytes)`. Returns false on malformed input rather than throwing —
144
- * a bad signature is a failed check, not an exception.
145
- *
146
- * NOT the §8.5 *receipt* shape, which is a 65-byte recoverable `r || s || v` and
147
- * where the spec says 64-byte signatures MUST be rejected. Different shapes; easy
148
- * to conflate if this ever grows a receipt path.
149
- */
150
- async function verifySecp256k1(
151
- publicKeyHex: string,
152
- signatureHex: string,
153
- payload: Uint8Array,
154
- ): Promise<boolean> {
155
- try {
156
- const sig = fromHex(signatureHex);
157
- if (sig.length !== 64) return false; // r||s only; DER / recoverable forms are not §4.3
158
- const msgHash = await sha256(payload);
159
- return secp256k1.verify(sig, msgHash, publicKey(publicKeyHex), {
160
- prehash: false, // we hand it the sha256 digest, per §4.3
161
- // Accept high-s as well as low-s. ECDSA malleability is meaningless for a
162
- // signature over a FIXED payload — an attacker who can flip s already has a
163
- // valid endorsement and still cannot sign a different keyset digest. Leaving
164
- // the default on would reject ~half of otherwise-valid endorsements from any
165
- // signer that doesn't normalize, as an intermittent attestation failure.
166
- lowS: false,
167
- });
168
- } catch {
169
- return false;
170
- }
171
- }
172
-
173
- /**
174
- * Identity key bytes. §7.1 pins secp256k1 public keys as 65-byte uncompressed SEC1
175
- * and requires that "the 64-byte uncompressed form without the `0x04` prefix MUST be
176
- * accepted and treated as the same key" — so restore the prefix when it's absent.
177
- * The live gateway sends the 65-byte form; do NOT prefix that one again.
178
- */
179
- function publicKey(hex: string): Uint8Array {
180
- const raw = fromHex(hex);
181
- if (raw.length === 64) {
182
- const sec1 = new Uint8Array(65);
183
- sec1[0] = 0x04;
184
- sec1.set(raw, 1);
185
- return sec1;
186
- }
187
- return raw;
188
- }
189
-
190
- function pushEqual(checks: Check[], name: string, actual: string, expected: string): void {
191
- const ok = actual === expected;
192
- checks.push({ name, ok, ...(ok ? {} : { detail: `report ${actual} != recomputed ${expected}` }) });
193
- }