@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 +1 -1
- package/attest.d.mts +10 -0
- package/attest.mjs +106 -1
- package/package.json +1 -1
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: "
|
|
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: "
|
|
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