Skip to main content

amadeus_node/
attestation.rs

1//! SGX Attestation module for Gramine
2//!
3//! This module provides SGX DCAP attestation functionality through Gramine's
4//! /dev/attestation pseudo-filesystem interface.
5//!
6//! # Overview
7//!
8//! - Reads SGX quotes from `/dev/attestation/quote`
9//! - Supports custom user report data (e.g., node public key hash)
10//! - Returns base64-encoded quote and certificate chain for remote verification
11//!
12//! # Usage
13//!
14//! ```rust
15//! let report = get_attestation_report(Some(b"my-custom-data"))?;
16//! // Send report.quote to remote verifier
17//! ```
18
19use serde::{Deserialize, Serialize};
20use std::fs::{File, OpenOptions};
21use std::io::{Read, Write};
22use thiserror::Error;
23
24/// Maximum size of an SGX quote (as defined by Intel SGX SDK)
25const SGX_QUOTE_MAX_SIZE: usize = 8192;
26
27/// Size of SGX report data field (64 bytes)
28const SGX_REPORT_DATA_SIZE: usize = 64;
29
30#[derive(Error, Debug)]
31pub enum AttestationError {
32    #[error("Attestation not available (not running in SGX enclave)")]
33    NotAvailable,
34
35    #[error("Failed to write user report data: {0}")]
36    WriteReportData(std::io::Error),
37
38    #[error("Failed to read quote: {0}")]
39    ReadQuote(std::io::Error),
40
41    #[error("Failed to read attestation type: {0}")]
42    ReadAttestationType(std::io::Error),
43
44    #[error("Invalid user report data size: expected {expected}, got {actual}")]
45    InvalidReportDataSize { expected: usize, actual: usize },
46
47    #[error("Attestation type is 'none' - SGX remote attestation not enabled")]
48    AttestationDisabled,
49}
50
51/// SGX attestation report containing quote and metadata
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct AttestationReport {
54    /// Base64-encoded SGX quote
55    pub quote: String,
56
57    /// Attestation type (e.g., "dcap")
58    pub attestation_type: String,
59
60    /// Size of the quote in bytes
61    pub quote_size: usize,
62
63    /// User report data that was embedded (base64-encoded)
64    pub user_report_data: String,
65}
66
67/// Check if SGX attestation is available
68///
69/// Returns true if running inside Gramine with SGX attestation enabled
70pub fn is_attestation_available() -> bool {
71    std::path::Path::new("/dev/attestation/attestation_type").exists()
72}
73
74/// Get the attestation type (e.g., "none" or "dcap")
75pub fn get_attestation_type() -> Result<String, AttestationError> {
76    let mut file = File::open("/dev/attestation/attestation_type")
77        .map_err(AttestationError::ReadAttestationType)?;
78
79    let mut attestation_type = String::new();
80    file.read_to_string(&mut attestation_type)
81        .map_err(AttestationError::ReadAttestationType)?;
82
83    Ok(attestation_type.trim().to_string())
84}
85
86/// Generate an SGX attestation report with optional user data
87///
88/// # Arguments
89///
90/// * `user_report_data` - Optional 64-byte data to embed in the SGX report.
91///   If None, zeros are used. Common use: hash of node's public key.
92///
93/// # Returns
94///
95/// `AttestationReport` containing the SGX quote and metadata
96///
97/// # Errors
98///
99/// Returns error if:
100/// - Not running in SGX enclave
101/// - User report data is not exactly 64 bytes
102/// - Failed to communicate with /dev/attestation
103///
104/// # Example
105///
106/// ```rust
107/// // Embed node public key hash in attestation
108/// use sha2::{Sha256, Digest};
109/// let mut hasher = Sha256::new();
110/// hasher.update(&node_public_key);
111/// let hash = hasher.finalize();
112///
113/// let mut report_data = [0u8; 64];
114/// report_data[..32].copy_from_slice(&hash);
115///
116/// let report = get_attestation_report(Some(&report_data))?;
117/// ```
118pub fn get_attestation_report(
119    user_report_data: Option<&[u8]>,
120) -> Result<AttestationReport, AttestationError> {
121    // Validate input size first (before checking SGX availability)
122    if let Some(data) = user_report_data {
123        if data.len() > SGX_REPORT_DATA_SIZE {
124            return Err(AttestationError::InvalidReportDataSize {
125                expected: SGX_REPORT_DATA_SIZE,
126                actual: data.len(),
127            });
128        }
129    }
130
131    // Check if attestation is available
132    if !is_attestation_available() {
133        return Err(AttestationError::NotAvailable);
134    }
135
136    // Check attestation type
137    let attestation_type = get_attestation_type()?;
138    if attestation_type == "none" {
139        return Err(AttestationError::AttestationDisabled);
140    }
141
142    // Prepare user report data (64 bytes, zero-padded if not provided)
143    let mut report_data = [0u8; SGX_REPORT_DATA_SIZE];
144    if let Some(data) = user_report_data {
145        report_data[..data.len()].copy_from_slice(data);
146    }
147
148    // Write user report data to /dev/attestation/user_report_data
149    {
150        let mut file = OpenOptions::new()
151            .write(true)
152            .open("/dev/attestation/user_report_data")
153            .map_err(AttestationError::WriteReportData)?;
154
155        file.write_all(&report_data)
156            .map_err(AttestationError::WriteReportData)?;
157    }
158
159    // Read SGX quote from /dev/attestation/quote
160    let mut quote_buffer = vec![0u8; SGX_QUOTE_MAX_SIZE];
161    let quote_size = {
162        let mut file = File::open("/dev/attestation/quote")
163            .map_err(AttestationError::ReadQuote)?;
164
165        file.read(&mut quote_buffer)
166            .map_err(AttestationError::ReadQuote)?
167    };
168
169    // Truncate to actual quote size
170    quote_buffer.truncate(quote_size);
171
172    use base64::engine::general_purpose::STANDARD;
173    use base64::Engine;
174
175    Ok(AttestationReport {
176        quote: STANDARD.encode(&quote_buffer),
177        attestation_type,
178        quote_size,
179        user_report_data: STANDARD.encode(&report_data),
180    })
181}
182
183/// Get attestation report with node's public key embedded
184///
185/// This is a convenience function that automatically embeds the SHA256 hash
186/// of the node's public key in the user report data field.
187///
188/// # Arguments
189///
190/// * `public_key` - The node's public key (32 bytes)
191///
192/// # Returns
193///
194/// `AttestationReport` with the public key hash embedded
195pub fn get_attestation_with_pubkey(public_key: &[u8]) -> Result<AttestationReport, AttestationError> {
196    use sha2::{Digest, Sha256};
197
198    // Hash the public key
199    let mut hasher = Sha256::new();
200    hasher.update(public_key);
201    let hash = hasher.finalize();
202
203    // Prepare report data with hash
204    let mut report_data = [0u8; SGX_REPORT_DATA_SIZE];
205    report_data[..32].copy_from_slice(&hash);
206
207    get_attestation_report(Some(&report_data))
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_attestation_availability() {
216        // This test will only pass when running in Gramine
217        let available = is_attestation_available();
218        println!("Attestation available: {}", available);
219    }
220
221    #[test]
222    fn test_report_data_validation() {
223        let too_large = vec![0u8; 65];
224        let result = get_attestation_report(Some(&too_large));
225        assert!(matches!(result, Err(AttestationError::InvalidReportDataSize { .. })));
226    }
227}