Crypto & firmware parsing

On this page

The vRoT & secure boot chain relies on three supporting subsystems. This page catalogs each: its role, its key files, and where it plugs into authentication. These are smaller modules, so citations are given at directory level with line-level references for the load-bearing symbols.

Subsystem Directory Role in the trust chain
Crypto primitives src/nv/crypto/ AES-256-GCM and NIST SP 800-38F key wrap for provisioning secrets
Debug tokens src/nv/debugtoken/ Signed TLV tokens that unlock debug features / bypass key revocation
Firmware parsers src/nv/fw_parser/ Typed views over AP and MCU image metadata

Crypto primitives — src/nv/crypto/

A deliberately small crypto surface, layered over mbedTLS, used by provisioning and key-management flows rather than by image authentication directly (image auth uses SHA-384 + ECDSA P-384 inside the SPDM helper).

Key files

  • aes_gcm.cpp / aes_gcm.h — one-shot AES-256-GCM authenticated encryption. aes_256_gcm_encrypt (src/nv/crypto/aes_gcm.cpp:55) validates buffer sizes and non-overlap, then calls mbedtls_gcm_crypt_and_tag (src/nv/crypto/aes_gcm.cpp:82). Sizes are fixed by the header: 32-byte key, 12-byte IV, 16-byte tag (src/nv/crypto/aes_gcm.h:29). The caller owns nonce uniqueness (src/nv/crypto/aes_gcm.h:37).
  • key_wrap.cpp / key_wrap.h — NIST SP 800-38F KW (RFC 3394) wrap and unwrap with a 256-bit AES KEK. nist_sp800_38f_kw_wrap and nist_sp800_38f_kw_unwrap are declared at src/nv/crypto/key_wrap.h:50; the wrap prepends the 0xA6 RFC 3394 ICV (src/nv/crypto/key_wrap.cpp:49) and unwrap verifies it, zeroing the output and returning FailKeyUnwrapIntegrity on mismatch (src/nv/crypto/key_wrap.cpp:186). Both support in-place operation.
  • key_clear_guard.h — a scope guard that zeroes key material on destruction via a volatile write loop (clear_key, src/nv/crypto/key_clear_guard.h:25; KeyClearGuard, src/nv/crypto/key_clear_guard.h:33). It is used inside aes_256_gcm_encrypt to wipe the mbedTLS scratch buffer (src/nv/crypto/aes_gcm.cpp:70).

How it feeds vRoT / secure boot. These primitives back LPU provisioning — the wrapped UDS/CEK key material recorded in the LpuProvisionState bits (src/nv/vrot/platform/lpu.h:38). Return codes reuse the shared nv::spdm::crypto::CryptoStatus type, so a crypto failure surfaces through the same status vocabulary the secure-boot FSM already handles. Unit coverage lives alongside the sources (aes_gcm-test.cpp, key_wrap-test.cpp) under src/nv/crypto/.


Debug tokens — src/nv/debugtoken/

Debug tokens are signed, TLV-encoded blobs that authorize otherwise-locked capabilities: debug firmware, MCU debug, and CPLD unlock. They are the sanctioned bypass mechanism the authentication path consults.

Key files

  • debugtoken.h — the on-flash and TLV format constants. Token types are a bitmask enum Type (FlashDebugFw, McuDebug, CpldDebug; src/nv/debugtoken/debugtoken.h:62); the TLV container is tagged with the magic "TLV1" = 0x31564C54 (src/nv/debugtoken/debugtoken.h:87) and carries SHA-384 / ECDSA-P384 fields sized by HashSize (src/nv/debugtoken/debugtoken.h:76).
  • debugtoken_tlv.cpp — the parser and verifier. auth_token_tlv (src/nv/debugtoken/debugtoken_tlv.cpp:398) walks the TLV list with bounds checks, hashes everything except the signature TLV, and verifies it with spdm::crypto::spdm_ecdsa_verify (src/nv/debugtoken/debugtoken_tlv.cpp:560) — the same ECDSA routine used for image metadata. Supporting helpers include build_tlv_index (:258), read_tlv_field (:352), and the flash address lookup get_token_flash_address (:54).

