diff --git a/crates/trident/src/osimage/cosi/derived_hc.rs b/crates/trident/src/osimage/cosi/derived_hc.rs index 622d62bb9e..f33f8b4ba2 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, }), }; @@ -838,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 @@ -876,6 +1041,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/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/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/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/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)] diff --git a/docs/Reference/Composable-OS-Image.md b/docs/Reference/Composable-OS-Image.md index 1f27b2adb2..5974450813 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 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 @@ -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..b061cde2fb --- /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 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" + } + } + }, + "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..77bc1a1e20 --- /dev/null +++ b/tests/cosi/metadata_samples/v1.3/valid/verity-uki-systemd-boot.json @@ -0,0 +1,127 @@ +{ + "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": 2147483648, + "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 + }, + { + "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 + } + ] + }, + "compression": { + "maxWindowLog": 22 + } +}