Skip to content

feat(tdx): add TDX backend with configfs-tsm direct adapter#68

Open
0xHansLee wants to merge 2 commits into
mainfrom
feat/impl-tdx-backend
Open

feat(tdx): add TDX backend with configfs-tsm direct adapter#68
0xHansLee wants to merge 2 commits into
mainfrom
feat/impl-tdx-backend

Conversation

@0xHansLee

@0xHansLee 0xHansLee commented May 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds an Intel TDX backend to story-kernel and aligns the kernel-reported identity with Story's hybrid on-chain TDXValidationHook under schema v3 (RTMR3-bound binary commitment).

  • New backend at enclave/tdx/, selectable via -tags tdx / make build-tdx. Implements the existing enclave.TEE / enclave.SealDB contract so service/, server/, and store/ stay backend-agnostic.
  • Vendor adapter abstraction at enclave/tdx/platform/ with the upstream Linux configfs-tsm path (direct adapter) as the supported vendor. Runtime selection via STORY_TDX_VENDOR with auto-detect fallback through Probe().
  • Sealed storage uses TPM2 PolicyOR over per-provider PCR sets (supportedProviders) combined with an AES-GCM hybrid wrap, so the TPM only seals the data-encryption key. PolicyOR digest population is driven by a bootstrap-mode WARN log so operators can pin the empirically measured digest on first deploy.
  • Identity reports codeCommitment = keccak256(RTMR3) to match the hybrid hook's binary half. RTMR3 is self-extended by the kernel exactly once at startup with SHA-384(/proc/self/exe), so the post-extend value SHA-384(0x00..00 || SHA-384(elf)) is fully determined by the running ELF. The chain-side hook independently checks the platform half keccak256(MRTD || RTMR0 || RTMR1 || RTMR2) against its approvePlatform whitelist; the kernel side does not gate on that.
  • Startup self-check asserts TPM responsiveness, non-zero PCRs, provider-digest match (or bootstrap-mode WARN), and a quote round-trip whose V4.report_data carries a canary.

How identity flows on chain

kernel side                                on-chain TDXValidationHook
RTMR3 ─── keccak256 ──► codeCommitment ──► whitelistEnclaveType[type=2].codeCommitment
                          (binary half)      (operator-managed; rotates with the ELF)

MRTD ┐
RTMR0│
RTMR1├── keccak256 ──► platformCommitment ──► approvedPlatforms[h]
RTMR2┘    (platform half, derived on chain)    (operator-managed; rotates with cloud firmware/initrd)

The matrix decomposes: N binaries × M platform vintages collapse from N×M into N + M governance rows, and each row tracks exactly one axis.

@0xHansLee 0xHansLee self-assigned this May 24, 2026
@wiz-837b06c6da

wiz-837b06c6da Bot commented May 24, 2026

Copy link
Copy Markdown

Wiz Scan Summary

Scanner Findings
Vulnerability Finding Vulnerabilities -
Data Finding Sensitive Data -
Secret Finding Secrets -
IaC Misconfiguration IaC Misconfigurations -
SAST Finding SAST Findings -
Software Management Finding Software Management Findings -
Total -

View scan details in Wiz

To detect these findings earlier in the dev lifecycle, try using Wiz Code VS Code Extension.

@0xHansLee
0xHansLee force-pushed the feat/impl-tdx-backend branch 2 times, most recently from d32137d to 1ab3d10 Compare May 26, 2026 09:06
@0xHansLee
0xHansLee force-pushed the feat/impl-tdx-backend branch from 05004f6 to 12f1e6f Compare May 26, 2026 13:11
@0xHansLee
0xHansLee marked this pull request as ready for review May 26, 2026 13:17
@jinn-agent

jinn-agent Bot commented May 26, 2026

Copy link
Copy Markdown

The PR is large and the input is truncated — enclave/tdx/ files (backend, seal, quote, selfcheck, providers, platform/) have null patches and could not be reviewed. Findings below are limited to the visible files.

