Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 167 additions & 1 deletion crates/trident/src/osimage/cosi/derived_hc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Comment thread
bfjelds marked this conversation as resolved.
corruption_option: Default::default(),
});

Expand Down Expand Up @@ -727,6 +744,7 @@ mod tests {
verity: Some(VerityMetadata {
file: sample_image_file("images/root-hash.img.zst"),
roothash: "abcd1234".to_string(),
signature: None,
}),
};

Expand All @@ -740,6 +758,7 @@ mod tests {
verity: Some(VerityMetadata {
file: sample_image_file("images/usr-hash.img.zst"),
roothash: "efgh5678".to_string(),
signature: None,
}),
};

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -876,6 +1041,7 @@ mod tests {
verity: Some(VerityMetadata {
file: sample_image_file("images/var-hash.img.zst"),
roothash: "badhash".to_string(),
signature: None,
}),
};

Expand Down
3 changes: 3 additions & 0 deletions crates/trident/src/osimage/cosi/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),

Expand Down
29 changes: 28 additions & 1 deletion crates/trident/src/osimage/cosi/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Comment thread
bfjelds marked this conversation as resolved.
}

impl KnownMetadataVersion {
Expand All @@ -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 },
}
}
}
Expand Down Expand Up @@ -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())
}),
)
})
}
}
Expand Down Expand Up @@ -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<ImageFile>,
Comment thread
bfjelds marked this conversation as resolved.
}

#[derive(Debug, Deserialize, Clone, Eq, PartialEq)]
Expand Down Expand Up @@ -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"),
Expand Down
7 changes: 7 additions & 0 deletions crates/trident/src/osimage/cosi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Comment thread
bfjelds marked this conversation as resolved.
}),
}),
}
}
Expand Down Expand Up @@ -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);
Expand Down
45 changes: 41 additions & 4 deletions crates/trident/src/osimage/cosi/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<HashMap<_, _>>();
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();

Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions crates/trident/src/osimage/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
}),
})
}
Expand Down
1 change: 1 addition & 0 deletions crates/trident/src/osimage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ pub struct GptPartitionInfo {
pub struct OsImageVerityHash {
pub roothash: String,
pub hash_image_file: OsImageFile,
pub signature_image_file: Option<OsImageFile>,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
Expand Down
Loading