Skip to content

Commit 6400d5f

Browse files
h4x3rotabclaude
andcommitted
add tsm-shim example: run unmodified configfs-tsm binaries via a sidecar
tsm-shim re-exposes the dstack guest-agent GetQuote RPC under the standard configfs-tsm file ABI (inblob/outblob), so unmodified TDX attestation binaries run on a stock dstack CVM with no OS change -- only docker-compose edits. Pure userspace: no FUSE, no CAP_SYS_ADMIN, no privileged container. - tsm-shim/: shim daemon (stdlib), Dockerfile, demo client, compose - .github/workflows/build-tsm-shim.yml: build & publish image to GHCR - README: index entry under Core Patterns > Attestation Verified on real Phala Cloud TDX (tdx.small / dstack-0.5.7): genuine 5010-byte DCAP quote, header 0400, report_data bound. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9744eab commit 6400d5f

7 files changed

Lines changed: 435 additions & 0 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: build tsm-shim
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- 'tsm-shim/**'
8+
- '.github/workflows/build-tsm-shim.yml'
9+
pull_request:
10+
paths:
11+
- 'tsm-shim/**'
12+
- '.github/workflows/build-tsm-shim.yml'
13+
workflow_dispatch:
14+
15+
permissions:
16+
contents: read
17+
packages: write
18+
19+
jobs:
20+
build:
21+
runs-on: ubuntu-latest
22+
env:
23+
IMAGE: ghcr.io/dstack-tee/dstack-tsm-shim
24+
steps:
25+
- uses: actions/checkout@v4
26+
27+
- uses: docker/setup-buildx-action@v3
28+
29+
# On PRs (incl. forks) GITHUB_TOKEN lacks packages:write, so only build.
30+
- name: Log in to GHCR
31+
if: github.event_name != 'pull_request'
32+
uses: docker/login-action@v3
33+
with:
34+
registry: ghcr.io
35+
username: ${{ github.actor }}
36+
password: ${{ secrets.GITHUB_TOKEN }}
37+
38+
- name: Docker metadata
39+
id: meta
40+
uses: docker/metadata-action@v5
41+
with:
42+
images: ${{ env.IMAGE }}
43+
tags: |
44+
type=raw,value=latest,enable={{is_default_branch}}
45+
type=sha,format=short
46+
47+
- name: Build and push
48+
uses: docker/build-push-action@v6
49+
with:
50+
context: tsm-shim
51+
file: tsm-shim/Dockerfile
52+
platforms: linux/amd64
53+
push: ${{ github.event_name != 'pull_request' }}
54+
tags: ${{ steps.meta.outputs.tags }}
55+
labels: ${{ steps.meta.outputs.labels }}

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ volumes:
128128
|---------|-------------|--------|
129129
| [timelock-nts](./timelock-nts) | Raw socket usage (what the SDK wraps) | Available |
130130
| [attestation/configid-based](./attestation/configid-based) | ConfigID-based verification | Available |
131+
| [tsm-shim](./tsm-shim) | Run unmodified `configfs-tsm` binaries (inblob/outblob) via a sidecar | Available |
131132

132133
### Gateway & Domains
133134

tsm-shim/Dockerfile

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Pure stdlib — no pip, no build deps. Built & pushed to GHCR by
2+
# .github/workflows/build-tsm-shim.yml.
3+
FROM python:3.12-slim
4+
5+
LABEL org.opencontainers.image.title="dstack-tsm-shim"
6+
LABEL org.opencontainers.image.description="configfs-tsm compatibility shim: re-exposes dstack guest-agent GetQuote as inblob/outblob FIFOs"
7+
LABEL org.opencontainers.image.source="https://github.com/Dstack-TEE/dstack-examples"
8+
9+
COPY tsm_shim.py /usr/local/bin/tsm_shim.py
10+
COPY demo-app.py /usr/local/bin/demo-app.py
11+
12+
ENV TSM_REPORT_DIR=/run/tsm/report \
13+
DSTACK_SOCKET=/var/run/dstack.sock
14+
15+
# Report healthy only once both FIFOs exist, so an app can gate on
16+
# `depends_on: { tsm-shim: { condition: service_healthy } }` with no race.
17+
HEALTHCHECK --interval=2s --timeout=2s --retries=30 --start-period=1s \
18+
CMD test -p "$TSM_REPORT_DIR/inblob" && test -p "$TSM_REPORT_DIR/outblob" || exit 1
19+
20+
# Default role is the shim daemon (reads TSM_REPORT_DIR / DSTACK_SOCKET from env).
21+
ENTRYPOINT ["python3", "/usr/local/bin/tsm_shim.py"]

tsm-shim/README.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# configfs-tsm shim (run unmodified TDX attestation binaries)
2+
3+
Some programs request a TDX quote through the **standard Linux interfaces**
4+
`configfs-tsm` (`/sys/kernel/config/tsm/report/*`, with `inblob`/`outblob`) and a
5+
`/dev/tdx-guest` device — rather than through the dstack SDK / guest-agent
6+
socket. On a stock dstack CVM those kernel interfaces aren't exposed to app
7+
containers, so such binaries fail out of the box.
8+
9+
This example ships a small **sidecar** that bridges the gap. It re-exposes the
10+
guest-agent's `GetQuote` RPC under the configfs-tsm file ABI: your app writes
11+
`report_data` to `inblob` and reads the raw Intel DCAP TDX quote from `outblob`,
12+
exactly as it would against the kernel.
13+
14+
- **No OS change** — pure userspace, runs in a normal container.
15+
- **No FUSE, no `CAP_SYS_ADMIN`, no privileged mode, no device passthrough.**
16+
- **No weaker attestation** — the quote is the genuine hardware quote and
17+
`report_data` is forwarded byte-for-byte.
18+
19+
The shim image is built and published to GHCR by
20+
[`.github/workflows/build-tsm-shim.yml`](../.github/workflows/build-tsm-shim.yml):
21+
`ghcr.io/dstack-tee/dstack-tsm-shim`.
22+
23+
## Try it
24+
25+
```bash
26+
phala deploy -n tsm-shim-example -c docker-compose.yaml
27+
phala cvms logs <app_id> -c app
28+
```
29+
30+
Expected `app` log:
31+
32+
```
33+
quote length : 5010 bytes
34+
quote header : 0400 (a TDX v4 quote starts with 0400)
35+
report_data bound in quote: True
36+
PASS - unmodified configfs-tsm app got a real TDX quote via the shim
37+
```
38+
39+
## Adopt it in your app
40+
41+
Add the `tsm-shim` service and two volumes, then add the four lines marked `(+)`
42+
to your existing service:
43+
44+
```yaml
45+
services:
46+
tsm-shim:
47+
image: ghcr.io/dstack-tee/dstack-tsm-shim:latest
48+
restart: unless-stopped
49+
volumes:
50+
- /var/run/dstack.sock:/var/run/dstack.sock
51+
- tsm-report:/run/tsm/report
52+
53+
my-app:
54+
image: your-app:latest
55+
# ... your existing config ...
56+
depends_on:
57+
tsm-shim:
58+
condition: service_healthy # (+) wait until the shim is ready
59+
environment:
60+
- TSM_REPORT_PATH=/run/tsm/report # (+) point your app at the shim dir
61+
volumes:
62+
- tsm-report:/run/tsm/report # (+) see the shim's inblob/outblob
63+
- tsm-devstub:/dev/tdx-guest # (+) satisfy /dev/tdx-guest checks
64+
65+
volumes:
66+
tsm-report: {}
67+
tsm-devstub: {}
68+
```
69+
70+
If your binary **hard-codes** `/sys/kernel/config/tsm/report`, mount the shared
71+
volume there instead of using `TSM_REPORT_PATH`:
72+
73+
```yaml
74+
volumes:
75+
- tsm-report:/sys/kernel/config/tsm/report
76+
```
77+
78+
For production, pin the image by digest (e.g.
79+
`ghcr.io/dstack-tee/dstack-tsm-shim:latest@sha256:...`).
80+
81+
## How it works
82+
83+
`tsm-shim` exposes `inblob` and `outblob` as **named pipes (FIFOs)** in a shared
84+
volume. A read of `outblob` blocks until the quote for the most recent `inblob`
85+
write is ready — which matches configfs-tsm's write-then-read contract with no
86+
race and no privileges. When `inblob` is written, the shim calls
87+
`POST /GetQuote` on `/var/run/dstack.sock` and writes the returned quote to
88+
`outblob`. The image reports healthy only once both FIFOs exist, so the app can
89+
gate on `depends_on: { condition: service_healthy }`.
90+
91+
The `/dev/tdx-guest` device can't be created inside another container from a
92+
sidecar, so an empty volume is mounted at that path — enough for the common
93+
"does the device exist?" check. (dstack permits mounting under `/dev`.)
94+
95+
## Files
96+
97+
| File | Purpose |
98+
|------|---------|
99+
| `tsm_shim.py` | the sidecar daemon (pure Python stdlib, no dependencies) |
100+
| `demo-app.py` | a stand-in unmodified configfs-tsm consumer (bundled self-test) |
101+
| `Dockerfile` | builds the published image |
102+
| `docker-compose.yaml` | the shim + demo app wired together |
103+
104+
## Limitations
105+
106+
- Covers the **configfs-tsm `inblob`/`outblob`** path (used by `go-configfs-tsm`,
107+
recent `libtdx-attest`, etc.).
108+
- Does **not** emulate the `/dev/tdx-guest` `TDX_CMD_GET_REPORT0` ioctl. That
109+
returns a raw, locally-MAC'd TDREPORT, which dstack does not expose and which
110+
can't be derived from a quote — so it isn't recoverable in userspace by any
111+
shim. Binaries that drive the device by ioctl rather than configfs are out of
112+
scope.
113+
- One quote at a time per shim instance (matches the kernel's single
114+
configfs-tsm entry). Run one shim per app.

