From eb782590f968ec9b96595bae9979d3e493ccdc56 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sat, 19 Sep 2026 21:41:23 -0700 Subject: [PATCH 1/2] fix(kms): hash the sev-snp chip_id into device_id on the key-release path --- dstack/kms/src/main_service.rs | 10 ++++-- dstack/kms/src/main_service/amd_attest.rs | 21 ++++++++----- dstack/kms/src/onboard_service.rs | 37 +++++++++++++++++++++++ 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/dstack/kms/src/main_service.rs b/dstack/kms/src/main_service.rs index d4bad106a..739e2e55a 100644 --- a/dstack/kms/src/main_service.rs +++ b/dstack/kms/src/main_service.rs @@ -1101,7 +1101,10 @@ mod tests { assert_eq!(boot_info.tee_variant, TeeVariant::DstackAmdSevSnp); assert_eq!(boot_info.mr_aggregated.len(), 32); - assert_eq!(boot_info.device_id, vec![0xab; 64]); + assert_eq!( + boot_info.device_id, + sha2::Sha256::digest([0xab; 64]).to_vec() + ); assert_eq!(boot_info.app_id, vec![0x11; 20]); } @@ -1137,7 +1140,10 @@ mod tests { let boot_info = build_boot_info_for_attestation(&attestation, false, &vm_config) .expect("self-contained SNP vm_config should not require KMS-local sev_snp config"); assert_eq!(boot_info.tee_variant, TeeVariant::DstackAmdSevSnp); - assert_eq!(boot_info.device_id, vec![0xab; 64]); + assert_eq!( + boot_info.device_id, + sha2::Sha256::digest([0xab; 64]).to_vec() + ); } fn snp_boot_info() -> BootInfo { diff --git a/dstack/kms/src/main_service/amd_attest.rs b/dstack/kms/src/main_service/amd_attest.rs index 569f964c5..10b6517f4 100644 --- a/dstack/kms/src/main_service/amd_attest.rs +++ b/dstack/kms/src/main_service/amd_attest.rs @@ -60,8 +60,10 @@ pub(crate) fn validate_amd_snp_measurement_binding( /// /// This helper first recomputes and validates the QEMU SNP launch measurement. /// `mr_system` is `sha256(MEASUREMENT)`, `mr_aggregated` is -/// `sha256(MEASUREMENT || HOST_DATA)`, and `device_id` is the -/// hardware-verified 64-byte SNP `chip_id`. `app_id`, `compose_hash`, +/// `sha256(MEASUREMENT || HOST_DATA)`, and `device_id` is +/// `sha256(chip_id)` over the hardware-verified 64-byte SNP `chip_id` — the +/// same 32-byte form `GetAttestationInfo` reports and operators register, so +/// device allowlists compare equal. `app_id`, `compose_hash`, /// `instance_id`, and key provider identity come from the MrConfigV3 document /// bound by HOST_DATA. /// @@ -116,7 +118,7 @@ fn build_amd_snp_boot_info_with_tcb_status( app_id: mr_config.app_id.clone().unwrap_or_default(), compose_hash: mr_config.compose_hash.clone(), instance_id: mr_config.instance_id.clone().unwrap_or_default(), - device_id: verified_chip_id.to_vec(), + device_id: Sha256::digest(verified_chip_id).to_vec(), key_provider_info, tcb_status: tcb_status.to_string(), advisory_ids: advisory_ids.to_vec(), @@ -368,7 +370,7 @@ mod tests { .expect("matching measurement should build snp boot info"); assert_eq!(boot_info.tee_variant, TeeVariant::DstackAmdSevSnp); assert_eq!(boot_info.mr_aggregated.len(), 32); - assert_eq!(boot_info.device_id, chip_id.to_vec()); + assert_eq!(boot_info.device_id, Sha256::digest(chip_id).to_vec()); assert_eq!(boot_info.app_id, vec![0x11; 20]); assert_eq!(boot_info.compose_hash, vec![0x22; 32]); assert_eq!(boot_info.os_image_hash, test_os_image_hash(&input).unwrap()); @@ -404,7 +406,7 @@ mod tests { .expect("verified snp attestation should feed boot info helper"); assert_eq!(boot_info.mr_aggregated.len(), 32); - assert_eq!(boot_info.device_id, chip_id.to_vec()); + assert_eq!(boot_info.device_id, Sha256::digest(chip_id).to_vec()); assert_eq!(boot_info.app_id, vec![0x11; 20]); assert_eq!(boot_info.tcb_status, "UpToDate"); Ok(()) @@ -486,7 +488,7 @@ mod tests { .expect("vm_config-carried snp measurement inputs should build boot info"); assert_eq!(boot_info.mr_aggregated.len(), 32); - assert_eq!(boot_info.device_id, chip_id.to_vec()); + assert_eq!(boot_info.device_id, Sha256::digest(chip_id).to_vec()); assert_eq!(boot_info.app_id, vec![0x11; 20]); Ok(()) } @@ -715,8 +717,11 @@ mod tests { let boot_info = build_amd_snp_boot_info(&verified, &[0x01; 64], &input).unwrap(); let changed_boot_info = build_amd_snp_boot_info(&verified, &[0x02; 64], &input).unwrap(); - assert_eq!(boot_info.device_id, vec![0x01; 64]); - assert_eq!(changed_boot_info.device_id, vec![0x02; 64]); + assert_eq!(boot_info.device_id, Sha256::digest([0x01; 64]).to_vec()); + assert_eq!( + changed_boot_info.device_id, + Sha256::digest([0x02; 64]).to_vec() + ); assert_ne!(boot_info.device_id, changed_boot_info.device_id); assert_eq!(boot_info.instance_id, changed_boot_info.instance_id); assert_eq!( diff --git a/dstack/kms/src/onboard_service.rs b/dstack/kms/src/onboard_service.rs index 3bcc42f10..6f59b85b2 100644 --- a/dstack/kms/src/onboard_service.rs +++ b/dstack/kms/src/onboard_service.rs @@ -396,6 +396,43 @@ mod tests { assert_eq!(response.kms_contract_address, "0x1234"); } + /// The device id an operator registers is the one `GetAttestationInfo` + /// reports, so the id `GetAppKey` presents for authorization has to be the + /// same bytes — otherwise an SNP device allowlist can never match. + #[test] + fn snp_attestation_info_device_id_matches_the_authorization_device_id() { + let input = valid_snp_measurement_input(); + let measurement = compute_expected_measurement(&input).unwrap(); + let mr_config = valid_snp_mr_config(); + let attestation = verified_snp_attestation(measurement, [0xab; 64]); + let snp_document = snp_measurement_document(&input); + let os_image_hash = dstack_types::image_hash_from_sha256sum(&snp_document.checksum_file); + let vm_config = serde_json::json!({ + "os_image_hash": hex::encode(os_image_hash), + "sev_snp_measurement": serde_json::to_string(&snp_document).unwrap(), + "mr_config": mr_config.to_canonical_json(), + }) + .to_string(); + + let reported = build_attestation_info_response( + &attestation, + "dstack-amd-sev-snp".to_string(), + &vm_config, + "test-site".to_string(), + "https://rpc.example".to_string(), + "0x1234".to_string(), + ) + .expect("snp attestation info should be derived from snp boot info"); + let presented = build_boot_info_for_attestation(&attestation, false, &vm_config) + .expect("snp attestation should build boot info"); + + assert_eq!( + reported.device_id, presented.device_id, + "registered and presented SNP device ids must be identical" + ); + assert_eq!(presented.device_id.len(), 32, "device id must fit bytes32"); + } + #[test] fn onboarding_domain_accepts_dns_name() { validate_onboarding_domain("kms.example.com").unwrap(); From 3e7930fd65c473b2dfec7371ef4f95ded2e08715 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 23 Sep 2026 21:17:03 -0700 Subject: [PATCH 2/2] refactor(kms): report the boot-info device_id from GetAttestationInfo Reuse boot_info.device_id instead of re-hashing the raw device id, so the id operators register and the id key release presents come from a single derivation. This makes the dedicated parity test redundant. --- dstack/kms/src/onboard_service.rs | 43 ++----------------------------- 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/dstack/kms/src/onboard_service.rs b/dstack/kms/src/onboard_service.rs index 6f59b85b2..41b26a299 100644 --- a/dstack/kms/src/onboard_service.rs +++ b/dstack/kms/src/onboard_service.rs @@ -29,7 +29,6 @@ use ra_tls::{ rcgen::{Certificate, KeyPair, PKCS_ECDSA_P256_SHA256}, }; use safe_write::{safe_write, safe_write_with_mode}; -use sha2::Digest; use tokio::sync::Mutex as AsyncMutex; use tracing::info; @@ -238,16 +237,15 @@ fn build_attestation_info_response( ) -> Result { let boot_info = build_boot_info_for_attestation(verified, false, vm_config) .context("Failed to decode app info")?; - let raw_device_id = verified.report.get_devide_id(); Ok(AttestationInfoResponse { - device_id: sha2::Sha256::digest(&raw_device_id).to_vec(), + device_id: boot_info.device_id, mr_aggregated: boot_info.mr_aggregated, os_image_hash: boot_info.os_image_hash, tee_variant, site_name, eth_rpc_url, kms_contract_address, - ppid: raw_device_id, + ppid: verified.report.get_devide_id(), }) } @@ -396,43 +394,6 @@ mod tests { assert_eq!(response.kms_contract_address, "0x1234"); } - /// The device id an operator registers is the one `GetAttestationInfo` - /// reports, so the id `GetAppKey` presents for authorization has to be the - /// same bytes — otherwise an SNP device allowlist can never match. - #[test] - fn snp_attestation_info_device_id_matches_the_authorization_device_id() { - let input = valid_snp_measurement_input(); - let measurement = compute_expected_measurement(&input).unwrap(); - let mr_config = valid_snp_mr_config(); - let attestation = verified_snp_attestation(measurement, [0xab; 64]); - let snp_document = snp_measurement_document(&input); - let os_image_hash = dstack_types::image_hash_from_sha256sum(&snp_document.checksum_file); - let vm_config = serde_json::json!({ - "os_image_hash": hex::encode(os_image_hash), - "sev_snp_measurement": serde_json::to_string(&snp_document).unwrap(), - "mr_config": mr_config.to_canonical_json(), - }) - .to_string(); - - let reported = build_attestation_info_response( - &attestation, - "dstack-amd-sev-snp".to_string(), - &vm_config, - "test-site".to_string(), - "https://rpc.example".to_string(), - "0x1234".to_string(), - ) - .expect("snp attestation info should be derived from snp boot info"); - let presented = build_boot_info_for_attestation(&attestation, false, &vm_config) - .expect("snp attestation should build boot info"); - - assert_eq!( - reported.device_id, presented.device_id, - "registered and presented SNP device ids must be identical" - ); - assert_eq!(presented.device_id.len(), 32, "device id must fit bytes32"); - } - #[test] fn onboarding_domain_accepts_dns_name() { validate_onboarding_domain("kms.example.com").unwrap();