Skip to main content

attestation_verifier/
lib.rs

1#![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/// Minimal verifier for Intel TDX quotes.
31///
32/// By default the verifier reads `PCCS_URL` from the environment and falls back
33/// to [`PHALA_PCCS_URL`].
34#[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    /// Creates a verifier that fetches collaterals from the given PCCS endpoint.
47    pub fn new(pccs_url: impl Into<String>) -> Self {
48        Self {
49            pccs_url: pccs_url.into(),
50        }
51    }
52
53    /// Creates a verifier from `PCCS_URL`, or falls back to [`PHALA_PCCS_URL`].
54    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    /// Returns the PCCS URL used by this verifier.
60    pub fn pccs_url(&self) -> &str {
61        &self.pccs_url
62    }
63
64    /// Verifies a raw TDX quote.
65    pub async fn verify_quote(&self, quote: &[u8]) -> Result<()> {
66        self.verify_quote_at(quote, current_unix_time()?).await
67    }
68
69    /// Verifies a raw TDX quote at the provided Unix timestamp.
70    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    /// Decodes a raw quote from hex and verifies it.
84    pub async fn verify_quote_hex(&self, quote_hex: &str) -> Result<()> {
85        let quote = decode_quote_hex(quote_hex)?;
86        self.verify_quote(&quote).await
87    }
88
89    /// Decodes a raw quote from base64 and verifies it.
90    pub async fn verify_quote_base64(&self, quote_base64: &str) -> Result<()> {
91        let quote = decode_quote_base64(quote_base64)?;
92        self.verify_quote(&quote).await
93    }
94
95    /// Decodes and verifies a TDX JSON payload shaped like `{"tdx":{"quote":"..."}}`.
96    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(&quote).await
99    }
100
101    /// Decodes and verifies a hex-encoded TDX JSON payload shaped like
102    /// `{"tdx":{"quote":"..."}}`.
103    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(&quote).await
106    }
107}
108
109/// Verifies a raw TDX quote with the default verifier.
110pub async fn verify_quote(quote: &[u8]) -> Result<()> {
111    Verifier::default().verify_quote(quote).await
112}
113
114/// Verifies a raw TDX quote hex string with the default verifier.
115pub async fn verify_quote_hex(quote_hex: &str) -> Result<()> {
116    Verifier::default().verify_quote_hex(quote_hex).await
117}
118
119/// Verifies a raw TDX quote base64 string with the default verifier.
120pub async fn verify_quote_base64(quote_base64: &str) -> Result<()> {
121    Verifier::default().verify_quote_base64(quote_base64).await
122}
123
124/// Verifies a TDX JSON payload shaped like `{"tdx":{"quote":"..."}}` with the
125/// default verifier.
126pub 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
132/// Verifies a hex-encoded TDX JSON payload shaped like `{"tdx":{"quote":"..."}}`
133/// with the default verifier.
134pub 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
140/// Extracts the 64-byte TDX report data from a raw quote.
141pub 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
153/// Extracts the TDX report data from a raw quote and returns it as lowercase hex.
154pub fn extract_report_data_hex(quote: &[u8]) -> Result<String> {
155    Ok(hex::encode(extract_report_data(quote)?))
156}
157
158/// Decodes a raw quote from hex.
159pub fn decode_quote_hex(quote_hex: &str) -> Result<Vec<u8>> {
160    Ok(hex::decode(normalize_hex(quote_hex))?)
161}
162
163/// Decodes a raw quote from base64.
164pub fn decode_quote_base64(quote_base64: &str) -> Result<Vec<u8>> {
165    Ok(general_purpose::STANDARD.decode(quote_base64.trim())?)
166}
167
168/// Decodes a TDX JSON payload shaped like `{"tdx":{"quote":"..."}}`.
169pub 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
174/// Decodes a hex-encoded TDX JSON payload shaped like `{"tdx":{"quote":"..."}}`.
175pub 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(&quote).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(&quote).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(&quote);
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}