Skip to content

Commit 2f609a3

Browse files
committed
refactor(attestation): remove AWS review churn
Restore the established string-error measurement API while using or_panic for invariant-only CBOR encoding. Consolidate TCB and key-release policy helpers, move AWS image-binding tests to the verifier, and remove redundant tests, dead accessors, and unrelated TPM/NSM diff noise.
1 parent f301ebf commit 2f609a3

26 files changed

Lines changed: 271 additions & 564 deletions

File tree

dstack/Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dstack/dstack-attest/src/attestation.rs

Lines changed: 18 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -489,20 +489,6 @@ impl DstackVerifiedReport {
489489
| DstackVerifiedReport::DstackAwsNitroTpm(_) => None,
490490
}
491491
}
492-
493-
pub fn aws_nitro_tpm_public_key(&self) -> Option<&[u8]> {
494-
match self {
495-
DstackVerifiedReport::DstackAwsNitroTpm(report) => report.public_key.as_deref(),
496-
_ => None,
497-
}
498-
}
499-
500-
pub fn aws_nitro_tpm_nonce(&self) -> Option<&[u8]> {
501-
match self {
502-
DstackVerifiedReport::DstackAwsNitroTpm(report) => report.nonce.as_deref(),
503-
_ => None,
504-
}
505-
}
506492
}
507493

508494
/// Represents a verified attestation
@@ -1111,27 +1097,20 @@ impl DstackNitroQuote {
11111097
const AWS_NITRO_TPM_BOOT_PCRS: &[u16] = &[4, 7, 12];
11121098
/// All dstack measured events (TDX RTMR3 analogue). Non-resettable on NitroTPM.
11131099
const AWS_NITRO_TPM_EVENT_PCR: u16 = 14;
1114-
/// Optional config commitment PCR (mr_config-like). Extended from shared-disk config only.
1115-
pub const AWS_NITRO_TPM_CONFIG_PCR: u16 = 8;
1100+
/// Optional config commitment PCR, extended once from the guest-computed MrConfig V2 id.
1101+
pub(crate) const AWS_NITRO_TPM_CONFIG_PCR: u16 = 8;
11161102

11171103
fn aws_nitro_tpm_pcr(pcrs: &std::collections::BTreeMap<u16, Vec<u8>>, index: u16) -> Result<&[u8]> {
11181104
pcrs.get(&index)
11191105
.map(Vec::as_slice)
11201106
.with_context(|| format!("PCR {index} not found"))
11211107
}
11221108

1123-
/// Replay boundary for AWS event PCR, aligned with bare TDX RTMR3:
1124-
/// - `boottime_mr`: stop at `boot-mr-done` (early / KMS pin snapshot)
1125-
/// - otherwise: replay the full event log present in the quote
1126-
fn aws_nitro_tpm_event_boundary(boottime_mr: bool) -> Option<&'static str> {
1127-
boottime_mr.then_some("boot-mr-done")
1128-
}
1129-
11301109
fn aws_nitro_tpm_replayed_event_pcr(
11311110
runtime_events: &[RuntimeEvent],
11321111
boottime_mr: bool,
11331112
) -> <Sha384 as Hasher>::Output {
1134-
replay_runtime_events::<Sha384>(runtime_events, aws_nitro_tpm_event_boundary(boottime_mr))
1113+
replay_runtime_events::<Sha384>(runtime_events, boottime_mr.then_some("boot-mr-done"))
11351114
}
11361115

11371116
/// Bind the event log to the quoted PCR14 register.
@@ -1180,7 +1159,7 @@ pub fn aws_nitro_tpm_boot_pcr_digest(
11801159
}
11811160

