DOE Stack
The Caliptra subsystem supports SPDM, Secure-SPDM over PCI Data Object Exchange (DOE) mailbox protocol. The following diagram gives the over view of the DOE send and receive stack.
DOE Receive stack:
DOE Send stack:
sequenceDiagram
participant Host as "Host(TSM)"
participant SoC_PCI_DOE_FSM as "SoC PCI DOE Listener"
participant MCU_DOE_TRANSPORT_DRIVER as "MCU DOE Transport Driver"
participant DOE_CAPSULE as "DOE Capsule"
participant SPDM_APP as "SPDM App"
SPDM_APP -->> DOE_CAPSULE: App invokes receive_message<br> for SPDM or Secure-SPDM Data Object type
Host ->> Host: Host waits until<br> the `DOE Busy` bit is cleared in <br>DOE Status Register
loop While there is remaining DOE object to send
Host ->> SoC_PCI_DOE_FSM : Host starts sending DOE data object
SoC_PCI_DOE_FSM ->> SoC_PCI_DOE_FSM: Prepare the message in staging area <br> (eg: Mailbox or shared memory)
Note right of Host: Repeat until Host sets DOE Go bit
Host ->> SoC_PCI_DOE_FSM: Host writes `DOE Go` bit <br>in DOE Control Register<br> to indicate message ready
end
SoC_PCI_DOE_FSM ->> MCU_DOE_TRANSPORT_DRIVER: Notify that a new DOE object<br> is available to consume
MCU_DOE_TRANSPORT_DRIVER ->> DOE_CAPSULE: receive() callback
alt if DOE object is `Data Object 0`
DOE_CAPSULE ->> DOE_CAPSULE: Copy DOE object payload <br>into local buffer
DOE_CAPSULE ->> DOE_CAPSULE: Handle DOE Discovery
DOE_CAPSULE ->> DOE_CAPSULE: Prepare DOE Discovery response object
else if DOE object is `Data Object 1 or 2`
DOE_CAPSULE ->> DOE_CAPSULE: Copy DOE object payload <br>into app buffer
DOE_CAPSULE -->> SPDM_APP: Invoke upcall to userspace<br> to receive() message
end
DOE_CAPSULE ->> MCU_DOE_TRANSPORT_DRIVER: set_rx_buffer()<br> to set the receive buffer for the next DOE object
SPDM_APP ->> SPDM_APP: App processes message <br>and prepares DOE response
SPDM_APP -->> DOE_CAPSULE: App invokes send_message <br>to send DOE response
DOE_CAPSULE ->> MCU_DOE_TRANSPORT_DRIVER: invoke transmit()<br> to send the DOE response
MCU_DOE_TRANSPORT_DRIVER ->> SoC_PCI_DOE_FSM: Notify that DOE response is ready to send
SoC_PCI_DOE_FSM ->> Host: Set `Data Object Ready` bit in<br> DOE Status Register
DOE Capsule
The DOE capsule implements the system calls for the user space applications to send and receive the DOE data objects.
During board initialization, a DoeDriver instance is created and registered with a unique driver number. This instance manages the handling of DOE Discovery (Data Object Type 0), SPDM (Data Object Type 1), and Secure-SPDM (Data Object Type 2) data objects.
DOE Discovery handling
Every well-formed DOE Discovery Request is answered with a DOE Discovery Response; the capsule never silently discards one. The response contents depend on the request:
| Request | Discovery Response |
|---|---|
| Index 0, 1 or 2 with a supported Discovery Version | Vendor ID 0001h (PCI-SIG), the matching Data Object Protocol, and the next index (wrapping to 0 after the last protocol) |
| Index greater than 2 | Vendor ID FFFFh, Data Object Protocol 0, Next Index 0 |
| Unsupported DOE Discovery Version | Vendor ID FFFFh, Data Object Protocol 0, Next Index 0 |
A Vendor ID of FFFFh tells the requester there is no Data Object Protocol at the requested index, which is how discovery enumeration terminates. Responding rather than dropping the request also keeps the DOE transport from stalling a requester that is waiting on Data Object Ready.
The DOE Discovery Version occupies bits 15:8 of the request payload. Version 00h (used when the DOE Extended Capability Version is 01h, where the field is reserved) and version 02h (required when the Capability Version is 02h or greater) are accepted; any other value is rejected with the FFFFh response above.
/// PCI-SIG Vendor ID that defined the data object type
const PCI_SIG_VENDOR_ID: u16 = 0x0001;
/// Data Object Protocol
const DATA_OBJECT_PROTOCOL_DOE_DISCOVERY: u8 = 0x00;
const DATA_OBJECT_PROTOCOL_CMA_SPDM: u8 = 0x01;
const DATA_OBJECT_PROTOCOL_SECURE_CMA_SPDM: u8 = 0x02;
pub const DOE_SPDM_DRIVER_NUM: usize = 0xA000_0010;
/// IDs for subscribe calls
mod upcall {
/// Callback for when the message is received
pub const MESSAGE_RECEIVED: usize = 0;
/// Callback for when the message is transmitted.
pub const MESSAGE_TRANSMITTED: usize = 1;
/// Number of upcalls
pub const COUNT: u8 = 2;
}
/// IDs for read-only allow buffers
mod ro_allow {
/// Buffer for the message to be transmitted
pub const MESSAGE_WRITE: usize = 0;
/// Number of read-only allow buffers
pub const COUNT: u8 = 1;
}
/// IDs for read-write allow buffers
mod rw_allow {
/// Buffer for the message to be received
pub const MESSAGE_READ: usize = 0;
/// Number of read-write allow buffers
pub const COUNT: u8 = 1;
}
#[derive(Default)]
pub struct App {
waiting_rx: Cell<bool>, // Indicates if a message is waiting to be received
pending_tx: Cell<bool>, // Indicates if a message is in progress
}
pub struct DoeDriver {
doe_transport: & dyn DoeTransport,
apps: Grant<
App,
UpcallCount<{ upcall::COUNT }>,
AllowRoCount<{ ro_allow::COUNT }>,
AllowRwCount<{ rw_allow::COUNT }>,
>,
current_app: Cell<Option<ProcessId>>,
}
DOE Transport Trait
The DOE Transport trait defines a platform-agnostic interface for sending and receiving DOE data objects. Integrators must provide a SoC-specific implementation of this trait to enable PCI-DOE communication with the host.
pub trait DoeTransportTxClient<'a> {
/// Called by driver to notify that the DOE data object transmission is done.
///
/// # Arguments
/// * `result` - Result indicating success or failure of the transmission
fn send_done(&self, result: Result<(), ErrorCode>);
}
pub trait DoeTransportRxClient {
/// Called to receive a DOE data object.
///
/// # Arguments
/// * `rx_buf` - buffer containing the received DOE data object
/// * `len_dw` - The length of the data received in dwords
fn receive(&self, rx_buf: &'static mut [u32], len_dw: usize);
}
pub trait DoeTransport<'a> {
/// Sets the transmit and receive clients for the DOE transport instance
fn set_tx_client(&self, client: &'a dyn DoeTransportTxClient<'a>);
fn set_rx_client(&self, client: &'a dyn DoeTransportRxClient);
/// Sets the buffer used for receiving incoming DOE Objects.
/// This should be called in receive()
fn set_rx_buffer(&self, rx_buf: &'static mut [u32]);
/// Gets the maximum size of the data object that can be sent or received over DOE Transport.
fn max_data_object_size_dw(&self) -> usize;
/// Enable the DOE transport driver instance.
fn enable(&self);
/// Disable the DOE transport driver instance.
fn disable(&self);
/// Send DOE Object to be transmitted over SoC specific DOE transport.
///
/// # Arguments
/// * `tx_buf` - Iterator that yields u32 values from data object to be transmitted.
/// * `len` - The length of the message in dwords (4-byte words).
fn transmit(&self, tx_buf: impl Iterator<Item = u32>, len_dw: usize) -> Result<(), ErrorCode>;
}