Skip to content

JSON collaterals serialization with Bytemuck, and implementation of their corresponding ZeroCopy types #19

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 26 commits into
base: dev
Choose a base branch
from

Conversation

preston4896
Copy link
Collaborator

@preston4896 preston4896 commented May 14, 2025

Overview

The pod module in dcap-rs provides an efficient method for handling Intel SGX/TDX attestation data structures (TcbInfo and EnclaveIdentity) using a zero-copy approach. This is particularly valuable in memory-constrained environments like Solana programs.

Instead of deserializing data into deeply nested Rust structs (which require significant heap allocations), the zero-copy approach allows direct access to data within a contiguous byte buffer, reading only what's needed without allocating new memory for each component.

Serialization Format and Process

General Binary Format

All serialized structures follow a common pattern:

[Signature (64 bytes)] | [Main Header (fixed-size)] | [Variable Payload]

The main header contains metadata needed to interpret the variable payload, including lengths, counts, and flags. The payload contains all variable-length and nested data in a carefully structured format.

Serialization Process

  1. For both TcbInfo and EnclaveIdentity, serialization begins with creating a fixed-size header structure.
  2. Header fields are populated with metadata from the original Rust struct.
  3. A payload byte vector is incrementally built by appending:
    • Fixed-size data (like TDX module data)
    • String data (sometimes stored as ASCII/hex strings)
    • Serialized nested structures (each with their own headers and payloads)
  4. Proper alignment is maintained using padding bytes where necessary
  5. The signature, header, and payload are combined into a single byte vector

For example, in SerializedTcbInfo::from_rust_tcb_info(), the function:

  • Creates and populates a TcbInfoHeader
  • Builds the payload by appending data for TDX module, TDX identities, and TCB levels
  • Tracks offsets and sizes to populate header metadata fields

Deserialization and Zero-Copy Access

Deserialization Process

  1. The signature is extracted from the first 64 bytes.
  2. The fixed-size header is read using bytemuck::try_from_bytes().
  3. Based on header metadata, slices of the payload are created for each section.
  4. These slices are used to create zero-copy view structs that reference the original buffer.

For example, TcbInfoZeroCopy::from_bytes():

pub fn from_bytes(bytes: &'a [u8]) -> Result<Self, ZeroCopyError> {
    // Extract header from start of bytes
    let (header_bytes, main_payload) = bytes.split_at(core::mem::size_of::<TcbInfoHeader>());
    let header: &TcbInfoHeader = cast_slice(header_bytes)?;
    
    // Use header metadata to slice the payload into sections
    let mut current_offset = 0;
    let tdx_module_data_payload = if header.tdx_module_present == 1 {
        // Extract TDX module data based on length in header
        let len = header.tdx_module_data_len as usize;
        let slice = main_payload.get(current_offset..current_offset + len)?;
        current_offset += len;
        Some(cast_slice(slice)?)
    } else {
        None
    };
    
    // Extract remaining sections similarly
    // ...
    
    Ok(Self {
        header,
        tdx_module_data_payload,
        tdx_module_identities_section_payload,
        tcb_levels_section_payload,
    })
}

Accessing Payload Data

The zero-copy structs provide accessor methods that:

  1. Use header metadata to locate data within the payload
  2. Interpret bytes directly from the original buffer
  3. Return either references to the data or parsed values

Examples from TcbInfoZeroCopy:

pub fn fmspc(&self) -> [u8; 6] { 
    hex::decode(from_utf8(&self.header.fmspc_hex).unwrap()).unwrap().try_into().unwrap()
}

pub fn tcb_levels(&self) -> TcbLevelIter<'a> {
    TcbLevelIter::new(
        self.tcb_levels_section_payload,
        self.header.tcb_levels_count,
    )
}

Handling Variable and Nested Data

Payload Structure

The payload is organized hierarchically:

  • Top-level payload contains sections for different components (TDX module, TCB levels, etc.)
  • Each section may contain multiple items (e.g., multiple TCB levels)
  • Each item has its own header and payload, embedded within the parent's payload

The header at each level contains metadata needed to locate and interpret its children:

  • Counts (number of items in a collection)
  • Lengths (size of sections in bytes)
  • Presence flags (for optional components)