How it feeds vRoT / secure boot. Two hooks connect it to the boot chain:

  1. Key-revocation bypass. During AP authentication, when a prod-signed image is required but the image is debug-signed, the SPDM helper calls nv::debugtoken::check_flash_debug_fw_token_for_ap (src/nv/debugtoken/debugtoken_tlv.cpp:1142) — a valid token lets the otherwise-revoked debug key through (src/nv/spdm/spdm_crypto_helper.cpp:198).
  2. Boot-time feature sync. When SecureBoot reaches BootComplete it calls sync_debug_token_features_on_boot (src/nv/debugtoken/debugtoken_tlv.cpp:1152, declared at src/nv/debugtoken/debugtoken.h:565), which locks the CPLD-unlock feature by default and re-enables it only if a cached, authorized CpldDebug token is present. It drives the hardware through the vRoT op nv::vrot::set_debug_token_feature (src/nv/vrot/interface/interface.h:76).

Interpretation. Debug tokens widen what a device will run, but only under their own ECDSA signature check — the bypass is itself authenticated, not a blanket override.


Firmware parsers — src/nv/fw_parser/

The parsers turn raw flash regions into typed, validated metadata views. They are split by processor: ap for the vRoT-managed application processor and mcu for the SMA’s own images.

AP parser — fw_parser_ap.cpp / fw_parser_ap.h

fw_parser_ap.h defines the wire structures the whole chain depends on: ApFwMetadata, a [[gnu::packed]] 4096-byte block asserted at compile time (src/nv/fw_parser/fw_parser_ap.h:86, :190). Inside it are the ECDSA P-384 NvSignature (src/nv/fw_parser/fw_parser_ap.h:39), the SHA-384 MetadataHashTableEntry table (src/nv/fw_parser/fw_parser_ap.h:74), and the verif_pub_key used to select the signing key. PublicKeyIndex (src/nv/fw_parser/fw_parser_ap.h:31) enumerates the debug/prod key slots, and ApFwStatus (src/nv/fw_parser/fw_parser_ap.h:8) is the status vocabulary the secure-boot FSM records.

The accessors are thin readers over the persisted verdict, not fresh flash hashers: get_ap_metadata_data (src/nv/fw_parser/fw_parser_ap.h:163) and its siblings all funnel through get_ap_metadata_data_from_flash (src/nv/fw_parser/fw_parser_ap.cpp:17), which returns data only when the stored ap_auth_result is Success or ApAuthInProgress (src/nv/fw_parser/fw_parser_ap.cpp:34). get_ap_signing_key_index (src/nv/fw_parser/fw_parser_ap.cpp:104) reports whether a slot was debug- or prod-signed by comparing verif_pub_key against the built-in ApFwPublicKeys (src/nv/fw_parser/fw_parser_ap.cpp:115).

MCU parser — fw_parser_mcu.cpp / fw_parser_mcu.h

A thin nv::-namespace facade over the corepdk sys::fw_parser::mcu implementation. get_fw_image_address maps a ParsingFwType (FMC, active, inactive, slot 0/1) to a flash address, resolving the current boot index for the slot cases (src/nv/fw_parser/fw_parser_mcu.cpp:6). The version and signing-key accessors — get_security_version, get_image_signing_key_version, get_firmware_version — simply forward to the sys:: layer (src/nv/fw_parser/fw_parser_mcu.cpp:25), declared at src/nv/fw_parser/fw_parser_mcu.h:45.

How it feeds vRoT / secure boot. The AP metadata structs are the shared contract between three parties: the SPDM helper writes a TbsData into the persisted AuthenticateData after verification, the secure-boot FSM stores and retries on it, and these accessors expose it to PLDM/inventory consumers — gated so unauthenticated slots never yield metadata. The MCU parser supplies the SMA’s own version/rollback data used elsewhere in the boot flow.


