| // 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 ( |
| "crypto/rand" |
| "crypto/rsa" |
| "crypto/x509" |
| "encoding/pem" |
| "fmt" |
| "io" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "strings" |
| "testing" |
| |
| "cos.googlesource.com/cos/tools.git/src/pkg/tools/partutil" |
| ) |
| |
| const srcImage = "testdata/disk.img" |
| |
| func TestExtend(t *testing.T) { |
| diskSizeGB := 1 |
| oemFSSize := "10M" |
| targetOEMPartitionSector := uint64(40960) |
| tmpDir := t.TempDir() |
| outImage := filepath.Join(tmpDir, "out.img") |
| if err := Extend(srcImage, outImage, oemFSSize, diskSizeGB); err != nil { |
| t.Fatalf("Failed to run Extend: %v", err) |
| } |
| size, err := partutil.ReadPartitionSize(outImage, oemPartitionNum) |
| if err != nil { |
| t.Fatalf("Failed to read partition size: %v", err) |
| } |
| if size != targetOEMPartitionSector { |
| t.Fatalf("Wrong partition size, expected: %d, actual: %d ", targetOEMPartitionSector, size) |
| } |
| } |
| |
| func TestPreloadDir(t *testing.T) { |
| targetImage := "targetImage" |
| if err := copyFile(srcImage, targetImage); err != nil { |
| t.Fatalf("Failed to setup test image: %v", err) |
| } |
| defer os.Remove(targetImage) |
| |
| srcDir := "testdata/preload_dir" |
| oemFSSize := "1M" |
| partitionSize := int64(2097152) |
| oemFile, err := PreloadDir(srcDir, oemFSSize, partitionSize) |
| if err != nil { |
| t.Fatalf("Failed to run PreloadDir: %v", err) |
| } |
| |
| if err := WriteOEMFileToImage(oemFile, targetImage); err != nil { |
| t.Fatalf("Failed to write oem to image: %v", err) |
| } |
| |
| loopDev, err := setupLoopDevice(targetImage) |
| if err != nil { |
| t.Fatalf("Failed to setup loop device: %v", err) |
| } |
| defer detachLoopDevice(loopDev) |
| partitionDevice := loopDev + "p8" |
| tmpDir := t.TempDir() |
| mountCmd := exec.Command("sudo", "mount", partitionDevice, tmpDir) |
| if err := mountCmd.Run(); err != nil { |
| t.Fatalf("Failed to mount partition: %v", err) |
| } |
| defer exec.Command("sudo", "umount", tmpDir).Run() |
| |
| data, err := os.ReadFile(tmpDir + "/a.txt") |
| if err != nil { |
| t.Fatalf("Could not read a.txt: %v", err) |
| } |
| expectedString := "1234567890" |
| if string(data) != expectedString { |
| t.Fatalf("Wrong data. Want: %q, got: %q", expectedString, string(data)) |
| } |
| } |
| |
| func copyFile(src, dst string) error { |
| sourceFile, err := os.Open(src) |
| if err != nil { |
| return fmt.Errorf("Failed to open source image: %v", err) |
| } |
| defer sourceFile.Close() |
| |
| destFile, err := os.Create(dst) |
| if err != nil { |
| return fmt.Errorf("Failed to create test image: %v", err) |
| } |
| defer destFile.Close() |
| _, err = io.Copy(destFile, sourceFile) |
| return nil |
| } |
| |
| func setupLoopDevice(loopDev string) (string, error) { |
| cmd := exec.Command("sudo", "losetup", "-f", "-P", "--show", loopDev) |
| output, err := cmd.CombinedOutput() |
| if err != nil { |
| return "", fmt.Errorf("failed to setup loop device: %v, output: %s", err, string(output)) |
| } |
| // Output usually contains a newline, e.g., "/dev/loop0\n" |
| loopDevice := strings.TrimSpace(string(output)) |
| if loopDevice == "" { |
| return "", fmt.Errorf("losetup returned an empty device name") |
| } |
| return loopDevice, nil |
| } |
| |
| func detachLoopDevice(device string) error { |
| return exec.Command("sudo", "losetup", "-d", device).Run() |
| } |
| |
| func TestSignIMAHashes_HashOnly(t *testing.T) { |
| oemImg, tmpDir := setupTestOEMImage(t) |
| |
| hashImg := filepath.Join(tmpDir, "hash.img") |
| if err := copyFile(oemImg, hashImg); err != nil { |
| t.Fatalf("Failed to copy image: %v", err) |
| } |
| |
| if err := SignIMAHashes(hashImg, "hash", ""); err != nil { |
| t.Fatalf("SignIMAHashes failed with hash-only: %v", err) |
| } |
| |
| // Mount the image manually to verify that the file has the security.ima xattr. |
| mntDir := t.TempDir() |
| cmd := exec.Command("mount", "-o", "loop", hashImg, mntDir) |
| if err := cmd.Run(); err != nil { |
| t.Fatalf("Failed to mount image for verification: %v", err) |
| } |
| defer exec.Command("umount", mntDir).Run() |
| |
| // Check xattr on the signed file. |
| getfattrCmd := exec.Command("getfattr", "-n", "security.ima", filepath.Join(mntDir, "test.txt")) |
| if err := getfattrCmd.Run(); err != nil { |
| t.Fatalf("security.ima xattr is missing on hash-signed file: %v", err) |
| } |
| } |
| |
| func TestSignIMAHashes_Signature(t *testing.T) { |
| oemImg, tmpDir := setupTestOEMImage(t) |
| |
| sigImg := filepath.Join(tmpDir, "sig.img") |
| if err := copyFile(oemImg, sigImg); err != nil { |
| t.Fatalf("Failed to copy image: %v", err) |
| } |
| |
| // Generate a private key. |
| keyPath := filepath.Join(tmpDir, "private_key.pem") |
| if err := generatePrivateKey(keyPath); err != nil { |
| t.Fatalf("Failed to generate private key: %v", err) |
| } |
| |
| // Sign the hashes using the private key. |
| if err := SignIMAHashes(sigImg, "sign", keyPath); err != nil { |
| t.Fatalf("SignIMAHashes failed with signature: %v", err) |
| } |
| |
| // Mount the image manually to verify. |
| mntDir := t.TempDir() |
| cmd := exec.Command("mount", "-o", "loop", sigImg, mntDir) |
| if err := cmd.Run(); err != nil { |
| t.Fatalf("Failed to mount image for verification: %v", err) |
| } |
| defer exec.Command("umount", mntDir).Run() |
| |
| // Check xattr on the signed file. |
| getfattrCmd := exec.Command("getfattr", "-n", "security.ima", filepath.Join(mntDir, "test.txt")) |
| if err := getfattrCmd.Run(); err != nil { |
| t.Fatalf("security.ima xattr is missing on signature-signed file: %v", err) |
| } |
| } |
| |
| func setupTestOEMImage(t *testing.T) (string, string) { |
| t.Helper() |
| if _, err := exec.LookPath("evmctl"); err != nil { |
| t.Fatalf("Skipping test: evmctl is not installed") |
| } |
| if _, err := exec.LookPath("getfattr"); err != nil { |
| t.Fatalf("Skipping test: getfattr is not installed") |
| } |
| |
| tmpDir := t.TempDir() |
| |
| // Create a dummy source directory with some files. |
| srcDir := filepath.Join(tmpDir, "src") |
| if err := os.Mkdir(srcDir, 0755); err != nil { |
| t.Fatalf("Failed to create source dir: %v", err) |
| } |
| testFile := filepath.Join(srcDir, "test.txt") |
| if err := os.WriteFile(testFile, []byte("hello world"), 0644); err != nil { |
| t.Fatalf("Failed to write test file: %v", err) |
| } |
| |
| // Preload the directory to generate the ext4 image. |
| oemFSSize := "1M" |
| partitionSize := int64(2097152) |
| oemImg, err := PreloadDir(srcDir, oemFSSize, partitionSize) |
| if err != nil { |
| t.Fatalf("Failed to preload dir: %v", err) |
| } |
| t.Cleanup(func() { |
| os.Remove(oemImg) |
| }) |
| |
| return oemImg, tmpDir |
| } |
| |
| func generatePrivateKey(path string) error { |
| key, err := rsa.GenerateKey(rand.Reader, 2048) |
| if err != nil { |
| return err |
| } |
| file, err := os.Create(path) |
| if err != nil { |
| return err |
| } |
| defer file.Close() |
| |
| pemBlock := &pem.Block{ |
| Type: "RSA PRIVATE KEY", |
| Bytes: x509.MarshalPKCS1PrivateKey(key), |
| } |
| return pem.Encode(file, pemBlock) |
| } |