Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Hardware captures and their records are checked byte for byte (SHA-256 pins in
# tools/check-tdx-verifier.mjs and tools/check-key-binding.py). Never convert them.
verify/fixtures/** -text
4 changes: 3 additions & 1 deletion .github/workflows/verifier.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ jobs:
with:
python-version: '3.12'
- name: Install the Python verifier the port is checked against
run: python -m pip install "agent-manifest==0.12.0"
run: python -m pip install "agent-manifest==0.12.0" "agentrust-trace==0.10.0"
- name: Check the key-binding capture's record, signature and REPORTDATA
run: python tools/check-key-binding.py
- name: Record Python verdicts
run: python tools/tdx-differential.py --out differential.json
- uses: actions/setup-node@v4
Expand Down
8 changes: 4 additions & 4 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -177,17 +177,17 @@ <h1>Prove what your AI ran, and what it did.</h1>
<figure class="verify-panel" id="verify-panel" aria-label="A genuine Intel TDX quote, verified in this browser">
<div class="verify-panel-bar"><span>/verify › tdx_quote.bin</span><span>offline · in this browser</span></div>
<ol class="verify-rows">
<li><span class="key">quote</span><span>Intel TDX v4, GCP C3, captured 2026-07-21</span><span></span></li>
<li><span class="key">quote</span><span>Intel TDX v4, GCP C3, captured 2026-09-14</span><span></span></li>
<li data-step="quote-signature"><span class="key">step 1</span><span>attestation key signature over header and TD report</span><span class="state">not run</span></li>
<li data-step="qe-binding"><span class="key">step 2</span><span>QE report binds the attestation key</span><span class="state">not run</span></li>
<li data-step="qe-report-signature"><span class="key">step 3</span><span>QE report signed by the platform PCK certificate</span><span class="state">not run</span></li>
<li data-step="pck-chain"><span class="key">step 4</span><span>PCK chain ends at the pinned Intel SGX Root CA</span><span class="state">not run</span></li>
<li data-step="reportdata"><span class="key">REPORTDATA</span><span>32 bytes set by the guest</span><span class="state note">see note</span></li>
<li data-step="reportdata"><span class="key">REPORTDATA</span><span>commits to the key that signed a published TRACE record</span><span class="state">not run</span></li>
<li data-step="verdict"><span class="key">verdict</span><span>genuine Intel TDX silicon signed this quote</span><span class="state">not run</span></li>
</ol>
</figure>
<div class="verify-foot">
<p><span class="tag">Note</span>This quote proves genuine Intel TDX silicon signed it. Its REPORTDATA holds a manifest hash whose input was not published, so it does not yet tie a specific record to this machine.</p>
<p><span class="tag">Note</span>Genuine Intel TDX silicon signed this quote, and its REPORTDATA commits to the key that signed the TRACE record published beside it. It does not show that the software inside the trust domain was the image anyone intended.</p>
<p>Runs in your browser. Nothing is sent back to us.</p>
</div>
</div>
Expand Down Expand Up @@ -271,7 +271,7 @@ <h3>AMD SEV-SNP</h3>
<article class="ecosystem-card">
<h3>Intel TDX</h3>
<p>GCP C3. Quotes verify offline to the pinned Intel SGX Root CA, no collateral service needed.</p>
<a class="evidence-link" href="/verify/">Check the 2026-07-21 capture yourself →</a>
<a class="evidence-link" href="/verify/">Check the 2026-09-14 capture yourself →</a>
</article>
<article class="ecosystem-card">
<h3>NVIDIA H100 confidential computing</h3>
Expand Down
2 changes: 1 addition & 1 deletion llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ AgenTrust is the ecosystem at https://agentrust-io.com and the GitHub organizati
## Start here

- [Overview](https://agentrust-io.com/): The verifiable AI supply chain from model weights to agent actions, the hardware it is validated on, and what the evidence does and does not prove.
- [Verify an Intel TDX quote](https://agentrust-io.com/verify/): Runs the four-step DCAP check on a genuine GCP confidential VM quote in the browser, ending at the pinned Intel SGX Root CA. A pass proves genuine Intel TDX silicon signed the quote. These captures do not tie a TRACE record to that machine, and the check does not appraise TCB currency or revocation.
- [Verify an Intel TDX quote](https://agentrust-io.com/verify/): Runs the four-step DCAP check on a genuine GCP confidential VM quote in the browser, ending at the pinned Intel SGX Root CA, then checks that the quote's REPORTDATA commits to the signing key of a TRACE record published beside it. A pass proves genuine Intel TDX silicon signed the quote and bound that key. The check does not appraise TCB currency or revocation, or show that the measured image is the one anyone intended.
- [10-minute tool-call tutorial](https://agentrust-io.com/quickstart/): Write a policy, observe a denied call, and inspect a signed session record on a laptop. Software mode provides no hardware isolation or hardware-backed provenance.
- [Weight Custody Manifest (WCM)](https://wcm.agentrust-io.com/): Bind model-weight identity and custody terms to key-release policy. The local walkthrough uses synthetic evidence and a placeholder key; it does not load a real model or demonstrate hardware protection.
- [Runnable demos](https://agentrust-io.com/demos/): Software examples for policy decisions, evidence verification, delegation, and model-weight custody. Follow each demo's stated prerequisites and limits.
Expand Down
84 changes: 84 additions & 0 deletions tools/check-key-binding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Check the key-binding TDX capture published on /verify/.

verify/fixtures/gcp-tdx-2026-09-14-keybind_record.json carries a TRACE v0.3 record
signed inside a GCP C3 trust domain, with the quote that trust domain produced.
The page claims four things about it, and this checks each one with the Python
tools the page's JavaScript is held to:

1. the quote verifies to the pinned Intel SGX Root CA (agent_manifest._tdx_verify);
2. the record carries that exact quote, and claims its MRTD;
3. REPORT_DATA[0:32] is SHA-256 of the record's cnf.jwk.x, and the rest is zero;
4. the record signature verifies under that key, over the SDK's canonical bytes.

It also checks that the published capture program hashes to the digest the record
claims as its build provenance.

It does not validate the record against the TRACE v0.3 draft schema, which lives
in trace-spec, and it does not appraise TCB currency, revocation, or whether the
MRTD is an image anyone intended.
"""
import base64
import hashlib
import json
from pathlib import Path
import sys

from agent_manifest._tdx_verify import parse_tdx_quote, verify_tdx_quote
from agentrust_trace.sign import _canonical_bytes, _pubkey_from_jwk

ROOT = Path(__file__).resolve().parents[1]
FIXTURES = ROOT / 'verify' / 'fixtures'
FIXTURE = FIXTURES / 'gcp-tdx-2026-09-14-keybind_record.json'
PROFILE = 'tag:agentrust-io.com,2026:trace-v0.3'


def unb64u(text):
return base64.urlsafe_b64decode(text + '=' * (-len(text) % 4))


def main():
fixture = json.loads(FIXTURE.read_text(encoding='utf-8'))
record = fixture['record']
quote = (FIXTURES / fixture['quote_file']).read_bytes()
failures = []

def check(condition, message):
if not condition:
failures.append(message)

check(record.get('eat_profile') == PROFILE, f'record profile is not {PROFILE}')
check(verify_tdx_quote(quote) is True, 'quote does not verify to the pinned Intel root')

parsed = parse_tdx_quote(quote)
evidence = record['runtime']['evidence']
check(evidence.get('format') == 'tdx-quote-v4', 'evidence format is not tdx-quote-v4')
check(unb64u(evidence['quote']) == quote, 'record does not carry the published quote')
check(record['runtime'].get('measurement') == 'sha384:' + parsed.mrtd.hex(), 'record does not claim the quote MRTD')

jwk = record['cnf']['jwk']
key = unb64u(jwk['x'])
check(jwk.get('kty') == 'OKP' and jwk.get('crv') == 'Ed25519', 'record key is not Ed25519')
check(jwk['x'] == fixture['public_key_b64u'], 'fixture key differs from the record key')
check(parsed.report_data == hashlib.sha256(key).digest() + bytes(32), 'REPORT_DATA does not commit to the record key')
check(parsed.report_data.hex() == fixture['report_data_hex'], 'fixture REPORT_DATA differs from the quote')

body = _canonical_bytes({k: v for k, v in record.items() if k != 'signature'})
try:
_pubkey_from_jwk(jwk).verify(unb64u(record['signature']), body)
except Exception as error:
failures.append(f'record signature does not verify: {type(error).__name__}')

program = (FIXTURES / 'gcp-tdx-2026-09-14-capture.py').read_bytes()
digest = hashlib.sha256(program).hexdigest()
check(digest == fixture['capture_script_sha256'], 'published capture program differs from the one that ran')
check(record['build_provenance'].get('digest') == 'sha256:' + digest, 'record build provenance is not the capture program')

if failures:
print('\n'.join(f'FAIL {f}' for f in failures))
return 1
print('PASS key-binding capture: quote verifies, REPORT_DATA commits to the record key, record signature verifies')
return 0


if __name__ == '__main__':
sys.exit(main())
31 changes: 25 additions & 6 deletions tools/check-tdx-verifier.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/
import { readFile } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import { checkKeyBinding } from '../verify/key-binding.js';
import {
verifyTdxQuote, pinnedRootFingerprint, OFF_MRTD, QUOTE_HEADER_LENGTH,
} from '../verify/tdx-verify.js';
Expand All @@ -23,27 +24,45 @@ const WHEN = '2026-09-14T00:00:00Z';
// swapped or re-captured fixture fails here rather than changing what
// "a genuine quote" refers to on the public page.
const CAPTURES = {
'gcp-tdx-2026-07-21-tdx_quote.bin': 'f9efbac112efe510aa8ccd20703b063591b8c2c54c474d0ff1d6500299bae0ba',
'gcp-tdx-2026-07-21-tdx_quote_manifest.bin': '1ae04c74b564ef8795d4c4e4ffd1835d080d9dad4f8879e5cd1e8249503828b2',
'gcp-tdx-2026-07-21-tdx_quote.bin': {
sha256: 'f9efbac112efe510aa8ccd20703b063591b8c2c54c474d0ff1d6500299bae0ba',
mrtd: '9bf86e6280ec4282b8b5822d8166410a456cdb720109aa799f0011fa63df1de3ee5e35e293fc410c061433163acb03a6',
},
'gcp-tdx-2026-07-21-tdx_quote_manifest.bin': {
sha256: '1ae04c74b564ef8795d4c4e4ffd1835d080d9dad4f8879e5cd1e8249503828b2',
mrtd: '9bf86e6280ec4282b8b5822d8166410a456cdb720109aa799f0011fa63df1de3ee5e35e293fc410c061433163acb03a6',
},
// A different trust domain, captured to bind a TRACE record's signing key.
'gcp-tdx-2026-09-14-keybind_quote.bin': {
sha256: '2217b3d640b2e4cdabd34604ea59df7f4ea23ed9702d3ec040689dca20ce1d61',
mrtd: 'c1ee9c16e3afc506cfe042c5b846a368528f3b37618eafb27469bc114cf914e9222c91618470e7f2b28ac360968270a5',
record: 'gcp-tdx-2026-09-14-keybind_record.json',
},
};
const MRTD = '9bf86e6280ec4282b8b5822d8166410a456cdb720109aa799f0011fa63df1de3ee5e35e293fc410c061433163acb03a6';

const failures = [];
const check = (condition, message) => { if (!condition) failures.push(message); };

const load = async (name) => new Uint8Array(await readFile(new URL(`verify/fixtures/${name}`, root)));

for (const [name, digest] of Object.entries(CAPTURES)) {
for (const [name, { sha256: digest, mrtd, record }] of Object.entries(CAPTURES)) {
const quote = await load(name);
check(createHash('sha256').update(quote).digest('hex') === digest, `${name}: not the committed hardware capture`);

const result = await verifyTdxQuote(quote, { verificationTime: WHEN });
check(result.accepted, `${name}: genuine capture rejected (${result.error})`);
check(result.steps.every((s) => s.status === 'pass'), `${name}: a step did not pass`);
check(result.quote && result.quote.mrtd === MRTD, `${name}: MRTD differs from the capture's`);
check(result.quote && result.quote.mrtd === mrtd, `${name}: MRTD differs from the capture's`);
check(result.quote && result.quote.reportData.slice(64) === '0'.repeat(64), `${name}: REPORTDATA tail is not zero`);
check(result.chain.length === 3, `${name}: expected a three-certificate PCK chain`);

// Only the key-binding capture commits to a record key; the July captures bind
// a manifest hash, and the check must say no for them rather than pass vacuously.
const fixture = JSON.parse(await readFile(new URL(`verify/fixtures/${record || CAPTURES['gcp-tdx-2026-09-14-keybind_quote.bin'].record}`, root), 'utf8'));
const binding = await checkKeyBinding(result, fixture.record, quote);
if (record) check(binding.bound && binding.sameQuote && binding.sameMeasurement, `${name}: REPORTDATA does not bind the published record key`);
else check(!binding.bound && !binding.sameQuote, `${name}: key binding passed for a quote that does not commit to the record key`);

const tampered = quote.slice();
tampered[QUOTE_HEADER_LENGTH + OFF_MRTD] ^= 0xff;
const t = await verifyTdxQuote(tampered, { verificationTime: WHEN });
Expand Down Expand Up @@ -94,4 +113,4 @@ if (failures.length) {
console.error(failures.join('\n'));
process.exit(1);
}
console.log('PASS both GCP TDX captures verify; tampered quote and expired chain are rejected at the right step');
console.log('PASS all three GCP TDX captures verify, the key-binding capture commits to its record key; tampered quote and expired chain are rejected at the right step');
1 change: 1 addition & 0 deletions tools/tdx-differential.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
CAPTURES = [
"gcp-tdx-2026-07-21-tdx_quote.bin",
"gcp-tdx-2026-07-21-tdx_quote_manifest.bin",
"gcp-tdx-2026-09-14-keybind_quote.bin",
]
# Fixed, so a certificate expiring can never make the two runs disagree.
VERIFICATION_TIME = "2026-09-14T00:00:00+00:00"
Expand Down
71 changes: 71 additions & 0 deletions verify/fixtures/gcp-tdx-2026-09-14-capture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import base64
import hashlib
import json
import pathlib
import time

from agentrust_trace.sign import sign_record
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

# Generated inside the TD. The private half is never serialized.
key = Ed25519PrivateKey.generate()
public = key.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
report_data = hashlib.sha256(public).digest() + bytes(32)

report = pathlib.Path("/sys/kernel/config/tsm/report/agentrust-capture")
report.mkdir()
(report / "inblob").write_bytes(report_data)
quote = (report / "outblob").read_bytes()
provider = (report / "provider").read_text().strip()
generation = (report / "generation").read_text().strip()
report.rmdir()

b64u = lambda raw: base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
mrtd = quote[48 + 136:48 + 184]
script_sha256 = hashlib.sha256(pathlib.Path(__file__).read_bytes()).hexdigest()

record = {
"eat_profile": "tag:agentrust-io.com,2026:trace-v0.3",
"iat": int(time.time()),
"subject": "spiffe://agentrust-io.com/capture/gcp-tdx-key-binding",
"model": {"provider": "none", "model_id": "none: attestation capture, no model loaded"},
"runtime": {
"platform": "intel-tdx",
"measurement": "sha384:" + mrtd.hex(),
"firmware_version": "gcp-c3",
"evidence": {
"format": "tdx-quote-v4",
"quote": b64u(quote),
"collateral": "embedded",
"binds": "cnf-key",
},
},
# No policy governed this capture: the digest is of empty input and the mode
# only declares, it enforces nothing.
"policy": {"bundle_hash": "sha256:" + hashlib.sha256(b"").hexdigest(), "enforcement_mode": "declared"},
"data_class": "public",
# The digest is of this capture program, published beside the capture, so it
# can be recomputed. SLSA level 0: nothing attests how the VM image was built.
"build_provenance": {"slsa_level": 0, "digest": "sha256:" + script_sha256},
"appraisal": {"status": "none", "verifier": "https://github.com/agentrust-io/agent-manifest"},
}
signed = sign_record(record, key)

bundle = json.dumps(
{
"quote_b64": base64.b64encode(quote).decode(),
"public_key_b64u": b64u(public),
"report_data_hex": report_data.hex(),
"tsm_provider": provider,
"tsm_generation": generation,
"capture_script_sha256": script_sha256,
"record": signed,
},
separators=(",", ":"),
).encode()
encoded = base64.b64encode(bundle).decode()
chunks = [encoded[i:i + 900] for i in range(0, len(encoded), 900)]
print(f"AGT-BUNDLE sha256={hashlib.sha256(bundle).hexdigest()} chunks={len(chunks)}", flush=True)
for i, chunk in enumerate(chunks):
print(f"AGT-CHUNK {i:04d} {chunk}", flush=True)
Binary file not shown.
Loading
Loading