Flash format & key material

The parsers above read a fixed on-flash contract. Onboarding a new LPU-backed AP means defining that contract in three places: the slot geometry, the 4 KiB metadata block, and the AP image layout — with the SMA’s own CFPA region holding the key material that makes encrypted images work. This section is the on-flash view; the firmware parsers above are the typed readers over it.

Slot geometry

The LPU external SPI flash is split into two equal slots, each storing the AP image first and the 4 KiB metadata block at the end (src/nv/vrot/platform/lpu.h:98). The geometry is derived entirely from one project constant, NV_VROT_LPU_FLASH_SIZE_BYTES (src/nv/vrot/platform/lpu.h:101):

LPU SPI flash = NV_VROT_LPU_FLASH_SIZE_BYTES        (LpuFlashLayout, lpu.h:100)
  SlotCount = 2   SlotSize = FlashSize/2   MetadataRegionSize = 4096   ImageSize = SlotSize-4096

  Slot 0  (Active, ApSlotUseActive 0xFF)
    ├─ AP image     [0 .. SlotSize-4096)      partition Slot0Image    = 0
    └─ ApFwMetadata [SlotSize-4096 .. SlotSize)  partition Slot0Metadata = 1
  Slot 1  (Update, ApSlotUseUpdate 0xFE)
    ├─ AP image                                partition Slot1Image    = 2
    └─ ApFwMetadata                            partition Slot1Metadata = 3

Slot roles and the four partition indices are fixed at src/nv/vrot/interface/interface.h:32 and src/nv/vrot/platform/lpu.h:110. The two-slot split is what lets SecureBoot retry the other slot on failure and lets ap_background_copy mirror one into the other.

AP metadata block — ApFwMetadata (4096 B, signed)

The AP parser introduces the struct types; here is the security-critical offset map of the packed 4096-byte block (src/nv/fw_parser/fw_parser_ap.h:86). Everything up to the signature is the to-be-signed tbs_data:

Offset Field What it anchors
0–15 sec_version, image placement anti-rollback input + where the image lives
16–31 key_revocation_info revocation state
160–671 hash_table[8] — 8× MetadataHashTableEntry (64 B: offset, length, SHA-384 hash[48]) per-sub-image integrity (src/nv/fw_parser/fw_parser_ap.h:74)
672–767 verif_pub_key[96] selects the signing key (src/nv/fw_parser/fw_parser_ap.h:118)
951–1046 nv_signatureNvSignature (ECDSA P-384 r‖s, 48+48 B) authenticity of everything above (src/nv/fw_parser/fw_parser_ap.h:39, :149)
1047–4095 reserved_2[3049] (must be 0) padding to 4096 (src/nv/fw_parser/fw_parser_ap.h:152)

Also inside tbs_data: an AP SPI opcode allow-list and 8× region-attribute entries (MetadataRegionAttributeEntry, 12 B, RW/erase flags, src/nv/fw_parser/fw_parser_ap.h:56) that constrain what the AP may do to its own flash. So the signed block carries not just the image hashes but the AP’s flash-access policy.

AP image (LPU fw-data) — 16-byte block format

The image itself has its own format, defined in namespace nv::vrot::lpu (src/nv/vrot/platform/lpu_flash_layout.h). Everything is 16-byte blocks, little-endian, CRC-16 poly 0xA2EB, opened by a pointer block with magic 0x1F27 (src/nv/vrot/platform/lpu_flash_layout.h:84). The is_encrypted bit (EncryptedFlagMask, bit 0, src/nv/vrot/platform/lpu_flash_layout.h:92) selects one of two shapes:

Cleartext (is_encrypted == 0)
  Blk 0    PointerBlock : magic 0x1F27, num_spi_blocks, fw_idx, fw_size, uds_idx, enc=0, crc16
  Blk 1..N SPI records  : {spi_cmd, spi_addr, data...}   (AP flash-programming script)
  Blk N+1  SPI footer   : reserved + crc16
  idx..    FW image     : firmware payload (fw_size)
  end      FW footer    : reserved + crc16

