use sha2::{Digest, Sha384};
use crate::near::report::{AttestationInfo, DstackEvent};
pub fn replay_rtmr(events: &[DstackEvent], imr: u32) -> Option<[u8; 48]> {
let mut mr = vec![0u8; 48];
for event in events.iter().filter(|e| e.imr == imr) {
let mut digest = hex::decode(&event.digest).ok()?;
if digest.len() < 48 {
digest.resize(48, 0);
}
mr.extend_from_slice(&digest);
mr = Sha384::digest(&mr).to_vec();
}
mr.as_slice().try_into().ok()
}
fn event_payload<'a>(events: &'a [DstackEvent], name: &str) -> Option<&'a str> {
events
.iter()
.take_while(|e| !(e.imr == 3 && e.event == "system-ready"))
.find(|e| e.imr == 3 && e.event == name)
.map(|e| e.event_payload.as_str())
}
pub fn event_log_binds_info(
events: &[DstackEvent],
quote_rtmr3: &[u8; 48],
info: &AttestationInfo,
) -> bool {
match replay_rtmr(events, 3) {
Some(replayed) if &replayed == quote_rtmr3 => {}
_ => return false,
}
let app_id_ok =
event_payload(events, "app-id").is_some_and(|p| p.eq_ignore_ascii_case(&info.app_id));
let compose_ok = event_payload(events, "compose-hash")
.is_some_and(|p| p.eq_ignore_ascii_case(&info.compose_hash));
let os_ok = event_payload(events, "os-image-hash")
.is_some_and(|p| p.eq_ignore_ascii_case(&info.os_image_hash));
let kms_ok = event_payload(events, "key-provider")
.and_then(|p| hex::decode(p).ok())
.and_then(|b| String::from_utf8(b).ok())
.is_some_and(|s| s == info.key_provider_info);
app_id_ok && compose_ok && os_ok && kms_ok
}
#[cfg(test)]
mod tests {
use super::*;
use crate::near::report::AttestationReport;
use crate::near::tdx::parse_tdx_quote;
const FIXTURE: &str = include_str!("../../tests/fixtures/near_report.json");
fn fixture() -> AttestationReport {
serde_json::from_str(FIXTURE).unwrap()
}
#[test]
fn event_log_replays_to_the_quote_rtmr3_and_binds_info() {
let r = fixture();
let m = &r.model_attestations[0];
let measurements = parse_tdx_quote(&hex::decode(&m.intel_quote).unwrap()).unwrap();
assert_eq!(replay_rtmr(&m.event_log, 3), Some(measurements.rtmr3));
assert!(event_log_binds_info(
&m.event_log,
&measurements.rtmr3,
&m.info
));
}
#[test]
fn rejects_when_rtmr3_does_not_match_the_quote() {
let r = fixture();
let m = &r.model_attestations[0];
assert!(!event_log_binds_info(&m.event_log, &[0u8; 48], &m.info));
}
#[test]
fn rejects_when_info_is_forged_against_a_genuine_log() {
let r = fixture();
let m = &r.model_attestations[0];
let measurements = parse_tdx_quote(&hex::decode(&m.intel_quote).unwrap()).unwrap();
let mut info = m.info.clone();
info.app_id = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef".to_string();
assert!(!event_log_binds_info(
&m.event_log,
&measurements.rtmr3,
&info
));
}
#[test]
fn rejects_a_tampered_event_digest() {
let r = fixture();
let mut m = r.model_attestations[0].clone();
let measurements = parse_tdx_quote(&hex::decode(&m.intel_quote).unwrap()).unwrap();
if let Some(ev) = m
.event_log
.iter_mut()
.find(|e| e.imr == 3 && e.event == "app-id")
{
ev.digest = "00".repeat(48);
}
assert!(!event_log_binds_info(
&m.event_log,
&measurements.rtmr3,
&m.info
));
}
}