Skip to content

Commit e28d69e

Browse files
authored
Merge pull request #17 from flashbots/peg/measurements-http-headers
Include measurements in HTTP headers
2 parents bd5866c + d48d121 commit e28d69e

File tree

7 files changed

+513
-173
lines changed

7 files changed

+513
-173
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ configfs-tsm = "0.0.2"
2020
rand_core = { version = "0.6.4", features = ["getrandom"] }
2121
dcap-qvl = "0.3.4"
2222
hex = "0.4.3"
23+
hyper = { version = "1.7.0", features = ["server"] }
24+
hyper-util = "0.1.17"
25+
http-body-util = "0.1.3"
26+
bytes = "1.10.1"
27+
http = "1.3.1"
28+
serde_json = "1.0.145"
2329

2430
[dev-dependencies]
2531
rcgen = "0.14.5"

README.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,30 @@ It has three commands:
1010

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

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

15-
This shares some code with [ameba23/attested-channels](https://github.com/ameba23/attested-channels) and may eventually be merged with that crate.
15+
## Measurement headers
16+
17+
When attestation is validated successfully, the following values are injected into the request / response headers:
18+
19+
Header name: `X-Flashbots-Measurement`
20+
21+
Header value:
22+
```json
23+
{
24+
"0": "48 byte MRTD value encoded as hex",
25+
"1": "48 byte RTMR0 value encoded as hex",
26+
"2": "48 byte RTMR1 value encoded as hex",
27+
"3": "48 byte RTMR2 value encoded as hex",
28+
"4": "48 byte RTMR3 value encoded as hex",
29+
}
30+
```
31+
32+
Header name: `X-Flashbots-Attestation-Type`
33+
34+
Header value:
35+
36+
One of `none`, `dummy`, `azure-tdx`, `qemu-tdx`, `gcp-tdx`.
37+
38+
These aim to match the header formatting used by `cvm-reverse-proxy`.
1639

src/attestation.rs

Lines changed: 132 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
use std::time::SystemTimeError;
1+
use std::{
2+
collections::HashMap,
3+
fmt::{self, Display, Formatter},
4+
time::SystemTimeError,
5+
};
26

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

22+
#[derive(Debug, Clone, PartialEq)]
23+
pub struct Measurements {
24+
pub platform: PlatformMeasurements,
25+
pub cvm_image: CvmImageMeasurements,
26+
}
27+
28+
impl Measurements {
29+
pub fn to_header_format(&self) -> Result<HeaderValue, MeasurementFormatError> {
30+
let mut measurements_map = HashMap::new();
31+
measurements_map.insert(0, hex::encode(self.platform.mrtd));
32+
measurements_map.insert(1, hex::encode(self.platform.rtmr0));
33+
measurements_map.insert(2, hex::encode(self.cvm_image.rtmr1));
34+
measurements_map.insert(3, hex::encode(self.cvm_image.rtmr2));
35+
measurements_map.insert(4, hex::encode(self.cvm_image.rtmr3));
36+
Ok(HeaderValue::from_str(&serde_json::to_string(
37+
&measurements_map,
38+
)?)?)
39+
}
40+
41+
pub fn from_header_format(input: &str) -> Result<Self, MeasurementFormatError> {
42+
let measurements_map: HashMap<u32, String> = serde_json::from_str(input)?;
43+
let measurements_map: HashMap<u32, [u8; 48]> = measurements_map
44+
.into_iter()
45+
.map(|(k, v)| (k, hex::decode(v).unwrap().try_into().unwrap()))
46+
.collect();
47+
48+
Ok(Self {
49+
platform: PlatformMeasurements {
50+
mrtd: *measurements_map
51+
.get(&0)
52+
.ok_or(MeasurementFormatError::MissingValue("MRTD".to_string()))?,
53+
rtmr0: *measurements_map
54+
.get(&1)
55+
.ok_or(MeasurementFormatError::MissingValue("RTMR0".to_string()))?,
56+
},
57+
cvm_image: CvmImageMeasurements {
58+
rtmr1: *measurements_map
59+
.get(&2)
60+
.ok_or(MeasurementFormatError::MissingValue("RTMR1".to_string()))?,
61+
rtmr2: *measurements_map
62+
.get(&3)
63+
.ok_or(MeasurementFormatError::MissingValue("RTMR2".to_string()))?,
64+
rtmr3: *measurements_map
65+
.get(&4)
66+
.ok_or(MeasurementFormatError::MissingValue("RTMR3".to_string()))?,
67+
},
68+
})
69+
}
70+
}
71+
72+
#[derive(Error, Debug)]
73+
pub enum MeasurementFormatError {
74+
#[error("JSON: {0}")]
75+
Json(#[from] serde_json::Error),
76+
#[error("Missing value: {0}")]
77+
MissingValue(String),
78+
#[error("Invalid header value: {0}")]
79+
BadHeaderValue(#[from] InvalidHeaderValue),
80+
}
81+
82+
/// Type of attestaion used
83+
/// Only supported (or soon-to-be supported) types are given
84+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
85+
pub enum AttestationType {
86+
/// No attestion
87+
None,
88+
/// Mock attestion
89+
Dummy,
90+
/// TDX on Google Cloud Platform
91+
GcpTdx,
92+
/// TDX on Azure, with MAA
93+
AzureTdx,
94+
/// TDX on Qemu (no cloud platform)
95+
QemuTdx,
96+
}
97+
98+
impl AttestationType {
99+
/// Matches the names used by Constellation aTLS
100+
pub fn as_str(&self) -> &'static str {
101+
match self {
102+
AttestationType::None => "none",
103+
AttestationType::Dummy => "dummy",
104+
AttestationType::AzureTdx => "azure-tdx",
105+
AttestationType::QemuTdx => "qemu-tdx",
106+
AttestationType::GcpTdx => "gcp-tdx",
107+
}
108+
}
109+
}
110+
111+
impl Display for AttestationType {
112+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
113+
f.write_str(self.as_str())
114+
}
115+
}
116+
17117
/// Defines how to generate a quote
18118
pub trait QuoteGenerator: Clone + Send + 'static {
19-
/// Whether this is CVM attestation. This should always return true except for the [NoQuoteGenerator] case.
20-
///
21-
/// When false, allows TLS client to be configured without client authentication
22-
fn is_cvm(&self) -> bool;
119+
/// Type of attestation used
120+
fn attestation_type(&self) -> AttestationType;
23121

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

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

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

48144
/// Quote generation using configfs_tsm
49145
#[derive(Clone)]
50-
pub struct DcapTdxQuoteGenerator;
146+
pub struct DcapTdxQuoteGenerator {
147+
pub attestation_type: AttestationType,
148+
}
51149

52150
impl QuoteGenerator for DcapTdxQuoteGenerator {
53-
fn is_cvm(&self) -> bool {
54-
true
151+
/// Type of attestation used
152+
fn attestation_type(&self) -> AttestationType {
153+
self.attestation_type
55154
}
56155

57156
fn create_attestation(
@@ -66,7 +165,7 @@ impl QuoteGenerator for DcapTdxQuoteGenerator {
66165
}
67166

68167
/// Measurements determined by the CVM platform
69-
#[derive(Clone, PartialEq)]
168+
#[derive(Clone, PartialEq, Debug)]
70169
pub struct PlatformMeasurements {
71170
pub mrtd: [u8; 48],
72171
pub rtmr0: [u8; 48],
@@ -96,7 +195,7 @@ impl PlatformMeasurements {
96195
}
97196

98197
/// Measurements determined by the CVM image
99-
#[derive(Clone, PartialEq)]
198+
#[derive(Clone, PartialEq, Debug)]
100199
pub struct CvmImageMeasurements {
101200
pub rtmr1: [u8; 48],
102201
pub rtmr2: [u8; 48],
@@ -132,6 +231,7 @@ impl CvmImageMeasurements {
132231
/// OS image specific measurements
133232
#[derive(Clone)]
134233
pub struct DcapTdxQuoteVerifier {
234+
pub attestation_type: AttestationType,
135235
/// Platform specific allowed Measurements
136236
/// Currently an option as this may be determined internally on a per-platform basis (Eg: GCP)
137237
pub accepted_platform_measurements: Option<Vec<PlatformMeasurements>>,
@@ -142,16 +242,17 @@ pub struct DcapTdxQuoteVerifier {
142242
}
143243

144244
impl QuoteVerifier for DcapTdxQuoteVerifier {
145-
fn is_cvm(&self) -> bool {
146-
true
245+
/// Type of attestation used
246+
fn attestation_type(&self) -> AttestationType {
247+
self.attestation_type
147248
}
148249

149250
async fn verify_attestation(
150251
&self,
151252
input: Vec<u8>,
152253
cert_chain: &[CertificateDer<'_>],
153254
exporter: [u8; 32],
154-
) -> Result<(), AttestationError> {
255+
) -> Result<Option<Measurements>, AttestationError> {
155256
let quote_input = compute_report_input(cert_chain, exporter)?;
156257
let (platform_measurements, image_measurements) = if cfg!(not(test)) {
157258
let now = std::time::SystemTime::now()
@@ -205,7 +306,10 @@ impl QuoteVerifier for DcapTdxQuoteVerifier {
205306
return Err(AttestationError::UnacceptableOsImageMeasurements);
206307
}
207308

208-
Ok(())
309+
Ok(Some(Measurements {
310+
platform: platform_measurements,
311+
cvm_image: image_measurements,
312+
}))
209313
}
210314
}
211315

@@ -236,8 +340,9 @@ pub fn compute_report_input(
236340
pub struct NoQuoteGenerator;
237341

238342
impl QuoteGenerator for NoQuoteGenerator {
239-
fn is_cvm(&self) -> bool {
240-
false
343+
/// Type of attestation used
344+
fn attestation_type(&self) -> AttestationType {
345+
AttestationType::None
241346
}
242347

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

257362
impl QuoteVerifier for NoQuoteVerifier {
258-
fn is_cvm(&self) -> bool {
259-
false
363+
/// Type of attestation used
364+
fn attestation_type(&self) -> AttestationType {
365+
AttestationType::None
260366
}
367+
261368
/// Ensure that an empty attestation is given
262369
async fn verify_attestation(
263370
&self,
264371
input: Vec<u8>,
265372
_cert_chain: &[CertificateDer<'_>],
266373
_exporter: [u8; 32],
267-
) -> Result<(), AttestationError> {
374+
) -> Result<Option<Measurements>, AttestationError> {
268375
if input.is_empty() {
269-
Ok(())
376+
Ok(None)
270377
} else {
271378
Err(AttestationError::AttestationGivenWhenNoneExpected)
272379
}

0 commit comments

Comments
 (0)