carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package host
4
5import (
6 "context"
7 "crypto/sha256"
8 "encoding/hex"
9 "github.com/carlosframework/platform/internal/safefile"
10 "io"
11 "os"
12 "os/exec"
13 "path/filepath"
14 "testing"
15
16 "golang.org/x/sys/unix"
17)
18
19func TestClaimExecutablePinsVerifiedBytes(t *testing.T) {
20 if os.Geteuid() != 0 {
21 t.Skip("requires root to create and replace a trusted executable")
22 }
23 path := filepath.Join(t.TempDir(), "binary")
24 body := []byte("approved executable bytes")
25 if err := os.WriteFile(path, body, 0555); err != nil {
26 t.Fatal(err)
27 }
28 digest := sha256.Sum256(body)
29 f, err := verifiedClaimExecutable(context.Background(), exec.Command(path), path, hex.EncodeToString(digest[:]))
30 if err != nil {
31 t.Fatal(err)
32 }
33 defer f.Close()
34 if err = os.Rename(path, path+".old"); err != nil {
35 t.Fatal(err)
36 }
37 if err = os.WriteFile(path, []byte("replacement"), 0555); err != nil {
38 t.Fatal(err)
39 }
40 got, err := io.ReadAll(f)
41 if err != nil || string(got) != string(body) {
42 t.Fatalf("pinned bytes=%q %v", got, err)
43 }
44 if next, err := verifiedClaimExecutable(context.Background(), exec.Command(path), path, hex.EncodeToString(digest[:])); err == nil {
45 next.Close()
46 t.Fatal("changed bytes admitted")
47 }
48}
49
50func TestClaimExecutableRefusesNonRootOwner(t *testing.T) {
51 path := filepath.Join(t.TempDir(), "binary")
52 body := []byte("approved executable bytes")
53 if err := os.WriteFile(path, body, 0555); err != nil {
54 t.Fatal(err)
55 }
56 if os.Geteuid() == 0 {
57 if err := os.Chown(path, 65534, 65534); err != nil {
58 t.Fatal(err)
59 }
60 }
61 digest := sha256.Sum256(body)
62 if f, err := verifiedClaimExecutable(t.Context(), exec.Command(path), path, hex.EncodeToString(digest[:])); err == nil {
63 f.Close()
64 t.Fatal("non-root-owned executable accepted")
65 }
66}
67
68func TestClaimExecutableAcceptsRootOwnedSystemBinary(t *testing.T) {
69 path, err := filepath.EvalSymlinks("/usr/bin/true")
70 if err != nil {
71 t.Skipf("requires a root-owned system executable: %v", err)
72 }
73 var st unix.Stat_t
74 if err := unix.Stat(path, &st); err != nil || st.Uid != 0 || st.Nlink != 1 || st.Mode&0022 != 0 {
75 t.Skip("requires a root-owned single-link system executable")
76 }
77 body, err := os.ReadFile(path)
78 if err != nil {
79 t.Fatal(err)
80 }
81 digest := sha256.Sum256(body)
82 f, err := verifiedClaimExecutable(t.Context(), exec.Command(path), path, hex.EncodeToString(digest[:]))
83 if err != nil {
84 t.Fatal(err)
85 }
86 defer f.Close()
87 if f.Name() != path {
88 t.Fatalf("verified executable name %q, want %q", f.Name(), path)
89 }
90 got, err := io.ReadAll(f)
91 if err != nil || string(got) != string(body) {
92 t.Fatalf("pinned system executable differs: %v", err)
93 }
94 digest[0] ^= 1
95 if f, err := verifiedClaimExecutable(t.Context(), exec.Command(path), path, hex.EncodeToString(digest[:])); err == nil {
96 f.Close()
97 t.Fatal("wrong system executable digest accepted")
98 }
99}
100func TestClaimExecutableRefusesSymlinkAndWritableArtifact(t *testing.T) {
101 dir := t.TempDir()
102 path := filepath.Join(dir, "binary")
103 body := []byte("x")
104 digest := sha256.Sum256(body)
105 if err := os.WriteFile(path, body, 0777); err != nil {
106 t.Fatal(err)
107 }
108 if err := os.Chmod(path, 0777); err != nil {
109 t.Fatal(err)
110 }
111 if f, err := verifiedClaimExecutable(context.Background(), exec.Command(path), path, hex.EncodeToString(digest[:])); err == nil {
112 f.Close()
113 t.Fatal("mutable artifact admitted")
114 }
115 if err := os.Chmod(path, 0555); err != nil {
116 t.Fatal(err)
117 }
118 link := filepath.Join(dir, "link")
119 if err := os.Symlink(path, link); err != nil {
120 t.Fatal(err)
121 }
122 if f, err := verifiedClaimExecutable(context.Background(), exec.Command(link), link, hex.EncodeToString(digest[:])); err == nil {
123 f.Close()
124 t.Fatal("symlink admitted")
125 }
126}
127
128func TestClaimExecutableRefusesHardLinkAndSymlinkParent(t *testing.T) {
129 for _, kind := range []string{"hardlink", "parent"} {
130 t.Run(kind, func(t *testing.T) {
131 dir := t.TempDir()
132 path := filepath.Join(dir, "binary")
133 body := []byte("approved executable bytes")
134 if err := os.WriteFile(path, body, 0555); err != nil {
135 t.Fatal(err)
136 }
137 unsafe := filepath.Join(dir, "unsafe")
138 if kind == "hardlink" {
139 if err := os.Link(path, unsafe); err != nil {
140 t.Fatal(err)
141 }
142 } else {
143 if err := os.Symlink(dir, unsafe); err != nil {
144 t.Fatal(err)
145 }
146 unsafe = filepath.Join(unsafe, "binary")
147 }
148 digest := sha256.Sum256(body)
149 f, err := verifiedClaimExecutable(t.Context(), exec.Command(unsafe), unsafe, hex.EncodeToString(digest[:]))
150 if err == nil {
151 f.Close()
152 t.Fatal("unsafe executable accepted")
153 }
154 })
155 }
156}
157
158func TestClaimExecutableNameKeepsPinnedFile(t *testing.T) {
159 path := filepath.Join(t.TempDir(), "program")
160 if err := os.WriteFile(path, []byte("verified bytes"), 0700); err != nil {
161 t.Fatal(err)
162 }
163 f, err := safefile.OpenFile(path, os.O_RDONLY, 0)
164 if err != nil {
165 t.Fatal(err)
166 }
167 t.Cleanup(func() { f.Close() })
168 before, err := f.Stat()
169 if err != nil {
170 t.Fatal(err)
171 }
172 if err := os.Rename(path, path+".old"); err != nil {
173 t.Fatal(err)
174 }
175 if err := os.WriteFile(path, []byte("replacement"), 0700); err != nil {
176 t.Fatal(err)
177 }
178 named, err := claimExecutableName(f, path)
179 if err != nil {
180 t.Fatal(err)
181 }
182 defer named.Close()
183 after, err := named.Stat()
184 if err != nil {
185 t.Fatal(err)
186 }
187 if named.Name() != path || !os.SameFile(before, after) {
188 t.Fatal("named executable lost its identity", named.Name(), path)
189 }
190 body, err := io.ReadAll(named)
191 if err != nil || string(body) != "verified bytes" {
192 t.Fatal(string(body), err)
193 }
194}
195