pub mod reportdata;
#[cfg(feature = "dcap")]
pub mod dcap;
#[cfg(feature = "dcap-fetch")]
pub mod collateral;
#[cfg(all(feature = "client", feature = "dcap-fetch"))]
pub mod live;
use reportdata::{CurveTag, expected_reportdata};
use std::fmt;
pub const MEASUREMENT_LEN: usize = 48;
pub type Measurement = [u8; MEASUREMENT_LEN];
#[derive(Clone, Debug)]
pub struct VerifiedQuote {
pub mr_td: Measurement,
pub rt_mr: [Measurement; 4],
pub mr_seam: Measurement,
pub mr_signer_seam: Measurement,
pub td_attributes: [u8; 8],
pub xfam: [u8; 8],
pub report_data: [u8; 64],
}
#[derive(Clone, Default)]
pub struct MeasurementPolicy {
pub mr_td: Option<Measurement>,
pub rt_mr: [Option<Measurement>; 4],
pub mr_seam: Option<Measurement>,
pub mr_signer_seam: Option<Measurement>,
pub td_attributes: Option<[u8; 8]>,
pub xfam: Option<[u8; 8]>,
}
impl MeasurementPolicy {
fn check(&self, q: &VerifiedQuote) -> Result<(), VerifyError> {
fn pin<const N: usize>(
field: &'static str,
want: &Option<[u8; N]>,
got: &[u8; N],
) -> Result<(), VerifyError> {
match want {
Some(w) if w != got => Err(VerifyError::MeasurementMismatch(field)),
_ => Ok(()),
}
}
pin("mr_td", &self.mr_td, &q.mr_td)?;
pin("rt_mr0", &self.rt_mr[0], &q.rt_mr[0])?;
pin("rt_mr1", &self.rt_mr[1], &q.rt_mr[1])?;
pin("rt_mr2", &self.rt_mr[2], &q.rt_mr[2])?;
pin("rt_mr3", &self.rt_mr[3], &q.rt_mr[3])?;
pin("mr_seam", &self.mr_seam, &q.mr_seam)?;
pin("mr_signer_seam", &self.mr_signer_seam, &q.mr_signer_seam)?;
pin("td_attributes", &self.td_attributes, &q.td_attributes)?;
pin("xfam", &self.xfam, &q.xfam)?;
Ok(())
}
}
#[derive(Clone, Default)]
pub struct ExpectedReportData {
pub pubkeys: Vec<(CurveTag, Vec<u8>)>,
pub image_digests: Vec<u8>,
pub report_data: Vec<u8>,
}
pub trait QuoteVerifier {
fn verify_quote(&self, raw_quote: &[u8]) -> Result<VerifiedQuote, VerifyError>;
}
pub fn verify_attestation(
raw_quote: &[u8],
verifier: &dyn QuoteVerifier,
policy: &MeasurementPolicy,
expected: &ExpectedReportData,
) -> Result<VerifiedQuote, VerifyError> {
if raw_quote.is_empty() {
return Err(VerifyError::EmptyQuote);
}
let quote = verifier.verify_quote(raw_quote)?;
policy.check("e)?;
let expected_rd = expected_reportdata(
&expected.pubkeys,
&expected.image_digests,
&expected.report_data,
);
if quote.report_data != expected_rd {
return Err(VerifyError::ReportDataMismatch);
}
Ok(quote)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VerifyError {
EmptyQuote,
QuoteVerification(String),
MeasurementMismatch(&'static str),
ReportDataMismatch,
Collateral(String),
Transport(String),
}
impl fmt::Display for VerifyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
VerifyError::EmptyQuote => {
write!(
f,
"attestation rejected: empty raw_quote (signer produced no TD Quote)"
)
}
VerifyError::QuoteVerification(e) => {
write!(
f,
"attestation rejected: DCAP/QVL quote verification failed: {e}"
)
}
VerifyError::MeasurementMismatch(field) => {
write!(
f,
"attestation rejected: measurement {field} does not match policy"
)
}
VerifyError::ReportDataMismatch => write!(
f,
"attestation rejected: REPORTDATA mismatch — quote does not bind the expected \
pubkeys/images/report_data"
),
VerifyError::Collateral(e) => {
write!(
f,
"attestation rejected: could not fetch DCAP collateral: {e}"
)
}
VerifyError::Transport(e) => {
write!(f, "attestation rejected: could not fetch attestation: {e}")
}
}
}
}
impl std::error::Error for VerifyError {}
#[cfg(test)]
mod tests {
use super::*;
fn meas(b: u8) -> Measurement {
[b; MEASUREMENT_LEN]
}
struct StubVerifier(VerifiedQuote);
impl QuoteVerifier for StubVerifier {
fn verify_quote(&self, _raw_quote: &[u8]) -> Result<VerifiedQuote, VerifyError> {
Ok(self.0.clone())
}
}
fn quote_with_reportdata(report_data: [u8; 64]) -> VerifiedQuote {
VerifiedQuote {
mr_td: meas(0x11),
rt_mr: [meas(0x20), meas(0x21), meas(0x22), meas(0x23)],
mr_seam: meas(0x30),
mr_signer_seam: meas(0x31),
td_attributes: [0u8; 8],
xfam: [0u8; 8],
report_data,
}
}
fn expected() -> ExpectedReportData {
ExpectedReportData {
pubkeys: vec![(CurveTag::Secp256k1, b"pubkey-evm".to_vec())],
image_digests: b"img".to_vec(),
report_data: b"nonce".to_vec(),
}
}
#[test]
fn accepts_matching_quote() {
let exp = expected();
let rd = expected_reportdata(&exp.pubkeys, &exp.image_digests, &exp.report_data);
let v = StubVerifier(quote_with_reportdata(rd));
let policy = MeasurementPolicy {
mr_td: Some(meas(0x11)),
rt_mr: [Some(meas(0x20)), None, None, None],
..Default::default()
};
assert!(verify_attestation(b"raw", &v, &policy, &exp).is_ok());
}
#[test]
fn rejects_reportdata_mismatch() {
let exp = expected();
let wrong = expected_reportdata(&exp.pubkeys, &exp.image_digests, b"different-nonce");
let v = StubVerifier(quote_with_reportdata(wrong));
let err = verify_attestation(b"raw", &v, &MeasurementPolicy::default(), &exp).unwrap_err();
assert_eq!(err, VerifyError::ReportDataMismatch);
}
#[test]
fn rejects_measurement_mismatch() {
let exp = expected();
let rd = expected_reportdata(&exp.pubkeys, &exp.image_digests, &exp.report_data);
let v = StubVerifier(quote_with_reportdata(rd));
let policy = MeasurementPolicy {
mr_td: Some(meas(0xFF)), ..Default::default()
};
let err = verify_attestation(b"raw", &v, &policy, &exp).unwrap_err();
assert_eq!(err, VerifyError::MeasurementMismatch("mr_td"));
}
#[test]
fn rejects_empty_quote_before_calling_verifier() {
struct Panicking;
impl QuoteVerifier for Panicking {
fn verify_quote(&self, _: &[u8]) -> Result<VerifiedQuote, VerifyError> {
panic!("must not be called for an empty quote");
}
}
let err = verify_attestation(b"", &Panicking, &MeasurementPolicy::default(), &expected())
.unwrap_err();
assert_eq!(err, VerifyError::EmptyQuote);
}
#[test]
fn rejects_unpinned_is_allowed_but_pinned_must_match() {
let exp = expected();
let rd = expected_reportdata(&exp.pubkeys, &exp.image_digests, &exp.report_data);
let v = StubVerifier(quote_with_reportdata(rd));
assert!(verify_attestation(b"raw", &v, &MeasurementPolicy::default(), &exp).is_ok());
}
}