Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Caliptra Manufacturer Control Unit (MCU) Firmware and SDK

Spec revision: 0.9

The Caliptra MCU firmware is be provided as a reference software development kit (SDK) with a consistent foundation for building a quantum-resilient and standards-compliant Root of Trust (RoT) for SoC implementers. It extends the Caliptra core system to provide the Caliptra Subsystem set of services to the encompassing system.

While Caliptra Core provides support for Identity, Secure Boot, Measured Boot, and Attestation, the Caliptra MCU firmware will be responsible for enabling Recovery, RoT Services, and Platform integration support. All SoC RoTs have specific initialization sequences and scenarios that need to be supported beyond standard RoT features. Hence, the MCU firmware will be distributed as Rust SDK with batteries included to build RoT Applications.

The Caliptra MCU SDK is composed of two major parts:

ROM: When the MCU first boots, it executes a small, bare metal ROM. The ROM is responsible for sending non-secret fuses to Caliptra core so that it can complete booting. The MCU ROM will be silicon-integrator-specific. However, the MCU SDK will provide a ROM framework that binds the MCU and Caliptra. The ROM will also be used with the software emulator, RTL Simulator, and FPGA stacks. For more details, see the ROM specification.

Runtime: The majority of the MCU firmware SDK is the runtime firmware, which provides the majority of the services after booting. Most of the documentation here consists of the documentation for the runtime.

MCU Diagram

Principles

Caliptra 2.x firmware aspires to be the foundation for the RoT used in SoCs integrating Caliptra. Hence architecture, design and implementation must abide by certain guiding principles. Many of these principles are the founding principles for the Caliptra Project.

The MCU firmware SDK will follow the same principles as the Caliptra core's firmware.

  • Open and Extensible: The MCU SDK is published as open source and available via GitHub, and uses a build toolchain for repeatable builds. The MCU SDK provided is a reference implementation, and is intended to be extended and adaptable by integrators.

  • Consistent: Vendors require features like Identity, Secure Boot, Measured Boot, Attestation, Recovery, Update, Anti-Rollback, and ownership transfer. There are standards defined by various organizations around these feature areas. The MCU SDK will follow TCG, DMTF, OCP, PCIe and other standards to guarantee consistency on RoT features and behavior.

  • Compliant: The MCU SDK must be compliant with security standards and audits. Security audits will be performed and audit reports will be published on GitHub. In addition, the SDK will also be audited for OCP SAFE and short form reports will be published to GitHub.

  • Secure and Safe Code: The MCU SDK will follow the standards in Caliptra Core for security and memory safety as first principle and will be implemented in Rust.

SDK Features

The following sections give a short overview of the features and goals of the MCU SDK.

Development

Software emulators for Caliptra, MCU, and a reference SoC will be provided as part of the MCU SDK. Software emulators will be an integrated part of CI/CD pipelines. In addition, Caliptra also has RTL Simulators based on Verilator that can be used for regression testing. The Caliptra hardware team also has reference hardware running on FPGA platforms.

RTOS

MCU provides various simultaneous, complex RoT services. Hence an RTOS is needed. As the MCU SDK is implemented in Rust, we need Rust-based RTOS. MCU will leverage the Tock Embedded Operating System, an RTOS designed for secure embedded systems. MCU uses the Tock Kernel while providing a Caliptra async user mode API surface area. Tock provides a good security and isolation model to build an ROT stack.

Drivers

Tock has a clean user and kernel mode separation. Components that directly access hardware and provide abstractions to user mode are implemented as Tock drivers and capsules (kernel modules). The MCU SDK will provide the following drivers:

  • UART Driver
  • I3C Driver
  • Fuse Controller Driver
  • Caliptra Mailbox Driver
  • MCI Mailbox Driver
  • SPI Flash Driver (may be replaced by a silicon-specific flash driver)

Silicon integrators may provide their own drivers to implement SoC-specific features that are not provided by the MCU SDK. For example, if an SoC implements TEE-IO, the silicon vendor will be responsible for providing PCIe IDE and TDISP drivers.

Stacks

As the Caliptra RoT is built on the foundation of industry standard protocols, the MCU SDK will provide the following stacks:

  • MCTP
  • PLDM
  • PLDM Firmware Update
  • OCP Streaming Boot
  • SPDM based Attestation
  • PCIe IDE
  • TDISP
  • SPI Flash Boot

APIs and Services

Caliptra RoT will provide common foundational services and APIs required to be implemented by all RoT. The following are the APIs and services provided by MCU SDK:

  • Image Loading
  • Attestation
  • Signing Key management and revocation
  • Anti-Rollback protection
  • Firmware Update
  • Life Cycle Management
  • Secure Debug Unlock
  • Ownership Transfer
  • Cryptographic API
  • Certificate Store
  • Key-Value Store
  • Logging and Tracing API

Applications

Each SoC has unique features and may need a subset of security services. Hence silicon integrators will be responsible for authoring ROT applications leveraging MCU SDK. MCU SDK will ship with a set of applications that will demonstrate the features of the MCU SDK.

Tooling and Documentation

The MCU SDK will feature

  • Repeatable build environments to build RoT Applications
  • Image generation, verification, and signing tools
  • Libraries for interacting with Caliptra ROT to perform various functions like attestation, firmware updates, log retrieval, etc.
  • An MCU SDK Architecture and Design Specifications
  • An MCU SDK Developers' Guide
  • An MCU SDK Integrators' Guide
  • The MCU SDK API

Reference ROM Specification

The reference ROM is executed when the MCU starts.

The ROM's main responsibilities to the overall Caliptra subsystem are to:

  • Send non-secret fuses to Caliptra core
  • Initialize I3C and the firmware recovery interface
  • Jump to firmware

It can also handle any other custom SoC-specific initialization that needs to happen early.

Boot Flows

There are three main boot flows that needs to execute for its role in the Caliptra subsystem:

  • Cold Boot Flow
  • Firmware Update Flow
  • Warm Reset Flow

These are selected based on the MCI RESET_REASON register that is set by hardware whenver the MCU is reset.

Cold Boot Flow

  1. Check the MCI RESET_REASON register for MCU status (it should be in cold boot mode)
  2. Initialize I3C recovery interface. For AXI bypass boot, only the recovery interface initialization is required; basic I3C initialization can be skipped.
    • For I3C boot: Initialize I3C registers according to the initialization sequence, then initialize I3C recovery interface per the recovery flow.
    • For AXI bypass boot: Only initialize the recovery interface registers needed for streaming boot.
  3. Assert Caliptra boot go signal to bring Caliptra out of reset.
  4. Read Caliptra SoC FLOW_STATUS register to wait for Caliptra Ready for Fuses state.
  5. Anything SoC-specific can happen here
    1. Stash to Caliptra if required (i.e., if any security-sensitive code is loaded, such as PLL programming or configuration loading). Note: Caliptra must be up and ready for fuses before stashing can occur.
  6. Read non-secret fuses from the OTP controller. The authoritative fuse map is contained in the main Caliptra specification.
  7. Write fuse data to Caliptra SoC interface fuse registers. The following fuses are written to the corresponding Caliptra registers:
  8. Configure MCU mailbox AXI users (see Security Configuration below).
  9. Set mailbox AXI user lock registers.
  10. [2.1] Set FC_FIPS_ZEROZATION to the appropriate value.
  11. Set SS_CONFIG_DONE_STICKY, SS_CONFIG_DONE registers to lock MCI configuration.
  12. Verify PK hashes and MCU mailbox AXI users after locking (see Security Configuration below).
  13. Poll on Caliptra FLOW_STATUS registers for Caliptra to deassert the Ready for Fuses state.
  14. Handle device ownership transfer, if applicable.
  15. Stash the MCU ROM and other measurements to Caliptra.
  16. Send the RI_DOWNLOAD_FIRMWARE command to Caliptra to start the firmware loading process. Caliptra will:
    1. Follow all of the steps in the Caliptra ROM documentation for firmware loading in the ROM cold reset.
    2. Transition to Caliptra runtime firmware.
    3. Load the SoC manifest over the recovery interface and verify it.
    4. Load the MCU runtime over the recovery interface registers to the MCU SRAM.
    5. Verify the MCU runtime against the SoC manifest.
    6. [2.1] If the MCU runtime is encrypted:
      1. Caliptra runtime returns to mailbox processing mode.
      2. MCU derives or imports the decryption key to the Caliptra cryptographic mailbox.
      3. MCU issues a CM_AES_GCM_DECRYPT_DMA command to decrypt the firmware in MCU SRAM.
      4. MCU issues the ACTIVATE_FIRMARE command to Caliptra activate the MCU firmware.
    7. Caliptra sets the MCI FW_EXEC_CTRL[2] bit to indicate that MCU firmware is ready
  17. Wait for Caliptra to indicate MCU firmware is ready by polling the firmware ready status.
  18. MCU ROM triggers a reset by writing 0x1 to the MCI RESET_REQUEST register. This generates a hardware reset of the MCU core while maintaining power. The MCI hardware automatically sets RESET_REASON to FirmwareBootReset, causing the MCU to restart and enter the Firmware Boot Reset flow, which will jump to the loaded firmware.
  19. Clear the watchdog timer.
sequenceDiagram
    note right of mcu: check reset reason
    mcu->>mci: assert Caliptra boot go
    note right of mcu: initialize recovery interface
    loop wait for ready for fuses
    mcu->>caliptra: read flow status
    end
    note right of mcu: SoC-specific init
    opt if required
        mcu->>caliptra: stash
    end
    mcu->>otp: read non-secret fuses
    otp->>mcu: non-secret fuses
    mcu->>caliptra: set non-secret fuses
    mcu->>mci: configure MCU mailbox AXI users
    mcu->>mci: lock MCU mailbox AXI users
    mcu->>mci: set SS_CONFIG_DONE_STICKY
    mcu->>mci: set SS_CONFIG_DONE
    mcu->>mci: verify PK hashes
    mcu->>mci: verify MCU mailbox AXI users
    loop wait for NOT ready for fuses
    mcu->>caliptra: read flow status
    end
    mcu->>caliptra: RI_DOWNLOAD_FIRMWARE command
    note right of caliptra: authenticate FW header/TOC
    note right of caliptra: verify signatures
    note right of caliptra: load/decrypt Caliptra FW
    note right of caliptra: derive runtime keys
    alt encrypted MCU FW
        note right of caliptra: decrypt and load MCU FW to SRAM
    else unencrypted MCU FW
        note right of caliptra: load MCU FW to SRAM
    end
    caliptra->>mci: set FW_EXEC_CTL[2]
    loop wait for firmware ready
    mcu->>mci: check FW_EXEC_CTL
    end
    mcu->>mci: write RESET_REQUEST (0x1)
    note right of mcu: resets to Firmware Boot flow

Firmware Boot Flow

This flow is used to boot the MCU into the MCU Runtime Firmware following either a cold or warm reset. It ensures that the runtime firmware is properly loaded and ready for execution.

  1. Check the MCI RESET_REASON register for MCU status (it should be in firmware boot reset mode FirmwareBootReset)
  2. Set flow checkpoint to indicate firmware boot flow has started
  3. Validate that firmware was actually loaded by checking the firmware entry point is not zero
  4. Set flow milestone to indicate firmware boot flow completion
  5. Jump directly to runtime firmware at the configured SRAM offset
sequenceDiagram
    note right of mcu: check reset reason (FirmwareBootReset)
    note right of mcu: set flow checkpoint
    note right of mcu: validate firmware at entry point
    alt firmware valid
        note right of mcu: set completion milestone
        note right of mcu: jump to runtime firmware
    else firmware invalid
        note right of mcu: fatal error - halt
    end

Hitless Firmware Update Flow

Hitless Update Flow is triggered when MCU runtime FW requests an update of the MCU FW by sending the ACTIVATE_FIRMWARE mailbox command to Caliptra. Upon receiving the mailbox command, Caliptra will initialize the MCU reset sequence causing the MCU to boot to ROM and run the Hitless Firmware Update Flow.

  1. Check the MCI RESET_REASON register for reset status (it should be in hitless firmware update mode FirmwareHitlessUpdate).
  2. Enable the notif_cptra_mcu_reset_req_sts interrupt.
  3. Check if firmware is already available by reading the interrupt status
  4. Clear notif_cptra_mcu_reset_req_sts interrupt status
  5. If firmware is available:
    1. Wait for Caliptra to clear FW_EXEC_CTRL[2]. This will be indicated when notif_cptra_mcu_reset_req_sts interrupt status bit is set
    2. Clear the notif_cptra_mcu_reset_req_sts interrupt. This triggers Caliptra to copy MCU FW from the staging area to MCU SRAM.
  6. Wait for Caliptra to set FW_EXEC_CTRL[2].
  7. Release Caliptra mailbox. Hitless Update is triggered by a mailbox command from MCU to Caliptra which causes it to reboot to ROM, therefore the mailbox needs to be released after the update is complete.
  8. Jump to runtime firmware at the configured SRAM offset
sequenceDiagram
    note right of mcu: check reset reason (FirmwareHitlessUpdate)
    note right of mcu: enable reset request interrupt
    mcu->>mci: check if firmware already available
    mcu->>mci: clear reset request interrupt status
    alt firmware already available
        loop wait for Caliptra to clear FW_EXEC_CTRL[2]
            mcu->>mci: check reset request status
        end
        mcu->>mci: clear reset request interrupt (triggers FW copy)
    end
    loop wait for Caliptra to set FW_EXEC_CTRL[2]
        mcu->>caliptra: check fw_ready status
    end
    mcu->>caliptra: release mailbox (finish response)
    loop verify firmware ready
        mcu->>caliptra: check fw_ready status
    end
    note right of mcu: jump to runtime firmware

Warm Reset Flow

Warm Reset Flow occurs when the subsystem reset is toggled while powergood is maintained high. This is allowed when MCU and Caliptra already loaded their respective mutable firmware, prior to the warm reset. MCU and Caliptra FW will not be reloaded in this flow.

  1. Check the MCI RESET_REASON register for reset status (it should be in warm reset mode WarmReset)
  2. Assert Caliptra boot go signal to bring Caliptra out of reset.
  3. Wait for Caliptra to be ready for fuses (even though fuses won't be rewritten)
  4. Set SS_CONFIG_DONE register to lock MCI configuration until next warm reset.
  5. Signal fuse write done to Caliptra to complete the fuse handshake protocol
  6. Wait for Caliptra to deassert ready for fuses state
  7. Wait for Caliptra to indicate that MCU firmware is ready in SRAM
  8. Validate that firmware was actually loaded by checking the firmware entry point is not zero
  9. Set flow checkpoint and milestone to indicate warm reset flow completion
  10. Trigger a warm reset to transition to FirmwareBootReset flow which will jump to the firmware
sequenceDiagram
    note right of mcu: check reset reason (WarmReset)
    note right of mcu: set flow checkpoint
    mcu->>caliptra: assert boot go signal
    loop wait for ready for fuses
        mcu->>caliptra: check ready for fuses status
    end
    mcu->>mci: set SS_CONFIG_DONE
    mcu->>mci: set SS_CONFIG_DONE
    mcu->>caliptra: signal fuse write done
    loop wait for NOT ready for fuses
        mcu->>caliptra: check ready for fuses status
    end
    loop wait for firmware ready
        mcu->>caliptra: check firmware ready status
    end
    note right of mcu: validate firmware at entry point
    note right of mcu: set completion milestone
    mcu->>mci: trigger warm reset (to FirmwareBootReset)

Failures

On any fatal or non-fatal failure, MCU ROM can use the MCI registers FW_ERROR_FATAL and FW_ERROR_NON_FATAL to assert the appropriate errors.

In addition, SoC-specific failure handling may occur.

There will also be a watchdog timer running to ensure that the MCU is reset if not the ROM flow is not progressing properly.

Security Configuration

The MCU ROM is responsible for configuring and locking security-sensitive MCI registers during the boot process. This section describes the security requirements and verification steps that must be performed.

Configuration Locking

The MCI provides two configuration lock registers:

  • SS_CONFIG_DONE_STICKY: Locks configuration registers until the next cold reset. This is set during cold boot after all sticky configuration is complete.
  • SS_CONFIG_DONE: Locks configuration registers until the next warm reset. This is set during both cold boot and warm boot flows.

These registers must be set after configuring security-sensitive registers and before signaling fuse write done to Caliptra.

Security Requirement: After setting these registers, MCU ROM must verify that they are actually set by reading them back. If either register fails to set, the ROM must trigger a fatal error (ROM_SOC_SS_CONFIG_DONE_VERIFY_FAILED) and halt. This ensures that the locking mechanism itself has not been tampered with.

Production Debug Unlock PK Hash Verification

The PROD_DEBUG_UNLOCK_PK_HASH_REG registers in MCI store the public key hashes used for production debug unlock authentication. These registers are writable by the MCU but have no access protections before SS_CONFIG_DONE_STICKY is set. This creates a window where another AXI user could potentially manipulate the PK hashes between configuration and locking.

Security Requirement: MCU ROM must verify that the PK hash values written to MCI match the expected values from the fuse controller after both SS_CONFIG_DONE_STICKY and SS_CONFIG_DONE are set. If verification fails, the ROM must trigger a fatal error and halt.

The verification process:

  1. MCU ROM writes PK hash values from fuses to PROD_DEBUG_UNLOCK_PK_HASH_REG registers
  2. [2.1] MCU ROM writes the FC_FIPS_ZEROZATION register
  3. MCU ROM sets SS_CONFIG_DONE_STICKY and SS_CONFIG_DONE to lock the registers
  4. MCU ROM reads back the PK hash register values and compares them against the original fuse values
  5. [2.1] MCU ROM reads back the FC_FIPS_ZEROIZATION register and verifies its value.
  6. If any mismatch is detected, MCU ROM reports a fatal error (ROM_SOC_PK_HASH_VERIFY_FAILED)

MCU Mailbox AXI User Verification

The MBOX0_VALID_AXI_USER and MBOX1_VALID_AXI_USER registers control which AXI users can access the MCU mailboxes. These registers are locked using MBOX0_AXI_USER_LOCK and MBOX1_AXI_USER_LOCK, which make the corresponding AXI user registers read-only. Note that unlike the PK hash registers, the MBOX registers are not affected by SS_CONFIG_DONE*—only the LOCK registers control their access.

Security Requirement: MCU ROM must configure the MCU mailbox AXI users, lock them using the LOCK registers, and verify that both the AXI user values and the lock register values match the expected configuration. If verification fails, the ROM must trigger a fatal error and halt. The verification is performed after SS_CONFIG_DONE_STICKY and SS_CONFIG_DONE are set to consolidate all MCI register verification in one place.

The verification process:

  1. MCU ROM configures MBOX[0,1]_VALID_AXI_USER registers with the expected AXI user values
  2. MCU ROM sets MBOX[0,1]_AXI_USER_LOCK to lock each configured slot (this makes the AXI user registers read-only)
  3. MCU ROM sets SS_CONFIG_DONE_STICKY and SS_CONFIG_DONE
  4. MCU ROM verifies that both config done registers are actually set
  5. MCU ROM reads back the MBOX[0,1]_VALID_AXI_USER register values and compares them against the expected configuration
  6. MCU ROM reads back the MBOX[0,1]_AXI_USER_LOCK register values and verifies they match the expected lock status
  7. If any mismatch is detected in either the AXI user values or lock status, MCU ROM reports a fatal error (ROM_SOC_MCU_MBOX_AXI_USER_VERIFY_FAILED)

Warm Reset Considerations

On warm reset, SS_CONFIG_DONE_STICKY remains set from the cold boot, so sticky registers are already locked and do not need to be reconfigured or verified. However, SS_CONFIG_DONE is cleared on warm reset, so:

  • MCU ROM must set SS_CONFIG_DONE after configuring any non-sticky registers
  • MCU ROM must verify that SS_CONFIG_DONE is actually set
  • Verification of sticky registers (PK hashes, MCU mailbox AXI users) is not required on warm reset since they are already locked

Device Ownership Transfer

This contains details about the Caliptra implementation of Device Ownership Transfer (DOT) with MCU to assist.

Device Ownership Transfer (DOT) is a security mechanism implemented in Caliptra that enables device owners to establish code signing capabilities rooted in the hardware root of trust without permanently burning the Code Authentication Key (CAK) into fuses. This provides flexibility in ownership management while maintaining strong security guarantees.

Reference: OCP Device Ownership Transfer specification.

Table of Contents

  1. Diagrams
  2. Glossary
  3. DOT Modes
  4. Cryptographic Binding Mechanism
  5. System Components
  6. State Machine
  7. Initialization Flow
  8. Runtime Commands
  9. Lifecycle Transitions
  10. Recovery Mechanisms
  11. Security Considerations

Diagrams


Glossary

BMC (Baseboard Management Controller): System management controller that interfaces with Caliptra to issue DOT commands and manage recovery procedures.

CAK (Code Authentication Key): The public key used to authenticate firmware and code running on the device. This is the owner's code signing key rooted in Caliptra.

Caliptra: Hardware root of trust providing secure boot and cryptographic services.

Caliptra_Core: Component within Caliptra that performs cryptographic operations offload (key derivation, HMAC, signature verification), derives DOT_EFFECTIVE_KEY, authenticates DOT_BLOBs and commands, and manages owner public key hash.

Caliptra_MCU: Microcontroller component that manages DOT state machine, handles runtime commands, controls fuse burning operations, coordinates with Caliptra_Core, and manages Ownership_Storage.

DOT (Device Ownership Transfer): Security mechanism for flexible ownership management that enables device owners to establish code signing capabilities rooted in hardware without permanently burning keys into fuses.

DOT_BLOB: A cryptographically authenticated data structure containing the CAK and LAK, sealed with the DOT_EFFECTIVE_KEY via HMAC. Stored in external flash storage.

DOT_EFFECTIVE_KEY: A key derived from DOT_ROOT_KEY and the DOT_FUSE_ARRAY value, used to authenticate DOT_BLOBs via HMAC. The derivation varies based on EVEN/ODD state.

DOT_FUSE_ARRAY: A minimal fuse array using 1 bit per state change to track DOT state transitions. The fuse value acts as a counter that increments with each state change (one-time programmable).

DOT_ROOT_KEY: A hardware-derived secret key unique to the silicon, used as the basis for deriving DOT_EFFECTIVE_KEY. Provides silicon binding.

EVEN STATE: Uninitialized/Volatile state (fuse value % 2 == 0) where no persistent ownership is bound to the silicon.

FMC (First Mutable Code): First stage of mutable firmware. In Caliptra MCU architecture, FMC and RT are not differentiated as separate binaries (FMC is RT).

HMAC (Hash-based Message Authentication Code): Cryptographic authentication method used to seal and verify DOT_BLOBs.

LAK (Lock Authentication Key): The private key used to lock/unlock the DOT state and control the Disabled state. The entity possessing LAK.priv has the authority to:

  • Lock ownership to the device (DOT_LOCK)
  • Disable DOT while maintaining ownership (DOT_DISABLE)
  • Unlock and release ownership (DOT_UNLOCK)

ODD STATE: Locked/Disabled state (fuse value % 2 == 1) where ownership is cryptographically bound to the silicon via DOT_BLOB.

Ownership_Storage: Volatile memory (e.g., FLOP-based register) that stores the current CAK and LAK during runtime. Must be retained across at least one MCU reset level and invalidated power cycle. Contents are not updatable once marked valid by a non-Caliptra entity. Also stores desired DOT_FUSE_ARRAY state for pending transitions.

ROM (Read-Only Memory): Immutable boot code that executes first on device startup.

RT (Runtime): Main operating firmware that executes after ROM and FMC initialization.

VendorKey: Vendor master key used for DOT_OVERRIDE operations in catastrophic recovery scenarios. Highest privilege key in the system.

Volatile DOT: Operating mode where CAK is installed per boot cycle and ownership is lost on power cycle. No fuse burning required.

Mutable Locking DOT: Operating mode where ownership is locked and bound to silicon via cryptographic binding using DOT_FUSE_ARRAY. Persists across power cycles.

Goals

  • Enable owner-specific code signing rooted in Caliptra (the root of trust)
  • Avoid permanent fuse programming for ownership keys
  • Support both temporary (volatile) and persistent (mutable locking) ownership models
  • Provide secure ownership transfer mechanisms
  • Enable recovery from corrupted states

DOT Modes

1. Volatile DOT

Characteristics:

  • CAK is installed per boot cycle
  • Ownership information is stored only in Ownership_Storage
  • Power cycle clears ownership
  • No fuse burning required
  • DOT_FUSE_ARRAY remains in EVEN state

Use Cases:

  • Temporary ownership scenarios

Flow:

Uninitialized (EVEN) → [DOT_CAK_INSTALL] → Volatile (EVEN) → [Power Cycle] → Uninitialized (EVEN)

2. Mutable Locking DOT

Characteristics:

  • CAK is locked and bound to silicon via cryptographic binding
  • Uses DOT_FUSE_ARRAY to create cryptographic binding (1 bit per state change)
  • Ownership persists across power cycles
  • Requires LAK authentication for lock/unlock/disable operations
  • DOT_BLOB stored in external storage (flash)
  • State transitions require fuse burning
  • Supports both Locked (with CAK) and Disabled (without CAK) states

Use Cases:

  • Long-term ownership binding

Flow:

Uninitialized (EVEN) → Volatile (EVEN) → [DOT_LOCK] → Locked (ODD) → [DOT_UNLOCK] → Volatile (EVEN) → Uninitialized (EVEN)
                     ↘ [DOT_DISABLE] → Disabled (ODD) → [DOT_UNLOCK] → Uninitialized (EVEN)

Cryptographic Binding Mechanism

The mutable locking DOT mechanism achieves secure binding without secure storage through a clever cryptographic construction:

Key Derivation

The DOT_EFFECTIVE_KEY is derived as follows:

DOT_EFFECTIVE_KEY = KDF(DOT_ROOT_KEY, DOT_FUSE_ARRAY_VALUE)

State-Dependent Derivation

In EVEN STATE (n):

  • DOT_EFFECTIVE_KEY is derived using DOT_FUSE_ARRAY value (n+1)
  • This represents the key that will seal the NEXT DOT_BLOB
  • Allows pre-computation before fuse burning

In ODD STATE (n):

  • DOT_EFFECTIVE_KEY is derived using DOT_FUSE_ARRAY value (n)
  • This represents the key that authenticates the CURRENT DOT_BLOB
  • Used to verify the stored DOT_BLOB on boot

DOT_BLOB Authentication

DOT_BLOB = {CAK, LAK, metadata}
HMAC_TAG = HMAC-SHA-512(DOT_EFFECTIVE_KEY, DOT_BLOB)

The DOT_BLOB is authenticated on every boot in ODD state to ensure the CAK and LAK is authentic.

Security Properties

  1. Binding to Silicon: DOT_ROOT_KEY is unique per device, preventing DOT_BLOB portability
  2. Binding to State: Fuse value is incorporated into key derivation, preventing rollback attacks
  3. Forward Security: Unlocking increments fuses, invalidating old DOT_BLOBs
  4. No Secure Storage Required: Cryptographic binding replaces need for secure non-volatile storage

System Components

Caliptra_MCU

  • Manages DOT state machine
  • Handles runtime commands
  • Controls fuse burning operations
  • Coordinates with Caliptra_Core for cryptographic operations
  • Manages Ownership_Storage

Caliptra_Core

  • Performs cryptographic operations offload(key derivation, HMAC, signature verification)
  • Derives DOT_EFFECTIVE_KEY
  • Authenticates DOT_BLOBs and commands
  • Manages owner public key hash (SET_OWNER_PK_HASH)

DOT_FUSE_ARRAY

  • Hardware fuse array
  • Stores state counter (increments from 0 to maximum)
  • Read during initialization
  • Written during state transitions
  • One-time programmable (OTP) per bit

Ownership_Storage

  • Volatile storage for current CAK and LAK (ex, FLOP based register)
  • Content must be retained across at least one MCU reset level; should be retained across as many MCU reset levels as possible
  • Ownership data must be invalidated and/or scrubbed on power cycle
  • Ownership data must not be updatable once marked as valid by a non-Caliptra entity
  • Stores desired DOT_FUSE_ARRAY state for pending transitions

Storage (Flash)

  • Non-volatile external storage
  • Stores DOT_BLOB (with redundancy)
  • Not assumed to be secure

BMC (Baseboard Management Controller)

  • Issues DOT commands
  • Manages recovery procedures
  • Handles reset coordination
  • May maintain backup DOT_BLOBs

State Machine

States

1. Uninitialized (EVEN State)

  • DOT_FUSE_ARRAY is in EVEN state
  • No CAK in Ownership_Storage
  • Device boots without owner authentication
  • No DOT_BLOB exists

2. Volatile (EVEN State)

  • DOT_FUSE_ARRAY is in EVEN state
  • CAK present in Ownership_Storage
  • Device boots with owner authentication
  • Ownership lost on power cycle
  • No DOT_BLOB present

3. Locked (ODD State)

  • DOT_FUSE_ARRAY is in ODD state
  • CAK present in Ownership_Storage (retrieved from DOT_BLOB)
  • DOT_BLOB authenticated
  • Device boots with owner authentication
  • Ownership persists across power cycles
  • DOT_BLOB present in storage

4. Disabled (ODD State)

  • DOT_FUSE_ARRAY is in ODD state
  • LAK present in DOT_BLOB, but no CAK
  • Device boots without code authentication enforcement
  • Ownership is locked to silicon (via LAK) preventing unauthorized takeover
  • Useful when owner doesn't want code signing but wants to prevent others from claiming ownership

5. Corrupted (ODD State)

  • DOT_FUSE_ARRAY is in ODD state
  • DOT_BLOB is corrupted or missing
  • Device boots into recovery mode (ROM or FMC)
  • No CAK available
  • Special recovery or override commands accepted

State Transitions

         ┌─────────────────┐
         │ Uninitialized   │ (EVEN, n)
         │ No CAK          │
         └────────┬────────┘
                  │
                  ├─────────────────────────────────────┐
                  │ DOT_CAK_INSTALL + Reset             │ DOT_DISABLE + Reset + Fuse Burn
                  ▼                                     ▼
         ┌─────────────────┐                  ┌─────────────────┐
         │ Volatile        │ (EVEN, n)        │ Disabled        │ (ODD, n+1)
         │ CAK in RAM      │                  │ LAK from BLOB   │
         └────────┬────────┘                  │ No CAK          │
                  │ DOT_LOCK + Reset          └────────┬────────┘
                  │ + Fuse Burn                        │ DOT_UNLOCK + Reset + Fuse Burn
                  ▼                                    │
         ┌─────────────────┐                           │
         │ Locked          │ (ODD, n+1)                │
         │ CAK from BLOB   │                           │
         └────────┬────────┘                           │
                  │ DOT_UNLOCK + Reset + Fuse Burn     │
                  ▼                                    │
         ┌─────────────────┐                           │
         │ Volatile        │ (EVEN, n+2)               │
         │ CAK in RAM      │                           │
         └────────┬────────┘                           │
                  │ Power Cycle                        │
                  └─────────────┬──────────────────────┘
                                ▼
                       ┌─────────────────┐
                       │ Uninitialized   │ (EVEN, n+2)
                       │ No CAK          │
                       └─────────────────┘

Initialization Flow

Boot Sequence (DOT ROM Startup)

  1. MCU ROM Startup

    • Caliptra_MCU boots and starts MCU ROM
    • MCU ROM initializes Caliptra_Core
  2. Read DOT_FUSE_ARRAY State

    • MCU ROM reads current DOT_FUSE_ARRAY value (n)
    • Determines if state is EVEN or ODD
  3. Derive DOT_EFFECTIVE_KEY

    • MCU ROM calls Caliptra_Core ROM to derive key
    • If EVEN state: Derive with (n+1) for next DOT_BLOB sealing
    • If ODD state: Derive with (n) for current DOT_BLOB authentication
  4. ODD State Processing

    • Read DOT_BLOB from storage
    • Authenticate DOT_BLOB using HMAC with DOT_EFFECTIVE_KEY
    • If authentic: Extract CAK/LAK and program into Ownership_Storage
    • If corrupted: Boot into DOT recovery mode (recovery flow documented in later diagram)
  5. Set Owner Public Key

    • Read CAK from Ownership_Storage (if present)
    • Call Caliptra_Core ROM SET_OWNER_PK_HASH with CAK
  6. Boot Firmware

    • Call Caliptra_Core ROM RI_DOWNLOAD_FIRMWARE
    • MCU ROM resets and jumps to RT (Runtime)

State Determination Logic

if (DOT_FUSE_ARRAY % 2 == 0) {
    // EVEN STATE - Unlocked/Disabled
    DOT_EFFECTIVE_KEY = Derive(DOT_ROOT_KEY, n+1)
    // Ready to seal new DOT_BLOB
} else {
    // ODD STATE - Locked/Enabled
    DOT_EFFECTIVE_KEY = Derive(DOT_ROOT_KEY, n)
    DOT_BLOB = Read_Storage()
    if (Authenticate(DOT_BLOB, DOT_EFFECTIVE_KEY)) {
        CAK, LAK = Extract(DOT_BLOB)
        Write_Ownership_Storage(CAK, LAK)
    } else {
        Boot_Recovery_Mode()
    }
}

Runtime Commands

1. DOT_CAK_INSTALL

Purpose: Install CAK for volatile DOT ownership.

Preconditions:

  • No existing ownership (Ownership_Storage empty)
  • DOT_FUSE_ARRAY is in EVEN state

Flow:

  1. BMC issues DOT_CAK_INSTALL command with CAK (and optionally LAK)
  2. MCU RT checks Ownership_Storage for existing ownership
    • If ownership exists: Return error
  3. MCU RT writes CAK/LAK to Ownership_Storage
  4. Request subsystem reset (lower level reset that should not clear out Ownership_Storage)
  5. On next boot, CAK will be active for firmware authentication

Result: Device enters Volatile state with CAK active until power cycle.

2. DOT_LOCK

Purpose: Lock current volatile ownership to silicon, creating mutable locking DOT.

Preconditions:

  • DOT_FUSE_ARRAY in EVEN state
  • CAK present in Ownership_Storage

Flow:

  1. BMC issues DOT_LOCK command (signed with LAK.priv)
  2. MCU RT checks DOT_FUSE_ARRAY state
    • If ODD state: Return error (already locked)
  3. MCU RT authenticates command with LAK.pub
    • If authentication fails: Return error
  4. MCU RT creates DOT_BLOB containing CAK and LAK
  5. MCU RT seals DOT_BLOB with HMAC-SHA-512(DOT_EFFECTIVE_KEY, DOT_BLOB)
    • DOT_EFFECTIVE_KEY was derived with (n+1) during boot
  6. Write DOT_BLOB to storage (primary and redundant copies)
  7. Update Ownership_Storage with desired DOT_FUSE_ARRAY state = (n+1)
  8. Return success and request subsystem reset
  9. On next boot (see State Management, diagram reference: dot-3-state):
    • ROM/FMC reads Ownership_Storage desired state
    • Verifies DOT_BLOB is valid in storage
    • Burns DOT_FUSE_ARRAY from (n) to (n+1)
    • Requests another reset
  10. On subsequent boot:
    • Device boots in ODD state (n+1)
    • DOT_BLOB is authenticated and CAK/LAK retrieved
    • Device is now in Locked state

Result: Device enters Locked state with ownership persisting across power cycles.

3. DOT_DISABLE

Diagram is above.

Purpose: Lock DOT mechanism in disabled state directly from uninitialized state, maintaining ownership control without code authentication requirements.

Use Case: When the owner does not want to sign code with CAK but also does not want to leave the system in an uninitialized state where others could take over ownership. This provides a secure "parked" state where:

  • The device remains under the owner's control (via LAK)
  • No code authentication is enforced (no CAK)
  • Unauthorized parties cannot install their own CAK or disable the device
  • The owner can later unlock to return to uninitialized state

Preconditions:

  • DOT_FUSE_ARRAY in EVEN state
  • Device in Uninitialized state (no ownership in Ownership_Storage)
  • LAK provided in command

Flow:

  1. BMC issues DOT_DISABLE command with LAK (signed with LAK.priv)
  2. MCU RT checks DOT_FUSE_ARRAY state
    • If ODD state: Return error (already locked or disabled)
  3. MCU RT checks Ownership_Storage is empty (Uninitialized state)
    • If ownership exists: Return error
  4. MCU RT authenticates command with LAK.pub
    • If authentication fails: Return error
  5. MCU RT creates DOT_BLOB containing LAK but no CAK
  6. MCU RT seals DOT_BLOB with HMAC-SHA512(DOT_EFFECTIVE_KEY, DOT_BLOB)
  7. Write DOT_BLOB to storage (primary and redundant copies)
  8. Update Ownership_Storage with desired DOT_FUSE_ARRAY state = (n+1)
  9. Return success and request subsystem reset
  10. On next boot (see State Management):
    • ROM/FMC reads Ownership_Storage desired state
    • Verifies DOT_BLOB is valid in storage
    • Burns DOT_FUSE_ARRAY from (n) to (n+1)
    • Requests another reset
  11. On subsequent boot:
    • Device boots in ODD state (n+1)
    • DOT_BLOB is authenticated and LAK recovered (but no CAK)
    • Device is now in Disabled state

Result: Device enters Disabled state - ownership is locked to the silicon (preventing takeover) but no code authentication is active. The owner retains authority to unlock via LAK.priv, which will return the device to Uninitialized state.

4. DOT_UNLOCK_CHALLENGE / DOT_UNLOCK

Purpose: Unlock and unbind ownership from silicon, returning to volatile or uninitialized state.

Preconditions:

  • DOT_FUSE_ARRAY in ODD state (Locked or Disabled)
  • Valid DOT_BLOB in storage (for LAK.pub)

Flow:

  1. BMC issues DOT_UNLOCK_CHALLENGE
  2. MCU RT checks DOT_FUSE_ARRAY state
    • If EVEN state: Return error (already unlocked)
  3. MCU RT generates and returns challenge
    • Challenge based on Unlock_Method from previous LOCK/DISABLE
  4. BMC provides signed challenge with LAK.priv
  5. BMC issues DOT_UNLOCK command with signed challenge
  6. MCU RT verifies challenge matches
  7. MCU RT authenticates command using LAK.pub from DOT_BLOB
    • If authentication fails: Return error
  8. MCU RT updates Ownership_Storage with desired DOT_FUSE_ARRAY state = (n+1)
  9. Return success and request subsystem reset
  10. On next boot (see State Management):
    • ROM/FMC reads Ownership_Storage desired state
    • Burns DOT_FUSE_ARRAY from (n) to (n+1)
    • Erases DOT_BLOB from storage (no longer needed)
    • Requests another reset
  11. On subsequent boot:
    • Device boots in EVEN state (n+1)
    • If unlocked from Locked state: CAK still in Ownership_Storage → enters Volatile state
    • If unlocked from Disabled state: No CAK in Ownership_Storage → enters Uninitialized state

Result:

  • From Locked: Device enters Volatile state. Power cycle will return to Uninitialized.
  • From Disabled: Device enters Uninitialized state immediately (no CAK was present).

State Management

State transitions require fuse burning, which is performed by ROM/FMC (not RT) for security reasons. The Ownership_Storage is used to communicate the desired state change from RT to ROM/FMC.

State Transition Protocol

Executed by ROM/FMC on boot:

  1. Check Ownership_Storage for desired DOT_FUSE_ARRAY state
  2. Verify desired state is exactly (n+1) where n is current state
  3. If transition from EVEN to ODD (LOCK/DISABLE):
    • Read DOT_BLOB from storage
    • Authenticate DOT_BLOB with DOT_EFFECTIVE_KEY
    • If invalid: Abort (should not happen if RT did its job)
    • Increment DOT_FUSE_ARRAY from (n) to (n+1)
  4. If transition from ODD to EVEN (UNLOCK/ENABLE):
    • Increment DOT_FUSE_ARRAY from (n) to (n+1)
    • Erase DOT_BLOB from storage
  5. Request subsystem reset

ROM vs FMC Implementation Options

The DOT specification allows integrators of Caliptra MCU to choose which component handles fuse burning and state transitions. This flexibility accommodates different security models and implementation constraints.

Option 1: ROM-Based State Transitions

Characteristics:

  • ROM performs all fuse burning and state transition operations
  • Provides a known, immutable, and trusted implementation
  • State transitions occur in the most trusted execution environment

Advantages:

  • ROM provides a sane and known state
  • Immutable code reduces attack surface
  • Consistent behavior across all boots

Disadvantages:

  • Requires complex and error-prone code (fuse burning) to be in ROM
  • ROM must implement complicated protocols with BMC or other EC
  • Any bugs or limitations in ROM cannot be fixed without hardware revision
  • Less flexibility for integrators to customize behavior

Option 2: FMC-Based State Transitions

Characteristics:

  • FMC/RT performs fuse burning and state transition operations
  • Note: In Caliptra MCU architecture, FMC and RT are not differentiated as separate binaries (FMC is RT)
  • ROM indicates to FMC that it is in a DOT-related recovery/transition boot

Advantages:

  • Gives integrators more flexibility to implement fuse burning in mutable code
  • Easier to update and fix issues in the field
  • Simplifies ROM implementation
  • Allows customization of protocols and recovery flows

Disadvantages:

  • Requires ROM to signal FMC about DOT recovery/transition boot mode
  • FMC must be trusted to handle state transitions correctly
  • FMC must block normal system boot and only conduct DOT recovery/override operations when in recovery mode

Requirements for FMC Implementation:

  • ROM must indicate to FMC that it is in DOT-related recovery/transition boot
  • FMC must not allow full system boot during DOT operations
  • FMC must only accept DOT recovery/override commands in recovery mode
  • FMC must properly validate and execute state transitions

Implementation Guidelines

Regardless of which option is chosen:

  • Fuse burning is a sensitive operation and must not be performed by RT during normal operation
  • The component performing fuse burning must validate DOT_BLOB before committing to fuse changes
  • State transitions must be atomic (fuse + storage operations together)
  • The system must be able to recover from partial state transitions

Lifecycle Transitions

Full Lifecycle: Uninitialized → Volatile → Locked

Phase 1: Uninitialized → Volatile

  1. Device boots with DOT_FUSE_ARRAY in EVEN state
  2. Ownership_Storage is empty
  3. No owner authentication active
  4. BMC issues DOT_CAK_INSTALL with CAK and LAK
  5. CAK/LAK written to Ownership_Storage
  6. Reset device
  7. Device boots with CAK active for authentication
  8. Result: Volatile ownership (lost on power cycle)

Phase 2: Volatile → Locked

  1. Device in Volatile state (EVEN, CAK in RAM)
  2. BMC issues DOT_LOCK command (signed with LAK.priv)
  3. Command authenticated with LAK.pub
  4. MCU RT creates DOT_BLOB with CAK and LAK
  5. DOT_BLOB sealed with HMAC using DOT_EFFECTIVE_KEY
  6. DOT_BLOB written to flash storage
  7. Ownership_Storage updated with desired state (n+1)
  8. Reset device
  9. ROM/FMC reads desired state from Ownership_Storage
  10. ROM/FMC validates DOT_BLOB in storage
  11. ROM/FMC burns DOT_FUSE_ARRAY from EVEN(n) to ODD(n+1)
  12. Reset device again
  13. Device boots in ODD state
  14. DOT_BLOB authenticated and CAK/LAK recovered to Ownership_Storage
  15. Result: Locked ownership (persists across power cycles)

Full Lifecycle: Locked → Volatile → Uninitialized

Phase 1: Locked → Volatile

  1. Device in Locked state (ODD, CAK from DOT_BLOB)
  2. BMC issues DOT_UNLOCK_CHALLENGE
  3. MCU RT returns challenge
  4. BMC signs challenge with LAK.priv
  5. BMC issues DOT_UNLOCK command with signed challenge
  6. MCU RT verifies challenge and authenticates with LAK.pub
  7. Ownership_Storage updated with desired state (n+1)
  8. Reset device
  9. ROM/FMC reads desired state
  10. ROM/FMC burns DOT_FUSE_ARRAY from ODD(n) to EVEN(n+1)
  11. ROM/FMC erases DOT_BLOB from storage
  12. Reset device again
  13. Device boots in EVEN state with CAK still in Ownership_Storage
  14. Result: Volatile ownership (temporary)

Phase 2: Volatile → Uninitialized

  1. Device in Volatile state (EVEN, CAK in RAM)
  2. BMC power cycles device
  3. Ownership_Storage contents lost
  4. Device boots with no CAK
  5. Result: Uninitialized state (no ownership)

Recovery Mechanisms

Failure scenario in dot-1-init

Detection:

  • Device in ODD state (should have valid DOT_BLOB)
  • DOT_BLOB read from storage
  • HMAC authentication fails

Recovery Flow:

  1. MCU ROM detects corrupted DOT_BLOB during initialization
  2. MCU ROM boots MCU RT without CAK/LAK
  3. MCU RT flag indicates DOT recovery mode
  4. RT only accepts recovery commands (no normal boot)
  5. BMC detects silicon in DOT recovery mode
    • Via boot progress tracking
    • Via error codes
    • Via explicit notification
  6. MCU ROM/FMC conduct DOT recovery flow
    • Option 1: Recovery (BMC or EC has backup DOT_BLOB)
    • Option 2: Override (Vendor only command)

Option 1: DOT_RECOVERY Command

When: BMC has a backup copy of the DOT_BLOB

Flow:

  1. BMC issues DOT_RECOVERY command with backup DOT_BLOB
  2. MCU authenticates DOT_BLOB with DOT_EFFECTIVE_KEY
    • If authentication fails: Return error and abort
  3. MCU writes authenticated DOT_BLOB to flash
  4. Request subsystem reset
  5. On next boot, DOT_BLOB will be valid
  6. Result: Device returns to Locked state with recovered ownership

Requirements:

  • BMC must maintain backup DOT_BLOB
  • DOT_BLOB must match current DOT_FUSE_ARRAY state
  • Cannot recover if backup is also corrupted

Option 2: DOT_OVERRIDE Command

When: BMC does not have backup DOT_BLOB (catastrophic recovery, RMA to vendor)

Flow:

  1. BMC issues DOT_UNLOCK_CHALLENGE
  2. MCU returns challenge
  3. BMC signs challenge with Vendor private key (VendorKey.priv)
  4. BMC issues DOT_OVERRIDE command with signed challenge
  5. MCU verifies challenge matches
  6. MCU authenticates command with Vendor public key (VendorKey.pub)
    • If authentication fails: Return error and abort
  7. MCU burns DOT_FUSE_ARRAY from ODD(n) to EVEN(n+1)
    • This is an ODD to EVEN transition only
  8. Request subsystem reset
  9. On next boot, device is in EVEN state (Uninitialized)
  10. Result: Device unlocked, but ownership lost (factory reset equivalent)

Recovery State Machine

Locked (ODD, n) + Corrupted BLOB
    ↓
Recovery Mode
    ├─→ [DOT_RECOVERY with valid backup] → Locked (ODD, n) [restored]
    └─→ [DOT_OVERRIDE with Vendor key] → Uninitialized (EVEN, n+1) [ownership lost]

Security Considerations

Security Properties

  1. Silicon Binding

    • DOT_ROOT_KEY is unique per device, for Caliptra 2.0/2.1, it is up to the integrator to decide how to form this per-device unique key.
    • DOT_BLOBs cannot be authenticated / used between devices of the same model
    • Each device requires its own unique DOT_BLOB
  2. State Binding

    • DOT_EFFECTIVE_KEY incorporates fuse state
    • Old DOT_BLOBs become invalid after unlock
    • Prevents rollback to previous ownership states
  3. Forward Security

    • Unlocking increments fuses
    • Previous DOT_BLOBs cannot be replayed
    • Each lock operation requires new DOT_BLOB
  4. Authenticity

    • DOT_LOCK requires LAK.priv signature
    • DOT_DISABLE requires LAK.priv signature
    • DOT_UNLOCK requires LAK.priv signature
    • DOT_OVERRIDE requires VendorKey.priv signature
    • All commands that touches fuse state are cryptographically authenticated
  5. Ownership Protection

    • Disabled state prevents unauthorized ownership takeover
    • Owner maintains control via LAK even without CAK enforcement
    • Uninitialized devices can be claimed by any party, Disabled devices cannot
  6. Minimal Fuse Usage

    • Only 1 bit per state change
    • Efficient use of limited fuse resources
    • Supports many lock/unlock cycles

Threat Model

Protected Against:

  • Unauthorized ownership changes (requires LAK.priv)
  • DOT_BLOB tampering (HMAC-authenticated)
  • DOT_BLOB replay from different device (silicon binding)
  • DOT_BLOB replay from previous state (state binding)
  • Rollback attacks (fuse increment prevents rollback)
  • Storage attacks (cryptographic binding, no trust in storage)

Not Protected Against:

  • Physical attacks on fuses (assumed secure)
  • Compromise of DOT_ROOT_KEY (silicon security boundary)
  • Compromise of LAK.priv (ownership credential)
  • Compromise of VendorKey.priv (vendor master key)
  • Side-channel attacks on cryptographic operations

Best Practices

  1. LAK Management

    • Store LAK.priv securely (HSM, secure enclave)
    • Control LAK.priv access carefully
    • Consider key escrow for recovery scenarios
    • Implement proper key rotation when re-locking
  2. DOT_BLOB Backup

    • Maintain redundant backups of DOT_BLOB
    • Store in multiple locations
    • Verify backups periodically
    • Secure backup storage (integrity and availability)
  3. State Transition Management

    • Verify DOT_BLOB integrity before DOT_LOCK fuse burn
    • Ensure storage is reliable
    • Handle reset failures gracefully
    • Monitor fuse budget (total available transitions)
  4. Recovery Planning

    • Define recovery procedures between device and BMC or EC
    • Test recovery scenarios
    • Maintain secure access to VendorKey.priv
    • Document override procedures
  5. Operational Security

    • Audit DOT command invocations
    • Monitor DOT state changes
    • Alert on recovery mode entries
    • Track fuse usage over time

Fuse Budget Planning

Each state transition consumes one fuse bit:

Uninitialized(0) → Volatile(0) → Locked(1) → Volatile(2) → Locked(3) → ...

With N fuse bits:

  • Maximum N state transitions
  • Approximately N/2 lock/unlock cycles
  • Plan for expected device lifetime
  • Reserve bits for recovery scenarios

Example with 128-bit fuse array:

  • 256 total state transitions
  • ~128 lock/unlock cycles
  • More than sufficient for most use cases

Appendix: Command Reference

Command Summary Table

CommandState RequirementAuthenticationFuse ImpactPurpose
DOT_CAK_INSTALLEVENNoneNoInstall volatile ownership
DOT_LOCKEVENLAK.privYes (n→n+1)Lock ownership to silicon
DOT_DISABLEEVENLAK.privYes (n→n+1)Disable DOT in locked state
DOT_UNLOCK_CHALLENGEODDNoneNoRequest unlock challenge
DOT_UNLOCKODDLAK.privYes (n→n+1)Unlock ownership from silicon
DOT_RECOVERYODD (Recovery)DOT_BLOB HMACNoRestore corrupted DOT_BLOB
DOT_OVERRIDEODD (Recovery)VendorKey.privYes (n→n+1)Force unlock (destructive)

State Transition Table

From StateCommandTo StateFuse ChangeStorage Change
Uninitialized (EVEN)DOT_CAK_INSTALLVolatile (EVEN)NoNone
Volatile (EVEN)Power CycleUninitialized (EVEN)NoNone
Volatile (EVEN)DOT_LOCKLocked (ODD)EVEN→ODDCreate DOT_BLOB
Volatile (EVEN)DOT_DISABLEDisabled (ODD)EVEN→ODDCreate DOT_BLOB (no CAK)
Locked (ODD)DOT_UNLOCKVolatile (EVEN)ODD→EVENDelete DOT_BLOB
Locked (ODD)Corrupted BLOBRecovery (ODD)NoNone
Recovery (ODD)DOT_RECOVERYLocked (ODD)NoRestore DOT_BLOB
Recovery (ODD)DOT_OVERRIDEUninitialized (EVEN)ODD→EVENDelete DOT_BLOB

Runtime Specification

The MCU runtime firmware is based on the Tock real-time, embedded operation system. Tock provides the ability for us to run multiple high-level applications concurrently and securely. For more specific information on how Tock internals work, please see the Tock kernel documentation.

Any RISC-V code that needs to run in M-mode, e.g., low-level drivers, should run in the Tock board or a capsule loaded by the board.

In Tock, the "board" is the code that runs that does all of the hardware initialization and starts the Tock kernel. The Tock board is essentially a custom a kernel for each SoC.

The applications are higher-level RISC-V code that only interact with the rest of the world through Tock system calls. For instance, an app might be responsible for running a PLDM flow and uses a Tock capsule to interact with the MCTP stack to communicate with the rest of the SoC.

Architecture

The overall architecture for the MCU firmware stack is thought of in layers:

  • At the highest layer are the user-mode applications that run specific flows that are relevant for vendors. These run more complex, dynamic protocols, like PLDM and SPDM.
  • These interact with common user-mode APIs for protocols like MCTP. These handle the details of converting low-level system calls to the Tock kernel and capsules into synchronous and asynchronous APIs.
    • Neither the applications nor the user-mode APIs have the ability to access hardware directly.
  • The Tock kernel is responsible for scheduling the applications and routing system calls to the appropriate capsules. Everything at the Tock kernel level and below execute in machine mode with full privileges.
  • The capsules are Tock kernel modules that implement specific specific workflows and capabilities, like using the MCTP stack or accessing the Caliptra mailbox.
    • Everything at the capsule layer and above should be independent of the hardware specifics. Everything below the capsule layer is specific to the hardware implementations.
  • The capsules in turn talk to specific drivers. These are generally implementations of specific Rust traits, like Tock HILs, that provide access to hardware.
  • Two of the most fundamental pieces of Rust code sit at the bottom: the chip and board files. The chip file contains microcontroller-specific code, such as dealing with interrupts, can should be able to be reused on different boards that use the same microcontroller.
  • The board file is the heart of Tock: it is the main() function, and is responsible for creating and initializing all of the hardware drivers, the chip file, creating the Tock kernel, loading and configuring the capsules, and starting the main execution loop.

PLDM Update Package

This section describes the PLDM Update Package used for Caliptra streaming boot and firmware update.

The update package follows the DMTF Firmware Update Specification v.1.3.0.

PLDM FW Update Package
Package Header Information
Firmware Dev ID Descriptors
Downstream Dev ID Descriptors
Component Image Information
Package Header Checksum
Package Payload Checksum
Component 1 (Caliptra FMC + RT)
Component 2 (SoC Manifest)
Component 3 (MCU RT)
Component 4 (SoC Image 1)
...
Component N (SoC Image N-3)
Component N + 1 (Streaming Boot Image for full flash)

The PLDM FW Update Package contains:

  1. Caliptra FMC (First Mutable Code) and RT (Runtime) Image Bundle
  2. The SOC Manifest for the MCU RT image and other SOC Images
  3. The MCU RT image
  4. Other SOC Images, if any
  5. A full image of the flash containing all images (see SPI Flash Layout Documentation)

Note: All fields are little-endian byte-ordered unless specified otherwise.

Package Header Information

FieldSizeDefinition
PackageHeaderIdentifier16Set to 0x7B291C996DB64208801B0202E6463C78 (v1.3.0 UUID) (big endian)
PackageHeaderFormatRevision1Set to 0x04 (v1.3.0 header format revision)
PackageHeaderSize2The total byte count of this header structure, including fields within the Package Header Information,
Firmware Device Identification Area, Downstream Device Identification Area,
Component Image Information Area, and Checksum sections.
PackageReleaseDateTime13The date and time when this package was released.
Refer to the PLDM Base Specification for field format definition.
ComponentBitmapBitLength2Number of bits used to represent the bitmap in the ApplicableComponents field for a matching device.
This value is a multiple of 8 and is large enough to contain a bit for each component in the package.
PackageVersionStringType1The type of string used in the PackageVersionString field.
Refer to DMTF Firmware Update Specification v.1.3.0 Table 33 for values.
PackageVersionStringLength1Length, in bytes, of the PackageVersionString field.
PackageVersionStringVariablePackage version information, up to 255 bytes.
Contains a variable type string describing the version of this firmware update package.

Firmware Device ID Descriptor

FieldSizeDefinition
RecordLength2The total length in bytes for this record. The length includes the RecordLength, DescriptorCount, DeviceUpdateOptionFlags, ComponentImageSetVersionStringType, ComponentSetVersionStringLength, FirmwareDevicePackageDataLength, ApplicableComponents, ComponentImageSetVersionString, and RecordDescriptors, and FirmwareDevicePackageData fields.
DescriptorCount1The number of descriptors included within the RecordDescriptors field for this record.
DeviceUpdateOptionFlags432-bit field where each bit represents an update option.
bit 1 is set to 1 (Support Flashless/Streaming Boot)
For other options, refer to DeviceUpdateOptionFlags description in DMTF Firmware Update Specification v.1.3.0.
ComponentImageSetVersionStringType1The type of string used in the ComponentImageSetVersionString field. Refer to DMTF Firmware Update Specification v.1.3.0 Table 33 for values.
ComponentImageSetVersionStringLength1Length, in bytes, of the ComponentImageSetVersionString.
FirmwareDevicePackageDataLength2Length in bytes of the FirmwareDevicePackageData field.
If no data is provided, set to 0x0000.
ReferenceManifestLength4Length in bytes of the ReferenceManifestData field. If no data is provided, set to 0x00000000.
ApplicableComponentsVariableBitmap indicating which firmware components apply to devices matching this Device Identifier record. A set bit indicates the Nth component in the payload is applicable to this device.
bit 0: Caliptra FMC + RT
bit 1: SOC Manifest
bit 2 : MCU RT
bit 3 to N: Reserved for other SoC images
bit N+1: Full SPI Flash image used in streaming boot
ComponentImageSetVersionStringVariableComponent Image Set version information, up to 255 bytes.
Describes the version of component images applicable to the firmware device indicated in this record.
RecordDescriptorsVariableThese descriptors are defined by the vendor (i.e. integrators of Caliptra ROT)
Refer to DMTF Firmware Update Specification v.1.3.0 Table 7 for details of these fields and the values that can be selected.
FirmwareDevicePackageDataVariableOptional data provided within the firmware update package for the FD during the update process.
If FirmwareDevicePackageDataLength is 0x0000, this field contains no data.
ReferenceManifestDataVariableOptional data field containing a Reference Manifest for the firmware update.
If present, it describes the firmware update provided by this package.
If ReferenceManifestLength is 0x00000000, this field contains no data.

Downstream Device ID Descriptor

There are no Downstream Device ID records for this package

FieldSizeDefinition
DownstreamDeviceIDRecordCount10

Component Image Information

FieldSizeDefinition
ComponentImageCount2Count of individually defined component images contained within this firmware update package.
ComponentImageInformationVariableContains details for each component image within this firmware update package.

Component Image Information Record

FieldSizeDefinition
ComponentClassification20x000A: For Caliptra FMC+RT  (Firmware)
0x0001: For SoC Manifest  (Other)
0x000A: For MCU RT: (Firmware)
For other SoC images, Refer to DMTF Firmware Update Specification v.1.3.0 Table 32 for values.
ComponentIdentifier2Unique value selected by the FD vendor to distinguish between component images.
0x0001: Caliptra FMC+RT
0x0002: SoC Manifest:
0x0003: MCU RT
0x1000-0xFFFF - Reserved for other vendor-defined SoC images
ComponentComparisonStamp4Value used as a comparison in determining if a firmware component is down-level or up-level. When ComponentOptions bit 1 is set, this field should use a comparison stamp format (e.g., MajorMinorRevisionPatch). If not set, use 0xFFFFFFFF.
ComponentOptions2Refer to ComponentOptions definition in DMTF Firmware Update Specification v.1.3.0
RequestedComponentActivationMethod2Refer to RequestedComponentActivationMethoddefinition inDMTF Firmware Update Specification v.1.3.0
ComponentLocationOffset4Offset in bytes from byte 0 of the package header to where the component image begins.
ComponentSize4Size in bytes of the Component image.
ComponentVersionStringType1Type of string used in the ComponentVersionString field. Refer toDMTF Firmware Update Specification v.1.3.0 Table 33 for values.
ComponentVersionStringLength1Length, in bytes, of the ComponentVersionString.
ComponentVersionStringVariableComponent version information, up to 255 bytes. Contains a variable type string describing the component version.
ComponentOpaqueDataLength4Length in bytes of the ComponentOpaqueData field. If no data is provided, set to 0x00000000.
ComponentOpaqueDataVariableOptional data transferred to the FD/FDP during the firmware update

Checksum

FieldSizeDefinition
PackageHeaderChecksum4The integrity checksum of the PLDM Package Header. It is calculated starting at the first byte of the PLDM Firmware Update Header and includes all bytes of the package header structure except for the bytes in this field and the PackagePayloadChecksum field. The CRC-32 algorithm with polynomial 0x04C11DB7 (as used by IEEE 802.3) is used for checksum computation, processing one byte at a time with the least significant bit first.
PackagePayloadChecksum4The integrity checksum of the PLDM Package Payload. It is calculated starting at the first byte immediately following this field and includes all bytes of the firmware package payload structure, including any padding between component images. The CRC-32 algorithm with polynomial 0x04C11DB7 (as used by IEEE 802.3) is used for checksum computation, processing one byte at a time with the least significant bit first.

Component 1 - Caliptra FMC + RT

This is the package bundle for the Caliptra FMC + RT defined in Caliptra Firmware Image Bundle Format.

Component 2 - SOC Manifest

This is the SoC Manifest defined in SoC Manifest.

It provides the signature verification and specific data needed to decode the image.

Component 3 - MCU RT

This is the Image binary of the MCU Realtime firmware.

Component 4 to N - SoC Images

These are reserved for vendor SoC images, if any.

Component N + 1 - Full SPI Flash image

This component contains the full flash image as defined in the SPI Flash Layout Documentation from the Header section until the end of the images section. This is can be used for loading all the images at once for streaming boot.

SPI Flash Layout

Overall, the SPI Flash consists of a Header, Checksum and an Image Payload (which includes the image information and the image binary).

The specific images of the flash consists of the Caliptra FW, MCU RT, SoC Manifest, and other SoC images, if any.

Layout

Note: All fields are little-endian byte-ordered unless specified otherwise.

A typical overall flash layout is:

Flash Layout
Header
Payload

The Payload contains the following fields:

Payload
Image Info (Caliptra FMC + RT)
Image Info (SoC Manifest)
Image Info (MCU RT)
Image Info (SoC Image 1)
...
Image Info (SoC Image N)
Caliptra FMC + RT Package
SoC Manifest
MCU RT
SoC Image 1
...
SoC Image N

The Header section contains the metadata for the images.

FieldSize (bytes)Description
Magic Number4A unique identifier to mark the start of the header.
The value must be 0x464C5348 ("FLSH" in ASCII) for flash or 0x54465450 ("TFTP" in ASCII) for Network Boot
Header Version2The header version format, allowing for backward compatibility if the package format changes over time.
(Current version is 0x0002)
Image Count2The number of images contained in the Payload.
Each image will have its own image information section.
Payload Offset4Offset in bytes of the header to where the first byte of the Payload is located.
Header Checksum4Checksum calculated for the header excluding this field

Image Information

The Image Information section is repeated for each image and provides detailed manifest data specific to that image.

FieldSize (bytes)Descr
Identifier4Vendor selected unique value to distinguish between images.
0x00000000: Caliptra FMC+RT
0x00000001: SoC Manifest
0x00000002: MCU RT
0x00001000-0xFFFFFFFF - Reserved for other Vendor-defined SoC images
ImageLocationOffset4Offset in bytes from byte 0 of the header to where the image content begins. Used in flash-based boot.
Size4Size in bytes of the image. This is the actual size of the image without padding.
The image itself as written to the flash should be 4-byte aligned and additional
padding will be required to guarantee alignment.
Filename64Used in network boot to specify the TFTP path relative to the TFTP server root. Field is not used in flash-based boot
Image Checksum4Checksum calculated for the binary image.
Image Info Checksum4Checksum calculated for the header excluding this field

Image

The images (raw binary data) are appended after the Image Information section, and should be in the same order as their corresponding Image Information.

FieldSize (bytes)Description
DataNImage content.
Note: The image should be 4-byte aligned.
If the size of a firmware image is not a multiple of 4 bytes,
0x00 padding bytes will be added to meet the alignment requirement.
Only used in flash-based boot

Flash Controller

Overview

The Flash Controller stack in the Caliptra MCU firmware is designed to provide efficient and reliable communication with flash devices, which is the foundation to enable flash-based boot flow.

We will primarily be targetting SPI flash controllers, so we will use that as our primary example throughout.

This document outlines the different SPI flash configurations being supported, the stack architecture, component interface and userspace API to interact with the SPI flash stack.

SPI Flash Configurations

The SPI flash stack supports various configurations to cater to different use cases and hardware setups. The diagram below shows the flash configurations supported.

flash_config

1. Single-Flash Configuration In this setup, a single SPI flash device is connected to the flash controller. Typically, the flash device is divided into two halves: the first half serves as the primary flash, storing the active running firmware image, while the second half is designated as the recovery flash, containing the recovery image. Additional partitions, such as a staging area for firmware updates, flash store for certificates and debug logging can also be incorporated into the primary flash.

2. Dual-Flash Configuration In this setup, two SPI flash devices are connected to the same flash controller using different chip selects. This configuration provides increased storage capacity and redundancy. Typically, flash device 0 serves as the primary flash, storing the active running firmware image and additional partitions such as a staging area for firmware updates, flash store for certificates and debug logging. Flash device 1 is designated as the recovery flash, containing the recovery image.

3. Multi-Flash Configuration In more complex systems, multiple flash controllers may be used, each with one or more SPI flash devices. This configuration provides flexibility and scalability. For example, a backup flash can be added to recover the SoC and provide more resiliency for the system.

Architecture

The SPI flash stack design leverages TockOS's kernel space support for flash and associated virtualizer layers. The stack, from top to bottom, comprises the flash userland API, flash partition capsule, flash virtualizer and vendor-specific flash controller layer. SPI flash stack architecture with simplified flash controller layer is shown in the diagram below.

SPI flash stack architecture diagram

  • Flash Userland API

    • Provides syscall library for userspace applications to issue IO requests (read, write, erase) to flash devices. Userspace application will instantiate the syscall library with unique driver number for individual flash partition.
  • Flash Partition Capsule

    • Defines the flash partition structure with offset and size, providing methods for reading, writing, and erasing arbitrary lengths of data within the partitions. Each partition is logically represented by a FlashUser, which leverages the existing flash virtualizer layer to ensure flash operations are serialized and managed correctly. It also implements SyscallDriver trait to interact with the userland API.
  • Vendor-specific Flash controller Layer

    • Flash controller driver implements the kernel::hil::flash::Flash trait, which defines the standard interface (read, write, erase) for flash page-based operations.
      • Additional methods provided in the flash controller driver include:
        • Initializing the SPI flash device and configuring settings such as clock speed, address mode, and other parameters.
        • Checking the status of the flash device, such as whether it is busy or ready for a new operation.
        • Erasing larger regions of flash memory, such as sectors or blocks, in addition to individual pages.
        • Reading the device ID, manufacturer ID, or other identifying information from the flash device.
        • Retrieving information about the flash memory layout, such as the size of pages, sectors, and blocks from SFDP.
        • Performing advanced read/write operations using specific commands supported by the flash device.
    • A flash controller virtualizer VirtualFlashCtrl should be implemented to support the configuration of multiple flash devices connected to the same flash controller via different chip selects. The diagram below shows the stack to enable this scenario.

flash controller layer with virtualizer

Common Interfaces

Flash Userland API

It is defined in SPI flash syscall library to provide async interface (read, write, erase) to underlying flash devices.

/// spi_flash/src/lib.rs
///
/// A structure representing an asynchronous SPI flash memory interface.
struct AsyncSpiFlash {
    // The driver number associated with this SPI flash interface.
    driver_num: u32,
}

/// Represents an asynchronous SPI flash memory interface.
///
/// This struct provides methods to interact with SPI flash memory in an asynchronous manner,
/// allowing for non-blocking read, write, and erase operations.
impl AsyncSpiFlash {
    /// Creates a new instance of `AsyncSpiFlash`.
    ///
    /// # Arguments
    ///
    /// * `driver_num` - The driver number associated with the SPI flash.
    fn new(driver_num: u32) -> Self {};

    /// Checks if the SPI flash exists.
    ///
    /// # Returns
    ///
    /// * `Ok(())` if the SPI flash exists.
    /// * `Err(ErrorCode)` if there is an error.
    fn exists() -> Result<(), ErrorCode> {};

    /// Reads an arbitrary number of bytes from the flash memory.
    ///
    /// This method reads `len` bytes from the flash memory starting at the specified `address`
    /// and stores them in the provided `buf`.
    ///
    /// # Arguments
    ///
    /// * `address` - The starting address to read from.
    /// * `len` - The number of bytes to read.
    /// * `buf` - The buffer to store the read bytes.
    ///
    /// # Returns
    ///
    /// * `Ok(())` if the read operation is successful.
    /// * `Err(ErrorCode)` if there is an error.
    async fn read(&self, address: usize, len: usize, buf: &mut [u8]) -> Result<(), ErrorCode>;

    /// Writes an arbitrary number of bytes to the flash memory.
    ///
    /// This method writes the bytes from the provided `buf` to the flash memory starting at the
    /// specified `address`.
    ///
    /// # Arguments
    ///
    /// * `address` - The starting address to write to.
    /// * `buf` - The buffer containing the bytes to write.
    ///
    /// # Returns
    ///
    /// * `Ok(())` if the write operation is successful.
    /// * `Err(ErrorCode)` if there is an error.
    async fn write(&self, address: usize, buf: &[u8]) -> Result<(), ErrorCode>;

    /// Erases an arbitrary number of bytes from the flash memory.
    ///
    /// This method erases `len` bytes from the flash memory starting at the specified `address`.
    ///
    /// # Arguments
    ///
    /// * `address` - The starting address to erase from.
    /// * `len` - The number of bytes to erase.
    ///
    /// # Returns
    ///
    /// * `Ok(())` if the erase operation is successful.
    /// * `Err(ErrorCode)` if there is an error.
    async fn erase(&self, address: usize, len: usize) -> Result<(), ErrorCode>;
}

Flash partition capsule

/// A structure representing a partition of a flash memory.
///
/// This structure allows for operations on a specific partition of the flash memory,
/// defined by a start address and a size.
///
/// # Type Parameters
/// - `F`: The type of the flash memory, which must implement the `Flash` trait.
///
/// # Fields
/// - `flash_user`: A reference to the `FlashUser` that provides access to the flash memory.
/// - `start_address`: The starting address of the flash partition.
/// - `size`: The size of the flash partition.
/// - `client`: An optional reference to a client that implements the `FlashPartitionClient` trait.
struct FlashPartition<F: Flash> {
    flash_user: &FlashUser<F>,
    start_address: usize,
    size: usize,
    client: OptionalCell<&dyn FlashPartitionClient>,
}

/// A partition of a flash memory device.
///
/// This struct represents a partition of a flash memory device, allowing
/// operations such as reading, writing, and erasing within the partition.
///
/// # Type Parameters
///
/// - `F`: A type that implements the `Flash` trait.
impl<F: Flash> FlashPartition<F> {
    /// Creates a new `FlashPartition`.
    ///
    /// # Arguments
    ///
    /// - `flash_user`: A reference to the `FlashUser` that owns the flash memory device.
    /// - `start_address`: The starting address of the partition within the flash memory device.
    /// - `size`: The size of the partition in bytes.
    ///
    /// # Returns
    ///
    /// A new `FlashPartition` instance.
    fn new(
        flash_user: &FlashUser<, F>,
        start_address: usize,
        size: usize,
    ) -> FlashPartition<F> {}

    /// Sets the client for the flash partition.
    ///
    /// # Arguments
    ///
    /// - `client`: A reference to an object that implements the `FlashPartitionClient` trait.
    fn set_client(&self, client: &dyn FlashPartitionClient) {}

    /// Reads data from the flash partition.
    ///
    /// # Arguments
    ///
    /// - `buffer`: A mutable reference to a buffer where the read data will be stored.
    /// - `offset`: The offset within the partition from which to start reading.
    /// - `length`: The number of bytes to read.
    ///
    /// # Returns
    ///
    /// A `Result` indicating success or an error code.
    fn read(
        &self,
        buffer: &'static mut [u8],
        offset: usize,
        length: usize,
    ) -> Result<(), ErrorCode> {}

    /// Writes data to the flash partition.
    ///
    /// # Arguments
    ///
    /// - `buffer`: A mutable reference to a buffer containing the data to be written.
    /// - `offset`: The offset within the partition at which to start writing.
    /// - `length`: The number of bytes to write.
    ///
    /// # Returns
    ///
    /// A `Result` indicating success or an error code.
    fn write(
        &self,
        buffer: &'static mut [u8],
        offset: usize,
        length: usize,
    ) -> Result<(), ErrorCode> {}

    /// Erases data from the flash partition.
    ///
    /// # Arguments
    ///
    /// - `offset`: The offset within the partition at which to start erasing.
    /// - `length`: The number of bytes to erase.
    ///
    /// # Returns
    ///
    /// A `Result` indicating success or an error code.
    fn erase(&self, offset: usize, length: usize) -> Result<(), ErrorCode> {}
}

/// Implementation of the `SyscallDriver` trait for the `FlashPartition` struct.
/// This implementation provides support for reading, writing, and erasing flash memory,
/// as well as allowing read/write and read-only buffers, and subscribing to callbacks.
impl<F: Flash> SyscallDriver for FlashPartition< F> {
    ///
    /// Handles commands from userspace.
    ///
    /// # Arguments
    ///
    /// * `command_number` - The command number to execute.
    /// * `arg1` - The first argument for the command.
    /// * `arg2` - The second argument for the command.
    /// * `process_id` - The ID of the process making the command.
    ///
    /// # Returns
    ///
    /// A `CommandReturn` indicating the result of the command.
    ///
    /// Commands:
    /// - `0`: Success (no operation).
    /// - `1`: Read operation. Reads `arg2` bytes from offset `arg1`.
    /// - `2`: Write operation. Writes `arg2` bytes to offset `arg1`.
    /// - `3`: Erase operation. Erases `arg2` bytes from offset `arg1`.
    /// - Any other command: Not supported.
    fn command(
        &self,
        command_number: usize,
        arg1: usize,
        arg2: usize,
        process_id: ProcessId,
    ) -> CommandReturn {};

    ///
    /// Allows a process to provide a read/write buffer.
    ///
    /// # Arguments
    ///
    /// * `process_id` - The ID of the process providing the buffer.
    /// * `readwrite_number` - The identifier for the buffer.
    /// * `buffer` - The buffer to be used for read/write operations.
    ///
    /// # Returns
    ///
    /// A `Result` indicating success or failure.
    ///
    /// Buffers:
    /// - `0`: Write buffer.
    /// - Any other buffer: Not supported.
    fn allow_readwrite(
        &self,
        process_id: ProcessId,
        readwrite_number: usize,
        buffer: Option<WriteableProcessBuffer>,
    ) -> Result<(), ErrorCode>;

    ///
    /// Allows a process to provide a read-only buffer.
    ///
    /// # Arguments
    ///
    /// * `process_id` - The ID of the process providing the buffer.
    /// * `readonly_number` - The identifier for the buffer.
    /// * `buffer` - The buffer to be used for read-only operations.
    ///
    /// # Returns
    ///
    /// A `Result` indicating success or failure.
    ///
    /// Buffers:
    /// - `0`: Read buffer.
    /// - Any other buffer: Not supported.
    fn allow_readonly(
        &self,
        process_id: ProcessId,
        readonly_number: usize,
        buffer: Option<ReadableProcessBuffer>,
    ) -> Result<(), ErrorCode>{}

    ///
    /// Subscribes a process to a callback.
    ///
    /// # Arguments
    ///
    /// * `subscribe_number` - The identifier for the callback.
    /// * `callback` - The callback to be subscribed.
    /// * `process_id` - The ID of the process subscribing to the callback.
    ///
    /// # Returns
    ///
    /// A `Result` containing the previous callback if successful, or an error code if not.
    ///
    /// Callbacks:
    /// - `0`: General callback.
    /// - Any other callback: Not supported.
    fn subscribe(
        &self,
        subscribe_number: usize,
        callback: Option<Callback>,
        process_id: ProcessId,
    ) -> Result<Callback, (Option<Callback>, ErrorCode)>;
}

Flash Controller Driver Capsule

This module is vendor-specific. The common trait to be implemented is within kernel::hil::flash::Flash.

Image Loading

Overview

The Image Loading module is a component of the MCU Runtime SDK designed for managing SOC images. This module provides APIs for:

  • Loading SOC images to target components. The SOC images could come from a flash storage or from another platform capable of streaming images through PLDM T5 (e.g., a BMC Recovery Agent).
  • Verifying and authenticating the SOC Images through the Caliptra Core. Images that are loaded to the target SOC components will be authenticated using a mailbox command to the Caliptra core and are verified against the measurements in the SOC Manifest.

The diagram below provides an example of how the Caliptra subsystem, integrated with custom SOC elements (highlighted in green), facilitates the loading of SOC images to vendor components.

Custom SOC elements:

  • External Flash : A flash storage containing SOC manifest and the SOC images.
  • Vendor CPU: A custom CPU that executes code from a coupled Vendor RAM
  • Vendor RAM: RAM exclusively used by the Vendor CPU and is programmable via AXI bus.
  • Vendor Cfg Storage: A volatile memory storage used to contain vendor specific configurations.
  • SOC Images SOC Image 1 is a firmware for Vendor CPU and loaded to Vendor RAM. SOC Image 2 is a configuration binary to be loaded to Vendor Cfg Storage.
  • SOC Config : A register accessible by the MCU ROM to select appropriate source (flash or PLDM) for loading the SOC images.
  • Caliptra 'Go' Wire : A signal controlled by the Caliptra core routed to the reset line of the Vendor CPU.

flash_config

Supported Topologies

Definitions

  • Image Component : The binary data (e.g. firmware, configuration) identified uniquely by a component_id.
  • Image Location : The location where the image component is loaded (e.g. an instruction or data memory) identified by an image_id. The location should have an associated AXI address.

Image and Instance Mapping

  • Each image location will be associated to an image component in the SOC Manifest.

Topology 1: One Image Component per Image Location

In this topology, each image location has its own component. For example, 2 MCUs with their own instruction memories running 2 different firmware images.

flowchart TD
        img1a["Image Component (component_id=1)"]
        mem1a["Instruction Memory 1 (image_id=1)"]
        mcu1a["Vendor MCU 1"]
        img1a --> mem1a --> mcu1a

        img2["Image Component (component_id=2)"]
        mem2["Instruction Memory 2 (image_id=2)"]
        mcu2["Vendor MCU 2"]
        img2 --> mem2 --> mcu2

Another configuration for this topology is when the component is shared across multiple processing elements.

flowchart TD


        img1b["Image Component (component_id=1)"]
        mem1b["Instruction Memory 1 (image_id=1)"]
        mcu1b["Vendor MCU 1"]
        mcu2b["Vendor MCU 2"]
        img1b --> mem1b
        mem1b --> mcu1b
        mem1b --> mcu2b

Topology 2: One Image for multiple instances

flowchart TD

%% Image
img["Image Component(component_id=1)"]

%% Instruction Memories
mem1["Instruction Memory 1 (image_id=1)"]
mem2["Instruction Memory 2 (image_id=2)"]
mem3["Instruction Memory 3 (image_id=3)"]

%% Vendor MCUs
mcu1["Vendor MCU 1"]
mcu2["Vendor MCU 2"]
mcu3["Vendor MCU 3"]

%% Connections
img --> mem1 --> mcu1
img --> mem2 --> mcu2
img --> mem3 --> mcu3

Image Loader supports architectures with a combination of these topologies.

Image Loading Steps

The sequence diagram below shows the high level steps of loading MCU RT image and SOC images.

  • Red Arrows indicates actions taken by Caliptra RT
  • Purple Arrows indicates actions taken by MCU ROM
  • Blue Arrows indicates actions taken by MCU RT
  • Black Arrows indicates actions taken by the PLDM FW Update Agent

flash_config

The following steps are done for every SOC image:

flash_config

The following outlines the steps carried out by the MCU RT during the SOC boot process:

  1. MCU ROM reads a SOC Configuration register (implementation specific) to determine the source of the images to load (Flash/PLDM).

  2. Caliptra RT authorizes and loads Caliptra RT (refer to Caliptra Subsystem boot flow for the detailed steps).

  3. Caliptra switches to Caliptra RT FW.

  4. Caliptra RT indicates to Recovery I/F that it is ready for the SOC manifest image (refer to Caliptra Subsystem Recovery Sequence for the detailed steps).

  5. Retrieve SOC Manifest

    1. If image is coming from PLDM, PLDM FW Update Agent transfers SOC manifest to Recovery I/F
    2. If Image is coming from Flash, MCU ROM transfers SOC manifest from flash to Recovery I/F
  6. Caliptra RT transfers SOC Manifest to Caliptra Mailbox (MB) SRAM

  7. Caliptra RT will authenticate its image sitting in Caliptra MB SRAM

  8. Caliptra RT indicates to Recovery I/F that it is ready for the next image that should be the MCU RT Image (refer to Caliptra Subsystem Recovery Sequence for the detailed steps)..

  9. Retrieve MCU RT Image

    1. If Image is coming from PLDM, PLDM FW Update Agent sends MCU RT Image to Recovery I/F (refer to Caliptra Subsystem boot flow).
    2. If Image is coming from Flash, MCU ROM transfers MCU RT Image to Recovery I/F
  10. Caliptra RT FW will read the recovery interface registers over AXI manager interface and write the image to MCU SRAM aperture

  11. Caliptra RT FW will instruct its SHA accelerator to hash the MCU RT Image in the MCU SRAM.

  12. Caliptra RT FW will use this hash and verify it against the hash in the SOC manifest.

  13. Once the digest is verified, Caliptra RT FW sets the EXEC/GO bit.

  14. The EXEC/GO bit sets a Caliptra wire to MCU (as a consequence of setting the EXEC/GO bit in the previous step). When MCU detects this event, it sets a parameter using the FW HandOff table to indicate the image source (i.e. the image source where it booted from).

  15. MCU switches to MCU RT

  16. MCU RT retrieves the image source from HandOff table

For every image that needs to be loaded, user initiates a call to load an image identified by an image_id:

  1. MCU RT application initializes the image loader based on the boot_source. The application need to specify the boot_source as Flash or PLDM.
  2. Retrieve TOC 18.1 If boot_source = PLDM: 18.1.1 MCU starts the PLDM service. 18.1.2–18.1.3 PLDM Update agent queries device identifiers and receives a response. Device Identifier is vendor specific and should correspond to the device identifier in the PLDM Package in the Update Agent. 18.1.4–18.1.5 PLDM Update agent requests and receives firmware parameters. The PLDM firmware parameter to be used should be for the streaming boot component (refer to the PLDM Package documentation)
Field NameDescriptionValue to Use
ComponentActivationMethodsDefines activation methods supported by the FD.0x0000 (Automatic)
CapabilitiesDuringUpdateCapabilities of the firmware component during update.0x00000002 (Downgrade permitted)
ActiveComponentVersionStringDescribes the currently active version of the component.None
PendingComponentVersionStringDescribes the version of the component that is pending activation.None
ComponentIdentifierUnique ID for the component.0xFFFF
ComponentClassificationIndexUsed to distinguish identical component classifications with different instances.0x00
ActiveComponentComparisonStampComparison stamp for active version.0x00000000
ActiveComponentVersionStringTypeString type for active version string.0x01 (ASCII)
ActiveComponentVersionStringLengthLength of the active version string.0x00
ActiveComponentReleaseDateRelease date of the active component in YYYYMMDD format."00000000"
PendingComponentComparisonStampComparison stamp for pending version.None
PendingComponentVersionStringTypeString type for pending version string.0x01 (ASCII)
PendingComponentVersionStringLengthLength of the pending version string.0x00
PendingComponentReleaseDateRelease date of the pending version in YYYYMMDD format."00000000"
ComponentCountNumber of components on the FD.0x0001
ActiveComponentImageSetVersionStringTypeType for image set version string (active).0x01 (ASCII)
ActiveComponentImageSetVersionStringLengthLength of image set version string (active).0x00
PendingComponentImageSetVersionStringTypeType for image set version string (pending).0x01 (ASCII)
PendingComponentImageSetVersionStringLengthLength of image set version string (pending).0x00
ActiveComponentImageSetVersionStringVersion string of the active image set.None
PendingComponentImageSetVersionStringVersion string of the pending image set.None

18.1.6–18.1.7 PLDM sends a RequestUpdate, MCU responds with approval. 18.1.8–18.1.9 PLDM sends component information using PassTableComponent request, MCU responds with success. 18.1.10–18.1.11 PLDM sends an UpdateComponent, MCU acknowledges.

18.1.12. Retrieve TOC (Table of Contents) via PLDM starting from offset 0 until the end of TOC 18.1.12.1 MCU send RequestFirmwareData 18.1.12.2 PLDM responds with the TOC chunk

18.2 If boot_source = Flash: 18.2.1 MCU reads the TOC directly from Flash.

For each SOC instance to be loaded and authorized:

19.1 MCU RT initiates to load the an image component for location for image_id=1 by executing image_loader.load_and_authorize(image_id).

19.2–19.3 MCU retrieves the image_info using Caliptra mailbox for the provided image_id. This should return the load_address and the corresponding component_id associated to the image_id.

19.4 MCU determines the image offset and length using the TOC and component_id.

19.5. Transfer image in chunks (from image_offset to image_offset + image_size): 19.5.1.1 If from Flash: Flash sends the image chunk to MCU.

19.5.2.1–19.5.2.2 If from PLDM:

MCU requests firmware data from PLDM.

PLDM responds with the image chunk.

19.5.3 MCU writes the image chunk to the appropriate component destination (e.g., Vendor RAM or Config) identified by the load_address.

19.6 After download is completed, MCU sends an authorize(instance_id) command to Caliptra via Mailbox.

19.7 Caliptra forwards the image to the SHA Accelerator.

19.8 Caliptra verifies the image hash in SHA Acc against the value in the SOC manifest.

19.9 On successful verification, Caliptra returns a success response via the Mailbox to the MCU

20 After all SOC instances' images are loaded, MCU RT application deinitializes the image loader.

21 If boot_source = PLDM, MCU finalizes the PLDM firmware update sequence.

21.1–21.2 MCU sends VerifyComplete, and PLDM Update agent responds.

21.3–21.4 MCU sends ApplyComplete, and PLDM Update agent responds.

21.5–21.6 PLDM Update agent issues an Activate, and MCU confirms.

21.7 MCU stops the PLDM service.

Architecture

The following diagram presents the software stack architecture where the Image Loading module resides.

sw_stack

At the top of the stack, the user application interacts with the Image Loading module through high-level APIs. The user application is responsible for initiating the image loading and verification.

The Image Loading module provides the interface to retrieve and parse the manifest from the flash storage, and transfer SOC images from the storage to the target destination.

Application Interfaces

The APIs are presented as methods of the ImageLoader trait.

#![allow(unused)]


fn main() {
/// Trait defining the Image Loading module
pub trait ImageLoaderAPI {
    /// Loads the specified SoC image to a storage mapped to the AXI bus memory map.
    ///
    /// # Parameters
    /// image_id: The unsigned integer identifier for the image location
    ///
    /// # Returns
    /// - `Ok()`: Image has been loaded and authorized succesfully.
    /// - `Err(ErrorCode)`: Indication of the failure to load or authorize the image.
    async fn load_and_authorize(&self, image_id: u32) -> Result<(), ErrorCode>;


    /// Releases any resources held by ImageLoader
    /// Finalizes the PLDM Update and stops the PLDM service if started
    async fn finalize();

}


/// ImageLoader Implementation
pub struct ImageLoader {
    pub fn new(boot_source: ImageSource) -> Result<(), ErrorCode> {
        // if boot_source = Flash, read TOC from flash
        // if boot_source = PLDM, start PLDM service, and download TOC from offset 0
    }
}

impl ImageLoaderAPI for ImageLoader {
    // API Implementation
}

pub enum ImageSource {
   // Image is located in Flash
   Flash,
   // Image is retrieved via PLDM
   // The PLDM DeviceID to be used should be specified
   Pldm(DeviceId),
}




Example Usage:

/// Load image for instances 1 and 2 from flash
let image_loader = ImageLoader::new(ImageSource::Flash)?;
image_loader.load_and_authorize(1).await?;
image_loader.load_and_authorize(2).await?;
image_loader.finalize();

/// Load image for instances 3 and 4 from PLDM
let image_loader = ImageLoader::new(ImageSource::Pldm(DEVICE_ID))?;
image_loader.load_and_authorize(3).await?;
image_loader.load_and_authorize(4).await?;
image_loader.finalize();




}

Recovery Boot Flow

During system initialization, the recovery boot flow ensures that valid firmware is successfully loaded into the Caliptra core, MCU, and other SoC elements.

In flash-based systems, an A/B partitioning mechanism is used to enhance reliability by allowing fallback to a working firmware image in case of boot failure.

The method of selecting which partition to boot from is system-specific. For example, the active partition may be determined by a hardware pin, a soft fuse or register, or an entry in the flash partition table.

Caliptra Boot Flow

This section describes the actions taken by the Caliptra ROM and Caliptra FMC/Runtime (RT) during boot, particularly in handling boot failures.

If a corrupted or unauthorized Caliptra FMC+RT image is downloaded, the Caliptra ROM sets the CPTRA_FW_ERROR_FATAL register. MCU then initiates a recovery mechanism to provide the Caliptra core with a valid firmware image on the next SOC reset.

If the firmware image fails to execute properly (e.g., hangs), the Caliptra watchdog triggers a timeout, causing an error to be asserted to the MCU ROM, which then initiates the recovery process.

flowchart TD
    CaliptraFlow([Start]) -->Start
    Start[Execute Caliptra ROM] --> StartWD[Start Watchdog]
    StartWD --> SetReady[Set RECOVERY_STATUS = Awaiting Image]

    SetReady -->|Recovery Image Available| DownloadFMC[Stream FMC + RT from Recovery Interface]
    DownloadFMC --> AuthFMC[Authenticate FMC + RT]

    AuthFMC -->|Auth Pass| BootRuntime[Execute FMC + RT]
    AuthFMC -->|Auth Fail| ClearSignals[Set RECOVERY_STATUS = AuthFailed]

    ClearSignals -->|Warm Boot| NonFatal[Set cptra_error_non_fatal]
    ClearSignals -->|Cold Boot| Fatal[Set cptra_error_fatal]

    NonFatal --> WaitReset[Wait for SOC Reset] -->|Reset| Start
    Fatal --> WaitReset

    BootRuntime --> LoadManifest[Load & <br>Auth SoC Manifest]
    LoadManifest -->|Auth Pass| LoadMCURT[Load & Auth MCU RT]
    LoadMCURT -->|Auth Pass| StartMCURT[Set Exec/Go for MCU<br>Set RECOVERY_STATUS = 'Recovery successful']
    LoadMCURT -->|Auth Fail| ClearSignals

    CPTRA_WD_TIMEOUT[Watchdog Timeout] --> Fatal

    StartMCURT --> Done_cptra([Done])

MCU ROM Boot Flow

This section describes how the MCU ROM handles failures during the loading of Caliptra firmware and MCU Runtime firmware.

To avoid infinite reboot loops caused by a faulty firmware image, each partition maintains a boot count, which tracks how many times the system has attempted to boot from it.

On power-up, the MCU ROM checks the partition table for the status and boot count of the currently active partition.

If the count is below a predefined threshold (e.g., MAX_COUNT), the system increments the count and attempts to boot the partition.

If the count exceeds MAX_COUNT, the partition is marked as unhealthy, and the MCU triggers a recovery flow, switching to another valid partition if available.

After a successful boot, the partition status is marked as "Boot Successful". Future boots may reset the count or ignore it.

This mechanism ensures automatic rollback to a working firmware without manual intervention.

flowchart TD
    MCUFlow([Start])-->MCUROM
    TIMEOUT([Watchdog<br>Timeout])-->MCUROM
    MCUROM[Execute MCU ROM]
    MCUROM --> StartMCUWD[Start MCU<br>Watchdog]-->
    READ_PART[Retrieve Active<br>Partition]-->CHECK_BOOT_COUNT
    MonitorErrors[Monitor Caliptra<br>Errors] --> |cptra_error_non_fatal<br>cptra_error_fatal| Recover
    CHECK_BOOT_COUNT[Check active partition<br>boot count<br>and status] --> |Status =='Valid'<br>&&<br>Boot count >= MAX_COUNT| Recover
    CHECK_BOOT_COUNT --> |Status =='Boot Successful'<br>i.e. partition booted before| CHECK_RESET_REASON
    CHECK_BOOT_COUNT --> |Status='Valid'<br>&&<br>Boot count < MAX_COUNT| INCREMENT_COUNT[Increment Boot count] -->  CHECK_RESET_REASON

    CHECK_RESET_REASON[Get RESET_REASON]-->|Cold Boot|LoadFMC[Load Caliptra<br>FMC + Runtime<br>from Active Flash<br>Partition]
    CHECK_RESET_REASON-->|Warm Boot|LoadMCURTWarm[Load MCU RT<br>from Active Partition]-->AuthorizeMCURT[Authorize MCU RT]

    AuthorizeMCURT-->|Auth Pass|MCURTStart
    AuthorizeMCURT-->|Auth Fail|Recover
    LoadFMC --> WaitRecoveryReady[Wait for<br>RECOVERY_STATUS<br>to be 'Awaiting<br>Image']
        --> |RECOVERY_STATUS =<br>'Awaiting Image'| StreamFMC[Stream Caliptra<br>FMC + Runtime<br>to Recovery Interface]
    StreamFMC --> WaitForCaliptraBoot[Wait Caliptra<br>RT Boot]



    WaitForCaliptraBoot  --> |Caliptra RT Booted,<br>RECOVERY_STATUS =<br>'Recovery successful'| StreamManifest[Load Manifest<br>from Active<br>partition and<br>stream to Recovery<br>Interface]
        --> StreamMCURT[Load MCU RT<br>from Active<br>partition and<br>stream to Recovery<br>Interface]

    StreamMCURT--> WaitMCURTBoot[Wait for MCU<br>Exec/Go Bit<br>to be set]

    WaitMCURTBoot-->|Exec Go/Bit<br>set|MCURTStart([MCU Runtime Boot Flow])


    Recover([MCU Recovery Flow])

MCU Recovery Flow

When a boot failure occurs (e.g., due to exceeding maximum boot attempts, authentication failure, or watchdog timeout), the MCU Recovery Flow is triggered to restore system operability by switching to a known-good firmware partition.

If the active partition fails to boot, the system attempts to revert to the other available and valid partition.

The MCU will indicate to the SoC that an error occured and will need a full SOC Reset.

flowchart TD
    Start([MCU Recovery Flow])--> InvalidateActivePartition[Set Active<br>Partition as<br>Invalid] --> SetStatusBootFailed[Set Partition<br>Status as Boot Failed]
        --> SetNewActivePartition[Set other valid<br>partition, if any,<br>as Active]
        --> SetFatalError[Set FW_ERROR_FATAL<br> MCI Reg]
        --> WaitForSOCReset[Wait for<br>SOC Reset]

MCU Runtime and SoC Image(s) Boot Flow

When the MCU ROM hands off execution to the MCU Runtime firmware, it keeps the MCU watchdog timer active. If the watchdog expires at any point—indicating a hang or prolonged stall—the MCU will be reset. Upon reset, the MCU ROM will evaluate the boot count and initiate the recovery flow if necessary (e.g., if the boot count exceeds the defined threshold).

The MCU Runtime firmware is responsible for loading and authorizing the remaining SoC images. If all SoC images are successfully loaded, authenticated, and booted, the partition's boot status can be marked as "Boot Successful." However, if any of the images fail to load or validate, the system behavior is left to user-defined policy. For example, the user may choose to trigger the recovery flow to fall back to a previous version of the SoC images or halt execution entirely to prevent further operation under an unstable configuration.

flowchart TD
    Start([MCU Runtime<br>Boot Flow])-->MCURuntime
    MCURuntime[Execute MCU<br>Runtime FW]
    MCURuntime-->StartWatchdog[Start MCU<br>Watchdog]
    StartWatchdog-->LoadAndAuthorizeSocImage[Load and<br>Authorize SOC<br>Images]

    LoadAndAuthorizeSocImage --> |Auth Pass| SUCCESSFUL_BOOT[Set Partition <br>Status as <br>'Boot Successful'] --> Done([Done])
    LoadAndAuthorizeSocImage --> |Auth Fail| UserDefinedOption[User Defined<br>Logic - e.g.<br>Switch to other<br>partition or Hang]

    TIMEOUT([MCU  Watchdog<br>Timeout])-->MCURESET[MCU Reset]-->MCUROM([MCU ROM])

References:

  • OCP Recovery Interface [https://www.opencompute.org/documents/ocp-recovery-document-1p0-final-1-pdf]
  • Caliptra Error and Recovery Flow [https://github.com/chipsalliance/caliptra-rtl/blob/main/docs/CaliptraIntegrationSpecification.md#caliptra-error-and-recovery-flow]
  • Fatal and Non-Fatal Errors [https://github.com/chipsalliance/Caliptra/blob/main/doc/Caliptra.md#error-reporting-and-handling]

MCTP Stack

The Caliptra subsystem supports SPDM, PLDM, and Caliptra vendor-defined message protocols over MCTP.

The MCTP base protocol is implemented as a Tock Capsule, which also handles the essential MCTP Control messages. Additionally, it offers a syscall interface to userspace, enabling the sending and receiving of MCTP messages for other supported protocols. Caliptra MCTP endpoint has only one EID and supports dynamic assignment by the MCTP bus owner.

MCTP Packets are delivered over physical I3C medium using I3C transfers. Caliptra MCTP endpoint always plays the role of I3C Target and is managed by an external I3C controller. Minimum transmission size is based on the MCTP baseline MTU (for I3C it is 69 bytes: 64 bytes MCTP payload + 4 bytes MCTP header + 1 byte PEC). Larger than the baseline transfer may be possible after discovery and negotiation with the I3C controller. The negotiated MTU size will be queried from the I3C Target peripheral driver by MCTP capsule.

MCTP Send Sequence

sequenceDiagram
    participant Application
    participant VirtualMCTPDriver
    participant MuxMCTPDriver
    participant MCTPI3CBinding
    participant I3CTarget
    participant I3CController
    Application--)VirtualMCTPDriver: Send request/response <br/>message to a destination EID
    VirtualMCTPDriver->>VirtualMCTPDriver: mctp_sender.send_message. <br/>Sets the mctp_sender context.
    VirtualMCTPDriver->> MuxMCTPDriver: mctp_mux_sender.send() <br/> Adds mctp_sender to the tail of sender_list.
    loop Packetize the entire message payload
        MuxMCTPDriver->>MuxMCTPDriver: Add MCTP Transport header.
        MuxMCTPDriver->>MCTPI3CBinding: transmit() MCTP packet
        MCTPI3CBinding->>MCTPI3CBinding: Compute PEC and add <br/>to the end of the packet.
        MCTPI3CBinding->>I3CTarget: transmit() MCTP packet with PEC
        alt IBI mode enabled
            I3CTarget->>I3CController: IBI with MDB
            I3CController--)I3CTarget: Private Read Request
            I3CTarget--)I3CController: MCTP packet
            I3CTarget->>I3CTarget: result = SUCCESS
        else Polling mode
            I3CTarget->>I3CTarget: 1. Set the pending read bit. <br/> 2. Set the alarm for tx_timeout time
            alt Controller sent GETSTATUS CCC
                I3CTarget--)I3CController: Report nonzero value
                I3CController--)I3CTarget: Private Read Request
                I3CTarget--)I3CController: MCTP packet
                I3CTarget->>I3CTarget: set result = SUCCESS
            else alarm fired
                I3CTarget->>I3CTarget: set result = TIMEOUT
            end
        end
        I3CTarget->>MCTPI3CBinding: send_done() with result. <br/>Return packet buffer.
        MCTPI3CBinding->>MuxMCTPDriver: send_done() with result.<br/> Return packet buffer.
    end

The send stack is as shown in the picture below:

The MCTP Send stack

MCTP Receive sequence

sequenceDiagram
    participant I3CController
    participant I3CTarget
    participant MCTPI3CBinding
    participant MuxMCTPDriver
    participant VirtualMCTPDriver
    participant Application
    loop Assemble packets until eom
        I3CController--)I3CTarget: I3C Private Write transfer
        I3CTarget->>MCTPI3CBinding: if no rx buffer, call write_expected() callback
        MCTPI3CBinding->> MuxMCTPDriver: write_expected() callback
        MuxMCTPDriver->>MCTPI3CBinding: set_rx_buffer() with buffer to receive packet
        MCTPI3CBinding->> I3CTarget: set_rx_buffer() with buffer to receive the packet
        I3CTarget--) I3CController : Send ACK
        I3CController--)I3CTarget: MCTP packet
        Note over I3CController, I3CTarget: Receive entire MCTP packet <br/>including the PEC until Sr/P.
        I3CTarget->> MCTPI3CBinding: receive() to receive the packet
        MCTPI3CBinding ->> MCTPI3CBinding: Check the PEC, and pass the packet <br/>with MCTPHeader to Mux MCTP layer
        MCTPI3CBinding->>MuxMCTPDriver: receive() to receive the packet
        MuxMCTPDriver->>MuxMCTPDriver: Process MCTP transport header on packet, <br/> and assemble if matches any pending Rx states<br/>or handle MCTP control msg.
    end
    MuxMCTPDriver->>VirtualMCTPDriver: receive() call to receive the assembled message.
    VirtualMCTPDriver--)Application: Schedule upcall to receive the request/response.

(The receive stack picture is nearly identical to the send stack above.)picture below:

Syscall Library in userspace

Userspace applications can use syscall library in to send and receive MCTP messages. The following APIs are provided by the MCTP syscall library. Each user space application will instantiate the Mctp module with appropriate driver number. The Mctp module provides the following APIs to send and receive MCTP messages.

//! The MCTP library provides the interface to send and receive MCTP messages.
//! The MCTP library is implemented as an async library to allow the userspace application to send and receive MCTP messages asynchronously.
//!
//! Usage
//! -----
//!```Rust
//! use mctp::Mctp;
//!
//! const SPDM_MESSAGE_TYPE: u8 = 0x5;
//! const SECURE_SPDM_MESSAGE_TYPE: u8 = 0x6;
//!
//! #[embassy_executor::task]
//! async fn async_main() {
//!     /// Initialize the MCTP driver with the driver number
//!     let mctp = Mctp::<TockSyscalls>::new(MCTP_SPDM_DRIVER_NUM);
//!
//!     loop {
//!         /// Receive the MCTP request
//!         let mut rx_buf = [0; 1024];
//!         let res = mctp.receive_request(&mut rx_buf).await;
//!         match res {
//!             Ok((req_len, msg_info)) => {
//!                 /// Process the received message
//!                 /// ........
//!                 /// Send the response message
//!                 let mut tx_buf = [0; 1024];
//!                 let result = mctp.send_response(&tx_buf, msg_info).await;
//!                 match result {
//!                     Ok(_) => {
//!                         /// Handle the send response success
//!                     }
//!                     Err(e) => {
//!                         /// Handle the send response error
//!                     }
//!                 }
//!             }
//!             Err(e) => {
//!                 /// Handle the receive request error
//!             }
//!         }
//!     }
//! }
//!```

/// mctp/src/lib.rs
type EndpointId = u8;
type Tag = u8;

pub struct MessageInfo {
    eid: EndpointId,
    tag: Tag,
}

pub struct Mctp<S: Syscalls> {
    syscall: PhantomData<S>,
    driver_num: u32,
}

impl<S: Syscalls> Mctp<S> {
    /// Create a new instance of the MCTP driver
    ///
    /// # Arguments
    /// * `driver_num` - The driver number for the MCTP driver
    ///
    /// # Returns
    /// * `Mctp` - The MCTP driver instance
    pub fn new(drv_num: u32) -> Self;
    /// Check if the MCTP driver for a specific message type exists
    ///
    /// # Returns
    /// * `bool` - `true` if the driver exists, `false` otherwise
    pub fn exists() -> Result<(), ErrorCode>;
    /// Receive the MCTP request.
    /// Receives a message from any source EID. The user should use the returned MessageInfo to send a response.
    ///
    /// # Arguments
    /// * `req` - The buffer to store the received request payload
    ///
    /// # Returns
    /// * `(u32, MessageInfo)` - On success, returns tuple containing length of the request received and the message information containing the source EID, message tag
    /// * `ErrorCode` - The error code on failure
    pub async fn receive_request(&self, req: &mut [u8]) -> Result<(u32, MessageInfo), ErrorCode>;
    /// Send the MCTP response to an endpoint
    ///
    /// # Arguments
    /// * `resp` - The buffer containing the response payload
    /// * `info` - The message information containing the destination EID, message tag which was received in `receive_request` call
    ///
    /// # Returns
    /// * `()` - On success
    /// * `ErrorCode` - The error code on failure
    pub async fn send_response(&self, resp: &[u8], info: MessageInfo) -> Result<(), ErrorCode>;
    /// Send the MCTP request to the destination EID
    /// The function returns the message tag assigned to the request by the MCTP Capsule.
    /// This tag will be used in the `receive_response` call to receive the corresponding response.
    ///
    /// # Arguments
    /// * `dest_eid` - The destination EID to which the request is to be sent
    /// * `req` - The payload to be sent in the request
    ///
    /// # Returns
    /// * `Tag` - The message tag assigned to the request
    /// * `ErrorCode` - The error code on failure
    pub async fn send_request(&self, dest_eid: u8, req: &[u8]) -> Result<Tag, ErrorCode>;
    /// Receive the MCTP response from an endpoint
    ///
    /// # Arguments
    /// * `resp` - The buffer to store the received response payload from the endpoint
    /// * `tag` - The message tag to match against the response message
    /// 
    /// # Returns
    /// * `(u32, MessageInfo)` - On success, returns tuple containing length of the response received and the message information containing the source EID, message tag
    /// * `ErrorCode` - The error code on failure
    pub async fn receive_response(&self, resp: &mut [u8], tag: Tag) -> Result<(u32, MessageInfo), ErrorCode>;
}

Userspace Driver and Virtualizer layer

During the board initialization, three separate instances of the virtual MCTP driver are created, each assigned a unique driver number. These instances correspond to the SPDM, PLDM, and Vendor Defined message types. Each driver instance is designed to communicate directly with its corresponding protocol application.

/// define custom driver numbers for Caliptra
pub enum NUM {
    ...
    // Mctp
    MctpSpdm                  = 0xA0000,
    MctpSecureSpdm            = 0xA0001,
    MctpPldm                  = 0xA0002,
    MctpVenDef                = 0xA0003,
    ...
}

/// mctp/driver.rs
pub const MCTP_SPDM_DRIVER_NUM: usize = driver::NUM::MctpSpdm;
pub const MCTP_PLDM_DRIVER_NUM: usize = driver::NUM::MctpPldm;
pub const MCTP_VENDEF_DRIVER_NUM: usize = driver::NUM::MctpVenDef;

Syscalls provided

Virtual MCTP driver provides system calls to interact with the userspace application. This layer implements the SyscallDriver trait.

The following are the list of system calls provided by the MCTP Capsule.

  1. Read-Write Allow

    • Allow number: 0
      • Description: Used to set up the Rx buffer for receiving the MCTP Request/Response message payload.
      • Argument: Slice into which the received MCTP Request/Response message should be stored.
  2. Read-Only Allow

    • Allow number: 0
      • Description: Used to set up the Tx buffer for sending the MCTP Request/Response message payload.
      • Argument: Slice containing the MCTP message payload to be transmitted.
  3. Subscribe

    • Subscribe number 0:
      • Description: Callback when message is received.
      • Argument 1: The callback
      • Argument 2: App specific data
    • Subscribe number 1:
      • Description: Callback when message is transmitted.
      • Argument 1: The callback
      • Argument 2: App specific data
  4. Command

    • Command number 0:
      • Description: Existence check
    • Command number 1:
      • Description: Receive Request
      • Argument1 : Source EID
      • Argument2 : Message Tag
    • Command number 2:
      • Description: Receive Response
      • Argument1 : Source EID
      • Argument2 : Message Tag
    • Command number 3:
      • Description: Send Request
      • Argument1 : Destination EID
      • Argument2 : Message Tag
    • Command number 4:
      • Description: Send Response
      • Argument1 : Destination EID
      • Argument2 : Message Tag

Virtualized Layer

MCTP capsule stores the following process context specific information in the Process's grant region.

enum OperationType {
    Tx,
    Rx,
    Idle
}
struct OperationCtx {
    msg_tag : u8,
    peer_eid : u8,
    is_busy: bool,
    op_type:OperationType,
}

#[derive(default)]
pub struct App {
    pending_rx: OperationCtx,
    bound_msg_type : u8,
}

/// Implements userspace driver for a particular msg_type.
pub struct VirtualMCTPDriver {
    mctp_sender: &dyn MCTPSender,
    apps : Grant<App, 2 /*upcalls*/, 1 /*allowro*/, 1/*allow_rw*/>,
    app_id: Cell<Option<ProcessID>>,
    msg_types: [u8],
    kernel_msg_buffer: MapCell<SubSliceMut<'static, u8>>,
}

MCTP Send state

/// The trait that provides an interface to send the MCTP messages to MCTP kernel stack.
pub trait MCTPSender {
    /// Sets the client for the `MCTPSender` instance.
    /// In this case it is MCTPTxState which is instantiated at the time of
    fn set_client(&self, client: &dyn MCTPTxClient);

    /// Sends the message to the MCTP kernel stack.
    fn send_msg(&self, dest_eid: u8, msg_tag: u8, msg_payload: SubSliceMut<'static, u8>) -> Result<(), SubSliceMut<'static, u8>>;
}

/// This is the trait implemented by VirtualMCTPDriver instance to get notified after
/// message is sent.
/// The 'send_done' function in this trait is invoked after the MCTPSender
/// has completed sending the requested message.
pub trait MCTPTxClient {
    fn send_done(&self, msg_tag: Option<u8>, result: Result<(), ErrorCode>, msg_payload: SubSliceMut<'static, u8> )
}

pub struct MCTPTxState<M:MCTPTransportBinding> {
    /// MCTP Mux driver reference
    mctp_mux_sender: &MuxMCTPDriver<M>,
    /// Client to invoke when send done. This is set to the corresponding VirtualMCTPDriver instance.
    client: OptionalCell<&dyn MCTPTxClient>,
    /// next MCTPTxState node in the list
    next: ListLink<MCTPTxState<M: MCTPTransportBinding>>,
    /// The message buffer is set by the virtual MCTP driver when it issues the Tx request.
    msg_payload: MapCell<SubSliceMut<'static, u8>>,
}

MCTP Receive state

/// This is the trait implemented by VirtualMCTPDriver instance to get notified of
/// the messages received on corresponding msg_type.
pub trait MCTPRxClient {
    fn receive(&self, dst_eid: u8, msg_type: u8, msg_tag: u8, msg_payload: &[u8]);
}

/// Receive state
pub struct MCTPRxState {
    /// Client (implements the MCTPRxClient trait)
    client: OptionalCell<&dyn MCTPRxClient>,
    /// static Message buffer
    msg_payload: MapCell<'static, [u8]>,
    /// next MCTPRxState node
    next: ListLink<MCTPRxState>,
}

MCTP Mux Layer

The MCTP Mux layer acts as the sole Tx client to the rest of the MCTP stack. The MuxMCTPDriver struct contains a list of statically allocated sender structs that implement the MCTPSender trait. This struct provides methods to packetize the message of the inflight (popped from head of the list) send request. The MCTP Mux layer also contains a list of statically allocated receiver structs that implement the MCTPRxClient trait. This struct provides methods to assemble the received packets into a complete message.

If the message originates from the device (with msg_tag = 0x8), a new msg_tag will be allocated and provided to the client via the send_done callback. This msg_tag is passed to the application layer which uses it to issue the receive response command. For response messages, where msg_tag values range between 0 and 7, the same value is used to encapsulate the MCTP transport header on each packet.

MCTP Mux layer is the single receive client for the MCTP Device Layer. This layer is instantiated with a single contiguous buffer for Rx packet of size kernel::hil:i3c::MAX_TRANSMISSION_UNIT. The Rx buffer is provided to the I3C target driver layer to receive the packets when the I3C controller initiates a private write transfer to the I3C Target.

The virtualized upper layer ensures that only one message is transmitted per driver instance at a time. Receive is event based. The received packet in the Rx buffer is matched against the pending receive requests by the use

/// The MUX struct manages multiple MCTP driver users (clients).
pub struct MuxMCTPDriver<M: MCTPTransportBinding> {
    /// Reference to the MCTP transport binding layer that implements the MCTPTransportBinding trait.
    mctp_device: &dyn M,
    /// Global message tag. Increment by 1 for next tag up to 7 and wrap around.
    next_msg_tag: u8,
    /// Local EID assigned to the MCTP endpoint.
    local_eid: u8,
    /// Maximum transmission unit (MTU) size.
    mtu: u8,
    /// List of outstanding send requests
    sender_list: List<MCTPTxState<M>>,
    /// List of outstanding receive requests
    receiver_list: List<MCTPRxState>,
    /// Static buffer for tx packet. (may not be needed)
    tx_pkt_buffer: MapCell<SubSliceMut<'static, u8>>,
    /// Static buffer for rx packet
    rx_pkt_buffer: MapCell<SubSliceMut<'static, u8>>,
}

MCTP Transport binding layer

The following is the generic interface for the MCTP physical transport binding layer. Implementer of this trait will add physical medium specific header/trailer to the MCTP packet.

/// This trait contains the interface definition
/// for sending the MCTP packet through MCTP transport binding layer.
pub trait MCTPTransportBinding {
	/// Set the client that will be called when the packet is transmitted.
	fn set_tx_client(&self, client: &TxClient);

	/// Set the client that will be called when the packet is received.
	fn set_rx_client(&self, client: &RxClient);

	/// Set the buffer that will be used for receiving packets.
	fn set_rx_buffer(&self, rx_buf: &'static mut [u8]);

	fn transmit(&self, tx_buffer: &'static mut [u8]);

	/// Enable/Disable the I3C target device
	fn enable();
	fn disable();

	/// Get the maximum transmission unit (MTU) size.
	fn get_mtu_size() -> usize;
}

MCTP I3C Transport binding layer is responsible for checking the PEC for received packets and adding the PEC for transmitted packets over the I3C medium. It is mostly a passthrough for the MCTP Base layer except, it will need the I3C target device address for PEC calculation.

This layer is also a sole Rx and Tx client for the I3C Target device driver.

pub struct MCTPI3CBinding {
	/// Reference to the I3C Target device driver.
	mctp_i3c : &dyn I3CTarget,
	rx_client: OptionCell<&dyn RxClient>,
	tx_client: OptionCell<&dyn TxClient>,
	/// I3C Target device address needed for PEC calculation.
	device_address: u8,
}

HIL for I3C Target Device

The following trait defined standard and shared interface for I3C Target hardware driver.

/// hil/i3c.rs

pub trait TxClient {
	/// Called when the packet has been transmitted. (Calls this after the ACK is received from Controller)
	fn send_done(&self,
		  tx_buffer: &'static mut [u8],
		  acked: bool,
		  result : Result<(), ErrorCode>);
}

pub trait RxClient {
	/// Called when a complete MCTP packet is received and ready to be processed.
	fn receive(&self, rx_buffer: &'static mut [u8], len : usize);

	/// Called when the I3C Controller has requested a private Write by addressing the target
	/// and the driver needs buffer to receive the data.
	/// The client should call set_rx_buffer() to set the buffer.
	fn write_expected(&self);
}


pub trait I3CTarget {
	/// Set the client that will be called when the packet is transmitted.
	fn set_tx_client(&self, client: &TxClient);

	/// Set the client that will be called when the packet is received.
	fn set_rx_client(&self, client: &RxClient);

	/// Set the buffer that will be used for receiving packets.
	fn set_rx_buffer(&self, rx_buf: &'static mut [u8]);
I
	/// Transmit a packet.
	fn transmit(&self, tx_buf: &'static mut[u8], len : usize) -> Result<(), (ErrorCode, &'static mut [u8])>;

	/// Enable/disable the I3C target device
	fn enable();
	fn disable();

	/// Get the maximum transmission unit (MTU) size.
	fn get_mtu_size() -> usize;

	/// Get the address of the I3C target device. Needed for PEC calculation.
	fn get_address() -> u8;
}

SPDM

The Security Protocol and Data Model (SPDM) is a protocol designed to ensure secure communication between hardware components by focusing on mutual authentication and the establishment of secure channels over potentially insecure media. SPDM enables devices to verify each other's identities and configurations, leveraging X.509v3 certificates to ensure cryptographic security. Designed for interoperability, it can work across various transport and physical media, often utilizing the Management Component Transport Protocol (MCTP). This protocol is especially valuable in environments where secure hardware communication is crucial, such as data centers and enterprise systems.

Specifications

SpecificationDocument Link
Security Protocol and Data ModelDSP0274
Secured Messages using SPDMDSP0277
SPDM over MCTP Binding SpecificationDSP0275
Secured Messages using SPDM over MCTP BindingDSP0276

SPDM Protocol Sequence

sequenceDiagram
    participant Requester
    participant Responder

    Requester->>Responder: GetVersion
    Responder-->>Requester: Version
    Requester->>Responder: GetCapabilities
    Responder-->>Requester: Capabilities
    Requester->>Responder: NegotiateAlgorithms
    Responder-->>Requester: Algorithms
    opt If supported
        Requester->>Responder: GetDigests
        Responder-->>Requester: Digests
    end
    opt If needed
        Requester->>Responder: GetCertificate
        Responder-->>Requester: Certificate
    end
    opt If supported
        Requester->>Responder: Challenge
        Responder-->>Requester: ChallengeAuth
    end
    opt If supported
        Requester->>Responder: GetMeasurements
        Responder-->>Requester: Measurements
    end
    opt If supported
        Requester->>Responder: KeyExchange
        Responder-->>Requester: KeyExchangeRsp
    end
    rect rgb(255, 255, 204)
        note right of Requester: Secure Session
        opt If supported
            Requester->>Responder: Finish
            Responder-->>Requester: FinishRsp
        end
    end

Class Diagram

classDiagram
    direction RL
    MCTP Transport <|--|> SPDMResponder: processRequest() / sendResponse()
    SecureSessionMgr <|-- SPDMResponder: processSecureMessage()
    TranscriptMgr <|-- SPDMResponder
    TranscriptMgr <|-- SecureSessionMgr
    class SPDMResponder{
      - transcriptMgr
      - sessionMgr
      + processRequest()
      + sendResponse()
    }
    class SecureSessionMgr {
      -transcriptMgr
      +TraitMethods
    }
    class TranscriptMgr{
      +TraitMethods
    }

SPDM Responder

The Responder is responsible for receiving and processing requests from the Requestor. It authenticates the Requestor's identity, attests its own state and configuration, and establishes a secure communication channel. The Responder uses cryptographic techniques, such as validating X.509v3 certificates, to ensure the integrity and confidentiality of the exchanged data.

Responder supported messages

The SPDM Responder supports the following messages:

MessageDescription
VERSIONRetrieves version information
CAPABILITIESRetrieves SPDM capabilities
ALGORITHMSRetrieves the negotiated algorithms
DIGESTSRetrieves digest of the certificate chains
CERTIFICATERetrieves certificate chains
MEASUREMENTSRetrieves measurements of elements such as intenral state
KEY_EXCHANGE_RSPRetrieves the responder's public key information
FINISH_RSPProvide key confirmation, bind the identity of each party to the exchanged keys
END_SESSION_ACKEnd session acknowledgment
ERRORError message

Responder Interface

pub struct SpdmResponder<T: MctpTransport, U: SpdmTranscriptManager, V: SpdmSecureSessionManager> {
    transport: T,
    transcript_manager: U,
    session_manager: V,
    data_buffer: [u8; MAX_SPDM_MESSAGE_SIZE],
}

impl<T: MctpTransport, U: SpdmTranscriptManager, V: SpdmSecureSessionManager> SpdmResponder<T, U, V> {
    pub fn new(transport: T, transcript_manager: U, session_manager: V) -> Self {
        SpdmResponder {
            transport,
            transcript_manager,
            session_manager,
            data_buffer: [0; MAX_SPDM_MESSAGE_SIZE],
        }
    }

    pub async fn handle_request(&mut self, request_info: u32) {
        // request_info: Bits[16:23] Message Type [SPDM | Secure SPDM]
    }
}

Transcript Manager

The Transcript Manager is for managing the transcript and the transcript hash. The transcript is a sequential concatenation of prescribed full messages or message fields. The transcript hash is the cryptographic hash of this transcript, computed using the negotiated hash algorithm. This component ensures the integrity and authenticity of SPDM communications by managing these essential elements.

Transcript Manager Interface

pub trait SpdmTranscriptManager {
    /// Set the hash algorithm. The algorithm can be set only once.
    ///
    /// # Parameters
    /// - `hash_algo`: Hash algorithm to set.
    ///
    /// # Returns
    /// - `Result<(), SpdmError>`: Returns `Ok(())` if the hash algorithm was set, or an error code.
    fn set_hash_algo(&self, hash_algo: HashType) -> Result<(), SpdmError>;

    /// Set the SPDM negotiated version to be used for communication.
    ///
    /// # Parameters
    /// - `spdm_version`: SPDM negotiated version.
    fn set_spdm_version(&self, spdm_version: u8);

    /// Update the transcript with a message.
    ///
    /// # Parameters
    /// - `context_type`:        Transcript context to update.
    /// - `message`:             Message to add to the transcript.
    /// - `use_session_context`: Use session context to update an SPDM session transcript.
    /// - `session_idx`:         SPDM session index.
    ///
    /// # Returns
    /// - `Result<(), SpdmError>`:
    /// Returns `Ok(())` if the message was added to the transcript successfully, or an error code.
    async fn update(
        &self,
        context_type: SpdmTranscriptManagerContextType, // [VCA, M1M2, L1L2, TH]
        message: &[u8],
        use_session_context: bool,
        session_idx: u8,
    ) -> Result<(), SpdmError>;

    /// Get the hash based on the hash type. The hashing operation is finished if `finish_hash` is set
    /// to `true`. In that case, an additional call to update will start a new hashing operation.
    /// If `finish_hash` is set to `false`, the hash is not finalized and can be updated with additional
    /// calls to update.
    ///
    /// # Parameters
    /// - `context_type`:        Transcript context type to get the hash from.
    /// - `finish_hash`:         Flag to indicate to finish the hash.
    /// - `use_session_context`: Use session context to update an SPDM session transcript.
    /// - `session_idx`:         SPDM session index.
    /// - `hash`:                Buffer to copy the hash to.
    ///
    /// # Returns
    /// - `Result<Vec<u8>, SpdmError>`: Returns the hash if the operation was successful, or an error code.
    fn get_hash(
        &self,
        context_type: SpdmTranscriptManagerContextType, // [VCA, M1M2, L1L2, TH]
        finish_hash: bool,
        use_session_context: bool,
        session_idx: u8,
        hash: &mut [u8]
    ) -> Result<(), SpdmError>;

    /// Reset a transcript context.
    ///
    /// # Parameters
    /// - `context_type`:        Transcript context to reset.
    /// - `use_session_context`: Use session context to update an SPDM session transcript.
    /// - `session_idx`:         SPDM session index.
    fn reset_transcript(
        &self,
        context_type: SpdmTranscriptManagerContextType, // [VCA, M1M2, L1L2, TH]
        use_session_context: bool,
        session_idx: u8,
    );
}

SPDM Secure Session Manager

The SPDM Secure Session Manager is responsible for managing secure sessions within the SPDM protocol framework. It provides mechanisms to create, release, and retrieve secure sessions. The manager can set and query the state of a session, ensuring secure communication between devices. It generates necessary cryptographic keys, including shared secrets, handshake keys, and data keys, through asynchronous methods. Additionally, it verifies the integrity and optionally decrypts secure messages, and encodes messages with appropriate security measures. The manager also tracks session validity and can reset session states and identifiers as needed, ensuring robust and secure session management.

Secure Session Manager Interface

pub trait SpdmSecureSessionManager {
    /// Create a new SPDM secure session.
    ///
    /// # Parameters
    /// - `session_id`:      Session Id for the session.
    /// - `is_requester`:    True if the session is for the requester, false otherwise.
    /// - `connection_info`: SPDM connection info.
    ///
    /// # Returns
    /// - `Option<&SpdmSecureSession>`:
    ///    A pointer to the created SPDM secure session or `None` if the session could not be created.
    fn create_session(
        &self,
        session_id: u32,
        is_requester: bool,
        connection_info: &SpdmConnectionInfo,
    ) -> Result<&SpdmSecureSession, SpdmError>;


    /// Release an SPDM secure session.
    ///
    /// # Parameters
    /// - `session_id`: Session Id for the session.
    fn release_session(&self, session_id: u32);

    /// Get an SPDM secure session.
    ///
    /// # Parameters
    /// - `session_id`: Session Id for the session.
    ///
    /// # Returns
    /// - `Option<&SpdmSecureSession>`: A pointer to the SPDM secure session or `None` if the session does not exist.
    fn get_session(& self, session_id: u32) -> Option<& SpdmSecureSession>;

    /// Set the session state for an SPDM secure session.
    ///
    /// # Parameters
    /// - `session_id`:    Session Id for the session.
    /// - `session_state`: Session state to set.
    fn set_session_state(&self, session_id: u32, session_state: SpdmSecureSessionState);

    /// Reset the Session Manager.
    fn reset(&self);

    /// Generate the shared secret from peer and local public keys.
    ///
    /// # Parameters
    /// - `session`:            SPDM session info.
    /// - `peer_pub_key_point`: Peer public key in point format.
    /// - `local_public_key`: Generated local public key in point format on return.
    ///
    /// # Returns
    /// - `Result<(), SpdmError>`:
    ///    Returns the local public key in point format if the shared secret is generated successfully, or an error code.
    fn generate_shared_secret(
        &self,
        session: &SpdmSecureSession,
        peer_pub_key_point: &EccPointPublicKey,
        local_public_key: &mut [u8]
    ) -> Result<(), SpdmError>;

    /// Generate handshake keys for an SPDM secure session.
    ///
    /// # Parameters
    /// - `session`: Secure Session.
    ///
    /// # Returns
    /// - `Result<(), SpdmError>`: Returns `Ok(())` if the handshake keys are generated successfully, or an error code.
    async fn generate_session_handshake_keys(&self, session: &mut SpdmSecureSession) -> Result<(), SpdmError>;

    /// Generate data keys for an SPDM secure session.
    ///
    /// # Parameters
    /// - `session`: SPDM Secure Session.
    ///
    /// # Returns
    /// - `Result<(), SpdmError>`: Returns `Ok(())` if the data keys are generated successfully, or an error code.
    async fn generate_session_data_keys(&self, session: &mut SpdmSecureSession) -> Result<(), SpdmError>;

    /// Query if the last session is active.
    ///
    /// # Returns
    /// - `bool`: True if the last session is active, false otherwise.
    fn is_last_session_id_valid(&self) -> bool;

    /// Get the last session id.
    ///
    /// # Returns
    /// - `u32`: Last session id.
    fn get_last_session_id(&self) -> u32;

    /// Reset the last session id validity.
    fn reset_last_session_id_validity(&self);

    /// Decode a secure message. This includes MAC verification and optionally decryption.
    ///
    /// # Parameters
    /// - `request`: SPDM request message to be decoded.
    ///
    /// # Returns
    /// - `Result<(), SpdmError>`: Returns `Ok(())` if the secure message is decoded successfully, or an error code.
    async fn decode_secure_message(&self, request: &mut [u8]) -> Result<(), SpdmError>;

    /// Encode a secure message. This includes MAC generation and optionally encryption.
    ///
    /// # Parameters
    /// - `response`: SPDM response message to be encoded.
    ///
    /// # Returns
    /// - `Result<(), SpdmError>`: Returns `Ok(())` if the secure message is encoded successfully, or an error code.
    async fn encode_secure_message(&self, response: &mut [u8]) -> Result<(), SpdmError>;
}

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:

The DOE Tock receive stack

DOE Send stack:

The DOE Tock 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.


/// 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>;
}

IDE_KM - Integrity and Data Encryption Key Management Protocol Support

The Caliptra subsystem enables IDE_KM protocol support over SPDM secure sessions by transmitting IDE_KM messages as application data. The IDE_KM protocol manages the provisioning of encryption keys for IDE streams, providing confidentiality, integrity, and replay protection for Translation Layer Packets (TLPs).

To implement IDE_KM, devices must provide the IdeDriver trait implementation. This trait defines the interfaces and configuration necessary for secure IDE key management. This documentation describes how to integrate IDE_KM with the Caliptra subsystem, outlines implementation requirements, and offers guidance for usage.

#![allow(unused)]

fn main() {
pub const IDE_STREAM_KEY_SIZE_DW: usize = 8;
pub const IDE_STREAM_IV_SIZE_DW: usize = 2;
pub const MAX_SELECTIVE_IDE_ADDR_ASSOC_BLOCK_COUNT: usize = 15;

/// Port Configuration structure contains the configuration and capabilities for a specific IDE port.
pub struct PortConfig {
    port_index: u8,
    function_num: u8,
    bus_num: u8,
    segment: u8,
    max_port_index: u8,
}

/// IDE Capability and Control Register Block
pub struct IdeRegBlock {
    ide_cap_reg: IdeCapabilityReg,
    ide_ctrl_reg: IdeControlReg,
}

/// Link IDE Register Block
pub struct LinkIdeStreamRegBlock {
    ctrl_reg: LinkIdeStreamControlReg,
    status_reg: LinkIdeStreamStatusReg,
}

/// Selective IDE Stream Register Block
pub struct SelectiveIdeStreamRegBlock {
    capability_reg: SelectiveIdeStreamCapabilityReg,
    ctrl_reg: SelectiveIdeStreamControlReg,
    status_reg: SelectiveIdeStreamStatusReg,
    rid_association_reg_1: SelectiveIdeRidAssociationReg1,
    rid_association_reg_2: SelectiveIdeRidAssociationReg2,
    addr_association_reg_block: [AddrAssociationRegBlock; MAX_SELECTIVE_IDE_ADDR_ASSOC_BLOCK_COUNT],
}

/// IDE Address Association Register Block
pub struct AddrAssociationRegBlock {
    reg1: IdeAddrAssociationReg1,
    reg2: IdeAddrAssociationReg2,
    reg3: IdeAddrAssociationReg3,
}

/// IDE Driver Error Types
pub enum IdeDriverError {
    InvalidPortIndex,
    UnsupportedPortIndex,
    InvalidStreamId,
    InvalidArgument,
    GetPortConfigFail,
    KeyProgFail,
    KeySetGoFail,
    KeySetStopFail,
    NoMemory,
}

pub type IdeDriverResult<T> = Result<T, IdeDriverError>;


/// KeyInfo structure contains information about the key set, direction, and sub-stream.
bitfield! {
    #[derive(Clone, Copy, PartialEq, Eq)]
    pub struct KeyInfo(u8);
    impl Debug;
    pub key_set_bit, set_key_set_bit: 0;
    pub key_direction, set_key_direction: 1;
    reserved, _: 3, 2;
    pub key_sub_stream, set_key_sub_stream: 7, 4;
}

/// IDE Driver Trait
///
/// Provides an interface for Integrity and Data Encryption (IDE) key management operations.
/// This trait abstracts hardware-specific implementations for different platforms.
#[async_trait]
pub trait IdeDriver {
    /// Get the port configuration for a given port index.
    ///
    /// # Arguments
    /// * `port_index` - The index of the port to retrieve the configuration for.
    ///
    /// # Returns
    /// A result containing the `PortConfig` for the specified port index, or an error
    /// if the port index is invalid or unsupported.
    fn port_config(&self, port_index: u8) -> IdeDriverResult<PortConfig>;

    /// Get the IDE register block.
    ///
    /// # Arguments
    /// * `port_index` - The index of the port.
    ///
    /// # Returns
    /// A result containing the `IdeRegBlock` for the specified port, or an error
    fn ide_register_block(&self, port_index: u8) -> IdeDriverResult<IdeRegBlock>;

    /// Get the link IDE register block for a specific port and block index.
    ///
    /// # Arguments
    /// * `port_index` - The index of the port.
    /// * `block_index` - The index of the register block.
    ///
    /// # Returns
    /// A result containing the `LinkIdeStreamRegBlock` for the specified port and block
    fn link_ide_reg_block(
        &self,
        port_index: u8,
        block_index: u8,
    ) -> IdeDriverResult<LinkIdeStreamRegBlock>;

    /// Get the selective IDE register block for a specific port and block index.
    ///
    /// # Arguments
    /// * `port_index` - The index of the port.
    /// * `block_index` - The index of the register block.
    ///
    /// # Returns
    /// A result containing the `SelectiveIdeStreamRegBlock` for the specified port and block
    fn selective_ide_reg_block(
        &self,
        port_index: u8,
        block_index: u8,
    ) -> IdeDriverResult<SelectiveIdeStreamRegBlock>;

    /// Key programming for a specific port and stream.
    ///
    /// # Arguments
    /// * `stream_id` - Stream ID
    /// * `key_info` - Key information containing key set bit, direction, and sub-stream.
    /// * `port_index` - Port to which the key is to be programmed.
    /// * `key` - The key data to be programmed (8 DWORDs).
    /// * `iv` - The initialization vector (2 DWORDs).
    ///
    /// # Returns
    /// A result containing the status of the key programming operation:
    /// - `00h`: Successful
    /// - `01h`: Incorrect Length
    /// - `02h`: Unsupported Port Index value
    /// - `03h`: Unsupported value in other fields
    /// - `04h`: Unspecified Failure
    async fn key_prog(
        &self,
        stream_id: u8,
        key_info: KeyInfo,
        port_index: u8,
        key: &[u32; IDE_STREAM_KEY_SIZE_DW],
        iv: &[u32; IDE_STREAM_IV_SIZE_DW],
    ) -> IdeDriverResult<u8>;

    /// Start using the key set for a specific port and stream.
    ///
    /// # Arguments
    /// * `stream_id` - Stream ID
    /// * `key_info` - Key information containing key set bit, direction, and sub-stream.
    /// * `port_index` - Port to which the key set is to be started.
    ///
    /// # Returns
    /// A result containing the updated `KeyInfo` after starting the key set, or an
    /// error if the operation fails.
    async fn key_set_go(
        &self,
        stream_id: u8,
        key_info: KeyInfo,
        port_index: u8,
    ) -> IdeDriverResult<KeyInfo>;

    /// Stop the key set for a specific port and stream.
    ///
    /// # Arguments
    /// * `stream_id` - Stream ID
    /// * `key_info` - Key information containing key set bit, direction, and sub-stream.
    /// * `port_index` - Port to which the key set is to be stopped
    ///
    /// # Returns
    /// A result containing the updated `KeyInfo` after stopping the key set, or an error
    /// if the operation fails.
    async fn key_set_stop(
        &self,
        stream_id: u8,
        key_info: KeyInfo,
        port_index: u8,
    ) -> IdeDriverResult<KeyInfo>;
}
}

TDISP (TEE Device Interface Security Protocol) Support

Caliptra Subsystem supports handling of TDISP messages by processing them as VENDOR_DEFINED_REQUEST/VENDOR_DEFINED_RESPONSE message payloads. These messages are transported and processed within the secure session established between the host and the TDISP device as specified by Secured CMA/SPDM.

To facilitate the TDISP protocol, the devices must implement TdispDriver trait as defined below.

#![allow(unused)]
fn main() {
/// Error codes returned by TDISP driver
pub enum TdispDriverError {
    /// Input parameter is null or invalid.
    InvalidArgument,
    /// Memory allocation failed.
    NoMemory,
    /// The driver failed to get TDISP capabilities.
    GetTdispCapabilitiesFail,
    /// The driver failed to get the device interface state.
    GetDeviceInterfaceStateFail,
    /// The driver failed to lock the device interface.
    LockInterfaceReqFail,
    /// The driver failed to start the device interface.
    StartInterfaceReqFail,
    /// The driver failed to stop the device interface.
    StopInterfaceReqFail,
    /// The driver failed to get the device interface report.
    GetInterfaceReportFail,
    /// The driver failed to get the mmio ranges.
    GetMmioRangesFail,
    /// The driver function is not implemented.
    FunctionNotImplemented,
}

pub type TdispDriverResult<T> = Result<T, TdispDriverError>;

/// FunctionID of the device hosting the TDI.
bitfield! {
    #[derive(FromBytes, IntoBytes, Immutable, Default)]
    #[repr(C)]
    pub struct FunctionId(u32);
    impl Debug;
    u16;
    pub requester_id, set_requester_id: 15, 0; // Bits 15:0 Requester ID
    u8;
    pub requester_segment, set_requester_segment: 23, 16; // Bits 23:16 Requester Segment
    pub requester_segment_valid, set_requester_segment_valid: 24, 24; // Bit 24 Requester Segment Valid
    reserved, _: 31, 25; // Bits 31:25 Reserved
}

/// TDISP Responder Capabilities
pub struct TdispResponderCapabilities {
    dsm_capabilities: u32,
    req_msgs_supported: [u8; 16],
    lock_interface_flags_supported: u16,
    reserved: [u8; 3],
    dev_addr_width: u8,
    num_req_this: u8,
    num_req_all: u8,
}

/// Parameters passed along with the LOCK_INTERFACE_REQUEST
pub struct TdispLockInterfaceParam {
    flags: TdispLockInterfaceFlags,
    default_stream_id: u8,
    reserved: u8,
    mmio_reporting_offset: [u8; 8],
    bind_p2p_addr_mask: [u8; 8],
}

/// TDISP Interface flags
bitfield! {
#[repr(C)]
pub struct TdispLockInterfaceFlags(u16);
impl Debug;
u8;
    pub no_fw_update, set_no_fw_update: 0, 0; // Bit 0 NO_FW_UPDATE
    pub system_cache_line_size, set_system_cache_line_size: 1, 1; // Bits 1:1 SYSTEM_CACHE_LINE_SIZE
    pub lock_msix, set_lock_msix: 2, 2; // Bit 2 LOCK_MSIX
    pub bind_p2p, set_bind_p2p: 3, 3; // Bit 3 BIND_P2P
    pub all_req_redirect, set_all_req_redirect: 4, 4; // Bit 4 ALL_REQUEST_REDIRECT
    pub reserved, _: 15, 5; // Bits 15:5 Reserved
}

/// TDI Status
pub enum TdiStatus {
    ConfigUnlocked = 0,
    ConfigLocked = 1,
    Run = 2,
    Error = 3,
    Reserved,
}

/// TDISP Driver trait that defines the interface for TDISP operations.
/// This trait is intended to be implemented by a TDISP driver
/// that interacts with the TDISP device.
#[async_trait]
pub trait TdispDriver: Send + Sync {
    /// Gets the TDISP device capabilities.
    ///
    /// # Arguments
    /// * `req_caps` - Requester (TSM) capability flags
    /// * `resp_caps` - Responder (DSM) capability flags
    ///
    /// # Returns
    /// 0 on success or an error response code as per the TDISP specification on failure.
    async fn get_capabilities(
        &self,
        req_caps: TdispReqCapabilities,
        resp_caps: &mut TdispRespCapabilities,
    ) -> TdispDriverResult<u32>;

    /// Lock Interface Request
    ///
    /// # Arguments
    /// * `function_id` - Device Interface Function ID
    /// * `param` - Lock Interface parameters from the request
    ///
    /// # Returns
    /// 0 on success or an error response code as per the TDISP specification on failure.
    async fn lock_interface(
        &mut self,
        function_id: FunctionId,
        param: TdispLockInterfaceParam,
    ) -> TdispDriverResult<u32>;

    /// Get the length of the device interface report.
    ///
    /// # Arguments
    /// * `function_id` - Device Interface Function ID
    /// * `intf_report_len` - Total device interface report length(output)
    ///
    /// # Returns
    /// Length of the device interface report on success or an error response code.
    async fn get_device_interface_report_len(
        &self,
        function_id: FunctionId,
        intf_report_len: &mut u16,
    ) -> TdispDriverResult<u32>;

    /// Get the device interface report.
    ///
    /// # Arguments
    /// * `function_id` - Device Interface Function ID
    /// * `offset` - Offset from the start of the report requested
    /// * `report` - report buffer slice to fill
    /// * `copied` - Length of the TDI report copied
    ///
    ///
    /// # Returns
    /// 0 on success or an error response code as per the TDISP specification on failure.
    async fn get_device_interface_report(
        &self,
        function_id: FunctionId,
        offset: u16,
        report: &mut [u8],
        copied: &mut usize,
    ) -> TdispDriverResult<u32>;

    /// Get the device interface state.
    ///
    /// # Arguments
    /// * `function_id` - Device Interface Function ID
    /// * `tdi_state` - Device Interface State to fill
    ///
    /// # Returns
    /// 0 on success or an error response code as per the TDISP specification on failure.
    async fn get_device_interface_state(
        &self,
        function_id: FunctionId,
        tdi_state: &mut TdiStatus,
    ) -> TdispDriverResult<u32>;

    /// Start the device interface.
    ///
    /// # Arguments
    /// * `function_id` - Device Interface Function ID
    ///
    /// # Returns
    /// 0 on success or an error response code as per the TDISP specification on failure.
    async fn start_interface(&mut self, function_id: FunctionId) -> TdispDriverResult<u32>;

    /// Stop the device interface.
    ///
    /// # Arguments
    /// * `function_id` - Device Interface Function ID
    ///
    /// # Returns
    /// 0 on success or an error response code as per the TDISP specification on failure.
    async fn stop_interface(&mut self, function_id: FunctionId) -> TdispDriverResult<u32>;
}
}

In-field Provisioning and Management of SPDM Certificate Slots

This document provides guidance for provisioning and managing certificate slots on Caliptra devices via the SPDM protocol, conforming to the OCP Device Identity Provisioning Specification.

Supporting the multiple PKI ownership model defined by OCP requires SPDM Responder to support multiple asymmetric key pairs in the connection (MULTI_KEY_CONN_RSP is true). Consequently, OCP Device Identity Provisioning features requires SPDM version 1.3 or later.

Caliptra Device Identity Key Pairs Discovery

Caliptra device shall support the following 3 key pairs for certificate slot provisioning and management:

  • LDevID Key Pair (SPDM Key pair ID 1)
  • FMC Alias Key Pair (SPDM Key pair ID 2)
  • RT Alias Key Pair (SPDM Key pair ID 3)

The IdevID key pair is pre‑provisioned by the vendor in SPDM certificate slot 0 and is treated as non‑configurable by the implementation.

The information about these key pairs can be retrieved by issuing the GET_KEY_PAIR_INFO request. The SPDM responder sets the GET_KEY_PAIR_INFO_CAP capability bit to advertise support for KEY_PAIR_INFO response messages. The implementation does not allow the requester to modify any parameters associated with Caliptra key pairs; as a result, the SET_KEY_PAIR_INFO_CAP capability bit remains cleared.

SPDM Certificate Slot mapping to OCP PKI entities

The default SPDM certificate slot mapping to OCP PKI entities is as follows:

  • Slot 0: Vendor (pre-provisioned, fixed and read-only)
  • Slot 1: Unused (available for future use)
  • Slot 2: Owner (provisionable by Owner PKI)
  • Slot 3: Tenant (provisionable by Tenant PKI)

But the actual mapping can be discovered by the SPDM Requester using the OCP_GET_SLOT_ID_MAPPING request.

Provisioning workflows using SPDM protocol

It is assumed that the Vendor slot is pre‑provisioned and meets the SPDM requirements for initial device attestation. The Vendor slot may also be used to establish a secure session with the SPDM Requester (Owner/Tenant PKI) for subsequent certificate‑slot provisioning.

The following sequence diagram illustrates the workflow for provisioning Owner certificate slots using the SPDM protocol.

sequenceDiagram
    participant SPDMRequester as SPDM Requester/PKI Owner
    participant MCU as Caliptra MCU/SPDM Responder
    participant CaliptraRT
    Note over SPDMRequester,MCU: ...<br/> 1. Perform initial Device Attestation using Vendor Slot. <br/>The requester has the device identity certificates.<br/> ...
    opt 
        Note over SPDMRequester,MCU: 2. Retrieve Device Identity Key Pair Info
        SPDMRequester->>+MCU: GET_KEY_PAIR_INFO (KeyPairID:1 (LDevID))
        MCU->>-SPDMRequester: KEY_PAIR_INFO (TotalKeyPairs: 3, KeyPairID: 1, AssocCertSlotMask: 0x00)
    end
    opt 
        Note over SPDMRequester,MCU: 3. Discover SPDM Slot ID mapping for different OCP PKI entities
        SPDMRequester->>+MCU: OCP_GET_SLOT_ID_MAPPING
        MCU->>-SPDMRequester: OCP_SLOT_ID_MAPPING (Vendor:0, Owner: 2, Tenant:3)
    end
    Note over SPDMRequester,MCU: 4. Generate Envelope Signed CSR <br/> for key pair ID 1
    SPDMRequester->>+MCU: GET_ENVELOPE_SIGNED_CSR (KeyPairID: 1, Nonce: nonce)
    MCU->>+CaliptraRT: Request envelope signed LDevID CSR (nonce)
    CaliptraRT->>-MCU: Envelope signed LDevID CSR signed by RT alias key
    MCU->>-SPDMRequester: ENVELOPE_SIGNED_CSR (Envelope signed CSR data)
    SPDMRequester-->>SPDMRequester: Validate the CSR and <br/>Issue endorsement certificate for LDevID
    critical Within secure session and/or with requester authorization
        Note over SPDMRequester,MCU: 5. Complete Owner Slot Provisioning
        SPDMRequester->>+MCU: SET_CERTIFICATE (SlotID: 2, KeyPairID: 1, CertChain)
        MCU->>-SPDMRequester:SET_CERTIFICATE_RSP (SlotID: 2)
    end
    Note over SPDMRequester,MCU: 6. Verify Owner Slot Certificate Installation
    SPDMRequester->>+MCU: GET_KEY_PAIR_INFO (KeyPairID: 1)
    MCU->>-SPDMRequester: KEY_PAIR_INFO (TotalKeyPairs: 3, KeyPairID: 1, AssocCertSlotMask: 0x04)
    opt Get installed certificate chain and validate
        SPDMRequester->>+MCU: GET_CERTIFICATE (SlotID: 2)
        MCU->>-SPDMRequester: CERTIFICATE (SlotID: 2, CertChain)
        SPDMRequester-->>SPDMRequester: Validate the installed certificate chain
    end
    Note over SPDMRequester,MCU: ...<br/> 7. Perform attestation using the newly installed Owner slot

Envelope-signed CSR generation

The MCU's SPDM responder supports retrieval of an envelope‑signed Certificate Signing Request (CSR) for a specified Device Identity Key-pair ID via the OCP vendor‑defined GET_ENVELOPE_SIGNED_CSR command. Upon receiving this request, the MCU forwards it to the Caliptra RT firmware, which generates the CSR and returns it encapsulated in an EAT (Entity Attestation Token). The EAT is signed using the RT Alias key pair. The nonce provided in the request is forwarded to the RT firmware and included in the EAT to ensure freshness.

Authorization and Security Considerations

TBD

PLDM Stack

Overview

The Platform Level Data Model (PLDM) is a suite of specifications that define a common data model and message formats for communication between management controllers and managed devices. It is designed to standardize the way devices communicate in a platform management environment, enabling interoperability and simplifying the management of hardware components. In the context of Caliptra MCU, PLDM Base Protocol and PLDM for Firmware Update Protocol are supported by the PLDM stack to facilitate the following use cases:

  • PLDM message control and discovery: This feature enables the device to be discovered by the platform management controller (typically a BMC) following MCTP enumeration. It forms the foundation for running other PLDM protocols.

  • Streaming boot remainder firmware: The PLDM firmware update protocol defines standardized messages and data structures for obtaining firmware code and data. The MCU leverages it to stream boot the remainder firmware, which is any vendor-specific SoC or other firmware. There are several customized amendments to the PLDM firmware update specification to enable streaming boot and automatic activation. Details are available in the OCP whitepaper, Flashless Boot using OCP, PCIe, and DMTF Standards.

  • Impactless firmware update: PLDM firmware update over MCTP is a well-established approach for firmware updates, supporting multiple firmware components within a single package. Updates can be applied to a subset of components supported by the Firmware Device (FD), which is a valuable property to enable impactless updates. Details can be found in the firmware update spec.

Architecture

The PLDM stack in MCU runtime is a modular implementation that supports the PLDM base protocol as a responder and the PLDM firmware update protocol as a Firmware Device (FD). It operates in the userspace of MCU runtime and interacts with the MCTP transport layer to handle communication.

PLDM Stack for Base Protocol

PLDM stack base

  • PLDM service
    • Listens for incoming PLDM requests.
    • Extracts the command opcode from the request and invokes the corresponding handler to process.
    • Sends the response via the MCTP transport layer.
  • Command handler
    • Interacts with the message decoding library to decode the request.
    • Executes the command.
    • Encodes the response by interacting with the message encoding library.

PLDM Stack for Firmware Update Protocol

PLDM stack firmware update

  • PLDM service

    • Listens for incoming firmware update requests and responses.
    • Extracts the command opcode from the request or response and invokes the corresponding handler to process.
    • Sends the response via the MCTP transport layer.
  • Firmware update service

    • Firmware update command handler

      • Decodes the request or response.
      • Executes the appropriate actions based on the command and interacts with the state machine to trigger the state transition.
      • Encodes responses by interacting with the message encoding library.
    • Firmware update state control

      • Maintains the current state of the firmware update process.
      • Provides mechanisms to transition between different states, ensuring the correct sequence of operations.
      • Interacts with the firmware update service to manage state transitions based on the stage of the update process and signals the request initiator to process the outgoing request.
    • Request initiator

      • Listens for the signal from state control to process the outgoing request.
      • Interacts with the message encoding library to encode the request.

PLDM Base Protocol Sequence

The table below shows the command codes of the base protocol that are supported by the PLDM stack as a responder.

Command NameCommand CodeDirectionRequirement
GetTID0x02UA -> FDMandatory
GetPLDMVersion0x03UA -> FDMandatory
GetPLDMTypes0x04UA -> FDMandatory
GetPLDMCommands0x05UA -> FDMandatory
SetTID0x01UA -> FDOptional

The diagram below shows the PLDM message control and discovery sequence.

sequenceDiagram
        participant UA as Update Agent
        participant FD as Firmware Device
        UA->>FD: GetTID
        FD-->>UA: TID Response
        UA->>FD: GetPLDMTypes
        FD-->>UA: PLDMTypes Response (type-0, type-5)
        UA->>FD: GetPLDMVersion for type-0
        FD-->>UA: PLDMVersion Response
        UA->>FD: GetPLDMCommands for type-0
        FD-->>UA: PLDMCommands Response
        UA->>FD: GetPLDMVersion for type-5
        FD-->>UA: PLDMVersion Response
        UA->>FD: GetPLDMCommands for type-5
        FD-->>UA: PLDMCommands Response

PLDM Firmware Update Protocol Sequence

The table below shows the inventory commands and firmware update commands supported by the PLDM stack as FD.

Command NameCommand CodeDirectionRequirement
QueryDeviceIdentifiers0x01UA -> FDMandatory
GetFirmwareParameters0x02UA -> FDMandatory
RequestUpdate0x10UA -> FDMandatory
PassComponentTable0x13UA -> FDMandatory
UpdateComponent0x14UA -> FDMandatory
RequestFirmwareData0x15FD -> UAMandatory
TransferComplete0x16FD -> UAMandatory
VerifyComplete0x17FD -> UAMandatory
ApplyComplete0x18FD -> UAMandatory
GetMetaData0x19FD -> UAMandatory
ActivateFirmware0x1AUA -> FDMandatory
GetStatus0x1BUA -> FDMandatory
CancelUpdateComponent0x1CUA -> FDMandatory
CancelUpdate0x1DUA -> FDMandatory

The diagram below shows a complete PLDM firmware update sequence:

sequenceDiagram
        participant UA as Update Agent
        participant FD as Firmware Device
        UA->>FD: QueryDeviceIdentifiers
        FD-->>UA: DeviceIdentifiers Response
        UA->>FD: GetFirmwareParameters
        FD-->>UA: FirmwareParameters Response
        UA->>FD: RequestUpdate
        FD-->>UA: Update Response
        UA->>FD: PassComponentTable
        FD-->>UA: ComponentTable Response
        UA->>FD: UpdateComponent
        FD-->>UA: UpdateComponent Response
        FD->>UA: RequestFirmwareData
        UA-->>FD: FirmwareData Response
        FD->>UA: TransferComplete
        UA-->>FD: TransferComplete Response
        Note over FD: Verifying component
        FD->>UA: VerifyComplete
        UA-->>FD: VerifyComplete Response
        FD->>UA: ApplyComplete
        UA-->>FD: ApplyComplete Response
        UA->>FD: ActivateFirmware
        FD-->>UA: ActivateFirmware Response
        UA->>FD: GetStatus
        FD-->>UA: Status Response

Interface

The PLDM stack is designed as a library that supports the PLDM base protocol as a responder and the PLDM firmware update protocol as a Firmware Device (FD). The diagram below shows the interface and components inside the stack. PldmFwUpdateServiceMgr serves as the interface between the PLDM stack and upper-level APIs, such as Firmware Update and Streaming Boot.

classDiagram
        class PldmFwUpdateServiceMgr {
                <<interface>>
                +start_service() Result<(), PldmServiceError>
                +stop_service() Result<(), PldmServiceError>
        }

        class PldmFwUpdateService {
                +transport: T
                +cmd_interface_responder: MessageResponder
                +cmd_interface_requester: MessageRequester
                +notification: Notifications
                +start_service() Result<(), PldmServiceError>
                +stop_service() Result<(), PldmServiceError>
        }

        class MessageResponder {
                <<interface>>
                +process_request(payload: &mut [u8]) Result<usize, MessageHandlerError>
                +process_response(payload: &mut [u8]) Result<usize, MessageHandlerError>
        }

        class MessageRequester {
                <<interface>>
                +generate_request(payload: &mut [u8]) Result<usize, MessageHandlerError>
        }

        class CommandHandler {
                <<interface>>
                +execute(payload: &mut [u8], context: &mut PldmContext) usize
        }

        PldmFwUpdateServiceMgr <|-- PldmFwUpdateService
        PldmFwUpdateService o-- MessageResponder
        PldmFwUpdateService o-- MessageRequester
        MessageResponder ..> CommandHandler : invoke
/// Trait representing a PLDM Firmware Update Service Manager.
///
/// This trait defines the necessary methods to start and stop the PLDM firmware update service.
///
/// # Methods
///
/// * `start_service` - Asynchronously starts the PLDM firmware update service.
///    This method creates an async task to listen for and process incoming requests.
/// * `stop_service` - Asynchronously stops the PLDM firmware update service.
///
/// # Errors
///
/// Both methods return a `Result` which, on error, contains a `PldmServiceError`.
pub trait PldmFwUpdateServiceMgr {
    async fn start_service(&self) -> Result<(), PldmServiceError>;
    async fn stop_service(&self) -> Result<(), PldmServiceError>;
}
pub struct PldmServiceError(pub NonZeroU32);

/// Represents a PLDM Firmware Update Service.
///
/// This struct is responsible for handling the PLDM firmware update process
/// using the specified transport, message responder, and message requester.
///
/// # Type Parameters
///
/// * `T` - A type that implements the `Mctp` trait, representing the transport layer.
/// * `U` - A type that implements the `MessageResponder` trait, representing the command interface responder.
/// * `R` - A type that implements the `MessageRequester` trait, representing the command interface requester.
/// * `N` - A type that implements the `Notifications` trait, representing the notification interface.
///
/// # Fields
///
/// * `transport` - The transport layer used for communication.
/// * `cmd_interface_responder` - The command interface responder.
/// * `cmd_interface_requester` - The command interface requester.
/// *  `notification` - The notification interface to upper API.
/// * `other fields` - Additional fields required for the service.
pub struct PldmFwUpdateService<T: Mctp, U: MessageResponder, R: MessageRequester, N: Notifications> {
    transport: T,
    cmd_interface_responder: U,
    cmd_interface_requester: R,
    notifications: N,
    // other fields
}

/// Trait representing the notification interface for the PLDM firmware update service.
///
/// This trait defines the necessary methods that will be called by the `PldmFwUpdateService`
/// to notify the upper-level API of specific events during the firmware update process.
pub trait Notifications {
    fn on_request_update_received(&self, payload: &[u8]) -> Result<(), ErrCode>;
    fn on_image_verify(&self, payload: &[u8]) -> Result<(), ErrCode>;
    fn on_image_apply(&self, payload: &[u8]) -> Result<(), ErrCode>;
    fn on_update_component_received(&self, payload: &[u8]) -> Result<(), ErrCode>;
    fn on_activate_firmware_received(&self, payload: &[u8]) -> Result<(), ErrCode>;
}

impl PldmFwUpdateServiceMgr for PldmFwUpdateService {
    async fn start_service(&self) -> Result<(), PldmServiceError> {};
    async fn stop_service(&self) -> Result<(), PldmServiceError> {};
}

/// Trait representing a message responder that can process requests and responses asynchronously.
///
/// # Required Methods
///
/// - `process_request(&self, payload: &mut [u8]) -> Result<usize, MessageHandlerError>`:
///   Processes an incoming request message.
///   - `payload`: A mutable reference to the payload data to be processed.
///   - Returns a `Result` containing the size of the processed data or a `MessageHandlerError`.
///
/// - `process_response(&self, payload: &mut [u8]) -> Result<usize, MessageHandlerError>`:
///   Processes an incoming response message.
///   - `payload`: A mutable reference to the payload data to be processed.
///   - Returns a `Result` containing the size of the processed data or a `MessageHandlerError`.
///
/// # Errors
///
/// Both methods return a `Result` which, on failure, contains a `MessageHandlerError`.
pub trait MessageResponder: Send + Sync {
    async fn process_request(&self, payload: &mut [u8]) -> Result<usize, MessageHandlerError>;
    async fn process_response(&self, payload: &mut [u8]) -> Result<usize, MessageHandlerError>;
}

/// Trait representing a message requester that can generate requests asynchronously.
pub trait MessageRequester: Send + Sync {
    /// Asynchronously generates a request with the given payload.
    ///
    /// # Arguments
    ///
    /// * `payload` - A mutable reference to a byte slice that will be used to generate the request.
    ///
    /// # Returns
    ///
    /// A `Result` containing the size of the generated request on success, or a `MessageHandlerError` on failure.
    async fn generate_request(&self, payload: &mut [u8]) -> Result<usize, MessageHandlerError>;
}
pub struct MessageHandlerError(pub NonZeroU32);

/// Trait representing a command handler that can execute commands asynchronously.
///
/// # Required Methods
///
/// - `execute(&self, payload: &mut [u8], context: &mut PldmContext) -> usize`:
///   Executes a command with the given payload and context.
///   - `payload`: A mutable reference to the payload data to be processed.
///   - `context`: A mutable reference to the PLDM context.
///   - Returns the size of the processed data.
pub trait CommandHandler: Send + Sync {
    async fn execute(&self, payload: &mut [u8], context: &mut PldmContext) -> usize;
}

/// Represents a PLDM Command Interface Responder.
///
/// This struct is responsible for handling the PLDM command interface
/// by maintaining a collection of command handlers and the PLDM context.
///
/// # Fields
///
/// * `handlers` - A heapless hash table that maps command codes to their respective command handlers.
/// * `context` - The PLDM context used for processing commands.
/// * `other fields` - Additional fields required for the command interface responder.
pub struct CmdInterfaceResponder<'a, const N: usize> {
    handlers: heapless::FnvIndexMap<u8, &'a dyn CommandHandler, N>,
    context: PldmContext,
    // Other fields
}

pub struct CmdInterfaceRequester<'a, const N: usize> {
    handlers: heapless::FnvIndexMap<u8, &'a dyn CommandHandler, N>,
    // Other fields
}

Firmware Update

Overview

The MCU SDK offers a comprehensive API designed to facilitate firmware updates for Caliptra FMC & RT, MCU RT, and other SoC images. These updates are performed using the PLDM - T5 protocol and are supported for both streaming boot systems and flash boot systems.

Architecture

The MCU PLDM stack handles PLDM firmware messages from an external Firmware Update Agent. The stack generates upstream notifications to the Firmware Update API to handle application-specific actions such as writing firmware chunks to a staging or SPI Flash storage location, verifying components, etc through the Image Loading API. The API notifies the application of the start and completion of the firmware update process.

graph TD;
    A[Application / Initiator] <--> B[API];
    subgraph B[API]
        direction LR
        B1[Firmware Update] <--> B2[Image Loading];
        B2 <--> B3[DMA]
        B2 <--> B4[Flash]
        B2 <--> B5[Mailbox]
    end
    B <--> C[PLDM];

PLDM Firmware Download Sequence

The diagram below shows the steps and interactions between different software layers during the firmware update process.

sequenceDiagram
    title Firmware Update Service Initialization

    actor App as Initiator
    participant API as Firmware Update API
    participant Firmware as PLDM Stack - T5
    participant PLDM as Update Agent

    App->>API: Start Firmware Update Service
    loop for all components
    API->>API: Retrieve firmware metadata from Caliptra core
    end

    API->>Firmware: Start Firmware Update Service
    Firmware->>Firmware: Start listen loop
    activate Firmware

Query Device Information

sequenceDiagram
    title Query Device Information

    actor App as Initiator
    participant API as Firmware Update API
    participant Firmware as PLDM Stack - T5
    participant PLDM as Update Agent

    PLDM->>Firmware: QueryDeviceIdentifiers
    Firmware-->>PLDM: DeviceIdentifiers

    PLDM->>Firmware: GetFirmwareParameters
    Firmware-->>PLDM: FirmwareParameters

Request Update and Pass Components

sequenceDiagram
    title Request Update and Pass Components

    actor App as Initiator
    participant API as Firmware Update API
    participant Firmware as PLDM Stack - T5
    participant PLDM as Update Agent

    PLDM->>Firmware: RequestUpdate
    Firmware->>API: Update Available Notification
    API->>App: Update Available Notification
    API-->>Firmware: Ok
    Firmware-->>PLDM: RequestUpdate Response

    loop until all component info passed
        PLDM->>Firmware: PassComponent(component)
        Firmware-->>PLDM: PassComponent Response
    end

Updating Components

sequenceDiagram
    title Updating Components

    actor App as Initiator
    participant API as Firmware Update API
    participant Firmware as PLDM Stack - T5
    participant PLDM as Update Agent

    loop for every component
        PLDM->>Firmware: UpdateComponent(component)
        Firmware->>API: UpdateComponent Notification

        alt Caliptra FMC+RT or SoC Manifest
            API->>API: Acquire mailbox lock
        else MCU RT or SoC Image
            API->>API: GET_STAGING_ADDRESS
        end

        API-->>Firmware: Ok
        Firmware-->>PLDM: UpdateComponent Response
    end

Requesting and Transferring Firmware Data

sequenceDiagram
    title Requesting and Transferring Firmware Data

    actor App as Initiator
    participant API as Firmware Update API
    participant Firmware as PLDM Stack - T5
    participant PLDM as Update Agent

    loop until component is downloaded
        Firmware->>PLDM: RequestFirmwareData
        PLDM-->>Firmware: FirmwareData
        Firmware->>API: FirmwareData Notification

        alt SoC Manifest
            API->>API: Stream as SoC_MANIFEST<br/>Mailbox command
        else Caliptra FMC+RT
            API->>API: Stream as CALIPTRA_FW_LOAD<br/>Mailbox Command
        else MCU RT or SoC Image
            API->>API: Write to Staging Area
        end
        API-->>Firmware: Ok
    end

    API-->>API: Release Mailbox Lock (if acquired)

    Firmware-->>PLDM: Transfer Complete

Verifying Components

sequenceDiagram
    title Verifying Components

    actor App as Initiator
    participant API as Firmware Update API
    participant Firmware as PLDM Stack - T5
    participant PLDM as Update Agent

    Firmware->>API: Verify Component Notification

    alt SoC Manifest
        API->>API: Process SET_AUTH_MANIFEST<br/> Mailbox Command Response
    else MCU RT or SoC image
        API->>API: Verify through AUTHORIZE_AND_STASH<br/>Mailbox Command
    end

    API-->>Firmware: Ok
    Firmware-->>PLDM: VerifyComplete

Applying Components

sequenceDiagram
    title Applying Components

    actor App as Initiator
    participant API as Firmware Update API
    participant Firmware as PLDM Stack - T5
    participant PLDM as Update Agent

    Firmware->>API: Apply component Notification
    alt Firmware Update for devices with Flash
        alt SoC Manifest
            API->>API: Write SoC Manifest to Flash Storage
        else MCU RT or SoC Image
            API->>API: Copy image from Staging to Flash Storage
        end
    end

    API->>Firmware: Ok
    Firmware->>PLDM: ApplyComplete

Activating Firmware

sequenceDiagram
    title Activating Firmware

    actor App as Initiator
    participant API as Firmware Update API
    participant Firmware as PLDM Stack - T5
    participant PLDM as Update Agent

    PLDM->>Firmware: ActivateFirmware
    Firmware->>API: Activate Notification
    alt MCU RT or SoC Image
        API->>API: Send Activate Image Mailbox Command
    end
    API-->>Firmware: Ok
    API->>App: UpdateComplete
    Firmware-->>PLDM: Activate Response

Firmware Update Flow

Full Image Update for Flash Boot System

Option 1: Updating the full flash image as a single PLDM firmware component

PLDM update packages natively support selecting applicable components using the ApplicableComponents bitfield in the package header. For the case of Component N + 1, it is treated as a single component by the PLDM Update Agent. This component can encapsulate multiple embedded images each with its corresponding image information entry, checksum and flash header. The structure and layout of Component N + 1 align with the flash layout definition in flash_layout.md.

PLDM FW Update Package
Package Header Information
Firmware Dev ID Descriptors
Downstream Dev ID Descriptors
Component Image Information
Package Header Checksum
Package Payload Checksum
Component 1 (Caliptra FMC + RT)
Component 2 (SoC Manifest)
Component 3 (MCU RT)
Component 4 (SoC Image 1)
...
Component N (SoC Image N-3)
Component N + 1 (Full image for flash-boot system)
Component N + 1 structure
Flash header
Checksum
Image Info (Caliptra FMC + RT)
Image Info (SoC Manifest)
Image Info (MCU RT)
Image Info (SoC Image 1)
...
Image Info (SoC Image N - 3)
Caliptra FMC + RT
SoC Manifest
MCU RT
SoC Image 1
...
SoC Image N - 3

To support full image updates, a SoC-defined staging memory must be provided to store the incoming payload. The designated staging area for Component 1 must be accessible by the Caliptra ROM to fetch and authorize the image. This staging area could be located in MCU SRAM or MCI mailbox SRAM, as defined in Caliptra 2.1. If the SoC-defined staging memory does not meet this requirement, the image must be copied to the compliant region, which may slightly impact performance. For other components, if the staging memory (e.g., a staging partition on flash) is not directly accessible by the Caliptra core's DMA engine for reading and hashing the image, the MCU must perform the cryptographic operations to compute the hash. The computed hash is then sent via a mailbox command for authorization.

Detailed steps: Note: Actions below are performed by MCU RT Firmware

  1. An initiator, such as a custom user application, starts the firmware update service through the Firmware Update API. This action initializes the responder loop in the PLDM stack, enabling it to listen for incoming PLDM messages from the PLDM agent. The API queries firmware component metadata from the Caliptra core (e.g., component version numbers, classifications, etc.) using a mailbox command. This metadata is used to construct the Device Identifiers and Firmware Parameters, as specified in the DMTF DSP0267 1.3.0 standard. (TBD: Confirm if the mailbox command can provide metadata for the full image.)
  2. The PLDM stack notifies the API when a firmware image becomes available for update.
  3. The PLDM stack notifies the API which component is being downloaded using the UpdateComponent notification.
  4. The PLDM stack sends a FirmwareData notification to the API for each received firmware chunk, including the data, size, and chunk offset. The API's download handler writes the received firmware data to the staging memory.
  5. Once all firmware chunks are downloaded, the PLDM stack notifies the API to verify the component. The API processes the component to extract and identify individual embedded images, referred to as subcomponents. The verification process is performed sequentially for each subcomponent: a. For the Caliptra FMC + RT subcomponent, the MCU sends it to the Caliptra core using the CALIPTRA_FW_UPLOAD mailbox command. When this command is executed, Caliptra core firmware is authorized and activated in one shot. The new core image is taken into effect after core reset. b. For the SoC Manifest subcomponent, the MCU sends it to the Caliptra core using the SET_AUTH_MANIFEST mailbox command. The mailbox response confirms the authenticity and correctness of the manifest. c. For MCU RT or SoC Image subcomponents, the MCU sends the AUTHORIZE_AND_STASH mailbox command, indicating that the image to be verified resides in the staging area.
  6. After verification, the PLDM stack notifies the API to apply the image. The MCU writes the images from the temporary staging area to the inactive flash partition. Refer to A/B Partition Mechanism for more details.
  7. When the Update Agent issues the ActivateFirmware command, the API updates the partition table to mark the inactive partition as active. The API may provide a handler to initiate a warm reset, enabling the new image to execute from flash.

Option 2: Updating the full flash image as multiple PLDM firmware components

In this approach, the full flash image is divided into 1 to N distinct firmware components. The ApplicableComponents bitfield in the PLDM package header identifies the selected components, while the component image information provides metadata for each component, including the total number of components. The PLDM Update Agent requests update on each component sequentially, adhering to the order specified in the component image information. Each component is verified and applied by the device. PLDM Update Agent issues ActivateFirmware command to inform the device to prepare all successfully applied components to become active at the next activation.

PLDM FW Update Package
Package Header Information
Firmware Dev ID Descriptors
Downstream Dev ID Descriptors
Component Image Information
Package Header Checksum
Package Payload Checksum
Component 1 (Caliptra FMC + RT)
Component 2 (SoC Manifest)
Component 3 (MCU RT)
Component 4 (SoC Image 1)
...
Component N (SoC Image N-3)
Component N + 1 (Full image for flash-boot system)

Detailed steps:

Note: Actions below are performed by MCU RT Firmware

  1. An initiator, such as a custom user application, starts the firmware update service through the Firmware Update API. This action initializes the responder loop in the PLDM stack, enabling it to listen for incoming PLDM messages from the PLDM agent. The API queries firmware component metadata from the Caliptra core (e.g., component version numbers, classifications, etc.) using a mailbox command. This metadata is used to construct the Device Identifiers and Firmware Parameters, as specified in the DMTF DSP0267 1.3.0 standard. (TBD: Confirm if the mailbox command can provide metadata for the full image.)
  2. The PLDM stack notifies the API when a firmware image becomes available for update.
  3. The PLDM stack notifies the API which component is being downloaded using the UpdateComponent notification. The 1st firmware component received to update should be Caliptra FMC + RT.
  4. The PLDM stack sends a FirmwareData notification to the API for each received firmware chunk, including the data, size, and chunk offset. The API's download handler writes the received firmware data to the staging memory.
  5. Once all firmware chunks are downloaded, the PLDM stack notifies the API to verify the component. a. If the component is Caliptra FMC+RT, MCU sends it to Caliptra core using the CALIPTRA_FW_UPLOAD mailbox command. b. If the component is a SoC Manifest, the mailbox via the SET_AUTH_MANIFEST mailbox command. c. If the component is an MCU RT or SoC Image, it is written to a staging area defined in SoC manifest.
  6. After verification, the PLDM stack notifies the API to apply the image. The MCU writes the images from the temporary staging area to the inactive flash partition. Refer to A/B Partition Mechanism for more details.
  7. Repeat steps 3 through 6 for Component 2, 3, 4 ... N.
  8. After all firmware components have been transferred and applied, Update Agent issues ActivateFirmware command to inform the device to prepare all successfully applied components to become active at the next activation. The API updates the partition table to mark the inactive partition as active. The API may provide a handler to initiate a warm reset, enabling the new image to execute from flash.

A/B Partition Mechanism

The A/B partition mechanism is a robust approach to ensure seamless and reliable firmware updates for flash boot systems. When partition A is active, it contains the currently running firmware, while partition B remains inactive and is used as the target for firmware updates. This ensures that the system can always revert to the previous active partition in case of an update failure.

Partition Layout

The location of A/B partitions can either reside on a single flash device or be distributed across separate flash devices. In a single flash device setup, both partitions share the same physical storage, simplifying design and reducing costs. However, this approach may introduce performance bottlenecks during simultaneous read/write operations and poses a single point of failure. On the other hand, using separate flash devices for A/B partitions enhances redundancy and reliability, allowing parallel operations that improve update performance. This configuration, while more expensive and complex, is ideal for systems requiring high reliability and scalability. The choice between these configurations depends on the specific requirements of the system, such as cost constraints, performance needs, and reliability expectations.

Partition A (Active)Partition B (Inactive)
Flash headerFlash header
ChecksumChecksum
Image Info (Caliptra FMC + RT)Image Info (Caliptra FMC + RT)
Image Info (SoC Manifest)Image Info (SoC Manifest)
Image Info (MCU RT)Image Info (MCU RT)
Image Info (SoC Image 1)Image Info (SoC Image 1)
......
Image Info (SoC Image N - 3)Image Info (SoC Image N - 3)
Caliptra FMC + RTCaliptra FMC + RT
SoC ManifestSoC Manifest
MCU RTMCU RT
SoC Image 1SoC Image 1
......
SoC Image N - 3SoC Image N - 3

Partition Selection

  • Partition Table

For the A/B partition mechanism, the bootloader (MCU ROM) determines which partition to load the firmware image from by using a partition selection mechanism. The implementation of partition selection is system-specific. A common approach involves using a partition table stored in a reserved area of flash. The table below shows an example partition table format:

Field NameSizeDescription
Active Partition1 byteIndicates the active partition (A or B).
Partition A Status1 byteRefer to Partition Status for values
Partition B Status1 byteRefer to Partition Status for values.
Rollback Flag1 byteIndicates if rollback is required.
Reserved4 byteReserved
CheckSum4 byte
  • Partition Status

Bits 7:4: Boot Attempt Count

Bits 3:0:

ValueDescription
0Invalid
1Valid
2Boot Failed
3Boot Successful
  • Partition Table Usage
    • During Normal Boot
      • The MCU ROM reads the partition table to determine:
        • The active partition to boot from.
        • Whether the active partition is valid and bootable.
      • If the active partition is valid, the bootloader loads the firmware image from it and boots the system.
      • If the firmware in the active partition fails to boot (e.g., due to corruption or verification failure), the bootloader:
        • Checks the Rollback Flag.
        • Switches to the other partition if rollback is required.
    • During Firmware Update
      • In the ActivateFirmware phase, the partition table or status flags are updated to mark the inactive partition as the new active partition.
      • Steps to Update:
        1. Set the Active Partition field to the inactive partition (A or B).
        2. Optionally mark the previously active partition as inactive or valid for rollback.
        3. Write the updated partition table or status flags back to the reserved area in non-volatile memory.

Partial firmware update

Below are the supported scenarios:

  1. Caliptra Core Firmware Update

    • Updates the Caliptra FMC + Caliptra RT component. (Component 1 in PLDM package)
  2. MCU Runtime Firmware Update

    • Updates the SoC Manifest and MCU RT firmware together.(Component 2 and Component 3 in PLDM package)
    • The updated SoC Manifest includes the hash entry for the new MCU RT firmware.
  3. SoC Firmware Update

    • Updates the SoC Manifest along with associated SoC images. (Component 2, Component 4.. N in PLDM package)
    • The updated SoC Manifest contains hash entries for the new SoC firmware components.

These scenarios are designed to maintain system integrity and ensure seamless updates for both streaming boot system and flash boot system.

Detailed steps:

Note: Actions below are performed by MCU RT Firmware.

  1. An initiator (such as a custom user application) starts the firmware service through the Firmware Update API. This will start the responder loop in the PLDM stack that will listen for PLDM messages coming from the PLDM agent. The API queries firmware component metadata from the Caliptra core (e.g., component version numbers, classification, etc.) using a mailbox command to construct the Device Identifiers and Firmware Parameters, as defined by the DMTF DSP0267 1.3.0 specification, needed by the PLDM stack.
  2. The PLDM stack notifies the API if a firmware image is available for update.
  3. The PLDM stack notifies the API which component is being downloaded using the UpdateComponent notification. If the image is an MCU RT or SoC Image, the staging address is retrieved from the SoC Manifest stored in the Caliptra Core using a mailbox command. For Caliptra FMC+RT and the SoC Manifest, if it is flash boot system, staging address should be provided. Otherwise,the mailbox lock is acquired since these images are streamed directly through the mailbox interface. The lock is released after all chunks of the image have been transferred.
  4. The PLDM stack sends a FirmwareData notification to the API for each received firmware chunk, including the data, size, and chunk offset.
    1. If the component is a SoC Manifest, it is streamed to the mailbox via the SET_AUTH_MANIFEST mailbox command. If it is flash boot system, it is also written to staging area.
    2. If the component is Caliptra FMC+RT,it is streamed to the Caliptra core using the CALIPTRA_FW_UPLOAD mailbox command. If it is flash boot system, it is also written to staging area.
    3. If the component is an MCU RT or SoC Image, it is written to a staging area determined in step 3.
  5. Once all firmware chunks are downloaded, the PLDM stack notifies the API to verify the component.
    1. If the component is a SoC Manifest, the MCU waits for the SET_AUTH_MANIFEST mailbox command response, which indicates the authenticity and correctness of the manifest.
    2. If the component is an MCU RT or SoC Image, the MCU sends the AUTHORIZE_AND_STASH command, indicating that the image to be verified is in the staging area. Note: The AUTHORIZE_AND_STASH command computes the SHA of the image via the SHA-Acc by streaming the image from the staging area to the SHA-Acc through DMA. The computed SHA is compared against the SHA in the SoC Manifest for the specific image.
  6. After verification, the PLDM stack notifies the API to apply the image. The MCU writes the images to SPI Flash storage from the temporary staging area (if flash is available on the device).
  7. When the Update Agent sends the ActivateFirmware command, the API sends an ActivateImage mailbox command to the Caliptra core. The Caliptra core processes the activation according to the Caliptra specification.

Interfaces

#![allow(unused)]
fn main() {
pub trait FirmwareUpdateApi {

    /// Start the firmware update service.
    ///
    /// # Returns
    /// Returns a future that will remain unset until the service is stopped.
    /// Ok(()) - The service has been terminated successfully.
    /// Err(FirmwareUpdateError) - The service has been terminated with an error.
    async fn start_service(&self) -> Result<(), FirmwareUpdateError>;

    /// Stop the firmware update service.
    ///
    /// # Returns
    /// Ok() - The service has been terminated successfully.
    /// Err(ErrorCode) - The service can not be stopped.
    fn stop_service(&self) -> Result<(), ErrorCode>;

    /// Register a callback to be called when a firmware update event occurs.
    ///
    /// # Arguments
    /// callback - The callback to be called when a firmware update event occurs.
    fn register_callback(&self, callback: FirmwareUpdateCallback);


}

/// Define the callback function signature for firmware update events.
/// Returns Ok(()) if the notification is handled successfully, otherwise an error code.
pub type FirmwareUpdateCallback = fn(FirmwareUpdateNotification) -> Result<(),ErrorCode>;

pub enum FirmwareUpdateNotification<'a>{
    // Firmware Update is available and ready for download.
    UpdateAvailable,

    // Firmware Update is complete.
    UpdateComplete,

    // Firmware Update is canceled.
    UpdateCanceled,

}
}

External Mailbox (MCI Mailbox) Commands Spec

Overview

This document outlines the external mailbox commands that enable SoC agents to interact with the MCU via MCI mailbox. These commands support a wide range of functionalities, including querying device-specific information, retrieving debug and attestation logs, managing certificates, utilizing cryptographic services and secure debugging in production environment.

  • Device Identification and Capabilities

    • Retrieve firmware versions, unique device identifiers, and device capabilities to ensure compatibility and proper configuration.
    • Query device-specific information such as chip identifiers or subsystem details.
  • Debugging and Diagnostics

    • Retrieve debug logs to analyze device behavior, diagnose issues, and monitor runtime states.
    • Clear logs to reset diagnostic data and maintain storage efficiency.
  • Certificate Management

    • Export Certificate Signing Requests (CSRs) for device keys to facilitate secure provisioning.
    • Import signed certificates to establish a trusted certificate chain for device authentication.
  • Cryptographic Services

    • AES encryption and decryption
    • SHA hashing
    • Random number generation
    • Digital signing
    • Signature verification
    • Key exchange
  • Debug Unlock Mechanisms

    • Facilitate secure debugging in production environments
    • Ensure controlled access to debugging features
  • In-Field Fuse Provisioning

Mailbox Commands List

NameCommand CodeDescription
MC_FIRMWARE_VERSION0x4D46_5756 ("MFWV")Retrieves the version of the target firmware.
MC_DEVICE_CAPABILITIES0x4D43_4150 ("MCAP")Retrieve the device capabilities.
MC_DEVICE_ID0x4D44_4944 ("MDID")Retrieves the device ID.
MC_DEVICE_INFO0x4D44_494E ("MDIN")Retrieves information about the target device.
MC_EXPORT_IDEV_CSR0x4D49_4352 ("MICR")Exports the IDEVID Self-Signed Certificate Signing Request.
MC_IMPORT_IDEV_CERT0x4D49_4943 ("MIIC")Allows SoC to import DER-encoded IDevId certificate on every boot.
MC_GET_LOG0x4D47_4C47 ("MGLG")Retrieves the internal log for the RoT.
MC_CLEAR_LOG0x4D43_4C47 ("MCLG")Clears the log in the RoT subsystem.
MC_SHA_INIT0x4D43_5349 ("MCSI")Starts the computation of a SHA hash of data.
MC_SHA_UPDATE0x4D43_5355 ("MCSU")Continues a SHA computation started by MC_SHA_INIT or another MC_SHA_UPDATE.
MC_SHA_FINAL0x4D43_5346 ("MCSF")Finalizes the computation of a SHA and produces the hash of all the data.
MC_HMAC0x4D43_484D ("MCHM")Computes an HMAC according to RFC 2104.
MC_HMAC_KDF_COUNTER0x4D43_4B43 ("MCKC")Computes HMAC KDF in Counter Mode as specified in NIST SP800-108.
MC_HKDF_EXTRACT0x4D43_4B54 ("MCKT")Implements HKDF-Extract as specified in RFC 5869.
MC_HKDF_EXPAND0x4D43_4B50 ("MCKP")Implements HKDF-Expand as specified in RFC 5869.
MC_AES_ENCRYPT_INIT0x4D43_4349 ("MCCI")Starts an AES encryption operation.
MC_AES_ENCRYPT_UPDATE0x4D43_4355 ("MCCU")Continues an AES encryption operation started by MC_AES_ENCRYPT_INIT.
MC_AES_DECRYPT_INIT0x4D43_414A ("MCAJ")Starts an AES-256 decryption operation.
MC_AES_DECRYPT_UPDATE0x4D43_4155 ("MCAU")Continues an AES decryption operation started by MC_AES_DECRYPT_INIT.
MC_AES_GCM_ENCRYPT_INIT0x4D43_4749 ("MCGI")Starts an AES-256-GCM encryption operation.
MC_AES_GCM_ENCRYPT_UPDATE0x4D43_4755 ("MCGU")Continues an AES-GCM encryption operation started by MC_AES_GCM_ENCRYPT_INIT.
MC_AES_GCM_ENCRYPT_FINAL0x4D43_4746 ("MCGF")Finalizes the AES-GCM encryption operation and produces the final ciphertext and tag.
MC_AES_GCM_DECRYPT_INIT0x4D43_4449 ("MCDI")Starts an AES-256-GCM decryption operation.
MC_AES_GCM_DECRYPT_UPDATE0x4D43_4455 ("MCDU")Continues an AES-GCM decryption operation started by MC_AES_GCM_DECRYPT_INIT.
MC_AES_GCM_DECRYPT_FINAL0x4D43_4446 ("MCDF")Finalizes the AES-GCM decryption operation and verifies the tag.
MC_ECDH_GENERATE0x4D43_4547 ("MCEG")Computes the first half of an Elliptic Curve Diffie-Hellman exchange.
MC_ECDH_FINISH0x4D43_4546 ("MCEF")Computes the second half of an Elliptic Curve Diffie-Hellman exchange.
MC_ECDSA_CMK_PUBLIC_KEY0x4D43_4550 ("MCEP")Generates an ECDSA public key from a CMK.
MC_ECDSA_CMK_SIGN0x4D43_4553 ("MCES")Creates an ECDSA signature using a CMK.
MC_ECDSA_CMK_VERIFY0x4D43_4556 ("MCEV")Validates an ECDSA signature using a CMK.
MC_RANDOM_STIR0x4D43_5253 ("MCRS")Adds additional entropy to the internal deterministic random bit generator.
MC_RANDOM_GENERATE0x4D43_5247 ("MCRG")Generates random bytes from the internal RNG.
MC_IMPORT0x4D43_494D ("MCIM")Imports a specified key and returns a CMK for it.
MC_DELETE0x4D43_444C ("MCDL")Deletes the object stored with the given mailbox ID.
MC_ECDSA384_SIG_VERIFY0x4D45_4356 ("MECV")Verifies an ECDSA P-384 signature.
MC_LMS_SIG_VERIFY0x4D4C_4D56 ("MLMV")Verifies an LMS signature.
MC_ECDSA384_SIGN0x4D45_4353 ("MECS")Requests to sign a SHA-384 digest with the DPE leaf certificate.
MC_MLDSA_SIGN0x4D4C_4D53 ("MLMS")Requests to sign a SHA-384 digest with the DPE leaf certificate using MLDSA.
MC_PRODUCTION_DEBUG_UNLOCK_REQ0x4D44_5552 ("MDUR")Requests debug unlock in a production environment.
MC_PRODUCTION_DEBUG_UNLOCK_TOKEN0x4D44_5554 ("MDUT")Sends the debug unlock token.
MC_FUSE_READ0x4946_5052 ("IFPR")See fuses spec for details
MC_FUSE_WRITE0x4946_5057 ("IFPW")See fuses spec for details
MC_FUSE_LOCK_PARTITION0x4946_504B ("IFPK")See fuses spec for details

Command Format

MC_FIRMWARE_VERSION

Retrieves the version of the target firmware.

Command Code: 0x4D46_5756 ("MFWV")

Table: MC_FIRMWARE_VERSION input arguments

NameTypeDescription
chksumu32
indexu32- 00h = Caliptra core firmware
- 01h = MCU runtime firmware
- 02h = SoC firmware
Additional indexes are firmware-specific

Table: MC_FIRMWARE_VERSION output arguments

NameTypeDescription
chksumu32
fips_statusu32FIPS approved or an error
versionu8[32]Firmware Version Number in ASCII format

MC_DEVICE_CAPABILITIES

Retrieve the device capabilites.

Command Code: 0x4D43_4150 ("MCAP")

Table: MC_DEVICE_CAPABILITIES input arguments

NameTypeDescription
chksumu32

Table: MC_DEVICE_CAPABILITIES output arguments

NameTypeDescription
chksumu32
fips_statusu32FIPS approved or an error
capsu8[32]- Bytes [0:7]: Reserved for Caliptra RT
- Bytes [8:11]: Reserved for Caliptra FMC
- Bytes [12:15]: Reserved for Caliptra ROM
- Bytes [16:23]: Reserved for MCU RT
- Bytes [24:27]: Reserved for MCU ROM
- Bytes [28:31]: Reserved

MC_DEVICE_ID

Retrieves the device ID.

Command Code: 0x4D44_4944 ("MDID")

Table: MC_DEVICE_ID input arguments

NameTypeDescription
chksumu32

Table: MC_DEVICE_ID output arguments

NameTypeDescription
chksumu32
fips_statusu32FIPS approved or an error
vendor_idu16Vendor ID; LSB
device_idu16Device ID; LSB
subsystem_vendor_idu16Subsystem Vendor ID; LSB
subsystem_idu16Subsystem ID; LSB

MC_DEVICE_INFO

Retrieves information about the target device.

Command Code: 0x4D44_494E ("MDIN")

Table: MC_DEVICE_INFO input arguments

NameTypeDescription
chksumu32
indexu32Information Index:
- 00h = Unique Chip Identifier
Additional indexes are firmware-specific

Table: MC_DEVICE_INFO output arguments

NameTypeDescription
chksumu32
fips_statusu32FIPS approved or an error
data_sizeu32Size of the requested data in bytes
datau8[data_size]Requested information in binary format

MC_EXPORT_IDEV_CSR

Exports the IDEVID Self-Signed Certificate Signing Request.

Command Code: 0x4D49_4352 ("MICR")

Table: MC_EXPORT_IDEV_CSR input arguments

NameTypeDescription
chksumu32
indexu32Information Index:
- 00h = IDEVID ECC CSR
- 01h = IDEVID MLDSA CSR

Table: MC_EXPORT_IDEV_CSR output arguments

NameTypeDescription
chksumu32
fips_statusu32FIPS approved or an error
data_sizeu32Length in bytes of the valid data in the data field.
datau8[data_size]DER-encoded IDevID certificate signing request.

MC_IMPORT_IDEV_CERT

Allows SoC to import DER-encoded IDevId certificate on every boot. The IDevId certificate is added to the start of the certificate chain.

Command Code: 0x4D49_4943 ("MIIC")

Table: MC_IMPORT_IDEV_CERT input arguments

NameTypeDescription
chksumu32
cert_sizeu32Size of the DER-encoded IDevID certificate.
certu8[1024]DER-encoded IDevID certificate.

Table: MC_IMPORT_IDEV_CERT output arguments

NameTypeDescription
chksumu32
fips_statusu32FIPS approved or an error.

MC_GET_LOG

Retrieves the internal log for the RoT. There are two types of logs available: the Debug Log, which contains RoT application information and machine state, and the Attestation Measurement Log, which is similar to the TCG log.

Command Code: 0x4D47_4C47 ("MGLG")

Table: MC_GET_LOG input arguments

NameTypeDescription
chksumu32Checksum over input data
log typeu32Type of log to retrieve:
- 0 = Debug Log
- 1 = Attestation Log

Table: MC_GET_LOG output arguments

NameTypeDescription
chksumu32
fips_statusu32FIPS approved or an error.
data_sizeu32Size of the log data in bytes
datau8[data_size]Log contents

Debug Log Format:

The debug log reported by the device has no specified format, as this can vary between different devices and is not necessary for attestation. It is expected that diagnostic utilities for the device will be able to understand the exposed log information. A recommended entry format is provided here:

OffsetDescription
1:7Log Entry Header
8:9Format of the entry (e.g., 1 for current format)
10Severity of the entry
11Identifier for the component that generated the message
12Identifier for the entry message
13:16Message-specific argument
17:20Message-specific argument

MC_CLEAR_LOG

Clears the log in the RoT subsystem.

Command Code: 0x4D43_4C47 ("MCLG")

Table: MC_CLEAR_LOG input arguments

NameTypeDescription
chksumu32Checksum over input data
log typeu32Type of log to retrieve:
- 0 = Debug Log
- 1 = Attestation Log

Table: MC_CLEAR_LOG output arguments

NameTypeDescription
chksumu32
fips_statusu32FIPS approved or an error.

MC_ECDSA384_SIG_VERIFY

Verifies an ECDSA P-384 signature. The hash to be verified is taken from the input.

Command Code: 0x4D45_4356 ("MECV")

Table: MC_ECDSA384_SIG_VERIFY input arguments

NameTypeDescription
chksumu32Checksum over other input arguments, computed by the caller. Little endian.
pub_key_xu8[48]X portion of the ECDSA verification key.
pub_key_yu8[48]Y portion of the ECDSA verification key.
signature_ru8[48]R portion of the signature to verify.
signature_su8[48]S portion of the signature to verify.
hashu8[48]SHA-384 digest to verify.

Table: MC_ECDSA384_SIG_VERIFY output arguments

NameTypeDescription
chksumu32Checksum over other output arguments, computed by responder. Little endian.
fips_statusu32Indicates if the command is FIPS approved or an error.

MC_LMS_SIG_VERIFY

Verifies an LMS signature. The hash to be verified is taken from the input.

Command Code: 0x4D4C_4D56 ("MLMV")

Table: MC_LMS_SIG_VERIFY input arguments

NameTypeDescription
chksumu32Checksum over other input arguments, computed by the caller. Little endian.
pub_key_tree_typeu8[4]LMS public key algorithm type. Must equal 12.
pub_key_ots_typeu8[4]LM-OTS algorithm type. Must equal 7.
pub_key_idu8[16]"I" Private key identifier
pub_key_digestu8[24]"T[1]" Public key hash value
signature_qu8[4]Leaf of the Merkle tree where the OTS public key appears
signature_otsu8[1252]LM-OTS signature
signature_tree_typeu8[4]LMS signature Algorithm type. Must equal 12.
signature_tree_pathu8[360]Path through the tree from the leaf associated with the LM-OTS signature to the root
hashu8[48]SHA384 digest to verify.

Table: MC_LMS_SIG_VERIFY output arguments

NameTypeDescription
chksumu32Checksum over other output arguments, computed by MCU. Little endian.
fips_statusu32Indicates if the command is FIPS approved or an error.

MC_ECDSA384_SIGN

Requests to sign SHA-384 digest with DPE leaf cert.

Command Code: 0x4D45_4353 ("MECS")

Table: MC_ECDSA384_SIGN input arguments

NameTypeDescription
chksumu32Checksum over other input arguments, computed by the caller. Little endian.
digestu8[48]SHA-384 digest to be signed.

Table: MC_ECDSA384_SIGN output arguments

NameTypeDescription
chksumu32Checksum over other output arguments, computed by MCU. Little endian.
fips_statusu32Indicates if the command is FIPS approved or an error.
derived_pubkey_xu8[48]The X BigNum of the ECDSA public key associated with the signing key.
derived_pubkey_yu8[48]The Y BigNum of the ECDSA public key associated with the signing key.
signature_ru8[48]The R BigNum of an ECDSA signature.
signature_su8[48]The S BigNum of an ECDSA signature.

MC_MLDSA_SIGN

Request to sign the SHA-384 digest with DPE leaf cert.

Command Code: 0x4D4C_4D53 ("MMLS")

Table: MC_MLDSA_SIGN input arguments

NameTypeDescription
chksumu32Checksum over other input arguments, computed by the caller. Little endian.
digestu8[48]SHA-384 digest to be signed.

Table: MC_MLDSA_SIGN output arguments

NameTypeDescription
chksumu32
fips_statusu32FIPS approved or an error
pub_key_tree_typeu8[4]LMS public key algorithm type.
pub_key_ots_typeu8[4]LM-OTS algorithm type.
pub_key_idu8[16]Private key identifier.
pub_key_digestu8[24]Public key hash value.
signature_qu8[4]Leaf of the Merkle tree for the OTS key.
signature_otsu8[1252]LM-OTS signature.
signature_tree_pathu8[360]Path through the Merkle tree to the root.

MC_PRODUCTION_DEBUG_UNLOCK_REQ

Requests debug unlock in production environment.

Command Code: 0x4D44_5552 ("MDUR")

Table: MC_PRODUCTION_DEBUG_UNLOCK_REQ input arguments

NameTypeDescription
chksumu32
lengthu32Length of the message in DWORDs
unlock_levelu8Debug unlock Level (Number 1-8)
reservedu8[3]Reserved field

Table: MC_PRODUCTION_DEBUG_UNLOCK_REQ output arguments

NameTypeDescription
chksumu32Checksum over other output arguments.
fips_statusu32FIPS approved or an error
lengthu32Length of the message in DWORDs.
unique_device_identifieru8[32]Device identifier of the Caliptra device.
challengeu8[48]Random number challenge.

MC_PRODUCTION_DEBUG_UNLOCK_TOKEN

Sends the debug unlock token.

Command Code: 0x4D44_5554 ("MDUT")

Table: MC_PRODUCTION_DEBUG_UNLOCK_TOKEN input arguments

NameTypeDescription
chksumu32Checksum over other input arguments.
fips_statusu32FIPS approved or an error
lengthu32Length of the message in DWORDs.
unique_device_identifieru8[32]Device identifier of the Caliptra device.
unlock_levelu8Debug unlock level (1-8).
reservedu8[3]Reserved field.
challengeu8[48]Random number challenge.
ecc_public_keyu32[24]ECC public key in hardware format (little endian).
mldsa_public_keyu32[648]MLDSA public key in hardware format (little endian).
ecc_signatureu32[24]ECC P-384 signature of the message hashed using SHA2-384 (R and S coordinates).
mldsa_signatureu32[1157]MLDSA signature of the message hashed using SHA2-512 (4627 bytes + 1 reserved byte).

Table: MC_PRODUCTION_DEBUG_UNLOCK_TOKEN output arguments

NameTypeDescription
chksumu32
fips_statusu32FIPS approved or an error

MC_FUSE_READ

Reads fuse values.

Command Code: 0x4946_5052 ("IFPR")

Table: MC_FUSE_READ input arguments

NameTypeDescription
chksumu32
partitionu32Partition number to read from
entryu32Entry to read

Table: MC_FUSE_READ output arguments

NameTypeDescription
chksumu32
fips_statusu32FIPS approved or an error
length (bits)u32Number of bits that are valid
datau8[...]Fuse data (length/8)

MC_FUSE_WRITE

Write fuse values.

Start bit is counting from the least significant bit.

Command Code: 0x4946_5057 ("IFPW")

Table: MC_FUSE_WRITE input arguments

NameTypeDescription
chksumu32
partitionu32Partition number to write to
entryu32Entry to write
start bitu32Starting bit to write to (least significant bit in entry is 0).
lengthu32in bits
datau8[...]length/8

Table: MC_FUSE_WRITE output arguments

NameTypeDescription
chksumu32
fips_statusu32FIPS approved or an error

Caveats:

  • This command is idempotent, so that identical writes will have no effect.
  • Will fail if any of the existing data is 1 but is set to 0 in the input data. Existing data that is 0 but set to 1 will be burned to a 1.
  • Writes to buffered partitions will not take effect until the next reset.

MC_FUSE_LOCK_PARTITION

Lock a partition.

Command Code: 0x4946_504B ("IFPK")

Table: MC_FUSE_LOCK_PARTITION input arguments

NameTypeDescription
chksumu32
partitionu32Partition number to lock

Table: MC_FUSE_LOCK_PARTITION output arguments

NameTypeDescription
chksumu32
fips_statusu32FIPS approved or an error

Caveats:

  • This command is idempotent, so that locking a partition twice has no effect.
  • Locking a partition causes subsequent writes to it to fail.
  • Locking does not fully take effect until the next reset.

Cryptographic Command Format

The MCI mailbox cryptographic commands are mapped to their corresponding Caliptra Mailbox Cryptographic commands. The mapping is detailed in the table below. For the specific format of each command, refer to the Mailbox Commands: Cryptographic Mailbox (2.0).

Table: mapping MCI Mailbox Crypto Commands to Caliptra Crypto Mailbox Commands

MCI Mailbox Crypto CommandsCaliptra Mailbox Crypto Commands
MC_SHA_INITCM_SHA_INIT
MC_SHA_UPDATECM_SHA_UPDATE
MC_SHA_FINALCM_SHA_FINAL
MC_HMACCM_HMAC
MC_HMAC_KDF_COUNTERCM_HMAC_KDF_COUNTER
MC_HKDF_EXTRACTCM_HKDF_EXTRACT
MC_HKDF_EXPANDCM_HKDF_EXPAND
MC_AES_ENCRYPT_INITCM_AES_ENCRYPT_INIT
MC_AES_ENCRYPT_UPDATECM_AES_ENCRYPT_UPDATE
MC_AES_DECRYPT_INITCM_AES_DECRYPT_INIT
MC_AES_DECRYPT_UPDATECM_AES_DECRYPT_UPDATE
MC_AES_GCM_ENCRYPT_INITCM_AES_GCM_ENCRYPT_INIT
MC_AES_GCM_ENCRYPT_UPDATECM_AES_GCM_ENCRYPT_UPDATE
MC_AES_GCM_ENCRYPT_FINALCM_AES_GCM_ENCRYPT_FINAL
MC_AES_GCM_DECRYPT_INITCM_AES_GCM_DECRYPT_INIT
MC_AES_GCM_DECRYPT_UPDATECM_AES_GCM_DECRYPT_UPDATE
MC_AES_GCM_DECRYPT_FINALCM_AES_GCM_DECRYPT_FINAL
MC_ECDH_GENERATECM_ECDH_GENERATE
MC_ECDH_FINISHCM_ECDH_FINISH
MC_ECDSA_CMK_PUBLIC_KEYCM_ECDSA_PUBLIC_KEY
MC_ECDSA_CMK_SIGNCM_ECDSA_SIGN
MC_ECDSA_CMK_VERIFYCM_ECDSA_VERIFY
MC_RANDOM_STIRCM_RANDOM_STIR
MC_RANDOM_GENERATECM_RANDOM_GENERATE
MC_IMPORTCM_IMPORT
MC_DELETECM_DELETE

External MCTP VDM Commands Spec

Overview

This document specifies the external command protocol used by the Baseboard Management Controller (BMC) to communicate with the device integrating the Caliptra RoT subsystem for querying device-specific information, retrieving debug logs and attestation logs, managing certificates and secure debug unlock etc. The protocol is based on the MCTP (Management Component Transport Protocol) over the I3C interface and uses a vendor-defined message type (0x7E).

  • Device Identification and Capabilities

    • Retrieve firmware versions, unique device identifiers, and device capabilities to ensure compatibility and proper configuration.
    • Query device-specific information such as chip identifiers or subsystem details.
  • Debugging and Diagnostics

    • Retrieve debug logs to analyze device behavior, diagnose issues, and monitor runtime states.
    • Clear logs to reset diagnostic data and maintain storage efficiency.
  • Certificate Management

    • Export Certificate Signing Requests (CSRs) for device keys to facilitate secure provisioning.
    • Import signed certificates to establish a trusted certificate chain for device authentication.
  • Debug Unlock Mechanisms

    • Facilitate secure debugging in production environments
    • Ensure controlled access to debugging features

Protocol

  • Transport Layer: MCTP
  • Message Type: The message type is 0x7E as per the MCTP Base Specification. This message type supports Vendor Defined Messages, where the vendor is identified by the PCI-based Vendor ID. The initial message header is specified in the MCTP Base Specification and detailed below for completeness:
Field NameByte(s)Description
Request Data
PCI/PCIe Vendor ID1:2The MCTP Vendor ID formatted per 00h Vendor ID format offset.
Vendor-Defined Message Body3:NVendor-defined message body, 0 to N bytes.
Response Data
PCI/PCIe Vendor ID1:2The value is formatted per 00h Vendor ID offset.
Vendor-Defined Message Body3:MVendor-defined message body, 0 to M bytes.

The Vendor ID is a 16-bit unsigned integer, described in the PCI 2.3 specification. The value identifies the device manufacturer. The message body and content are described in the sections below.

Message Format

This section describes the MCTP message format used to support Caliptra subsystem external command protocol. The request/response message body encapsulates the Vendor Defined MCTP message within the MCTP transport. Details of MCTP message encapsulation can be found in the MCTP Base Specification. The MCTP Get Vendor Defined Message Support command allows discovery of the vendor-defined messages supported by an endpoint. This discovery process identifies the vendor organization and the supported message types. The format of this request is specified in the MCTP Base Specification.

For the Caliptra external command protocol, the following information is returned in response to the MCTP Get Vendor Defined Message Support request:

  • Vendor ID Format: 0
  • PCI Vendor ID: 0x1414
  • Command Set Version: 4

The following table provides detailed descriptions of the fields used in the Caliptra external command protocol:

Field NameDescription
IC(MCTP Integrity Check bit) Indicates whether the MCTP message is covered by an overall MCTP message payload integrity check.
Message TypeIndicates an MCTP Vendor Defined Message.
MCTP PCI VendorID for PCI Vendor. Caliptra messages use the Microsoft PCI ID of 0x1414.
Request TypeDistinguishes between request and response messages: set to 1 for requests, and 0 for responses.
CryptIndicates whether the Message Payload and Command are encrypted.
Command CodeThe command ID for the operation to execute.
Msg Integrity CheckRepresents the optional presence of a message type-specific integrity check over the contents of the message body. If present (indicated by the IC bit), the Message Integrity Check field is carried in the last bytes of the message body.

The following table describes the MCTP message format used in the Caliptra external command protocol:

Table: MCTP Vendor Defined Message Format Vendor defined message format

The protocol header fields are to be included only in the first packet of a multiple-packet MCTP message. After reconstruction of the message body, the protocol header will be used to interpret the message contents. Reserved fields must be set to 0.

Command List

The following table describes the commands defined under this specification. There are two categories: (1) Required commands (R) that are mandatory for all implementations, (2) Optional commands (O) that may be utilized if the specific implementation requires it.

Message NameCommandR/ODescription
Firmware Version01hRRetrieve firmware version information.
Device Capabilities02hRRetrieve device capabilities.
Device ID03hRRetrieve device ID.
Device Information04hRRetrieve device information.
Export CSR05hRExport CSR for device keys.
Import Certificate06hRImport CA-signed certificate.
Get Certificate State07hRCheck the state of the signed certificate chain.
Get Log08hRRetrieve debug log or attestation log.
Clear Log09hRClear log information.
Request Debug Unlock0AhORequest debug unlock in production environment.
Authorize Debug Unlock Token0BhOSend debug unlock token to device for authorization.

Command Format

This section defines the structure of the Message Payload field, as referenced in the "MCTP Vendor Defined Message Format" table for each command's request and response messages.

Firmware Version

Retrieves the version of the target firmware.

Request Payload:

Byte(s)NameTypeDescription
0:3area_indexu32Area Index:
- 00h = Caliptra core firmware
- 01h = MCU runtime firmware
- 02h = SoC firmware
Additional indexes are firmware-specific

Response Payload:

Byte(s)NameTypeDescription
0:3completion_codeu32Command completion status
4:35versionu8[32]Firmware Version Number in ASCII format

Device Capabilities

Request Payload: Empty

Response Payload:

Byte(s)NameTypeDescription
0:3completion_codeu32Command completion status
4:35capsu8[32]Device Capabilities:
- Bytes [0:7]: Reserved for Caliptra RT
- Bytes [8:11]: Reserved for Caliptra FMC
- Bytes [12:15]: Reserved for Caliptra ROM
- Bytes [16:23]: Reserved for MCU RT
- Bytes [24:27]: Reserved for MCU ROM
- Bytes [28:31]: Reserved

Device ID

This command retrieves the device ID. The request for this command contains no additional payload.

Request Payload: Empty

Response Payload:

Byte(s)NameTypeDescription
0:3completion_codeu32Command completion status
4:5vendor_idu16Vendor ID; LSB
6:7device_idu16Device ID; LSB
8:9subsystem_vendor_idu16Subsystem Vendor ID; LSB
10:11subsystem_idu16Subsystem ID; LSB

Device Information

This command retrieves information about the target device.

Request Payload:

Byte(s)NameTypeDescription
0:3info_indexu32Information Index:
- 00h = Unique Chip Identifier
Additional indexes are firmware-specific

Response Payload:

Byte(s)NameTypeDescription
0:3completion_codeu32Command completion status
4:7data_sizeu32Size of the requested data in bytes
8:Ndatau8[data_size]Requested information in binary format

Export CSR

Exports the IDEVID Self-Signed Certificate Signing Request.

Request Payload:

Byte(s)NameTypeDescription
0:3indexu32Index: Default = 0
- 00h = IDEVID ECC CSR
- 01h = IDEVID MLDSA CSR

Response Payload:

Byte(s)NameTypeDescription
0:3completion_codeu32Command completion status
4:7data_sizeu32Length in bytes of the valid data in the data field
8:Ndatau8[data_size]DER-encoded IDevID certificate signing request

Import Certificate

Allows SoC to import DER-encoded IDevId certificate on every boot. The IDevId certificate is added to the start of the certificate chain.

Request Payload:

Byte(s)NameTypeDescription
0:3cert_sizeu32Size of the DER-encoded IDevID certificate.
4:Ncertu8[cert_size]DER-encoded certificate

Response Payload:

Byte(s)NameTypeDescription
0:3completion_codeu32Command completion status

Get Certificate State

Determines the state of the certificate chain for signed certificates that have been sent to the device. The request for this command contains no additional payload.

Request Payload: Empty

Response Payload:

Byte(s)NameTypeDescription
0:3completion_codeu32Command completion status
4:7stateu32State:
- 0 = A valid chain has been provisioned.
- 1 = A valid chain has not been provisioned.
- 2 = The stored chain is being validated.
8:11error_detailsu32Error details if chain validation has failed.

Get Log

Retrieves the internal log for the RoT. There are two types of logs available: the Debug Log, which contains RoT application information and machine state and the Attestation Measurement Log, which is similar to the TCG log.

Table: log types

Log TypeDescription
0Debug Log
1Attestation Log

Request Payload:

Byte(s)NameTypeDescription
0:3log_typeu32Type of log to retrieve
- 0 = Debug Log
- 1 = Attestation Log

Response Payload:

Byte(s)NameTypeDescription
0:3completion_codeu32Command completion status
4:7data_sizeu32Size of the log data in bytes
8:Ndatau8[data_size]Log contents

The length is determined by the end of the log or the packet size based on device capabilities. If the response spans multiple MCTP messages, the end of the response will be determined by an MCTP message with a payload smaller than the maximum payload supported by both devices. To guarantee a response will never fall exactly on the max payload boundary, the responder must send back an extra packet with zero payload.

Debug Log Format:

The debug log reported by the device has no specified format, as this can vary between different devices and is not necessary for attestation. It is expected that diagnostic utilities for the device will be able to understand the exposed log information. A recommended entry format is provided here:

OffsetDescription
1:7Log Entry Header
8:9Format of the entry (e.g., 1 for current format)
10Severity of the entry
11Identifier for the component that generated the message
12Identifier for the entry message
13:16Message-specific argument
17:20Message-specific argument

Clear Log

Clears the log in the RoT subsystem.

Request Payload:

Byte(s)NameTypeDescription
0:3log_typeu32Log Type:
- 0 = Debug Log
- 1 = Attestation Log

Response Payload:

Byte(s)NameTypeDescription
0:3completion_codeu32Command completion status

Request Debug Unlock

Requests debug unlock in production environment.

Request Payload:

Byte(s)NameTypeDescription
0:3lengthu32Length of the message in DWORDs
4unlock_levelu8Debug unlock level (1-8)
5:7reservedu8[3]Reserved field

Response Payload:

Byte(s)NameTypeDescription
0:3completion_codeu32Command completion status
4:7lengthu32Length of the message in DWORDs
8:39unique_device_identifieru8[32]Device identifier of the Caliptra device
40:87challengeu8[48]Random number challenge

Authorize Debug Unlock Token

Authorizes the debug unlock token.

Request Payload:

Byte(s)NameTypeDescription
0:3lengthu32Length of the message in DWORDs
4:35unique_device_identifieru8[32]Device identifier of the Caliptra device
36unlock_levelu8Debug unlock level (1-8)
37:39reservedu8[3]Reserved field
40:87challengeu8[48]Random number challenge
88:183ecc_public_keyu32[24]ECC public key in hardware format (little endian)
184:2635mldsa_public_keyu32[648]MLDSA public key in hardware format (little endian)
2636:2731ecc_signatureu32[24]ECC P-384 signature of the message hashed using SHA2-384 (R and S coordinates)
2732:6195mldsa_signatureu32[1157]MLDSA signature of the message hashed using SHA2-512 (4627 bytes + 1 reserved byte)

Response Payload:

Byte(s)NameTypeDescription
0:3completion_codeu32Command completion status

Unified Handling of External Commands

The Caliptra MCU firmware provides two external command interfaces: MCTP VDM external commands and MCI Mailbox commands. Although these interfaces operate over different protocols, they deliver overlapping functionality to external clients.

Table: Overlapping commands between MCTP VDM and MCI mailbox
MCTP VDM CommandMCI Mailbox CommandDescription
Firmware VersionMC_FIRMWARE_VERSIONRetrieves the version of the firmware.
Device CapabilitiesMC_DEVICE_CAPABILITIESRetrieves device capabilities.
Device IDMC_DEVICE_IDRetrieves the device ID.
Device InformationMC_DEVICE_INFORetrieves device information.
Export CSRMC_EXPORT_IDEV_CSRExports the IDEVID CSR.
Import CertificateMC_IMPORT_IDEV_CERTImports the IDevID certificate.
Get LogMC_GET_LOGRetrieves the internal log.
Clear LogMC_CLEAR_LOGClears the log in the RoT subsystem.
Request Debug UnlockMC_PRODUCTION_DEBUG_UNLOCK_REQRequests debug unlock in a production environment.
Authorize Debug Unlock TokenMC_PRODUCTION_DEBUG_UNLOCK_TOKENSends the debug unlock token for authorization.

To ensure consistent command behavior and maximize code reuse, we define a protocol-agnostic command handler trait with unified command IDs and input/output types. Both MCTP VDM and MCI mailbox frontends parse their protocol, map to the unified command and call the same backend handler, ensuring code reuse and consistent behavior.

  • Architecture
flowchart TD
    A0[Application]
    %% Protocol-specific entry points
    A1[MCTP VDM Service API]
    A2[MCI Mailbox Service API]

    %% Protocol-specific routers/parsers
    B1[MCTP VDM Parser</br>Encoding/Decoding </br>Router Protocol specific]
    B2[MCI Mailbox Parser and Router Protocol specific]

    %% Protocol-agnostic handler
    C[Common Command Handler
Protocol agnostic]

    %% Device APIs
    D2[Logging API]
    D3[Caliptra Mailbox API]
    D4[Keystore API]

    %% Flow connections
    A0 --> A1
    A0 --> A2
    A1 --> B1
    A2 --> B2

    B1 --> C
    B2 --> C

    C --> D2
    C --> D3
    C --> D4
    C --> ..

    %% Layer grouping
    subgraph Protocol_Specific["Protocol-Specific Layer"]
        B1
        B2
    end

    subgraph Protocol_Agnostic["Protocol-Agnostic Layer"]
        C
    end

    subgraph Device_APIs["Other Service API Layer"]
        D2
        D3
        D4
        ..
    end
  • Interface
/// A trait for handling protocol-agnostic commands asynchronously.
///
/// Implementors of this trait can process commands identified by [`UnifiedCommandId`],
/// using the provided input and output buffers, and return a [`Result`] indicating
/// success or a [`CommandError`].
#[async_trait]
pub trait UnifiedCommandHandler {
    /// Handles a unified command asynchronously.
    ///
    /// # Arguments
    ///
    /// * `command` - The identifier of the command to handle.
    /// * `input` - The input data and protocol information for the command.
    /// * `output` - The output buffer and protocol information for the command response.
    ///
    /// # Returns
    ///
    /// * `Ok(())` if the command was handled successfully.
    /// * `Err(CommandError)` if an error occurred during command handling.
    async fn handle_command<'a>(
        &self,
        command: UnifiedCommandId,
        input: CommandInput<'a>,
        output: CommandOutput<'a>,
    ) -> Result<(), CommandError>;
}

/// Adapter for handling MCTP VDM protocol commands using a UnifiedCommandHandler.
///
/// This struct parses MCTP VDM messages, maps them to unified commands, and delegates
/// handling to the provided UnifiedCommandHandler implementation. The same buffer is
/// used for both request and response payloads.
pub struct MctpVdmAdapter<H: UnifiedCommandHandler + Send + Sync> {
    handler: H,
}

impl<H: UnifiedCommandHandler + Send + Sync> MctpVdmAdapter<H> {
    /// Creates a new MctpVdmAdapter with the given unified command handler.
    pub fn new(handler: H) -> Self {
        Self { handler }
    }

    /// Handles an incoming MCTP VDM message asynchronously.
    ///
    /// Parses the message, maps it to a UnifiedCommandId, and invokes the handler.
    /// The same buffer is used for both request and response.
    /// Returns the response length or an error.
    pub async fn handle_vdm_message<'a>(
        &self,
        buf: &'a mut [u8],
        req_len: usize,
    ) -> Result<usize, CommandError> {
    }
}

/// Identifiers for all supported unified commands.
///
/// Each variant represents a distinct command that can be handled by a [`UnifiedCommandHandler`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum UnifiedCommandId {
    /// Query the firmware version.
    FirmwareVersion,
    /// Query the device capabilities.
    DeviceCapabilities,
    /// Query the device ID.
    DeviceId,
    /// Query device information.
    DeviceInformation,
    /// Export a Certificate Signing Request (CSR).
    ExportCsr,
    /// Import a certificate.
    ImportCertificate,
    /// Query the state of a certificate.
    GetCertificateState,
    /// Retrieve device logs.
    GetLog,
    /// Clear device logs.
    ClearLog,
    // ... add more as needed
}

/// Input data and protocol information for a unified command.
///
/// The input buffer is provided as a byte slice, along with the protocol used.
pub struct CommandInput<'a> {
    /// Input data buffer.
    pub data: &'a [u8],
    /// Protocol used for the command.
    pub protocol: CommandProtocol,
}

/// Output data and protocol information for a unified command.
///
/// The output buffer is provided as a mutable byte slice, along with the protocol used
/// and the length of the valid output data.
pub struct CommandOutput<'a> {
    /// Output data buffer.
    pub data: &'a mut [u8],
    /// Protocol used for the command.
    pub protocol: CommandProtocol,
    /// Length of valid data written to the output buffer.
    pub len: usize,
}

/// Supported protocols for unified commands.
///
/// Indicates the protocol over which the command is received or sent.
#[derive(Debug, Clone, Copy)]
pub enum CommandProtocol {
    /// Management Component Transport Protocol (MCTP).
    Mctp,
    /// Management Controller Interface Mailbox (MCI MBX).
    MciMbx,
}

/// Errors that can occur during unified command handling.
///
/// Used as the error type in [`UnifiedCommandHandler::handle_command`].
#[derive(Debug)]
pub enum CommandError {
    /// The command is not recognized or supported.
    InvalidCommand,
    /// The input data is invalid or malformed.
    InvalidInput,
    /// An internal error occurred during command handling.
    InternalError,
    /// The command is not supported in the current context.
    NotSupported,
    /// The handler is busy and cannot process the command at this time.
    Busy,
    // Protocol-specific errors can be added here
}

Logging Stack

Overview

This document describes the design of the logging stack for Caliptra MCU firmware. The logging stack provides a unified, extensible and robust mechanism for capturing, storing, and retrieving logs from both kernel/driver and userspace components. It supports multiple backends (RAM, flash, etc.), multiple log types and safe concurrent access.

Generic Log Entry Format

Each log entry consists of a fixed-size header followed by variable-length data. The header provides metadata about the entry, while the data contains the log-specific payload.

Table: generic log entry header

Field NameTypeSize (bytes)Description
log_magicu162Start of entry marker
lengthu162Total length of the entry (header + data)
entry_idu324Unique identifier for the log entry

Table: generic log entry

Field NameTypeSize (bytes)Description
log_magicu162Start of entry marker
lengthu162Total length of the entry (header + data)
entry_idu324Unique identifier for the log entry
data[u8]variableLog entry payload

The data field may include additional structured information (such as severity, component, message index, timestamp) depending on the log type.

Reference Format - Debug Log Entry Data

Table: Reference DebugLogEntryData fields

Field NameTypeSize (bytes)Description
formatu162Format of the log entry
severityu81Severity level of the entry (error, info, warning, etc.)
componentu81System component that generated the entry
msg_indexu81Identifier for the entry message
arg1u324Message-specific argument
arg2u324Message-specific argument
cycle_countu648Elapsed processor cycles since boot.
Note: This is a raw cycle count for performance reasons. Converting to milliseconds requires dividing by the clock frequency, which can be done offline or by the log consumer if needed.

All log entries are stored as a contiguous byte array in the backend storage.

Backend Logging Storages

  • Flash Backend

    • Stores log entries in a dedicated flash partition.
    • Ensures persistence across resets and power cycles.
    • Suitable for audit, crash, and security logs.
    • Partitioning is enforced using a flash partition capsule.
  • RAM Backend

    • Implements a circular buffer in RAM.
    • Provides fast access, but is volatile (logs are lost on reset).
    • Primarily used for debug and runtime diagnostics.
  • Multiple Log Types

    • Each log type (e.g., debug, TCG) can use a separate backend instance or partition.
    • The syscall driver and API accept a log type parameter to route requests to the appropriate backend.

Architecture

The logging module leverages the Tock Kernel Log HIL to provide a unified and extensible logging API. It supports multiple storage backends, with initial implementations for both flash-based and RAM-based storage in MCU SDK.

flowchart TD

    A[Userspace App]
    B[Logging API]
    C[Logging Syscall Driver]
    subgraph Capsule Layer
        D1[Log Capsule RAM]
        D2[Log Capsule Flash<br/>capsules/extra/src/log.rs]
    end

    F1[hil::flash]
    F2[hil::log]

    subgraph Log Storage Backend
         G1[Volatile Memory RAM]
         G2[Flash Ctrl]
    end

    %% Show vertical stacking by linking invisible nodes
    A -.-> B
    B -.-> C
    C -.-> D1
    C -.-> D2
    D1 -.-> F2
    D2 -.-> F1
    D2 -.-> F2
    F1 -.-> G2
    D1 -.-> G1

Key Components

  • Logging API: provides a high-level, backend-agnostic interface for application to use logging module.
  • Logging Syscall Driver: bridges userspace and kernel space, exposing logging operations to applications via Tock syscalls.
  • Log Capsules: implement the kernel::hil::log interface for each backend storage type. Tock provides a capsule capsules/extra/src/log.rsfor flash-based log storage, which can be used as-is. To add RAM support, a new capsule implementing the same HIL interface should be created. This modular design enables easy integration of new storage backends by simply implementing the required HIL and capsule, without modifying the core logging logic.
  • Tock Kernel Log HIL: provides the abstraction for logging operations.

Logging API

The logging API is defined by the Logger trait, which provides a uniform interface for log creation, flushing, clearing, size querying, and reading log contents.

#![allow(unused)]
fn main() {
/// Defines the API for logging information.
pub trait Logger {
    /// Add a new entry to the log.
    ///
    /// # Arguments
    /// * `entry` - A byte slice containing the log entry data (including header and payload).
    ///
    /// # Returns
    /// * `Ok(())` if the entry was successfully added.
    /// * `Err(LoggingError)` if the operation failed (e.g., insufficient space).
    fn create_entry(&mut self, entry: &[u8]) -> Result<(), LoggingError>;

    /// Flush the internal buffers to the underlying storage.
    ///
    /// Ensures that any buffered log data is written to persistent storage (if applicable).
    ///
    /// # Returns
    /// * `Ok(())` if the flush was successful.
    /// * `Err(LoggingError)` if the operation failed.
    fn flush(&mut self) -> Result<(), LoggingError>;

    /// Remove all data from the log.
    ///
    /// Clears all log entries from the backend storage.
    ///
    /// # Returns
    /// * `Ok(())` if the log was successfully cleared.
    /// * `Err(LoggingError)` if the operation failed.
    fn clear(&mut self) -> Result<(), LoggingError>;

    /// Get the amount of data currently stored in the log (including headers).
    ///
    /// # Returns
    /// * `Ok(usize)` with the total number of bytes currently stored.
    /// * `Err(LoggingError)` if the operation failed.
    fn get_size(&self) -> Result<usize, LoggingError>;

    /// Get the current contents of the log (raw log data, including headers).
    ///
    /// Copies up to `buffer.len()` bytes of log data starting from `offset` into `buffer`.
    ///
    /// # Arguments
    /// * `offset` - The starting byte offset within the log.
    /// * `buffer` - The buffer to fill with log data.
    ///
    /// # Returns
    /// * `Ok(usize)` with the number of bytes copied into the buffer.
    /// * `Err(LoggingError)` if the operation failed.
    fn read_contents(&self, offset: usize, buffer: &mut [u8]) -> Result<usize, LoggingError>;
}
}

This trait is implemented by all log backends (RAM, flash, etc.) and is used by both kernel/driver code and userspace via the syscall driver.

Syscall Driver

Below is an example skeleton of a syscall driver implementation. This driver translates userspace commands into calls to the appropriate methods of the Logger trait.

#![allow(unused)]

fn main() {
pub const DRIVER_NUM: usize = 0xB0000; // Example driver number

pub struct LogSyscallDriver<'a> {
    logger: &'a dyn Logger,
    grant: Grant<LogGrantData>,
    // Other fields
}

pub struct LogGrantData {
    // Per-process state if needed
}

impl<'a> LogSyscallDriver<'a> {
    pub fn new(logger: &'a dyn Logger, grant: Grant<LogGrantData>) -> Self {
        LogSyscallDriver { logger, grant }
    }
}

impl<'a> SyscallDriver for LogSyscallDriver<'a> {
    fn command(
        &self,
        command_num: usize,
        arg1: usize,
        arg2: usize,
        process: AppId,
    ) -> CommandReturn {
        match command_num {
            0 => { /* create_entry */ }
            1 => { /* flush */ }
            2 => { /* clear */ }
            3 => { /* get_size */ }
            4 => { /* read_contents (not supported in this example) */ }
            _ => { /* unknown command */ }
        }
    }
}
}

Permission Control

Permission control restricts log access to authorized components at both kernel and userspace levels:

  • Kernel/Driver Level: Only trusted kernel capsules and drivers can access log backends. Privileged kernel code instantiates log capsules, which validate caller identity for sensitive operations.

  • Userspace Level: The syscall driver enforces per-process, per-log-type access policies (read-only, write-only, full, or no access) defined in kernel configuration. Each syscall is checked against these policies; unauthorized requests are rejected.

Permission Check Flow:

  1. Userspace issues a syscall.
  2. Driver retrieves process identity and log type.
  3. Permissions are checked.
  4. If allowed, the operation proceeds; otherwise, an error is returned.

This ensures only authorized access and modification of log data.

Kernel/Driver and Userspace Usage

  • Kernel/Driver calls the Logger trait methods directly on the log capsule or syscall driver.
  • Userspace issues syscalls that forwards the requests to corresponding log capsules.

In-Field Fuse Programming (IFP) Specification

MCU has the OpenTitan fuse controller (OTP) and Lifecycle controller (LC), which sit on top of a process-specific fuse macro and manages them with a uniform interface.

The hardware has provisioned the OTP controller with an overall structure that has Caliptra- and general Subsystem-specific blocks, which are required, and vendor-defined blocks.

As part of the Fuse API, we support defining the vendor-specific fuses and assist in provisioning the Caliptra- and vendor-specific fuses.

In general, the Fuse API gives the SoC an easy way to read and write fuses with bit granularity via MCTP or mailbox commands.

Fuse Defining

We provide a new fuse definition hjson file format to assist in further defining fuses.

There are two places where we expand on the standard OTP memory map:

  1. Defining how many bits are backed by actual fuses in each field
  2. Defining vendor-specific fields

For an example of (1), the CPTRA_SS_OWNER_ECC_REVOCATION is specified as 1 byte (8 bits) in the memory map, but the may only have 4 bits backed by actual fuses in hardware. Our additional fuse map contains a section to define how many bits are available in each field as this information is vendor-specific and hence not present in the standard memory map.

For (2), there are two defined partitions, VENDOR_SECRET_PROD_PARTITION (520 bits) and VENDOR_NON_SECRET_PROD_PARTITION (984 bits) in the standard memory map. In addition, the vendor can choose to provide additional fuses. We provide a way to split these up into specific areas and automatically generate Rust code to manage them.

Fuse definition file

Here is an example fuse definition file, for example, vendor_fuses.hjson:

{
  // vendor-specific secret fuses
  secret_vendor: [
    {"example_key1": 48}, // size in bytes
    {"example_key2": 48}, // size in bytes
    {"example_key3": 48}, // size in bytes
    {"example_key4": 48}, // size in bytes
  ],
  // vendor-specific non-secret-fuses
  non_secret_vendor: [
    {"example_key_revocation": 1}
  ],
  // TBD how we allow additional fuses outside of these areas, if this is allowed by OTP
  other_fuses: {},
  // entries to define how many bits are in each field, and potentially other information
  fields: [
    // set specifics on Subsystem fuses
    // By default, all bits in each field are assumed to be backed by actual fuse bits.
    // Names should be globally unique
    {name: "CPTRA_SS_OWNER_ECC_REVOCATION", bits: 4}, // size in bits
    // set specifics on vendor-specific fuses
    {name: "example_key_revocation", bits: 4},
  ]
}

By default, all bits in each field are assumed to be backed by actual fuse bits unless they have an entry in the fields array.

Fuse definition script

We will provide a script, cargo xtask fuse-autogen, that can be used to process an .hjson file into:

  • Firmware definitions and code for the fuses and bits (Rust)
  • Documentation on the fuses and bits (Markdown)

The command will use either a --platform flag to indicate the file is platforms/{platform}/fuse_bits.hjson or a --file to specify the file manually.

This script can optionally generate extra commands for programming specific fuses as well as Rust and C code to access them.

Fuse Provisioning Commands

The fuse provisioning commands can be used over our generic command interface through MCTP, mailboxes, etc., or through commands in firmware manifest (except read operations).

The commands are:

  • Write
  • Read
  • Lock partition

Authorization

Only authorized SoC users shall call these commands. This can be authorized through the mechanism itself (i.e., a mailbox that only a trusted SoC component has access to) or cryptographically.

Some example cryptographic mechanisms that could be used:

  • A certificate provided to MCU during boot, which will be used to validate signatures on each message.
  • An HMAC key imported into Caliptra's cryptographic mailbox during boot, which MCU can use to validate a request. (The key could itself be stored as fuses.)

It will be integrator-defined how which authorization mechanism is required, but we will provide reference implementations with stubs for source-based and HMAC-based authorization.

These commands reference partition, fuse, and bit numbers that must use the same numbers generated from the Fuse definition script.

Limitations

  • Reading is not allowed for secret partitions. The secret partitions can only be read via integrator-specific hardware (or by Caliptra for its secret partitions).
  • Writing and locking are idempotent.
  • Once a partition is locked, it cannot be written to again.
  • u32s are represented in little-endian byte order
  • Bytes are expected to be implemented in fuses in standard (big-endian) bit order, i.e., 76543210.
  • If a byte is only partially filled with bits, then the high-order bits will be 0.
  • Partitions, fields, and bits are specified in the particular platform's otp_mmap.hjson and vendor_fuses.hjson files.

MC_FUSE_READ

Reads fuse values.

Command Code: 0x4946_5052 ("IFPR")

Table: MC_FUSE_READ input arguments

NameTypeDescription
chksumu32
partitionu32Partition number to read from
entryu32Entry to read

Table: MC_FUSE_READ output arguments

NameTypeDescription
chksumu32
fips_statusu32FIPS approved or an error
length (bits)u32Number of bits that are valid
datau8[...]Fuse data (length/8)

MC_FUSE_WRITE

Write fuse values.

Start bit is counting from the least significant bit.

Command Code: 0x4946_5057 ("IFPW")

Table: MC_FUSE_WRITE input arguments

NameTypeDescription
chksumu32
partitionu32Partition number to write to
entryu32Entry to write
start bitu32Starting bit to write to (least significant bit in entry is 0).
lengthu32in bits
datau8[...]length/8

Table: MC_FUSE_WRITE output arguments

NameTypeDescription
chksumu32
fips_statusu32FIPS approved or an error

Caveats:

  • This command is idempotent, so that identical writes will have no effect.
  • Will fail if any of the existing data is 1 but is set to 0 in the input data. Existing data that is 0 but set to 1 will be burned to a 1.
  • Writes to buffered partitions will not take effect until the next reset.

MC_FUSE_LOCK_PARTITION

Lock a partition.

Command Code: 0x4946_504B ("IFPK")

Table: MC_FUSE_LOCK_PARTITION input arguments

NameTypeDescription
chksumu32
partitionu32Partition number to lock

Table: MC_FUSE_LOCK_PARTITION output arguments

NameTypeDescription
chksumu32
fips_statusu32FIPS approved or an error

Caveats:

  • This command is idempotent, so that locking a partition twice has no effect.
  • Locking a partition causes subsequent writes to it to fail.
  • Locking does not fully take effect until the next reset.

Field Entropy Provisioning

Field entropy fuses can only be burned by Caliptra runtime, so the flow for provisioning them is different and any changes in them will invalidate any OCP Device Ownership Transfer blob stored on the device. See the MCU OCP DOT implementation spec.

This will be an MCU mailbox API command (or other signal to MCU runtime) to trigger that MCU should use the Caliptra mailbox PROGRAM_FIELD_ENTROPY command. If a mailbox command is used, it must be authorized in the same way as other fuse programming API commands (see authorization)

If OCP DOT is being used, then the MCU ROM must also update the DOT blob after Caliptra core burns field entropy. Failure to do so may "brick" a device or require a DOT recovery flow, as the ownership information could be corrupted after provisioning field entropy if the DOT root key is derived from the LDevID CDI.

The flow will be:

  1. MCU runtime receives a MCU_PROGRAM_FIELD_ENTROPY command through a mailbox or other signal.
  2. If DOT is enabled and the DOT root key is derived from LDevID, we need to store the DOT blob (and re-sign it after FE is updated):
    1. MCU runtime stores the current DOT blob in non-volatile storage, e.g., SRAM that won't be cleared on reset
    2. MCU runtime verifies this DOT blob using the current DOT root key.
    3. MCU runtime indicates that the DOT blob must be checked and re-derived by setting a register or other signal in non-volatile storage, for example, a generic output wire or SRAM.
    4. Sign the DOT blob copy with a new special DOT root key derived from IDevID.
  3. MCU runtime sends to Caliptra's runtime mailbox the PROGRAM_FIELD_ENTROPY command.
  4. MCU runtime waits for a successful response.
  5. MCU runtime initiates a reset.
  6. If DOT is enabled and the DOT root key is derived from LDevID
    1. MCU ROM checks if we are in a FE programming mode (i.e., from register, input wires, or SRAM contents)
    2. MCU derives both special DOT root key (from IDevID) and the normal DOT root key (from LDevID)
    3. MCU ROM verifies the DOT blob copy in non-volatile storage against the special IDevID DOT root key.
    4. MCU ROM copies the DOT blob copy into the standard DOT blob location
    5. MCU ROM asks Caliptra core signs the DOT blob with the current DOT root key.
    6. Continue with the normal boot flow
sequenceDiagram
  Participant SoC

  Note over MCU,Caliptra: Runtime
  SoC->>MCU: MCU_PROGRAM_FIELD_ENTROPY

  alt DOT enabled and LDevID DOT key used
  MCU->>Caliptra: Derive special DOT key
  MCU->>Caliptra: HMAC DOT blob with special DOT key
  end

  MCU->>Caliptra: PROGRAM_FIELD_ENTROPY
  Caliptra->>OTP: (Burn)
  Caliptra->>MCU: Done
  MCU->>SoC: Reset subsystem

  Note over MCU,Caliptra: ROM
  alt DOT enabled and LDevID DOT key used
  MCU->>Caliptra: Derive DOT normal and special key
  MCU->>Caliptra: HMAC DOT blob with special key
  MCU->>MCU: verify HMAC
  MCU->>Caliptra: HMAC DOT blob with normal key
  end

Fuse Layout Options

The firmware uses a FuseLayout enum to define how fuse values are stored and interpreted. This provides flexibility for different encoding schemes including redundancy and error tolerance. The layout policy can be customized per platform via McuFuseLayoutPolicy.

Note that u32s (dwords) are always packed in little-endian byte order and big-endian bit order.

Layout Types

Single

Values are stored literally without any encoding.

Values stored this way in fuses should be protected with ECC.

Example: A 4-bit value 0b1101 is stored as 0b1101

OneHot

The logical value is the count of bits set to 1 in the fuse field. This is commonly used for version numbers and counters where incrementing requires only burning one additional fuse.

The values can span multiple u32 words but the result is always a single u32 count value.

Example:

  • 0b0000 → 0
  • 0b0111 → 3

Caution: This method of storing fuses is dangerous, since generally ECC cannot be used if the value can be incremented after the initial value is written.

LinearMajorityVote

Each logical bit is duplicated multiple times within a single u32 (or across adjacent u32s). The final value uses majority voting for each bit position to provide error tolerance.

Duplication is limited to < 32x and should be odd.

Example: With 2-bit logical value and 3x duplication:

  • Raw fuses: 0b100_110_111 (bit 0 has votes 111, bit 1 has votes 110, bit 2 has votes 100)
  • Result: 0b011

Generally this method is used for values that do not have ECC protection but are only written once, usually that only are a few bits in size or less.

OneHotLinearMajorityVote

Combines LinearMajorityVote with OneHot encoding. First applies majority voting to each duplicated bit, then counts the number of bits set.

This is the recommended way to

Example: With 3 logical bits and 3x duplication:

  • Raw fuses: 0b100_110_111 (bit 0 has votes 111, bit 1 has votes 110, bit 2 has votes 100)
  • After majority vote on each bit: 0b011 (2 bits set)
  • Result: 2

Generally this method is used for counters and version numbers that can be updated in production and therefore do not have ECC protection.

WordMajorityVote

Entire u32 words are duplicated. Each bit position across the duplicated words is decided by majority vote.

Example: With 3 duplicated words:

  • Raw fuses: [0b100, 0b110, 0b111]
  • Bit 0: votes are 0,0,1 → majority is 0
  • Bit 1: votes are 0,1,1 → majority is 1
  • Bit 2: votes are 1,1,1 → majority is 1
  • Result: [0b110]

This is an alternative to LinearMajorityVote for larger values.

Common Limitations

  • Maximum result size for single value extraction is 32 bits
  • For multi-word extraction, the result array size must match the expected output
  • All layouts return McuError::ROM_FUSE_LAYOUT_TOO_LARGE if constraints are violated
  • Unsupported or invalid configurations return McuError::ROM_UNSUPPORTED_FUSE_LAYOUT
  • Majority vote calculations use ceiling division (e.g., need ≥2 votes for 3 duplicates)

Default Platform Configuration

The default McuFuseLayoutPolicy uses:

  • Single: For certificate data, identifiers, and validity flags that are assumed to be protected by ECC
  • OneHotLinearMajorityVote (3x): For SVN fields and counters
  • LinearMajorityVote (3x): for single revocation fields
  • WordMajorityVote (3x): for revocation bitmasks

This provides a balance between redundancy for critical security fields and storage efficiency for larger data structures.

Firmware Bundler

Overview

The firmware-bundler package is a library for unifying and simplifying the process for building rom and runtime bundles of the subsystem applications for deployment. As such the bundler is composed of 3 primary functionalities:

  1. Generate Linker Scripts - Using a provided Manifest generate linker scripts for the various applications
  2. Execute binary Build - Compile the binaries with rustc and the linker script generated for a binary
  3. Bundle Binaries - Bundle the tockOS runtime applications into a single binary blob for deployment.

The firmware-bundler can either be used directly by platforms within the caliptra-mcu-sw repository, or integrated with a Vendor's Out of Tree repository via an xtask extension to Cargo.

Manifest

The principle mechanism for configuring a run of the firmware-bundler is a manifest toml file describing the applications to build and the platform to deploy them to. By describing the memory layout of the bundle, explicit linker files can be generated and budgets can be checked prior executing the build itself, allowing a single pass architecture (See notes for support for 2 pass builds).

The following is a sample manifest:

# The platform describes where the firmware bundle will be deployed to.  It
# should describe hardware characteristics like memory hierarchy and rustc
# target tuple.
[platform]
# The name of this platform.  This is used for naming various binaries, but does
# not effect the contents of the output bundle.
name = "user-recognizable-name"

# The target tuple to compile this bundle for.  If any rustc configuration
# options are required they should be set for this tuple in the `config.toml`
# file.
tuple = "riscv32imc-unknown-none-elf"

# The alignment each binary's intruction and data blocks should match.  If not
# specified defaults to 8.
default_alignment = 4

# The page size for Tock linker scripts.  If not defined the base page size of
# kernel layout linker script is used.
page_size = 256

# The following sections are used to describe the memory layout of the platform
# under build.  The offset describes where in the space the section starts, and
# size indicates its length.
#
# Both are in bytes and can be specified in either decimal or hexadecimal (with
# 0x prefix).

# The ROM memory.  This is where the ROM binary's instructions will reside.
[platform.rom]
offset = 0x0000_0000
size = 0x2_0000

# The ITCM (or ICCM) memory.  This is where the runtime's instructions will
# reside, both kernel and user space applications.
[platform.itcm]
offset = 0x1000_0000
size = 0x4_0000

# The RAM  memory.  This is where the runtime's data will reside, both kernel
# and user space applications.
[platform.ram]
offset = 0x2000_0000
size = 0x4_0000

# An optional memory for ROM code's data.
[platform.dccm]
offset = 0x3000_0000
size = 0x4000

# An optional memory indicating the location within the memory hieararchy of
# flash.
[platform.flash]
offset = 0x3BFE_0000
size = 0x2_0000

# After the platform descriptions are the binaries to include in the bundled
# artifacts.  The 'name' field in each binary should match a package within the
# Cargo workspace the firmware-bundler is being run in.

# The rom binary is an optional application run at power on.  It allocates its
# instructions from the rom memory block and data from dccm.
[rom]
# The name of the rom binary.  It must match a package within the Cargo
# workspace.
name = "rom"
# The instruction memory required for the binary.
exec_mem = {
  # The amount of memory reserved for this application.  It must fit within the
  # given constraint.
  "size" = 0x2000

  # The alignment the offset of the binary must match. If not provided the
  # default alignment of the platform is used.
  "alignment" = 0x10
}

# The data memory required for this binary.
ram = {
  "size" = 0x4000
}

# The amount of space within RAM to reserve for the stack.  This, in addition
# to the exception_stack must be less than or equal to the RAM size.
stack = 0x2800

# The amount of space within RAM to reserve for the excption stack.  This will
# used in exception handling to avoid any issues with the application stack,
# like stack overflows.  This is an optional field.
exception_stack = 0x800

# The kernel tockOS binary.  It must be specified and contains the same fields
# as the rom.
[kernel]
name = "kernel"
exec_mem = {
  "size" = 0x2_0000
}
ram = {
  "size" = 0x1_0000
}
stack = 0xa000

# After the kernel, an unlimited number of user applications can be specified.
# These will allocate from the ITCM and RAM in the order they are specified.
[[app]]
name = "user-app-1"
exec_mem = {
  "size" = 0x2_0000
}
ram = {
  "size" = 0x1_0000
}
stack = 0xa000

[[app]]
name = "user-app-2"
exec_mem = {
  "size" = 0x1_0000
}
ram = {
  "size" = 0x3_0000
}
stack = 0x2_0000

Integration

It is intended for mcu-firmware-bundler to be integrated as an xtask command. xtask is a paradigm for extending Cargo to support additional build commands as required by a project. Once you've integrated xtask, you should be able to add the mcu-firmware-bundler::Command as a separate command within your xtask match logic. This will allow a user to invoke the firmware bundler commands from the cli, and easily produce bundled binaries using the same technique used to build and test rust repositories normally.

E.g.

cargo xtask <firmware-bundler-command> bundle path/to/manifest_file.toml

Build outputs from the mcu-firmware-bundler are placed within the target/<target-tuple> directory for the Cargo workspace. Linker scripts are placed within the linker-scripts directory while binary outputs are placed in the release directory.

To get a full understanding of the firmware bundler commands and their various options, invoke the help command, which provides both the arguments and helpful documentation.

cargo xtask <firmware-bundler-command> --help

For an example of integration, take a look at the caliptra-mcu-sw integration of the firmware bundler in caliptra-mcu-sw/xtask/src/main.rs

Caliptra Subsystem Provisioning Guide

Goal / Disclaimer

This document aims to provide Caliptra Subsystem (SS) silicon vendors/manufacturers with guidance on how to provision devices during manufacturing. Nothing in this document is a requirement for Caliptra Trademark compliance. Note, this document focuses on the technical and operational aspects of provisioning a device with Caliptra Subsystem. Details on how to secure provisioning infrastructure to meet various certification requirements are outside the scope of this document.

Assets

There are two categories of assets that must be provisioned into a product that integrates Caliptra Subsystem before the device becomes fully operational:

  1. fuses
  2. IDevID (UDS) certificate

Fuses

Caliptra SS contains a One-Time Programmable (OTP) fuse memory bank that must be provisioned during SoC manufacturing. The fuse bank contains several logical fields organized across a set of partitions. Additionally, Caliptra SS contains a fuse-backed lifecycle FSM (known as the lifecycle controller), whose states gate debug capabilities of a device, thus requiring specific fuse fields be programmed in specific life-cycle states. The table below describes the fuse map and the lifecycle states they should be provisioned by.

Certificates

When requested, the IDevID CSR is generated by the Caliptra Core ROM after receiving the UDS Seed from the Caliptra Subsystem fuse controller. It should be endorsed by an external CA protected via an HSM on an isolated manufacturing network1.

There are two different IDevID certificates to be generated, based on the cryptographic keys they are constructed for:

  1. ECC secp384r1
  2. ML-DSA-87

The signature sizes for each are shown in the following table. These signatures should be stored in the larger SoC’s nonvolatile storage or other accessible service.

Certificate typeSignature size (Bytes)
ECC Secp384r196
ML-DSA-874627

To be able to reconstruct the IDevID certificate on boot, Caliptra also requires the original certificate attributes be stored in the SoC's fuses. The size and encoding of this data is fixed for both certificate types (ECC and ML-DSA), and detailed in the Caliptra specification.

Provisioning Sequence

The Caliptra Subsystem provisioning sequence is described below. The provisioning sequence is designed to suit various requirements among downstream integrators, including:

  1. providing an ATE-friendly provisioning sequence where DUT operations can be performed: a. directly over JTAG, and/or b. by loading custom MCU firmware into SRAM (via JTAG).
  2. The ability to program different fuse partitions at various lifecycle stages.

In addition to the provisioning flow description, the Caliptra project aims to provide a general reference provisioning flow, and supporting tooling / firmware, upstream. These are currently under development, and tied to a caliptra-mcu-sw GitHub milestone.

Before describing the sequence, we provide some background information on two important hardware blocks that facilitate the provisioning sequence, namely:

  1. lifecycle controller, and
  2. fuse controller.

Background: Lifecycle Controller Architecture

Caliptra Subsystem provides a hardware backed lifecycle FSM (known as the lifecycle controller) that transitions through several lifecycle states shown in the diagram below:

Caliptra Subsystem LC Architecture

States only move forward and persist across chip resets; once advanced, it cannot revert to a previous state as lifecycle states are encoded in fuses. Transitions between some states require the use of password-like tokens. All tokens are provisioned in fuses, except the "Raw Unlock Token", which is a netlist constant. The purpose of each lifecycle state is described below. Additionally below is a table that summarizes which states allow debug (i.e. JTAG and DFT) access to various Caliptra Subsystem and SoC components.

Lifecycle StateJTAG AccessDFT Access
RawLCCNone
TestUnlocked[0–7]LCC, CLTAP*, MCU, CoreSS, SoC
TestLocked[0–7]LCCNone
ManufLCC, CLTAP*None
Manuf Debug UnlockLCC, CLTAP*, MCU, CoreSoC*
ProdLCCNone
Prod Debug UnlockLCC, CLTAP*, MCU, CoreSoC*
ProdEndLCCNone
RMALCC, CLTAP*, MCU, CoreSS, SoC
ScrapLCCNone

* Integration Configurable
CLTAP = Chip Level TAP

TestUnlocked[0–7] states are used for initial testing to ensure chip health. All lifecycle tokens in the SECRET_LC_TRANSITION_PARTITION fuse partition should be provisioned in TestUnlocked0 to allow further actuation of the lifecycle controller during manufacturing. After testing, the chip is locked by entering a TestLocked[0–7] state to close debug access.

Using a provisioned token, a chip can transition from any of the TestUnlock[0–7] / TestLocked[0–7] states to the Manuf(acturing) state. In this state, debug access is once again opened, and the UDS and other manufacturing-specific fuses are injected. All production debug unlock tokens for the production state must be provisioned in this state, and the partition locked.

After manufacturing, the chip enters a production state (Prod or ProdEnd). Additional fuse provisioning (e.g., field entropy, revocation bits) may occur here, but only for fuse partitions not already locked in manufacturing. Entry into debug mode in production uses signature authentication with public keys, supporting multiple debug levels (up to 8), each with different privileges.

Production End & RMA marks the end of the chip’s functional lifecycle. The chip is no longer considered part of the active fleet and is not used for regular operations. Additionally, from any lifecycle state, the chip may transition (unconditionally without token) to the Scrap state, where fuse zeroization is performed, in accordance with FIPS requirements, to securely erase all secret assets stored in fuses.

Background: Fuse Controller

As mentioned above, Caliptra Subsystem contains a One-Time Programmable (OTP) fuse memory bank that is controlled by a hardware block known as the fuse_ctrl (or fuse controller). The OTP memory bank is split into partitions, and each partition:

  1. contains a digest field over the partition, and
  2. can be write-locked by writing to the digest field.

The locking mechanism prevents further changes to certain partitions (e.g., UDS). Once locked, these fuses cannot be overwritten for the lifetime of the device, with the exception of zeroizable partitions which may be cleared once a device is decommissioned. While the fuse memory map may be expanded for a particular integration, see the integration guide for more details, a baseline memory map is provided upstream.

Provisioning Sequence

Below is a summary of the provisioning flow required to bring a Caliptra Subsystem device from a raw lifecycle state to a production lifecycle state. This flow assumes all devices in a raw state have all fuses in their, unprogrammed, initial state, i.e., all bits set to zeros. Additionally, while this flow may be performed at a manufacturing stage of the integrator's choosing, we describe the following operations from the perspective of a tester, as if the operations were performed entirely at a Final Test (FT) silicon manufacturing stage with the aid of an Automated Test Equipment (ATE).

Caliptra Provisioning Flow

  1. Tester drives a lifecycle transition via JTAG: Raw → TestUnlocked0
  2. Perform wafer sort testing.
  3. Tester programs the following fuses (via JTAG or by loading MCU firmware loaded to SRAM):
    1. vendor test
    2. Lifecycle tokens
    3. Manuf debug unlock token
  4. Perform more wafer sort testing.
  5. Tester drives a lifecycle transition via JTAG: TestUnlocked* → Manuf
  6. Tester programs remaining fuses:
    1. Secure Boot Key hashes / types
    2. UDS fuses via programming sequence with Caliptra Core API
  7. Tester requests Caliptra Core to generate HMAC endorsed IDevID CSR and extract from DUT.
  8. Tester forward CSR and HMAC to HSM for authentication and endorsement.
  9. HSM validates HMAC and endorses IDevID TBS certificate.
  10. [Optional] Tester program IDevID certificate into integration-specific non-volatile storage.
  11. Tester drives a lifecycle transition via JTAG: Manuf → Prod[End]

Appendix

Lifecycle State Constraints for Provisioning Fuses

The fuse partition below corresponds to the Caliptra Subsystem 2.1.1 reference fuse map.

PartitionItemSize [B]Lifecycle State
SW_TEST_UNLOCK_PARTITIONCPTRA_SS_MANUF_DEBUG_UNLOCK_TOKEN64TEST_UNLOCKED
SECRET_MANUF_PARTITIONCPTRA_CORE_UDS_SEED64MANUF
SECRET_PROD_PARTITION_0CPTRA_CORE_FIELD_ENTROPY_08In-Field
SECRET_PROD_PARTITION_1CPTRA_CORE_FIELD_ENTROPY_18In-Field
SECRET_PROD_PARTITION_2CPTRA_CORE_FIELD_ENTROPY_28In-Field
SECRET_PROD_PARTITION_3CPTRA_CORE_FIELD_ENTROPY_38In-Field
SW_MANUF_PARTITIONCPTRA_CORE_ANTI_ROLLBACK_DISABLE4MANUF
CPTRA_CORE_IDEVID_CERT_IDEVID_ATTR96MANUF
SOC_SPECIFIC_IDEVID_CERTIFICATE4MANUF
CPTRA_CORE_IDEVID_MANUF_HSM_IDENTIFIER16MANUF
CPTRA_CORE_SOC_STEPPING_ID4MANUF
CPTRA_SS_PROD_DEBUG_UNLOCK_PKS_048MANUF
CPTRA_SS_PROD_DEBUG_UNLOCK_PKS_148MANUF
CPTRA_SS_PROD_DEBUG_UNLOCK_PKS_248MANUF
CPTRA_SS_PROD_DEBUG_UNLOCK_PKS_348MANUF
CPTRA_SS_PROD_DEBUG_UNLOCK_PKS_448MANUF
CPTRA_SS_PROD_DEBUG_UNLOCK_PKS_548MANUF
CPTRA_SS_PROD_DEBUG_UNLOCK_PKS_648MANUF
CPTRA_SS_PROD_DEBUG_UNLOCK_PKS_748MANUF
SECRET_LC_TRANSITION_PARTITIONCPTRA_SS_TEST_UNLOCK_TOKEN_116TEST_UNLOCKED
CPTRA_SS_TEST_UNLOCK_TOKEN_216TEST_UNLOCKED
CPTRA_SS_TEST_UNLOCK_TOKEN_316TEST_UNLOCKED
CPTRA_SS_TEST_UNLOCK_TOKEN_416TEST_UNLOCKED
CPTRA_SS_TEST_UNLOCK_TOKEN_516TEST_UNLOCKED
CPTRA_SS_TEST_UNLOCK_TOKEN_616TEST_UNLOCKED
CPTRA_SS_TEST_UNLOCK_TOKEN_716TEST_UNLOCKED
CPTRA_SS_TEST_EXIT_TO_MANUF_TOKEN16TEST_UNLOCKED
CPTRA_SS_MANUF_TO_PROD_TOKEN16TEST_UNLOCKED
CPTRA_SS_PROD_TO_PROD_END_TOKEN16TEST_UNLOCKED
CPTRA_SS_RMA_TOKEN16TEST_UNLOCKED
SVN_PARTITIONCPTRA_CORE_FMC_KEY_MANIFEST_SVN4In-Field
CPTRA_CORE_RUNTIME_SVN16In-Field
CPTRA_CORE_SOC_MANIFEST_SVN16In-Field
CPTRA_CORE_SOC_MANIFEST_MAX_SVN4In-Field
VENDOR_TEST_PARTITIONVENDOR_TEST32TEST_UNLOCKED
VENDOR_HASHES_MANUF_PARTITIONCPTRA_CORE_VENDOR_PK_HASH_048MANUF
CPTRA_CORE_PQC_KEY_TYPE_04MANUF
VENDOR_HASHES_PROD_PARTITIONCPTRA_SS_OWNER_PK_HASH48MANUF
CPTRA_SS_OWNER_PQC_KEY_TYPE4MANUF
CPTRA_SS_OWNER_PK_HASH_VALID4MANUF
CPTRA_CORE_VENDOR_PK_HASH_148MANUF
CPTRA_CORE_PQC_KEY_TYPE_14MANUF
CPTRA_CORE_VENDOR_PK_HASH_248MANUF
CPTRA_CORE_PQC_KEY_TYPE_24MANUF
CPTRA_CORE_VENDOR_PK_HASH_348MANUF
CPTRA_CORE_PQC_KEY_TYPE_34MANUF
CPTRA_CORE_VENDOR_PK_HASH_448MANUF
CPTRA_CORE_PQC_KEY_TYPE_44MANUF
CPTRA_CORE_VENDOR_PK_HASH_548MANUF
CPTRA_CORE_PQC_KEY_TYPE_54MANUF
CPTRA_CORE_VENDOR_PK_HASH_648MANUF
CPTRA_CORE_PQC_KEY_TYPE_64MANUF
CPTRA_CORE_VENDOR_PK_HASH_748MANUF
CPTRA_CORE_PQC_KEY_TYPE_74MANUF
CPTRA_CORE_VENDOR_PK_HASH_848MANUF
CPTRA_CORE_PQC_KEY_TYPE_84MANUF
CPTRA_CORE_VENDOR_PK_HASH_948MANUF
CPTRA_CORE_PQC_KEY_TYPE_94MANUF
CPTRA_CORE_VENDOR_PK_HASH_1048MANUF
CPTRA_CORE_PQC_KEY_TYPE_104MANUF
CPTRA_CORE_VENDOR_PK_HASH_1148MANUF
CPTRA_CORE_PQC_KEY_TYPE_114MANUF
CPTRA_CORE_VENDOR_PK_HASH_1248MANUF
CPTRA_CORE_PQC_KEY_TYPE_124MANUF
CPTRA_CORE_VENDOR_PK_HASH_1348MANUF
CPTRA_CORE_PQC_KEY_TYPE_134MANUF
CPTRA_CORE_VENDOR_PK_HASH_1448MANUF
CPTRA_CORE_PQC_KEY_TYPE_144MANUF
CPTRA_CORE_VENDOR_PK_HASH_1548MANUF
CPTRA_CORE_PQC_KEY_TYPE_154MANUF
CPTRA_CORE_VENDOR_PK_HASH_VALID16MANUF
VENDOR_REVOCATIONS_PROD_PARTITIONCPTRA_SS_OWNER_ECC_REVOCATION4In-Field
CPTRA_SS_OWNER_LMS_REVOCATION4In-Field
CPTRA_SS_OWNER_MLDSA_REVOCATION4In-Field
CPTRA_CORE_ECC_REVOCATION_04In-Field
CPTRA_CORE_LMS_REVOCATION_04In-Field
CPTRA_CORE_MLDSA_REVOCATION_04In-Field
CPTRA_CORE_ECC_REVOCATION_14In-Field
CPTRA_CORE_LMS_REVOCATION_14In-Field
CPTRA_CORE_MLDSA_REVOCATION_14In-Field
CPTRA_CORE_ECC_REVOCATION_24In-Field
CPTRA_CORE_LMS_REVOCATION_24In-Field
CPTRA_CORE_MLDSA_REVOCATION_24In-Field
CPTRA_CORE_ECC_REVOCATION_34In-Field
CPTRA_CORE_LMS_REVOCATION_34In-Field
CPTRA_CORE_MLDSA_REVOCATION_34In-Field
CPTRA_CORE_ECC_REVOCATION_44In-Field
CPTRA_CORE_LMS_REVOCATION_44In-Field
CPTRA_CORE_MLDSA_REVOCATION_44In-Field
CPTRA_CORE_ECC_REVOCATION_54In-Field
CPTRA_CORE_LMS_REVOCATION_54In-Field
CPTRA_CORE_MLDSA_REVOCATION_54In-Field
CPTRA_CORE_ECC_REVOCATION_64In-Field
CPTRA_CORE_LMS_REVOCATION_64In-Field
CPTRA_CORE_MLDSA_REVOCATION_64In-Field
CPTRA_CORE_ECC_REVOCATION_74In-Field
CPTRA_CORE_LMS_REVOCATION_74In-Field
CPTRA_CORE_MLDSA_REVOCATION_74In-Field
CPTRA_CORE_ECC_REVOCATION_84In-Field
CPTRA_CORE_LMS_REVOCATION_84In-Field
CPTRA_CORE_MLDSA_REVOCATION_84In-Field
CPTRA_CORE_ECC_REVOCATION_94In-Field
CPTRA_CORE_LMS_REVOCATION_94In-Field
CPTRA_CORE_MLDSA_REVOCATION_94In-Field
CPTRA_CORE_ECC_REVOCATION_104In-Field
CPTRA_CORE_LMS_REVOCATION_104In-Field
CPTRA_CORE_MLDSA_REVOCATION_104In-Field
CPTRA_CORE_ECC_REVOCATION_114In-Field
CPTRA_CORE_LMS_REVOCATION_114In-Field
CPTRA_CORE_MLDSA_REVOCATION_114In-Field
CPTRA_CORE_ECC_REVOCATION_124In-Field
CPTRA_CORE_LMS_REVOCATION_124In-Field
CPTRA_CORE_MLDSA_REVOCATION_124In-Field
CPTRA_CORE_ECC_REVOCATION_134In-Field
CPTRA_CORE_LMS_REVOCATION_134In-Field
CPTRA_CORE_MLDSA_REVOCATION_134In-Field
CPTRA_CORE_ECC_REVOCATION_144In-Field
CPTRA_CORE_LMS_REVOCATION_144In-Field
CPTRA_CORE_MLDSA_REVOCATION_144In-Field
CPTRA_CORE_ECC_REVOCATION_154In-Field
CPTRA_CORE_LMS_REVOCATION_154In-Field
CPTRA_CORE_MLDSA_REVOCATION_154In-Field
VENDOR_SECRET_PROD_PARTITIONCPTRA_SS_VENDOR_SPECIFIC_SECRET_FUSE_032PROD
CPTRA_SS_VENDOR_SPECIFIC_SECRET_FUSE_132PROD
CPTRA_SS_VENDOR_SPECIFIC_SECRET_FUSE_232PROD
CPTRA_SS_VENDOR_SPECIFIC_SECRET_FUSE_332PROD
CPTRA_SS_VENDOR_SPECIFIC_SECRET_FUSE_432PROD
CPTRA_SS_VENDOR_SPECIFIC_SECRET_FUSE_532PROD
CPTRA_SS_VENDOR_SPECIFIC_SECRET_FUSE_632PROD
CPTRA_SS_VENDOR_SPECIFIC_SECRET_FUSE_732PROD
CPTRA_SS_VENDOR_SPECIFIC_SECRET_FUSE_832PROD
CPTRA_SS_VENDOR_SPECIFIC_SECRET_FUSE_932PROD
CPTRA_SS_VENDOR_SPECIFIC_SECRET_FUSE_1032PROD
CPTRA_SS_VENDOR_SPECIFIC_SECRET_FUSE_1132PROD
CPTRA_SS_VENDOR_SPECIFIC_SECRET_FUSE_1232PROD
CPTRA_SS_VENDOR_SPECIFIC_SECRET_FUSE_1332PROD
CPTRA_SS_VENDOR_SPECIFIC_SECRET_FUSE_1432PROD
CPTRA_SS_VENDOR_SPECIFIC_SECRET_FUSE_1532PROD
VENDOR_NON_SECRET_PROD_PARTITIONCPTRA_SS_VENDOR_SPECIFIC_NON_SECRET_FUSE_032PROD
CPTRA_SS_VENDOR_SPECIFIC_NON_SECRET_FUSE_132PROD
CPTRA_SS_VENDOR_SPECIFIC_NON_SECRET_FUSE_232PROD
CPTRA_SS_VENDOR_SPECIFIC_NON_SECRET_FUSE_332PROD
CPTRA_SS_VENDOR_SPECIFIC_NON_SECRET_FUSE_432PROD
CPTRA_SS_VENDOR_SPECIFIC_NON_SECRET_FUSE_532PROD
CPTRA_SS_VENDOR_SPECIFIC_NON_SECRET_FUSE_632PROD
CPTRA_SS_VENDOR_SPECIFIC_NON_SECRET_FUSE_732PROD
CPTRA_SS_VENDOR_SPECIFIC_NON_SECRET_FUSE_832PROD
CPTRA_SS_VENDOR_SPECIFIC_NON_SECRET_FUSE_932PROD
CPTRA_SS_VENDOR_SPECIFIC_NON_SECRET_FUSE_1032PROD
CPTRA_SS_VENDOR_SPECIFIC_NON_SECRET_FUSE_1132PROD
CPTRA_SS_VENDOR_SPECIFIC_NON_SECRET_FUSE_1232PROD
CPTRA_SS_VENDOR_SPECIFIC_NON_SECRET_FUSE_1332PROD
CPTRA_SS_VENDOR_SPECIFIC_NON_SECRET_FUSE_1432PROD
CPTRA_SS_VENDOR_SPECIFIC_NON_SECRET_FUSE_1532PROD
CPTRA_SS_LOCK_HEK_PROD_0CPTRA_SS_LOCK_HEK_PROD_0_RATCHET_SEED48PROD
CPTRA_SS_LOCK_HEK_PROD_1CPTRA_SS_LOCK_HEK_PROD_1_RATCHET_SEED48PROD
CPTRA_SS_LOCK_HEK_PROD_2CPTRA_SS_LOCK_HEK_PROD_2_RATCHET_SEED48PROD
CPTRA_SS_LOCK_HEK_PROD_3CPTRA_SS_LOCK_HEK_PROD_3_RATCHET_SEED48PROD
CPTRA_SS_LOCK_HEK_PROD_4CPTRA_SS_LOCK_HEK_PROD_4_RATCHET_SEED48PROD
CPTRA_SS_LOCK_HEK_PROD_5CPTRA_SS_LOCK_HEK_PROD_5_RATCHET_SEED48PROD
CPTRA_SS_LOCK_HEK_PROD_6CPTRA_SS_LOCK_HEK_PROD_6_RATCHET_SEED48PROD
CPTRA_SS_LOCK_HEK_PROD_7CPTRA_SS_LOCK_HEK_PROD_7_RATCHET_SEED48PROD

Glossary

The following acronyms and abbreviations are used throughout this document.

ATEAutomated Test Equipment
ODMOriginal Design Manufacturer
OSATOutsourced Semiconductor Assembly and Test
FTFinal Test
SLTSystem Level Test
UDSUnique Device Secret
CSRCertificate Signing Request

  1. Network isolation requirements are often dictated by certification requirements, while are outside the scope of this document.

Working with an FPGA

This section covers how to perform common MCU flows on an FPGA. You can always fallback to working on the FPGA directly, as it is an ARM Linux host, albeit low powered.

Pre Requisites

Host System

The machine that is used for development and cross compilation should have:

  • Rust
  • Docker
    • Make sure you pull the FPGA build image: $ docker pull ghcr.io/chipsalliance/caliptra-build-image:latest
  • rsync
  • git

NOTE: Setup rootless docker. Here are the steps here.

FPGA System

The FPGA should have the following installed (If you are not using a Caliptra image):

  • rsync
  • git
  • make
  • gcc
  • cargo-nextest
    • cargo-nextest should be useable by the root user. Consider installing it to the system path /usr/bin.

Suggestion: Download the latest FPGA Image from the Caliptra-SW FPGA Image build job. This ensures you are testing with the same system used in the FPGA CI. Developer images have a "-dev" suffix.

Suggested Development Flow

Prefer to develop on your main machine and use xtask to test your changes on the FPGA via ssh.

Setup SSH config

xtask will access the FPGA over SSH. You will need an SSH config to define how to do this.

This config should be added to ~/.ssh/config.

Host <FPGA-NAME> # Update me!
  Hostname <FPGA-IP-ADDRESS> # Update me!
  User ubuntu # Use "root" on CI image.

Developing over SSH

Most xtask-fpga fpga commands support running over SSH. This is the preferred and tested path. The SSH target is passed with the --target-host flag.

The xtask tool will cross-compile and then copy binaries to the FPGA over the SSH connection.

Bitstream

Caliptra Images

The Caliptra images use a segmented bitstream. The bitstream is automatically loaded by the bootstrap command, based on the FPGA configuration type.

NOTE: The core & subsystem base images are incompatible, and you must be sure to use the correct base image for the configuration you are using.

For example:

  • core-on-subsystem => Subsystem base image
  • subsystem => Subsystem base image
  • core => Core base image

Bitstream source

The subsystem bitstream is pulled from the subsystem.toml. The core bitstream is pulled from "core.toml" from the active caliptra-sw folder.

Ubuntu Image

Follow the instructions in hw/fpga/README.md on building and loading a bitstream.

FPGA bootstrap

The FPGA needs to be bootstrapped each time it is booted. This ensures that the kernel modules we use in this repo are present.

Run cargo xtask-fpga fpga bootstrap --target-host $SSH-FPGA-NAME to bootstrap the FPGA.

Configurations

The xtask flows supports three different configurations:

  • "Subsystem": This mode configures the FPGA to run Caliptra MCU flows.
  • "Core-on-Subsystem": This mode configures the FPGA to run Caliptra-SW flows for a subsystem Caliptra.
  • "Core": This mode allows running Caliptra in Core mode on the FPGA.
    • Note: You will need to override caliptra-sw to point to a local directory in the workspace Cargo.toml.

For example:

$ cargo xtask-fpga fpga bootstrap # The default configration is subsystem
$ cargo xtask-fpga fpga bootstrap --configuration subsystem
$ cargo xtask-fpga fpga bootstrap --configuration core-on-subsystem
$ cargo xtask-fpga fpga bootstrap --configuration core

Cross compiling

Firmware

Run cargo xtask-fpga fpga build --target-host $SSH-FPGA-NAME to create a firmware bundle. Using the --target-host flag will automatically copy the firmware to the FPGA host.

This command should be re-run after making any firmware changes.

Filter Caliptra Core firmeware

To improve build time when using the core-on-subsystem FPGA configuration, you can use the fw-id parameter to filter out firmware that you are uninterested in.

For example, the following only builds the Caliptra Core ROM

$ cargo xtask-fpga build --fw-id caliptra-rom

Only build MCU firmware

For configurations that additionally build Caliptra Core firmware, you can disable this with the --mcu flag.

For example:

$ cargo xtask-fpga build --mcu

Test Binaries

Run cargo xtask-fpga fpga build-test --target-host $SSH-FPGA-NAME to create a test archive. Using the --target-host flag will automatically copy the test binaries to the FPGA host.

This command should be re-run after making any test changes.

Test Workflow

FPGA Bootstrapping and FW/Test Building

A developer verifying changes against an FPGA should use the following sequence of commands to bootstrap their FPGA board by:

  1. downloading the caliptra-mcu-sw repo on their board,
  2. building all Caliptra Core and MCU firmware collateral, and
  3. building all FPGA tests for the FPGA environment.
$ cargo xtask-fpga fpga bootstrap --target-host $SSH-FPGA-NAME # Run this only once per boot.
$ cargo xtask-fpga fpga build --target-host $SSH-FPGA-NAME # Build firmware. Re-run every time firmware changes.
$ cargo xtask-fpga fpga build-test --target-host $SSH-FPGA-NAME # Build test binaries. Re-run every time tests change.

Dispatching all FPGA Tests

A developer verifying changes against an FPGA can dispatch the entire FPGA test in a single shot by running:

$ cargo xtask-fpga fpga test --target-host $SSH-FPGA-NAME

Dispatching a Single FPGA Test

A developer that only wishes to run a single test on the FPGA can replace the last command with the following:

# For example:
$ cargo xtask-fpga fpga test --target-host $SSH-FPGA-NAME \
    --test-filter="package(mcu-hw-model) and test(test_hash_token)"

The test filter flag is a wrapper for a Cargo Nextest filter set.

Test Caliptra Core on FPGA

The xtask workflow also supports running caliptra-sw tests on a subsystem FPGA.

Bootstrap and Build Test Binaries / FW

$ cargo xtask-fpga fpga bootstrap --target-host $SSH-FPGA-NAME --configuration core-on-subsystem # Run this only once per boot. Re-run bootstrap to change configurations
$ cargo xtask-fpga fpga build --target-host $SSH-FPGA-NAME  # Build firmware. Re-run every time firmware changes.
$ cargo xtask-fpga fpga build-test --target-host $SSH-FPGA-NAME # Build test binaries. Re-run every time tests change.

Running on FPGA

NOTE: The run command does not yet support running over ssh.

Firmware files

You can build firmware binaries (ROM, runtime, manifest, etc.) using a combination of:

  • cargo xtask-fpga rom-build --platform fpga
  • cargo xtask-fpga runtime-build --platform fpga
  • cargo xtask-fpga all-build --platform fpga

The all-build command will build Caliptra ROM, Caliptra firmware bundle, MCU ROM, MCU runtime, and the SoC manifest and package them all together in a ZIP file, which the fpga-run xtask can run.

These commands can be run on any host. It is not recommended to run them on the FPGA host as it is very slow (the build can take over an hour the first time); instead it is better to run the command on a different host and use scp or rsync to copy the ZIP in.

Running ROM and firmware

cargo xtask-fpga fpga-run or ./xtask-bin fpga-run can be used to run ROMs and firmware, either directly or from a ZIP file, e.g.,

./xtask-bin fpga-run --zip all-fw.zip

It also supports additional command-line options for testing various flows.

UDS Provisioning

Preliminaries

  1. Build OpenOCD 0.12.0 with --enable-sysfsgpio.
  2. Install gdb-multiarch
  3. Run a ROM flow with a blank OTP memory to transition the lifecycle state to TestUnlocked0.
  4. Run a ROM to burn lifecycle tokens for the other transitions.
  5. Run a ROM flow to transition the lifecycle state to Dev (this maps to the Manufacturing device lifecycle for Caliptra).
  6. Start a run with the bootfsm_break set to 1.
  7. Start the OpenOCD server:
sudo openocd --file openocd_caliptra.txt
  1. Connect to OpenOCD
telnet localhost 4444
  1. Verify connectivity in telnet:
> riscv.cpu riscv dmi_read 0x74
0xcccccccc
  1. Write the request
> riscv.cpu riscv dmi_write 0x70 4
> riscv.cpu riscv dmi_write 0x61 1

OTP

There is an example fully provisioned (UDS and FE burned and transitioned into production) .vmem file for loading into the OTP via the --otp in the repository in otp-prod-fe.vmem.

Caliptra Utility Host Library Design

Overview

The Caliptra Utility host library is a modular, extensible, and transport-agnostic Rust library designed to enable the development of utility tools that can run on both BMC and host systems. These utilities facilitate both in-band and out-of-band communication with the Caliptra subsystem on the device, supporting a variety of transport interfaces such as MCU mailbox and MCTP VDM. The library provides a unified API for all supported Caliptra commands, along with C bindings for integration with C/C++ applications, making it easy to build robust management, provisioning, and diagnostic tools for diverse deployment environments.

Design Goals

  • Modularity: Clear separation between transport layer, command processing, and application logic
  • Extensibility: Easy addition of new commands and transport mechanisms
  • Transport Agnostic: Support multiple communication channels without changing application code
  • Type Safety: Strong typing for command structures and responses
  • Thread Safety: Safe for use in multi-threaded environments

Use Cases

  • Device Identification and Capabilities
    • Retrieve firmware versions, unique device identifiers, and device capabilities to ensure compatibility and proper configuration.
    • Query device-specific information such as chip identifiers or subsystem details.
  • Debugging and Diagnostics
    • Retrieve debug logs to analyze device behavior, diagnose issues, and monitor runtime states.
    • Clear logs to reset diagnostic data and maintain storage efficiency.
  • Certificate Management
    • Export Certificate Signing Requests (CSRs) for device keys to facilitate secure provisioning.
    • Import signed certificates to establish a trusted certificate chain for device authentication.
  • Debug Unlock Mechanisms
    • Facilitate secure debugging in production environments
    • Ensure controlled access to debugging features
  • Cryptographic Services
    • AES encryption and decryption
    • SHA hashing
    • Random number generation
    • Digital signing
    • Signature verification
    • Key exchange

Architecture

Layered framework

flowchart TD
    classDef wideBox font-size:12px, padding:10px;

    A["Utility Application"]:::wideBox
    subgraph Library["Caliptra Utility Host Library"]
        B["Command Layer<br>(High-level APIs, transport-agnostic)"]:::wideBox
        C["Core Layer<br>(Command builder, Response parser, <br/>protocol mapping, session mgmt)"]:::wideBox
        D["Transport Abstraction Layer<br>(Unified interface for all transports)"]:::wideBox
        E["Transport Implementation Layer<br>(SoC Mailbox, MCTP VDM, <br/>Custom transports etc.)"]:::wideBox
    end

    A --> B
    B --> C
    C --> D
    D --> E

Component Descriptions

Command Layer

  • Purpose: Provides high-level, transport-agnostic APIs for Caliptra commands
  • Functionality:
    • Exposes simple function calls for each Caliptra command (e.g., caliptra_get_device_id())
    • Manages stateful operations like multi-part SHA/AES computations
    • Maintains command contexts across multiple related operations
    • Handles command-specific validation and parameter checking
    • Abstracts complex command sequences into simple APIs
  • Data Flow: Receives API calls → Validates parameters → Passes to Core Layer for execution

Core Layer

  • Purpose: Handles the mechanics of command execution and protocol management
  • Functionality:
    • Builds properly formatted command packets with headers and checksums
    • Parses and validates responses from the device
    • Implements retry logic for transient failures
    • Manages command sequencing and state machines
    • Handles protocol-level error recovery
  • Data Flow: Receives command requests → Formats packets → Uses Transport Layer → Parses responses

Transport Abstraction Layer

  • Purpose: Provides a unified interface to different physical transport mechanisms
  • Functionality:
    • Defines standard operations (open, close, send, receive)
    • Manages connection lifecycle and readiness checks
    • Handles transport-level timeouts and flow control
    • Provides both synchronous and asynchronous operation modes
    • Maps transport-specific errors to common error codes
  • Data Flow: Receives formatted packets → Routes to appropriate transport → Returns raw responses

Transport Implementation Layer

  • Purpose: Implements actual communication with hardware interfaces
  • Functionality:
    • SoC Mailbox: Direct memory-mapped I/O operations to mailbox registers
    • MCTP VDM: Socket-based communication over MCTP protocol
    • Future transports:
    • Handles hardware-specific initialization and configuration
    • Manages low-level data transfer and synchronization
  • Data Flow: Receives bytes to send → Writes to hardware → Reads response bytes

Interface Design

The library is organized into several Rust crates, each providing specific functionality:

CrateDescription
caliptra-util-host-transportTransport trait and implementations (Mailbox, etc.)
caliptra-util-host-sessionSession management and command execution
caliptra-util-host-command-typesCommand/response type definitions with zerocopy support
caliptra-util-host-commandsHigh-level command API functions
caliptra-util-host-osalOS abstraction layer for portability
caliptra-util-host-cbindingC bindings for the library

Transport Layer

The transport layer provides an abstraction for device communication through the Transport trait:

#![allow(unused)]
fn main() {
/// Transport trait for device communication
pub trait Transport: Send + Sync {
    /// Connect to the device
    fn connect(&mut self) -> TransportResult<()>;

    /// Disconnect from the device
    fn disconnect(&mut self) -> TransportResult<()>;

    /// Send a command with the given command ID and payload
    fn send(&mut self, command_id: u32, data: &[u8]) -> TransportResult<()>;

    /// Receive response data into the provided buffer
    fn receive(&mut self, buffer: &mut [u8]) -> TransportResult<usize>;

    /// Check if the transport is connected
    fn is_connected(&self) -> bool;
}

/// Transport error types
pub enum TransportError {
    ConnectionFailed(Option<&'static str>),
    SendFailed(Option<&'static str>),
    ReceiveFailed(Option<&'static str>),
    Timeout,
    NotSupported(&'static str),
    InvalidMessage,
    Disconnected,
    ConfigurationError(&'static str),
    IoError(&'static str),
    BufferError(&'static str),
    // ... additional error variants
}

/// Transport configuration
pub struct TransportConfig {
    pub max_message_size: usize,
    pub timeout_ms: u32,
}
}

Mailbox Transport Implementation

The Mailbox transport provides communication through a mailbox driver:

#![allow(unused)]
fn main() {
/// Trait for hardware mailbox communication
pub trait MailboxDriver: Send + Sync {
    /// Send a command and return response data
    fn send_command(&mut self, external_cmd: u32, payload: &[u8]) -> Result<&[u8], MailboxError>;

    /// Check if mailbox is ready
    fn is_ready(&self) -> bool;

    /// Connect to mailbox
    fn connect(&mut self) -> Result<(), MailboxError>;

    /// Disconnect from mailbox
    fn disconnect(&mut self) -> Result<(), MailboxError>;
}

/// Mailbox error types
pub enum MailboxError {
    NotReady,
    Timeout,
    InvalidCommand,
    CommunicationError,
    BufferOverflow,
    DeviceError(u32),
}

/// Mailbox Transport using dynamic dispatch
pub struct Mailbox<'a> { mailbox: &'a mut dyn MailboxDriver,

    // internal state...
}

impl<'a> Mailbox<'a> {
    /// Create a new mailbox transport with the given driver
    pub fn new(mailbox: &'a mut dyn MailboxDriver) -> Self;
}

impl Transport for Mailbox<'_> {
    // Transport trait implementation...
}
}

Session Layer

The session layer manages device connections and command execution:

#![allow(unused)]
fn main() {
/// Session state enumeration
#[repr(u32)]
pub enum SessionState {
    Disconnected = 0,
    Connecting = 1,
    Connected = 2,
    Authenticated = 3,
    Error = 4,
}

/// Session configuration
pub struct SessionConfig {
    pub connection_timeout_ms: u32,
    pub command_timeout_ms: u32,
    pub max_retries: u8,
    pub keepalive_interval_ms: u32,
    pub auto_reconnect: bool,
}

/// Session statistics
pub struct SessionStatistics {
    pub commands_sent: u64,
    pub commands_succeeded: u64,
    pub commands_failed: u64,
    pub bytes_sent: u64,
    pub bytes_received: u64,
    pub reconnect_count: u32,
    pub last_error_count: u32,
}

/// Session error enumeration
pub enum SessionError {
    SessionNotFound(u32),
    InvalidState { current: SessionState, expected: SessionState },
    TransportError(&'static str),
    OsalError(&'static str),
    ConfigurationError(&'static str),
    AuthenticationError(&'static str),
    ResourceError(&'static str),
    SerializationError(&'static str),
    MaxRetriesExceeded,
    InternalError(&'static str),
    Custom(&'static str),
}

/// Device session context using dynamic dispatch
pub struct CaliptraSession<'t> {
    pub session_id: u32,
    pub state: SessionState,
    pub config: SessionConfig,
    pub stats: SessionStatistics,
    // internal fields...
}

impl<'t> CaliptraSession<'t> {
    /// Create a new session with the given transport
    pub fn new(session_id: u32, transport: &'t mut dyn Transport) -> SessionResult<Self>;

    /// Create session with custom configuration
    pub fn with_config(
        session_id: u32,
        transport: &'t mut dyn Transport,
        config: SessionConfig,
    ) -> SessionResult<Self>;

    /// Connect to the device
    pub fn connect(&mut self) -> SessionResult<()>;

    /// Disconnect from device
    pub fn disconnect(&mut self) -> SessionResult<()>;

    /// Check if session is connected and ready
    pub fn is_ready(&self) -> bool;

    /// Execute a structured command through the session
    pub fn execute_command<Req, Resp>(&mut self, command: &Req) -> SessionResult<Resp>
    where
        Req: CommandRequest,
        Resp: CommandResponse;

    /// Execute a command with explicit command ID
    pub fn execute_command_with_id<Req>(
        &mut self,
        command_id: CaliptraCommandId,
        request: &Req,
    ) -> SessionResult<Req::Response>
    where
        Req: CommandRequest + IntoBytes,
        Req::Response: FromBytes + Immutable;

    /// Get session info
    pub fn get_info(&self) -> SessionInfo;

    /// Handle session errors and recovery
    pub fn handle_error(&mut self, error: SessionError) -> SessionResult<()>;
}
}

Command Types Layer

Command and response structures are defined using zerocopy for efficient serialization:

#![allow(unused)]
fn main() {
/// Caliptra command IDs
#[repr(u32)]
pub enum CaliptraCommandId {
    // Device Info Commands (0x0001-0x000F)
    GetFirmwareVersion = 0x0001,
    GetDeviceCapabilities = 0x0002,
    GetDeviceId = 0x0003,
    GetDeviceInfo = 0x0004,

    // Certificate Commands (0x1001-0x101F)
    GetIdevidCert = 0x1001,
    GetLdevidCert = 0x1002,
    GetFmcAliasCert = 0x1003,
    GetRtAliasCert = 0x1004,
    GetCertChain = 0x1010,
    StoreCertificate = 0x1011,

    // Hash Commands (0x2001-0x201F)
    HashInit = 0x2001,
    HashUpdate = 0x2002,
    HashFinalize = 0x2003,
    HashOneShot = 0x2004,

    // Symmetric Crypto Commands (0x3001-0x302F)
    AesInit = 0x3001,
    AesUpdate = 0x3002,
    AesFinalize = 0x3003,

    // Asymmetric Crypto Commands (0x4001-0x402F)
    EcdsaSign = 0x4001,
    EcdsaVerify = 0x4002,
    EcdhDerive = 0x4003,

    // Debug Commands (0x7001-0x701F)
    DebugEcho = 0x7001,
    DebugGetStatus = 0x7002,
    DebugGetLog = 0x7005,

    // Fuse Commands (0x8001-0x801F)
    FuseRead = 0x8001,
    FuseWrite = 0x8002,
    FuseLock = 0x8003,
}

/// Command processing errors
pub enum CommandError {
    InvalidCommand,
    InvalidRequest,
    InvalidResponse,
    InvalidResponseLength,
    ResponseTooLong,
    SerializationError,
    DeserializationError,
    ChecksumMismatch,
    Unsupported,
    BufferTooSmall,
    Custom(&'static str),
}
}

Example Command Types

#![allow(unused)]
fn main() {
/// Get device ID request
#[repr(C)]
#[derive(Debug, Clone, IntoBytes, FromBytes, Immutable)]
pub struct GetDeviceIdRequest {
    // Empty request - no parameters needed
}

/// Get device ID response
#[repr(C)]
#[derive(Debug, Clone, IntoBytes, FromBytes, Immutable)]
pub struct GetDeviceIdResponse {
    pub vendor_id: u16,
    pub device_id: u16,
    pub subsystem_vendor_id: u16,
    pub subsystem_id: u16,
}

impl CommandRequest for GetDeviceIdRequest {
    type Response = GetDeviceIdResponse;
    const COMMAND_ID: CaliptraCommandId = CaliptraCommandId::GetDeviceId;
}

/// Get firmware version request
#[repr(C)]
#[derive(Debug, Clone, IntoBytes, FromBytes, Immutable)]
pub struct GetFirmwareVersionRequest {
    pub index: u32, // 0 = ROM, 1 = Runtime
}

/// Firmware version response
#[repr(C)]
#[derive(Debug, Clone, IntoBytes, FromBytes, Immutable)]
pub struct GetFirmwareVersionResponse {
    pub common: CommonResponse,
    pub version: [u32; 4],   // Major, minor, patch, build
    ...
}
}

Command API Layer

The command API provides high-level functions for interacting with Caliptra devices:

#![allow(unused)]
fn main() {
/// High-level result type for API functions
pub type CaliptraResult<T> = Result<T, CaliptraApiError>;

/// API-specific error types
pub enum CaliptraApiError {
    Osal(OsalError),
    InvalidParameter(&'static str),
    SessionNotInitialized,
    TransportNotAvailable,
    CommandFailed(&'static str),
    SessionError(&'static str),
}

// ============================================================================
// Device Information Commands
// ============================================================================

/// Get device identification information
pub fn caliptra_cmd_get_device_id(
    session: &mut CaliptraSession,
) -> CaliptraResult<GetDeviceIdResponse>;

/// Get device information
pub fn caliptra_cmd_get_device_info(
    session: &mut CaliptraSession,
    info_type: u32,
) -> CaliptraResult<GetDeviceInfoResponse>;

/// Get device capabilities
pub fn caliptra_cmd_get_device_capabilities(
    session: &mut CaliptraSession,
) -> CaliptraResult<GetDeviceCapabilitiesResponse>;

/// Get firmware version
pub fn caliptra_cmd_get_firmware_version(
    session: &mut CaliptraSession,
    index: u32,  // 0 = ROM, 1 = Runtime
) -> CaliptraResult<GetFirmwareVersionResponse>;
}

C Bindings

The library provides C bindings through the cbinding crate for integration with C/C++ applications:

// Error code type for C API
typedef enum {
    CALIPTRA_SUCCESS = 0,
    CALIPTRA_ERROR_UNKNOWN = 1,
    CALIPTRA_ERROR_INVALID_ARGUMENT = 2,
    CALIPTRA_ERROR_TIMEOUT = 3,
    CALIPTRA_ERROR_NOT_SUPPORTED = 4,
    CALIPTRA_ERROR_TRANSPORT = 5,
    CALIPTRA_ERROR_PROTOCOL = 6,
    CALIPTRA_ERROR_DEVICE = 7,
    CALIPTRA_ERROR_MEMORY = 8,
    CALIPTRA_ERROR_BUSY = 9,
    CALIPTRA_ERROR_STATE = 10,
    CALIPTRA_ERROR_IO = 11,
} CaliptraError;

// Protocol types
typedef enum {
    CALIPTRA_PROTOCOL_MAILBOX = 0,
    CALIPTRA_PROTOCOL_MCTP_VDM = 1,
    CALIPTRA_PROTOCOL_CUSTOM = 2,
} CaliptraProtocolType;

// Opaque transport handle
typedef struct CaliptraTransport CaliptraTransport;

// Session management
CaliptraError caliptra_session_create_with_protocol(
    CaliptraTransport* transport,
    CaliptraProtocolType protocol_type,
    CaliptraSession** session
);

CaliptraError caliptra_session_connect(CaliptraSession* session);
CaliptraError caliptra_session_disconnect(CaliptraSession* session);
CaliptraError caliptra_session_destroy(CaliptraSession* session);

// Transport management
CaliptraError caliptra_transport_destroy(CaliptraTransport* transport);

// Device ID structure (C-compatible)
typedef struct {
    uint16_t vendor_id;
    uint16_t device_id;
    uint16_t subsystem_vendor_id;
    uint16_t subsystem_id;
} CaliptraDeviceId;

// Command APIs
CaliptraError caliptra_cmd_get_device_id(
    CaliptraSession* session,
    CaliptraDeviceId* device_id
);

Usage Example (Rust)

use caliptra_util_host_transport::{Mailbox, MailboxDriver};
use caliptra_util_host_session::CaliptraSession;
use caliptra_util_host_commands::api::caliptra_cmd_get_device_id;

// Implement MailboxDriver for your hardware
struct MyMailboxDriver { /* ... */ }

impl MailboxDriver for MyMailboxDriver {
    fn send_command(&mut self, cmd: u32, payload: &[u8]) -> Result<&[u8], MailboxError> {
        // Hardware-specific implementation
    }
    fn is_ready(&self) -> bool { true }
    fn connect(&mut self) -> Result<(), MailboxError> { Ok(()) }
    fn disconnect(&mut self) -> Result<(), MailboxError> { Ok(()) }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create mailbox driver
    let mut driver = MyMailboxDriver::new();

    // Create transport
    let mut transport = Mailbox::new(&mut driver);

    // Create session
    let mut session = CaliptraSession::new(1, &mut transport)?;

    // Connect
    session.connect()?;

    // Get device ID
    let device_id = caliptra_cmd_get_device_id(&mut session)?;
    println!("Vendor ID: 0x{:04x}", device_id.vendor_id);
    println!("Device ID: 0x{:04x}", device_id.device_id);

    // Disconnect
    session.disconnect()?;

    Ok(())
}

Extensibility with Plugin Architecture

The Caliptra Utility host library is designed to be highly extensible, supporting the dynamic addition of new commands, protocols, and transport mechanisms at runtime through a plugin architecture. This approach enables third-party developers and system integrators to extend the library’s capabilities without modifying or recompiling the core library.

flowchart TD
    subgraph App["Utility Application"]
        A1[Main App/CLI]
        A2["Plugin (Shared Library)"]
    end

    subgraph Library["Caliptra Utility Host Library"]
        B1[Command Layer]
        B2[Core Layer]
        B3[Transport Abstraction Layer]
        B4[Transport Impl Layer]
        B5[Plugin Loader & Registry]
    end

    A1 -->|Loads at runtime| A2
    A2 -- Registers new commands/transports/protocols --> B5
    A1 -->|Uses APIs| B1
    B1 --> B2
    B2 --> B3
    B3 --> B4
    B5 -- Makes available to --> B1
  • Plugins are shared libraries (e.g., .so or .dll files) that implement new commands, protocol handlers, or transport mechanisms.
  • The core library provides APIs to dynamically load, register, and discover these plugins at runtime.
  • This design preserves the existing layered architecture, while enabling runtime extensibility and modularity.

How Plugins Work

  1. Plugin Interface

    Each plugin implements a standard interface and exports a descriptor structure.

    #![allow(unused)]
    fn main() {
    /// Plugin descriptor containing metadata and registration function
    pub struct PluginDescriptor {
        /// Plugin name identifier
        pub name: &'static str,
        /// Plugin version string
        pub version: &'static str,
        /// Registration function called by the core library
        pub register: fn() -> CaliptraResult<()>,
        /// Optional cleanup function called on unload
        pub unregister: Option<fn() -> CaliptraResult<()>>,
    }
    
    /// Macro to export a plugin descriptor from a shared library
    #[macro_export]
    macro_rules! export_plugin {
        ($descriptor:expr) => {
            #[no_mangle]
            pub static CALIPTRA_PLUGIN_DESCRIPTOR: PluginDescriptor = $descriptor;
        };
    }
    }

    The plugin's register function is called by the core library to register new commands, transports, or protocol handlers.

  2. Dynamic Loading

    The core library provides APIs to load and unload plugins at runtime.

    #![allow(unused)]
    fn main() {
    /// Plugin manager for loading and managing plugins
    pub struct PluginManager {
        // Internal state...
    }
    
    impl PluginManager {
        /// Create a new plugin manager
        pub fn new() -> Self;
    
        /// Load a plugin from the specified path
        pub fn load(&mut self, path: &Path) -> CaliptraResult<PluginHandle>;
    
        /// Unload a plugin by name
        pub fn unload(&mut self, name: &str) -> CaliptraResult<()>;
    
        /// Get a list of loaded plugins
        pub fn loaded_plugins(&self) -> &[PluginInfo];
    }
    
    /// Handle to a loaded plugin
    pub struct PluginHandle {
        pub name: String,
        pub version: String,
    }
    
    /// Information about a loaded plugin
    pub struct PluginInfo {
        pub name: String,
        pub version: String,
        pub path: PathBuf,
    }
    }

    These use platform mechanisms (dlopen/dlsym on Linux, LoadLibrary on Windows) to load plugins at runtime.

  3. Dynamic Registration

    Plugins use registration and unregistration APIs to add or remove features.

    #![allow(unused)]
    fn main() {
    /// Command descriptor for plugin-provided commands
    pub struct CommandDescriptor {
        /// Command name
        pub name: &'static str,
        /// Command ID
        pub command_id: u32,
        /// Command execution function
        pub execute: fn(&mut CaliptraSession, &[u8]) -> CaliptraResult<Vec<u8>>,
        /// Human-readable description
        pub description: &'static str,
    }
    
    /// Transport factory for plugin-provided transports
    pub trait TransportFactory: Send + Sync {
        /// Transport type name
        fn name(&self) -> &str;
    
        /// Create a new transport instance
        fn create(&self, config: &TransportConfig) -> CaliptraResult<Box<dyn Transport>>;
    
        /// Human-readable description
        fn description(&self) -> &str;
    }
    
    /// Protocol handler for plugin-provided protocol implementations
    pub trait ProtocolHandler: Send + Sync {
        /// Protocol type this handler supports
        fn protocol_type(&self) -> ProtocolType;
    
        /// Process an incoming message
        fn handle_message(&mut self, message: &[u8]) -> CaliptraResult<Vec<u8>>;
    }
    
    // Registration functions
    /// Register a new command
    pub fn register_command(desc: CommandDescriptor) -> CaliptraResult<()>;
    
    /// Unregister a command by name
    pub fn unregister_command(name: &str) -> CaliptraResult<()>;
    
    /// Register a new transport factory
    pub fn register_transport(factory: Box<dyn TransportFactory>) -> CaliptraResult<()>;
    
    /// Unregister a transport by name
    pub fn unregister_transport(name: &str) -> CaliptraResult<()>;
    
    /// Register a protocol handler
    pub fn register_protocol_handler(handler: Box<dyn ProtocolHandler>) -> CaliptraResult<()>;
    
    /// Unregister a protocol handler
    pub fn unregister_protocol_handler(protocol_type: ProtocolType) -> CaliptraResult<()>;
    }

    The library maintains internal registries of available commands, transports, and protocol handlers. When a plugin is unloaded, it should call the appropriate unregister functions to cleanly remove its features and release resources.

  4. Discovery and Usage

    Applications can enumerate available commands and transports at runtime.

    #![allow(unused)]
    fn main() {
    /// Get a list of registered commands
    pub fn list_commands() -> Vec<&'static CommandDescriptor>;
    
    /// Get a list of registered transport factories
    pub fn list_transports() -> Vec<&'static str>;
    
    /// Get a command descriptor by name
    pub fn get_command(name: &str) -> Option<&'static CommandDescriptor>;
    
    /// Create a transport instance by name
    pub fn create_transport(name: &str, config: &TransportConfig) -> CaliptraResult<Box<dyn Transport>>;
    }

    This enables dynamic feature discovery and flexible application logic.

Extensibility for SPDM and PLDM Support

The Caliptra Utility host library architecture is designed to accommodate future extensions for industry-standard security and firmware management protocols. Specifically, the library can be extended to support SPDM (Security Protocol and Data Model) for device attestation and PLDM (Platform Level Data Model) for firmware updates by leveraging DMTF open source libraries.

Design Approach: Transport Adapter Pattern

Rather than implementing SPDM and PLDM protocols directly, the library adopts a transport adapter pattern that aligns with how DMTF libraries are designed:

  • libspdm expects the application to provide transport send/receive callbacks
  • libpldm is primarily a message encoding/decoding library that needs a transport mechanism

The host library's core value is providing transport abstraction. By providing transport adapters, the library enables applications to use libspdm and libpldm directly while leveraging the library's transport infrastructure.

Integration Architecture

flowchart TB
    subgraph Host["Host"]
        direction TB
        subgraph App["Application"]
            CAL_APP["Caliptra Commands"]
            SPDM_APP["SPDM Attestation"]
            PLDM_APP["PLDM Firmware Update"]
        end
        subgraph DMTF["Open Source Libraries"]
            LSPDM["DMTF/libspdm"]
            LPLDM["libpldm"]
        end
        subgraph HostLib["Caliptra Utility Host Library"]
            direction TB
            CMD["Command Layer"]
            CORE["Core Layer"]
            ADAPTER["Protocol Transport Adapters"]
            TAL["Transport Abstraction Layer"]
        end
    end

    CAL_APP --> CMD
    SPDM_APP --> LSPDM
    PLDM_APP --> LPLDM
    LSPDM -->|"transport callbacks"| ADAPTER
    LPLDM -->|"send/receive"| ADAPTER
    CMD --> CORE
    CORE --> TAL
    ADAPTER --> TAL

OpenBMC Integration

For out-of-band management scenarios, the host library is eligible to integrate with OpenBMC:

  • libpldm: OpenBMC already includes libpldm as a core component, enabling seamless integration for PLDM firmware update workflows
  • DMTF/libspdm: Can be integrated into OpenBMC for SPDM attestation services
  • D-Bus Services: The library's C bindings enable integration with OpenBMC D-Bus services for system management
  • MCTP Transport: Leverages OpenBMC's MCTP infrastructure for device communication

Future Work

Note: The SPDM and PLDM transport adapter implementations are not provided in the current release. The architecture described above outlines the extensibility path for future integration. Third-party developers and system integrators can implement these adapters using the documented transport interfaces and DMTF open source libraries.

Network Recovery Boot

This document outlines the design for a lightweight network recovery boot mechanism for the Caliptra subsystem. The system enables the Caliptra SS to download firmware images over the network through a dedicated Network Boot Coprocessor within a ROM environment, providing a resilient fallback path when flash memory is corrupted.

The network boot coprocessor acts as an intermediary between remote image servers and the Caliptra SS, handling network communications including DHCP configuration, TFTP server discovery, and firmware image downloads. The system supports downloading multiple firmware components that includes the Caliptra SS and SoC images through a firmware ID-based mapping system.

Motivation

  • Flash Dependency Risk: Boot failure if both flashes are corrupted
  • Recovery Challenge: Physical intervention is costly in hyperscale environments
  • Design Goals:
    • Minimal MCU ROM footprint
    • Consistent with OCP streaming boot model for early firmware (Caliptra FMC + RT, SoC Manifest, MCU RT)
    • Secure image retrieval
    • Resilient fallback path
  • Solution:
    • Use a dedicated co-processor with a lightweight network stack
    • Automatically configure networking via DHCP
    • Securely download Caliptra early firmware images into the Caliptra subsystem

System Architecture

flowchart LR
    subgraph Caliptra_Subsystem["Caliptra Subsystem"]
        Caliptra["Caliptra"]
        RecoveryIF["Recovery I/F"]
        MCU_ROM["MCU ROM"]
        Mailbox["Mailbox"]
        MCU_RT["MCU Runtime"]

        Caliptra <--> RecoveryIF
        RecoveryIF <--> MCU_ROM
        Caliptra <--> Mailbox
        Mailbox <--> MCU_RT
    end

    MCU_ROM <--> Network_ROM["Network ROM<br/>- DHCP Client<br/>- TFTP Client<br/>- FW ID Mapping"]
    MCU_RT <--> Network_ROM
    Network_ROM <--> Image_Server["Image Server<br/>- Image Store<br/>- DHCP Server<br/>- TFTP Server<br/>- Config File"]

Network Recovery Boot Flow

Stage 1: Booting early firmware

  • Network recovery boot initiation The following diagram illustrates the high-level flow using the BootSourceProvider interface, showing how InitiateBoot drives DHCP and TOC fetch:
sequenceDiagram
    participant MCU as MCU ROM
    participant NET as Network ROM<br>(BootSourceProvider)
    participant IMG as Image Server

    MCU->>NET: Initiate boot request
    NET->>IMG: Network Configuration Request (DHCPv4/DHCPv6 Discovery)
    IMG-->>NET: Network Configuration Response (DHCPv4/DHCPv6 Offer)
    NET->>IMG: TFTP GET config file (TOC)
    IMG-->>NET: TOC (FW ID mappings)
    NET->>NET: Store FW ID to filename mappings
    NET-->>MCU: Network boot available
  • Early firmware image transfer

Once the boot source is initialized, the MCU ROM uses the BootSourceProvider methods to fetch each firmware component:

sequenceDiagram
    participant CRIF as Caliptra Recovery I/F
    participant MCU as MCU ROM
    participant NET as Network ROM<br>(BootSourceProvider)
    participant IMG as Image Server

    loop For each image_id (0,1,2)
        MCU->>CRIF: Poll recovery readiness
        CRIF-->>MCU: Awaiting recovery image_id

        MCU->>NET: get_image_metadata(image_id)
        NET-->>MCU: ImageMetadata { size, checksum, version }

        MCU->>CRIF: Set image size (INDIRECT_FIFO_CTRL.write)

        MCU->>NET: download_image(image_id)
        NET->>IMG: TFTP GET image file

        loop Image transfer by chunk
            IMG-->>NET: Image chunk
            NET-->>MCU: Forward image chunk (ImageStream::read_chunk)
            MCU->>CRIF: Write chunk
            MCU-->>NET: Chunk ACK
        end
    end

    MCU->>NET: Finalize network boot

Stage 2: Booting remainder firmware

This section describes the flow for loading and authenticating SoC images at runtime through the MCU Runtime. The MCU Runtime coordinates image authorization with the Caliptra Core, while Network ROM handles downloading image data from the network.

  • Network recovery boot initiation
sequenceDiagram
    participant MCURT as MCU RT
    participant NET as Network ROM<br>(BootSourceProvider)
    participant IMG as Image Server

    MCURT->>NET: Initiate boot request
    opt If TOC not already cached
        NET->>IMG: TFTP GET config file (TOC)
        IMG-->>NET: TOC (FW ID mappings)
        NET->>NET: Store FW ID to filename mappings
    end
    NET-->>MCURT: Network boot available
  • Remainder firmware images transfer
sequenceDiagram
    participant CORE as Caliptra RT
    participant MCURT as MCU RT
    participant NET as Network ROM<br>(BootSourceProvider)
    participant IMG as Image Server

    loop For each SoC image(image_id)
        MCURT->>CORE: get_image_info(image_id)
        CORE-->>MCURT: ImageInfo { load_address }

        MCURT->>NET: get_image_metadata(image_id)
        NET-->>MCURT: ImageMetadata { size, checksum, version }

        MCURT->>NET: download_image(image_id)
        NET->>IMG: TFTP GET image file
        loop Image transfer by chunk
            IMG-->>NET: Image chunk
            NET-->>MCURT: Image chunk

            MCURT->>MCURT: Write chunk to load_address
        end

        MCURT->>CORE: authorize(image_id)
        CORE-->>MCURT: Success/Error response
    end

    MCURT->>NET: Finalize network boot

Protocol Support

The boot source provider supports a minimal set of protocols optimized for the Caliptra ROM environment:

DHCP (Dynamic Host Configuration Protocol)

  • Purpose: Automatic network configuration
  • Advantages:
    • Standard network configuration protocol
    • Minimal overhead for basic IP assignment
    • Simple UDP-based protocol
  • Implementation: Client-side DHCP for IP address, gateway, and boot server discovery

TFTP (Trivial File Transfer Protocol)

  • Purpose: Lightweight file transfer for firmware images
  • Advantages:
    • Extremely lightweight - minimal overhead perfect for ROM environments
    • Simple UDP-based protocol - easy to implement securely
    • Small code footprint (~5-10KB implementation)
    • Standard protocol for network boot scenarios
  • Implementation: Client-side TFTP for firmware image download

IPv4 and IPv6 Support

  • Dual-Stack: Support both IPv4 and IPv6 throughout discovery and transfer
  • UDPv4/UDPv6: TFTP runs over UDP; ensure lwIP IPv6 and UDP are enabled
  • DHCPv4/DHCPv6: Acquire network configuration via DHCP for both families
  • Address Selection: Prefer IPv6 when available; fall back to IPv4
  • TOC URLs: TOC entries may reference IPv4 or IPv6 hosts; support tftp://[IPv6] URLs

DHCP Options for TFTP

  • DHCPv4 (RFC 2132)
    • Option 66: TFTP server name (hostname or IP address)
    • Option 67: Bootfile name (path to TOC or image)
    • Optional: Option 43 (Vendor-Specific) for custom parameters
  • DHCPv6 (RFC 5970)
    • Option 59: Bootfile URL (e.g., tftp://server/path/to/toc.bin)
    • Option 60: Bootfile Parameters (optional, for additional metadata)
    • Note: DHCPv6 does not define a separate TFTP server option; use Bootfile URL

Boot Source Provider Interface

The Boot Source Provider Interface defines a generic contract for boot image providers, enabling support for multiple boot sources (network boot coprocessor, flash device, or other custom implementations). The MCU ROM communicates with any boot source through this unified interface.

Messaging Protocol

The boot source provider communication uses a simple request-response messaging protocol, with the MCU ROM initiating requests and the BootSourceProvider responding. The following section defines the message types, packet formats, and field definitions.

Message Types and Packet Formats

1. Initiate Boot Request

Initiates the boot source discovery process.

Request Packet:

OffsetSizeFieldDescription
01Message Type0x01 - InitiateBoot
13ReservedMust be 0
44Protocol VersionVersion of the messaging protocol
8NSource SpecificSource-specific initialization parameters

Response Packet:

OffsetSizeFieldDescription
01Message Type0x81 - InitiateBoot Response
11Status0x00=Started, 0x01=InProgress, 0x02–0xFF=Error code
22ReservedMust be 0

2. Get Image Metadata Request

Queries metadata about a specific firmware image.

Request Packet:

OffsetSizeFieldDescription
01Message Type0x02 - Image Info Request
11Firmware ID0=CaliptraFmcRt, 1=SocManifest, 2=McuRt, 0x10000000..-SoC
22ReservedMust be 0

Response Packet:

OffsetSizeFieldDescription
01Message Type0x82 - Image Info Response
11Status0x00=Success, non-zero=Error
22ReservedMust be 0
44Image SizeTotal size in bytes
832ChecksumChecksum of the image
404VersionImage version number
444FlagsBit 0: Compressed, Bit 1: Signed, etc.
484ReservedFor future use

3. Image Download Request

Initiates download of a firmware image. The image is transferred in chunks. The MCU ROM and BootSourceProvider should share a fixed CHUNK_SIZE configuration to indicate the maximum number of bytes that can be transferred in a chunk.

Request Packet:

OffsetSizeFieldDescription
01Message Type0x03 - Image Download Request
11Firmware ID0=CaliptraFmcRt, 1=SocManifest, 2=McuRt, 0x10000000..-SoC
22ReservedMust be 0
44ReservedCan be extended to support flash-based boot
84ReservedCan be extended to support flash-based boot

Response Packet (per chunk):

OffsetSizeFieldDescription
01Message Type0x83 - Image Chunk
11Status0x00=Success, non-zero=Error
22Sequence NumberFor ordered delivery
44OffsetCurrent byte offset in image
84Chunk SizeSize of data in this chunk. If value is less than CHUNK_SIZE, then this is the last chunk.
12NImage DataChunk payload (size = Chunk Size field)

4. Chunk Acknowledgment

Acknowledges receipt of an image chunk and provides flow control.

Request Packet:

OffsetSizeFieldDescription
01Message Type0x04 - Chunk ACK
11Firmware IDFirmware being transferred
22Sequence NumberSequence number to be acknowledged
44Reserved
84FlagsBit 0: Ready for next, Bit 1: Error detected

5. Finalize

Notifies the boot source of recovery completion or error. This allows the BootSourceProvider to free up any allocated resources, terminate connections and stop services (if any).

Request Packet:

OffsetSizeFieldDescription
01Message Type0x05 - Finalize
11Status0x00=Success, non-zero=Error
22Error CodeSpecific error code if Status != 0
44ReservedFor future use

Response Packet:

OffsetSizeFieldDescription
01Message Type0x85 - Finalize ACK
11Status0x00=Acknowledged, non-zero=Error
22ReservedMust be 0
44Cleanup FlagsBit 0: Clear TOC, Bit 1: Reset connection
84ReservedFor future use

Message Summary Table

Message TypeCodeDirectionPurpose
Initiate Boot Request0x01MCU → SourceInitiate boot source discovery
Initiate Boot Response0x81Source → MCUConfirm discovery and image availability
Image Metadata Request0x02MCU → SourceQuery image metadata
Image Metadata Response0x82Source → MCUReturn image metadata and checksums
Image Download Request0x03MCU → SourceStart image transfer
Image Chunk0x83Source → MCUSend image data chunk
Chunk ACK0x04MCU → SourceAcknowledge chunk and flow control
Finalize0x05MCU → SourceNotify recovery completion/error
Finalize ACK0x85Source → MCUFinal acknowledgment

Error Codes

Error Code  Description
----------  -----------
0x00        Success / No Error
0x01        Invalid Message Type
0x02        Invalid Firmware ID
0x03        Image Not Found / Not Available
0x04        Checksum Mismatch
0x05        Transfer Timeout
0x06        Source Not Ready
0x07        Invalid Parameters
0x08        Corrupted Data
0x09        Insufficient Space
0x0A        Checksum Verification Failed
0xFF        Unknown / Unspecified Error

Core Operations

Boot source providers implement the following core operations:

Initialization

  • Source Initialization: Initialize the boot source and make it ready for image requests
  • Status Discovery: Determine availability and readiness of the boot source
  • Configuration Discovery: Discover firmware image metadata and availability

Image Provisioning

  • Image Metadata Query: Query information about available firmware images (size, checksums, etc.)
  • Image Download: Download firmware images by firmware ID
  • Data Streaming: Stream image data to the MCU ROM for direct transfer to Caliptra SS

Supported Firmware IDs

  • ID 0: Caliptra FMC+RT image
  • ID 1: SoC Manifest
  • ID 2: MCU RT image
  • ID 0x10000000 - 0x1FFFFFFF: Reserved for SoC Images (range supports up to 268,435,456 distinct SoC image IDs)

Boot Source Provider Interface

#![allow(unused)]
fn main() {
/// Generic boot source provider interface for the MCU ROM
/// This interface abstracts different boot sources (network, flash, etc.)
pub trait BootSourceProvider {
    type Error;

    /// Initialize the boot source
    /// This performs source-specific initialization (e.g., DHCP for network, etc.)
    fn initiate_boot(&mut self) -> Result<BootSourceStatus, Self::Error>;

    /// Get information about a firmware image
    fn get_image_metadata(&self, firmware_id: FirmwareId) -> Result<ImageInfo, Self::Error>;

    /// Download firmware image by ID
    /// Returns a stream for reading image data in chunks
    fn download_image(&mut self, firmware_id: FirmwareId) -> Result<ImageStream, Self::Error>;

    /// Get boot source status and capabilities
    fn get_boot_source_status(&self) -> Result<BootSourceStatus, Self::Error>;

    /// Deinitialize the boot source
    fn finalize(&self) -> Result<BootSourceStatus, Self::Error>;
}

/// Firmware ID enumeration
#[derive(Debug, Clone, Copy)]
pub enum FirmwareId {
    /// Caliptra FMC+RT image
    CaliptraFmcRt = 0,
    /// SoC Manifest
    SocManifest = 1,
    /// MCU RT image
    McuRt = 2,
    /// SoC Image (raw u32 value in range 0x10000000 - 0x1FFFFFFF)
    SocImage(u32),
}

/// Boot source initialization and capability status
#[derive(Debug)]
pub struct BootSourceStatus {
    pub ready: bool,
    pub initialized: bool,
    pub config_available: bool,
    pub available_images: Vec<u32>,
}

/// Metadata for a firmware image
#[derive(Debug, Clone)]
pub struct ImageInfo {
    pub firmware_id: FirmwareId,
    pub size: u64,
    pub checksum: Option<[u8; 32]>,
    pub version: Option<u32>,
    pub metadata: Vec<u8>,
}

/// Streaming interface for image data
pub trait ImageStream {
    /// Read next chunk of image data
    fn read_chunk(&mut self, buffer: &mut [u8]) -> Result<usize, Error>;

    /// Get total image size if known
    fn total_size(&self) -> Option<u64>;

    /// Check if stream is complete
    fn is_complete(&self) -> bool;
}
}

Implementation Example: Network Boot Coprocessor

For a network boot coprocessor implementation, the boot source provider would:

  1. Initialize: Perform DHCP discovery, locate TFTP server, download TOC
  2. Get Image Metadata: Query image metadata from downloaded TOC
  3. Download Image: Fetch image from TFTP server and stream to MCU ROM
#![allow(unused)]
fn main() {
/// Network-based boot source provider implementation
pub struct NetworkBootSource {
    dhcp_client: DhcpClient,
    tftp_client: TftpClient,
    toc: TableOfContents,
}

impl BootSourceProvider for NetworkBootSource {
    type Error = NetworkBootError;

    fn initiate_boot(&mut self) -> Result<BootSourceStatus, Self::Error> {
        // 1. Perform DHCP discovery
        self.dhcp_client.discover()?;

        // 2. Download TOC via TFTP
        self.toc = self.tftp_client.download_config()?;

        Ok(BootSourceStatus {
            ready: true,
            initialized: true,
            config_available: true,
            available_images: self.toc.firmware_mappings.keys().copied().collect(),
        })
    }

    fn get_image_metadata(&self, firmware_id: FirmwareId) -> Result<ImageInfo, Self::Error> {
        let mapping = self.toc.get_mapping(firmware_id)?;
        Ok(ImageInfo {
            firmware_id,
            size: mapping.size,
            checksum: mapping.checksum,
            version: mapping.version.clone(),
        })
    }

    fn download_image(&mut self, firmware_id: FirmwareId) -> Result<ImageStream, Self::Error> {
        let mapping = self.toc.get_mapping(firmware_id)?;
        self.tftp_client.get_file(&mapping.filename)
    }

    fn get_boot_source_status(&self) -> Result<BootSourceStatus, Self::Error> {
        // Return current network and TFTP status
        Ok(BootSourceStatus {
            ready: self.tftp_client.is_reachable(),
            initialized: true,
            config_available: true,
            available_images: self.toc.firmware_mappings.keys().copied().collect(),
        })
    }

    fn finalize(&self) -> Result<BootSourceStatus, Self::Error> {
        Ok(BootSourceStatus::Success)
    }
}
}

Usage Example

#![allow(unused)]
fn main() {
// Example: MCU ROM boot process using generic boot source
fn recovery_boot(mut boot_source: &mut dyn BootSourceProvider) -> Result<(), Error> {
    // 1. Initialize boot source
    let status = boot_source.initiate_boot()?;

    if !status.ready || !status.initialized {
        return Err(Error::BootSourceNotAvailable);
    }

    // 2. Download each firmware image
    for firmware_id in [FirmwareId::CaliptraFmcRt, FirmwareId::SocManifest, FirmwareId::McuRt] {
        // Get image metadata
        let image_info = boot_source.get_image_metadata(firmware_id)?;

        // Set up recovery interface with image size
        set_recovery_image_size(image_info.size)?;

        // Download image
        let mut stream = boot_source.download_image(firmware_id)?;

        // Stream image chunks to recovery interface
        load_image_stream(stream, ImageDestination::Recovery)?;
    }

    // 3. Finalize recovery
    boot_source.finalize()?;

    Ok(())
}

fn load_image_stream(mut stream: ImageStream, dest: ImageDestination) -> Result<(), Error> {
    let mut buffer = [0u8; 4096];
    while !stream.is_complete() {
        let bytes_read = stream.read_chunk(&mut buffer)?;
        if bytes_read > 0 {
            write_image_chunk(dest, &buffer[..bytes_read])?;
        }
    }
    Ok(())
}
}

Configuration File Format (TOC - Table of Contents)

During network boot, the coprocessor first downloads a Table of Contents (TOC) configuration file. This file maps firmware IDs to their corresponding filenames and metadata, allowing the boot process to locate and retrieve the correct firmware images. The TOC follows the same format used for FLASH storage, as described in the Flash Layout specification.

Network Stack Implementation

lwIP (Lightweight IP) will be used with Rust bindings/wrappers to support DHCP and TFTP

Repository: https://git.savannah.nongnu.org/cgit/lwip.git