Skip to content

Commit 7cba24d

Browse files
feat(verify): publish a TDX capture that binds a TRACE record's signing key (#69)
The July GCP captures bind a manifest hash whose input was never published, so /verify and the homepage could only say genuine silicon. A capture taken on 2026-09-14 in a new C3 trust domain puts SHA-256 of an Ed25519 key, generated inside the TD, in REPORTDATA and signs a TRACE v0.3 record with it. The quote, the record and the capture program are published under verify/fixtures. verify/key-binding.js checks the binding in the browser and the homepage panel; tools/check-key-binding.py checks the record signature, binding, quote, MRTD and program digest with the Python SDK in the verifier job. The differential now covers three captures, 5,526 inputs. verify/fixtures is marked -text, since every file there is pinned by hash. Claude-Session: https://claude.ai/code/session_013aK3gVWzNdcM3hZ2o2awK2 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 9d71db6 commit 7cba24d

14 files changed

Lines changed: 328 additions & 38 deletions

‎.gitattributes‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Hardware captures and their records are checked byte for byte (SHA-256 pins in
2+
# tools/check-tdx-verifier.mjs and tools/check-key-binding.py). Never convert them.
3+
verify/fixtures/** -text

‎.github/workflows/verifier.yml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ jobs:
2929
with:
3030
python-version: '3.12'
3131
- name: Install the Python verifier the port is checked against
32-
run: python -m pip install "agent-manifest==0.12.0"
32+
run: python -m pip install "agent-manifest==0.12.0" "agentrust-trace==0.10.0"
33+
- name: Check the key-binding capture's record, signature and REPORTDATA
34+
run: python tools/check-key-binding.py
3335
- name: Record Python verdicts
3436
run: python tools/tdx-differential.py --out differential.json
3537
- uses: actions/setup-node@v4

‎index.html‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,17 +177,17 @@ <h1>Prove what your AI ran, and what it did.</h1>
177177
<figure class="verify-panel" id="verify-panel" aria-label="A genuine Intel TDX quote, verified in this browser">
178178
<div class="verify-panel-bar"><span>/verify › tdx_quote.bin</span><span>offline · in this browser</span></div>
179179
<ol class="verify-rows">
180-
<li><span class="key">quote</span><span>Intel TDX v4, GCP C3, captured 2026-07-21</span><span></span></li>
180+
<li><span class="key">quote</span><span>Intel TDX v4, GCP C3, captured 2026-09-14</span><span></span></li>
181181
<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>
182182
<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>
183183
<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>
184184
<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>
185-
<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>
185+
<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>
186186
<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>
187187
</ol>
188188
</figure>
189189
<div class="verify-foot">
190-
<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>
190+
<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>
191191
<p>Runs in your browser. Nothing is sent back to us.</p>
192192
</div>
193193
</div>
@@ -271,7 +271,7 @@ <h3>AMD SEV-SNP</h3>
271271
<article class="ecosystem-card">
272272
<h3>Intel TDX</h3>
273273
<p>GCP C3. Quotes verify offline to the pinned Intel SGX Root CA, no collateral service needed.</p>
274-
<a class="evidence-link" href="/verify/">Check the 2026-07-21 capture yourself →</a>
274+
<a class="evidence-link" href="/verify/">Check the 2026-09-14 capture yourself →</a>
275275
</article>
276276
<article class="ecosystem-card">
277277
<h3>NVIDIA H100 confidential computing</h3>

‎llms.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ AgenTrust is the ecosystem at https://agentrust-io.com and the GitHub organizati
77
## Start here
88

99
- [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.
10-
- [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.
10+
- [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.
1111
- [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.
1212
- [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.
1313
- [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.

‎tools/check-key-binding.py‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""Check the key-binding TDX capture published on /verify/.
2+
3+
verify/fixtures/gcp-tdx-2026-09-14-keybind_record.json carries a TRACE v0.3 record
4+
signed inside a GCP C3 trust domain, with the quote that trust domain produced.
5+
The page claims four things about it, and this checks each one with the Python
6+
tools the page's JavaScript is held to:
7+
8+
1. the quote verifies to the pinned Intel SGX Root CA (agent_manifest._tdx_verify);
9+
2. the record carries that exact quote, and claims its MRTD;
10+
3. REPORT_DATA[0:32] is SHA-256 of the record's cnf.jwk.x, and the rest is zero;
11+
4. the record signature verifies under that key, over the SDK's canonical bytes.
12+
13+
It also checks that the published capture program hashes to the digest the record
14+
claims as its build provenance.
15+
16+
It does not validate the record against the TRACE v0.3 draft schema, which lives
17+
in trace-spec, and it does not appraise TCB currency, revocation, or whether the
18+
MRTD is an image anyone intended.
19+
"""
20+
import base64
21+
import hashlib
22+
import json
23+
from pathlib import Path
24+
import sys
25+
26+
from agent_manifest._tdx_verify import parse_tdx_quote, verify_tdx_quote
27+
from agentrust_trace.sign import _canonical_bytes, _pubkey_from_jwk
28+
29+
ROOT = Path(__file__).resolve().parents[1]
30+
FIXTURES = ROOT / 'verify' / 'fixtures'
31+
FIXTURE = FIXTURES / 'gcp-tdx-2026-09-14-keybind_record.json'
32+
PROFILE = 'tag:agentrust-io.com,2026:trace-v0.3'
33+
34+
35+
def unb64u(text):
36+
return base64.urlsafe_b64decode(text + '=' * (-len(text) % 4))
37+
38+
39+
def main():
40+
fixture = json.loads(FIXTURE.read_text(encoding='utf-8'))
41+
record = fixture['record']
42+
quote = (FIXTURES / fixture['quote_file']).read_bytes()
43+
failures = []
44+
45+
def check(condition, message):
46+
if not condition:
47+
failures.append(message)
48+
49+
check(record.get('eat_profile') == PROFILE, f'record profile is not {PROFILE}')
50+
check(verify_tdx_quote(quote) is True, 'quote does not verify to the pinned Intel root')
51+
52+
parsed = parse_tdx_quote(quote)
53+
evidence = record['runtime']['evidence']
54+
check(evidence.get('format') == 'tdx-quote-v4', 'evidence format is not tdx-quote-v4')
55+
check(unb64u(evidence['quote']) == quote, 'record does not carry the published quote')
56+
check(record['runtime'].get('measurement') == 'sha384:' + parsed.mrtd.hex(), 'record does not claim the quote MRTD')
57+
58+
jwk = record['cnf']['jwk']
59+
key = unb64u(jwk['x'])
60+
check(jwk.get('kty') == 'OKP' and jwk.get('crv') == 'Ed25519', 'record key is not Ed25519')
61+
check(jwk['x'] == fixture['public_key_b64u'], 'fixture key differs from the record key')
62+
check(parsed.report_data == hashlib.sha256(key).digest() + bytes(32), 'REPORT_DATA does not commit to the record key')
63+
check(parsed.report_data.hex() == fixture['report_data_hex'], 'fixture REPORT_DATA differs from the quote')
64+
65+
body = _canonical_bytes({k: v for k, v in record.items() if k != 'signature'})
66+
try:
67+
_pubkey_from_jwk(jwk).verify(unb64u(record['signature']), body)
68+
except Exception as error:
69+
failures.append(f'record signature does not verify: {type(error).__name__}')
70+
71+
program = (FIXTURES / 'gcp-tdx-2026-09-14-capture.py').read_bytes()
72+
digest = hashlib.sha256(program).hexdigest()
73+
check(digest == fixture['capture_script_sha256'], 'published capture program differs from the one that ran')
74+
check(record['build_provenance'].get('digest') == 'sha256:' + digest, 'record build provenance is not the capture program')
75+
76+
if failures:
77+
print('\n'.join(f'FAIL {f}' for f in failures))
78+
return 1
79+
print('PASS key-binding capture: quote verifies, REPORT_DATA commits to the record key, record signature verifies')
80+
return 0
81+
82+
83+
if __name__ == '__main__':
84+
sys.exit(main())

‎tools/check-tdx-verifier.mjs‎

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
*/
1313
import { readFile } from 'node:fs/promises';
1414
import { createHash } from 'node:crypto';
15+
import { checkKeyBinding } from '../verify/key-binding.js';
1516
import {
1617
verifyTdxQuote, pinnedRootFingerprint, OFF_MRTD, QUOTE_HEADER_LENGTH,
1718
} from '../verify/tdx-verify.js';
@@ -23,27 +24,45 @@ const WHEN = '2026-09-14T00:00:00Z';
2324
// swapped or re-captured fixture fails here rather than changing what
2425
// "a genuine quote" refers to on the public page.
2526
const CAPTURES = {
26-
'gcp-tdx-2026-07-21-tdx_quote.bin': 'f9efbac112efe510aa8ccd20703b063591b8c2c54c474d0ff1d6500299bae0ba',
27-
'gcp-tdx-2026-07-21-tdx_quote_manifest.bin': '1ae04c74b564ef8795d4c4e4ffd1835d080d9dad4f8879e5cd1e8249503828b2',
27+
'gcp-tdx-2026-07-21-tdx_quote.bin': {
28+
sha256: 'f9efbac112efe510aa8ccd20703b063591b8c2c54c474d0ff1d6500299bae0ba',
29+
mrtd: '9bf86e6280ec4282b8b5822d8166410a456cdb720109aa799f0011fa63df1de3ee5e35e293fc410c061433163acb03a6',
30+
},
31+
'gcp-tdx-2026-07-21-tdx_quote_manifest.bin': {
32+
sha256: '1ae04c74b564ef8795d4c4e4ffd1835d080d9dad4f8879e5cd1e8249503828b2',
33+
mrtd: '9bf86e6280ec4282b8b5822d8166410a456cdb720109aa799f0011fa63df1de3ee5e35e293fc410c061433163acb03a6',
34+
},
35+
// A different trust domain, captured to bind a TRACE record's signing key.
36+
'gcp-tdx-2026-09-14-keybind_quote.bin': {
37+
sha256: '2217b3d640b2e4cdabd34604ea59df7f4ea23ed9702d3ec040689dca20ce1d61',
38+
mrtd: 'c1ee9c16e3afc506cfe042c5b846a368528f3b37618eafb27469bc114cf914e9222c91618470e7f2b28ac360968270a5',
39+
record: 'gcp-tdx-2026-09-14-keybind_record.json',
40+
},
2841
};
29-
const MRTD = '9bf86e6280ec4282b8b5822d8166410a456cdb720109aa799f0011fa63df1de3ee5e35e293fc410c061433163acb03a6';
3042

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

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

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

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

59+
// Only the key-binding capture commits to a record key; the July captures bind
60+
// a manifest hash, and the check must say no for them rather than pass vacuously.
61+
const fixture = JSON.parse(await readFile(new URL(`verify/fixtures/${record || CAPTURES['gcp-tdx-2026-09-14-keybind_quote.bin'].record}`, root), 'utf8'));
62+
const binding = await checkKeyBinding(result, fixture.record, quote);
63+
if (record) check(binding.bound && binding.sameQuote && binding.sameMeasurement, `${name}: REPORTDATA does not bind the published record key`);
64+
else check(!binding.bound && !binding.sameQuote, `${name}: key binding passed for a quote that does not commit to the record key`);
65+
4766
const tampered = quote.slice();
4867
tampered[QUOTE_HEADER_LENGTH + OFF_MRTD] ^= 0xff;
4968
const t = await verifyTdxQuote(tampered, { verificationTime: WHEN });
@@ -94,4 +113,4 @@ if (failures.length) {
94113
console.error(failures.join('\n'));
95114
process.exit(1);
96115
}
97-
console.log('PASS both GCP TDX captures verify; tampered quote and expired chain are rejected at the right step');
116+
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');

‎tools/tdx-differential.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
CAPTURES = [
3131
"gcp-tdx-2026-07-21-tdx_quote.bin",
3232
"gcp-tdx-2026-07-21-tdx_quote_manifest.bin",
33+
"gcp-tdx-2026-09-14-keybind_quote.bin",
3334
]
3435
# Fixed, so a certificate expiring can never make the two runs disagree.
3536
VERIFICATION_TIME = "2026-09-14T00:00:00+00:00"
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import base64
2+
import hashlib
3+
import json
4+
import pathlib
5+
import time
6+
7+
from agentrust_trace.sign import sign_record
8+
from cryptography.hazmat.primitives import serialization
9+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
10+
11+
# Generated inside the TD. The private half is never serialized.
12+
key = Ed25519PrivateKey.generate()
13+
public = key.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
14+
report_data = hashlib.sha256(public).digest() + bytes(32)
15+
16+
report = pathlib.Path("/sys/kernel/config/tsm/report/agentrust-capture")
17+
report.mkdir()
18+
(report / "inblob").write_bytes(report_data)
19+
quote = (report / "outblob").read_bytes()
20+
provider = (report / "provider").read_text().strip()
21+
generation = (report / "generation").read_text().strip()
22+
report.rmdir()
23+
24+
b64u = lambda raw: base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
25+
mrtd = quote[48 + 136:48 + 184]
26+
script_sha256 = hashlib.sha256(pathlib.Path(__file__).read_bytes()).hexdigest()
27+
28+
record = {
29+
"eat_profile": "tag:agentrust-io.com,2026:trace-v0.3",
30+
"iat": int(time.time()),
31+
"subject": "spiffe://agentrust-io.com/capture/gcp-tdx-key-binding",
32+
"model": {"provider": "none", "model_id": "none: attestation capture, no model loaded"},
33+
"runtime": {
34+
"platform": "intel-tdx",
35+
"measurement": "sha384:" + mrtd.hex(),
36+
"firmware_version": "gcp-c3",
37+
"evidence": {
38+
"format": "tdx-quote-v4",
39+
"quote": b64u(quote),
40+
"collateral": "embedded",
41+
"binds": "cnf-key",
42+
},
43+
},
44+
# No policy governed this capture: the digest is of empty input and the mode
45+
# only declares, it enforces nothing.
46+
"policy": {"bundle_hash": "sha256:" + hashlib.sha256(b"").hexdigest(), "enforcement_mode": "declared"},
47+
"data_class": "public",
48+
# The digest is of this capture program, published beside the capture, so it
49+
# can be recomputed. SLSA level 0: nothing attests how the VM image was built.
50+
"build_provenance": {"slsa_level": 0, "digest": "sha256:" + script_sha256},
51+
"appraisal": {"status": "none", "verifier": "https://github.com/agentrust-io/agent-manifest"},
52+
}
53+
signed = sign_record(record, key)
54+
55+
bundle = json.dumps(
56+
{
57+
"quote_b64": base64.b64encode(quote).decode(),
58+
"public_key_b64u": b64u(public),
59+
"report_data_hex": report_data.hex(),
60+
"tsm_provider": provider,
61+
"tsm_generation": generation,
62+
"capture_script_sha256": script_sha256,
63+
"record": signed,
64+
},
65+
separators=(",", ":"),
66+
).encode()
67+
encoded = base64.b64encode(bundle).decode()
68+
chunks = [encoded[i:i + 900] for i in range(0, len(encoded), 900)]
69+
print(f"AGT-BUNDLE sha256={hashlib.sha256(bundle).hexdigest()} chunks={len(chunks)}", flush=True)
70+
for i, chunk in enumerate(chunks):
71+
print(f"AGT-CHUNK {i:04d} {chunk}", flush=True)
7.81 KB
Binary file not shown.

0 commit comments

Comments
 (0)