Iterators for Nested Structures

Complex nested collections use iterator types (e.g., TcbLevelIter, TdxModuleIdentityIter):

  1. Iterators are initialized with:

    • A byte slice containing the section payload
    • A count of items from the parent header
  2. The iterator maintains:

    • Current position in the byte slice
    • Index of the current item
    • Item count to determine when iteration is complete
  3. For each .next() call:

    • The iterator reads a fixed-size header at the current position
    • Uses header metadata to determine payload boundaries
    • Creates a zero-copy view struct for the item
    • Advances position accounting for padding and alignment

Example structure of TcbLevelIter:

pub struct TcbLevelIter<'a> {
    bytes: &'a [u8],      // Slice of payload containing all TCB levels
    count: u32,           // Total number of levels to iterate over
    current_index: usize, // Current index in the iteration
    current_offset: usize, // Current byte offset in the payload
}

impl<'a> Iterator for TcbLevelIter<'a> {
    type Item = Result<TcbLevelZeroCopy<'a>, ZeroCopyError>;
    
    fn next(&mut self) -> Option<Self::Item> {
        if self.current_index >= self.count as usize {
            return None; // Iteration complete
        }
        
        // Extract header at current offset
        let header: &TcbLevelHeader = match cast_slice(&self.bytes[self.current_offset..]) {
            Ok(h) => h,
            Err(e) => return Some(Err(e)),
        };
        
        // Get section payload based on header metadata
        let total_payload_size = /* calculate from header fields */;
        let payload_slice = &self.bytes[self.current_offset + mem::size_of::<TcbLevelHeader>()..
                                         self.current_offset + total_payload_size];
        
        // Create view struct
        let result = TcbLevelZeroCopy::new(header, payload_slice);
        
        // Update state for next item
        self.current_index += 1;
        self.current_offset += total_payload_size;
        
        // Align to required boundary for next header
        self.current_offset = align_up(self.current_offset, align_of::<TcbLevelHeader>());
        
        Some(result)
    }
}

Relationship Between Types

Original Rust Types vs. Zero-Copy Types

  • Original Types (TcbInfo, EnclaveIdentity):

    • Full Rust structs with owned data
    • Support normal Rust operations (clone, serialize to JSON, etc.)
    • Use heap allocations for strings and collections
    • Simple to use but memory-intensive
  • Zero-Copy Types (TcbInfoZeroCopy, EnclaveIdentityZeroCopy):

    • View types that borrow slices of the original byte array
    • No heap allocations during access
    • Lifetime tied to the source byte array
    • Provide accessor methods and iterators for efficient access
    • More complex to use but minimal memory overhead

Conversion Between Types

When needed, zero-copy types can be converted to original types (gaining usability but losing memory efficiency):

// In the conversion module:
pub fn tcb_info_from_zero_copy(view: &TcbInfoZeroCopy) -> Result<TcbInfo, ZeroCopyError> {
    // Convert zero-copy view to owned TcbInfo struct
    // ...
}

// Usage in parse_tcb_pod_bytes:
let tcb_info_view = TcbInfoZeroCopy::from_bytes(bytes)?;
let rust_tcb_info = tcb_info_from_zero_copy(&tcb_info_view)?;

Summary

The zero-copy design in dcap-rs allows for efficient handling of complex attestation data:

  1. Space Efficiency: Data is accessed directly from the source buffer without duplicating structures.
  2. Minimal Allocations: Only the bytes needed for the original buffer are allocated.
  3. Performance: Avoids parsing entire structures when only portions are needed.
  4. Flexibility: Can convert between zero-copy views and full Rust structs when necessary.

This approach is particularly valuable in memory-constrained environments like Solana programs, where traditional deserialization would be too resource-intensive.

@preston4896 preston4896 marked this pull request as ready for review May 15, 2025 05:04
@preston4896 preston4896 requested a review from udsamani May 15, 2025 05:04
@preston4896 preston4896 changed the title TcbInfo Serialization with Bytemuck, and implementation of TcbInfo ZeroCopy types JSON collaterals serialization with Bytemuck, and implementation of their corresponding ZeroCopy types May 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant