Skip to content

Commit ca7909f

Browse files
committed
docs and SDK follow-up for AWS NitroTPM simplify
- UKI/tools: document no per-app mr_config_id in cmdline; config via PCR8 - auth-simple: KMS allowlist is mrAggregated-only; fix examples and tests - SDKs (Rust/Python/JS/Go/curl): drop GetQuote/Attest attestation_options - production runbook + security-model: PCR8 config, single PCR14 event lane, no recipient v2; security-eval banner for superseding historical sections
1 parent 4062083 commit ca7909f

16 files changed

Lines changed: 75 additions & 345 deletions

File tree

docs/aws-attested-instance-security-evaluation.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# AWS EC2 Instance Attestation Security Evaluation
22

3+
> **Current model (post-simplify, PR #753):** App/config is **not** embedded
4+
> in UKI cmdline. Shared-disk `MrConfigV3` is measured into **PCR8**. All dstack
5+
> events extend **SHA384 PCR14** only (TDX RTMR3 analogue; no PCR23 runtime
6+
> split). `os_image_hash` prefers unified `sha256(sha256sum.txt)` via
7+
> `VmConfig.aws_measurement`. GetAppKey is RA-TLS v1 only (no recipient v2).
8+
> KMS auth pins early **`mrAggregated`** only. Historical sections below that
9+
> describe cmdline `dstack.mr_config_id`, PCR23 runtime, recipient encryption,
10+
> or `kms.composeHashes` are superseded by this model.
11+
312
Date: 2026-07-03
413

514
This note compares dstack attestation with AWS EC2 instance

docs/aws-ec2-production-verifier-runbook.md

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -151,22 +151,26 @@ PCR12: 0000000000000000000000000000000000000000000000000000000000000000000000000
151151
UKI AuthentiCode SHA256: <uki-authenticode-sha256>
152152
```
153153

154-
For a dynamic app package, use the deployment-specific manifest instead. It adds
155-
the app package's measured `dstack.mr_config_id` alongside its own boot PCR set:
154+
For a dynamic app package, the **UKI/AMI stays generic**. App/config identity is
155+
on the shared disk (`MrConfigV3` in `.sys-config.json` / host-shared) and is
156+
measured into **PCR8** at guest setup (not embedded as `dstack.mr_config_id` in
157+
the UKI cmdline). Record the expected config id and shared-disk hash in the
158+
deployment-specific release package:
156159

157160
```text
158-
dstack_os_image_hash: <os-image-hash>
159-
PCR4: <pcr4>
160-
PCR7: <pcr7>
161-
PCR12: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
162-
UKI AuthentiCode SHA256: <uki-authenticode-sha256>
163-
dstack.mr_config_id: <mr-config-id>
161+
dstack_os_image_hash: <os-image-hash> # prefer sha256(sha256sum.txt) + aws_measurement
162+
PCR4/PCR7/PCR12: <boot PCRs for the AMI UKI>
163+
PCR8: <sha384 extend of MrConfigV3 config id from shared disk>
164+
PCR14: <event-log replay; early pin uses boot-mr-done cutoff>
165+
shared MrConfigV3 document hash / config id: <mr-config-id>
164166
```
165167

166168
## 3. Verify Attestation Freshness
167169

168170
Production challenge-response verification must use a non-empty freshness
169-
policy. The verifier rejects empty freshness objects.
171+
policy. The verifier rejects empty freshness objects. Challenge binding uses
172+
**report_data → NitroTPM user_data** (same role as TDX/GCP); there is no
173+
GetAppKey recipient `public_key` / v2 encryption path.
170174

171175
Example verifier request shape:
172176

@@ -175,8 +179,6 @@ Example verifier request shape:
175179
"attestation": "hex-encoded-dstack-attestation",
176180
"freshness": {
177181
"expected_report_data": "hex-encoded-64-byte-challenge",
178-
"expected_nonce": "hex-encoded-nitrotpm-nonce",
179-
"expected_public_key": "hex-encoded-nitrotpm-recipient-public-key",
180182
"max_age_seconds": 300
181183
}
182184
}
@@ -216,13 +218,14 @@ The relying party should then check `details.boot_info` against its auth policy.
216218
For AWS NitroTPM, the policy must require:
217219

218220
- accepted `tcbStatus` (`UpToDate` by default, matching the normalized value);
219-
- accepted `osImageHash`;
221+
- accepted `osImageHash` (unified `sha256(sha256sum.txt)` when `aws_measurement`
222+
is present; otherwise legacy PCR-derived hash);
220223
- accepted app `composeHash` and `appId`;
221-
- accepted KMS identity, preferably stable KMS `composeHash` plus OS image for
222-
allow-any-device deployments;
223-
- verified PCR14-derived `mrAggregated`;
224-
- recipient-key binding for key release;
225-
- non-empty freshness verification for external verifier flows.
224+
- accepted KMS identity via **early `mrAggregated`** (boot-mr-done), same as
225+
bare TDX — not `kms.composeHashes`;
226+
- verified PCR14 event-log replay (single event lane; no PCR23 runtime split);
227+
- verified PCR8 config commitment against shared-disk MrConfigV3;
228+
- non-empty freshness via `report_data` for external verifier flows.
226229

227230
The `attestationMode` field is carried in the verified boot info for
228231
observability; a relying party may additionally assert

docs/security/security-model.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Infrastructure operators can still deny service. They can shut down your workloa
4343
| Firmware and boot path | Boot measurement | TDX/SNP MRTD and RTMR0-2, or AWS NitroTPM PCR4/PCR7/PCR12 |
4444
| OS image | Reproducible image measurement | dstack OS image hash and platform reference measurements |
4545
| Application launch identity | Event-log replay | RTMR3 on TDX-family platforms, SHA384 PCR14 on AWS NitroTPM |
46-
| Application runtime telemetry | Event-log replay when policy requires it | RTMR3 on TDX-family platforms, SHA384 PCR23 on AWS NitroTPM |
46+
| Application runtime telemetry | Event-log replay when policy requires it | RTMR3 on TDX-family platforms; SHA384 PCR14 on AWS NitroTPM (same event lane as launch; TDX RTMR3 analogue) |
4747

4848
### Isolation
4949

@@ -90,7 +90,7 @@ dstack implements layered verification from platform hardware to application ide
9090

9191
**Application launch layer.** Your application launch identity includes the compose-hash, app ID, instance ID, key-provider identity, and OS image hash. On TDX-family platforms, dstack extends those events into RTMR3. On AWS NitroTPM, dstack extends those launch events into non-resettable SHA384 PCR14 and treats `system-ready` as the launch boundary. Each container image must use SHA256 digest pinning. This proves which normalized container configuration was authorized before key release.
9292

93-
**Runtime telemetry layer.** Application-owned runtime events remain available after launch. On AWS NitroTPM, those events use PCR23, which is resettable and must not gate pre-secret launch authorization. Treat PCR23 as runtime telemetry unless a workload-specific policy explicitly verifies it.
93+
**Runtime telemetry layer.** Application-owned runtime events remain available after launch. On AWS NitroTPM they extend the same SHA384 PCR14 event lane as OS launch events (like TDX RTMR3). Policy may still treat `system-ready` as a logical boundary when interpreting the event log; there is no separate PCR23 runtime split.
9494

9595
**Key management layer.** The KMS root CA public key hash is recorded as the key-provider launch event. This binds your workload to a specific KMS instance. The KMS itself runs with its own attestation evidence, so you can verify the KMS the same way you verify any workload.
9696

@@ -153,7 +153,7 @@ Use this checklist to verify a workload running in a dstack CVM.
153153
- [ ] compose-hash matches your docker-compose
154154
- [ ] All images use SHA256 digests (no mutable tags)
155155
- [ ] Launch event log replays correctly (RTMR3 on TDX-family platforms, PCR14 on AWS NitroTPM)
156-
- [ ] `mr_config_id` matches the expected app/config target when the platform uses measured config IDs
156+
- [ ] Config commitment matches the expected app/config target (TDX `mr_config_id` / SEV `HOST_DATA` / AWS PCR8 from shared-disk MrConfigV3)
157157
- [ ] reportData contains your challenge (replay protection)
158158

159159
**Key management verification:**

dstack/kms/auth-simple/README.md

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ bun install
2121
Create `auth-config.json` (see `auth-config.example.json`).
2222

2323
For KMS deployment, you must allowlist the OS image hash and at least one KMS
24-
identity value. Use `kms.composeHashes` for stable KMS app identity, especially
25-
on AWS NitroTPM where `mrAggregated` includes per-instance key-provider state.
26-
Use `kms.mrAggregated` when you want to pin an exact aggregate MR.
24+
identity value via `kms.mrAggregated` (the early/boot-mr-done aggregate MR,
25+
same pinning model as bare TDX). App compose hashes still use
26+
`apps.<appId>.composeHashes`.
2727

2828
```json
2929
{
3030
"osImages": ["0x0b327bcd642788b0517de3ff46d31ebd3847b6c64ea40bacde268bb9f1c8ec83"],
3131
"kms": {
32-
"composeHashes": ["0x<kms-compose-hash>"],
32+
"mrAggregated": ["0x<kms-early-mr-aggregated>"],
3333
"allowAnyDevice": true
3434
},
3535
"apps": {}
@@ -45,8 +45,7 @@ Add more fields as you deploy Gateway and apps:
4545
"allowedAdvisoryIds": [],
4646
"gatewayAppId": "0x...",
4747
"kms": {
48-
"composeHashes": ["0x..."],
49-
"mrAggregated": [],
48+
"mrAggregated": ["0x..."],
5049
"devices": [],
5150
"allowAnyDevice": true
5251
},
@@ -64,18 +63,16 @@ Add more fields as you deploy Gateway and apps:
6463

6564
AWS NitroTPM has no TDX/SNP-style TCB advisory surface, so the KMS normalizes a
6665
verified NitroTPM attestation to `tcbStatus: "UpToDate"` before calling
67-
auth-simple; no mode-specific TCB handling is needed here. Use the OS image hash
68-
plus KMS/app compose hashes for stable policy, since `mrAggregated` includes
69-
per-instance key-provider state. Use `mrAggregated` only when you want to pin one
70-
exact instance measurement.
66+
auth-simple. Pin KMS via early `mrAggregated` (boot-mr-done snapshot), same as
67+
bare TDX. Prefer a stable AMI/`os_image_hash` and empty TPM key-provider id so
68+
that early MR is precomputable.
7169

7270
```json
7371
{
7472
"osImages": ["0x<aws-os-image-hash>"],
7573
"allowedAdvisoryIds": [],
7674
"kms": {
77-
"composeHashes": ["0x<kms-compose-hash>"],
78-
"mrAggregated": [],
75+
"mrAggregated": ["0x<kms-early-mr-aggregated>"],
7976
"devices": [],
8077
"allowAnyDevice": true
8178
},
@@ -103,8 +100,7 @@ not verify raw NitroTPM evidence by itself.
103100
| `gatewayAppId` | No | Gateway app ID (add after Gateway deployment) |
104101
| `allowedTcbStatuses` | No | Allowed verifier-derived TCB status strings. Defaults to `["UpToDate"]`; non-up-to-date SNP/TDX statuses remain fail-closed unless explicitly allowlisted for testing. |
105102
| `allowedAdvisoryIds` | No | Advisory IDs permitted in `advisoryIds`. Defaults to `[]`, which rejects any advisory. |
106-
| `kms.composeHashes` | Required if `kms.mrAggregated` is empty | Allowed KMS compose hashes. Use this for stable KMS app identity. |
107-
| `kms.mrAggregated` | Required if `kms.composeHashes` is empty | Allowed KMS aggregate MR values. Use this for exact MR pinning. |
103+
| `kms.mrAggregated` | Yes | Allowed KMS early aggregate MR values (boot-mr-done). |
108104
| `kms.devices` | No | Allowed KMS device IDs |
109105
| `kms.allowAnyDevice` | No | If true, skip device ID check for KMS |
110106
| `apps.<appId>.composeHashes` | No | Allowed compose hashes for this app |
@@ -211,9 +207,7 @@ KMS boot authorization.
211207
2. Every `advisoryIds` entry must be listed in `allowedAdvisoryIds` (default: none allowed)
212208
3. `osImageHash` must be in `osImages` array
213209
4. At least one KMS identity allowlist must be configured:
214-
- `kms.composeHashes`, for stable KMS app identity such as AWS NitroTPM where `mrAggregated` can include per-instance key-provider state
215-
- `kms.mrAggregated`, for exact aggregate-MR authorization
216-
5. Every configured KMS identity allowlist must match. If both `kms.composeHashes` and `kms.mrAggregated` are non-empty, both are enforced.
210+
- `kms.mrAggregated` early/boot-mr-done aggregate MR (required)
217211
6. `deviceId` must be in `kms.devices` unless `allowAnyDevice` is true
218212

219213
### App Boot Validation

dstack/kms/auth-simple/auth-config.example.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@
33
"0x0b327bcd642788b0517de3ff46d31ebd3847b6c64ea40bacde268bb9f1c8ec83"
44
],
55
"kms": {
6-
"composeHashes": [
7-
"0x<kms-compose-hash>"
8-
],
96
"mrAggregated": [
10-
"0x<kms-mr-aggregated>"
7+
"0x<kms-early-mr-aggregated-boot-mr-done>"
118
],
129
"devices": [],
1310
"allowAnyDevice": true

dstack/kms/auth-simple/index.test.ts

Lines changed: 1 addition & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -238,62 +238,9 @@ describe('auth-simple', () => {
238238
const json = await res.json();
239239

240240
expect(json.isAllowed).toBe(false);
241-
expect(json.reason).toContain('identity');
242-
});
243-
244-
it('allows KMS boot with a stable compose hash identity', async () => {
245-
writeTestConfig({
246-
gatewayAppId: '0xgateway',
247-
osImages: ['0x1fbb0cf9cc6cfbf23d6b779776fabad2c5403d643badb9e5e238615e4960a78a'],
248-
kms: {
249-
composeHashes: ['0xcompose456'],
250-
allowAnyDevice: true
251-
}
252-
});
253-
254-
const res = await app.fetch(new Request('http://localhost/bootAuth/kms', {
255-
method: 'POST',
256-
headers: { 'Content-Type': 'application/json' },
257-
body: JSON.stringify({ ...baseBootInfo, mrAggregated: '0xinstanceboundmr' })
258-
}));
259-
const json = await res.json();
260-
261-
expect(json.isAllowed).toBe(true);
262-
expect(json.reason).toBe('');
241+
expect(json.reason).toContain('MR');
263242
});
264243

265-
it('requires all configured KMS identity allowlists to match', async () => {
266-
writeTestConfig({
267-
gatewayAppId: '0xgateway',
268-
osImages: ['0x1fbb0cf9cc6cfbf23d6b779776fabad2c5403d643badb9e5e238615e4960a78a'],
269-
kms: {
270-
composeHashes: ['0xcompose456'],
271-
mrAggregated: ['0xabc123'],
272-
allowAnyDevice: true
273-
}
274-
});
275-
276-
const wrongCompose = await app.fetch(new Request('http://localhost/bootAuth/kms', {
277-
method: 'POST',
278-
headers: { 'Content-Type': 'application/json' },
279-
body: JSON.stringify({ ...baseBootInfo, composeHash: '0xwrong' })
280-
}));
281-
expect((await wrongCompose.json()).isAllowed).toBe(false);
282-
283-
const wrongMr = await app.fetch(new Request('http://localhost/bootAuth/kms', {
284-
method: 'POST',
285-
headers: { 'Content-Type': 'application/json' },
286-
body: JSON.stringify({ ...baseBootInfo, mrAggregated: '0xwrong' })
287-
}));
288-
expect((await wrongMr.json()).isAllowed).toBe(false);
289-
290-
const matching = await app.fetch(new Request('http://localhost/bootAuth/kms', {
291-
method: 'POST',
292-
headers: { 'Content-Type': 'application/json' },
293-
body: JSON.stringify(baseBootInfo)
294-
}));
295-
expect((await matching.json()).isAllowed).toBe(true);
296-
});
297244

298245
it('allows KMS boot with allowAnyDevice', async () => {
299246
writeTestConfig({

dstack/kms/auth-simple/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ const AppConfigSchema = z.object({
3636

3737
const KmsConfigSchema = z.object({
3838
mrAggregated: z.array(z.string()).default([]),
39-
composeHashes: z.array(z.string()).default([]),
4039
devices: z.array(z.string()).default([]),
4140
allowAnyDevice: z.boolean().default(false)
4241
});

os/yocto/layers/meta-dstack/recipes-core/images/dstack-uki.bb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ INITRAMFS_FSTYPES = "cpio.gz"
1818
# Kernel settings
1919
KERNEL_IMAGETYPE = "bzImage"
2020

21-
# Base kernel cmdline (verity hash added dynamically)
21+
# Base kernel cmdline (verity hash added dynamically).
22+
# Do NOT embed per-app dstack.mr_config_id here: AWS app/config binding is
23+
# measured from the shared-disk MrConfigV3 into NitroTPM PCR8 at guest setup,
24+
# so the UKI/AMI stays app-independent (PCR4 is OS-only).
2225
UKI_CMDLINE_BASE = "console=ttyS0 init=/init panic=1 net.ifnames=0 biosdevname=0 \
2326
mce=off oops=panic pci=noearly pci=nommconf random.trust_cpu=y random.trust_bootloader=n \
2427
tsc=reliable no-kvmclock"

os/yocto/tools/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ the guest-OS backend interface; the supported build entrypoint is
1010
- `aws/` contains AWS EC2 NitroTPM image validation, AMI promotion, live-smoke,
1111
and release-evidence helpers. These scripts are Yocto OS artifact consumers,
1212
so they stay with the backend rather than in the repository-wide `tools/`.
13+
App/config binding for AWS is **not** baked into the UKI cmdline; release
14+
packages should ship a shared-disk MrConfigV3 (measured into PCR8 at boot)
15+
and optional `aws_measurement` for unified `os_image_hash`.
1316

1417
The legacy cross-cutting helpers live under [`../../../tools/`](../../../tools/)
1518
instead of inside this backend.

sdk/curl/api.md

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -118,22 +118,14 @@ Generates a quote with given plain report data. For platform-agnostic verificati
118118
| Field | Type | Description | Example |
119119
|-------|------|-------------|----------|
120120
| `report_data` | string | Report data of max length 64 bytes. Padding with 0s if less than 64 bytes. | `"1234deadbeaf"` |
121-
| `attestation_options.aws_nitro_tpm.nonce` | string | Optional AWS EC2 NitroTPM attestation document nonce, hex-encoded. Only valid on `dstack-aws-nitro-tpm`. | `"001122"` |
122-
| `attestation_options.aws_nitro_tpm.public_key` | string | Optional AWS EC2 NitroTPM attestation document public key, hex-encoded. Only valid on `dstack-aws-nitro-tpm`. | `"deadbeef"` |
123121

124122
**Example:**
125123
```bash
126124
curl --unix-socket /var/run/dstack.sock -X POST \
127125
http://dstack/GetQuote \
128126
-H 'Content-Type: application/json' \
129127
-d '{
130-
"report_data": "1234deadbeaf",
131-
"attestation_options": {
132-
"aws_nitro_tpm": {
133-
"nonce": "001122",
134-
"public_key": "deadbeef"
135-
}
136-
}
128+
"report_data": "1234deadbeaf"
137129
}'
138130
```
139131
Or
@@ -272,22 +264,14 @@ You can submit the returned `attestation` directly to the verifier `/verify` end
272264
| Field | Type | Description | Example |
273265
|-------|------|-------------|----------|
274266
| `report_data` | string | Report data of max length 64 bytes. Padding with 0s if less than 64 bytes. | `"1234deadbeaf"` |
275-
| `attestation_options.aws_nitro_tpm.nonce` | string | Optional AWS EC2 NitroTPM attestation document nonce, hex-encoded. Only valid on `dstack-aws-nitro-tpm`. | `"001122"` |
276-
| `attestation_options.aws_nitro_tpm.public_key` | string | Optional AWS EC2 NitroTPM attestation document public key, hex-encoded. Only valid on `dstack-aws-nitro-tpm`. | `"deadbeef"` |
277267

278268
**Example:**
279269
```bash
280270
curl --unix-socket /var/run/dstack.sock -X POST \
281271
http://dstack/Attest \
282272
-H 'Content-Type: application/json' \
283273
-d '{
284-
"report_data": "1234deadbeaf",
285-
"attestation_options": {
286-
"aws_nitro_tpm": {
287-
"nonce": "001122",
288-
"public_key": "deadbeef"
289-
}
290-
}
274+
"report_data": "1234deadbeaf"
291275
}'
292276
```
293277
Or

0 commit comments

Comments
 (0)