//go:build linux package host import ( "context" "crypto/sha256" "encoding/hex" "github.com/carlosframework/platform/internal/safefile" "io" "os" "os/exec" "path/filepath" "testing" "golang.org/x/sys/unix" ) func TestClaimExecutablePinsVerifiedBytes(t *testing.T) { if os.Geteuid() != 0 { t.Skip("requires root to create and replace a trusted executable") } path := filepath.Join(t.TempDir(), "binary") body := []byte("approved executable bytes") if err := os.WriteFile(path, body, 0555); err != nil { t.Fatal(err) } digest := sha256.Sum256(body) f, err := verifiedClaimExecutable(context.Background(), exec.Command(path), path, hex.EncodeToString(digest[:])) if err != nil { t.Fatal(err) } defer f.Close() if err = os.Rename(path, path+".old"); err != nil { t.Fatal(err) } if err = os.WriteFile(path, []byte("replacement"), 0555); err != nil { t.Fatal(err) } got, err := io.ReadAll(f) if err != nil || string(got) != string(body) { t.Fatalf("pinned bytes=%q %v", got, err) } if next, err := verifiedClaimExecutable(context.Background(), exec.Command(path), path, hex.EncodeToString(digest[:])); err == nil { next.Close() t.Fatal("changed bytes admitted") } } func TestClaimExecutableRefusesNonRootOwner(t *testing.T) { path := filepath.Join(t.TempDir(), "binary") body := []byte("approved executable bytes") if err := os.WriteFile(path, body, 0555); err != nil { t.Fatal(err) } if os.Geteuid() == 0 { if err := os.Chown(path, 65534, 65534); err != nil { t.Fatal(err) } } digest := sha256.Sum256(body) if f, err := verifiedClaimExecutable(t.Context(), exec.Command(path), path, hex.EncodeToString(digest[:])); err == nil { f.Close() t.Fatal("non-root-owned executable accepted") } } func TestClaimExecutableAcceptsRootOwnedSystemBinary(t *testing.T) { path, err := filepath.EvalSymlinks("/usr/bin/true") if err != nil { t.Skipf("requires a root-owned system executable: %v", err) } var st unix.Stat_t if err := unix.Stat(path, &st); err != nil || st.Uid != 0 || st.Nlink != 1 || st.Mode&0022 != 0 { t.Skip("requires a root-owned single-link system executable") } body, err := os.ReadFile(path) if err != nil { t.Fatal(err) } digest := sha256.Sum256(body) f, err := verifiedClaimExecutable(t.Context(), exec.Command(path), path, hex.EncodeToString(digest[:])) if err != nil { t.Fatal(err) } defer f.Close() if f.Name() != path { t.Fatalf("verified executable name %q, want %q", f.Name(), path) } got, err := io.ReadAll(f) if err != nil || string(got) != string(body) { t.Fatalf("pinned system executable differs: %v", err) } digest[0] ^= 1 if f, err := verifiedClaimExecutable(t.Context(), exec.Command(path), path, hex.EncodeToString(digest[:])); err == nil { f.Close() t.Fatal("wrong system executable digest accepted") } } func TestClaimExecutableRefusesSymlinkAndWritableArtifact(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "binary") body := []byte("x") digest := sha256.Sum256(body) if err := os.WriteFile(path, body, 0777); err != nil { t.Fatal(err) } if err := os.Chmod(path, 0777); err != nil { t.Fatal(err) } if f, err := verifiedClaimExecutable(context.Background(), exec.Command(path), path, hex.EncodeToString(digest[:])); err == nil { f.Close() t.Fatal("mutable artifact admitted") } if err := os.Chmod(path, 0555); err != nil { t.Fatal(err) } link := filepath.Join(dir, "link") if err := os.Symlink(path, link); err != nil { t.Fatal(err) } if f, err := verifiedClaimExecutable(context.Background(), exec.Command(link), link, hex.EncodeToString(digest[:])); err == nil { f.Close() t.Fatal("symlink admitted") } } func TestClaimExecutableRefusesHardLinkAndSymlinkParent(t *testing.T) { for _, kind := range []string{"hardlink", "parent"} { t.Run(kind, func(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "binary") body := []byte("approved executable bytes") if err := os.WriteFile(path, body, 0555); err != nil { t.Fatal(err) } unsafe := filepath.Join(dir, "unsafe") if kind == "hardlink" { if err := os.Link(path, unsafe); err != nil { t.Fatal(err) } } else { if err := os.Symlink(dir, unsafe); err != nil { t.Fatal(err) } unsafe = filepath.Join(unsafe, "binary") } digest := sha256.Sum256(body) f, err := verifiedClaimExecutable(t.Context(), exec.Command(unsafe), unsafe, hex.EncodeToString(digest[:])) if err == nil { f.Close() t.Fatal("unsafe executable accepted") } }) } } func TestClaimExecutableNameKeepsPinnedFile(t *testing.T) { path := filepath.Join(t.TempDir(), "program") if err := os.WriteFile(path, []byte("verified bytes"), 0700); err != nil { t.Fatal(err) } f, err := safefile.OpenFile(path, os.O_RDONLY, 0) if err != nil { t.Fatal(err) } t.Cleanup(func() { f.Close() }) before, err := f.Stat() if err != nil { t.Fatal(err) } if err := os.Rename(path, path+".old"); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte("replacement"), 0700); err != nil { t.Fatal(err) } named, err := claimExecutableName(f, path) if err != nil { t.Fatal(err) } defer named.Close() after, err := named.Stat() if err != nil { t.Fatal(err) } if named.Name() != path || !os.SameFile(before, after) { t.Fatal("named executable lost its identity", named.Name(), path) } body, err := io.ReadAll(named) if err != nil || string(body) != "verified bytes" { t.Fatal(string(body), err) } }