Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
38e80bb
First step towards integrating easy-tee/attest
ameba23 Jun 3, 2026
3227a16
Add test for measurement policy
ameba23 Jun 3, 2026
25ea83f
Also check rtmr0
ameba23 Jun 3, 2026
bced75d
Also check mrtd
ameba23 Jun 3, 2026
b25a42f
Fix to compile for azure feature
ameba23 Jun 3, 2026
9e0c263
Clippy
ameba23 Jun 3, 2026
737d790
Error handling
ameba23 Jun 3, 2026
5d20913
Typo, comments
ameba23 Jun 3, 2026
f245588
Use AttestationEvidence directly in AttestationExchangeMessage
ameba23 Jun 4, 2026
54dff24
Do not allow converting AttestationType::None to a attest-types::Atte…
ameba23 Jun 4, 2026
54e86c0
fmt
ameba23 Jun 5, 2026
c296367
Merge branch 'main' into peg/attest-integrate-incremental-00
ameba23 Jun 12, 2026
46fca45
Update to use ah/gcp-hobgen branch of the attest repo
ameba23 Jun 16, 2026
d75fda9
Update to use ah/gcp-hobgen branch of the attest repo
ameba23 Jun 16, 2026
52370b0
Logging, fix for attestation-provider-server
ameba23 Jun 16, 2026
bcc671f
Cache known GCP firmware indexed by MRTD
ameba23 Jun 16, 2026
ef5908d
Rm cache prewarm
ameba23 Jun 24, 2026
52579b8
Fmt
ameba23 Jun 24, 2026
bbabfff
Avoid api breaking change
ameba23 Jun 24, 2026
7d3f3ce
Rm unused test asset
ameba23 Jun 24, 2026
b61da02
Fixes following revert breaking api change
ameba23 Jun 24, 2026
6ae5aeb
Fmt
ameba23 Jun 24, 2026
fb070c8
Merge pull request #57 from flashbots/peg/gcp-known-firmware-cache
ameba23 Jun 24, 2026
870de13
Merge main
ameba23 Jun 24, 2026
4f53f7d
Fmt
ameba23 Jun 24, 2026
e99e89a
Add test demonstrating portable measurement policy with observed data…
ameba23 Jun 30, 2026
c78d660
Add test demonstrating portable measurement policy with observed data…
ameba23 Jun 30, 2026
bab2de8
Quote generator should not assume tdx-gcp
ameba23 Jun 30, 2026
59dd749
Switch to using pinned commit from head of main of attest repo now th…
ameba23 Jul 2, 2026
f002fa6
Never use placeholder platform metadata in production - drop support …
ameba23 Jul 2, 2026
bd68d39
Warn and bail when trying to match non-gcp measurements with portable…
ameba23 Jul 2, 2026
2024343
Update measurements JSON format and cover the new format in tests
ameba23 Jul 2, 2026
0f67d37
Document new measurement policy type
ameba23 Jul 3, 2026
7dd7223
Improve error handling during measurement checks
ameba23 Jul 3, 2026
17ac5d6
Revert changes from merge commit which are unrelated to this PR
ameba23 Jul 3, 2026
5790f19
On tokio runtime, wrap GCP firmware fetch in spawn_blocking to avoid …
ameba23 Jul 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
354 changes: 345 additions & 9 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion crates/attestation-provider-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ pub async fn attestation_provider_client(
.await?;

let remote_attestation_message = AttestationExchangeMessage::decode(&mut &response[..])?;
let remote_attestation_type = remote_attestation_message.attestation_type;
let remote_attestation_type = remote_attestation_message.attestation_type();

println!("Remote attestation type: {remote_attestation_type}");

Expand Down
4 changes: 3 additions & 1 deletion crates/attestation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ keywords = ["attestation", "CVM", "TDX"]
dcap-qvl = { workspace = true, features = ["danger-allow-tcb-override"] }
pccs = { workspace = true }
mock-tdx = { workspace = true, optional = true }
tokio = { workspace = true, features = ["fs"] }
tokio = { workspace = true, features = ["fs", "rt", "rt-multi-thread"] }
tokio-rustls = { workspace = true, default-features = false }
attest-types = { git = "https://github.com/easy-tee/attest.git", rev = "a64f147362b8948e2288015e476c40d04b11b661" }
attest-measure = {git = "https://github.com/easy-tee/attest.git", rev = "a64f147362b8948e2288015e476c40d04b11b661" }

anyhow = "1.0.100"
pem-rfc7468 = { version = "0.7.0", features = ["std"] }
Expand Down
65 changes: 61 additions & 4 deletions crates/attestation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ These objects have the following fields:
- `measurements` - an object with fields referring to the five measurement
registers. Field names are the same as for the measurement headers (see
below).
- `dcap_image_hashes` - an alternative to `measurements` that pins the hashes
of the boot components (UKI, kernel, initrd, cmdline, GPT disk GUID) rather
than raw register values. The verifier reconstructs the expected RTMRs at
check time from these hashes and platform-fetched firmware. See
[Portable measurement policies](#portable-measurement-policies) below.

A record may set either `measurements` or `dcap_image_hashes`, not both.

Each measurement register entry supports two mutually exclusive fields:

Expand Down Expand Up @@ -190,10 +197,11 @@ compatibility:
</details>

The only mandatory field is `attestation_type`. If an attestation type is
specified, but no measurements, *any* measurements will be accepted for this
attestation type. The measurements can still be checked up-stream by the source
client or target service using header injection described below. But it is then
up to these external programs to reject unacceptable measurements.
specified with neither `measurements` nor `dcap_image_hashes`, *any*
measurements will be accepted for this attestation type. The measurements can
still be checked up-stream by the source client or target service using header
injection for example. But it is then up to external programs to reject
unacceptable measurements.

### Measurement field names

Expand Down Expand Up @@ -240,3 +248,52 @@ Legacy numeric field names are still supported for backwards compatibility:
- "2" - RTMR1
- "3" - RTMR2
- "4" - RTMR3

### Portable measurement policies

The `measurements` format above specifies register values, so any change
to platform-injected values (firmware, RAM size, disk count, ACPI tables)
changes the expected register values even when the OS image is unchanged.

The `dcap_image_hashes` alternative allows you to specify the OS image's
boot-component hashes instead, and the verifier reconstructs the expected
register values from those hashes plus platform metadata fetched attest
verification time. The same policy record then matches the same OS images
across platform variants.

This can be done with the `attest measure` CLI from
[Easy-TEE/attest](https://github.com/Easy-TEE/attest) which outputs five
hex-encoded SHA-384 values:

- `uki_authenticode` - authenticode hash of the UKI (unified kernel image)
- `kernel_authenticode` - authenticode hash of the kernel binary
- `cmdline_hash` - hash of the kernel command line
- `initrd_hash` - hash of the initramfs
- `gpt_disk_guid_hash` - hash derived from GPT partition GUIDs

Example:

```JSON
[
{
"measurement_id": "flashbox-l1-v1.0.0",
"attestation_type": "gcp-tdx",
"dcap_image_hashes": {
"uki_authenticode": "fcaceb6d87694746ba2d93a87ef4209f2a7629b7f400097b93241e80b9ec3e1e80f9a4cd8028e6a83f297ea5de8d9abc",
"kernel_authenticode": "b6c5133268aa8b440509f3d53ee855a5cd3aeb6441eb109a9f27f14c43bce3e2383856df4af876501ceeb4c9a3b15f0c",
"cmdline_hash": "e03b89abf354a38976537b7a9138fd312e4cbf73b61eebc44086491701b1d167b9f6cb97a922325866c93e0834723d87",
"initrd_hash": "a5b3d4742045e7d08aa19953c35098e784826b01a84f60568fa69f1a848dafd96ec98b8df616d6142779c9b97318166b",
"gpt_disk_guid_hash": "180bac1af9c35cc15e909623c005289539b4da2840d9c9b658fd4968ea4f03e0159402d03da1afc9035e0db30804e282"
}
}
]
```

#### Supported attestation types for portable measurements

Portable policies currently only work with the `"gcp-tdx"` attestation type.
For GCP, the verifier fetches the platform firmware blob from Google's metadata
service (keyed by MRTD) and combines it with the image hashes to reconstruct
the expected registers. Support for other attestation types is planned; but
`dcap_image_hashes` record with any other attestation type is rejected when
parsing from JSON.
18 changes: 7 additions & 11 deletions crates/attestation/src/dcap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,16 +254,14 @@ pub fn verify_dcap_attestation_sync(

/// Create a mock quote for testing on non-confidential hardware
#[cfg(any(test, feature = "mock"))]
fn generate_quote(input: [u8; 64]) -> Result<Vec<u8>, tdx_attest::TdxAttestError> {
generate_mock_tdx_quote(input).map_err(|error| {
tdx_attest::TdxAttestError::QuoteFailure(format!("mock-tdx quote generation: {error}"))
})
fn generate_quote(input: [u8; 64]) -> Result<Vec<u8>, AttestationError> {
generate_mock_tdx_quote(input).map_err(|error| AttestationError::Mock(format!("{error}")))
}

/// Create a quote
#[cfg(not(any(test, feature = "mock")))]
fn generate_quote(input: [u8; 64]) -> Result<Vec<u8>, tdx_attest::TdxAttestError> {
tdx_attest::get_quote(&input)
fn generate_quote(input: [u8; 64]) -> Result<Vec<u8>, AttestationError> {
Ok(tdx_attest::get_quote(&input)?)
}

/// Given a [Report] get the input data regardless of report type
Expand Down Expand Up @@ -363,7 +361,7 @@ mod tests {
.unwrap();

assert_eq!(async_measurements, sync_measurements);
measurement_policy.check_measurement(&async_measurements).unwrap();
measurement_policy.check_measurement(&async_measurements, None).unwrap();
}

// This specifically tests a quote which has outdated TCB level from Azure
Expand Down Expand Up @@ -407,12 +405,10 @@ mod tests {
.unwrap();
let pccs = Pccs::new(Some(mock_pcs.base_url.clone()));
let expected_input_data = [0xA5; 64];
let attestation_bytes = create_dcap_attestation(expected_input_data).unwrap();
let quote = create_dcap_attestation(expected_input_data).unwrap();

let measurements =
verify_dcap_attestation(attestation_bytes, expected_input_data, Some(pccs))
.await
.unwrap();
verify_dcap_attestation(quote, expected_input_data, Some(pccs)).await.unwrap();

assert_eq!(measurements, crate::measurements::mock_dcap_measurements());
assert_eq!(mock_pcs.tcb_call_count(), 1);
Expand Down
188 changes: 188 additions & 0 deletions crates/attestation/src/gcp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
use std::{
collections::HashMap,
sync::{Arc, RwLock},
};

use attest_measure::dcap::DcapFirmware;
use thiserror::Error;

/// Maps MRTD values to GCP firmware to avoid re-fetching on subsequent
/// verification
#[derive(Clone, Debug, Default)]
pub(crate) struct GcpFirmwareCache {
cache: Arc<RwLock<HashMap<[u8; 48], attest_measure::dcap::DcapFirmware>>>,
}

impl GcpFirmwareCache {
pub(crate) fn new() -> Self {
Self { cache: Default::default() }
}

/// Retrieve firmware from cache or fetch if not present
pub(crate) fn get_or_fetch(
&self,
mrtd: [u8; 48],
) -> Result<DcapFirmware, GcpFirmwareCacheError> {
if let Some(firmware) =
self.cache.read().map_err(|_| GcpFirmwareCacheError::CacheLock)?.get(&mrtd).cloned()
{
return Ok(firmware);
}

let firmware = fetch_firmware(mrtd)?;
self.cache
.write()
.map_err(|_| GcpFirmwareCacheError::CacheLock)?
.insert(mrtd, firmware.clone());
Ok(firmware)
}
}

/// Fetch firmware from Google. If we are running inside a mutli-threaded
/// tokio runtime the blocking HTTP fetch is wrapped in `spawn_blocking`
pub(crate) fn fetch_firmware(mrtd: [u8; 48]) -> Result<DcapFirmware, GcpFirmwareCacheError> {
match tokio::runtime::Handle::try_current() {
Ok(handle)
if matches!(handle.runtime_flavor(), tokio::runtime::RuntimeFlavor::MultiThread) =>
{
tokio::task::block_in_place(|| {
handle.block_on(async move {
tokio::task::spawn_blocking(move || DcapFirmware::from_google(mrtd))
.await
.map_err(|err| GcpFirmwareCacheError::Join(err.to_string()))?
.map_err(GcpFirmwareCacheError::from)
})
})
}
_ => DcapFirmware::from_google(mrtd).map_err(GcpFirmwareCacheError::from),
}
}

#[derive(Debug, Error)]
pub(crate) enum GcpFirmwareCacheError {
#[error("Cache lock poisoned")]
CacheLock,
#[error("Firmware fetch: {0}")]
Firmware(#[from] attest_measure::dcap::GoogleError),
#[error("Firmware fetch task join: {0}")]
Join(String),
}

#[cfg(test)]
mod tests {
use attest_measure::dcap::DcapFirmware;
use attest_types::{AcpiHashes, DcapImageHashes};
use dcap_qvl::quote::Quote;

use crate::{
PlatformMetadata,
dcap::{get_quote_input_data, verify_dcap_attestation_with_given_timestamp},
gcp::GcpFirmwareCache,
measurements::{ExpectedMeasurements, MeasurementPolicy, MeasurementRecord},
};

/// Timestamp used with test fixture
const GCP_TDX_PORTABLE_FIXTURE_TIMESTAMP: u64 = 1_782_809_233;

/// Create a firmware cache with given firmware loaded
fn create_cache_with_firmware(firmware: DcapFirmware) -> GcpFirmwareCache {
let cache = GcpFirmwareCache::new();
cache.cache.write().unwrap().insert(firmware.mrtd, firmware);
cache
}

fn decode_dcap_hash(input: &str) -> [u8; 48] {
hex::decode(input).unwrap().try_into().unwrap()
}

/// Image hashes associated with test fixture
fn gcp_portable_image_hashes() -> DcapImageHashes {
DcapImageHashes {
uki_authenticode: decode_dcap_hash(
"82500f977e16a1e3fd47db792ac9c9fdd69caa73d8e719fe4489416355f23f5d0863ad796febfc1241bc3e868c3649a6",
),
kernel_authenticode: decode_dcap_hash(
"b2a6076ae199d325e553a5102cf1f4a18b5e67e36b33261ef20352052199ec5853b5133c0231b16f1198bb086f1cbfac",
),
cmdline_hash: decode_dcap_hash(
"e03b89abf354a38976537b7a9138fd312e4cbf73b61eebc44086491701b1d167b9f6cb97a922325866c93e0834723d87",
),
initrd_hash: decode_dcap_hash(
"99251a9997f552ce98364e3f7311ca47471e299b6fdb31226d738a10577959ab741cc2e7b8c268236153de568265d3f2",
),
gpt_disk_guid_hash: decode_dcap_hash(
"488fa3f08aae01c1a46b497319e8a7d3b7335c9ff4f4d7fe6a3dd62c844b03de22157c0303be58f10e3152687778e68d",
),
}
}

/// Platform metadata associated with test fixture
fn gcp_portable_platform_metadata() -> PlatformMetadata {
PlatformMetadata {
attestation_type: attest_types::AttestationType::GcpTdx,
ram_bytes: 17_179_869_184,
num_disks: 1,
acpi: Some(AcpiHashes {
loader: [
246, 12, 53, 229, 59, 178, 27, 70, 117, 207, 168, 219, 49, 14, 200, 142, 56,
205, 54, 157, 141, 70, 58, 205, 222, 129, 81, 34, 250, 139, 137, 59, 136, 150,
165, 120, 59, 83, 136, 86, 105, 62, 215, 100, 93, 219, 137, 126,
],
rsdp: [
80, 157, 207, 225, 11, 235, 93, 71, 12, 64, 242, 94, 48, 137, 83, 112, 148,
136, 49, 185, 207, 121, 219, 21, 217, 119, 231, 187, 168, 235, 66, 247, 32, 2,
18, 7, 26, 216, 177, 157, 96, 17, 117, 151, 121, 236, 237, 90,
],
tables: [
11, 176, 175, 160, 8, 135, 59, 220, 32, 222, 224, 247, 65, 218, 120, 150, 194,
191, 238, 233, 74, 229, 46, 155, 219, 249, 75, 200, 124, 50, 208, 74, 75, 31,
29, 130, 68, 144, 241, 218, 229, 116, 255, 109, 78, 75, 176, 179,
],
}),
}
}

#[tokio::test]
async fn test_gcp_tdx_portable_policy_with_stored_collateral() {
let attestation_bytes: &'static [u8] =
include_bytes!("../test-assets/gcp-tdx-1782809233226668671");
let collateral_bytes: &'static [u8] =
include_bytes!("../test-assets/gcp-tdx-collateral-1782809233226668671.yaml");
let firmware_bytes: &'static [u8] =
include_bytes!("../test-assets/gcp-tdx-firmware-1782809233226668671.yaml");

let expected_input_data = {
let quote = Quote::parse(attestation_bytes).unwrap();
get_quote_input_data(quote.report)
};

let collateral = serde_saphyr::from_slice(collateral_bytes).unwrap();
let firmware = serde_saphyr::from_slice(firmware_bytes).unwrap();
let measurements = verify_dcap_attestation_with_given_timestamp(
attestation_bytes.to_vec(),
expected_input_data,
None,
Some(collateral),
GCP_TDX_PORTABLE_FIXTURE_TIMESTAMP,
false,
)
.await
.unwrap();

let measurement_policy = MeasurementPolicy {
accepted_measurements: vec![MeasurementRecord {
measurement_id: "gcp-tdx-portable-image-hashes".to_string(),
measurements: ExpectedMeasurements::Image(gcp_portable_image_hashes()),
}],
};
let gcp_firmware_cache = create_cache_with_firmware(firmware);

measurement_policy
.check_measurement_with_gcp_cache(
&measurements,
Some(gcp_portable_platform_metadata()),
Some(&gcp_firmware_cache),
)
.unwrap();
}
}
Loading
Loading