11821161
impl DstackAwsNitroTpmQuote {
1183-
pub fn decode_pcrs(&self) -> Result<std::collections::BTreeMap<u16, Vec<u8>>> {
1162+
pub(crate) fn decode_pcrs(&self) -> Result<std::collections::BTreeMap<u16, Vec<u8>>> {
11841163
let cose = nsm_qvl::CoseSign1::from_bytes(&self.attestation_doc)
11851164
.context("failed to decode NitroTPM COSE document")?;
11861165
let doc = nsm_qvl::AttestationDocument::from_cbor(&cose.payload)
@@ -2036,12 +2015,11 @@ impl Attestation {
20362015
AttestationMode::DstackNitroEnclave => String::new(),
20372016
};
20382017
let runtime_events = match mode {
2039-
AttestationMode::DstackTdx | AttestationMode::DstackGcpTdx => {
2018+
AttestationMode::DstackTdx
2019+
| AttestationMode::DstackGcpTdx
2020+
| AttestationMode::DstackAwsNitroTpm => {
20402021
RuntimeEvent::read_all().context("Failed to read runtime events")?
20412022
}
2042-
AttestationMode::DstackAwsNitroTpm => {
2043-
RuntimeEvent::read_all().context("failed to read runtime events")?
2044-
}
20452023
AttestationMode::DstackAmdSevSnp => vec![],
20462024
AttestationMode::DstackNitroEnclave => match app_id {
20472025
Some(app_id) => vec![RuntimeEvent::new("app-id".to_string(), app_id.to_vec())],
@@ -2085,12 +2063,8 @@ impl Attestation {
20852063
AttestationMode::DstackAwsNitroTpm => {
20862064
// Challenge binding is report_data → NitroTPM user_data only
20872065
// (same role as TDX/GCP report_data; no separate nonce/public_key).
2088-
let attestation_doc = crate::aws_nitro_tpm::attestation_document(
2089-
Some(report_data.to_vec()),
2090-
None,
2091-
None,
2092-
)
2093-
.context("failed to get NitroTPM attestation document")?;
2066+
let attestation_doc = crate::aws_nitro_tpm::attestation_document(report_data)
2067+
.context("failed to get NitroTPM attestation document")?;
20942068
AttestationQuote::DstackAwsNitroTpm(DstackAwsNitroTpmQuote { attestation_doc })
20952069
}
20962070
};
@@ -2123,9 +2097,11 @@ impl Attestation {
21232097
.context("vm_config.aws_measurement is required on AWS NitroTPM")?;
21242098
document
21252099
.verify(&vm_config.os_image_hash)
2100+
.map_err(anyhow::Error::msg)
21262101
.context("aws_measurement does not match os_image_hash")?;
21272102
let measurement = document
21282103
.decode_measurement()
2104+
.map_err(anyhow::Error::msg)
21292105
.context("failed to decode aws_measurement")?;
21302106
let quoted_digest = aws_nitro_tpm_boot_pcr_digest(&pcrs)
21312107
.context("failed to compute boot_pcr_digest from attestation")?;
@@ -2699,6 +2675,13 @@ mod tests {
26992675
assert_eq!(mrs.mr_system, changed_mrs.mr_system);
27002676
assert_ne!(mrs.mr_aggregated, changed_mrs.mr_aggregated);
27012677

2678+
let mut changed_pcrs = pcrs.clone();
2679+
changed_pcrs.insert(12, vec![0x99; 48]);
2680+
let changed_pcr12 =
2681+
decode_mr_aws_nitro_tpm_from_pcrs(false, &mr_key_provider, &changed_pcrs, &events)?;
2682+
assert_ne!(mrs.mr_system, changed_pcr12.mr_system);
2683+
assert_ne!(mrs.mr_aggregated, changed_pcr12.mr_aggregated);
2684+
27022685
let mut missing_pcrs = pcrs.clone();
27032686
missing_pcrs.remove(&AWS_NITRO_TPM_EVENT_PCR);
27042687
let err = match decode_mr_aws_nitro_tpm_from_pcrs(
@@ -2727,89 +2710,6 @@ mod tests {
27272710
Ok(())
27282711
}
27292712

2730-
#[test]
2731-
fn aws_nitro_tpm_mr_aggregated_binds_pcr12() -> Result<()> {
2732-
let pcr4 = vec![0x04; 48];
2733-
let pcr7 = vec![0x07; 48];
2734-
let pcr12 = vec![0x12; 48];
2735-
let events = vec![
2736-
RuntimeEvent::new("app-id".into(), vec![0x11; 20]),
2737-
RuntimeEvent::new("compose-hash".into(), vec![0x22; 32]),
2738-
RuntimeEvent::new("instance-id".into(), vec![0x33; 20]),
2739-
RuntimeEvent::new("key-provider".into(), b"tpm".to_vec()),
2740-
];
2741-
let replayed_pcr14 = cc_eventlog::replay_events::<Sha384>(&events, None);
2742-
let mr_key_provider = sha256(b"aws nitrotpm key provider");
2743-
2744-
let mut pcrs = std::collections::BTreeMap::from([
2745-
(4u16, pcr4.clone()),
2746-
(7u16, pcr7.clone()),
2747-
(12u16, pcr12.clone()),
2748-
(AWS_NITRO_TPM_EVENT_PCR, replayed_pcr14.to_vec()),
2749-
]);
2750-
let base = decode_mr_aws_nitro_tpm_from_pcrs(false, &mr_key_provider, &pcrs, &events)?;
2751-
2752-
pcrs.insert(12u16, vec![0x99; 48]);
2753-
let changed_pcr12 =
2754-
decode_mr_aws_nitro_tpm_from_pcrs(false, &mr_key_provider, &pcrs, &events)?;
2755-
assert_ne!(base.mr_system, changed_pcr12.mr_system);
2756-
assert_ne!(base.mr_aggregated, changed_pcr12.mr_aggregated);
2757-
2758-
pcrs.remove(&12u16);
2759-
let err = match decode_mr_aws_nitro_tpm_from_pcrs(false, &mr_key_provider, &pcrs, &events) {
2760-
Ok(_) => panic!("missing PCR12 must be rejected"),
2761-
Err(err) => err,
2762-
};
2763-
assert!(
2764-
format!("{err:#}").contains("PCR 12 not found"),
2765-
"unexpected error: {err:#}"
2766-
);
2767-
Ok(())
2768-
}
2769-
2770-
#[test]
2771-
fn aws_nitro_tpm_rejects_reordered_or_replayed_runtime_events() -> Result<()> {
2772-
let events = vec![
2773-
RuntimeEvent::new("app-id".into(), vec![0x11; 20]),
2774-
RuntimeEvent::new("compose-hash".into(), vec![0x22; 32]),
2775-
RuntimeEvent::new("instance-id".into(), vec![0x33; 20]),
2776-
RuntimeEvent::new("key-provider".into(), b"tpm".to_vec()),
2777-
];
2778-
let quoted_pcr14 = cc_eventlog::replay_events::<Sha384>(&events, None);
2779-
let mr_key_provider = sha256(b"aws nitrotpm key provider");
2780-
let pcrs = std::collections::BTreeMap::from([
2781-
(4u16, vec![0x04; 48]),
2782-
(7u16, vec![0x07; 48]),
2783-
(12u16, vec![0x12; 48]),
2784-
(AWS_NITRO_TPM_EVENT_PCR, quoted_pcr14.to_vec()),
2785-
]);
2786-
2787-
let mut reordered = events.clone();
2788-
reordered.swap(1, 2);
2789-
let err =
2790-
match decode_mr_aws_nitro_tpm_from_pcrs(false, &mr_key_provider, &pcrs, &reordered) {
2791-
Ok(_) => panic!("runtime event reordering must be rejected"),
2792-
Err(err) => err,
2793-
};
2794-
assert!(
2795-
format!("{err:#}").contains("PCR14 mismatch"),
2796-
"unexpected error: {err:#}"
2797-
);
2798-
2799-
let mut replayed = events.clone();
2800-
replayed.push(RuntimeEvent::new("compose-hash".into(), vec![0x22; 32]));
2801-
let err = match decode_mr_aws_nitro_tpm_from_pcrs(false, &mr_key_provider, &pcrs, &replayed)
2802-
{
2803-
Ok(_) => panic!("runtime event replay/duplication must be rejected"),
2804-
Err(err) => err,
2805-
};
2806-
assert!(
2807-
format!("{err:#}").contains("PCR14 mismatch"),
2808-
"unexpected error: {err:#}"
2809-
);
2810-
Ok(())
2811-
}
2812-
28132713
#[test]
28142714
fn aws_nitro_tpm_pcr14_replays_full_event_log_like_rtmr3() -> Result<()> {
28152715
// Single PCR14 lane: all events (including after system-ready) are

dstack/dstack-attest/src/aws_nitro_tpm.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,11 @@ const TPMA_SESSION_CONTINUE_SESSION: u8 = 1 << 0;
2828

2929
type HmacSha512 = Hmac<Sha512>;
3030

31-
pub(crate) fn attestation_document(
32-
user_data: Option<Vec<u8>>,
33-
nonce: Option<Vec<u8>>,
34-
public_key: Option<Vec<u8>>,
35-
) -> Result<Vec<u8>> {
31+
pub(crate) fn attestation_document(report_data: &[u8]) -> Result<Vec<u8>> {
3632
let request = nsm_api::Request::Attestation {
37-
user_data: user_data.map(Into::into),
38-
nonce: nonce.map(Into::into),
39-
public_key: public_key.map(Into::into),
33+
user_data: Some(report_data.to_vec().into()),
34+
nonce: None,
35+
public_key: None,
4036
};
4137

4238
let device_path = tpm_device_path();

dstack/dstack-attest/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ pub fn measure_aws_config_pcr(config_id: &[u8; 48]) -> anyhow::Result<()> {
110110
/// The config id is extended as-is (it is already 48 bytes, the SHA384 bank
111111
/// digest size), so a verifier that recovers the claimed `config_id` can parse
112112
/// its version byte and recompute this value directly.
113-
pub fn expected_aws_config_pcr(config_id: &[u8; 48]) -> [u8; 48] {
113+
fn expected_aws_config_pcr(config_id: &[u8; 48]) -> [u8; 48] {
114114
use sha2::{Digest, Sha384};
115115
let mut material = [0u8; 96];
116116
material[48..].copy_from_slice(config_id);

dstack/dstack-attest/src/v1.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,6 @@ impl PlatformEvidence {
119119
}
120120
}
121121

122-
pub fn aws_nitro_tpm_attestation_doc(&self) -> Option<&[u8]> {
123-
match self {
124-
Self::AwsNitroTpm { attestation_doc } => Some(attestation_doc.as_slice()),
125-
_ => None,
126-
}
127-
}
128-
129122
pub fn sev_snp_report(&self) -> Option<&[u8]> {
130123
match self {
131124
Self::SevSnp { report, .. } => Some(report.as_slice()),

dstack/dstack-attest/tests/sev_snp_verify.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,7 @@ fn upgrade_snp_config_for_split_measurement(config: &str) -> String {
148148
serde_json::from_value(measurement_value).expect("legacy SNP measurement input");
149149
let measurement = sev_os_image_measurement_from_input(&input)
150150
.expect("image measurement")
151-
.to_cbor_vec()
152-
.expect("image measurement cbor");
151+
.to_cbor_vec();
153152
let sha256sum = format!(
154153
"{} {}\n",
155154
hex::encode(Sha256::digest(&measurement)),
@@ -200,7 +199,7 @@ fn with_image_measurement(
200199
let mut image = dstack_types::SevOsImageMeasurement::from_cbor_slice(&document.measurement)
201200
.expect("decode measurement.snp.cbor");
202201
f(&mut image);
203-
document.measurement = image.to_cbor_vec().expect("image measurement cbor");
202+
document.measurement = image.to_cbor_vec();
204203
document.checksum_file = format!(
205204
"{} {}\n",
206205
hex::encode(Sha256::digest(&document.measurement)),

dstack/dstack-mr/src/main.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ fn main() -> Result<()> {
7575
hex::decode(hash.trim()).context("GCP UKI Authenticode hash is not valid hex")?;
7676
let cbor = dstack_types::GcpOsImageMeasurement::new(hash)
7777
.map_err(anyhow::Error::msg)?
78-
.to_cbor_vec()?;
78+
.to_cbor_vec();
7979
std::io::stdout()
8080
.write_all(&cbor)
8181
.context("failed to write GCP measurement CBOR")?;
@@ -128,11 +128,15 @@ fn main() -> Result<()> {
128128
fn inspect_measurement(kind: &str, path: &Path) -> Result<Value> {
129129
let cbor = fs_err::read(path).with_context(|| format!("failed to read {}", path.display()))?;
130130
match kind {
131-
"tdx" => dstack_types::TdxOsImageMeasurement::cbor_json_value_from_slice(&cbor),
132-
"snp" | "sev" => dstack_types::SevOsImageMeasurement::cbor_json_value_from_slice(&cbor),
133-
"gcp" => dstack_types::GcpOsImageMeasurement::cbor_json_value_from_slice(&cbor),
131+
"tdx" => dstack_types::TdxOsImageMeasurement::cbor_json_value_from_slice(&cbor)
132+
.map_err(anyhow::Error::msg),
133+
"snp" | "sev" => dstack_types::SevOsImageMeasurement::cbor_json_value_from_slice(&cbor)
134+
.map_err(anyhow::Error::msg),
135+
"gcp" => dstack_types::GcpOsImageMeasurement::cbor_json_value_from_slice(&cbor)
136+
.map_err(anyhow::Error::msg),
134137
"aws" => {
135-
let measurement = dstack_types::AwsOsImageMeasurement::from_cbor_slice(&cbor)?;
138+
let measurement = dstack_types::AwsOsImageMeasurement::from_cbor_slice(&cbor)
139+
.map_err(anyhow::Error::msg)?;
136140
serde_json::to_value(measurement).context("failed to convert AWS measurement to JSON")
137141
}
138142
other => bail!("unknown measurement kind {other:?}; expected tdx, snp, gcp, or aws"),
@@ -161,7 +165,7 @@ fn aws_measurement_cbor(pcr4_hex: &str, pcr7_hex: &str, pcr12_hex: &str) -> Resu
161165
&decode_sha384_pcr_hex("pcr12", pcr12_hex)?,
162166
)
163167
.map_err(anyhow::Error::msg)?;
164-
measurement.to_cbor_vec()
168+
Ok(measurement.to_cbor_vec())
165169
}
166170

167171
fn read_hex_file(path: &str) -> Result<String> {

dstack/dstack-mr/src/sev.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -890,12 +890,12 @@ pub fn sev_os_image_measurement_for_image_dir(
890890

891891
/// Compute the AMD SEV-SNP measurement-material hash from an OS image directory.
892892
pub fn sev_measurement_hash_for_image_dir(image_dir: &Path) -> Result<[u8; 32]> {
893-
sev_os_image_measurement_for_image_dir(image_dir)?.measurement_hash()
893+
Ok(sev_os_image_measurement_for_image_dir(image_dir)?.measurement_hash())
894894
}
895895

896896
/// Generate the raw `measurement.snp.cbor` bytes for an image directory.
897897
pub fn sev_os_image_measurement_cbor_for_image_dir(image_dir: &Path) -> Result<Vec<u8>> {
898-
sev_os_image_measurement_for_image_dir(image_dir)?.to_cbor_vec()
898+
Ok(sev_os_image_measurement_for_image_dir(image_dir)?.to_cbor_vec())
899899
}
900900

901901
/// `sha256(MEASUREMENT || HOST_DATA)` — the SNP aggregated identity digest.
@@ -966,6 +966,7 @@ pub fn measurement_input_from_snp_document(
966966
document: &SnpMeasurementDocument,
967967
) -> Result<MeasurementInput> {
968968
let image = dstack_types::SevOsImageMeasurement::from_cbor_slice(&document.measurement)
969+
.map_err(anyhow::Error::msg)
969970
.context("invalid measurement.snp.cbor")?;
970971
Ok(MeasurementInput {
971972
base_cmdline: image.base_cmdline,
@@ -1043,6 +1044,7 @@ pub fn parse_snp_inputs_from_vm_config(vm_config: &str) -> Result<SnpLaunchInput
10431044
document.measurement.clone(),
10441045
)
10451046
.verify(&os_image_hash)
1047+
.map_err(anyhow::Error::msg)
10461048
.context("amd sev-snp measurement material does not match os_image_hash")?;
10471049
let input = measurement_input_from_snp_document(&document)?;
10481050
validate_measurement_input(&input)?;
@@ -1232,8 +1234,7 @@ mod tests {
12321234
fn snp_document(input: &MeasurementInput) -> SnpMeasurementDocument {
12331235
let measurement = sev_os_image_measurement_from_input(input)
12341236
.expect("image measurement")
1235-
.to_cbor_vec()
1236-
.expect("image measurement cbor");
1237+
.to_cbor_vec();
12371238
let sha256sum = format!(
12381239
"{} {}\n",
12391240
hex::encode(Sha256::digest(&measurement)),

dstack/dstack-mr/src/tdx.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,12 +324,12 @@ pub fn tdx_os_image_measurement_for_image_dir(image_dir: &Path) -> Result<TdxOsI
324324

325325
/// Generate the raw `measurement.tdx.cbor` bytes for an image directory.
326326
pub fn tdx_os_image_measurement_cbor_for_image_dir(image_dir: &Path) -> Result<Vec<u8>> {
327-
tdx_os_image_measurement_for_image_dir(image_dir)?.to_cbor_vec()
327+
Ok(tdx_os_image_measurement_for_image_dir(image_dir)?.to_cbor_vec())
328328
}
329329

330330
/// Compute the TDX static measurement-material hash for an image directory.
331331
pub fn tdx_measurement_hash_for_image_dir(image_dir: &Path) -> Result<[u8; 32]> {
332-
tdx_os_image_measurement_for_image_dir(image_dir)?.measurement_hash()
332+
Ok(tdx_os_image_measurement_for_image_dir(image_dir)?.measurement_hash())
333333
}
334334

335335
/// Compute expected TDX measurements from self-contained TDX measurement
@@ -358,6 +358,7 @@ pub fn tdx_measurements_from_measurement_document(
358358

359359
let measurement = document
360360
.decode_measurement()
361+
.map_err(anyhow::Error::msg)
361362
.context("failed to decode TDX measurement CBOR")?;
362363
let mrtd = select_mrtd(&measurement, vm_config)?;
363364

dstack/dstack-types/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ edition.workspace = true
1010
license.workspace = true
1111

1212
[dependencies]
13-
anyhow.workspace = true
1413
ciborium.workspace = true
1514
hex = { workspace = true, features = ["std"] }
1615
or-panic.workspace = true

0 commit comments

Comments
 (0)