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