secretvm-verify 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +312 -0
- package/dist/agent.d.ts +29 -0
- package/dist/agent.js +353 -0
- package/dist/agent.js.map +1 -0
- package/dist/amd.d.ts +2 -0
- package/dist/amd.js +287 -0
- package/dist/amd.js.map +1 -0
- package/dist/artifacts.d.ts +35 -0
- package/dist/artifacts.js +105 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/chains.d.ts +15 -0
- package/dist/chains.js +50 -0
- package/dist/chains.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +318 -0
- package/dist/cli.js.map +1 -0
- package/dist/cpu.d.ts +9 -0
- package/dist/cpu.js +54 -0
- package/dist/cpu.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/nvidia.d.ts +2 -0
- package/dist/nvidia.js +182 -0
- package/dist/nvidia.js.map +1 -0
- package/dist/rtmr.d.ts +10 -0
- package/dist/rtmr.js +45 -0
- package/dist/rtmr.js.map +1 -0
- package/dist/sevGctx.d.ts +38 -0
- package/dist/sevGctx.js +213 -0
- package/dist/sevGctx.js.map +1 -0
- package/dist/tdx.d.ts +11 -0
- package/dist/tdx.js +371 -0
- package/dist/tdx.js.map +1 -0
- package/dist/types.d.ts +18 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/dist/vm.d.ts +6 -0
- package/dist/vm.js +208 -0
- package/dist/vm.js.map +1 -0
- package/dist/workload.d.ts +62 -0
- package/dist/workload.js +253 -0
- package/dist/workload.js.map +1 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# secretvm-verify
|
|
2
|
+
|
|
3
|
+
Attestation verification SDK for confidential computing environments. Verifies Intel TDX, AMD SEV-SNP, and NVIDIA GPU attestation quotes, with end-to-end Secret VM verification that validates CPU attestation, GPU attestation, and the cryptographic bindings between them.
|
|
4
|
+
|
|
5
|
+
## What it verifies
|
|
6
|
+
|
|
7
|
+
- **Intel TDX** — Parses a TDX Quote v4, verifies the ECDSA-P256 signature chain (PCK -> Intermediate -> Root), validates QE report binding, and checks TCB status against Intel's Provisioning Certification Service.
|
|
8
|
+
- **AMD SEV-SNP** — Parses a SEV-SNP attestation report, fetches the VCEK certificate from AMD's Key Distribution Service, verifies the ECDSA-P384 report signature, and validates the certificate chain (VCEK -> ASK -> ARK).
|
|
9
|
+
- **NVIDIA GPU** — Submits GPU attestation evidence to NVIDIA's Remote Attestation Service (NRAS), verifies the returned JWT signatures against NVIDIA's published JWKS keys, and extracts per-GPU attestation claims.
|
|
10
|
+
- **SecretVM workload** — Given a TDX or SEV-SNP quote and a `docker-compose.yaml`, determines whether the quote was produced by a known SecretVM image and verifies the exact compose file that was booted.
|
|
11
|
+
- **Secret VM** — End-to-end verification that connects to a VM's attestation endpoints, verifies CPU and GPU attestation, and validates TLS and GPU cryptographic bindings.
|
|
12
|
+
- **ERC-8004 Agent verification** — End-to-end verification of on-chain AI agents registered under the [ERC-8004](https://eips.ethereum.org/EIPS/eip-8004) standard. Resolves agent metadata from any supported blockchain (Ethereum, Base, Arbitrum, Polygon, and 14 more), discovers the agent's TEE attestation endpoints, and runs the full verification flow. Three composable functions:
|
|
13
|
+
- **`resolveAgent`** — Queries the on-chain registry contract for the agent's metadata.
|
|
14
|
+
- **`verifyAgent`** — Takes agent metadata and runs full TEE verification against the agent's declared endpoints.
|
|
15
|
+
- **`checkAgent`** — End-to-end: resolves the agent on-chain, then verifies it.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install secretvm-verify
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
### Verify a Secret VM (recommended)
|
|
26
|
+
|
|
27
|
+
The simplest way to verify a VM — handles CPU detection, GPU detection, and all binding checks automatically.
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { checkSecretVm } from 'secretvm-verify';
|
|
31
|
+
|
|
32
|
+
const result = await checkSecretVm('my-vm.example.com');
|
|
33
|
+
|
|
34
|
+
console.log(result.valid); // true if all checks pass
|
|
35
|
+
console.log(result.attestationType); // "SECRET-VM"
|
|
36
|
+
console.log(result.checks); // { tls_cert_obtained: true, cpu_attestation_valid: true, ... }
|
|
37
|
+
console.log(result.report); // { tls_fingerprint: "...", cpu: {...}, cpu_type: "TDX", ... }
|
|
38
|
+
console.log(result.errors); // [] if no errors
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Verify an ERC-8004 agent
|
|
42
|
+
|
|
43
|
+
Verify an AI agent registered on-chain using the ERC-8004 standard. Supports 18 chains including Ethereum, Base, Arbitrum, and more.
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { checkAgent } from 'secretvm-verify';
|
|
47
|
+
|
|
48
|
+
// End-to-end: resolve on-chain + verify TEE attestation
|
|
49
|
+
const result = await checkAgent(38114, 'base');
|
|
50
|
+
|
|
51
|
+
console.log(result.valid); // true if all checks pass
|
|
52
|
+
console.log(result.attestationType); // "ERC-8004"
|
|
53
|
+
console.log(result.checks); // { agent_resolved: true, metadata_valid: true, ... }
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
You can also work with the individual steps:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { resolveAgent, verifyAgent } from 'secretvm-verify';
|
|
60
|
+
|
|
61
|
+
// Step 1: Resolve agent metadata from the blockchain
|
|
62
|
+
const metadata = await resolveAgent(38114, 'base');
|
|
63
|
+
console.log(metadata.name); // Agent name
|
|
64
|
+
console.log(metadata.services); // [{ name: "teequote", endpoint: "..." }, ...]
|
|
65
|
+
console.log(metadata.supportedTrust); // ["tee-attestation"]
|
|
66
|
+
|
|
67
|
+
// Step 2: Verify the agent's TEE attestation
|
|
68
|
+
const result = await verifyAgent(metadata);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**RPC configuration:** Set `SECRETVM_RPC_BASE` (or `SECRETVM_RPC_<CHAIN>`) environment variable to use your own RPC endpoint. Falls back to public RPCs if not set.
|
|
72
|
+
|
|
73
|
+
### Resolve SecretVM version from a quote
|
|
74
|
+
|
|
75
|
+
Given a TDX or SEV-SNP quote, determine which official SecretVM template and version produced it:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { resolveSecretVmVersion } from 'secretvm-verify';
|
|
79
|
+
import { readFileSync } from 'fs';
|
|
80
|
+
|
|
81
|
+
const result = resolveSecretVmVersion(readFileSync('cpu_quote.txt', 'utf8'));
|
|
82
|
+
if (result) {
|
|
83
|
+
console.log(result.template_name); // e.g. "small"
|
|
84
|
+
console.log(result.artifacts_ver); // e.g. "v0.0.25"
|
|
85
|
+
} else {
|
|
86
|
+
console.log('Not a known SecretVM');
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Verify a workload (quote + docker-compose)
|
|
91
|
+
|
|
92
|
+
Verify that a quote was produced by a known SecretVM *and* that it was running a specific `docker-compose.yaml`:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { verifyWorkload, formatWorkloadResult } from 'secretvm-verify';
|
|
96
|
+
import { readFileSync } from 'fs';
|
|
97
|
+
|
|
98
|
+
// Auto-detects TDX vs SEV-SNP:
|
|
99
|
+
const result = verifyWorkload(
|
|
100
|
+
readFileSync('cpu_quote.txt', 'utf8'),
|
|
101
|
+
readFileSync('docker-compose.yaml', 'utf8'),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
console.log(result.status); // "authentic_match" | "authentic_mismatch" | "not_authentic"
|
|
105
|
+
console.log(result.template_name); // e.g. "small" (undefined when not_authentic)
|
|
106
|
+
console.log(result.artifacts_ver); // e.g. "v0.0.25" (undefined when not_authentic)
|
|
107
|
+
console.log(result.env); // e.g. "prod" (undefined when not_authentic)
|
|
108
|
+
console.log(formatWorkloadResult(result)); // human-readable summary
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Verify a CPU quote (auto-detect TDX vs SEV-SNP)
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { checkCpuAttestation } from 'secretvm-verify';
|
|
115
|
+
import { readFileSync } from 'fs';
|
|
116
|
+
|
|
117
|
+
const result = await checkCpuAttestation(readFileSync('cpu_quote.txt', 'utf8'));
|
|
118
|
+
|
|
119
|
+
console.log(result.attestationType); // "TDX" or "SEV-SNP"
|
|
120
|
+
console.log(result.valid);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## API reference
|
|
124
|
+
|
|
125
|
+
### `AttestationResult`
|
|
126
|
+
|
|
127
|
+
All functions return an `AttestationResult` with these fields:
|
|
128
|
+
|
|
129
|
+
| Field | Type | Description |
|
|
130
|
+
|-------|------|-------------|
|
|
131
|
+
| `valid` | `boolean` | Overall pass/fail |
|
|
132
|
+
| `attestationType` | `string` | `"TDX"`, `"SEV-SNP"`, `"NVIDIA-GPU"`, or `"SECRET-VM"` |
|
|
133
|
+
| `checks` | `Record<string, boolean>` | Individual verification steps |
|
|
134
|
+
| `report` | `Record<string, any>` | Parsed attestation fields |
|
|
135
|
+
| `errors` | `string[]` | Error messages for failed checks |
|
|
136
|
+
|
|
137
|
+
### Functions
|
|
138
|
+
|
|
139
|
+
#### `checkSecretVm(url, product?)`
|
|
140
|
+
|
|
141
|
+
End-to-end Secret VM verification. Connects to `<url>:29343`, fetches CPU and GPU quotes, verifies both, and checks TLS and GPU bindings.
|
|
142
|
+
|
|
143
|
+
**Parameters:**
|
|
144
|
+
- `url` — VM address (e.g., `"my-vm.example.com"`, `"https://my-vm:29343"`)
|
|
145
|
+
- `product` — AMD product name (`"Genoa"`, `"Milan"`, `"Turin"`). Only needed for SEV-SNP, auto-detected if omitted.
|
|
146
|
+
|
|
147
|
+
#### `checkCpuAttestation(data, product?)`
|
|
148
|
+
|
|
149
|
+
Auto-detects Intel TDX vs AMD SEV-SNP and delegates to the appropriate function.
|
|
150
|
+
|
|
151
|
+
#### `checkTdxCpuAttestation(data)`
|
|
152
|
+
|
|
153
|
+
Verifies an Intel TDX Quote v4.
|
|
154
|
+
|
|
155
|
+
#### `checkSevCpuAttestation(data, product?)`
|
|
156
|
+
|
|
157
|
+
Verifies an AMD SEV-SNP attestation report.
|
|
158
|
+
|
|
159
|
+
#### `checkNvidiaGpuAttestation(data)`
|
|
160
|
+
|
|
161
|
+
Verifies NVIDIA GPU attestation via NRAS.
|
|
162
|
+
|
|
163
|
+
#### `resolveSecretVmVersion(data)`
|
|
164
|
+
|
|
165
|
+
Looks up a quote in the SecretVM artifact registry. Returns the matching template name and version, or `null` if not found.
|
|
166
|
+
|
|
167
|
+
#### `verifyWorkload(data, dockerComposeYaml)`
|
|
168
|
+
|
|
169
|
+
Auto-detects quote type and verifies that it was produced by a known SecretVM running the given docker-compose.
|
|
170
|
+
|
|
171
|
+
#### `verifyTdxWorkload(data, dockerComposeYaml)`
|
|
172
|
+
|
|
173
|
+
TDX-specific workload verification.
|
|
174
|
+
|
|
175
|
+
#### `verifySevWorkload(data, dockerComposeYaml)`
|
|
176
|
+
|
|
177
|
+
SEV-SNP-specific workload verification.
|
|
178
|
+
|
|
179
|
+
#### `formatWorkloadResult(result)`
|
|
180
|
+
|
|
181
|
+
Formats a `WorkloadResult` as a human-readable string.
|
|
182
|
+
|
|
183
|
+
### `WorkloadResult`
|
|
184
|
+
|
|
185
|
+
| Field | Type | Description |
|
|
186
|
+
|-------|------|-------------|
|
|
187
|
+
| `status` | `WorkloadStatus` | `"authentic_match"`, `"authentic_mismatch"`, or `"not_authentic"` |
|
|
188
|
+
| `template_name` | `string \| undefined` | SecretVM template (e.g. `"small"`) |
|
|
189
|
+
| `artifacts_ver` | `string \| undefined` | Artifacts version (e.g. `"v0.0.25"`) |
|
|
190
|
+
| `env` | `string \| undefined` | Environment (e.g. `"prod"`) |
|
|
191
|
+
|
|
192
|
+
### ERC-8004 Agent Functions
|
|
193
|
+
|
|
194
|
+
#### `checkAgent(agentId, chain)`
|
|
195
|
+
|
|
196
|
+
End-to-end ERC-8004 agent verification. Resolves agent metadata from the on-chain registry, then verifies TEE attestation.
|
|
197
|
+
|
|
198
|
+
**Parameters:**
|
|
199
|
+
- `agentId` — The agent's on-chain token ID (number)
|
|
200
|
+
- `chain` — Chain name (e.g. `"base"`, `"ethereum"`, `"arbitrum"`)
|
|
201
|
+
|
|
202
|
+
#### `resolveAgent(agentId, chain)`
|
|
203
|
+
|
|
204
|
+
Resolves an agent's metadata from the on-chain registry contract. Returns an `AgentMetadata` object.
|
|
205
|
+
|
|
206
|
+
#### `verifyAgent(metadata)`
|
|
207
|
+
|
|
208
|
+
Verifies an ERC-8004 agent given its metadata. Discovers teequote/workload endpoints and runs the full verification flow.
|
|
209
|
+
|
|
210
|
+
**Parameters:**
|
|
211
|
+
- `metadata` — `AgentMetadata` object with `name`, `supportedTrust`, and `services`
|
|
212
|
+
|
|
213
|
+
### `AgentMetadata`
|
|
214
|
+
|
|
215
|
+
| Field | Type | Description |
|
|
216
|
+
|-------|------|-------------|
|
|
217
|
+
| `name` | `string` | Agent name |
|
|
218
|
+
| `description` | `string \| undefined` | Agent description |
|
|
219
|
+
| `supportedTrust` | `string[]` | Trust models (must include `"tee-attestation"`) |
|
|
220
|
+
| `services` | `AgentService[]` | Service endpoints (`name` + `endpoint`) |
|
|
221
|
+
|
|
222
|
+
### RPC Configuration
|
|
223
|
+
|
|
224
|
+
An RPC URL **must** be provided via environment variable to use the ERC-8004 agent functions. No default RPCs are shipped with the package.
|
|
225
|
+
|
|
226
|
+
Set one of:
|
|
227
|
+
- `SECRETVM_RPC_<CHAIN>` — chain-specific (e.g. `SECRETVM_RPC_BASE`, `SECRETVM_RPC_ETHEREUM`)
|
|
228
|
+
- `SECRETVM_RPC_URL` — generic fallback for all chains
|
|
229
|
+
|
|
230
|
+
Example:
|
|
231
|
+
```bash
|
|
232
|
+
export SECRETVM_RPC_BASE="https://base-mainnet.g.alchemy.com/v2/YOUR_KEY"
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Supported chains: ethereum, base, arbitrum, sepolia, polygon, bnb, gnosis, linea, taiko, celo, avalanche, optimism, abstract, megaeth, mantle, soneium, xlayer, metis.
|
|
236
|
+
|
|
237
|
+
## CLI usage
|
|
238
|
+
|
|
239
|
+
Install globally:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
npm install -g secretvm-verify
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Or from the repo:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
cd node
|
|
249
|
+
npm install && npm run build
|
|
250
|
+
npm install -g .
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Then use from anywhere:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
# Verify a Secret VM (CPU + GPU + TLS binding)
|
|
257
|
+
secretvm-verify --secretvm yellow-krill.vm.scrtlabs.com
|
|
258
|
+
|
|
259
|
+
# Verify individual attestation quotes from files
|
|
260
|
+
secretvm-verify --tdx cpu_quote.txt
|
|
261
|
+
secretvm-verify --sev amd_cpu_quote.txt --product Genoa
|
|
262
|
+
secretvm-verify --gpu gpu_attest.txt
|
|
263
|
+
|
|
264
|
+
# Auto-detect CPU quote type (TDX vs SEV-SNP)
|
|
265
|
+
secretvm-verify --cpu cpu_quote.txt
|
|
266
|
+
|
|
267
|
+
# Resolve which SecretVM version produced a quote
|
|
268
|
+
secretvm-verify --resolve-version cpu_quote.txt
|
|
269
|
+
secretvm-verify -rv cpu_quote.txt
|
|
270
|
+
|
|
271
|
+
# Verify a quote + docker-compose match
|
|
272
|
+
secretvm-verify --verify-workload cpu_quote.txt --compose docker-compose.yaml
|
|
273
|
+
secretvm-verify -vw cpu_quote.txt --compose docker-compose.yaml
|
|
274
|
+
|
|
275
|
+
# JSON output (any command)
|
|
276
|
+
secretvm-verify --secretvm yellow-krill.vm.scrtlabs.com --raw
|
|
277
|
+
|
|
278
|
+
# Verbose output (all attestation fields)
|
|
279
|
+
secretvm-verify --secretvm yellow-krill.vm.scrtlabs.com -v
|
|
280
|
+
|
|
281
|
+
# Verify an ERC-8004 agent on-chain
|
|
282
|
+
SECRETVM_RPC_BASE="https://..." secretvm-verify --check-agent 38114 --chain base
|
|
283
|
+
secretvm-verify --check-agent 38114 --chain base -v
|
|
284
|
+
|
|
285
|
+
# Verify an agent from a metadata JSON file
|
|
286
|
+
secretvm-verify --agent metadata.json
|
|
287
|
+
|
|
288
|
+
# A bare URL defaults to --secretvm
|
|
289
|
+
secretvm-verify yellow-krill.vm.scrtlabs.com
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## External services
|
|
293
|
+
|
|
294
|
+
The library contacts these services during verification:
|
|
295
|
+
|
|
296
|
+
| Service | Used by | Purpose |
|
|
297
|
+
|---------|---------|---------|
|
|
298
|
+
| [Intel PCS](https://api.trustedservices.intel.com) | TDX | TCB status lookup |
|
|
299
|
+
| [AMD KDS](https://kdsintf.amd.com) | SEV-SNP | VCEK certificate and cert chain |
|
|
300
|
+
| [NVIDIA NRAS](https://nras.attestation.nvidia.com) | GPU | GPU attestation verification |
|
|
301
|
+
|
|
302
|
+
**Note:** AMD KDS has rate limits. If you encounter 429 errors, specify the `product` parameter to reduce the number of requests.
|
|
303
|
+
|
|
304
|
+
## Requirements
|
|
305
|
+
|
|
306
|
+
- Node.js >= 18 (uses built-in `crypto`, `fetch`)
|
|
307
|
+
- `openssl` CLI (required for AMD SEV-SNP certificate chain verification)
|
|
308
|
+
- `yaml` (npm dependency, included)
|
|
309
|
+
|
|
310
|
+
## License
|
|
311
|
+
|
|
312
|
+
MIT
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { AttestationResult } from "./types.js";
|
|
2
|
+
import type { AgentMetadata } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Resolve an ERC-8004 agent's metadata from the on-chain registry.
|
|
5
|
+
*
|
|
6
|
+
* Queries the registry contract for the agent's tokenURI, fetches the
|
|
7
|
+
* metadata JSON, and returns a normalized AgentMetadata object.
|
|
8
|
+
*
|
|
9
|
+
* RPC URL resolution priority:
|
|
10
|
+
* 1. SECRETVM_RPC_<CHAIN> env var (e.g. SECRETVM_RPC_BASE)
|
|
11
|
+
* 2. SECRETVM_RPC_URL env var (generic fallback)
|
|
12
|
+
* 3. Default public RPC for the chain
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveAgent(agentId: number, chain: string): Promise<AgentMetadata>;
|
|
15
|
+
/**
|
|
16
|
+
* Verify an ERC-8004 agent given its metadata.
|
|
17
|
+
*
|
|
18
|
+
* Discovers teequote and workload endpoints from the metadata, then runs
|
|
19
|
+
* the full verification flow: TLS cert, CPU quote, TLS binding, GPU quote,
|
|
20
|
+
* GPU binding, and workload verification.
|
|
21
|
+
*/
|
|
22
|
+
export declare function verifyAgent(metadata: AgentMetadata): Promise<AttestationResult>;
|
|
23
|
+
/**
|
|
24
|
+
* End-to-end ERC-8004 agent verification.
|
|
25
|
+
*
|
|
26
|
+
* Resolves the agent's metadata from the on-chain registry, then runs
|
|
27
|
+
* the full verification flow via verifyAgent.
|
|
28
|
+
*/
|
|
29
|
+
export declare function checkAgent(agentId: number, chain: string): Promise<AttestationResult>;
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import tls from "node:tls";
|
|
3
|
+
import { ethers } from "ethers";
|
|
4
|
+
import { getChainConfig, getRpcUrl } from "./chains.js";
|
|
5
|
+
import { makeResult } from "./types.js";
|
|
6
|
+
import { checkCpuAttestation } from "./cpu.js";
|
|
7
|
+
import { checkNvidiaGpuAttestation } from "./nvidia.js";
|
|
8
|
+
import { verifyWorkload } from "./workload.js";
|
|
9
|
+
const REGISTRY_ABI = [
|
|
10
|
+
"function tokenURI(uint256 tokenId) view returns (string)",
|
|
11
|
+
"function agentURI(uint256 agentId) view returns (string)",
|
|
12
|
+
];
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Helpers
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
function normalizeServices(raw) {
|
|
17
|
+
if (!Array.isArray(raw))
|
|
18
|
+
return [];
|
|
19
|
+
return raw.map((service, index) => {
|
|
20
|
+
const entry = (service ?? {});
|
|
21
|
+
const name = typeof entry.name === "string" ? entry.name : "";
|
|
22
|
+
const endpoint = typeof entry.endpoint === "string" ? entry.endpoint : "";
|
|
23
|
+
return {
|
|
24
|
+
name: name || `service-${index + 1}`,
|
|
25
|
+
endpoint,
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function getTlsCertFingerprint(host, port) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const socket = tls.connect({ host, port, rejectUnauthorized: true }, () => {
|
|
32
|
+
const cert = socket.getPeerX509Certificate();
|
|
33
|
+
if (!cert) {
|
|
34
|
+
socket.destroy();
|
|
35
|
+
return reject(new Error("No certificate received"));
|
|
36
|
+
}
|
|
37
|
+
const fingerprint = crypto
|
|
38
|
+
.createHash("sha256")
|
|
39
|
+
.update(cert.raw)
|
|
40
|
+
.digest();
|
|
41
|
+
socket.destroy();
|
|
42
|
+
resolve(fingerprint);
|
|
43
|
+
});
|
|
44
|
+
socket.on("error", reject);
|
|
45
|
+
socket.setTimeout(10_000, () => {
|
|
46
|
+
socket.destroy();
|
|
47
|
+
reject(new Error("TLS connection timed out"));
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function extractDockerCompose(raw) {
|
|
52
|
+
let text = raw.trim();
|
|
53
|
+
const preMatch = text.match(/<pre>([\s\S]*?)<\/pre>/i);
|
|
54
|
+
if (preMatch)
|
|
55
|
+
text = preMatch[1];
|
|
56
|
+
text = text
|
|
57
|
+
.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)))
|
|
58
|
+
.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
|
|
59
|
+
.replace(/&/g, "&")
|
|
60
|
+
.replace(/</g, "<")
|
|
61
|
+
.replace(/>/g, ">")
|
|
62
|
+
.replace(/"/g, '"')
|
|
63
|
+
.replace(/'/g, "'");
|
|
64
|
+
text = text.replace(/[\u200B\u200C\u200D\uFEFF]/g, "");
|
|
65
|
+
return text;
|
|
66
|
+
}
|
|
67
|
+
function findTeequoteEndpoint(services) {
|
|
68
|
+
for (const s of services) {
|
|
69
|
+
if (s.name.toLowerCase() === "teequote" && s.endpoint)
|
|
70
|
+
return s.endpoint;
|
|
71
|
+
}
|
|
72
|
+
for (const s of services) {
|
|
73
|
+
if (s.endpoint && s.endpoint.includes(":29343"))
|
|
74
|
+
return s.endpoint;
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
function findWorkloadEndpoint(services) {
|
|
79
|
+
for (const s of services) {
|
|
80
|
+
if (s.name.toLowerCase() === "workload" && s.endpoint)
|
|
81
|
+
return s.endpoint;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
function normalizeEndpoint(endpoint) {
|
|
86
|
+
if (!endpoint.startsWith("http://") && !endpoint.startsWith("https://")) {
|
|
87
|
+
return `https://${endpoint}`;
|
|
88
|
+
}
|
|
89
|
+
return endpoint;
|
|
90
|
+
}
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Public: resolveAgent
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
/**
|
|
95
|
+
* Resolve an ERC-8004 agent's metadata from the on-chain registry.
|
|
96
|
+
*
|
|
97
|
+
* Queries the registry contract for the agent's tokenURI, fetches the
|
|
98
|
+
* metadata JSON, and returns a normalized AgentMetadata object.
|
|
99
|
+
*
|
|
100
|
+
* RPC URL resolution priority:
|
|
101
|
+
* 1. SECRETVM_RPC_<CHAIN> env var (e.g. SECRETVM_RPC_BASE)
|
|
102
|
+
* 2. SECRETVM_RPC_URL env var (generic fallback)
|
|
103
|
+
* 3. Default public RPC for the chain
|
|
104
|
+
*/
|
|
105
|
+
export async function resolveAgent(agentId, chain) {
|
|
106
|
+
const chainConfig = getChainConfig(chain);
|
|
107
|
+
const rpcUrl = getRpcUrl(chain);
|
|
108
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
109
|
+
const contract = new ethers.Contract(chainConfig.registryAddress, REGISTRY_ABI, provider);
|
|
110
|
+
let tokenUri;
|
|
111
|
+
try {
|
|
112
|
+
tokenUri = await contract.tokenURI(agentId);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
try {
|
|
116
|
+
tokenUri = await contract.agentURI(agentId);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
throw new Error(`Could not find tokenURI or agentURI for agent ${agentId} on ${chainConfig.name}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (!tokenUri || tokenUri.trim() === "") {
|
|
123
|
+
throw new Error(`Registry returned empty tokenURI for agent ${agentId}`);
|
|
124
|
+
}
|
|
125
|
+
let manifest;
|
|
126
|
+
if (tokenUri.startsWith("data:")) {
|
|
127
|
+
// Handle data URIs (e.g. data:application/json;base64,...)
|
|
128
|
+
const encoded = tokenUri.split(",", 2)[1];
|
|
129
|
+
manifest = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
let fetchUrl = tokenUri;
|
|
133
|
+
if (fetchUrl.startsWith("ipfs://")) {
|
|
134
|
+
fetchUrl = fetchUrl.replace("ipfs://", "https://ipfs.io/ipfs/");
|
|
135
|
+
}
|
|
136
|
+
const resp = await fetch(fetchUrl);
|
|
137
|
+
if (!resp.ok) {
|
|
138
|
+
throw new Error(`Failed to fetch agent metadata from ${fetchUrl}: HTTP ${resp.status}`);
|
|
139
|
+
}
|
|
140
|
+
manifest = (await resp.json());
|
|
141
|
+
}
|
|
142
|
+
const trust = manifest.supportedTrust ??
|
|
143
|
+
manifest.supported_trust ??
|
|
144
|
+
[];
|
|
145
|
+
return {
|
|
146
|
+
name: typeof manifest.name === "string" && manifest.name.trim()
|
|
147
|
+
? manifest.name
|
|
148
|
+
: `Agent ${agentId}`,
|
|
149
|
+
description: typeof manifest.description === "string" ? manifest.description : undefined,
|
|
150
|
+
supportedTrust: Array.isArray(trust) ? trust : [],
|
|
151
|
+
services: normalizeServices(manifest.services ?? manifest.endpoints),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// Public: verifyAgent
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
/**
|
|
158
|
+
* Verify an ERC-8004 agent given its metadata.
|
|
159
|
+
*
|
|
160
|
+
* Discovers teequote and workload endpoints from the metadata, then runs
|
|
161
|
+
* the full verification flow: TLS cert, CPU quote, TLS binding, GPU quote,
|
|
162
|
+
* GPU binding, and workload verification.
|
|
163
|
+
*/
|
|
164
|
+
export async function verifyAgent(metadata) {
|
|
165
|
+
const errors = [];
|
|
166
|
+
const checks = {};
|
|
167
|
+
const report = {};
|
|
168
|
+
report.agent_name = metadata.name;
|
|
169
|
+
// 1. Validate metadata
|
|
170
|
+
const hasTeeAttestation = metadata.supportedTrust
|
|
171
|
+
.map((t) => t.toLowerCase())
|
|
172
|
+
.includes("tee-attestation");
|
|
173
|
+
if (!hasTeeAttestation) {
|
|
174
|
+
errors.push("Agent does not support tee-attestation");
|
|
175
|
+
checks.metadata_valid = false;
|
|
176
|
+
return makeResult("ERC-8004", { checks, report, errors });
|
|
177
|
+
}
|
|
178
|
+
const teequoteEndpoint = findTeequoteEndpoint(metadata.services);
|
|
179
|
+
if (!teequoteEndpoint) {
|
|
180
|
+
errors.push("No teequote service endpoint found in agent metadata");
|
|
181
|
+
checks.metadata_valid = false;
|
|
182
|
+
return makeResult("ERC-8004", { checks, report, errors });
|
|
183
|
+
}
|
|
184
|
+
checks.metadata_valid = true;
|
|
185
|
+
// 2. Derive URLs
|
|
186
|
+
const baseUrl = normalizeEndpoint(teequoteEndpoint).replace(/\/+$/, "");
|
|
187
|
+
const cpuUrl = baseUrl.endsWith("/cpu") ? baseUrl : `${baseUrl}/cpu`;
|
|
188
|
+
const gpuUrl = baseUrl.endsWith("/cpu")
|
|
189
|
+
? baseUrl.replace(/\/cpu$/, "/gpu")
|
|
190
|
+
: `${baseUrl}/gpu`;
|
|
191
|
+
const workloadService = findWorkloadEndpoint(metadata.services);
|
|
192
|
+
const workloadUrl = workloadService
|
|
193
|
+
? normalizeEndpoint(workloadService)
|
|
194
|
+
: baseUrl.endsWith("/cpu")
|
|
195
|
+
? baseUrl.replace(/\/cpu$/, "/docker-compose")
|
|
196
|
+
: `${baseUrl}/docker-compose`;
|
|
197
|
+
const parsed = new URL(cpuUrl.endsWith("/cpu") ? cpuUrl.replace(/\/cpu$/, "") : cpuUrl);
|
|
198
|
+
const host = parsed.hostname;
|
|
199
|
+
const port = parsed.port ? Number(parsed.port) : 443;
|
|
200
|
+
// 3. TLS certificate fingerprint
|
|
201
|
+
let tlsFingerprint;
|
|
202
|
+
try {
|
|
203
|
+
tlsFingerprint = await getTlsCertFingerprint(host, port);
|
|
204
|
+
checks.tls_cert_obtained = true;
|
|
205
|
+
report.tls_fingerprint = tlsFingerprint.toString("hex");
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
errors.push(`Failed to get TLS certificate: ${e.message}`);
|
|
209
|
+
checks.tls_cert_obtained = false;
|
|
210
|
+
return makeResult("ERC-8004", { checks, report, errors });
|
|
211
|
+
}
|
|
212
|
+
// 4. Fetch and verify CPU quote
|
|
213
|
+
let cpuData;
|
|
214
|
+
try {
|
|
215
|
+
const resp = await fetch(cpuUrl);
|
|
216
|
+
if (!resp.ok)
|
|
217
|
+
throw new Error(`HTTP ${resp.status}`);
|
|
218
|
+
cpuData = await resp.text();
|
|
219
|
+
checks.cpu_quote_fetched = true;
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
errors.push(`Failed to fetch CPU quote: ${e.message}`);
|
|
223
|
+
checks.cpu_quote_fetched = false;
|
|
224
|
+
return makeResult("ERC-8004", { checks, report, errors });
|
|
225
|
+
}
|
|
226
|
+
const cpuResult = await checkCpuAttestation(cpuData);
|
|
227
|
+
checks.cpu_attestation_valid = cpuResult.valid;
|
|
228
|
+
report.cpu = cpuResult.report;
|
|
229
|
+
report.cpu_type = cpuResult.attestationType;
|
|
230
|
+
if (!cpuResult.valid)
|
|
231
|
+
errors.push(...cpuResult.errors);
|
|
232
|
+
// 5. TLS binding
|
|
233
|
+
const reportDataHex = cpuResult.report.report_data ?? "";
|
|
234
|
+
if (reportDataHex.length >= 64) {
|
|
235
|
+
const firstHalf = reportDataHex.slice(0, 64);
|
|
236
|
+
checks.tls_binding = firstHalf === tlsFingerprint.toString("hex");
|
|
237
|
+
if (!checks.tls_binding) {
|
|
238
|
+
errors.push(`TLS binding failed: report_data first half (${firstHalf.slice(0, 16)}...) ` +
|
|
239
|
+
`!= TLS fingerprint (${tlsFingerprint.toString("hex").slice(0, 16)}...)`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
checks.tls_binding = false;
|
|
244
|
+
errors.push("report_data too short for TLS binding check");
|
|
245
|
+
}
|
|
246
|
+
// 6. GPU quote (optional)
|
|
247
|
+
let gpuPresent = false;
|
|
248
|
+
let gpuData = "";
|
|
249
|
+
try {
|
|
250
|
+
const resp = await fetch(gpuUrl);
|
|
251
|
+
if (resp.ok) {
|
|
252
|
+
gpuData = await resp.text();
|
|
253
|
+
const gpuJson = JSON.parse(gpuData);
|
|
254
|
+
if ("error" in gpuJson) {
|
|
255
|
+
checks.gpu_quote_fetched = false;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
gpuPresent = true;
|
|
259
|
+
checks.gpu_quote_fetched = true;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
checks.gpu_quote_fetched = false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
checks.gpu_quote_fetched = false;
|
|
268
|
+
}
|
|
269
|
+
if (gpuPresent) {
|
|
270
|
+
const gpuResult = await checkNvidiaGpuAttestation(gpuData);
|
|
271
|
+
checks.gpu_attestation_valid = gpuResult.valid;
|
|
272
|
+
report.gpu = gpuResult.report;
|
|
273
|
+
if (!gpuResult.valid)
|
|
274
|
+
errors.push(...gpuResult.errors);
|
|
275
|
+
const gpuJson = JSON.parse(gpuData);
|
|
276
|
+
const gpuNonce = gpuJson.nonce ?? "";
|
|
277
|
+
if (reportDataHex.length >= 128) {
|
|
278
|
+
const secondHalf = reportDataHex.slice(64, 128);
|
|
279
|
+
checks.gpu_binding = secondHalf === gpuNonce;
|
|
280
|
+
if (!checks.gpu_binding) {
|
|
281
|
+
errors.push(`GPU binding failed: report_data second half (${secondHalf.slice(0, 16)}...) ` +
|
|
282
|
+
`!= GPU nonce (${gpuNonce.slice(0, 16)}...)`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
checks.gpu_binding = false;
|
|
287
|
+
errors.push("report_data too short for GPU binding check");
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// 7. Workload verification
|
|
291
|
+
try {
|
|
292
|
+
const resp = await fetch(workloadUrl);
|
|
293
|
+
if (!resp.ok)
|
|
294
|
+
throw new Error(`HTTP ${resp.status}`);
|
|
295
|
+
const dockerCompose = extractDockerCompose(await resp.text());
|
|
296
|
+
checks.workload_fetched = true;
|
|
297
|
+
const workloadResult = verifyWorkload(cpuData, dockerCompose);
|
|
298
|
+
checks.workload_verified = workloadResult.status === "authentic_match";
|
|
299
|
+
report.workload = workloadResult;
|
|
300
|
+
if (workloadResult.status === "authentic_mismatch") {
|
|
301
|
+
errors.push("Workload mismatch: VM is authentic but docker-compose does not match");
|
|
302
|
+
}
|
|
303
|
+
else if (workloadResult.status === "not_authentic") {
|
|
304
|
+
errors.push("Workload verification failed: not an authentic SecretVM");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
catch (e) {
|
|
308
|
+
errors.push(`Failed to fetch workload: ${e.message}`);
|
|
309
|
+
checks.workload_fetched = false;
|
|
310
|
+
}
|
|
311
|
+
// Overall validity
|
|
312
|
+
const requiredChecks = [
|
|
313
|
+
checks.metadata_valid,
|
|
314
|
+
checks.tls_cert_obtained,
|
|
315
|
+
checks.cpu_quote_fetched,
|
|
316
|
+
checks.cpu_attestation_valid,
|
|
317
|
+
checks.tls_binding,
|
|
318
|
+
!!checks.workload_verified,
|
|
319
|
+
];
|
|
320
|
+
if (gpuPresent) {
|
|
321
|
+
requiredChecks.push(!!checks.gpu_attestation_valid);
|
|
322
|
+
requiredChecks.push(!!checks.gpu_binding);
|
|
323
|
+
}
|
|
324
|
+
const valid = requiredChecks.every(Boolean);
|
|
325
|
+
return makeResult("ERC-8004", { valid, checks, report, errors });
|
|
326
|
+
}
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
// Public: checkAgent
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
/**
|
|
331
|
+
* End-to-end ERC-8004 agent verification.
|
|
332
|
+
*
|
|
333
|
+
* Resolves the agent's metadata from the on-chain registry, then runs
|
|
334
|
+
* the full verification flow via verifyAgent.
|
|
335
|
+
*/
|
|
336
|
+
export async function checkAgent(agentId, chain) {
|
|
337
|
+
const errors = [];
|
|
338
|
+
const checks = {};
|
|
339
|
+
let metadata;
|
|
340
|
+
try {
|
|
341
|
+
metadata = await resolveAgent(agentId, chain);
|
|
342
|
+
checks.agent_resolved = true;
|
|
343
|
+
}
|
|
344
|
+
catch (e) {
|
|
345
|
+
errors.push(`Failed to resolve agent: ${e.message}`);
|
|
346
|
+
checks.agent_resolved = false;
|
|
347
|
+
return makeResult("ERC-8004", { checks, errors });
|
|
348
|
+
}
|
|
349
|
+
const result = await verifyAgent(metadata);
|
|
350
|
+
result.checks = { agent_resolved: true, ...result.checks };
|
|
351
|
+
return result;
|
|
352
|
+
}
|
|
353
|
+
//# sourceMappingURL=agent.js.map
|