| // 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" |
| "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, cmdlineScript 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) |
| } |
| grubFile, err := extractGrubFile(imagePath) |
| if err != nil { |
| return fmt.Errorf("failed to extract EFI partition: %v", err) |
| } |
| oldGrub, err := os.ReadFile(grubFile) |
| if err != nil { |
| return fmt.Errorf("failed to read grub file: %v", err) |
| } |
| |
| uuid, err := fetchPartitionUUID(imagePath, oemPartitionNum) |
| if err != nil { |
| return fmt.Errorf("failed to find OEM partition UUID: %v", err) |
| } |
| |
| // update dm-verity to grub. |
| newGrub, err := updateGRUB(string(oldGrub), dmRootName, uuid, hash, salt, oemFSSize4K) |
| if err != nil { |
| return fmt.Errorf("failed to add dm-verity entry to grub: %v", err) |
| } |
| if err := os.WriteFile(grubFile, []byte(newGrub), 0644); err != nil { |
| return fmt.Errorf("failed to write to grub file: %v", err) |
| } |
| |
| if cmdlineScript != "" { |
| if err := runCmdlineScript(cmdlineScript, grubFile); err != nil { |
| return fmt.Errorf("failed to run cmdline script: %v", err) |
| } |
| } |
| |
| if err := writeGrubToImage(grubFile, imagePath); err != nil { |
| return fmt.Errorf("failed to write grub to image 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 extractGrubFile(imagePath string) (string, error) { |
| efiStart, err := readPartitionStart(imagePath, efiPartitionNum) |
| if err != nil { |
| return "", fmt.Errorf("failed to read efi start: %v", err) |
| } |
| |
| grubFileName := "grub_file" |
| cmd := exec.Command( |
| "mcopy", |
| "-i", fmt.Sprintf("%s@@%d", imagePath, efiStart), |
| fmt.Sprintf("::%s", grubPath), |
| grubFileName, |
| ) |
| outputBytes, err := cmd.CombinedOutput() |
| if err != nil { |
| return "", fmt.Errorf("mcopy failed: %v, output: %s", err, string(outputBytes)) |
| } |
| |
| return grubFileName, nil |
| } |
| |
| func writeGrubToImage(grubFile, imagePath string) error { |
| efiStart, err := readPartitionStart(imagePath, efiPartitionNum) |
| if err != nil { |
| return fmt.Errorf("failed to read efi start: %v", err) |
| } |
| cmd := exec.Command( |
| "mcopy", |
| "-o", |
| "-i", fmt.Sprintf("%s@@%d", imagePath, efiStart), |
| grubFile, |
| fmt.Sprintf("::%s", grubPath), |
| ) |
| outputBytes, err := cmd.CombinedOutput() |
| if err != nil { |
| return fmt.Errorf("mcopy failed: %v, output: %s", err, string(outputBytes)) |
| } |
| |
| return nil |
| } |
| |
| func fetchPartitionUUID(imagePath string, partitionNum int) (string, error) { |
| partitionStart, err := readPartitionStart(imagePath, partitionNum) |
| if err != nil { |
| return "", fmt.Errorf("failed to read OEM partition start: %v", err) |
| } |
| cmd := exec.Command( |
| "blkid", |
| "-o", "value", |
| "-s", "UUID", |
| "-p", |
| fmt.Sprintf("--offset=%d", partitionStart), |
| 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 |
| } |
| |
| // readPartitionStart reads partition start in bytes. |
| func readPartitionStart(imagePath string, partitionNum int) (uint64, error) { |
| efiStartSector, err := partutil.ReadPartitionStart(imagePath, partitionNum) |
| if err != nil { |
| return 0, fmt.Errorf("failed to read OEM partition start: %v", err) |
| } |
| return efiStartSector * sectorSize, nil |
| } |
| |
| func runCmdlineScript(script, grubFile string) error { |
| cmd := exec.Command("bash", script, grubFile) |
| outputBytes, err := cmd.CombinedOutput() |
| if err != nil { |
| return fmt.Errorf("failed to run bash script: %v, output: %s", err, string(outputBytes)) |
| } |
| return nil |
| } |