Skip to main content
Atlas is designed to be easily extended with new TEE verifiers. This guide walks through adding support for a new TEE (e.g., AMD SEV-SNP, ARM CCA).

Overview

Adding a new TEE verifier involves:
  1. Create module structure for your TEE
  2. Define report type and add to Report enum
  3. Implement AtlsVerifier trait
  4. Create config and builder
  5. Create policy type implementing IntoVerifier
  6. Add variants to Policy and Verifier enums
  7. Re-export in lib.rs

Step 1: Create module structure

Create a new module for your TEE implementation:
core/src/
└── my_tee/
    ├── mod.rs
    ├── verifier.rs
    ├── config.rs
    └── policy.rs
In my_tee/mod.rs, re-export your types:
mod config;
mod policy;
mod verifier;

pub use config::{MyTeeVerifierConfig, MyTeeVerifierBuilder};
pub use policy::MyTeePolicy;
pub use verifier::MyTeeVerifier;

Step 2: Define report type

Add a variant to the Report enum in core/src/verifier.rs:
pub enum Report {
    Tdx(VerifiedReport),
    MyTee(MyTeeReport),  // Add your variant
}

impl Report {
    pub fn as_my_tee(&self) -> Option<&MyTeeReport> {
        match self {
            Report::MyTee(r) => Some(r),
            _ => None,
        }
    }

    pub fn into_my_tee(self) -> Option<MyTeeReport> {
        match self {
            Report::MyTee(r) => Some(r),
            _ => None,
        }
    }
}

Step 3: Implement AtlsVerifier

Create your verifier in my_tee/verifier.rs:
use crate::error::AtlsVerificationError;
use crate::verifier::{AsyncByteStream, AtlsVerifier, Report};

pub struct MyTeeVerifier {
    config: MyTeeVerifierConfig,
}

impl MyTeeVerifier {
    pub fn new(config: MyTeeVerifierConfig) -> Result<Self, AtlsVerificationError> {
        // Validate configuration
        Ok(Self { config })
    }

    pub fn builder() -> MyTeeVerifierBuilder {
        MyTeeVerifierBuilder::new()
    }

    async fn fetch_evidence<S>(
        &self,
        stream: &mut S,
        hostname: &str,
    ) -> Result<Vec<u8>, AtlsVerificationError>
    where
        S: AsyncByteStream,
    {
        // Fetch attestation evidence from server
        // e.g., HTTP POST to /attestation endpoint
        todo!()
    }

    fn verify_evidence(&self, evidence: &[u8]) -> Result<MyTeeReport, AtlsVerificationError> {
        // Verify evidence cryptographically
        // e.g., validate signatures, check measurements
        todo!()
    }

    fn verify_cert_binding(
        &self,
        peer_cert: &[u8],
        evidence: &[u8],
    ) -> Result<(), AtlsVerificationError> {
        // Verify certificate is bound to the evidence
        // e.g., cert hash in attestation report
        todo!()
    }

    fn verify_measurements(&self, report: &MyTeeReport) -> Result<(), AtlsVerificationError> {
        // Verify measurements against policy
        todo!()
    }
}

Native implementation

Implement AtlsVerifier for native targets:
#[cfg(not(target_arch = "wasm32"))]
impl AtlsVerifier for MyTeeVerifier {
    async fn verify<S>(
        &self,
        stream: &mut S,
        peer_cert: &[u8],
        session_ekm: &[u8],
        hostname: &str,
    ) -> Result<Report, AtlsVerificationError>
    where
        S: AsyncByteStream,
    {
        // 1. Fetch attestation evidence from server
        let evidence = self.fetch_evidence(stream, hostname).await?;

        // 2. Verify evidence cryptographically
        let report = self.verify_evidence(&evidence)?;

        // 3. Verify certificate binding
        self.verify_cert_binding(peer_cert, &evidence)?;

        // 4. Verify session EKM binding (optional, TEE-specific)
        // Bind session_ekm to evidence to prevent relay attacks

        // 5. Verify measurements against policy
        self.verify_measurements(&report)?;

        Ok(Report::MyTee(report))
    }
}

WASM implementation

Provide a WASM-compatible implementation:
#[cfg(target_arch = "wasm32")]
impl AtlsVerifier for MyTeeVerifier {
    async fn verify<S>(
        &self,
        stream: &mut S,
        peer_cert: &[u8],
        session_ekm: &[u8],
        hostname: &str,
    ) -> Result<Report, AtlsVerificationError>
    where
        S: AsyncByteStream,
    {
        // Same implementation as native version
        // Only the trait bounds differ
    }
}
The WASM implementation is typically identical to native. The main difference is trait bounds (no Send requirement).

Step 4: Create config and builder

In my_tee/config.rs, define configuration and builder:
use crate::error::AtlsVerificationError;
use super::MyTeeVerifier;

#[derive(Debug, Clone)]
pub struct MyTeeVerifierConfig {
    pub expected_measurement: Option<String>,
    pub allowed_status: Vec<String>,
    // ... other config fields
}

impl Default for MyTeeVerifierConfig {
    fn default() -> Self {
        Self {
            expected_measurement: None,
            allowed_status: vec!["Valid".into()],
        }
    }
}

pub struct MyTeeVerifierBuilder {
    config: MyTeeVerifierConfig,
}

impl MyTeeVerifierBuilder {
    pub fn new() -> Self {
        Self {
            config: MyTeeVerifierConfig::default(),
        }
    }

    pub fn expected_measurement(mut self, m: impl Into<String>) -> Self {
        self.config.expected_measurement = Some(m.into());
        self
    }