tsm-shim/demo-app.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#!/usr/bin/env python3
2+
"""Demo "unmodified" attestation app — a standard configfs-tsm consumer.
3+
4+
Does exactly what a real binary built against the Linux TSM interface does:
5+
1. check that the TDX guest device exists,
6+
2. write up to 64 bytes of report_data to <dir>/inblob,
7+
3. read the raw Intel DCAP TDX quote from <dir>/outblob.
8+
9+
The only deployment-specific knob is TSM_REPORT_PATH. On a stock dstack CVM that
10+
directory is served by the tsm-shim sidecar instead of the kernel.
11+
"""
12+
import hashlib
13+
import os
14+
import sys
15+
import time
16+
17+
18+
def detect_tdx() -> bool:
19+
return os.path.exists("/dev/tdx-guest") or os.path.exists("/dev/tdx_guest")
20+
21+
22+
def main() -> None:
23+
report_dir = os.environ.get("TSM_REPORT_PATH", "/sys/kernel/config/tsm/report/dstack")
24+
25+
if not detect_tdx():
26+
print("FAIL: no TDX guest device (/dev/tdx-guest)")
27+
sys.exit(1)
28+
29+
# `depends_on: condition: service_healthy` already gates startup on the shim
30+
# FIFOs existing; this short retry just mirrors what real attestation libs do.
31+
for _ in range(100):
32+
if os.path.exists(f"{report_dir}/inblob"):
33+
break
34+
time.sleep(0.1)
35+
36+
report_data = hashlib.sha256(b"dstack-tsm-shim-demo").digest() # 32 bytes
37+
with open(f"{report_dir}/inblob", "wb") as f:
38+
f.write(report_data[:64].ljust(64, b"\0"))
39+
with open(f"{report_dir}/outblob", "rb") as f:
40+
quote = f.read()
41+
42+
print(f"report_dir : {report_dir}")
43+
print(f"report_data : {report_data.hex()}")
44+
print(f"quote length : {len(quote)} bytes")
45+
print(f"quote header : {quote[:2].hex()} (a TDX v4 quote starts with 0400)")
46+
bound = report_data[:32] in quote
47+
print(f"report_data bound in quote: {bound}")
48+
print(
49+
"PASS - unmodified configfs-tsm app got a real TDX quote via the shim"
50+
if (quote[:2].hex() == "0400" and bound)
51+
else "FAIL - unexpected quote (header or report_data binding off)"
52+
)
53+
54+
sys.stdout.flush()
55+
while True:
56+
time.sleep(3600)
57+
58+
59+
if __name__ == "__main__":
60+
main()

tsm-shim/docker-compose.yaml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: tsm-shim-example
2+
3+
# Run an UNMODIFIED configfs-tsm attestation binary on a stock dstack CVM.
4+
#
5+
# `tsm-shim` is a prebuilt sidecar (built & published to GHCR by
6+
# .github/workflows/build-tsm-shim.yml) that re-exposes the dstack guest-agent's
7+
# GetQuote RPC under the standard configfs-tsm file ABI (inblob/outblob). Your
8+
# app talks to the kernel-style interface it already expects; the shim forwards
9+
# report_data to real TDX hardware and returns the genuine quote. No OS change,
10+
# no FUSE, no privileged container, no extra capabilities.
11+
#
12+
# The `app` service below is a self-test (it reuses the shim image only to run a
13+
# bundled demo client). In your own deployment, replace `app` with your service
14+
# and add the four lines marked (+).
15+
16+
services:
17+
# ---------- the shim sidecar ----------
18+
tsm-shim:
19+
image: ghcr.io/dstack-tee/dstack-tsm-shim:latest
20+
restart: unless-stopped
21+
volumes:
22+
- /var/run/dstack.sock:/var/run/dstack.sock # source of real quotes
23+
- tsm-report:/run/tsm/report # FIFOs shared with the app
24+
25+
# ---------- your app (here: a bundled demo client) ----------
26+
app:
27+
image: ghcr.io/dstack-tee/dstack-tsm-shim:latest # <- replace with your image
28+
entrypoint: ["python3", "/usr/local/bin/demo-app.py"] # <- your app's entrypoint
29+
depends_on:
30+
tsm-shim:
31+
condition: service_healthy # (+) wait until FIFOs exist
32+
environment:
33+
- TSM_REPORT_PATH=/run/tsm/report # (+) point the app at the shim
34+
volumes:
35+
- tsm-report:/run/tsm/report # (+) see the shim's FIFOs
36+
- tsm-devstub:/dev/tdx-guest # (+) make /dev/tdx-guest "exist"
37+
38+
volumes:
39+
tsm-report: {}
40+
tsm-devstub: {}

0 commit comments

Comments
 (0)