From 6da83e1bcf9430aede91ca5105b26e20e89bc9a8 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Thu, 8 Oct 2026 16:02:55 +0000 Subject: [PATCH 1/5] cosi: add v1.3 spec with optional verity signature partition Introduces COSI metadata specification version 1.3, adding an optional `signature` field to `VerityMetadata` that points at an ImageFile holding a detached PKCS#7/DER signature of the verity root hash. - `KnownMetadataVersion::V1_3` documents the new version. - `filesystem_image_files()` includes the signature file so existing V1_2 partition-matching/size/hash validation covers it automatically. - `derive_host_configuration_inner` derives `VerityDevice.signature_device_id` by resolving the signature image's partition, linking the COSI image to the new HostConfig API field from the previous PR in this stack. - `OsImageVerityHash.signature_image_file` carries the mapped file through to the engine layer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/trident/src/osimage/cosi/derived_hc.rs | 22 +++++++++++++- crates/trident/src/osimage/cosi/metadata.rs | 29 ++++++++++++++++++- crates/trident/src/osimage/cosi/mod.rs | 7 +++++ crates/trident/src/osimage/mock.rs | 2 ++ crates/trident/src/osimage/mod.rs | 1 + 5 files changed, 59 insertions(+), 2 deletions(-) diff --git a/crates/trident/src/osimage/cosi/derived_hc.rs b/crates/trident/src/osimage/cosi/derived_hc.rs index 622d62bb9e..60d4f46b9d 100644 --- a/crates/trident/src/osimage/cosi/derived_hc.rs +++ b/crates/trident/src/osimage/cosi/derived_hc.rs @@ -110,12 +110,29 @@ pub(super) fn derive_host_configuration_inner( ), }; + // Optionally, get the id of the signature partition, if present. + let hash_signature_device_id = verity_device + .signature + .as_ref() + .map(|signature| { + partition_ids_by_file + .get(signature.path.as_path()) + .cloned() + .with_context(|| { + format!( + "Failed to find signature partition for verity device: {}", + signature.path.display() + ) + }) + }) + .transpose()?; + verity.push(VerityDevice { id: verity_id.clone(), name: verity_name, data_device_id: partition_id.clone(), hash_device_id: hash_partition_id.clone(), - hash_signature_device_id: None, + hash_signature_device_id, corruption_option: Default::default(), }); @@ -727,6 +744,7 @@ mod tests { verity: Some(VerityMetadata { file: sample_image_file("images/root-hash.img.zst"), roothash: "abcd1234".to_string(), + signature: None, }), }; @@ -740,6 +758,7 @@ mod tests { verity: Some(VerityMetadata { file: sample_image_file("images/usr-hash.img.zst"), roothash: "efgh5678".to_string(), + signature: None, }), }; @@ -876,6 +895,7 @@ mod tests { verity: Some(VerityMetadata { file: sample_image_file("images/var-hash.img.zst"), roothash: "badhash".to_string(), + signature: None, }), }; diff --git a/crates/trident/src/osimage/cosi/metadata.rs b/crates/trident/src/osimage/cosi/metadata.rs index dad49ff561..10b9a73063 100644 --- a/crates/trident/src/osimage/cosi/metadata.rs +++ b/crates/trident/src/osimage/cosi/metadata.rs @@ -32,6 +32,12 @@ pub(super) enum KnownMetadataVersion { /// /// Introduces partition metadata. V1_2, + + /// COSI metadata specification version 1.3. + /// + /// Introduces an optional dm-verity root hash signature partition. + #[allow(dead_code)] + V1_3, } impl KnownMetadataVersion { @@ -40,6 +46,7 @@ impl KnownMetadataVersion { Self::V1_0 => MetadataVersion { major: 1, minor: 0 }, Self::V1_1 => MetadataVersion { major: 1, minor: 1 }, Self::V1_2 => MetadataVersion { major: 1, minor: 2 }, + Self::V1_3 => MetadataVersion { major: 1, minor: 3 }, } } } @@ -167,7 +174,11 @@ impl CosiMetadata { // Get a flattened iterator over the image files and their verity files // (if any) .flat_map(|fs| { - iter::once(&fs.file).chain(fs.verity.as_ref().map(|verity| &verity.file)) + iter::once(&fs.file).chain( + fs.verity.as_ref().into_iter().flat_map(|verity| { + iter::once(&verity.file).chain(verity.signature.as_ref()) + }), + ) }) } } @@ -266,6 +277,13 @@ pub(crate) struct VerityMetadata { pub file: ImageFile, pub roothash: String, + + /// An optional detached PKCS#7 signature of the root hash, stored in its + /// own dedicated partition. + /// + /// Introduced in COSI metadata specification version 1.3. + #[serde(default)] + pub signature: Option, } #[derive(Debug, Deserialize, Clone, Eq, PartialEq)] @@ -481,6 +499,15 @@ mod tests { assert_invalid_version(r#""hello there""#); } + #[test] + fn test_known_metadata_version_as_version() { + assert_eq!( + KnownMetadataVersion::V1_3.as_version(), + MetadataVersion { major: 1, minor: 3 } + ); + assert!(KnownMetadataVersion::V1_3.as_version() > KnownMetadataVersion::V1_2.as_version()); + } + fn mock_image_file() -> ImageFile { ImageFile { path: PathBuf::from("/path/to/image"), diff --git a/crates/trident/src/osimage/cosi/mod.rs b/crates/trident/src/osimage/cosi/mod.rs index e831451d3b..6c47aec51d 100644 --- a/crates/trident/src/osimage/cosi/mod.rs +++ b/crates/trident/src/osimage/cosi/mod.rs @@ -532,6 +532,12 @@ fn cosi_image_to_os_image_filesystem(image: &metadata::Image) -> OsImageFileSyst path: verity.file.path, }, roothash: verity.roothash, + signature_image_file: verity.signature.map(|signature| OsImageFile { + compressed_size: signature.compressed_size, + sha384: signature.sha384, + uncompressed_size: signature.uncompressed_size, + path: signature.path, + }), }), } } @@ -1164,6 +1170,7 @@ mod tests { sha384: Sha384Hash::from(format!("{:x}", Sha384::digest(verity_data.as_bytes()))), }, roothash: root_hash.to_string(), + signature: None, }); let os_fs = cosi_image_to_os_image_filesystem(&cosi_img); diff --git a/crates/trident/src/osimage/mock.rs b/crates/trident/src/osimage/mock.rs index 5c9714cb6f..7789304e58 100644 --- a/crates/trident/src/osimage/mock.rs +++ b/crates/trident/src/osimage/mock.rs @@ -172,6 +172,7 @@ impl MockOsImage { verity: esp_img.verity.as_ref().map(|verity| OsImageVerityHash { roothash: verity.roothash.clone(), hash_image_file: mock_os_image_file(), + signature_image_file: None, }), }) } else { @@ -193,6 +194,7 @@ impl MockOsImage { verity: image.verity.as_ref().map(|verity| OsImageVerityHash { roothash: verity.roothash.clone(), hash_image_file: mock_os_image_file(), + signature_image_file: None, }), }) } diff --git a/crates/trident/src/osimage/mod.rs b/crates/trident/src/osimage/mod.rs index 4d8f9229db..3861955b61 100644 --- a/crates/trident/src/osimage/mod.rs +++ b/crates/trident/src/osimage/mod.rs @@ -337,6 +337,7 @@ pub struct GptPartitionInfo { pub struct OsImageVerityHash { pub roothash: String, pub hash_image_file: OsImageFile, + pub signature_image_file: Option, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)] From 4d56ff5530b9062c80dc6b1a4b09dfed24ef048e Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Thu, 8 Oct 2026 20:27:33 +0000 Subject: [PATCH 2/5] cosi: publish v1.3 spec/schema and test signature conversion Addresses review feedback on PR #835: - Document the COSI 1.3 revision (signature field on VerityConfig) in Composable-OS-Image.md, with a changelog entry and revision summary row. - Publish cosi-metadata-v1.3.schema.json alongside matching valid/invalid samples validated by the existing schema-validation workflow. - Add a derive_host_configuration_inner test exercising a non-None VerityMetadata.signature, asserting the derived hash_signature_device_id resolves to the correct partition. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/trident/src/osimage/cosi/derived_hc.rs | 146 ++++++++ docs/Reference/Composable-OS-Image.md | 15 +- .../cosi-metadata-v1.3.schema.json | 321 ++++++++++++++++++ .../v1.3/invalid/signature-wrong-type.json | 102 ++++++ .../v1.3/invalid/wrong-version.json | 95 ++++++ .../v1.3/valid/grub-simple.json | 95 ++++++ .../v1.3/valid/verity-uki-systemd-boot.json | 107 ++++++ 7 files changed, 877 insertions(+), 4 deletions(-) create mode 100644 docs/Reference/Composable-OS-Image/cosi-metadata-v1.3.schema.json create mode 100644 tests/cosi/metadata_samples/v1.3/invalid/signature-wrong-type.json create mode 100644 tests/cosi/metadata_samples/v1.3/invalid/wrong-version.json create mode 100644 tests/cosi/metadata_samples/v1.3/valid/grub-simple.json create mode 100644 tests/cosi/metadata_samples/v1.3/valid/verity-uki-systemd-boot.json diff --git a/crates/trident/src/osimage/cosi/derived_hc.rs b/crates/trident/src/osimage/cosi/derived_hc.rs index 60d4f46b9d..f33f8b4ba2 100644 --- a/crates/trident/src/osimage/cosi/derived_hc.rs +++ b/crates/trident/src/osimage/cosi/derived_hc.rs @@ -857,6 +857,152 @@ mod tests { ); } + /// Tests [`derive_host_configuration_inner`] with a verity device whose root + /// hash signature is stored on its own dedicated partition. + /// + /// Verifies that when the COSI metadata's verity entry carries a `signature` + /// image file, the derived `VerityDevice.hash_signature_device_id` correctly + /// resolves to the partition ID backing that signature image, while a + /// sibling verity device without a signature leaves the field unset. + #[test] + fn test_derive_host_configuration_inner_with_verity_signature() { + let (raw_gpt, disk_size, lba_size) = create_mock_gpt_disk_typed(&[ + ("esp", 64 * 1024, gpt::partition_types::EFI), + ("root", 256 * 1024, gpt::partition_types::LINUX_FS), + ("root-hash", 32 * 1024, gpt::partition_types::LINUX_FS), + ("root-hash-sig", 8 * 1024, gpt::partition_types::LINUX_FS), + ("usr", 256 * 1024, gpt::partition_types::LINUX_FS), + ("usr-hash", 32 * 1024, gpt::partition_types::LINUX_FS), + ]); + + let disk_info = DiskInfo { + size: disk_size, + lba_size, + partition_table_type: PartitionTableType::Gpt, + gpt_regions: vec![ + GptDiskRegion { + image: sample_image_file("gpt_primary.zst"), + region_type: GptRegionType::PrimaryGpt, + }, + GptDiskRegion { + image: sample_image_file("images/esp.img.zst"), + region_type: GptRegionType::Partition { number: 1 }, + }, + GptDiskRegion { + image: sample_image_file("images/root.img.zst"), + region_type: GptRegionType::Partition { number: 2 }, + }, + GptDiskRegion { + image: sample_image_file("images/root-hash.img.zst"), + region_type: GptRegionType::Partition { number: 3 }, + }, + GptDiskRegion { + image: sample_image_file("images/root-hash-sig.img.zst"), + region_type: GptRegionType::Partition { number: 4 }, + }, + GptDiskRegion { + image: sample_image_file("images/usr.img.zst"), + region_type: GptRegionType::Partition { number: 5 }, + }, + GptDiskRegion { + image: sample_image_file("images/usr-hash.img.zst"), + region_type: GptRegionType::Partition { number: 6 }, + }, + ], + }; + + // Root filesystem with verity pointing to the root-hash partition, and + // a detached signature backed by its own dedicated partition. + let root_image = Image { + file: sample_image_file("images/root.img.zst"), + mount_point: PathBuf::from("/"), + fs_type: OsImageFileSystemType::Ext4, + fs_uuid: OsUuid::Uuid(Uuid::new_v4()), + part_type: DiscoverablePartitionType::LinuxGeneric, + verity: Some(VerityMetadata { + file: sample_image_file("images/root-hash.img.zst"), + roothash: "abcd1234".to_string(), + signature: Some(sample_image_file("images/root-hash-sig.img.zst")), + }), + }; + + // /usr filesystem with verity pointing to the usr-hash partition, with + // no signature partition configured. + let usr_image = Image { + file: sample_image_file("images/usr.img.zst"), + mount_point: PathBuf::from("/usr"), + fs_type: OsImageFileSystemType::Ext4, + fs_uuid: OsUuid::Uuid(Uuid::new_v4()), + part_type: DiscoverablePartitionType::LinuxGeneric, + verity: Some(VerityMetadata { + file: sample_image_file("images/usr-hash.img.zst"), + roothash: "efgh5678".to_string(), + signature: None, + }), + }; + + let metadata = CosiMetadata { + version: KnownMetadataVersion::V1_3.as_version(), + id: Some(Uuid::new_v4()), + os_arch: SystemArchitecture::Amd64, + os_release: OsRelease::default(), + os_packages: None, + images: vec![ + sample_esp_image("images/esp.img.zst", "/boot/efi"), + root_image, + usr_image, + ], + bootloader: None, + disk: Some(disk_info), + compression: None, + }; + + let cosi = create_test_cosi(metadata, Some(raw_gpt)); + let result = derive_host_configuration_inner( + &cosi.source, + &cosi.metadata_sha384, + "/dev/sda", + &cosi.metadata.images, + cosi.partitioning_info.as_ref().unwrap(), + ); + assert!( + result.is_ok(), + "derive_host_configuration_inner with verity signature should succeed: {:?}", + result.unwrap_err() + ); + + let hc = result.unwrap(); + + // 6 partitions: esp, root, root-hash, root-hash-sig, usr, usr-hash. + assert_eq!( + hc.storage.disks[0].partitions.len(), + 6, + "Should have 6 partitions" + ); + + assert_eq!(hc.storage.verity.len(), 2, "Should have 2 verity devices"); + + // Root verity device has a signature partition: root-hash-sig is + // partition-4. + assert_eq!(hc.storage.verity[0].name, "root"); + assert_eq!(hc.storage.verity[0].data_device_id, "partition-2"); + assert_eq!(hc.storage.verity[0].hash_device_id, "partition-3"); + assert_eq!( + hc.storage.verity[0].hash_signature_device_id, + Some("partition-4".to_string()), + "Root verity device should resolve its signature partition" + ); + + // Usr verity device has no signature partition configured. + assert_eq!(hc.storage.verity[1].name, "usr"); + assert_eq!(hc.storage.verity[1].data_device_id, "partition-5"); + assert_eq!(hc.storage.verity[1].hash_device_id, "partition-6"); + assert_eq!( + hc.storage.verity[1].hash_signature_device_id, None, + "Usr verity device without a signature should leave the field unset" + ); + } + /// Tests [`derive_host_configuration_inner`] with verity at unsupported mount point. /// /// Verifies that an error is returned when a verity-enabled filesystem has a diff --git a/docs/Reference/Composable-OS-Image.md b/docs/Reference/Composable-OS-Image.md index 1f27b2adb2..2e2454bfd9 100644 --- a/docs/Reference/Composable-OS-Image.md +++ b/docs/Reference/Composable-OS-Image.md @@ -9,6 +9,7 @@ title: COSI Spec | Revision | Spec Date | | ------------------- | ---------- | +| [1.3](#revision-13) | TBD | | [1.2](#revision-12) | 2026-01-30 | | [1.1](#revision-11) | 2025-05-08 | | [1.0](#revision-10) | 2024-10-09 | @@ -223,6 +224,7 @@ compatibility. Machine-readable JSON Schema: +- [1.3](https://raw.githubusercontent.com/microsoft/trident/main/docs/Reference/Composable-OS-Image/cosi-metadata-v1.3.schema.json) - [1.2](https://raw.githubusercontent.com/microsoft/trident/main/docs/Reference/Composable-OS-Image/cosi-metadata-v1.2.schema.json) ##### Root Object @@ -286,10 +288,11 @@ _Notes:_ The `VerityConfig` object contains information required to set up a verity device on top of a data device. -| Field | Type | Added in | Required | Description | -| ---------- | ------------------------------ | -------- | --------------- | --------------------------------------------------------- | -| `image` | [ImageFile](#imagefile-object) | 1.0 | Yes (since 1.0) | Details of the hash partition image file in the tar file. | -| `roothash` | string | 1.0 | Yes (since 1.0) | Verity root hash. | +| Field | Type | Added in | Required | Description | +| ----------- | ------------------------------ | -------- | --------------- | ---------------------------------------------------------------------------------------------------------------- | +| `image` | [ImageFile](#imagefile-object) | 1.0 | Yes (since 1.0) | Details of the hash partition image file in the tar file. | +| `roothash` | string | 1.0 | Yes (since 1.0) | Verity root hash. | +| `signature` | [ImageFile](#imagefile-object) | 1.3 | No | Details of the detached PKCS#7 root hash signature file in the tar file, stored in its own dedicated partition. | ##### `ImageFile` Object @@ -630,6 +633,10 @@ making them invalid JSON. They are provided for illustration purposes only. ## Changelog +### Revision 1.3 + +- Added optional `signature` field to the `VerityConfig` object. + ### Revision 1.2 - Added `disk` field to the root object. diff --git a/docs/Reference/Composable-OS-Image/cosi-metadata-v1.3.schema.json b/docs/Reference/Composable-OS-Image/cosi-metadata-v1.3.schema.json new file mode 100644 index 0000000000..0626bc5507 --- /dev/null +++ b/docs/Reference/Composable-OS-Image/cosi-metadata-v1.3.schema.json @@ -0,0 +1,321 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/microsoft/trident/main/docs/Reference/Composable-OS-Image/cosi-metadata-v1.3.schema.json", + "title": "COSI Metadata (v1.3)", + "description": "JSON Schema for the `metadata.json` file of a Composable OS Image (COSI), COSI Specification revision 1.3. Derived from docs/Reference/Composable-OS-Image.md. Per the spec, readers MUST ignore unknown fields, so this schema does not forbid additional properties.", + "type": "object", + "required": [ + "version", + "osArch", + "osRelease", + "images", + "disk", + "osPackages", + "bootloader", + "compression" + ], + "properties": { + "version": { + "description": "The version of the metadata schema, in MAJOR.MINOR form. This schema targets revision 1.3.", + "type": "string", + "const": "1.3" + }, + "osArch": { + "description": "The architecture of the OS, using the names reported by `uname -m`. Case-insensitive; common aliases are accepted.", + "type": "string", + "pattern": "^(?:[Xx]86_64|[Xx]64|[Aa][Mm][Dd]64|[Aa][Rr][Mm]64|[Aa][Aa][Rr][Cc][Hh]64)$" + }, + "osRelease": { + "description": "The contents of `/etc/os-release` verbatim.", + "type": "string" + }, + "images": { + "description": "Filesystem metadata for each filesystem in the image.", + "type": "array", + "items": { "$ref": "#/$defs/Filesystem" } + }, + "disk": { + "description": "Original disk metadata.", + "$ref": "#/$defs/Disk" + }, + "osPackages": { + "description": "The list of packages installed in the OS.", + "type": "array", + "items": { "$ref": "#/$defs/OsPackage" } + }, + "bootloader": { + "description": "Information about the bootloader used by the OS.", + "$ref": "#/$defs/Bootloader" + }, + "id": { + "description": "A unique identifier for the COSI file (UUID, case-insensitive).", + "type": "string", + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" + }, + "compression": { + "description": "Compression metadata for the COSI file.", + "$ref": "#/$defs/Compression" + } + }, + "$defs": { + "ImageFile": { + "type": "object", + "required": ["path", "compressedSize", "uncompressedSize", "sha384"], + "properties": { + "path": { + "description": "Path of the compressed image file inside the tar file. MUST start with `images/`.", + "type": "string", + "pattern": "^images/.+" + }, + "compressedSize": { + "description": "Size of the compressed image in bytes.", + "type": "integer", + "minimum": 0 + }, + "uncompressedSize": { + "description": "Size of the raw uncompressed image in bytes.", + "type": "integer", + "minimum": 0 + }, + "sha384": { + "description": "SHA-384 hash of the compressed image (96 hex characters, case-insensitive).", + "type": "string", + "pattern": "^[0-9a-fA-F]{96}$" + } + } + }, + "VerityConfig": { + "type": "object", + "required": ["image", "roothash"], + "properties": { + "image": { + "description": "Details of the hash partition image file in the tar file.", + "$ref": "#/$defs/ImageFile" + }, + "roothash": { + "description": "Verity root hash.", + "type": "string" + }, + "signature": { + "description": "Details of the detached PKCS#7 root hash signature file in the tar file, stored in its own dedicated partition. Added in 1.3.", + "$ref": "#/$defs/ImageFile" + } + } + }, + "Filesystem": { + "type": "object", + "required": ["image", "mountPoint", "fsType", "fsUuid", "partType"], + "properties": { + "image": { + "description": "Details of the image file in the tar file.", + "$ref": "#/$defs/ImageFile" + }, + "mountPoint": { + "description": "The mount point of the filesystem.", + "type": "string" + }, + "fsType": { + "description": "The filesystem type, using the name recognized by the kernel (e.g. `ext4`, `vfat`).", + "type": "string", + "minLength": 1 + }, + "fsUuid": { + "description": "The UUID of the filesystem. Can be a proper UUID or a filesystem-specific identifier (e.g. a FAT volume serial such as `C3D4-250D`). MUST be unique across all filesystems.", + "type": "string", + "minLength": 1 + }, + "partType": { + "description": "The GPT partition type, as a UUID (case-insensitive). The ESP MUST be `c12a7328-f81f-11d2-ba4b-00a0c93ec93b`. Defaults to Generic Linux Data `0fc63daf-8483-4772-8e79-3d69d8477de4` when the type cannot be determined.", + "type": "string", + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" + }, + "verity": { + "description": "The verity metadata of the filesystem. MUST be specified if the OS opens this filesystem with dm-verity; otherwise omitted or null.", + "oneOf": [ + { "$ref": "#/$defs/VerityConfig" }, + { "type": "null" } + ] + } + } + }, + "Disk": { + "type": "object", + "required": ["size", "type", "lbaSize"], + "properties": { + "size": { + "description": "Size of the original disk in bytes. MUST be a multiple of `lbaSize`.", + "type": "integer", + "minimum": 0 + }, + "type": { + "description": "Partitioning type of the original disk. Currently only `gpt` is supported.", + "type": "string", + "enum": ["gpt"] + }, + "lbaSize": { + "description": "The size of a logical block address (LBA) in bytes. Must be 512 or 4096.", + "type": "integer", + "enum": [512, 4096] + }, + "gptRegions": { + "description": "Regions in the GPT disk, ordered by increasing start LBA (primary-gpt first). Required when `type` == `gpt`.", + "type": "array", + "minItems": 1, + "prefixItems": [ + { + "allOf": [ + { "$ref": "#/$defs/GptDiskRegion" }, + { + "properties": { "type": { "const": "primary-gpt" } }, + "required": ["type"] + } + ] + } + ], + "items": { "$ref": "#/$defs/GptDiskRegion" }, + "contains": { + "properties": { "type": { "const": "primary-gpt" } }, + "required": ["type"] + }, + "minContains": 1, + "maxContains": 1 + } + }, + "if": { + "properties": { "type": { "const": "gpt" } }, + "required": ["type"] + }, + "then": { + "required": ["gptRegions"] + } + }, + "GptDiskRegion": { + "type": "object", + "required": ["image", "type"], + "properties": { + "image": { + "description": "Details of the image file in the tar file.", + "$ref": "#/$defs/ImageFile" + }, + "type": { + "description": "The type of region this image represents.", + "type": "string", + "enum": ["primary-gpt", "partition"] + }, + "number": { + "description": "The partition's GPT entry index (1-based). Required when `type` == `partition`.", + "type": "integer", + "minimum": 1 + } + }, + "if": { + "properties": { "type": { "const": "partition" } }, + "required": ["type"] + }, + "then": { + "required": ["number"] + } + }, + "OsPackage": { + "type": "object", + "required": ["name", "version", "release", "arch"], + "properties": { + "name": { + "description": "The name of the package.", + "type": "string" + }, + "version": { + "description": "The version of the package installed.", + "type": "string" + }, + "release": { + "description": "The release of the package.", + "type": "string" + }, + "arch": { + "description": "The architecture of the package, as reported by RPM's `%{ARCH}` (e.g. `x86_64` or `aarch64` for architecture-specific packages, or `noarch` for architecture-independent packages).", + "type": "string" + } + } + }, + "Bootloader": { + "type": "object", + "required": ["type"], + "properties": { + "type": { + "description": "The type of the bootloader.", + "type": "string", + "enum": ["systemd-boot", "grub"] + }, + "systemdBoot": { + "description": "systemd-boot configuration. Required when `type` == `systemd-boot`; otherwise omitted or null.", + "oneOf": [ + { "$ref": "#/$defs/SystemDBoot" }, + { "type": "null" } + ] + } + }, + "if": { + "properties": { "type": { "const": "systemd-boot" } }, + "required": ["type"] + }, + "then": { + "required": ["systemdBoot"], + "properties": { + "systemdBoot": { "$ref": "#/$defs/SystemDBoot" } + } + }, + "else": { + "properties": { + "systemdBoot": { "type": "null" } + } + } + }, + "SystemDBoot": { + "type": "object", + "required": ["entries"], + "properties": { + "entries": { + "description": "The contents of the `loader/entries/*.conf` files in the systemd-boot EFI partition.", + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/SystemDBootEntry" } + } + } + }, + "SystemDBootEntry": { + "type": "object", + "required": ["type", "path", "cmdline", "kernel"], + "properties": { + "type": { + "description": "The type of the entry.", + "type": "string", + "enum": ["uki-standalone", "uki-config", "config"] + }, + "path": { + "description": "Absolute path (from the root FS) to the UKI or config.", + "type": "string" + }, + "cmdline": { + "description": "The kernel command line.", + "type": "string" + }, + "kernel": { + "description": "Kernel release as a string.", + "type": "string" + } + } + }, + "Compression": { + "type": "object", + "required": ["maxWindowLog"], + "properties": { + "maxWindowLog": { + "description": "The maximum zstd `windowLog` (the power-of-2 exponent, not the window size in bytes) used across all images in the COSI file.", + "type": "integer", + "minimum": 0 + } + } + } + } +} diff --git a/tests/cosi/metadata_samples/v1.3/invalid/signature-wrong-type.json b/tests/cosi/metadata_samples/v1.3/invalid/signature-wrong-type.json new file mode 100644 index 0000000000..1b763cdbce --- /dev/null +++ b/tests/cosi/metadata_samples/v1.3/invalid/signature-wrong-type.json @@ -0,0 +1,102 @@ +{ + "version": "1.3", + "osArch": "x86_64", + "images": [ + { + "image": { + "path": "images/esp.rawzst", + "compressedSize": 839345, + "uncompressedSize": 8388608, + "sha384": "2decc64a828dbbb76779731cd4afd3b86cc4ad0af06f4afe594e72e62e33e520a6649719fe43f09f11d518e485eae0db" + }, + "mountPoint": "/boot/efi", + "fsType": "vfat", + "fsUuid": "A1B2-C3D4", + "partType": "c12a7328-f81f-11d2-ba4b-00a0c93ec93b", + "verity": null + }, + { + "image": { + "path": "images/root.rawzst", + "compressedSize": 192874245, + "uncompressedSize": 899494400, + "sha384": "98ea4adbbb8ce0220d109d53d65825bd5a565248e4af3a9346d088918e7856ac2c42e13461cac67dbf3711ff69695ec3" + }, + "mountPoint": "/", + "fsType": "ext4", + "fsUuid": "88d2fa9b-7a32-450a-a9f8-aa9c3de79298", + "partType": "4f68bce3-e8cd-4db1-96e7-fbcaf984b709", + "verity": { + "image": { + "path": "images/root-verity.rawzst", + "compressedSize": 26214400, + "uncompressedSize": 524288000, + "sha384": "51356c53fbdd5c196395ccd389116f2e7769443cb4e945bc9b6bc3c805cf857c375df010469f8f45ef0c5b07456b023d" + }, + "roothash": "646c82fa4c3f97e6cddc3996315c7f04b2beb721fb24fa38835136492a84eb19", + "signature": "not-an-object" + } + } + ], + "osRelease": "NAME=\"Microsoft Azure Linux\"\nVERSION=\"3.0.20240824\"\nID=azurelinux\nVERSION_ID=\"3.0\"\nPRETTY_NAME=\"Microsoft Azure Linux 3.0\"\nANSI_COLOR=\"1;34\"\nHOME_URL=\"https://aka.ms/azurelinux\"\nBUG_REPORT_URL=\"https://aka.ms/azurelinux\"\nSUPPORT_URL=\"https://aka.ms/azurelinux\"\n", + "bootloader": { + "type": "systemd-boot", + "systemdBoot": { + "entries": [ + { + "type": "uki-standalone", + "path": "/boot/efi/EFI/Linux/azurelinux-uki.efi", + "cmdline": "root=/dev/disk/by-partuuid/88d2fa9b-7a32-450a-a9f8-aa9c3de79298 ro", + "kernel": "6.6.78.1-3.azl3" + } + ] + } + }, + "osPackages": [ + { + "name": "systemd", + "version": "255", + "release": "20.azl3", + "arch": "x86_64" + } + ], + "disk": { + "size": 1073741824, + "type": "gpt", + "lbaSize": 512, + "gptRegions": [ + { + "image": { + "path": "images/primary-gpt.rawzst", + "compressedSize": 16384, + "uncompressedSize": 32768, + "sha384": "a3f5c6e2b4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7" + }, + "type": "primary-gpt" + }, + { + "image": { + "path": "images/esp.rawzst", + "compressedSize": 839345, + "uncompressedSize": 8388608, + "sha384": "2decc64a828dbbb76779731cd4afd3b86cc4ad0af06f4afe594e72e62e33e520a6649719fe43f09f11d518e485eae0db" + }, + "type": "partition", + "number": 1 + }, + { + "image": { + "path": "images/root.rawzst", + "compressedSize": 192874245, + "uncompressedSize": 899494400, + "sha384": "98ea4adbbb8ce0220d109d53d65825bd5a565248e4af3a9346d088918e7856ac2c42e13461cac67dbf3711ff69695ec3" + }, + "type": "partition", + "number": 2 + } + ] + }, + "compression": { + "maxWindowLog": 22 + } +} diff --git a/tests/cosi/metadata_samples/v1.3/invalid/wrong-version.json b/tests/cosi/metadata_samples/v1.3/invalid/wrong-version.json new file mode 100644 index 0000000000..3e3e0c71ef --- /dev/null +++ b/tests/cosi/metadata_samples/v1.3/invalid/wrong-version.json @@ -0,0 +1,95 @@ +{ + "version": "1.2", + "osArch": "x86_64", + "images": [ + { + "image": { + "path": "images/esp.rawzst", + "compressedSize": 839345, + "uncompressedSize": 8388608, + "sha384": "2decc64a828dbbb76779731cd4afd3b86cc4ad0af06f4afe594e72e62e33e520a6649719fe43f09f11d518e485eae0db" + }, + "mountPoint": "/boot/efi", + "fsType": "vfat", + "fsUuid": "C3D4-250D", + "partType": "c12a7328-f81f-11d2-ba4b-00a0c93ec93b", + "verity": null + }, + { + "image": { + "path": "images/root.rawzst", + "compressedSize": 192874245, + "uncompressedSize": 899494400, + "sha384": "98ea4adbbb8ce0220d109d53d65825bd5a565248e4af3a9346d088918e7856ac2c42e13461cac67dbf3711ff69695ec3" + }, + "mountPoint": "/", + "fsType": "ext4", + "fsUuid": "88d2fa9b-7a32-450a-a9f8-aa9c3de79298", + "partType": "4f68bce3-e8cd-4db1-96e7-fbcaf984b709", + "verity": null + } + ], + "osRelease": "NAME=\"Microsoft Azure Linux\"\nVERSION=\"3.0.20240824\"\nID=azurelinux\nVERSION_ID=\"3.0\"\nPRETTY_NAME=\"Microsoft Azure Linux 3.0\"\nANSI_COLOR=\"1;34\"\nHOME_URL=\"https://aka.ms/azurelinux\"\nBUG_REPORT_URL=\"https://aka.ms/azurelinux\"\nSUPPORT_URL=\"https://aka.ms/azurelinux\"\n", + "bootloader": { + "type": "grub" + }, + "osPackages": [ + { + "name": "bash", + "version": "5.1.8", + "release": "1.azl3", + "arch": "x86_64" + }, + { + "name": "coreutils", + "version": "8.32", + "release": "1.azl3", + "arch": "x86_64" + }, + { + "name": "systemd", + "version": "255", + "release": "20.azl3", + "arch": "x86_64" + } + ], + "disk": { + "size": 1073741824, + "type": "gpt", + "lbaSize": 512, + "gptRegions": [ + { + "image": { + "path": "images/primary-gpt.rawzst", + "compressedSize": 16384, + "uncompressedSize": 32768, + "sha384": "a3f5c6e2b4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7" + }, + "type": "primary-gpt" + }, + { + "image": { + "path": "images/esp.rawzst", + "compressedSize": 839345, + "uncompressedSize": 8388608, + "sha384": "2decc64a828dbbb76779731cd4afd3b86cc4ad0af06f4afe594e72e62e33e520a6649719fe43f09f11d518e485eae0db" + }, + "type": "partition", + "number": 1 + }, + { + "image": { + "path": "images/root.rawzst", + "compressedSize": 192874245, + "uncompressedSize": 899494400, + "sha384": "98ea4adbbb8ce0220d109d53d65825bd5a565248e4af3a9346d088918e7856ac2c42e13461cac67dbf3711ff69695ec3" + }, + "type": "partition", + "number": 2 + } + ] + }, + "compression": { + "maxWindowLog": 30 + } +} diff --git a/tests/cosi/metadata_samples/v1.3/valid/grub-simple.json b/tests/cosi/metadata_samples/v1.3/valid/grub-simple.json new file mode 100644 index 0000000000..000f9dcf13 --- /dev/null +++ b/tests/cosi/metadata_samples/v1.3/valid/grub-simple.json @@ -0,0 +1,95 @@ +{ + "version": "1.3", + "osArch": "x86_64", + "images": [ + { + "image": { + "path": "images/esp.rawzst", + "compressedSize": 839345, + "uncompressedSize": 8388608, + "sha384": "2decc64a828dbbb76779731cd4afd3b86cc4ad0af06f4afe594e72e62e33e520a6649719fe43f09f11d518e485eae0db" + }, + "mountPoint": "/boot/efi", + "fsType": "vfat", + "fsUuid": "C3D4-250D", + "partType": "c12a7328-f81f-11d2-ba4b-00a0c93ec93b", + "verity": null + }, + { + "image": { + "path": "images/root.rawzst", + "compressedSize": 192874245, + "uncompressedSize": 899494400, + "sha384": "98ea4adbbb8ce0220d109d53d65825bd5a565248e4af3a9346d088918e7856ac2c42e13461cac67dbf3711ff69695ec3" + }, + "mountPoint": "/", + "fsType": "ext4", + "fsUuid": "88d2fa9b-7a32-450a-a9f8-aa9c3de79298", + "partType": "4f68bce3-e8cd-4db1-96e7-fbcaf984b709", + "verity": null + } + ], + "osRelease": "NAME=\"Microsoft Azure Linux\"\nVERSION=\"3.0.20240824\"\nID=azurelinux\nVERSION_ID=\"3.0\"\nPRETTY_NAME=\"Microsoft Azure Linux 3.0\"\nANSI_COLOR=\"1;34\"\nHOME_URL=\"https://aka.ms/azurelinux\"\nBUG_REPORT_URL=\"https://aka.ms/azurelinux\"\nSUPPORT_URL=\"https://aka.ms/azurelinux\"\n", + "bootloader": { + "type": "grub" + }, + "osPackages": [ + { + "name": "bash", + "version": "5.1.8", + "release": "1.azl3", + "arch": "x86_64" + }, + { + "name": "coreutils", + "version": "8.32", + "release": "1.azl3", + "arch": "x86_64" + }, + { + "name": "systemd", + "version": "255", + "release": "20.azl3", + "arch": "x86_64" + } + ], + "disk": { + "size": 1073741824, + "type": "gpt", + "lbaSize": 512, + "gptRegions": [ + { + "image": { + "path": "images/primary-gpt.rawzst", + "compressedSize": 16384, + "uncompressedSize": 32768, + "sha384": "a3f5c6e2b4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7" + }, + "type": "primary-gpt" + }, + { + "image": { + "path": "images/esp.rawzst", + "compressedSize": 839345, + "uncompressedSize": 8388608, + "sha384": "2decc64a828dbbb76779731cd4afd3b86cc4ad0af06f4afe594e72e62e33e520a6649719fe43f09f11d518e485eae0db" + }, + "type": "partition", + "number": 1 + }, + { + "image": { + "path": "images/root.rawzst", + "compressedSize": 192874245, + "uncompressedSize": 899494400, + "sha384": "98ea4adbbb8ce0220d109d53d65825bd5a565248e4af3a9346d088918e7856ac2c42e13461cac67dbf3711ff69695ec3" + }, + "type": "partition", + "number": 2 + } + ] + }, + "compression": { + "maxWindowLog": 30 + } +} diff --git a/tests/cosi/metadata_samples/v1.3/valid/verity-uki-systemd-boot.json b/tests/cosi/metadata_samples/v1.3/valid/verity-uki-systemd-boot.json new file mode 100644 index 0000000000..2dc96274cd --- /dev/null +++ b/tests/cosi/metadata_samples/v1.3/valid/verity-uki-systemd-boot.json @@ -0,0 +1,107 @@ +{ + "version": "1.3", + "osArch": "x86_64", + "images": [ + { + "image": { + "path": "images/esp.rawzst", + "compressedSize": 839345, + "uncompressedSize": 8388608, + "sha384": "2decc64a828dbbb76779731cd4afd3b86cc4ad0af06f4afe594e72e62e33e520a6649719fe43f09f11d518e485eae0db" + }, + "mountPoint": "/boot/efi", + "fsType": "vfat", + "fsUuid": "A1B2-C3D4", + "partType": "c12a7328-f81f-11d2-ba4b-00a0c93ec93b", + "verity": null + }, + { + "image": { + "path": "images/root.rawzst", + "compressedSize": 192874245, + "uncompressedSize": 899494400, + "sha384": "98ea4adbbb8ce0220d109d53d65825bd5a565248e4af3a9346d088918e7856ac2c42e13461cac67dbf3711ff69695ec3" + }, + "mountPoint": "/", + "fsType": "ext4", + "fsUuid": "88d2fa9b-7a32-450a-a9f8-aa9c3de79298", + "partType": "4f68bce3-e8cd-4db1-96e7-fbcaf984b709", + "verity": { + "image": { + "path": "images/root-verity.rawzst", + "compressedSize": 26214400, + "uncompressedSize": 524288000, + "sha384": "51356c53fbdd5c196395ccd389116f2e7769443cb4e945bc9b6bc3c805cf857c375df010469f8f45ef0c5b07456b023d" + }, + "roothash": "646c82fa4c3f97e6cddc3996315c7f04b2beb721fb24fa38835136492a84eb19", + "signature": { + "path": "images/root-verity-sig.rawzst", + "compressedSize": 512, + "uncompressedSize": 1024, + "sha384": "9ad73493dd99156f81ace36e0974ca30ad636215e044b3db6a549aaee5ea602d093d83b787bf41ddac21fd4e95e30425" + } + } + } + ], + "osRelease": "NAME=\"Microsoft Azure Linux\"\nVERSION=\"3.0.20240824\"\nID=azurelinux\nVERSION_ID=\"3.0\"\nPRETTY_NAME=\"Microsoft Azure Linux 3.0\"\nANSI_COLOR=\"1;34\"\nHOME_URL=\"https://aka.ms/azurelinux\"\nBUG_REPORT_URL=\"https://aka.ms/azurelinux\"\nSUPPORT_URL=\"https://aka.ms/azurelinux\"\n", + "bootloader": { + "type": "systemd-boot", + "systemdBoot": { + "entries": [ + { + "type": "uki-standalone", + "path": "/boot/efi/EFI/Linux/azurelinux-uki.efi", + "cmdline": "root=/dev/disk/by-partuuid/88d2fa9b-7a32-450a-a9f8-aa9c3de79298 ro", + "kernel": "6.6.78.1-3.azl3" + } + ] + } + }, + "osPackages": [ + { + "name": "systemd", + "version": "255", + "release": "20.azl3", + "arch": "x86_64" + } + ], + "disk": { + "size": 1073741824, + "type": "gpt", + "lbaSize": 512, + "gptRegions": [ + { + "image": { + "path": "images/primary-gpt.rawzst", + "compressedSize": 16384, + "uncompressedSize": 32768, + "sha384": "a3f5c6e2b4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7" + }, + "type": "primary-gpt" + }, + { + "image": { + "path": "images/esp.rawzst", + "compressedSize": 839345, + "uncompressedSize": 8388608, + "sha384": "2decc64a828dbbb76779731cd4afd3b86cc4ad0af06f4afe594e72e62e33e520a6649719fe43f09f11d518e485eae0db" + }, + "type": "partition", + "number": 1 + }, + { + "image": { + "path": "images/root.rawzst", + "compressedSize": 192874245, + "uncompressedSize": 899494400, + "sha384": "98ea4adbbb8ce0220d109d53d65825bd5a565248e4af3a9346d088918e7856ac2c42e13461cac67dbf3711ff69695ec3" + }, + "type": "partition", + "number": 2 + } + ] + }, + "compression": { + "maxWindowLog": 22 + } +} From 471a68732f2a8a3c8e61d0d06078f3bd1c2a41dd Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Thu, 8 Oct 2026 22:42:27 +0000 Subject: [PATCH 3/5] cosi: make the v1.3 valid sample a fully self-consistent COSI document Addresses review feedback on PR #835: The v1.3 verity-uki-systemd-boot.json sample declared a signature stored in a dedicated partition, but disk.gptRegions only listed the ESP and root partitions - no matching region for the root-verity hash or root-verity-sig partitions the verity block itself references. This made the sample schema-valid but not something Trident's own partition-consistency checks could ever accept as a real COSI file. Added matching gptRegions entries (partition numbers 3 and 4) for the hash and signature images, reusing their existing ImageFile metadata from the verity block so they stay byte-for-byte consistent. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../v1.3/valid/verity-uki-systemd-boot.json | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/cosi/metadata_samples/v1.3/valid/verity-uki-systemd-boot.json b/tests/cosi/metadata_samples/v1.3/valid/verity-uki-systemd-boot.json index 2dc96274cd..285e139b93 100644 --- a/tests/cosi/metadata_samples/v1.3/valid/verity-uki-systemd-boot.json +++ b/tests/cosi/metadata_samples/v1.3/valid/verity-uki-systemd-boot.json @@ -98,6 +98,26 @@ }, "type": "partition", "number": 2 + }, + { + "image": { + "path": "images/root-verity.rawzst", + "compressedSize": 26214400, + "uncompressedSize": 524288000, + "sha384": "51356c53fbdd5c196395ccd389116f2e7769443cb4e945bc9b6bc3c805cf857c375df010469f8f45ef0c5b07456b023d" + }, + "type": "partition", + "number": 3 + }, + { + "image": { + "path": "images/root-verity-sig.rawzst", + "compressedSize": 512, + "uncompressedSize": 1024, + "sha384": "9ad73493dd99156f81ace36e0974ca30ad636215e044b3db6a549aaee5ea602d093d83b787bf41ddac21fd4e95e30425" + }, + "type": "partition", + "number": 4 } ] }, From ff1332aa0971a22e6dfc28f18b920647f572a127 Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Thu, 8 Oct 2026 23:58:34 +0000 Subject: [PATCH 4/5] cosi: enlarge the v1.3 valid sample's disk to actually fit its partitions Addresses follow-up review feedback on PR #835: Adding GPT regions for the hash and signature partitions (previous commit) made the sample internally consistent for partition references, but the disk's declared size (1 GiB) was never big enough to hold all four partition images (esp + root + root-verity hash + root-verity-sig), which together need at least ~1.43 GiB uncompressed. Enlarged disk.size to 2 GiB, comfortably above that total. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../metadata_samples/v1.3/valid/verity-uki-systemd-boot.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cosi/metadata_samples/v1.3/valid/verity-uki-systemd-boot.json b/tests/cosi/metadata_samples/v1.3/valid/verity-uki-systemd-boot.json index 285e139b93..77bc1a1e20 100644 --- a/tests/cosi/metadata_samples/v1.3/valid/verity-uki-systemd-boot.json +++ b/tests/cosi/metadata_samples/v1.3/valid/verity-uki-systemd-boot.json @@ -66,7 +66,7 @@ } ], "disk": { - "size": 1073741824, + "size": 2147483648, "type": "gpt", "lbaSize": 512, "gptRegions": [ From fbee98e4baae150709f981d32629dd577ef5d3ff Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Sat, 10 Oct 2026 16:59:46 +0000 Subject: [PATCH 5/5] fix: reject aliased COSI image paths and specify DER signatures Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d61c42e7-3c43-4fe5-8c89-89d38ff9528b --- crates/trident/src/osimage/cosi/error.rs | 3 ++ crates/trident/src/osimage/cosi/validation.rs | 45 +++++++++++++++++-- docs/Reference/Composable-OS-Image.md | 2 +- .../cosi-metadata-v1.3.schema.json | 2 +- 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/crates/trident/src/osimage/cosi/error.rs b/crates/trident/src/osimage/cosi/error.rs index 889726c844..5113d9a46e 100644 --- a/crates/trident/src/osimage/cosi/error.rs +++ b/crates/trident/src/osimage/cosi/error.rs @@ -51,6 +51,9 @@ pub enum CosiMetadataErrorKind { #[error("Disk partition table type must be GPT, found '{0}'")] V1_2DiskPartitionTableNotGpt(String), + #[error("Duplicate filesystem or verity image file path: '{0}'")] + V1_2DuplicateImageFilePath(String), + #[error("Duplicate partition number: {0}")] V1_2DuplicatePartitionNumber(u32), diff --git a/crates/trident/src/osimage/cosi/validation.rs b/crates/trident/src/osimage/cosi/validation.rs index e2ce01939f..b3d59d65a6 100644 --- a/crates/trident/src/osimage/cosi/validation.rs +++ b/crates/trident/src/osimage/cosi/validation.rs @@ -147,10 +147,17 @@ impl CosiMetadata { // Collect known filesystem image paths for validation. It is mutable so that // we can remove entries as we match them to partitions and check if // there are any leftovers. - let mut filesystem_paths = self - .filesystem_image_files() - .map(|img| (img.path.as_path(), img)) - .collect::>(); + let mut filesystem_paths = HashMap::new(); + for image in self.filesystem_image_files() { + if filesystem_paths + .insert(image.path.as_path(), image) + .is_some() + { + return mk_err(CosiMetadataErrorKind::V1_2DuplicateImageFilePath( + image.path.display().to_string(), + )); + } + } let mut partition_numbers = HashSet::new(); @@ -587,6 +594,36 @@ mod tests { let metadata = parse_and_validate(base.clone()).unwrap(); assert_eq!(metadata.version, KnownMetadataVersion::V1_2); + let mut signed = base.clone(); + signed["version"] = json!("1.3"); + let mut signature = base["images"][0]["verity"]["image"].clone(); + signature["path"] = json!("path/to/image1.signature"); + signed["images"][0]["verity"]["signature"] = signature.clone(); + signed["disk"]["gptRegions"] + .as_array_mut() + .unwrap() + .push(json!({ + "type": "partition", + "number": 4, + "image": signature, + })); + parse_and_validate(signed.clone()).unwrap(); + + for image in [ + &base["images"][0]["image"], + &base["images"][0]["verity"]["image"], + &base["images"][1]["image"], + ] { + let mut aliased = signed.clone(); + aliased["images"][0]["verity"]["signature"] = image.clone(); + assert_validate_err_kind( + aliased, + CosiMetadataErrorKind::V1_2DuplicateImageFilePath( + image["path"].as_str().unwrap().to_string(), + ), + ); + } + // v1.2 requires compression info. let mut no_compression = base.clone(); no_compression diff --git a/docs/Reference/Composable-OS-Image.md b/docs/Reference/Composable-OS-Image.md index 2e2454bfd9..5974450813 100644 --- a/docs/Reference/Composable-OS-Image.md +++ b/docs/Reference/Composable-OS-Image.md @@ -292,7 +292,7 @@ device on top of a data device. | ----------- | ------------------------------ | -------- | --------------- | ---------------------------------------------------------------------------------------------------------------- | | `image` | [ImageFile](#imagefile-object) | 1.0 | Yes (since 1.0) | Details of the hash partition image file in the tar file. | | `roothash` | string | 1.0 | Yes (since 1.0) | Verity root hash. | -| `signature` | [ImageFile](#imagefile-object) | 1.3 | No | Details of the detached PKCS#7 root hash signature file in the tar file, stored in its own dedicated partition. | +| `signature` | [ImageFile](#imagefile-object) | 1.3 | No | Details of the root hash signature file in the tar file, stored in its own dedicated partition. The signature MUST be DER-encoded detached PKCS#7. | ##### `ImageFile` Object diff --git a/docs/Reference/Composable-OS-Image/cosi-metadata-v1.3.schema.json b/docs/Reference/Composable-OS-Image/cosi-metadata-v1.3.schema.json index 0626bc5507..b061cde2fb 100644 --- a/docs/Reference/Composable-OS-Image/cosi-metadata-v1.3.schema.json +++ b/docs/Reference/Composable-OS-Image/cosi-metadata-v1.3.schema.json @@ -97,7 +97,7 @@ "type": "string" }, "signature": { - "description": "Details of the detached PKCS#7 root hash signature file in the tar file, stored in its own dedicated partition. Added in 1.3.", + "description": "Details of the root hash signature file in the tar file, stored in its own dedicated partition. The signature MUST be DER-encoded detached PKCS#7. Added in 1.3.", "$ref": "#/$defs/ImageFile" } }