diff --git a/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go b/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go index ea5a0fc36..dc3e4856e 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go +++ b/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go @@ -3,7 +3,6 @@ package imagecustomizerlib import ( "archive/tar" "crypto/sha512" - _ "embed" "encoding/json" "fmt" "io" @@ -17,9 +16,10 @@ import ( ) type ImageBuildData struct { - Source string - KnownInfo outputPartitionMetadata - Metadata *Image + Source string + KnownInfo outputPartitionMetadata + Metadata *Image + VeritySource string } func convertToCosi(ic *ImageCustomizerParameters) error { @@ -41,9 +41,9 @@ func convertToCosi(ic *ImageCustomizerParameters) error { return err } - err = buildCosiFile(outputDir, ic.outputImageFile, partitionMetadataOutput, ic.imageUuidStr) + err = buildCosiFile(outputDir, ic.outputImageFile, partitionMetadataOutput, ic.verityMetadata, ic.imageUuidStr) if err != nil { - return fmt.Errorf("failed to build COSI:\n%w", err) + return fmt.Errorf("failed to build COSI file:\n%w", err) } logger.Log.Infof("Successfully converted to COSI: %s", ic.outputImageFile) @@ -56,45 +56,90 @@ func convertToCosi(ic *ImageCustomizerParameters) error { return nil } -func buildCosiFile(sourceDir string, outputFile string, expectedImages []outputPartitionMetadata, imageUuidStr string) error { - metadata := MetadataJson{ - Version: "1.0", - OsArch: runtime.GOARCH, - Id: imageUuidStr, - Images: make([]Image, len(expectedImages)), +func buildCosiFile(sourceDir string, outputFile string, partitions []outputPartitionMetadata, verityMetadata []verityDeviceMetadata, imageUuidStr string) error { + + // Pre-compute a map for quick lookup of partition metadata by UUID + partUuidToMetadata := make(map[string]outputPartitionMetadata) + for _, partition := range partitions { + partUuidToMetadata[partition.PartUuid] = partition } - if len(expectedImages) == 0 { - return fmt.Errorf("no images to build") + // Pre-compute a set of verity hash UUIDs for quick lookup + verityHashUuids := make(map[string]struct{}) + for _, verity := range verityMetadata { + verityHashUuids[verity.hashPartUuid] = struct{}{} } - // Create an interim metadata struct to combine the known data with the metadata - imageData := make([]ImageBuildData, len(expectedImages)) - for i, image := range expectedImages { - metadata := &metadata.Images[i] - imageData[i] = ImageBuildData{ - Source: path.Join(sourceDir, image.PartitionFilename), - Metadata: metadata, - KnownInfo: image, + imageData := []ImageBuildData{} + + for _, partition := range partitions { + // Skip verity hash partitions as their metadata will be assigned to the corresponding data partitions + if _, isVerityHash := verityHashUuids[partition.PartUuid]; isVerityHash { + continue + } + + metadataImage := Image{ + Image: ImageFile{ + Path: path.Join("images", partition.PartitionFilename), + UncompressedSize: partition.UncompressedSize, + }, + PartType: partition.PartitionTypeUuid, + MountPoint: partition.Mountpoint, + FsType: partition.FileSystemType, + FsUuid: partition.Uuid, + } + + imageDataEntry := ImageBuildData{ + Source: path.Join(sourceDir, partition.PartitionFilename), + Metadata: &metadataImage, + KnownInfo: partition, + } + + // Add Verity metadata if the partition has a matching entry in verityMetadata + for _, verity := range verityMetadata { + if partition.PartUuid == verity.dataPartUuid { + hashPartition, exists := partUuidToMetadata[verity.hashPartUuid] + if !exists { + return fmt.Errorf("missing metadata for hash partition UUID:\n%s", verity.hashPartUuid) + } + + metadataImage.Verity = &Verity{ + Hash: verity.hash, + Image: ImageFile{ + Path: path.Join("images", hashPartition.PartitionFilename), + UncompressedSize: hashPartition.UncompressedSize, + }, + } + + veritySourcePath := path.Join(sourceDir, hashPartition.PartitionFilename) + imageDataEntry.VeritySource = veritySourcePath + break + } } - metadata.Image.Path = path.Join("images", image.PartitionFilename) - metadata.PartType = image.PartitionTypeUuid - metadata.MountPoint = image.Mountpoint - metadata.FsType = image.FileSystemType - metadata.FsUuid = image.Uuid - metadata.UncompressedSize = image.UncompressedSize + imageData = append(imageData, imageDataEntry) } // Populate metadata for each image - for _, data := range imageData { - logger.Log.Infof("Processing image %s", data.Source) - err := populateMetadata(data) + for i := range imageData { + err := populateMetadata(&imageData[i]) if err != nil { - return fmt.Errorf("failed to populate metadata for %s:\n%w", data.Source, err) + return fmt.Errorf("failed to populate metadata for %s:\n%w", imageData[i].Source, err) } - logger.Log.Infof("Populated metadata for image %s", data.Source) + logger.Log.Infof("Populated metadata for image %s", imageData[i].Source) + } + + metadata := MetadataJson{ + Version: "1.0", + OsArch: runtime.GOARCH, + Id: imageUuidStr, + Images: make([]Image, len(imageData)), + } + + // Copy updated metadata + for i, data := range imageData { + metadata.Images[i] = *data.Metadata } // Marshal metadata.json @@ -133,26 +178,43 @@ func buildCosiFile(sourceDir string, outputFile string, expectedImages []outputP } func addToCosi(data ImageBuildData, tw *tar.Writer) error { - imageFile, err := os.Open(data.Source) + err := addFileToCosi(tw, data.Source, data.Metadata.Image) + if err != nil { + return fmt.Errorf("failed to add image file to COSI:\n%w", err) + } + + if data.VeritySource != "" && data.Metadata.Verity != nil { + err := addFileToCosi(tw, data.VeritySource, data.Metadata.Verity.Image) + if err != nil { + return fmt.Errorf("failed to add verity file to COSI:\n%w", err) + } + } + + return nil +} + +func addFileToCosi(tw *tar.Writer, source string, image ImageFile) error { + file, err := os.Open(source) if err != nil { - return fmt.Errorf("failed to open image file:\n%w", err) + return fmt.Errorf("failed to open file :\n%w", err) } - defer imageFile.Close() + defer file.Close() err = tw.WriteHeader(&tar.Header{ Typeflag: tar.TypeReg, - Name: data.Metadata.Image.Path, - Size: int64(data.Metadata.Image.CompressedSize), + Name: image.Path, + Size: int64(image.CompressedSize), Mode: 0o400, Format: tar.FormatPAX, }) + if err != nil { - return fmt.Errorf("failed to write tar header:\n%w", err) + return fmt.Errorf("failed to write tar header for file '%s':\n%w", image.Path, err) } - _, err = io.Copy(tw, imageFile) + _, err = io.Copy(tw, file) if err != nil { - return fmt.Errorf("failed to write image to COSI:\n%w", err) + return fmt.Errorf("failed to write image '%s' to COSI:\n%w", image.Path, err) } return nil @@ -172,21 +234,50 @@ func sha384sum(path string) (string, error) { return fmt.Sprintf("%x", sha384.Sum(nil)), nil } -func populateMetadata(data ImageBuildData) error { - stat, err := os.Stat(data.Source) +func populateImageFile(source string, imageFile *ImageFile) error { + stat, err := os.Stat(source) if err != nil { - return fmt.Errorf("filed to stat %s:\n%w", data.Source, err) + return fmt.Errorf("failed to stat %s:\n%w", source, err) } if stat.IsDir() { - return fmt.Errorf("%s is a directory", data.Source) + return fmt.Errorf("%s is a directory", source) } - data.Metadata.Image.CompressedSize = uint64(stat.Size()) + imageFile.CompressedSize = uint64(stat.Size()) - // Calculate the sha384 of the image - sha384, err := sha384sum(data.Source) + sha384, err := sha384sum(source) if err != nil { - return fmt.Errorf("failed to calculate sha384 of %s:\n%w", data.Source, err) + return fmt.Errorf("failed to calculate sha384 of %s:\n%w", source, err) + } + imageFile.Sha384 = sha384 + + return nil +} + +// Enriches the image metadata with size and checksum +func populateMetadata(data *ImageBuildData) error { + if err := populateImageFile(data.Source, &data.Metadata.Image); err != nil { + return fmt.Errorf("failed to populate metadata:\n%w", err) + } + + if err := populateVerityMetadata(data.VeritySource, data.Metadata.Verity); err != nil { + return fmt.Errorf("failed to populate verity metadata:\n%w", err) } - data.Metadata.Image.Sha384 = sha384 + + return nil +} + +func populateVerityMetadata(source string, verity *Verity) error { + if source == "" && verity == nil { + return nil + } + + if source == "" || verity == nil { + return fmt.Errorf("verity source and verity metadata must be both defined or both undefined") + } + + if err := populateImageFile(source, &verity.Image); err != nil { + return fmt.Errorf("failed to populate verity image metadata:\n%w", err) + } + return nil } diff --git a/toolkit/tools/pkg/imagecustomizerlib/cosimetadata.go b/toolkit/tools/pkg/imagecustomizerlib/cosimetadata.go index a240c6507..f1754719c 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/cosimetadata.go +++ b/toolkit/tools/pkg/imagecustomizerlib/cosimetadata.go @@ -9,18 +9,17 @@ type MetadataJson struct { } type Image struct { - Image ImageFile `json:"image"` - MountPoint string `json:"mountPoint"` - FsType string `json:"fsType"` - FsUuid string `json:"fsUuid"` - PartType string `json:"partType"` - Verity *Verity `json:"verity"` - UncompressedSize uint64 `json:"uncompressedSize"` + Image ImageFile `json:"image"` + MountPoint string `json:"mountPoint"` + FsType string `json:"fsType"` + FsUuid string `json:"fsUuid"` + PartType string `json:"partType"` + Verity *Verity `json:"verity"` } type Verity struct { - Image ImageFile `json:"image"` - Roothash string `json:"roothash"` + Image ImageFile `json:"image"` + Hash string `json:"hash"` } type ImageFile struct { diff --git a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go index d05959e41..a3f9fffe0 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go +++ b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go @@ -82,6 +82,14 @@ type ImageCustomizerParameters struct { imageUuid [UuidSize]byte imageUuidStr string + + verityMetadata []verityDeviceMetadata +} + +type verityDeviceMetadata struct { + hash string + dataPartUuid string + hashPartUuid string } func createImageCustomizerParameters(buildDir string, @@ -396,10 +404,18 @@ func customizeOSContents(ic *ImageCustomizerParameters) error { if len(ic.config.Storage.Verity) > 0 { // Customize image for dm-verity, setting up verity metadata and security features. - err = customizeVerityImageHelper(ic.buildDirAbs, ic.configPath, ic.config, ic.rawImageFile, partIdToPartUuid) + rootHash, dataPartUuid, roothashPartUuid, err := customizeVerityImageHelper(ic.buildDirAbs, ic.config, ic.rawImageFile, partIdToPartUuid) if err != nil { return err } + + verityMetadata := verityDeviceMetadata{ + hash: rootHash, + dataPartUuid: dataPartUuid, + hashPartUuid: roothashPartUuid, + } + + ic.verityMetadata = append(ic.verityMetadata, verityMetadata) } if ic.config.OS.Uki != nil { @@ -784,20 +800,20 @@ func shrinkFilesystemsHelper(buildImageFile string, verity []imagecustomizerapi. return nil } -func customizeVerityImageHelper(buildDir string, baseConfigPath string, config *imagecustomizerapi.Config, +func customizeVerityImageHelper(buildDir string, config *imagecustomizerapi.Config, buildImageFile string, partIdToPartUuid map[string]string, -) error { +) (string, string, string, error) { var err error loopback, err := safeloopback.NewLoopback(buildImageFile) if err != nil { - return fmt.Errorf("failed to connect to image file to provision verity:\n%w", err) + return "", "", "", fmt.Errorf("failed to connect to image file to provision verity:\n%w", err) } defer loopback.Close() diskPartitions, err := diskutils.GetDiskPartitions(loopback.DevicePath()) if err != nil { - return err + return "", "", "", err } // Verity support is limited to only rootfs at the moment, which is verified in the API validity checks. @@ -807,85 +823,96 @@ func customizeVerityImageHelper(buildDir string, baseConfigPath string, config * // Extract the partition block device path. dataPartition, err := idToPartitionBlockDevicePath(rootfsVerity.DataDeviceId, diskPartitions, partIdToPartUuid) if err != nil { - return err + return "", "", "", err } hashPartition, err := idToPartitionBlockDevicePath(rootfsVerity.HashDeviceId, diskPartitions, partIdToPartUuid) if err != nil { - return err + return "", "", "", err } // Extract root hash using regular expressions. verityOutput, _, err := shell.Execute("veritysetup", "format", dataPartition, hashPartition) if err != nil { - return fmt.Errorf("failed to calculate root hash:\n%w", err) + return "", "", "", fmt.Errorf("failed to calculate root hash:\n%w", err) } var rootHash string rootHashRegex, err := regexp.Compile(`Root hash:\s+([0-9a-fA-F]+)`) if err != nil { - // handle the error appropriately, for example: - return fmt.Errorf("failed to compile root hash regex: %w", err) + return "", "", "", fmt.Errorf("failed to compile root hash regex: %w", err) } rootHashMatches := rootHashRegex.FindStringSubmatch(verityOutput) if len(rootHashMatches) <= 1 { - return fmt.Errorf("failed to parse root hash from veritysetup output") + return "", "", "", fmt.Errorf("failed to parse root hash from veritysetup output") } rootHash = rootHashMatches[1] // Refresh disk partitions after running veritysetup so that the hash partition's UUID is correct. diskPartitions, err = diskutils.GetDiskPartitions(loopback.DevicePath()) if err != nil { - return err + return "", "", "", err + } + + // Identify the data partition UUID + dataPartUuid, ok := partIdToPartUuid[rootfsVerity.DataDeviceId] + if !ok { + return "", "", "", fmt.Errorf("failed to determine root partition UUID for DataDeviceId: %s", rootfsVerity.DataDeviceId) + } + + // Identify the hash partition UUID + hashPartUuid, ok := partIdToPartUuid[rootfsVerity.HashDeviceId] + if !ok { + return "", "", "", fmt.Errorf("failed to find hash partition UUID for HashDeviceId: %s", rootfsVerity.HashDeviceId) } systemBootPartition, err := findSystemBootPartition(diskPartitions) if err != nil { - return err + return "", "", "", err } bootPartition, err := findBootPartitionFromEsp(systemBootPartition, diskPartitions, buildDir) if err != nil { - return err + return "", "", "", err } bootPartitionTmpDir := filepath.Join(buildDir, tmpParitionDirName) // Temporarily mount the partition. bootPartitionMount, err := safemount.NewMount(bootPartition.Path, bootPartitionTmpDir, bootPartition.FileSystemType, 0, "", true) if err != nil { - return fmt.Errorf("failed to mount partition (%s):\n%w", bootPartition.Path, err) + return "", "", "", fmt.Errorf("failed to mount partition (%s):\n%w", bootPartition.Path, err) } defer bootPartitionMount.Close() grubCfgFullPath := filepath.Join(bootPartitionTmpDir, DefaultGrubCfgPath) if err != nil { - return fmt.Errorf("failed to stat file (%s):\n%w", grubCfgFullPath, err) + return "", "", "", fmt.Errorf("failed to stat file (%s):\n%w", grubCfgFullPath, err) } if config.OS.Uki != nil { // UKI is enabled, update kernel cmdline args file instead of grub.cfg. err = updateUkiKernelArgsForVerity(rootfsVerity, rootHash, partIdToPartUuid, diskPartitions, buildDir) if err != nil { - return fmt.Errorf("failed to update kernel cmdline arguments for verity:\n%w", err) + return "", "", "", fmt.Errorf("failed to update kernel cmdline arguments for verity:\n%w", err) } } else { // UKI is not enabled, update grub.cfg as usual. err = updateGrubConfigForVerity(rootfsVerity, rootHash, grubCfgFullPath, partIdToPartUuid, diskPartitions) if err != nil { - return fmt.Errorf("failed to update grub config for verity:\n%w", err) + return "", "", "", fmt.Errorf("failed to update grub config for verity:\n%w", err) } } err = bootPartitionMount.CleanClose() if err != nil { - return err + return "", "", "", err } err = loopback.CleanClose() if err != nil { - return err + return "", "", "", err } - return nil + return rootHash, dataPartUuid, hashPartUuid, nil } func checkDmVerityEnabled(rawImageFile string) error {