blob: fb49cac89e8b268395e176271b8f1ac08fc484a4 [file] [log] [blame]
// 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
}