attestation_verifier/
lib.rs1#![doc = include_str!("../README.md")]
2
3mod error;
4
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use base64::{engine::general_purpose, Engine as _};
8use dcap_qvl::collateral::get_collateral;
9use dcap_qvl::quote::Quote;
10use dcap_qvl::verify::ring::verify;
11pub use dcap_qvl::PHALA_PCCS_URL;
12use serde::Deserialize;
13
14pub use error::{AttestationError, Result};
15
16const MIN_QUOTE_LEN: usize = 48;
17pub const REPORT_DATA_LEN: usize = 64;
18pub const TDX_TEE_TYPE: u32 = 0x0000_0081;
19
20#[derive(Deserialize)]
21struct TdxQuoteBody {
22 quote: String,
23}
24
25#[derive(Deserialize)]
26struct TdxQuoteEnvelope {
27 tdx: TdxQuoteBody,
28}
29
30#[derive(Clone, Debug, Eq, PartialEq)]
35pub struct Verifier {
36 pccs_url: String,
37}
38
39impl Default for Verifier {
40 fn default() -> Self {
41 Self::from_env()
42 }
43}
44
45impl Verifier {
46 pub fn new(pccs_url: impl Into<String>) -> Self {
48 Self {
49 pccs_url: pccs_url.into(),
50 }
51 }
52
53 pub fn from_env() -> Self {
55 let pccs_url = std::env::var("PCCS_URL").unwrap_or_else(|_| PHALA_PCCS_URL.to_string());
56 Self::new(pccs_url)
57 }
58
59 pub fn pccs_url(&self) -> &str {
61 &self.pccs_url
62 }
63
64 pub async fn verify_quote(&self, quote: &[u8]) -> Result<()> {
66 self.verify_quote_at(quote, current_unix_time()?).await
67 }
68
69 pub async fn verify_quote_at(&self, quote: &[u8], unix_time: u64) -> Result<()> {
71 ensure_tdx_quote(quote)?;
72
73 let collateral = get_collateral(&self.pccs_url, quote)
74 .await
75 .map_err(|error| AttestationError::CollateralFetch(error.to_string()))?;
76
77 verify(quote, &collateral, unix_time)
78 .map_err(|error| AttestationError::Verification(error.to_string()))?;
79
80 Ok(())
81 }
82
83 pub async fn verify_quote_hex(&self, quote_hex: &str) -> Result<()> {
85 let quote = decode_quote_hex(quote_hex)?;
86 self.verify_quote("e).await
87 }
88
89 pub async fn verify_quote_base64(&self, quote_base64: &str) -> Result<()> {
91 let quote = decode_quote_base64(quote_base64)?;
92 self.verify_quote("e).await
93 }
94
95 pub async fn verify_tdx_quote_json(&self, tdx_quote_json: &str) -> Result<()> {
97 let quote = decode_tdx_quote_json(tdx_quote_json)?;
98 self.verify_quote("e).await
99 }
100
101 pub async fn verify_tdx_quote_json_hex(&self, tdx_quote_json_hex: &str) -> Result<()> {
104 let quote = decode_tdx_quote_json_hex(tdx_quote_json_hex)?;
105 self.verify_quote("e).await
106 }
107}
108
109pub async fn verify_quote(quote: &[u8]) -> Result<()> {
111 Verifier::default().verify_quote(quote).await
112}
113
114pub async fn verify_quote_hex(quote_hex: &str) -> Result<()> {
116 Verifier::default().verify_quote_hex(quote_hex).await
117}
118
119pub async fn verify_quote_base64(quote_base64: &str) -> Result<()> {
121 Verifier::default().verify_quote_base64(quote_base64).await
122}
123
124pub async fn verify_tdx_quote_json(tdx_quote_json: &str) -> Result<()> {
127 Verifier::default()
128 .verify_tdx_quote_json(tdx_quote_json)
129 .await
130}
131
132pub async fn verify_tdx_quote_json_hex(tdx_quote_json_hex: &str) -> Result<()> {
135 Verifier::default()
136 .verify_tdx_quote_json_hex(tdx_quote_json_hex)
137 .await
138}
139
140pub fn extract_report_data(quote: &[u8]) -> Result<[u8; REPORT_DATA_LEN]> {
142 ensure_tdx_quote(quote)?;
143
144 let parsed_quote =
145 Quote::parse(quote).map_err(|error| AttestationError::QuoteParse(error.to_string()))?;
146 let report = parsed_quote.report.as_td10().ok_or_else(|| {
147 AttestationError::QuoteParse("quote does not contain a TDX report body".to_string())
148 })?;
149
150 Ok(report.report_data)
151}
152
153pub fn extract_report_data_hex(quote: &[u8]) -> Result<String> {
155 Ok(hex::encode(extract_report_data(quote)?))
156}
157
158pub fn decode_quote_hex(quote_hex: &str) -> Result<Vec<u8>> {
160 Ok(hex::decode(normalize_hex(quote_hex))?)
161}
162
163pub fn decode_quote_base64(quote_base64: &str) -> Result<Vec<u8>> {
165 Ok(general_purpose::STANDARD.decode(quote_base64.trim())?)
166}
167
168pub fn decode_tdx_quote_json(tdx_quote_json: &str) -> Result<Vec<u8>> {
170 let envelope: TdxQuoteEnvelope = serde_json::from_str(tdx_quote_json)?;
171 decode_quote_base64(&envelope.tdx.quote)
172}
173
174pub fn decode_tdx_quote_json_hex(tdx_quote_json_hex: &str) -> Result<Vec<u8>> {
176 let json_bytes = decode_quote_hex(tdx_quote_json_hex)?;
177 let json = String::from_utf8(json_bytes)?;
178 decode_tdx_quote_json(&json)
179}
180
181fn normalize_hex(input: &str) -> &str {
182 let trimmed = input.trim();
183 trimmed
184 .strip_prefix("0x")
185 .or_else(|| trimmed.strip_prefix("0X"))
186 .unwrap_or(trimmed)
187}
188
189fn current_unix_time() -> Result<u64> {
190 Ok(SystemTime::now()
191 .duration_since(UNIX_EPOCH)
192 .map_err(|error| AttestationError::SystemTime(error.to_string()))?
193 .as_secs())
194}
195
196fn ensure_tdx_quote(quote: &[u8]) -> Result<()> {
197 if quote.len() < MIN_QUOTE_LEN {
198 return Err(AttestationError::QuoteTooShort {
199 expected: MIN_QUOTE_LEN,
200 actual: quote.len(),
201 });
202 }
203
204 let tee_type = u32::from_le_bytes([quote[4], quote[5], quote[6], quote[7]]);
205 if tee_type != TDX_TEE_TYPE {
206 return Err(AttestationError::InvalidTeeType { actual: tee_type });
207 }
208
209 Ok(())
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use serde_json::Value;
216
217 #[test]
218 fn decode_quote_hex_accepts_0x_prefix() {
219 let quote = decode_quote_hex("0x00010203").unwrap();
220 assert_eq!(quote, vec![0x00, 0x01, 0x02, 0x03]);
221 }
222
223 #[test]
224 fn decode_tdx_quote_json_hex_extracts_raw_quote() {
225 let example: Value = serde_json::from_str(include_str!("../example.json")).unwrap();
226 let wrapped_quote_hex = example
227 .pointer("/tdx_attestation/quote_hex")
228 .and_then(Value::as_str)
229 .unwrap();
230
231 let quote = decode_tdx_quote_json_hex(wrapped_quote_hex).unwrap();
232
233 assert!(quote.len() > MIN_QUOTE_LEN);
234 assert_eq!(
235 u32::from_le_bytes([quote[4], quote[5], quote[6], quote[7]]),
236 TDX_TEE_TYPE
237 );
238 }
239
240 #[test]
241 fn extract_report_data_hex_matches_example_json() {
242 let example: Value = serde_json::from_str(include_str!("../example.json")).unwrap();
243 let wrapped_quote_hex = example
244 .pointer("/tdx_attestation/quote_hex")
245 .and_then(Value::as_str)
246 .unwrap();
247 let quote = decode_tdx_quote_json_hex(wrapped_quote_hex).unwrap();
248 let report_data = extract_report_data_hex("e).unwrap();
249
250 assert_eq!(
251 report_data,
252 "ca5724112df93dc603b0df5568cc9cb850dec4492a8a6867ae5c798a0a4ca5a66968d64bfc078cd067317c6e54a5237bfaf413919f9a4bdf9692899fbf72ac19"
253 );
254 }
255
256 #[test]
257 fn extracted_report_data_differs_from_example_metadata_field() {
258 let example: Value = serde_json::from_str(include_str!("../example.json")).unwrap();
259 let wrapped_quote_hex = example
260 .pointer("/tdx_attestation/quote_hex")
261 .and_then(Value::as_str)
262 .unwrap();
263 let metadata_report_data = example
264 .pointer("/tdx_attestation/reportdata_hex")
265 .and_then(Value::as_str)
266 .unwrap();
267 let quote = decode_tdx_quote_json_hex(wrapped_quote_hex).unwrap();
268 let quote_report_data = extract_report_data_hex("e).unwrap();
269
270 assert_ne!(quote_report_data, metadata_report_data);
271 }
272
273 #[test]
274 fn verify_quote_rejects_short_quote() {
275 let result = ensure_tdx_quote(&[0u8; 16]);
276 assert!(matches!(
277 result,
278 Err(AttestationError::QuoteTooShort {
279 expected: MIN_QUOTE_LEN,
280 actual: 16
281 })
282 ));
283 }
284
285 #[test]
286 fn verify_quote_rejects_wrong_tee_type() {
287 let mut quote = vec![0u8; MIN_QUOTE_LEN];
288 quote[4..8].copy_from_slice(&0u32.to_le_bytes());
289
290 let result = ensure_tdx_quote("e);
291 assert!(matches!(
292 result,
293 Err(AttestationError::InvalidTeeType { actual: 0 })
294 ));
295 }
296
297 #[tokio::test]
298 #[ignore = "requires network access to PCCS"]
299 async fn verifies_example_quote() {
300 let example: Value = serde_json::from_str(include_str!("../example.json")).unwrap();
301 let wrapped_quote_hex = example
302 .pointer("/tdx_attestation/quote_hex")
303 .and_then(Value::as_str)
304 .unwrap();
305
306 verify_tdx_quote_json_hex(wrapped_quote_hex).await.unwrap();
307 }
308}