Skip to content

Commit cfbeab0

Browse files
committed
Verify azure attestation locally - not using MAA API
1 parent 3990753 commit cfbeab0

File tree

5 files changed

+107
-102
lines changed

5 files changed

+107
-102
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ 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"
3738

3839
[dev-dependencies]
3940
rcgen = "0.14.5"

src/attestation/azure.rs

Lines changed: 103 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -2,145 +2,141 @@
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};
13-
14-
// #[derive(Clone)]
15-
// pub struct MaaGenerator {
16-
// }
11+
use crate::attestation::{
12+
self, compute_report_input,
13+
measurements::{CvmImageMeasurements, Measurements, PlatformMeasurements},
14+
};
1715

1816
pub async fn create_azure_attestation(
1917
cert_chain: &[CertificateDer<'_>],
2018
exporter: [u8; 32],
2119
) -> Result<Vec<u8>, MaaError> {
22-
let maa_endpoint = "todo".to_string();
23-
let aad_access_token = "todo".to_string();
2420
let input_data = compute_report_input(cert_chain, exporter)
2521
.map_err(|e| MaaError::InputData(e.to_string()))?;
2622

2723
let td_report = report::get_report()?;
2824

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-
3525
// This makes a request to Azure Instance metadata service and gives us a binary response
3626
let td_quote_bytes = imds::get_td_quote(&td_report)?;
3727

3828
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();
4129

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]);
30+
// let quote_b64 = ;
31+
// let runtime_b64 = BASE64_URL_SAFE.encode(hcl_var_data);
4932

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
33+
let tpm_attestation = TpmAttest {
34+
ak_pub: vtpm::get_ak_pub()?,
35+
quote: vtpm::get_quote(&input_data)?,
36+
event_log: Vec::new(),
37+
instance_info: None,
6738
};
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-
}
7139

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-
}
95-
96-
#[derive(serde::Deserialize)]
97-
struct AttestationResponse {
98-
token: String,
99-
}
40+
let attestation_document = AttestationDocument {
41+
tdx_quote_base64: BASE64_URL_SAFE.encode(&td_quote_bytes),
42+
hcl_report_base64: BASE64_URL_SAFE.encode(&hcl_report_bytes),
43+
tpm_attestation,
44+
};
10045

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

10549
pub async fn verify_azure_attestation(
10650
input: Vec<u8>,
10751
cert_chain: &[CertificateDer<'_>],
10852
exporter: [u8; 32],
53+
pccs_url: Option<String>,
10954
) -> Result<super::measurements::Measurements, MaaError> {
110-
let _input_data = compute_report_input(cert_chain, exporter)
55+
let input_data = compute_report_input(cert_chain, exporter)
11156
.map_err(|e| MaaError::InputData(e.to_string()))?;
112-
let token = String::from_utf8(input)?;
11357

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

116-
todo!()
117-
}
60+
// Verify TDX quote (same as with DCAP) - TODO deduplicate this code
61+
let now = std::time::SystemTime::now()
62+
.duration_since(std::time::UNIX_EPOCH)
63+
.unwrap()
64+
.as_secs();
65+
66+
let tdx_quote_bytes = BASE64_URL_SAFE
67+
.decode(attestation_document.tdx_quote_base64)
68+
.unwrap();
69+
70+
let quote = dcap_qvl::quote::Quote::parse(&tdx_quote_bytes).unwrap();
11871

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();
72+
let ca = quote.ca().unwrap();
73+
let fmspc = hex::encode_upper(quote.fmspc().unwrap());
74+
let collateral = dcap_qvl::collateral::get_collateral_for_fmspc(
75+
&pccs_url
76+
.clone()
77+
.unwrap_or(attestation::dcap::PCS_URL.to_string()),
78+
fmspc,
79+
ca,
80+
false, // Indicates not SGX
81+
)
82+
.await
83+
.unwrap();
12484

125-
let claims: serde_json::Value = serde_json::from_slice(&claims_json).unwrap();
126-
println!("{claims}");
127-
Ok(())
85+
let _verified_report = dcap_qvl::verify::verify(&input, &collateral, now).unwrap();
86+
87+
// Check that hcl_report_bytes (hashed?) matches TDX quote report data
88+
// if get_quote_input_data(quote.report) != quote_input {
89+
// return Err(AttestationError::InputMismatch);
90+
// }
91+
92+
let hcl_report_bytes = BASE64_URL_SAFE
93+
.decode(attestation_document.hcl_report_base64)
94+
.unwrap();
95+
96+
let hcl_report = hcl::HclReport::new(hcl_report_bytes)?;
97+
//
98+
let var_data_hash = hcl_report.var_data_sha256();
99+
let hcl_ak_pub = hcl_report.ak_pub()?;
100+
let td_report: az_tdx_vtpm::tdx::TdReport = hcl_report.try_into()?;
101+
assert!(var_data_hash == td_report.report_mac.reportdata[..32]);
102+
103+
let vtpm_quote = attestation_document.tpm_attestation.quote;
104+
let hcl_ak_pub_der = hcl_ak_pub.key.try_to_der().unwrap();
105+
let pub_key = PKey::public_key_from_der(&hcl_ak_pub_der).unwrap();
106+
vtpm_quote.verify(&pub_key, &input_data)?;
107+
let _pcrs = vtpm_quote.pcrs_sha256();
108+
109+
Ok(Measurements {
110+
platform: PlatformMeasurements::from_dcap_qvl_quote(&quote).unwrap(),
111+
cvm_image: CvmImageMeasurements::from_dcap_qvl_quote(&quote).unwrap(),
112+
})
128113
}
129114

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
115+
/// The attestation evidence payload that gets sent over the channel
116+
#[derive(Debug, Serialize, Deserialize)]
117+
struct AttestationDocument {
118+
/// TDX quote from the IMDS
119+
tdx_quote_base64: String,
120+
/// Serialized HCL report
121+
hcl_report_base64: String,
122+
/// vTPM related evidence
123+
tpm_attestation: TpmAttest,
135124
}
136125

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>,
126+
#[derive(Debug, Serialize, Deserialize)]
127+
pub struct TpmAttest {
128+
/// vTPM Attestation Key (AK) public key
129+
// TODO do we need this? it is already given in HCL report
130+
pub ak_pub: vtpm::PublicKey,
131+
/// vTPM quotes over the selected PCR bank(s).
132+
pub quote: vtpm::Quote,
133+
/// Raw TCG event log bytes (UEFI + IMA)
134+
///
135+
/// `/sys/kernel/security/ima/ascii_runtime_measurements`,
136+
/// `/sys/kernel/security/tpm0/binary_bios_measurements`,
137+
pub event_log: Vec<u8>,
138+
/// Optional platform / instance metadata used to bind or verify the AK
139+
pub instance_info: Option<Vec<u8>>,
144140
}
145141

146142
#[derive(Error, Debug)]
@@ -163,6 +159,12 @@ pub enum MaaError {
163159
MaaProvider(http::StatusCode, String),
164160
#[error("Token is bad UTF8: {0}")]
165161
BadUtf8(#[from] FromUtf8Error),
162+
#[error("vTPM quote: {0}")]
163+
VtpmQuote(#[from] vtpm::QuoteError),
164+
#[error("AK public key: {0}")]
165+
AkPub(#[from] vtpm::AKPubError),
166+
#[error("vTPM quote could not be verified: {0}")]
167+
TpmQuoteVerify(#[from] vtpm::VerifyError),
166168
}
167169

168170
#[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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ impl AttestationVerifier {
199199
attestation_payload.attestation,
200200
cert_chain,
201201
exporter,
202+
self.pccs_url.clone(),
202203
)
203204
.await?
204205
}

0 commit comments

Comments
 (0)