oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package operator
4
5import (
6 "bytes"
7 "context"
8 "crypto/sha256"
9 "database/sql"
10 "encoding/hex"
11 "encoding/json"
12 "errors"
13 "io"
14 "os"
15 "os/exec"
16 "path/filepath"
17 "strings"
18 "testing"
19 "time"
20
21 "amadan.net/carlos/platform.git/launchctx"
22 family "amadan.net/rastrillo/rastrillo/crypto"
23 "golang.org/x/sys/unix"
24)
25
26func commandConfigFixture(v *Verifier) commandConfig {
27 p := v.pins
28 return commandConfig{1, "bds", "home", "home.example", p.Role, p.Origin, p.Installation, p.Registry, p.Device, p.PublicKey, v.kid, p.Actions}
29}
30
31func TestCommandConfigRequiresExactPins(t *testing.T) {
32 _, v, _ := fixture(t)
33 c := commandConfigFixture(v)
34 raw, err := json.Marshal(c)
35 if err != nil {
36 t.Fatal(err)
37 }
38 if _, _, err := parseCommandConfig(string(raw)); err != nil {
39 t.Fatal(err)
40 }
41 var fields map[string]json.RawMessage
42 if json.Unmarshal(raw, &fields) != nil {
43 t.Fatal("fixture")
44 }
45 for key, value := range fields {
46 delete(fields, key)
47 missing, _ := json.Marshal(fields)
48 if _, _, err := parseCommandConfig(string(missing)); err == nil {
49 t.Fatal("missing pin accepted", key)
50 }
51 fields[key] = value
52 }
53 for _, bad := range []string{"", string(raw) + string(raw), strings.Repeat("x", 8193), string(raw) + strings.Repeat(" ", 8193-len(raw)), strings.Replace(string(raw), `"version":1`, `"version":1,"version":1`, 1), strings.Replace(string(raw), `"version":1`, `"version":1,"extra":"x"`, 1), strings.Replace(string(raw), v.kid, strings.Repeat("a", 64), 1)} {
54 if _, _, err := parseCommandConfig(bad); err == nil {
55 t.Fatal("bad configuration accepted")
56 }
57 }
58}
59
60func TestCommandInvocationChild(t *testing.T) {
61 if os.Getenv("HOME_COMMAND_FIXTURE") == "" {
62 return
63 }
64 ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
65 defer cancel()
66 i, err := openCommandInvocation(ctx, os.Getenv("HOME_COMMAND_DATABASE"))
67 accepted := err == nil
68 if accepted {
69 var count int
70 if i.request.Authorization().OperationID != token(4) || i.launch.RequestID != token(5) || i.launch.AttemptID != token(6) || i.database.db.QueryRowContext(i.ctx, "SELECT count(*) FROM changes").Scan(&count) != nil || count != 0 {
71 os.Exit(3)
72 }
73 var wantClose error
74 switch os.Getenv("HOME_COMMAND_FIXTURE") {
75 case "swap-before-close":
76 path := os.Getenv("HOME_COMMAND_DATABASE")
77 if os.Rename(path, path+".saved") != nil || os.Rename(path+".other", path) != nil {
78 os.Exit(7)
79 }
80 wantClose = ErrUnavailable
81 case "deadline-before-close":
82 <-ctx.Done()
83 wantClose = ErrUnavailable
84 }
85 if err := i.close(); err != wantClose {
86 os.Exit(4)
87 }
88 if i.close() != ErrRefused {
89 os.Exit(5)
90 }
91 } else if err != ErrRefused {
92 os.Exit(6)
93 }
94 json.NewEncoder(os.Stdout).Encode(map[string]bool{"accepted": accepted})
95 os.Exit(0)
96}
97
98func TestCommandInvocationBindsSignatureBeforeOpeningDatabase(t *testing.T) {
99 image, err := os.ReadFile("/proc/self/exe")
100 if err != nil {
101 t.Fatal(err)
102 }
103 imageHash := sha256.Sum256(image)
104 for _, mode := range []string{"valid", "runtime-default", "swap-before-close", "deadline-before-close", "expired-authentic", "signer", "origin", "registry", "role", "action", "config-key", "transport-account", "transport-app", "transport-host", "inode"} {
105 t.Run(mode, func(t *testing.T) {
106 k, v, a := fixture(t)
107 cfg := commandConfigFixture(v)
108 if mode == "expired-authentic" {
109 a.Issued, a.Expires = 100, 200
110 }
111 switch mode {
112 case "signer":
113 otherKey, generateErr := family.Generate()
114 if generateErr != nil {
115 t.Fatal(generateErr)
116 }
117 k = otherKey
118 case "origin":
119 a.Origin = "https://other.example"
120 case "registry":
121 a.Registry = token(9)
122 case "role":
123 a.Role, a.Action = "credential", "credential.owner-seed"
124 case "action":
125 a.Action = "directory.seed-team"
126 case "config-key":
127 cfg.KeyID = strings.Repeat("a", 64)
128 }
129 input := signed(t, k, a)
130 inputHash := sha256.Sum256(input)
131 dir := t.TempDir()
132 database := filepath.Join(dir, "home.db")
133 seedOpenDB(t, database)
134 if mode == "swap-before-close" {
135 seedOpenDB(t, database+".other")
136 }
137 var st unix.Stat_t
138 if unix.Stat(database, &st) != nil {
139 t.Fatal("database stat")
140 }
141 path := filepath.Join(dir, "image")
142 if err := os.WriteFile(path, image, 0700); err != nil {
143 t.Fatal(err)
144 }
145 pin, err := os.Open(path)
146 if err != nil {
147 t.Fatal(err)
148 }
149 defer pin.Close()
150 digest := strings.Repeat("a", 64)
151 launch := launchctx.LaunchContext{Version: 1, Account: cfg.Account, App: cfg.App, Host: cfg.Host, RequestID: token(5), AttemptID: token(6), RequestDigest: digest, RecordIdentity: digest, ArtifactDigest: hex.EncodeToString(imageHash[:]), EnvironmentEnvelopeDigest: digest, GuardGeneration: 1, InputSHA256: hex.EncodeToString(inputHash[:]), DatabasePath: database, DatabaseDevice: uint64(st.Dev), DatabaseInode: st.Ino, ExecutionDeadline: time.Now().Add(10 * time.Second).UTC().Format(time.RFC3339Nano)}
152 switch mode {
153 case "transport-account":
154 launch.Account = "abc"
155 case "transport-app":
156 launch.App = "another"
157 case "transport-host":
158 launch.Host = "another.example"
159 case "inode":
160 launch.DatabaseInode++
161 }
162 raw, err := launchctx.EncodeLaunch(launch)
163 if err != nil {
164 t.Fatal(err)
165 }
166 config, err := json.Marshal(cfg)
167 if err != nil {
168 t.Fatal(err)
169 }
170 inR, inW, err := os.Pipe()
171 if err != nil {
172 t.Fatal(err)
173 }
174 defer inR.Close()
175 defer inW.Close()
176 contextR, contextW, err := os.Pipe()
177 if err != nil {
178 t.Fatal(err)
179 }
180 defer contextR.Close()
181 defer contextW.Close()
182 watch, err := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK)
183 if err != nil {
184 t.Fatal(err)
185 }
186 defer unix.Close(watch)
187 if _, err := unix.InotifyAddWatch(watch, database, unix.IN_OPEN); err != nil {
188 t.Fatal(err)
189 }
190 ctx, cancel := context.WithTimeout(t.Context(), 8*time.Second)
191 defer cancel()
192 cmd := exec.CommandContext(ctx, path, "-test.run=^TestCommandInvocationChild$")
193 cmd.Dir = "/"
194 cmd.Env = []string{"HOME_COMMAND_FIXTURE=" + mode, "HOME_COMMAND_DATABASE=" + database, commandConfigEnvironment + "=" + string(config)}
195 if mode != "runtime-default" {
196 cmd.Env = append(cmd.Env, "GODEBUG=containermaxprocs=0,updatemaxprocs=0")
197 }
198 cmd.Stdin = inR
199 cmd.ExtraFiles = []*os.File{contextR, pin}
200 var output bytes.Buffer
201 cmd.Stdout = &output
202 if err := cmd.Start(); err != nil {
203 t.Fatal(err)
204 }
205 inR.Close()
206 contextR.Close()
207 writes := make(chan struct{}, 2)
208 for _, item := range []struct {
209 fd *os.File
210 raw []byte
211 }{{inW, input}, {contextW, raw}} {
212 go func() {
213 io.Copy(item.fd, bytes.NewReader(item.raw))
214 item.fd.Close()
215 writes <- struct{}{}
216 }()
217 }
218 if err := cmd.Wait(); err != nil {
219 t.Fatal("child refused fixture", err)
220 }
221 <-writes
222 <-writes
223 var result struct{ Accepted bool }
224 want := mode == "valid" || mode == "runtime-default" || mode == "swap-before-close" || mode == "deadline-before-close" || mode == "expired-authentic"
225 if json.Unmarshal(output.Bytes(), &result) != nil || result.Accepted != want {
226 t.Fatal("wrong invocation result", output.String())
227 }
228 buf := make([]byte, 4096)
229 n, err := unix.Read(watch, buf)
230 opened := n > 0
231 if err != nil && !errors.Is(err, unix.EAGAIN) {
232 t.Fatal(err)
233 }
234 if mode == "swap-before-close" || mode == "deadline-before-close" {
235 original := database
236 if mode == "swap-before-close" {
237 original += ".saved"
238 }
239 db, err := sql.Open("sqlite", "file:"+original+"?mode=ro")
240 if err != nil {
241 t.Fatal(err)
242 }
243 var count int
244 queryErr := db.QueryRow("SELECT count(*) FROM changes").Scan(&count)
245 closeErr := db.Close()
246 if queryErr != nil || closeErr != nil || count != 0 {
247 t.Fatal("original database changed", queryErr, closeErr, count)
248 }
249 }
250 if opened != (want || mode == "inode") {
251 t.Fatal("database opened before signed scope validation", mode, opened)
252 }
253 })
254 }
255}
256