High-confidence issue found: sealdb.Close() silently drops storErr when dbErr is non-nil. Everything else in the visible surface (noop registration, SGX defensive copies, build-tag mutual exclusion, iterator positioning) looks correct. The prior setup-go@v6 CI concerns are not repeated.


Review iteration 11 · Commit 89a3aa1 · 2026-06-22T02:28:08Z

@0xHansLee
0xHansLee force-pushed the feat/impl-tdx-backend branch from 12f1e6f to b504b4b Compare May 27, 2026 01:07
Comment thread enclave/tdx/seal.go Outdated
0xHansLee added a commit to piplabs/story that referenced this pull request May 29, 2026
## Summary

On-chain validator for TDX V4/V5 attestation quotes used by the DKG
flow. Mirrors `SGXValidationHook` in structure (`Ownable2Step +
Pausable`) with TDX-specific quote field offsets.

## Identity model (v3 — RTMR3-bound binary, RTMR2-included platform)

Split along the operator / cloud boundary, where "operator-controlled"
means *the running Go binary*:

- **Binary identity** (operator-controlled): `keccak256(RTMR3)`,
whitelisted via `EnclaveTypeData.codeCommitment` (same path as SGX
`MRENCLAVE`). RTMR3 is self-extended by the kernel exactly once at
startup with `SHA-384(/proc/self/exe)` (see [story-kernel
#68](piplabs/story-kernel#68)), so its
post-extend value `SHA-384(0x00..00 || SHA-384(elf))` is fully
determined by the running ELF.
- **Platform identity** (cloud-managed): `keccak256(MRTD || RTMR0 ||
RTMR1 || RTMR2)`, approved via the hook's `approvePlatform` admin. RTMR2
moves into the platform half because it measures TD initrd + kernel
cmdline — both boot-image properties decided at TD launch by the
firmware/boot chain, never the userspace Go binary loaded from disk
after boot.

Why not `keccak256(RTMR2)` for the binary half: on every direct-launch
cloud TD measured, RTMR2 contains the TD initrd + cmdline digest. The
story-kernel Go binary is loaded from the rootfs after boot and is never
fed into RTMR2 by any link of the boot chain — so a `keccak256(RTMR2)`
rule would commit to a boot-image artifact, not to the running ELF. The
v3 split corrects this by claiming RTMR3 explicitly for the userspace
binary half.

This decouples binary release governance from cloud firmware vintage
rotation, so each axis updates independently. `REPORT_DATA[0:32]`
carries the kernel-bound caller commitment from `DKG.register` and is
compared against `expectedDataCommitment`, mirroring SGX's instance
binding.

## Onboarding flow

A TDX enclave type is enabled in two steps:

1. Governance: `DKG.whitelistEnclaveType(enclaveType, { codeCommitment =
keccak256(RTMR3), validationHookAddr = tdxProxy })`.
2. Hook owner: `TDXValidationHook.approvePlatform(keccak256(MRTD ||
RTMR0 || RTMR1 || RTMR2), label)`.

## Contract changes

- `TDXValidationHook` (new). `_computeBinaryCommitment` reads `RTMR3` at
offset 520; `_computePlatformCommitment` assembles the 192-byte preimage
from MRTD (offset 184) + the contiguous RTMR0/RTMR1/RTMR2 block (offsets
376..519).
- `ITDXValidationHook` (new). Admin surface:
`setAutomataValidationAddr`, `approvePlatform`, `revokePlatform`,
`isPlatformApproved`.
- `IAutomataDcapAttestationFee`: adds 1-arg overload (PR #816 pattern)
so standard TCB transitions take effect automatically.

`DKG.sol` / `IDKG.sol` are unchanged — ABI/storage compatible with the
live deployment.

## Adversary cost — both halves must succeed

To pass `register()` an attacker must produce a TDX quote where:

- `RTMR3` hashes to an approved binary commitment, **and**
- `MRTD || RTMR0 || RTMR1 || RTMR2` hashes to an approved platform
commitment.

The RTMR3 self-extend can only push values *into* RTMR3 (RTMRs are
append-only). An attacker binary `B'` that self-extends RTMR3 with
`SHA-384(honest_B)` to forge the binary commitment still has to be
loaded by the boot chain — which measures the kernel image + initrd into
RTMR1 and the initrd + cmdline into RTMR2. Either modification shifts
the platform commitment off any honest entry, and the platform-approval
check fails.
@0xHansLee
0xHansLee force-pushed the feat/impl-tdx-backend branch from b504b4b to 6a3ca9c Compare June 8, 2026 08:30
@yingyangxu2026

Copy link
Copy Markdown
Contributor

#68's 64/65 key-length asserts conflict with the now-merged #844 — please change them to 32/64 before merging.

This PR pins dkgPubKeySize=64 / enclaveCommKeySize=65 in service/utils.go::calculateReportData, with a comment saying they match DKG.sol::register. Both values are now wrong:

  1. Out of sync with the contract: #844 (merged into dkg/dev) already changed DKG.sol::register to require 32/64, to align with what the kernel actually emits.
  2. Out of sync with the kernel's own output (the more serious one): the kernel's DKG runs on ed25519, so dkgPubKey is inherently 32 bytes; enclaveCommKey is the secp256k1 key with the 0x04 prefix stripped = 64 bytes. This PR would feed the real 32/64 keys into its own 64/65 asserts → registration fails. The current dd7eca07 kernel registers fine precisely because the older calculateReportData had no such asserts.

Pinning the lengths to keep the packed preimage injective is the right idea — just pin to the correct values. Before merging, please change:

  • service/utils.go: dkgPubKeySize 64 → 32, enclaveCommKeySize 65 → 64
  • service/utils_test.go: validDkgPubKey 64 → 32; validEnclaveCommKey 65 → 64 and drop k[0] = 0x04 (the kernel already strips the prefix, so there's no 0x04 in the 64-byte form)

See issue #73 and story PR #844 for the kernel↔contract alignment.

@jinn-agent jinn-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 4 critical issue(s) that need attention.

Comment thread .github/workflows/golangci-lint.yml
Comment thread .github/workflows/buf.yml
Comment thread .github/workflows/gotest.yml
Comment thread .github/workflows/pre-commit.yml
@0xHansLee
0xHansLee force-pushed the feat/impl-tdx-backend branch 4 times, most recently from bef9a75 to 89a3aa1 Compare June 22, 2026 02:23
Extract the TEE-backend surface — code commitment, remote quote, and key
sealing — into a single interface with build-tag-selected implementations,
so a new backend can be added without changing call sites. Move the existing
SGX/Gramine code under enclave/sgx, add a no-op backend for non-TEE builds,
and split the sealed key store into enclave/sealdb. No behavioral change.
@0xHansLee
0xHansLee force-pushed the feat/impl-tdx-backend branch from 89a3aa1 to 302cbf1 Compare June 23, 2026 10:06
…ment

Implement the TDX backend behind the TEE interface:

- Acquire a raw TDX v4 quote via configfs-tsm (direct, non-paravisor adapter)
  with a vendor/platform selection layer and a fail-closed fallback.
- Derive the binary commitment from RTMR3, self-extended at init with the
  measurement of the running kernel binary (binary_commitment = keccak256(RTMR3)).
- Bind key and DKG-state sealing to the measured boot via PCR 12, which the
  backend self-extends with the binary measurement when the initrd-side
  measurement is unavailable under the hardened image.
- Pin the attestation report-data field sizes to the kernel's actual output
  and the contract's register (32-byte ed25519 dkgPubKey, 64-byte secp256k1 commKey).
- Add production-image setup scripts, platform-commitment extraction tooling,
  and operator/auditor setup docs.

Also bumps the CI setup-go action.
@0xHansLee
0xHansLee force-pushed the feat/impl-tdx-backend branch from 302cbf1 to a60277c Compare June 23, 2026 10:16

@ramtinms ramtinms left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏 nice work

@0xHansLee

Copy link
Copy Markdown
Collaborator Author

Let's hold to merge this PR until releasing v0.2.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants