| // Copyright 2026 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package oempreloader |
| |
| import ( |
| "bytes" |
| "fmt" |
| "io" |
| "os" |
| "os/exec" |
| "strconv" |
| "strings" |
| |
| "cos.googlesource.com/cos/tools.git/src/pkg/tools/partutil" |
| ) |
| |
| const ( |
| dmRootName = "oemroot" |
| grubPath = "/efi/boot/grub.cfg" |
| ) |
| |
| // RunVeritysetup handles veritysetup. Returns root hash and salt. |
| func RunVeritysetup(oemPartitionFile, oemFSSize string) (string, string, error) { |
| fmt.Printf("Executing veritysteup...\n") |
| oemFSSize4K, err := sizeStringTo4K(oemFSSize) |
| if err != nil { |
| return "", "", fmt.Errorf("failed to convert OEM FS size to 4K bytes: %v", err) |
| } |
| cmd := exec.Command( |
| "veritysetup", |
| "format", |
| oemPartitionFile, |
| oemPartitionFile, |
| "--data-block-size=4096", |
| "--hash-block-size=4096", |
| fmt.Sprintf("--data-blocks=%s", strconv.FormatUint(oemFSSize4K, 10)), |
| fmt.Sprintf("--hash-offset=%s", strconv.FormatUint(oemFSSize4K<<12, 10)), |
| "--no-superblock", |
| "--format=0", |
| ) |
| |
| var verityBuf bytes.Buffer |
| cmd.Stdout = &verityBuf |
| cmd.Stderr = os.Stderr |
| if err := cmd.Run(); err != nil { |
| return "", "", fmt.Errorf("failed to run veritysetup, stdout: %s, err: %v", verityBuf.String(), err) |
| } |
| return parseVerityOutput(verityBuf.String()) |
| } |
| |
| // UpdateKernelCmdLine extracts EFI partition, then updates kernel command |
| // line in it, and finally writes it back to the image file. |
| // Need to change after switching to command line baked inside kernel. |
| func UpdateKernelCmdLine(imagePath, hash, salt, oemFSSize string) error { |
| fmt.Printf("Updating kernel command line...\n") |
| oemFSSize4K, err := sizeStringTo4K(oemFSSize) |
| if err != nil { |
| return fmt.Errorf("failed to convert OEM FS size to 4K bytes: %v", err) |
| } |
| efiFile, err := extractEFIPartition(imagePath) |
| if err != nil { |
| return fmt.Errorf("failed to extract EFI partition: %v", err) |
| } |
| oldGrub, err := readGrub(efiFile) |
| if err != nil { |
| return fmt.Errorf("failed to read grub content: %v", err) |
| } |
| |
| uuid, err := fetchPartitionUUID(imagePath, OEMPartitionNum) |
| if err != nil { |
| return fmt.Errorf("failed to find OEM partition UUID: %v", err) |
| } |
| |
| newGrub, err := updateGRUB(oldGrub, dmRootName, uuid, hash, salt, oemFSSize4K) |
| if err != nil { |
| return fmt.Errorf("failed to add dm-verity entry to grub: %v", err) |
| } |
| if err := writeGrub(efiFile, newGrub); err != nil { |
| return fmt.Errorf("failed to write new grub file: %v", err) |
| } |
| if err := WritePartitionFileToImage(efiFile, imagePath, efiPartitionNum); err != nil { |
| return fmt.Errorf("failed to write new grub file: %v", err) |
| } |
| return nil |
| } |
| |
| // Output of veritysetup is like: |
| // VERITY header information for /dev/sdb1 |
| // UUID: |
| // Hash type: 0 |
| // Data blocks: 2048 |
| // Data block size: 4096 |
| // Hash block size: 4096 |
| // Hash algorithm: sha256 |
| // Salt: 9cd7ba29a1771b2097a7d72be8c13b29766d7617c3b924eb0cf23ff5071fee47 |
| // Root hash: d6b862d01e01e6417a1b5e7eb0eed2a2189594b74325dd0749cd83bbf78f5dc8 |
| func parseVerityOutput(output string) (string, string, error) { |
| var rootHash, salt string |
| lines := strings.Split(output, "\n") |
| for _, line := range lines { |
| if strings.Contains(line, "Root hash:") { |
| fields := strings.Fields(line) |
| if len(fields) >= 3 { |
| rootHash = fields[2] |
| } |
| } |
| if strings.Contains(line, "Salt:") { |
| fields := strings.Fields(line) |
| if len(fields) >= 2 { |
| salt = fields[1] |
| } |
| } |
| } |
| |
| if rootHash == "" || salt == "" { |
| return "", "", fmt.Errorf("failed to extract verity parameters from output") |
| } |
| return rootHash, salt, nil |
| } |
| |
| func extractEFIPartition(imagePath string) (string, error) { |
| efiStartSector, err := partutil.ReadPartitionStart(imagePath, efiPartitionNum) |
| if err != nil { |
| return "", fmt.Errorf("failed to read OEM partition start: %v", err) |
| } |
| efiSizeSector, err := partutil.ReadPartitionSize(imagePath, efiPartitionNum) |
| if err != nil { |
| return "", fmt.Errorf("failed to read OEM partition start: %v", err) |
| } |
| |
| img, err := os.Open(imagePath) |
| if err != nil { |
| return "", fmt.Errorf("failed to open image file: %v", err) |
| } |
| defer img.Close() |
| |
| efiFileName := "efi_file" |
| out, err := os.Create(efiFileName) |
| if err != nil { |
| return "", fmt.Errorf("failed to create efi file: %v", err) |
| } |
| defer out.Close() |
| |
| _, err = img.Seek(int64(efiStartSector*sectorSize), io.SeekStart) |
| if err != nil { |
| return "", fmt.Errorf("failed to seek to efi start: %v", err) |
| } |
| _, err = io.CopyN(out, img, int64(efiSizeSector*sectorSize)) |
| if err != nil { |
| return "", fmt.Errorf("failed to copy from image: %v", err) |
| } |
| return efiFileName, nil |
| } |
| |
| func readGrub(efiPartitionFile string) (string, error) { |
| cmd := exec.Command( |
| "mtype", |
| "-i", |
| efiPartitionFile, |
| fmt.Sprintf("::%s", grubPath), |
| ) |
| outputBytes, err := cmd.CombinedOutput() |
| if err != nil { |
| return "", fmt.Errorf("mtype failed: %v, output: %s", err, string(outputBytes)) |
| } |
| return string(outputBytes), nil |
| } |
| |
| func writeGrub(efiPartitionFile, content string) error { |
| cmd := exec.Command( |
| "mcopy", |
| "-o", |
| "-i", efiPartitionFile, |
| "-", |
| fmt.Sprintf("::%s", grubPath), |
| ) |
| |
| cmd.Stdin = bytes.NewBufferString(content) |
| var stderr bytes.Buffer |
| cmd.Stderr = &stderr |
| err := cmd.Run() |
| if err != nil { |
| return fmt.Errorf("mcopy failed: %v, stderr: %s", err, stderr.String()) |
| } |
| return nil |
| } |
| |
| func fetchPartitionUUID(imagePath string, partitionNum int) (string, error) { |
| oemStartSector, err := partutil.ReadPartitionStart(imagePath, partitionNum) |
| if err != nil { |
| return "", fmt.Errorf("failed to read OEM partition start: %v", err) |
| } |
| offset := int64(oemStartSector * sectorSize) |
| cmd := exec.Command( |
| "blkid", |
| "-o", "value", |
| "-s", "UUID", |
| "-p", |
| fmt.Sprintf("--offset=%d", offset), |
| imagePath, |
| ) |
| outputBytes, err := cmd.CombinedOutput() |
| if err != nil { |
| return "", fmt.Errorf("blkid failed: %v, output: %s", err, string(outputBytes)) |
| } |
| return string(outputBytes), nil |
| } |
| |
| // Temporary solution, will remove once migrate to commandline baked in kernel. |
| // updateGRUB appends an dm-verity table entry to kernel command line in grub.cfg |
| // A target line in grub.cfg looks like |
| // ...... root=/dev/dm-0 dm="1 vroot none ro 1,0 4077568 verity |
| // payload=PARTUUID=8AC60384-1187-9E49-91CE-3ABD8DA295A7 |
| // hashtree=PARTUUID=8AC60384-1187-9E49-91CE-3ABD8DA295A7 hashstart=4077568 alg=sha256 |
| // root_hexdigest=xxxxxxxx salt=xxxxxxxx" |
| func updateGRUB(grubContent, name, partUUID, hash, salt string, oemFSSize4K uint64) (string, error) { |
| // from 4K blocks to 512B sectors |
| oemFSSizeSector := oemFSSize4K << 3 |
| entryStringV0 := fmt.Sprintf("%s none ro 1, 0 %d verity payload=PARTUUID=%s hashtree=PARTUUID=%s "+ |
| "hashstart=%d alg=sha256 root_hexdigest=%s salt=%s\"", name, oemFSSizeSector, |
| partUUID, partUUID, oemFSSizeSector, hash, salt) |
| entryStringV1 := fmt.Sprintf("%s,,,ro,0 %d verity 0 PARTUUID=%s PARTUUID=%s "+ |
| "4096 4096 %d %d sha256 %s %s\"", name, oemFSSizeSector, |
| partUUID, partUUID, oemFSSize4K, oemFSSize4K, hash, salt) |
| lines := strings.Split(string(grubContent), "\n") |
| // add the entry to all kernel command lines containing "dm=" |
| dmVersion := 0 |
| for idx, line := range lines { |
| if !strings.Contains(line, "dm=") && |
| !strings.Contains(line, "dm-mod.create=") { |
| continue |
| } |
| var startPos = strings.Index(line, "dm=") |
| if startPos == -1 { |
| startPos = strings.Index(line, "dm-mod.create=") |
| dmVersion = 1 |
| } |
| // remove the end quote. |
| lineBuf := []rune(line[:len(line)-1]) |
| if dmVersion == 0 { |
| // add number of entries. |
| lineBuf[startPos+4] = '2' |
| lines[idx] = strings.Join(append(strings.Split(string(lineBuf), ","), entryStringV0), ",") |
| } else { |
| configs := []string{string(lineBuf), entryStringV1} |
| lines[idx] = strings.Join(configs, ";") |
| } |
| } |
| // new content of grub.cfg |
| newGrubContent := strings.Join(lines, "\n") |
| return newGrubContent, nil |
| } |
| |
| func sizeStringTo4K(size string) (uint64, error) { |
| bytes, err := partutil.ConvertSizeToBytes(size) |
| if err != nil { |
| return 0, fmt.Errorf("failed to parse size string: %v", err) |
| } |
| return uint64(bytes / 1024 / 4), nil |
| } |