    pub fn allowed_status(mut self, statuses: Vec<String>) -> Self {
        self.config.allowed_status = statuses;
        self
    }

    pub fn build(self) -> Result<MyTeeVerifier, AtlsVerificationError> {
        MyTeeVerifier::new(self.config)
    }
}

Step 5: Create policy type

In my_tee/policy.rs, create a serde-compatible policy:
use serde::{Deserialize, Serialize};
use crate::error::AtlsVerificationError;
use crate::verifier::IntoVerifier;
use super::{MyTeeVerifier, MyTeeVerifierConfig};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MyTeePolicy {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub expected_measurement: Option<String>,
    
    #[serde(default = "default_allowed_status")]
    pub allowed_status: Vec<String>,
}

fn default_allowed_status() -> Vec<String> {
    vec!["Valid".into()]
}

impl Default for MyTeePolicy {
    fn default() -> Self {
        Self {
            expected_measurement: None,
            allowed_status: default_allowed_status(),
        }
    }
}

impl IntoVerifier for MyTeePolicy {
    type Verifier = MyTeeVerifier;

    fn into_verifier(self) -> Result<Self::Verifier, AtlsVerificationError> {
        let config = MyTeeVerifierConfig {
            expected_measurement: self.expected_measurement,
            allowed_status: self.allowed_status,
        };
        MyTeeVerifier::new(config)
    }
}

Step 6: Add to enums

Add to Policy enum

In core/src/policy.rs:
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Policy {
    #[serde(rename = "dstack_tdx")]
    DstackTdx(DstackTdxPolicy),
    
    #[serde(rename = "my_tee")]
    MyTee(MyTeePolicy),  // Add variant
}

impl Policy {
    pub fn into_verifier(self) -> Result<Verifier, AtlsVerificationError> {
        match self {
            Policy::DstackTdx(p) => Ok(Verifier::DstackTdx(p.into_verifier()?)),
            Policy::MyTee(p) => Ok(Verifier::MyTee(p.into_verifier()?)),  // Add arm
        }
    }
}

Add to Verifier enum

In core/src/verifier.rs:
pub enum Verifier {
    DstackTdx(DstackTDXVerifier),
    MyTee(MyTeeVerifier),  // Add variant
}

#[cfg(not(target_arch = "wasm32"))]
impl AtlsVerifier for Verifier {
    async fn verify<S>(
        &self,
        stream: &mut S,
        peer_cert: &[u8],
        session_ekm: &[u8],
        hostname: &str,
    ) -> Result<Report, AtlsVerificationError>
    where
        S: AsyncByteStream,
    {
        match self {
            Verifier::DstackTdx(v) => v.verify(stream, peer_cert, session_ekm, hostname).await,
            Verifier::MyTee(v) => v.verify(stream, peer_cert, session_ekm, hostname).await,  // Add arm
        }
    }
}

// Similar for wasm32 implementation

Step 7: Re-export in lib.rs

In core/src/lib.rs:
pub mod my_tee;

pub use my_tee::{
    MyTeePolicy,
    MyTeeVerifier,
    MyTeeVerifierBuilder,
    MyTeeVerifierConfig,
};

Testing your verifier

Create tests in my_tee/verifier.rs:
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_builder() {
        let verifier = MyTeeVerifier::builder()
            .expected_measurement("abc123")
            .build()
            .unwrap();
        
        assert!(verifier.config.expected_measurement.is_some());
    }

    #[test]
    fn test_policy_conversion() {
        let policy = MyTeePolicy {
            expected_measurement: Some("abc123".into()),
            allowed_status: vec!["Valid".into()],
        };
        
        let verifier = policy.into_verifier().unwrap();
        assert!(verifier.config.expected_measurement.is_some());
    }
}

Common patterns

Session binding

Bind the TLS session to the attestation evidence to prevent relay attacks:
// Include session_ekm in the evidence request
let nonce = generate_nonce();
let evidence_request = format!("{{\"nonce\": \"{}\", \"ekm\": \"{}\"}}", 
    hex::encode(nonce),
    hex::encode(session_ekm)
);

// Server includes SHA256(nonce || ekm) in attestation report_data
// Verify it matches
let expected = sha256(&[nonce, session_ekm].concat());
if evidence.report_data != expected {
    return Err(AtlsVerificationError::SessionBindingFailed);
}

Collateral caching

Cache verification collateral (CA certs, CRLs) to reduce network requests:
use std::sync::{Arc, RwLock};
use std::collections::HashMap;

struct CachedCollateral {
    data: Collateral,
    cached_at: u64,
}

pub struct MyTeeVerifier {
    config: MyTeeVerifierConfig,
    collateral_cache: Arc<RwLock<HashMap<String, CachedCollateral>>>,
}

impl MyTeeVerifier {
    async fn get_collateral(&self, key: &str) -> Result<Collateral, AtlsVerificationError> {
        // Check cache first
        if let Ok(guard) = self.collateral_cache.read() {
            if let Some(cached) = guard.get(key) {
                if !is_expired(cached.cached_at) {
                    return Ok(cached.data.clone());
                }
            }
        }

        // Fetch and cache
        let collateral = fetch_collateral(key).await?;
        if let Ok(mut guard) = self.collateral_cache.write() {
            guard.insert(key.to_string(), CachedCollateral {
                data: collateral.clone(),
                cached_at: now(),
            });
        }
        Ok(collateral)
    }
}

See also

Build docs developers (and LLMs) love