use std::sync::{Arc, RwLock};
use base64::Engine;
use chrono::Utc;
use ed25519_dalek::{Signer, SigningKey, VerifyingKey};
use rand::rngs::OsRng;
use sha2::{Digest, Sha256};
use crate::types::{MrEnclave, MrSigner, StartupQuote};
use crate::errors::SdkError;
use crate::gramine;
use crate::response::{hex_encode, AttestationHeader};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AttestationMode {
EveryResponse,
StartupOnly,
}
pub struct EnclaveAttestor {
signing_key: SigningKey,
pub public_key: VerifyingKey,
pub startup_quote: Arc<RwLock<Option<StartupQuote>>>,
pub mode: AttestationMode,
}
impl EnclaveAttestor {
pub async fn initialize() -> Result<Arc<Self>, SdkError> {
let mode = Self::detect_attestation_mode();
Self::initialize_with_mode(mode).await
}
pub async fn initialize_with_mode(mode: AttestationMode) -> Result<Arc<Self>, SdkError> {
tracing::info!(
mode = if Self::is_enclave_mode() { "enclave" } else { "dev" },
attestation_mode = ?mode,
"Initializing EnclaveAttestor"
);
let signing_key = SigningKey::generate(&mut OsRng);
let public_key = signing_key.verifying_key();
let quote = Self::get_startup_quote(&signing_key, &public_key)?;
tracing::info!("EnclaveAttestor initialized successfully");
Ok(Arc::new(Self {
signing_key,
public_key,
startup_quote: Arc::new(RwLock::new(Some(quote))),
mode,
}))
}
fn detect_attestation_mode() -> AttestationMode {
match std::env::var("GUARANTEE_ATTEST_MODE").as_deref() {
Ok("startup-only") => AttestationMode::StartupOnly,
_ => AttestationMode::EveryResponse,
}
}
fn is_enclave_mode() -> bool {
std::env::var("GUARANTEE_ENCLAVE")
.map(|v| v == "1")
.unwrap_or(false)
}
fn get_startup_quote(
signing_key: &SigningKey,
pub_key: &VerifyingKey,
) -> Result<StartupQuote, SdkError> {
if Self::is_enclave_mode() {
Self::get_real_quote(signing_key, pub_key)
} else {
Ok(Self::get_dev_quote(pub_key))
}
}
fn get_real_quote(
_signing_key: &SigningKey,
pub_key: &VerifyingKey,
) -> Result<StartupQuote, SdkError> {
let mut user_data = [0u8; 64];
let hash = Sha256::digest(pub_key.as_bytes());
user_data[..32].copy_from_slice(&hash);
gramine::write_user_report_data(&user_data)?;
let raw_quote = gramine::read_quote()?;
Ok(StartupQuote {
raw_quote,
mr_enclave: MrEnclave::new([0u8; 32]),
mr_signer: MrSigner::new([0u8; 32]),
attested_public_key: *pub_key,
produced_at: Utc::now(),
})
}
fn get_dev_quote(pub_key: &VerifyingKey) -> StartupQuote {
StartupQuote {
raw_quote: b"DEV_MODE_QUOTE".to_vec(),
mr_enclave: MrEnclave::new([0xDE_u8; 32]),
mr_signer: MrSigner::new([0xDE_u8; 32]),
attested_public_key: *pub_key,
produced_at: Utc::now(),
}
}
pub fn sign_response(&self, body: &[u8], request_id: &str) -> AttestationHeader {
tracing::debug!(%request_id, body_len = body.len(), "Signing response");
if self.mode == AttestationMode::StartupOnly {
return AttestationHeader {
version: 1,
signature_b64: String::new(),
payload_hash_hex: String::new(),
timestamp_ms: Utc::now().timestamp_millis() as u64,
public_key_hex: hex_encode(self.public_key.as_bytes()),
};
}
let timestamp_ms = Utc::now().timestamp_millis() as u64;
let mut hasher = Sha256::new();
hasher.update(body);
hasher.update(timestamp_ms.to_be_bytes());
hasher.update(request_id.as_bytes());
let payload_hash: [u8; 32] = hasher.finalize().into();
let signature = self.signing_key.sign(&payload_hash);
AttestationHeader {
version: 1,
signature_b64: base64::engine::general_purpose::STANDARD.encode(signature.to_bytes()),
payload_hash_hex: hex_encode(&payload_hash),
timestamp_ms,
public_key_hex: hex_encode(self.public_key.as_bytes()),
}
}
pub fn startup_attestation_json(&self) -> Result<serde_json::Value, SdkError> {
let quote_guard = self
.startup_quote
.read()
.map_err(|_| SdkError::NotInitialized)?;
let quote = quote_guard.as_ref();
Ok(serde_json::json!({
"public_key": hex_encode(self.public_key.as_bytes()),
"quote": quote.map(|q| base64::engine::general_purpose::STANDARD.encode(&q.raw_quote)),
"mr_enclave": quote.map(|q| q.mr_enclave.to_string()),
"mr_signer": quote.map(|q| q.mr_signer.to_string()),
"tee_type": if Self::is_enclave_mode() { "intel-sgx" } else { "dev-mode" },
"produced_at": quote.map(|q| q.produced_at.to_rfc3339()),
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::response::hex_encode;
#[tokio::test]
async fn initialize_succeeds_in_dev_mode() {
std::env::remove_var("GUARANTEE_ENCLAVE");
let attestor = EnclaveAttestor::initialize()
.await
.expect("should init in dev mode");
assert!(attestor.startup_quote.read().expect("lock").is_some());
}
#[tokio::test]
async fn sign_response_produces_valid_header() {
std::env::remove_var("GUARANTEE_ENCLAVE");
let attestor = EnclaveAttestor::initialize().await.expect("should init");
let header = attestor.sign_response(b"hello world", "req-123");
assert_eq!(header.version, 1);
assert!(!header.signature_b64.is_empty());
assert!(!header.payload_hash_hex.is_empty());
}
#[tokio::test]
async fn sign_response_signature_verifies() {
std::env::remove_var("GUARANTEE_ENCLAVE");
let attestor = EnclaveAttestor::initialize().await.expect("should init");
let body = b"test body";
let header = attestor.sign_response(body, "req-456");
let sig_bytes = base64::engine::general_purpose::STANDARD
.decode(&header.signature_b64)
.expect("valid base64");
let signature = ed25519_dalek::Signature::from_bytes(
sig_bytes.as_slice().try_into().expect("64 bytes"),
);
let hash_bytes = hex_decode(&header.payload_hash_hex);
use ed25519_dalek::Verifier;
assert!(attestor.public_key.verify(&hash_bytes, &signature).is_ok());
}
#[tokio::test]
async fn startup_attestation_json_has_required_fields() {
std::env::remove_var("GUARANTEE_ENCLAVE");
let attestor = EnclaveAttestor::initialize().await.expect("should init");
let json = attestor
.startup_attestation_json()
.expect("should produce json");
assert!(json.get("public_key").is_some());
assert!(json.get("quote").is_some());
assert!(json.get("mr_enclave").is_some());
assert!(json.get("tee_type").is_some());
}
#[tokio::test]
async fn startup_only_mode_returns_empty_signature() {
std::env::remove_var("GUARANTEE_ENCLAVE");
let attestor = EnclaveAttestor::initialize_with_mode(AttestationMode::StartupOnly)
.await
.expect("should init");
assert_eq!(attestor.mode, AttestationMode::StartupOnly);
let header = attestor.sign_response(b"hello world", "req-789");
assert_eq!(header.version, 1);
assert!(header.signature_b64.is_empty());
assert!(header.payload_hash_hex.is_empty());
assert!(!header.public_key_hex.is_empty());
}
#[tokio::test]
async fn every_response_mode_signs_responses() {
std::env::remove_var("GUARANTEE_ENCLAVE");
let attestor = EnclaveAttestor::initialize_with_mode(AttestationMode::EveryResponse)
.await
.expect("should init");
assert_eq!(attestor.mode, AttestationMode::EveryResponse);
let header = attestor.sign_response(b"hello world", "req-abc");
assert!(!header.signature_b64.is_empty());
assert!(!header.payload_hash_hex.is_empty());
}
fn hex_decode(hex: &str) -> Vec<u8> {
(0..hex.len())
.step_by(2)
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).expect("valid hex"))
.collect()
}
}