Skip to content

Commit dd6bea7

Browse files
authored
Merge pull request #42 from flashbots/peg/local-azure-verification
Verify azure attestation locally - not using MAA API
2 parents 3990753 + d3fb22e commit dd6bea7

File tree

6 files changed

+156
-103
lines changed

6 files changed

+156
-103
lines changed

Cargo.lock

Lines changed: 2 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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ josekit = "0.10.3"
3434
tracing = "0.1.41"
3535
tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json"] }
3636
parity-scale-codec = "3.7.5"
37+
openssl = "0.10.75"
38+
tss-esapi = "7.6.0"
3739

3840
[dev-dependencies]
3941
rcgen = "0.14.5"

src/attestation/azure.rs

Lines changed: 121 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -2,145 +2,154 @@
22
use std::string::FromUtf8Error;
33

44
use az_tdx_vtpm::{hcl, imds, report, vtpm};
5-
use tokio_rustls::rustls::pki_types::CertificateDer;
6-
// use openssl::pkey::{PKey, Public};
75
use base64::{engine::general_purpose::URL_SAFE as BASE64_URL_SAFE, Engine as _};
8-
use reqwest::Client;
9-
use serde::Serialize;
6+
use openssl::pkey::PKey;
7+
use serde::{Deserialize, Serialize};
108
use thiserror::Error;
9+
use tokio_rustls::rustls::pki_types::CertificateDer;
1110

12-
use crate::attestation::{compute_report_input, AttestationError};
11+
use crate::attestation::{
12+
self, compute_report_input,
13+
measurements::{CvmImageMeasurements, Measurements, PlatformMeasurements},
14+
nv_index,
15+
};
1316

14-
// #[derive(Clone)]
15-
// pub struct MaaGenerator {
16-
// }
17+
const TPM_AK_CERT_IDX: u32 = 0x1C101D0;
1718

