Skip to content

Commit 9bd9bac

Browse files
committed
Insert measurements into headers
1 parent e8ab377 commit 9bd9bac

File tree

6 files changed

+558
-35
lines changed

6 files changed

+558
-35
lines changed

:w

Lines changed: 386 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,386 @@
1+
use std::{collections::HashMap, time::SystemTimeError};
2+
3+
use configfs_tsm::QuoteGenerationError;
4+
use dcap_qvl::{
5+
collateral::get_collateral_for_fmspc,
6+
quote::{Quote, Report},
7+
};
8+
use sha2::{Digest, Sha256};
9+
use tdx_quote::QuoteParseError;
10+
use thiserror::Error;
11+
use tokio_rustls::rustls::pki_types::CertificateDer;
12+
use x509_parser::prelude::*;
13+
14+
/// For fetching collateral directly from intel, if no PCCS is specified
15+
const PCS_URL: &str = "https://api.trustedservices.intel.com";
16+
17+
#[derive(Debug, Clone, PartialEq)]
18+
pub struct Measurements {
19+
pub platform: PlatformMeasurements,
20+
pub cvm_image: CvmImageMeasurements,
21+
}
22+
23+
impl Measurements {
24+
pub fn to_header_format(&self) -> Result<String, MeasurementFormatError> {
25+
let mut measurements_map = HashMap::new();
26+
measurements_map.insert(0, hex::encode(self.platform.mrtd));
27+
measurements_map.insert(1, hex::encode(self.platform.rtmr0));
28+
measurements_map.insert(2, hex::encode(self.cvm_image.rtmr1));
29+
measurements_map.insert(3, hex::encode(self.cvm_image.rtmr2));
30+
measurements_map.insert(4, hex::encode(self.cvm_image.rtmr3));
31+
Ok(serde_json::to_string(&measurements_map)?)
32+
}
33+
34+
pub fn from_header_format(input: &str) -> Result<Self, MeasurementFormatError> {
35+
let measurements_map: HashMap<u32, String> = serde_json::from_str(input)?;
36+
let measurements_map: HashMap<u32, [u8; 48]> = measurements_map
37+
.into_iter()
38+
.map(|(k, v)| (k, hex::decode(v).unwrap().try_into().unwrap()))
39+
.collect();
40+
41+
Ok(Self {
42+
platform: PlatformMeasurements {
43+
mrtd: *measurements_map.get(&0).ok_or(MeasurementFormatError::MissingValue("MRTD".to_string())?,
44+
rtmr0: *measurements_map.get(&1).unwrap(),
45+
},
46+
cvm_image: CvmImageMeasurements {
47+
rtmr1: *measurements_map.get(&2).unwrap(),
48+
rtmr2: *measurements_map.get(&3).unwrap(),
49+
rtmr3: *measurements_map.get(&4).unwrap(),
50+
},
51+
})
52+
}
53+
}
54+
55+
#[derive(Error, Debug)]
56+
pub enum MeasurementFormatError {
57+
#[error("JSON: {0}")]
58+
Json(#[from] serde_json::Error),
59+
#[error("Missing value: {0}")]
60+
MissingValue(String),
61+
}
62+
63+
/// Defines how to generate a quote
64+
pub trait QuoteGenerator: Clone + Send + 'static {
65+
/// Whether this is CVM attestation. This should always return true except for the [NoQuoteGenerator] case.
66+
///
67+
/// When false, allows TLS client to be configured without client authentication
68+
fn is_cvm(&self) -> bool;
69+
70+
/// Generate an attestation
71+
fn create_attestation(
72+
&self,
73+
cert_chain: &[CertificateDer<'_>],
74+
exporter: [u8; 32],
75+
) -> Result<Vec<u8>, AttestationError>;
76+
}
77+
78+
/// Defines how to verify a quote
79+
pub trait QuoteVerifier: Clone + Send + 'static {
80+
/// Whether this is CVM attestation. This should always return true except for the [NoQuoteVerifier] case.
81+
///
82+
/// When false, allows TLS client to be configured without client authentication
83+
fn is_cvm(&self) -> bool;
84+
85+
/// Verify the given attestation payload
86+
fn verify_attestation(
87+
&self,
88+
input: Vec<u8>,
89+
cert_chain: &[CertificateDer<'_>],
90+
exporter: [u8; 32],
91+
) -> impl Future<Output = Result<Option<Measurements>, AttestationError>> + Send;
92+
}
93+
94+
/// Quote generation using configfs_tsm
95+
#[derive(Clone)]
96+
pub struct DcapTdxQuoteGenerator;
97+
98+
impl QuoteGenerator for DcapTdxQuoteGenerator {
99+
fn is_cvm(&self) -> bool {
100+
true
101+
}
102+
103+
fn create_attestation(
104+
&self,
105+
cert_chain: &[CertificateDer<'_>],
106+
exporter: [u8; 32],
107+
) -> Result<Vec<u8>, AttestationError> {
108+
let quote_input = compute_report_input(cert_chain, exporter)?;
109+
110+
Ok(generate_quote(quote_input)?)
111+
}
112+
}
113+
114+
/// Measurements determined by the CVM platform
115+
#[derive(Clone, PartialEq, Debug)]
116+
pub struct PlatformMeasurements {
117+
pub mrtd: [u8; 48],
118+
pub rtmr0: [u8; 48],
119+
}
120+
121+
impl PlatformMeasurements {
122+
fn from_dcap_qvl_quote(quote: &dcap_qvl::quote::Quote) -> Result<Self, AttestationError> {
123+
let report = match quote.report {
124+
Report::TD10(report) => report,
125+
Report::TD15(report) => report.base,
126+
Report::SgxEnclave(_) => {
127+
return Err(AttestationError::SgxNotSupported);
128+
}
129+
};
130+
Ok(Self {
131+
mrtd: report.mr_td,
132+
rtmr0: report.rt_mr0,
133+
})
134+
}
135+
136+
fn from_tdx_quote(quote: &tdx_quote::Quote) -> Self {
137+
Self {
138+
mrtd: quote.mrtd(),
139+
rtmr0: quote.rtmr0(),
140+
}
141+
}
142+
}
143+
144+
/// Measurements determined by the CVM image
145+
#[derive(Clone, PartialEq, Debug)]
146+
pub struct CvmImageMeasurements {
147+
pub rtmr1: [u8; 48],
148+
pub rtmr2: [u8; 48],
149+
pub rtmr3: [u8; 48],
150+
}
151+
152+
impl CvmImageMeasurements {
153+
fn from_dcap_qvl_quote(quote: &dcap_qvl::quote::Quote) -> Result<Self, AttestationError> {
154+
let report = match quote.report {
155+
Report::TD10(report) => report,
156+
Report::TD15(report) => report.base,
157+
Report::SgxEnclave(_) => {
158+
return Err(AttestationError::SgxNotSupported);
159+
}
160+
};
161+
Ok(Self {
162+
rtmr1: report.rt_mr1,
163+
rtmr2: report.rt_mr2,
164+
rtmr3: report.rt_mr3,
165+
})
166+
}
167+
168+
fn from_tdx_quote(quote: &tdx_quote::Quote) -> Self {
169+
Self {
170+
rtmr1: quote.rtmr1(),
171+
rtmr2: quote.rtmr2(),
172+
rtmr3: quote.rtmr3(),
173+
}
174+
}
175+
}
176+
177+
/// Verify DCAP TDX quotes, allowing them if they have one of a given set of platform-specific and
178+
/// OS image specific measurements
179+
#[derive(Clone)]
180+
pub struct DcapTdxQuoteVerifier {
181+
/// Platform specific allowed Measurements
182+
/// Currently an option as this may be determined internally on a per-platform basis (Eg: GCP)
183+
pub accepted_platform_measurements: Option<Vec<PlatformMeasurements>>,
184+
/// OS-image specific allows measurement - this is effectively a list of allowed OS images
185+
pub accepted_cvm_image_measurements: Vec<CvmImageMeasurements>,
186+
/// URL of a PCCS (defaults to Intel PCS)
187+
pub pccs_url: Option<String>,
188+
}
189+
190+
impl QuoteVerifier for DcapTdxQuoteVerifier {
191+
fn is_cvm(&self) -> bool {
192+
true
193+
}
194+
195+
async fn verify_attestation(
196+
&self,
197+
input: Vec<u8>,
198+
cert_chain: &[CertificateDer<'_>],
199+
exporter: [u8; 32],
200+
) -> Result<Option<Measurements>, AttestationError> {
201+
let quote_input = compute_report_input(cert_chain, exporter)?;
202+
let (platform_measurements, image_measurements) = if cfg!(not(test)) {
203+
let now = std::time::SystemTime::now()
204+
.duration_since(std::time::UNIX_EPOCH)?
205+
.as_secs();
206+
let quote = Quote::parse(&input)?;
207+
208+
let ca = quote.ca()?;
209+
let fmspc = hex::encode_upper(quote.fmspc()?);
210+
let collateral = get_collateral_for_fmspc(
211+
&self.pccs_url.clone().unwrap_or(PCS_URL.to_string()),
212+
fmspc,
213+
ca,
214+
false,
215+
)
216+
.await?;
217+
218+
let _verified_report = dcap_qvl::verify::verify(&input, &collateral, now)?;
219+
220+
let measurements = (
221+
PlatformMeasurements::from_dcap_qvl_quote(&quote)?,
222+
CvmImageMeasurements::from_dcap_qvl_quote(&quote)?,
223+
);
224+
if get_quote_input_data(quote.report) != quote_input {
225+
return Err(AttestationError::InputMismatch);
226+
}
227+
measurements
228+
} else {
229+
// In tests we use mock quotes which will fail to verify
230+
let quote = tdx_quote::Quote::from_bytes(&input)?;
231+
if quote.report_input_data() != quote_input {
232+
return Err(AttestationError::InputMismatch);
233+
}
234+
235+
(
236+
PlatformMeasurements::from_tdx_quote(&quote),
237+
CvmImageMeasurements::from_tdx_quote(&quote),
238+
)
239+
};
240+
241+
if let Some(accepted_platform_measurements) = &self.accepted_platform_measurements
242+
&& !accepted_platform_measurements.contains(&platform_measurements)
243+
{
244+
return Err(AttestationError::UnacceptablePlatformMeasurements);
245+
}
246+
247+
if !self
248+
.accepted_cvm_image_measurements
249+
.contains(&image_measurements)
250+
{
251+
return Err(AttestationError::UnacceptableOsImageMeasurements);
252+
}
253+
254+
Ok(Some(Measurements {
255+
platform: platform_measurements,
256+
cvm_image: image_measurements,
257+
}))
258+
}
259+
}
260+
261+
/// Given a [Report] get the input data regardless of report type
262+
fn get_quote_input_data(report: Report) -> [u8; 64] {
263+
match report {
264+
Report::TD10(r) => r.report_data,
265+
Report::TD15(r) => r.base.report_data,
266+
Report::SgxEnclave(r) => r.report_data,
267+
}
268+
}
269+
270+
/// Given a certificate chain and an exporter (session key material), build the quote input value
271+
/// SHA256(pki) || exporter
272+
pub fn compute_report_input(
273+
cert_chain: &[CertificateDer<'_>],
274+
exporter: [u8; 32],
275+
) -> Result<[u8; 64], AttestationError> {
276+
let mut quote_input = [0u8; 64];
277+
let pki_hash = get_pki_hash_from_certificate_chain(cert_chain)?;
278+
quote_input[..32].copy_from_slice(&pki_hash);
279+
quote_input[32..].copy_from_slice(&exporter);
280+
Ok(quote_input)
281+
}
282+
283+
/// For no CVM platform (eg: for one-sided remote-attested TLS)
284+
#[derive(Clone)]
285+
pub struct NoQuoteGenerator;
286+
287+
impl QuoteGenerator for NoQuoteGenerator {
288+
fn is_cvm(&self) -> bool {
289+
false
290+
}
291+
292+
/// Create an empty attestation
293+
fn create_attestation(
294+
&self,
295+
_cert_chain: &[CertificateDer<'_>],
296+
_exporter: [u8; 32],
297+
) -> Result<Vec<u8>, AttestationError> {
298+
Ok(Vec::new())
299+
}
300+
}
301+
302+
/// For no CVM platform (eg: for one-sided remote-attested TLS)
303+
#[derive(Clone)]
304+
pub struct NoQuoteVerifier;
305+
306+
impl QuoteVerifier for NoQuoteVerifier {
307+
fn is_cvm(&self) -> bool {
308+
false
309+
}
310+
/// Ensure that an empty attestation is given
311+
async fn verify_attestation(
312+
&self,
313+
input: Vec<u8>,
314+
_cert_chain: &[CertificateDer<'_>],
315+
_exporter: [u8; 32],
316+
) -> Result<Option<Measurements>, AttestationError> {
317+
if input.is_empty() {
318+
Ok(None)
319+
} else {
320+
Err(AttestationError::AttestationGivenWhenNoneExpected)
321+
}
322+
}
323+
}
324+
325+
/// Create a mock quote for testing on non-confidential hardware
326+
#[cfg(test)]
327+
fn generate_quote(input: [u8; 64]) -> Result<Vec<u8>, QuoteGenerationError> {
328+
let attestation_key = tdx_quote::SigningKey::random(&mut rand_core::OsRng);
329+
let provisioning_certification_key = tdx_quote::SigningKey::random(&mut rand_core::OsRng);
330+
Ok(tdx_quote::Quote::mock(
331+
attestation_key.clone(),
332+
provisioning_certification_key.clone(),
333+
input,
334+
b"Mock cert chain".to_vec(),
335+
)
336+
.as_bytes())
337+
}
338+
339+
/// Create a quote
340+
#[cfg(not(test))]
341+
fn generate_quote(input: [u8; 64]) -> Result<Vec<u8>, QuoteGenerationError> {
342+
configfs_tsm::create_quote(input)
343+
}
344+
345+
/// Given a certificate chain, get the [Sha256] hash of the public key of the leaf certificate
346+
fn get_pki_hash_from_certificate_chain(
347+
cert_chain: &[CertificateDer<'_>],
348+
) -> Result<[u8; 32], AttestationError> {
349+
let leaf_certificate = cert_chain.first().ok_or(AttestationError::NoCertificate)?;
350+
let (_, cert) = parse_x509_certificate(leaf_certificate.as_ref())?;
351+
let public_key = &cert.tbs_certificate.subject_pki;
352+
let key_bytes = public_key.subject_public_key.as_ref();
353+
354+
let mut hasher = Sha256::new();
355+
hasher.update(key_bytes);
356+
Ok(hasher.finalize().into())
357+
}
358+
359+
/// An error when generating or verifying an attestation
360+
#[derive(Error, Debug)]
361+
pub enum AttestationError {
362+
#[error("Certificate chain is empty")]
363+
NoCertificate,
364+
#[error("X509 parse: {0}")]
365+
X509Parse(#[from] x509_parser::asn1_rs::Err<x509_parser::error::X509Error>),
366+
#[error("X509: {0}")]
367+
X509(#[from] x509_parser::error::X509Error),
368+
#[error("Quote input is not as expected")]
369+
InputMismatch,
370+
#[error("Configuration mismatch - expected no remote attestation")]
371+
AttestationGivenWhenNoneExpected,
372+
#[error("Configfs-tsm quote generation: {0}")]
373+
QuoteGeneration(#[from] configfs_tsm::QuoteGenerationError),
374+
#[error("SGX quote given when TDX quote expected")]
375+
SgxNotSupported,
376+
#[error("Platform measurements do not match any accepted values")]
377+
UnacceptablePlatformMeasurements,
378+
#[error("OS image measurements do not match any accepted values")]
379+
UnacceptableOsImageMeasurements,
380+
#[error("System Time: {0}")]
381+
SystemTime(#[from] SystemTimeError),
382+
#[error("DCAP quote verification: {0}")]
383+
DcapQvl(#[from] anyhow::Error),
384+
#[error("Quote parse: {0}")]
385+
QuoteParse(#[from] QuoteParseError),
386+
}

0 commit comments

Comments
 (0)