Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
82 changes: 80 additions & 2 deletions crates/attestation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@ This crate provides:
- Attestation generation and verification for DCAP and (optionally) Azure
- Parsing and evaluation of measurement policies

## Verification results

`AttestationVerifier::verify_attestation` and
`AttestationVerifier::verify_attestation_sync` return the
`ExpectedMeasurements` value from the policy record that accepted the
attestation. This is the matched policy value, not the raw register values
extracted from the quote. For example, verification against a portable policy
returns `ExpectedMeasurements::Image`, while an allow-any DCAP policy returns
`ExpectedMeasurements::Dcap` with an empty register map. Successful
verification without attestation returns `ExpectedMeasurements::NoAttestation`.

Matched expected measurements can be transported in an HTTP header using
`ExpectedMeasurements::to_header_format` and reconstructed with
`ExpectedMeasurements::from_header_format`. See
[Expected measurement header format](#expected-measurement-header-format).

## Runtime Requirements

Verification uses the [`pccs`](../pccs) crate for collateral caching and
Expand Down Expand Up @@ -103,8 +119,7 @@ These objects have the following fields:
- `attestation_type` - a string containing one of the attestation types
(confidential computing platforms) described below.
- `measurements` - an object with fields referring to the five measurement
registers. Field names are the same as for the measurement headers (see
below).
registers. See [Measurement field names](#measurement-field-names).
- `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
Expand Down Expand Up @@ -248,6 +263,69 @@ Legacy numeric field names are still supported for backwards compatibility:
- "3" - RTMR2
- "4" - RTMR3

### Expected measurement header format

`ExpectedMeasurements::to_header_format` serializes the matched policy value
as self-describing JSON suitable for an HTTP `HeaderValue`. Hashes are
lowercase hexadecimal strings. Both SHA-384 DCAP values and SHA-256 Azure
values are represented as hex.

DCAP and Azure register values are arrays because a policy can accept more
than one value for each register. Header JSON uses numeric register keys:

- DCAP: `"0"` is MRTD, followed by `"1"` through `"4"` for RTMR0 through
RTMR3.
- Azure: the key is the PCR index.

DCAP example:

```JSON
{
"type": "dcap",
"measurements": {
"0": ["<96 hex characters>"],
"3": ["<96 hex characters>", "<96 hex characters>"]
}
}
```

Azure example:

```JSON
{
"type": "azure",
"measurements": {
"4": ["<64 hex characters>"],
"11": ["<64 hex characters>"]
}
}
```

Portable image-hash example:

```JSON
{
"type": "image",
"measurements": {
"uki_authenticode": "<96 hex characters>",
"kernel_authenticode": "<96 hex characters>",
"cmdline_hash": "<96 hex characters>",
"initrd_hash": "<96 hex characters>",
"gpt_disk_guid_hash": "<96 hex characters>"
}
}
```

No-attestation example:

```JSON
{"type":"no_attestation"}
```

The actual header value is compact JSON on one line. The decoder preserves
partial register policies and every alternative value in a register's
`expected_any` list.

### Portable measurement policies

The `measurements` format above specifies register values, so any change
Expand Down
4 changes: 3 additions & 1 deletion crates/attestation/src/gcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,14 @@ mod tests {
};
let gcp_firmware_cache = create_cache_with_firmware(firmware);

measurement_policy
let matched_measurements = measurement_policy
.check_measurement_with_gcp_cache(
&measurements,
Some(&gcp_portable_platform_metadata()),
Some(&gcp_firmware_cache),
)
.unwrap();

assert_eq!(matched_measurements, ExpectedMeasurements::Image(gcp_portable_image_hashes()));
}
}
123 changes: 79 additions & 44 deletions crates/attestation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use std::{

use attest_measure::platform::PlatformError;
pub use attest_types::{AttestationEvidence, PlatformMetadata};
use measurements::MultiMeasurements;
use measurements::{ExpectedMeasurements, MultiMeasurements};
use parity_scale_codec::{Decode, Encode};
use pccs::{Pccs, PccsError};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -535,13 +535,13 @@ impl AttestationVerifier {
}
}

/// Verify an attestation, and ensure the measurements match one of our
/// accepted measurements
/// Verify an attestation, and return the expected measurements from the
/// matching policy record.
pub async fn verify_attestation(
&self,
attestation_exchange_message: AttestationExchangeMessage,
expected_input_data: [u8; 64],
) -> Result<Option<MultiMeasurements>, AttestationError> {
) -> Result<ExpectedMeasurements, AttestationError> {
let attestation_type = attestation_exchange_message.attestation_type();
tracing::debug!("Verifying {attestation_type} attestation");

Expand All @@ -555,7 +555,7 @@ impl AttestationVerifier {
return Err(AttestationError::AttestationTypeNotAccepted);
}
if attestation_exchange_message.attestation_evidence.is_none() {
return Ok(None);
MultiMeasurements::NoAttestation
} else {
return Err(AttestationError::AttestationGivenWhenNoneExpected);
}
Expand Down Expand Up @@ -599,41 +599,45 @@ impl AttestationVerifier {
.attestation_evidence
.as_ref()
.map(|evidence| evidence.platform.clone());

let policy_state = self.measurement_policy_read().clone();
let policy_check = policy_state.policy.check_measurement_with_gcp_cache(
&measurements,
platform_metadata.as_ref(),
Some(&self.known_gcp_firmware),
);

if let Err(err) = policy_check {
// If this fails, and we have dynamic measurement policy, re-retrieve our
// measurement policy, then check the policy a second time
if let Some(file_or_url) = &self.dynamic_measurement_policy {
let new_measurement_policy =
MeasurementPolicy::from_file_or_url(file_or_url.to_string()).await?;
let measurement_policy =
self.set_measurement_policy(new_measurement_policy, policy_state.generation);
measurement_policy.check_measurement_with_gcp_cache(
&measurements,
platform_metadata.as_ref(),
Some(&self.known_gcp_firmware),
)?;
} else {
return Err(err);
let matched_measurements = match policy_check {
Ok(matched_measurements) => matched_measurements,
Err(err) => {
// If this fails, and we have dynamic measurement policy, re-retrieve our
// measurement policy, then check the policy a second time
if let Some(file_or_url) = &self.dynamic_measurement_policy {
let new_measurement_policy =
MeasurementPolicy::from_file_or_url(file_or_url.to_string()).await?;
let measurement_policy = self
.set_measurement_policy(new_measurement_policy, policy_state.generation);
measurement_policy.check_measurement_with_gcp_cache(
&measurements,
platform_metadata.as_ref(),
Some(&self.known_gcp_firmware),
)?
} else {
return Err(err);
}
}
}
};

tracing::debug!("Verification successful");
Ok(Some(measurements))
Ok(matched_measurements)
}

/// Verify an attestation synchronously, and return the expected
/// measurements from the matching policy record.
pub fn verify_attestation_sync(
&self,
attestation_exchange_message: AttestationExchangeMessage,
expected_input_data: [u8; 64],
) -> Result<Option<MultiMeasurements>, AttestationError> {
) -> Result<ExpectedMeasurements, AttestationError> {
let attestation_type = attestation_exchange_message.attestation_type();
tracing::debug!("Verifying {attestation_type} attestation");

Expand All @@ -647,7 +651,7 @@ impl AttestationVerifier {
return Err(AttestationError::AttestationTypeNotAccepted);
}
if attestation_exchange_message.attestation_evidence.is_none() {
return Ok(None);
MultiMeasurements::NoAttestation
} else {
return Err(AttestationError::AttestationGivenWhenNoneExpected);
}
Expand Down Expand Up @@ -703,24 +707,27 @@ impl AttestationVerifier {
Some(&self.known_gcp_firmware),
);

if let Err(err) = policy_check {
if let Some(file_or_url) = &self.dynamic_measurement_policy {
let new_measurement_policy =
MeasurementPolicy::from_file_or_url_sync(file_or_url.to_string())?;
let measurement_policy =
self.set_measurement_policy(new_measurement_policy, policy_state.generation);
measurement_policy.check_measurement_with_gcp_cache(
&measurements,
platform_metadata.as_ref(),
Some(&self.known_gcp_firmware),
)?;
} else {
return Err(err);
let matched_measurements = match policy_check {
Ok(matched_measurements) => matched_measurements,
Err(err) => {
if let Some(file_or_url) = &self.dynamic_measurement_policy {
let new_measurement_policy =
MeasurementPolicy::from_file_or_url_sync(file_or_url.to_string())?;
let measurement_policy = self
.set_measurement_policy(new_measurement_policy, policy_state.generation);
measurement_policy.check_measurement_with_gcp_cache(
&measurements,
platform_metadata.as_ref(),
Some(&self.known_gcp_firmware),
)?
} else {
return Err(err);
}
}
}
};

tracing::debug!("Verification successful");
Ok(Some(measurements))
Ok(matched_measurements)
}

/// Whether we allow no remote attestation
Expand Down Expand Up @@ -932,6 +939,22 @@ mod tests {
let _ = running_on_gcp();
}

#[tokio::test]
async fn verifier_returns_matched_no_attestation_measurements() {
let verifier = AttestationVerifier::expect_none();
let attestation = AttestationExchangeMessage::without_attestation();
let input_data = [0u8; 64];

assert_eq!(
verifier.verify_attestation(attestation.clone(), input_data).await.unwrap(),
ExpectedMeasurements::NoAttestation
);
assert_eq!(
verifier.verify_attestation_sync(attestation, input_data).unwrap(),
ExpectedMeasurements::NoAttestation
);
}

#[tokio::test]
async fn mock_verifier_supports_sync_verification() {
let input_data = [7u8; 64];
Expand All @@ -950,7 +973,10 @@ mod tests {

let result = verifier.verify_attestation_sync(attestation_evidence.into(), input_data);

assert!(result.is_ok(), "expected sync mock verification to succeed: {result:?}");
assert!(
matches!(result, Ok(ExpectedMeasurements::Dcap(_))),
"expected sync mock verification to return matched DCAP measurements: {result:?}"
);
}

#[test]
Expand All @@ -969,7 +995,10 @@ mod tests {
let generation = verifier_clone.measurement_policy_read().generation;
verifier_clone.set_measurement_policy(MeasurementPolicy::expect_none(), generation);

assert!(matches!(verifier.verify_attestation_sync(message, input_data), Ok(None)));
assert!(matches!(
verifier.verify_attestation_sync(message, input_data),
Ok(ExpectedMeasurements::NoAttestation)
));
}

#[test]
Expand Down Expand Up @@ -1009,7 +1038,10 @@ mod tests {

tokio::fs::write(&policy_path, br#"[{"attestation_type":"dcap-tdx"}]"#).await.unwrap();

verifier.verify_attestation(attestation.into(), input_data).await.unwrap();
let matched_measurements =
verifier.verify_attestation(attestation.into(), input_data).await.unwrap();

assert!(matches!(matched_measurements, ExpectedMeasurements::Dcap(_)));

assert!(verifier.measurement_policy().check_measurement(&measurements, None).is_ok());
}
Expand Down Expand Up @@ -1039,6 +1071,9 @@ mod tests {

std::fs::write(&policy_path, br#"[{"attestation_type":"dcap-tdx"}]"#).unwrap();

verifier.verify_attestation_sync(attestation.into(), input_data).unwrap();
let matched_measurements =
verifier.verify_attestation_sync(attestation.into(), input_data).unwrap();

assert!(matches!(matched_measurements, ExpectedMeasurements::Dcap(_)));
}
}
Loading
Loading