| // 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()) | |
| } | |
| func dmEntryOEM(name, partUUID, hash, salt string, oemFSSize4K uint64) string { | |
| oemFSSizeSector := oemFSSize4K << 3 | |
| return 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) | |
| } | |
| // 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, vmlinux string, cmdlineSedCommands []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) | |
| } | |
| uuid, err := fetchOEMUUID(imagePath) | |
| if err != nil { | |
| return fmt.Errorf("failed to find OEM partition UUID: %v", err) | |
| } | |
| oemCommand := fmt.Sprintf( | |
| `s|dm-mod.create="\(.*\)"|dm-mod.create="\1;%s"|g`, | |
| dmEntryOEM(dmRootName, uuid, hash, salt, oemFSSize4K)) | |
| sedCommands := append([]string{oemCommand}, cmdlineSedCommands...) | |
| for _, sedCmd := range sedCommands { | |
| cmd := exec.Command("cos_kernel_args", "--vmlinux", vmlinux, "sed", "--command", sedCmd, imagePath) | |
| cmd.Stdout = os.Stdout | |
| cmd.Stderr = os.Stderr | |
| if err := cmd.Run(); err != nil { | |
| return fmt.Errorf("error with sed command %q: %v", sedCmd, 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 fetchOEMUUID(imagePath string) (string, error) { | |
| return partutil.ReadPartitionUUID(imagePath, oemPartitionNum) | |
| } | |
| // 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 | |
| } |