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 callsmbedtls_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_wrapandnist_sp800_38f_kw_unwrapare declared atsrc/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 returningFailKeyUnwrapIntegrityon 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 avolatilewrite loop (clear_key,src/nv/crypto/key_clear_guard.h:25;KeyClearGuard,src/nv/crypto/key_clear_guard.h:33). It is used insideaes_256_gcm_encryptto 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 enumType(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 byHashSize(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 withspdm::crypto::spdm_ecdsa_verify(src/nv/debugtoken/debugtoken_tlv.cpp:560) — the same ECDSA routine used for image metadata. Supporting helpers includebuild_tlv_index(:258),read_tlv_field(:352), and the flash address lookupget_token_flash_address(:54).
How it feeds vRoT / secure boot. Two hooks connect it to the boot chain:
- 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). - Boot-time feature sync. When SecureBoot reaches
BootCompleteit callssync_debug_token_features_on_boot(src/nv/debugtoken/debugtoken_tlv.cpp:1152, declared atsrc/nv/debugtoken/debugtoken.h:565), which locks the CPLD-unlock feature by default and re-enables it only if a cached, authorizedCpldDebugtoken is present. It drives the hardware through the vRoT opnv::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_signature — NvSignature (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_macblocks 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) overtrng_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
validmarker (UdsWrappedKeySize/CekWrappedKeySize= 84,src/nv/vrot/platform/lpu.cpp:600). - The composed presence of these keys becomes the
LpuProvisionStatebitflag (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
- vRoT & secure boot — the state machine that consumes all of the above.
- Security & Root-of-Trust overview — section map.
- Build system — how
src/nv/crypto/links against mbedTLS and how per-project config selects the parsers that compile in.