@prismnetwork/agent-sdk 0.7.6 → 0.7.7

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 CHANGED
@@ -17,7 +17,7 @@ import { PrismAgent, DEFAULT_IMAGE } from "@prismnetwork/agent-sdk";
17
17
 
18
18
  const agent = new PrismAgent({
19
19
  privateKey: process.env.PRISM_AGENT_KEY, // agent's wallet
20
- escrow: "0xfD4228eEEfC49e4b76A0CD40af9fdd546220B2FD",
20
+ escrow: "0x62C042265991bEa17B07229322A01850974626dA",
21
21
  });
22
22
 
23
23
  await agent.authenticate();
package/attest.d.mts CHANGED
@@ -76,6 +76,16 @@ export interface VerifyConfidentialOptions {
76
76
 
77
77
  export declare function verifyConfidential(options: VerifyConfidentialOptions): Promise<AttestationResult>;
78
78
 
79
+ /// Everything a verifier can establish about a confidential endpoint without
80
+ /// paying for a call: quote to Intel's root, boot-log replay, the pinned
81
+ /// workload, and the GPU leg tied to the same TD. Spends nothing, needs no key.
82
+ export declare function attestEndpoint(
83
+ options?: Pick<
84
+ VerifyConfidentialOptions,
85
+ "base" | "model" | "expectedWorkload" | "nonce" | "now" | "observedSpki" | "collateralUrl" | "fetchImpl"
86
+ >,
87
+ ): Promise<AttestationResult>;
88
+
79
89
  export declare function gateNrasClaims(input: {
80
90
  overall: Record<string, unknown>;
81
91
  gpus: Record<string, Record<string, unknown>>;
package/attest.mjs CHANGED
@@ -56,7 +56,7 @@ export const EXPECTED_WORKLOAD = {
56
56
  "ghcr.io/redpill-ai/private-ai-launcher@sha256:c083ff9e6a5ddf10f6c9e9bb1f74cc618deebecfea5208b563c574399db4637c",
57
57
  repoUrl: "https://github.com/Dstack-TEE/private-ai-gateway.git",
58
58
  osImageHash: "bd369a8c2f9edb2b52dad48ac8e0b32dde5f1337c423a506b48d07403a7d8033",
59
- repoCommit: "b6b5c1b82f6fc59490db5a5255bf4493805e66c6",
59
+ repoCommit: "d567f6b4c4d93e33037ea202db04db86a8ad881c",
60
60
  };
61
61
 
62
62
  const NRAS_ATTEST_URL = "https://nras.attestation.nvidia.com/v3/attest/gpu";
@@ -587,6 +587,111 @@ export async function verifyConfidential({
587
587
  const CUSTODY_DETAIL =
588
588
  "no verifier appraises the KMS custody chain yet; encrypt end to end rather than rely on it";
589
589
 
590
+ /// The endpoint-only appraisal: everything a verifier can establish about a
591
+ /// confidential endpoint without paying for a call. It spends nothing and needs
592
+ /// no key. The receipt-family checks belong to a specific paid call and are not
593
+ /// in this transcript; what is here is who could read a prompt sent right now —
594
+ /// the quote to Intel's root, the boot log replay, the pinned workload, and the
595
+ /// NVIDIA GPU leg tied to the same TD.
596
+ export async function attestEndpoint({
597
+ base = DEFAULT_CONFIDENTIAL_BASE,
598
+ model,
599
+ expectedWorkload = EXPECTED_WORKLOAD,
600
+ nonce = randomNonce(),
601
+ now = Math.floor(Date.now() / 1000),
602
+ observedSpki = null,
603
+ collateralUrl = undefined,
604
+ fetchImpl = fetch,
605
+ } = {}) {
606
+ const t = new Transcript();
607
+ const root = String(base).replace(/\/+$/, "");
608
+ const expectedSkips = new Set([CUSTODY]);
609
+ if (!observedSpki) expectedSkips.add(CHANNEL);
610
+ const settle = (extra) => verdict(t, expectedSkips, { nonce, model: model ?? null, ...extra });
611
+
612
+ const fetched = await getJson(fetchImpl, `${root}/v1/attestation?nonce=${nonce}`, "the attestation endpoint");
613
+ if (!fetched.ok) {
614
+ for (const id of ["keyset-digest", "report-data-binding", "tdx-quote", "rtmr3-replay", "compose-hash", WORKLOAD]) {
615
+ t.fail(id, fetched.detail);
616
+ }
617
+ t.skip("gpu-nras", "no attestation report to tie GPU evidence to");
618
+ t.skip("gpu-binding", "no attestation report to tie GPU evidence to");
619
+ channelCheck(t, null, observedSpki, root);
620
+ t.skip(CUSTODY, CUSTODY_DETAIL);
621
+ return settle({ keysetDigest: null, provenance: null });
622
+ }
623
+ const report = fetched.body;
624
+
625
+ const binding = await verifyReportBinding(report, nonce, { now });
626
+ const digest = binding.workloadKeysetDigest;
627
+ const keyset = binding.keyset;
628
+ const badBinding = binding.checks.find((c) => !c.ok);
629
+ if (badBinding) {
630
+ t.fail("keyset-digest", badBinding.detail ?? `${badBinding.name} failed`);
631
+ } else {
632
+ t.pass("keyset-digest", `${digest}, aci/1, valid until ${unixTime(keyset.not_after)}`);
633
+ }
634
+
635
+ const quote = await verifyQuote(report, { collateralUrl, now });
636
+ if (!quote.ok) {
637
+ t.fail("tdx-quote", quote.detail ?? "quote verification failed");
638
+ } else if (quote.status !== "UpToDate") {
639
+ const advisories = quote.advisoryIds?.length ? ` (${quote.advisoryIds.join(", ")})` : "";
640
+ t.fail("tdx-quote", `verified to Intel's root, but the platform TCB is ${quote.status}${advisories}`);
641
+ } else {
642
+ t.pass("tdx-quote", "verified to Intel's root, platform TCB up to date");
643
+ }
644
+
645
+ const slot = quote.ok ? quote.report.reportData : readReportData(report.attestation?.evidence?.quote);
646
+ const unverified = quote.ok ? "" : " (read off an unverified quote)";
647
+ if (!digest) {
648
+ t.fail("report-data-binding", "no key set was established, so nothing can be recomputed against the quote");
649
+ } else {
650
+ const expected = await computeReportData(digest, nonce);
651
+ const bound = slot.length === 64 ? toHex(slot.slice(0, 32)) : "nothing: the quote carries no report-data slot";
652
+ const padded = slot.length === 64 && slot.slice(32).every((b) => b === 0);
653
+ if (bound === expected && padded) {
654
+ t.pass("report-data-binding", `the quote's report data is sha256 of our nonce over ${digest}${unverified}`);
655
+ } else {
656
+ t.fail("report-data-binding", `the quote binds ${bound}, this nonce and key set produce ${expected}`);
657
+ }
658
+ }
659
+
660
+ let measurement = null;
661
+ try {
662
+ measurement = await verifyComposeMeasurement(report, { statedRtmr3: quote.ok ? quote.report.rtMr3 : null });
663
+ const rtmr3 = measurement.checks.find((c) => c.name === "rtmr3");
664
+ t.add("rtmr3-replay", rtmr3.ok ? "pass" : "fail", rtmr3.ok ? `${toHex(measurement.rtmr3)}${unverified}` : rtmr3.detail);
665
+ const composeHash = measurement.checks.find((c) => c.name === "compose_hash");
666
+ t.add(
667
+ "compose-hash",
668
+ composeHash.ok ? "pass" : "fail",
669
+ composeHash.ok
670
+ ? `sha256(app_compose)=${measurement.composeHash} measured before system-ready`
671
+ : composeHash.detail,
672
+ );
673
+ if (!measurement.ok) measurement = null;
674
+ } catch (err) {
675
+ t.fail("rtmr3-replay", `the report's boot evidence could not be replayed: ${err?.message ?? err}`);
676
+ t.fail("compose-hash", "no replayable boot evidence to measure the compose against");
677
+ }
678
+
679
+ let provenance = null;
680
+ if (!measurement) {
681
+ t.fail(WORKLOAD, "no measured compose to read the workload identity out of");
682
+ } else {
683
+ const identity = await appraiseWorkload(report, measurement, expectedWorkload);
684
+ provenance = identity.provenance ?? null;
685
+ t.add(WORKLOAD, identity.skipped ? "skip" : identity.ok ? "pass" : "fail", identity.detail);
686
+ }
687
+
688
+ await gpuChecks(t, { root, fetchImpl, model, digest, quote, collateralUrl, now });
689
+ channelCheck(t, keyset, observedSpki, root);
690
+ t.skip(CUSTODY, CUSTODY_DETAIL);
691
+
692
+ return settle({ keysetDigest: digest ?? null, provenance });
693
+ }
694
+
590
695
  /// The verdict rule over a finished check list. `failed` means a check actually
591
696
  /// failed. `incomplete` means nothing failed but evidence the verdict would have
592
697
  /// rested on was missing, which is a different thing to say and is said with a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prismnetwork/agent-sdk",
3
- "version": "0.7.6",
3
+ "version": "0.7.7",
4
4
  "description": "Headless GPU leasing and renter-encrypted storage on Prism Network for wallet-holding agents.",
5
5
  "type": "module",
6
6
  "main": "prism.mjs",