Encrypted (is_encrypted != 0) — adds confidentiality material inline
  Blk 0      PointerBlock : ... uds_idx, enc=1, crc16
  Blk 1..N   SPI records
  Blk N+1    SPI footer   : spi_iv[96b]
  Blk N+2..4 WrappedKey   : UDS-KW-wrapped CEK (320b across 3 blocks)
  Blk N+5    SPI MAC      : spi_mac[128b]   (AES-GCM tag over the SPI script)
  idx        HeaderFw     : hdr_packed{fw_size[19:0], uds_idx[23:20], enc[31]} + fw_iv[96b]
  idx+1..    FW image     : ciphertext
  end        FW MAC       : fw_mac[128b]    (AES-GCM tag over the firmware)

The wire structs are size-asserted: PointerBlock = 16 B (src/nv/vrot/platform/lpu_flash_layout.h:118), WrappedKey = 48 B (src/nv/vrot/platform/lpu_flash_layout.h:124), HeaderFirmware = 16 B (src/nv/vrot/platform/lpu_flash_layout.h:134). On the first PLDM update chunk the MCU patches the pointer block, the wrapped CEK, and the FW header via patch_encrypted_fw_header (src/nv/vrot/platform/lpu_flash_layout.h:221) after parse_pointer_block validates the magic/CRC and region containment (src/nv/vrot/platform/lpu_flash_layout.h:208).

Interpretation. The spi_mac / fw_mac blocks are the AES-GCM tags, and the CEK that decrypts the firmware travels UDS-wrapped inside the image — it is only usable on a device whose CFPA holds the matching UDS (below). An encrypted image is therefore bound to devices provisioned with that UDS, not portable.

CFPA key material (on the SMA/MCU side)

The keys that make encrypted images work live PUF-wrapped in the MCU’s CFPA customer region, laid out as CfpaCustomerData (src/nv/vrot/platform/lpu.cpp:632):

CfpaCustomerData (MCU CFPA customer region)
  ├─ WrappedUds  96 B  → PUF-wrapped UDS (plaintext UDS = 32 B) + valid marker
  ├─ WrappedCek  96 B  → PUF-wrapped CEK (plaintext CEK = 32 B) + valid marker
  └─ EcId        16 B  → endpoint/chip id (8 B) + valid marker
  • UDS (Unique Device Secret), 32 B, is TRNG-generated with an invalid-pattern reject loop — generate_valid_uds_key (src/nv/vrot/platform/lpu.cpp:695) over trng_generate (src/nv/vrot/platform/lpu.cpp:699).
  • CEK (Code Encryption Key), 32 B, is wrapped by the UDS via the NIST SP 800-38F KW primitive from Crypto primitives (32 B → 40 B wrapped, src/nv/vrot/platform/lpu.cpp:576).
  • Each CFPA blob is PUF-wrapped to 84 B and stored in a 96-B slot with a valid marker (UdsWrappedKeySize / CekWrappedKeySize = 84, src/nv/vrot/platform/lpu.cpp:600).
  • The composed presence of these keys becomes the LpuProvisionState bitflag (src/nv/vrot/platform/lpu.h:38) written to NPDS once per boot.

Interpretation. Two independent key hierarchies meet here. Authentication uses the SMA-baked ECDSA-P384 keys against the signed metadata (§ metadata block) and needs no per-device secret. Confidentiality uses the per-device, PUF-protected UDS→CEK chain in CFPA. A device can authenticate an AP image it cannot decrypt, and vice-versa — the two are orthogonal.


Where to go next


Built with Just the Docs. Source-traced against NVIDIA/OpenSMA at 9b183d3 (v02.0020.0000). Methodology under appendices/trace-methodology.

This site uses Just the Docs, a documentation theme for Jekyll.