Skip to content
Merged
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
6 changes: 6 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ configfs-tsm = "0.0.2"
rand_core = { version = "0.6.4", features = ["getrandom"] }
dcap-qvl = "0.3.4"
hex = "0.4.3"
hyper = { version = "1.7.0", features = ["server"] }
hyper-util = "0.1.17"
http-body-util = "0.1.3"
bytes = "1.10.1"
http = "1.3.1"
serde_json = "1.0.145"

[dev-dependencies]
rcgen = "0.14.5"
Expand Down
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,30 @@ It has three commands:

Unlike `cvm-reverse-proxy`, this uses post-handshake remote-attested TLS, meaning regular CA-signed TLS certificates can be used.

However attestation generation and verification is not yet implemented - there is a trait provided and mock attestation for testing purposes.
This repo shares some code with [ameba23/attested-channels](https://github.com/ameba23/attested-channels) and may eventually be merged with that crate.

This shares some code with [ameba23/attested-channels](https://github.com/ameba23/attested-channels) and may eventually be merged with that crate.
## Measurement headers

When attestation is validated successfully, the following values are injected into the request / response headers:

Header name: `X-Flashbots-Measurement`

Header value:
```json
{
"0": "48 byte MRTD value encoded as hex",
"1": "48 byte RTMR0 value encoded as hex",
"2": "48 byte RTMR1 value encoded as hex",
"3": "48 byte RTMR2 value encoded as hex",
"4": "48 byte RTMR3 value encoded as hex",
}
```

Header name: `X-Flashbots-Attestation-Type`

Header value:

One of `none`, `dummy`, `azure-tdx`, `qemu-tdx`, `gcp-tdx`.

These aim to match the header formatting used by `cvm-reverse-proxy`.

157 changes: 132 additions & 25 deletions src/attestation.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
use std::time::SystemTimeError;
use std::{
collections::HashMap,
fmt::{self, Display, Formatter},
time::SystemTimeError,
};

use configfs_tsm::QuoteGenerationError;
use dcap_qvl::{
collateral::get_collateral_for_fmspc,
quote::{Quote, Report},
};
use http::{header::InvalidHeaderValue, HeaderValue};
use sha2::{Digest, Sha256};
use tdx_quote::QuoteParseError;
use thiserror::Error;
Expand All @@ -14,12 +19,105 @@ use x509_parser::prelude::*;
/// For fetching collateral directly from intel, if no PCCS is specified
const PCS_URL: &str = "https://api.trustedservices.intel.com";

#[derive(Debug, Clone, PartialEq)]
pub struct Measurements {
pub platform: PlatformMeasurements,
pub cvm_image: CvmImageMeasurements,
}

impl Measurements {
pub fn to_header_format(&self) -> Result<HeaderValue, MeasurementFormatError> {
let mut measurements_map = HashMap::new();
measurements_map.insert(0, hex::encode(self.platform.mrtd));
measurements_map.insert(1, hex::encode(self.platform.rtmr0));
measurements_map.insert(2, hex::encode(self.cvm_image.rtmr1));
measurements_map.insert(3, hex::encode(self.cvm_image.rtmr2));
measurements_map.insert(4, hex::encode(self.cvm_image.rtmr3));
Ok(HeaderValue::from_str(&serde_json::to_string(
&measurements_map,
)?)?)
}

pub fn from_header_format(input: &str) -> Result<Self, MeasurementFormatError> {
let measurements_map: HashMap<u32, String> = serde_json::from_str(input)?;
let measurements_map: HashMap<u32, [u8; 48]> = measurements_map
.into_iter()
.map(|(k, v)| (k, hex::decode(v).unwrap().try_into().unwrap()))
.collect();

Ok(Self {
platform: PlatformMeasurements {
mrtd: *measurements_map
.get(&0)
.ok_or(MeasurementFormatError::MissingValue("MRTD".to_string()))?,
rtmr0: *measurements_map
.get(&1)
.ok_or(MeasurementFormatError::MissingValue("RTMR0".to_string()))?,
},
cvm_image: CvmImageMeasurements {
rtmr1: *measurements_map
.get(&2)
.ok_or(MeasurementFormatError::MissingValue("RTMR1".to_string()))?,
rtmr2: *measurements_map
.get(&3)
.ok_or(MeasurementFormatError::MissingValue("RTMR2".to_string()))?,
rtmr3: *measurements_map
.get(&4)
.ok_or(MeasurementFormatError::MissingValue("RTMR3".to_string()))?,
},
})
}
}

#[derive(Error, Debug)]
pub enum MeasurementFormatError {
#[error("JSON: {0}")]
Json(#[from] serde_json::Error),
#[error("Missing value: {0}")]
MissingValue(String),
#[error("Invalid header value: {0}")]
BadHeaderValue(#[from] InvalidHeaderValue),
}

/// Type of attestaion used
/// Only supported (or soon-to-be supported) types are given
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AttestationType {
/// No attestion
None,
/// Mock attestion
Dummy,
/// TDX on Google Cloud Platform
GcpTdx,
/// TDX on Azure, with MAA
AzureTdx,
/// TDX on Qemu (no cloud platform)
QemuTdx,
}

impl AttestationType {
/// Matches the names used by Constellation aTLS
pub fn as_str(&self) -> &'static str {
match self {
AttestationType::None => "none",
AttestationType::Dummy => "dummy",
AttestationType::AzureTdx => "azure-tdx",
AttestationType::QemuTdx => "qemu-tdx",
AttestationType::GcpTdx => "gcp-tdx",
}
}
}

impl Display for AttestationType {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}

/// Defines how to generate a quote
pub trait QuoteGenerator: Clone + Send + 'static {
/// Whether this is CVM attestation. This should always return true except for the [NoQuoteGenerator] case.
///
/// When false, allows TLS client to be configured without client authentication
fn is_cvm(&self) -> bool;
/// Type of attestation used
fn attestation_type(&self) -> AttestationType;

/// Generate an attestation
fn create_attestation(
Expand All @@ -31,27 +129,28 @@ pub trait QuoteGenerator: Clone + Send + 'static {

/// Defines how to verify a quote
pub trait QuoteVerifier: Clone + Send + 'static {
/// Whether this is CVM attestation. This should always return true except for the [NoQuoteVerifier] case.
///
/// When false, allows TLS client to be configured without client authentication
fn is_cvm(&self) -> bool;
/// Type of attestation used
fn attestation_type(&self) -> AttestationType;

/// Verify the given attestation payload
fn verify_attestation(
&self,
input: Vec<u8>,
cert_chain: &[CertificateDer<'_>],
exporter: [u8; 32],
) -> impl Future<Output = Result<(), AttestationError>> + Send;
) -> impl Future<Output = Result<Option<Measurements>, AttestationError>> + Send;
}

/// Quote generation using configfs_tsm
#[derive(Clone)]
pub struct DcapTdxQuoteGenerator;
pub struct DcapTdxQuoteGenerator {
pub attestation_type: AttestationType,
}

impl QuoteGenerator for DcapTdxQuoteGenerator {
fn is_cvm(&self) -> bool {
true
/// Type of attestation used
fn attestation_type(&self) -> AttestationType {
self.attestation_type
}

fn create_attestation(
Expand All @@ -66,7 +165,7 @@ impl QuoteGenerator for DcapTdxQuoteGenerator {
}

/// Measurements determined by the CVM platform
#[derive(Clone, PartialEq)]
#[derive(Clone, PartialEq, Debug)]
pub struct PlatformMeasurements {
pub mrtd: [u8; 48],
pub rtmr0: [u8; 48],
Expand Down Expand Up @@ -96,7 +195,7 @@ impl PlatformMeasurements {
}

/// Measurements determined by the CVM image
#[derive(Clone, PartialEq)]
#[derive(Clone, PartialEq, Debug)]
pub struct CvmImageMeasurements {
pub rtmr1: [u8; 48],
pub rtmr2: [u8; 48],
Expand Down Expand Up @@ -132,6 +231,7 @@ impl CvmImageMeasurements {
/// OS image specific measurements
#[derive(Clone)]
pub struct DcapTdxQuoteVerifier {
pub attestation_type: AttestationType,
/// Platform specific allowed Measurements
/// Currently an option as this may be determined internally on a per-platform basis (Eg: GCP)
pub accepted_platform_measurements: Option<Vec<PlatformMeasurements>>,
Expand All @@ -142,16 +242,17 @@ pub struct DcapTdxQuoteVerifier {
}

impl QuoteVerifier for DcapTdxQuoteVerifier {
fn is_cvm(&self) -> bool {
true
/// Type of attestation used
fn attestation_type(&self) -> AttestationType {
self.attestation_type
}

async fn verify_attestation(
&self,
input: Vec<u8>,
cert_chain: &[CertificateDer<'_>],
exporter: [u8; 32],
) -> Result<(), AttestationError> {
) -> Result<Option<Measurements>, AttestationError> {
let quote_input = compute_report_input(cert_chain, exporter)?;
let (platform_measurements, image_measurements) = if cfg!(not(test)) {
let now = std::time::SystemTime::now()
Expand Down Expand Up @@ -205,7 +306,10 @@ impl QuoteVerifier for DcapTdxQuoteVerifier {
return Err(AttestationError::UnacceptableOsImageMeasurements);
}

Ok(())
Ok(Some(Measurements {
platform: platform_measurements,
cvm_image: image_measurements,
}))
}
}

Expand Down Expand Up @@ -236,8 +340,9 @@ pub fn compute_report_input(
pub struct NoQuoteGenerator;

impl QuoteGenerator for NoQuoteGenerator {
fn is_cvm(&self) -> bool {
false
/// Type of attestation used
fn attestation_type(&self) -> AttestationType {
AttestationType::None
}

/// Create an empty attestation
Expand All @@ -255,18 +360,20 @@ impl QuoteGenerator for NoQuoteGenerator {
pub struct NoQuoteVerifier;

impl QuoteVerifier for NoQuoteVerifier {
fn is_cvm(&self) -> bool {
false
/// Type of attestation used
fn attestation_type(&self) -> AttestationType {
AttestationType::None
}

/// Ensure that an empty attestation is given
async fn verify_attestation(
&self,
input: Vec<u8>,
_cert_chain: &[CertificateDer<'_>],
_exporter: [u8; 32],
) -> Result<(), AttestationError> {
) -> Result<Option<Measurements>, AttestationError> {
if input.is_empty() {
Ok(())
Ok(None)
} else {
Err(AttestationError::AttestationGivenWhenNoneExpected)
}
Expand Down
Loading