1819
pub async fn create_azure_attestation(
1920
cert_chain: &[CertificateDer<'_>],
2021
exporter: [u8; 32],
2122
) -> Result<Vec<u8>, MaaError> {
22-
let maa_endpoint = "todo".to_string();
23-
let aad_access_token = "todo".to_string();
2423
let input_data = compute_report_input(cert_chain, exporter)
2524
.map_err(|e| MaaError::InputData(e.to_string()))?;
2625

2726
let td_report = report::get_report()?;
2827

29-
// let mrtd = td_report.tdinfo.mrtd;
30-
// let rtmr0 = td_report.tdinfo.rtrm[0];
31-
// let rtmr1 = td_report.tdinfo.rtrm[1];
32-
// let rtmr2 = td_report.tdinfo.rtrm[2];
33-
// let rtmr3 = td_report.tdinfo.rtrm[3];
34-
3528
// This makes a request to Azure Instance metadata service and gives us a binary response
3629
let td_quote_bytes = imds::get_td_quote(&td_report)?;
3730

3831
let hcl_report_bytes = vtpm::get_report_with_report_data(&input_data)?;
39-
let hcl_report = hcl::HclReport::new(hcl_report_bytes)?;
40-
let hcl_var_data = hcl_report.var_data();
41-
42-
// let bytes = vtpm::get_report().unwrap();
43-
// let hcl_report = hcl::HclReport::new(bytes).unwrap();
44-
// let var_data_hash = hcl_report.var_data_sha256();
45-
// let _ak_pub = hcl_report.ak_pub().unwrap();
46-
//
47-
// let td_report: tdx::TdReport = hcl_report.try_into().unwrap();
48-
// assert!(var_data_hash == td_report.report_mac.reportdata[..32]);
49-
50-
// let nonce = "a nonce".as_bytes();
51-
//
52-
// let tpm_quote = vtpm::get_quote(nonce).unwrap();
53-
// let der = ak_pub.key.try_to_der().unwrap();
54-
// let pub_key = PKey::public_key_from_der(&der).unwrap();
55-
// tpm_quote.verify(&pub_key, nonce).unwrap();
56-
57-
let quote_b64 = BASE64_URL_SAFE.encode(&td_quote_bytes);
58-
let runtime_b64 = BASE64_URL_SAFE.encode(hcl_var_data);
59-
60-
let tdx_vm_request = TdxVmRequest {
61-
quote: quote_b64,
62-
runtime_data: Some(RuntimeData {
63-
data: runtime_b64,
64-
data_type: "Binary",
65-
}),
66-
nonce: Some("my-app-nonce-or-session-id".to_string()), // TODO
67-
};
68-
let jwt_token = call_tdxvm_attestation(maa_endpoint, aad_access_token, &tdx_vm_request).await?;
69-
Ok(jwt_token.as_bytes().to_vec())
70-
}
7132

72-
/// Get a signed JWT from the azure API
73-
async fn call_tdxvm_attestation(
74-
maa_endpoint: String,
75-
aad_access_token: String,
76-
tdx_vm_request: &TdxVmRequest<'_>,
77-
) -> Result<String, MaaError> {
78-
let url = format!("{}/attest/TdxVm?api-version=2025-06-01", maa_endpoint);
79-
80-
let client = Client::new();
81-
let res = client
82-
.post(&url)
83-
.bearer_auth(&aad_access_token)
84-
.header("Content-Type", "application/json")
85-
.body(serde_json::to_vec(tdx_vm_request)?)
86-
.send()
87-
.await?;
88-
89-
let status = res.status();
90-
let text = res.text().await?;
91-
92-
if !status.is_success() {
93-
return Err(MaaError::MaaProvider(status, text));
94-
}
33+
let ak_certificate_der = read_ak_certificate_from_tpm()?;
34+
35+
let tpm_attestation = TpmAttest {
36+
ak_certificate_pem: pem_rfc7468::encode_string(
37+
"CERTIFICATE",
38+
pem_rfc7468::LineEnding::default(),
39+
&ak_certificate_der,
40+
)?,
41+
quote: vtpm::get_quote(&input_data)?,
42+
event_log: Vec::new(),
43+
instance_info: None,
44+
};
9545

96-
#[derive(serde::Deserialize)]
97-
struct AttestationResponse {
98-
token: String,
99-
}
46+
let attestation_document = AttestationDocument {
47+
tdx_quote_base64: BASE64_URL_SAFE.encode(&td_quote_bytes),
48+
hcl_report_base64: BASE64_URL_SAFE.encode(&hcl_report_bytes),
49+
tpm_attestation,
50+
};
10051

101-
let parsed: AttestationResponse = serde_json::from_str(&text)?;
102-
Ok(parsed.token) // Microsoft-signed JWT
52+
Ok(serde_json::to_vec(&attestation_document)?)
10353
}
10454

10555
pub async fn verify_azure_attestation(
10656
input: Vec<u8>,
10757
cert_chain: &[CertificateDer<'_>],
10858
exporter: [u8; 32],
59+
pccs_url: Option<String>,
10960
) -> Result<super::measurements::Measurements, MaaError> {
110-
let _input_data = compute_report_input(cert_chain, exporter)
61+
let input_data = compute_report_input(cert_chain, exporter)
11162
.map_err(|e| MaaError::InputData(e.to_string()))?;
112-
let token = String::from_utf8(input)?;
11363

114-
decode_jwt(&token).await.unwrap();
64+
let attestation_document: AttestationDocument = serde_json::from_slice(&input)?;
11565

116-
todo!()
117-
}
66+
// Verify TDX quote (same as with DCAP) - TODO deduplicate this code
67+
let now = std::time::SystemTime::now()
68+
.duration_since(std::time::UNIX_EPOCH)
69+
.unwrap()
70+
.as_secs();
71+
72+
let tdx_quote_bytes = BASE64_URL_SAFE
73+
.decode(attestation_document.tdx_quote_base64)
74+
.unwrap();
75+
76+
let quote = dcap_qvl::quote::Quote::parse(&tdx_quote_bytes).unwrap();
77+
78+
let ca = quote.ca().unwrap();
79+
let fmspc = hex::encode_upper(quote.fmspc().unwrap());
80+
let collateral = dcap_qvl::collateral::get_collateral_for_fmspc(
81+
&pccs_url
82+
.clone()
83+
.unwrap_or(attestation::dcap::PCS_URL.to_string()),
84+
fmspc,
85+
ca,
86+
false, // Indicates not SGX
87+
)
88+
.await
89+
.unwrap();
11890

119-
async fn decode_jwt(token: &str) -> Result<(), AttestationError> {
120-
// Parse payload (claims) without verification (TODO this will be swapped out once we have the
121-
// key-getting logic)
122-
let parts: Vec<&str> = token.split('.').collect();
123-
let claims_json = BASE64_URL_SAFE.decode(parts[1]).unwrap();
91+
let _verified_report = dcap_qvl::verify::verify(&input, &collateral, now).unwrap();
92+
93+
// Check that hcl_report_bytes (hashed?) matches TDX quote report data
94+
// if get_quote_input_data(quote.report) != quote_input {
95+
// return Err(AttestationError::InputMismatch);
96+
// }
97+
98+
let hcl_report_bytes = BASE64_URL_SAFE
99+
.decode(attestation_document.hcl_report_base64)
100+
.unwrap();
101+
102+
let hcl_report = hcl::HclReport::new(hcl_report_bytes)?;
103+
let var_data_hash = hcl_report.var_data_sha256();
104+
let hcl_ak_pub = hcl_report.ak_pub()?;
105+
let td_report: az_tdx_vtpm::tdx::TdReport = hcl_report.try_into()?;
106+
assert!(var_data_hash == td_report.report_mac.reportdata[..32]);
107+
108+
let vtpm_quote = attestation_document.tpm_attestation.quote;
109+
let hcl_ak_pub_der = hcl_ak_pub.key.try_to_der().unwrap();
110+
let pub_key = PKey::public_key_from_der(&hcl_ak_pub_der).unwrap();
111+
vtpm_quote.verify(&pub_key, &input_data)?;
112+
let _pcrs = vtpm_quote.pcrs_sha256();
113+
114+
// TODO parse AK certificate
115+
// Check that AK public key matches that from TPM quote
116+
// Verify AK certificate against microsoft root cert
117+
118+
Ok(Measurements {
119+
platform: PlatformMeasurements::from_dcap_qvl_quote(&quote).unwrap(),
120+
cvm_image: CvmImageMeasurements::from_dcap_qvl_quote(&quote).unwrap(),
121+
})
122+
}
124123

125-
let claims: serde_json::Value = serde_json::from_slice(&claims_json).unwrap();
126-
println!("{claims}");
127-
Ok(())
124+
/// The attestation evidence payload that gets sent over the channel
125+
#[derive(Debug, Serialize, Deserialize)]
126+
struct AttestationDocument {
127+
/// TDX quote from the IMDS
128+
tdx_quote_base64: String,
129+
/// Serialized HCL report
130+
hcl_report_base64: String,
131+
/// vTPM related evidence
132+
tpm_attestation: TpmAttest,
128133
}
129134

130-
#[derive(Serialize)]
131-
struct RuntimeData<'a> {
132-
data: String, // base64url of VarData bytes
133-
#[serde(rename = "dataType")]
134-
data_type: &'a str, // "Binary" in our case
135+
#[derive(Debug, Serialize, Deserialize)]
136+
struct TpmAttest {
137+
/// Attestation Key certificate from vTPM
138+
ak_certificate_pem: String,
139+
/// vTPM quotes over the selected PCR bank(s).
140+
quote: vtpm::Quote,
141+
/// Raw TCG event log bytes (UEFI + IMA)
142+
///
143+
/// `/sys/kernel/security/ima/ascii_runtime_measurements`,
144+
/// `/sys/kernel/security/tpm0/binary_bios_measurements`,
145+
event_log: Vec<u8>,
146+
/// Optional platform / instance metadata used to bind or verify the AK
147+
instance_info: Option<Vec<u8>>,
135148
}
136149

137-
#[derive(Serialize)]
138-
struct TdxVmRequest<'a> {
139-
quote: String, // base64 (TDX quote)
140-
#[serde(rename = "runtimeData", skip_serializing_if = "Option::is_none")]
141-
runtime_data: Option<RuntimeData<'a>>,
142-
#[serde(skip_serializing_if = "Option::is_none")]
143-
nonce: Option<String>,
150+
fn read_ak_certificate_from_tpm() -> Result<Vec<u8>, tss_esapi::Error> {
151+
let mut context = nv_index::get_session_context()?;
152+
nv_index::read_nv_index(&mut context, TPM_AK_CERT_IDX)
144153
}
145154

146155
#[derive(Error, Debug)]
@@ -163,6 +172,16 @@ pub enum MaaError {
163172
MaaProvider(http::StatusCode, String),
164173
#[error("Token is bad UTF8: {0}")]
165174
BadUtf8(#[from] FromUtf8Error),
175+
#[error("vTPM quote: {0}")]
176+
VtpmQuote(#[from] vtpm::QuoteError),
177+
#[error("AK public key: {0}")]
178+
AkPub(#[from] vtpm::AKPubError),
179+
#[error("vTPM quote could not be verified: {0}")]
180+
TpmQuoteVerify(#[from] vtpm::VerifyError),
181+
#[error("vTPM read: {0}")]
182+
TssEsapi(#[from] tss_esapi::Error),
183+
#[error("PEM encode: {0}")]
184+
Pem(#[from] pem_rfc7468::Error),
166185
}
167186

168187
#[cfg(test)]

src/attestation/dcap.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use dcap_qvl::{
1313
use tokio_rustls::rustls::pki_types::CertificateDer;
1414

1515
/// For fetching collateral directly from Intel, if no PCCS is specified
16-
const PCS_URL: &str = "https://api.trustedservices.intel.com";
16+
pub const PCS_URL: &str = "https://api.trustedservices.intel.com";
1717

1818
/// Quote generation using configfs_tsm
1919
pub async fn create_dcap_attestation(

src/attestation/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pub mod azure;
22
pub mod dcap;
33
pub mod measurements;
4+
pub mod nv_index;
45

56
use measurements::{MeasurementRecord, Measurements};
67
use parity_scale_codec::{Decode, Encode};
@@ -199,6 +200,7 @@ impl AttestationVerifier {
199200
attestation_payload.attestation,
200201
cert_chain,
201202
exporter,
203+
self.pccs_url.clone(),
202204
)
203205
.await?
204206
}

src/attestation/nv_index.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
use tss_esapi::{
2+
handles::NvIndexHandle,
3+
interface_types::{resource_handles::NvAuth, session_handles::AuthSession},
4+
structures::MaxNvBuffer,
5+
tcti_ldr::{DeviceConfig, TctiNameConf},
6+
Context,
7+
};
8+
9+
pub fn get_session_context() -> Result<Context, tss_esapi::Error> {
10+
let conf: TctiNameConf = TctiNameConf::Device(DeviceConfig::default());
11+
let mut context = Context::new(conf)?;
12+
let auth_session = AuthSession::Password;
13+
context.set_sessions((Some(auth_session), None, None));
14+
Ok(context)
15+
}
16+
17+
pub fn read_nv_index(ctx: &mut Context, index: u32) -> Result<Vec<u8>, tss_esapi::Error> {
18+
let handle = NvIndexHandle::from(index);
19+
let size = ctx
20+
.nv_read_public(handle)?
21+
.0
22+
.data_size()
23+
.try_into()
24+
.unwrap_or(0u16);
25+
26+
let data: MaxNvBuffer = ctx.nv_read(NvAuth::Owner, handle, size, 0)?;
27+
Ok(data.to_vec())
28+
}

0 commit comments

Comments
 (0)