earlgrey: add the `Manifest` structure Signed-off-by: Chris Frantz <cfrantz@google.com>
diff --git a/target/earlgrey/util/BUILD.bazel b/target/earlgrey/util/BUILD.bazel index 773892c..a301150 100644 --- a/target/earlgrey/util/BUILD.bazel +++ b/target/earlgrey/util/BUILD.bazel
@@ -1,7 +1,7 @@ # Licensed under the Apache-2.0 license # SPDX-License-Identifier: Apache-2.0 -load("@rules_rust//rust:defs.bzl", "rust_library") +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") load("//target/earlgrey:defs.bzl", "TARGET_COMPATIBLE_WITH") rust_library( @@ -14,6 +14,7 @@ "error.rs", "flash.rs", "lib.rs", + "manifest.rs", "misc.rs", "mubi.rs", "perso_tlv.rs", @@ -24,7 +25,6 @@ ], crate_name = "earlgrey_util", edition = "2024", - target_compatible_with = TARGET_COMPATIBLE_WITH, visibility = ["//visibility:public"], deps = [ "//hal/blocking/flash:driver", @@ -39,6 +39,11 @@ ], ) +rust_test( + name = "util_test", + crate = ":util", +) + rust_library( name = "dfu", srcs = ["dfu.rs"],
diff --git a/target/earlgrey/util/README.md b/target/earlgrey/util/README.md index 5579ab3..53ad15b 100644 --- a/target/earlgrey/util/README.md +++ b/target/earlgrey/util/README.md
@@ -18,9 +18,10 @@ * Owner-specific persistent storage space (2KiB). * **`mut_ref()`**: Unsafely retrieves a mutable reference to the physical Retention RAM address (requires identity mapping in system configuration). -### 3. Boot Stage Structures (`boot_log.rs`, `boot_svc.rs`) +### 3. Boot Stage Structures (`boot_log.rs`, `boot_svc.rs`, `manifest.rs`) * **`BootLog`**: Populated by ROM/ROM_EXT, providing details about the boot process (selected boot slot, versions, sizes, ownership states, and minimum security versions). * **`BootSvc`**: Structure for the Boot Services protocol. It allows the running application to request actions (like changing the next boot slot, upgrading minimum security versions, or unlocking/transferring ownership) that are executed upon the next reboot. +* **`Manifest`**: Structure for flash boot stage images. Placed at the start of ROM_EXT and BL0 images to describe versions, security requirements, code region bounds, and cryptographic signatures (ECDSA-P256 and optional SPHINCS+). * **`CheckDigest`**: Trait implemented by `BootLog` and `BootSvc` to validate SHA256 integrity digests over their contents. OpenTitan uses a reversed-byte order for digests stored in these structures. * **`GetData<T>`**: Trait and helper macros to safely extract typed command and response payloads from the generic `BootSvc` structure's data region.
diff --git a/target/earlgrey/util/lib.rs b/target/earlgrey/util/lib.rs index 178136f..159c611 100644 --- a/target/earlgrey/util/lib.rs +++ b/target/earlgrey/util/lib.rs
@@ -9,6 +9,7 @@ pub mod device_id; pub mod error; pub mod flash; +pub mod manifest; mod misc; mod mubi; mod perso_tlv;
diff --git a/target/earlgrey/util/manifest.rs b/target/earlgrey/util/manifest.rs new file mode 100644 index 0000000..be62075 --- /dev/null +++ b/target/earlgrey/util/manifest.rs
@@ -0,0 +1,437 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Target-side Rust `zerocopy` structures and constants for OpenTitan manifests. +//! +//! This module provides a Rust translation of the structures defined in +//! `sw/device/silicon_creator/lib/manifest.h` and related headers in the OpenTitan +//! repository. These structures are used to parse and validate boot stage images +//! stored in flash. +//! +//! Note: All structures assume 4-byte (word) alignment in memory, which is the +//! standard alignment for flash images on OpenTitan. + +use core::mem::size_of; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +use crate::rom_error::RomError; +use crate::tags::{HardenedBool, ManifestIdentifier}; + +/// Size of the manifest structure in bytes. +pub const CHIP_MANIFEST_SIZE: usize = 1024; + +/// Number of entries in the manifest extensions table. +pub const CHIP_MANIFEST_EXT_TABLE_ENTRY_COUNT: usize = 15; + +/// Alias for `CHIP_MANIFEST_EXT_TABLE_ENTRY_COUNT` used in table sizing. +pub const MANIFEST_EXT_TABLE_COUNT: usize = CHIP_MANIFEST_EXT_TABLE_ENTRY_COUNT; + +/// Value to use for unselected usage constraint words. +pub const MANIFEST_USAGE_CONSTRAINT_UNSELECTED_WORD_VAL: u32 = 0xA5A5A5A5; + +/// Manifest format major and minor versions. +pub const MANIFEST_VERSION_MAJOR_1: u16 = 0x71c3; +pub const MANIFEST_VERSION_MAJOR_2: u16 = 0x0002; +pub const MANIFEST_VERSION_MINOR_1: u16 = 0x6c47; + +/// Extension identifiers. +pub const MANIFEST_EXT_ID_SPX_KEY: u32 = 0x94ac01ec; +pub const MANIFEST_EXT_ID_SPX_SIGNATURE: u32 = 0xad77f84a; +pub const MANIFEST_EXT_ID_SECVER_WRITE: u32 = 0x3f086a41; +pub const MANIFEST_EXT_ID_ISFB: u32 = 0x42465349; +pub const MANIFEST_EXT_ID_ISFB_ERASE: u32 = 0x45465349; +pub const MANIFEST_EXT_ID_OWNER_TRANSFER_BLOB: u32 = 0x4254574f; + +/// Extension names (ASCII tags for debugging). +pub const MANIFEST_EXT_NAME_SPX_KEY: u32 = 0x30545845; // 'EXT0' +pub const MANIFEST_EXT_NAME_SPX_SIGNATURE: u32 = 0x31545845; // 'EXT1' +pub const MANIFEST_EXT_NAME_SECVER_WRITE: u32 = 0x56434553; // 'SECV' +pub const MANIFEST_EXT_NAME_ISFB: u32 = 0x42465349; // 'ISFB' +pub const MANIFEST_EXT_NAME_ISFB_ERASE: u32 = 0x45465349; // 'ISFE' +pub const MANIFEST_EXT_NAME_OWNER_TRANSFER_BLOB: u32 = 0x4254574f; // 'OWTB' + +/// `selector_bits` bit indices for usage constraints fields. +pub const MANIFEST_SELECTOR_BIT_DEVICE_ID_FIRST: u32 = 0; +pub const MANIFEST_SELECTOR_BIT_DEVICE_ID_LAST: u32 = 7; +pub const MANIFEST_SELECTOR_BIT_MANUF_STATE_CREATOR: u32 = 8; +pub const MANIFEST_SELECTOR_BIT_MANUF_STATE_OWNER: u32 = 9; +pub const MANIFEST_SELECTOR_BIT_LIFE_CYCLE_STATE: u32 = 10; + +/// Buffer of reserved bytes, padded with `0xa5` by default to match OpenTitan +/// image layout specifications. +#[repr(C)] +#[derive(Clone, Copy, Immutable, IntoBytes, FromBytes, KnownLayout, PartialEq, Eq)] +pub struct ReservedBuffer<const COUNT: usize> { + a: [u8; COUNT], +} + +impl<const COUNT: usize> Default for ReservedBuffer<COUNT> { + #[inline(always)] + fn default() -> Self { + Self { + a: core::array::from_fn(|_| 0xa5), + } + } +} + +/// Manifest format version (major and minor). +#[repr(C)] +#[derive( + Clone, Copy, Immutable, IntoBytes, FromBytes, KnownLayout, Default, Debug, PartialEq, Eq, +)] +pub struct ManifestVersion { + pub minor: u16, + pub major: u16, +} + +/// Manifest timestamp (Unix epoch seconds). +#[repr(C)] +#[derive( + Clone, Copy, Immutable, IntoBytes, FromBytes, KnownLayout, Default, Debug, PartialEq, Eq, +)] +pub struct Timestamp { + pub timestamp_low: u32, + pub timestamp_high: u32, +} + +/// Binding value used by the key manager to derive secret values. +#[repr(C)] +#[derive(Clone, Copy, Immutable, IntoBytes, FromBytes, KnownLayout, Default, PartialEq, Eq)] +pub struct KeymgrBindingValue { + pub data: [u32; 8], +} + +/// The 256-bit device identifier stored in the `HW_CFG0` partition in OTP. +#[repr(C)] +#[derive(Clone, Copy, Immutable, IntoBytes, FromBytes, KnownLayout, Default, PartialEq, Eq)] +pub struct LifecycleDeviceId { + pub device_id: [u32; 8], +} + +/// Holds an ECDSA-P256 public key. +#[repr(C)] +#[derive(Clone, Copy, Immutable, IntoBytes, FromBytes, KnownLayout, Default, PartialEq, Eq)] +pub struct EcdsaP256PublicKey { + pub x: [u32; 8], + pub y: [u32; 8], +} + +/// Holds an ECDSA-P256 signature. +#[repr(C)] +#[derive(Clone, Copy, Immutable, IntoBytes, FromBytes, KnownLayout, Default, PartialEq, Eq)] +pub struct EcdsaP256Signature { + pub r: [u32; 8], + pub s: [u32; 8], +} + +/// Holds an SPX (SPHINCS+) signature. +#[repr(C)] +#[derive(Clone, Immutable, IntoBytes, FromBytes, KnownLayout, PartialEq, Eq)] +pub struct SigverifySpxSignature { + pub data: [u32; 1964], +} + +impl Default for SigverifySpxSignature { + #[inline(always)] + fn default() -> Self { + Self { data: [0; 1964] } + } +} + +/// Holds an SPX (SPHINCS+) public key. +#[repr(C)] +#[derive(Clone, Copy, Immutable, IntoBytes, FromBytes, KnownLayout, Default, PartialEq, Eq)] +pub struct SigverifySpxKey { + pub data: [u32; 8], +} + +/// Usage constraints applied to the boot stage image. +#[repr(C)] +#[derive(Clone, Immutable, IntoBytes, FromBytes, KnownLayout, PartialEq, Eq)] +pub struct ManifestUsageConstraints { + pub selector_bits: u32, + pub device_id: LifecycleDeviceId, + pub manuf_state_creator: u32, + pub manuf_state_owner: u32, + pub life_cycle_state: u32, +} + +impl Default for ManifestUsageConstraints { + #[inline(always)] + fn default() -> Self { + Self { + selector_bits: 0, + device_id: LifecycleDeviceId { + device_id: [MANIFEST_USAGE_CONSTRAINT_UNSELECTED_WORD_VAL; 8], + }, + manuf_state_creator: MANIFEST_USAGE_CONSTRAINT_UNSELECTED_WORD_VAL, + manuf_state_owner: MANIFEST_USAGE_CONSTRAINT_UNSELECTED_WORD_VAL, + life_cycle_state: MANIFEST_USAGE_CONSTRAINT_UNSELECTED_WORD_VAL, + } + } +} + +/// An entry in the manifest extensions table. +#[repr(C)] +#[derive( + Clone, Copy, Immutable, IntoBytes, FromBytes, KnownLayout, Default, Debug, PartialEq, Eq, +)] +pub struct ManifestExtTableEntry { + pub identifier: u32, + pub offset: u32, +} + +/// The table of manifest extensions. +#[repr(C)] +#[derive(Clone, Immutable, IntoBytes, FromBytes, KnownLayout, Default, PartialEq, Eq)] +pub struct ManifestExtTable { + pub entries: [ManifestExtTableEntry; MANIFEST_EXT_TABLE_COUNT], +} + +/// The common header for all manifest extensions. +#[repr(C)] +#[derive( + Clone, Copy, Immutable, IntoBytes, FromBytes, KnownLayout, Default, Debug, PartialEq, Eq, +)] +pub struct ManifestExtHeader { + pub identifier: u32, + pub name: u32, +} + +/// Manifest extension: SPX public key. +#[repr(C)] +#[derive(Clone, Immutable, IntoBytes, FromBytes, KnownLayout, Default, PartialEq, Eq)] +pub struct ManifestExtSpxKey { + pub header: ManifestExtHeader, + pub key: SigverifySpxKey, +} + +/// Manifest extension: SPX signature. +#[repr(C)] +#[derive(Clone, Immutable, IntoBytes, FromBytes, KnownLayout, Default, PartialEq, Eq)] +pub struct ManifestExtSpxSignature { + pub header: ManifestExtHeader, + pub signature: SigverifySpxSignature, +} + +/// Manifest extension: Security Version Write enable flag. +#[repr(C)] +#[derive(Clone, Copy, Immutable, IntoBytes, FromBytes, KnownLayout, PartialEq, Eq)] +pub struct ManifestExtSecVerWrite { + pub header: ManifestExtHeader, + pub write: HardenedBool, +} + +/// Manifest extension: ISFB Erase Policy enable flag. +#[repr(C)] +#[derive(Clone, Copy, Immutable, IntoBytes, FromBytes, KnownLayout, PartialEq, Eq)] +pub struct ManifestExtIsfbErase { + pub header: ManifestExtHeader, + pub erase_allowed: HardenedBool, +} + +/// Integrator Specific Firmware Binding (ISFB) product expression. +#[repr(C)] +#[derive( + Clone, Copy, Immutable, IntoBytes, FromBytes, KnownLayout, Default, Debug, PartialEq, Eq, +)] +pub struct ManifestExtProductExpr { + pub mask: u32, + pub value: u32, +} + +/// Manifest extension: Integrator Specific Firmware Binding (ISFB). +/// +/// This structure covers the FIXED part of the extension. Use `product_exprs` +/// or `parse_full` to access the variable-length `product_expr` array that follows it. +#[repr(C)] +#[derive(Clone, Immutable, IntoBytes, FromBytes, KnownLayout, Default, PartialEq, Eq)] +pub struct ManifestExtIsfb { + pub header: ManifestExtHeader, + pub strike_mask: [u32; 4], + pub product_expr_count: u32, +} + +impl ManifestExtIsfb { + /// Gets a slice of the `ManifestExtProductExpr` array following the fixed part, + /// given the remainder of the byte slice for this extension. + pub fn product_exprs<'a>( + &self, + remainder: &'a [u8], + ) -> Option<(&'a [ManifestExtProductExpr], &'a [u8])> { + let count = self.product_expr_count as usize; + <[ManifestExtProductExpr]>::ref_from_prefix_with_elems(remainder, count).ok() + } + + /// Parses the fixed part and the variable-length product expression array + /// from a single byte slice sized to this extension. + pub fn parse_full(ext_bytes: &[u8]) -> Option<(&Self, &[ManifestExtProductExpr], &[u8])> { + let (fixed, remainder) = Self::ref_from_prefix(ext_bytes).ok()?; + let count = fixed.product_expr_count as usize; + let (exprs, suffix) = + <[ManifestExtProductExpr]>::ref_from_prefix_with_elems(remainder, count).ok()?; + Some((fixed, exprs, suffix)) + } +} + +/// Manifest extension: Owner Transfer Blob. +/// +/// This structure covers the FIXED part of the extension. The detached signature +/// (if present) immediately follows this structure in flash. +#[repr(C)] +#[derive(Clone, Immutable, IntoBytes, FromBytes, KnownLayout, PartialEq, Eq)] +pub struct ManifestExtOwnerTransferBlob { + pub header: ManifestExtHeader, + pub owner_block: [u8; 2048], +} + +impl ManifestExtOwnerTransferBlob { + /// Parses the fixed part and returns the remaining bytes as the detached signature + /// (assuming `ext_bytes` is bounded to this extension's actual length). + pub fn parse_full(ext_bytes: &[u8]) -> Option<(&Self, &[u8])> { + let (fixed, remainder) = Self::ref_from_prefix(ext_bytes).ok()?; + Some((fixed, remainder)) + } +} + +/// The Manifest structure for boot stage images stored in flash. +#[repr(C)] +#[derive(Clone, Immutable, IntoBytes, FromBytes, KnownLayout)] +pub struct Manifest { + /// ECDSA P256 signature of the image. + pub ecdsa_signature: EcdsaP256Signature, + /// Reserved space for signature padding. + pub reserved_signature: ReservedBuffer<160>, + /// Reserved space (unsigned region). + pub reserved_unsigned: ReservedBuffer<160>, + /// Usage constraints applied to this image. + pub usage_constraints: ManifestUsageConstraints, + /// Signer's ECDSA NIST P256 public key. + pub ecdsa_public_key: EcdsaP256PublicKey, + /// Reserved space for public key padding. + pub reserved_public_key: ReservedBuffer<160>, + /// Reserved space. + pub reserved: ReservedBuffer<156>, + /// Address where the manifest is expected to be loaded. + pub manifest_base_address: u32, + /// Address translation (hardened boolean). + pub address_translation: u32, + /// Manifest identifier ('OTRE' or 'OTB0'). + pub identifier: ManifestIdentifier, + /// Manifest format major and minor version. + pub manifest_version: ManifestVersion, + /// Offset of the end of the signed region relative to the start of the manifest. + pub signed_region_end: u32, + /// Length of the image including the manifest in bytes. + pub length: u32, + /// Image major version. + pub version_major: u32, + /// Image minor version. + pub version_minor: u32, + /// Security version of the image for anti-rollback. + pub security_version: u32, + /// Image timestamp. + pub timestamp: Timestamp, + /// Binding value used by key manager. + pub binding_value: KeymgrBindingValue, + /// Maximum allowed version for keys at the next boot stage. + pub max_key_version: u32, + /// Offset of the start of the executable region in bytes. + pub code_start: u32, + /// Offset of the end of the executable region (exclusive) in bytes. + pub code_end: u32, + /// Offset of the entry point in bytes. + pub entry_point: u32, + /// Table of manifest extensions. + pub extensions: ManifestExtTable, +} + +impl Manifest { + /// Validates the structure and offsets of the manifest. + /// + /// This function implements the checks from `manifest_check` in OpenTitan's + /// `manifest.h`. + /// TODO: consider evolving this function to add addtional heuristics. + pub fn check(&self) -> Result<(), RomError> { + if self.manifest_version.major != MANIFEST_VERSION_MAJOR_2 { + return Err(RomError::ManifestBadVersionMajor); + } + + if self.signed_region_end > self.length { + return Err(RomError::ManifestBadSignedRegion); + } + + if self.code_start >= self.code_end + || (self.code_start as usize) < size_of::<Self>() + || self.code_end > self.signed_region_end + || (self.code_start & 0x3) != 0 + || (self.code_end & 0x3) != 0 + { + return Err(RomError::ManifestBadCodeRegion); + } + + if self.entry_point < self.code_start + || self.entry_point >= self.code_end + || (self.entry_point & 0x3) != 0 + { + return Err(RomError::ManifestBadEntryPoint); + } + + for i in 0..MANIFEST_EXT_TABLE_COUNT { + if (self.extensions.entries[i].offset & 0x3) != 0 { + return Err(RomError::ManifestBadExtension); + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use core::mem::{offset_of, size_of}; + + #[test] + fn test_manifest_layout() { + assert_eq!(offset_of!(Manifest, ecdsa_signature), 0); + assert_eq!(offset_of!(Manifest, reserved_signature), 64); + assert_eq!(offset_of!(Manifest, reserved_unsigned), 224); + assert_eq!(offset_of!(Manifest, usage_constraints), 384); + assert_eq!(offset_of!(Manifest, ecdsa_public_key), 432); + assert_eq!(offset_of!(Manifest, reserved_public_key), 496); + assert_eq!(offset_of!(Manifest, reserved), 656); + assert_eq!(offset_of!(Manifest, manifest_base_address), 812); + assert_eq!(offset_of!(Manifest, address_translation), 816); + assert_eq!(offset_of!(Manifest, identifier), 820); + assert_eq!(offset_of!(Manifest, manifest_version), 824); + assert_eq!(offset_of!(Manifest, signed_region_end), 828); + assert_eq!(offset_of!(Manifest, length), 832); + assert_eq!(offset_of!(Manifest, version_major), 836); + assert_eq!(offset_of!(Manifest, version_minor), 840); + assert_eq!(offset_of!(Manifest, security_version), 844); + assert_eq!(offset_of!(Manifest, timestamp), 848); + assert_eq!(offset_of!(Manifest, binding_value), 856); + assert_eq!(offset_of!(Manifest, max_key_version), 888); + assert_eq!(offset_of!(Manifest, code_start), 892); + assert_eq!(offset_of!(Manifest, code_end), 896); + assert_eq!(offset_of!(Manifest, entry_point), 900); + assert_eq!(offset_of!(Manifest, extensions), 904); + assert_eq!(size_of::<Manifest>(), CHIP_MANIFEST_SIZE); + } + + #[test] + fn test_isfb_layout() { + assert_eq!(offset_of!(ManifestExtIsfb, header), 0); + assert_eq!(offset_of!(ManifestExtIsfb, strike_mask), 8); + assert_eq!(offset_of!(ManifestExtIsfb, product_expr_count), 24); + assert_eq!(size_of::<ManifestExtIsfb>(), 28); + } + + #[test] + fn test_owner_transfer_blob_layout() { + assert_eq!(offset_of!(ManifestExtOwnerTransferBlob, header), 0); + assert_eq!(offset_of!(ManifestExtOwnerTransferBlob, owner_block), 8); + assert_eq!(size_of::<ManifestExtOwnerTransferBlob>(), 2056); + } +}