carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "flag"
9 "fmt"
10 "io"
11 "log"
12 "os"
13 "path/filepath"
14 "time"
15
16 "github.com/carlosframework/platform/internal/historyreadback"
17 "github.com/carlosframework/platform/internal/hostactors"
18 "github.com/carlosframework/platform/internal/release"
19 "github.com/carlosframework/platform/internal/safefile"
20)
21
22const historyUsage = "usage: carlos proof history <status|reconstruct|retire|resume> --bucket <bucket> --state <file> [--box <box> --enrollment <id> --account <sqid> --app <app> --host <host>] [--input <file>] [--wait <duration>]"
23const historyPending = "History readback is not complete. Resume with the same --state file. Last result: %v"
24
25type historyConnect func(string, bool) (hostactors.Directory, release.Signer, error)
26
27func runHistory(args []string) {
28 err := historyCommand(context.Background(), args, func(bucket string, resume bool) (hostactors.Directory, release.Signer, error) {
29 if configured := os.Getenv("CARLOS_DEPLOYMENT_BUCKET"); configured != "" && configured != bucket {
30 return hostactors.Directory{}, release.Signer{}, errors.New("--bucket must match CARLOS_DEPLOYMENT_BUCKET")
31 }
32 d := hostactors.Directory{Store: openStore(), PublicKey: publicKey(), Bucket: bucket}
33 if resume {
34 return d, release.Signer{}, nil
35 }
36 return d, releaseSigner(), nil
37 }, stdout)
38 if err != nil && !errors.Is(err, flag.ErrHelp) {
39 log.Fatal(err)
40 }
41}
42
43func historyCommand(ctx context.Context, args []string, connect historyConnect, out io.Writer) error {
44 return historyCommandWithSave(ctx, args, connect, out, historySave)
45}
46
47func historyCommandWithSave(ctx context.Context, args []string, connect historyConnect, out io.Writer, save func(string, historyreadback.Operation) error) error {
48 if len(args) == 0 {
49 return errors.New(historyUsage)
50 }
51 verb := args[0]
52 if verb != "status" && verb != "reconstruct" && verb != "retire" && verb != "resume" {
53 return errors.New(historyUsage)
54 }
55 fs := flag.NewFlagSet("proof history "+verb, flag.ContinueOnError)
56 fs.SetOutput(out)
57 bucket := fs.String("bucket", os.Getenv("CARLOS_DEPLOYMENT_BUCKET"), "deployment bucket named in the signed box roster")
58 box := fs.String("box", "", "box named in the signed roster")
59 enrollment := fs.String("enrollment", "", "current box enrollment ID")
60 account := fs.String("account", "", "account sqid")
61 app := fs.String("app", "", "app name")
62 host := fs.String("host", "", "canonical instance hostname")
63 state := fs.String("state", "", "save the signed operation here before delivery; resume reuses this file")
64 input := fs.String("input", "", "JSON reconstruction or retirement decision to sign")
65 wait := fs.Duration("wait", 30*time.Second, "how long to wait for verified root readback")
66 if err := fs.Parse(args[1:]); err != nil {
67 return err
68 }
69 resume := verb == "resume"
70 scope := historyreadback.Scope{Bucket: *bucket, Box: *box, Enrollment: *enrollment, Binding: release.HistoryBinding{Account: *account, App: *app, Host: *host}}
71 if fs.NArg() != 0 || *bucket == "" || *state == "" || *wait <= 0 || (resume && (*input != "" || *box != "" || *enrollment != "" || *account != "" || *app != "" || *host != "")) || (!resume && (*box == "" || *enrollment == "" || *account == "" || *app == "" || *host == "")) || ((verb == "reconstruct" || verb == "retire") != (*input != "")) {
72 return errors.New(historyUsage)
73 }
74 ctx, cancel := context.WithTimeout(ctx, *wait)
75 defer cancel()
76 var op historyreadback.Operation
77 var reconstruction *release.HistoryReconstruction
78 var retirement *release.HistoryRecoveryRetirement
79 if resume {
80 if err := historyReadFile(*state, &op, true); err != nil {
81 return err
82 }
83 } else if verb == "reconstruct" {
84 reconstruction = &release.HistoryReconstruction{}
85 if err := historyReadFile(*input, reconstruction, false); err != nil {
86 return err
87 }
88 } else if verb == "retire" {
89 retirement = &release.HistoryRecoveryRetirement{}
90 if err := historyReadFile(*input, retirement, false); err != nil {
91 return err
92 }
93 }
94 d, signer, err := connect(*bucket, resume)
95 if err != nil {
96 return err
97 }
98 if !resume {
99 op, err = historyreadback.PrepareOperation(ctx, d, signer, scope, reconstruction, retirement, time.Now())
100 if err != nil {
101 return err
102 }
103 if err = save(*state, op); err != nil {
104 return err
105 }
106 }
107 if err = op.Deliver(ctx, d, time.Now()); err != nil {
108 return err
109 }
110 for {
111 receipt, readErr := op.Read(ctx, d, time.Now())
112 if readErr == nil {
113 enc := json.NewEncoder(out)
114 enc.SetIndent("", " ")
115 return enc.Encode(receipt)
116 }
117 timer := time.NewTimer(time.Second)
118 select {
119 case <-ctx.Done():
120 timer.Stop()
121 return fmt.Errorf(historyPending, readErr)
122 case <-timer.C:
123 }
124 }
125}
126
127func historyReadFile(path string, dst any, repair bool) error {
128 f, err := safefile.OpenFile(path, os.O_RDONLY, 0)
129 if err != nil {
130 return err
131 }
132 defer f.Close()
133 raw, err := io.ReadAll(io.LimitReader(f, 3*release.MaxRootEnvelopeBytes+1))
134 if err != nil {
135 return err
136 }
137 if len(raw) > 3*release.MaxRootEnvelopeBytes {
138 return os.ErrPermission
139 }
140 dec := json.NewDecoder(bytes.NewReader(raw))
141 dec.DisallowUnknownFields()
142 if err = dec.Decode(dst); err != nil {
143 return err
144 }
145 if err = dec.Decode(new(any)); err != io.EOF {
146 return os.ErrPermission
147 }
148 if repair {
149 return historySync(f, path)
150 }
151 return nil
152}
153
154func historySave(path string, op historyreadback.Operation) error {
155 raw, err := json.Marshal(op)
156 if err != nil {
157 return err
158 }
159 f, err := safefile.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
160 if err != nil {
161 return err
162 }
163 defer f.Close()
164 if n, err := f.Write(raw); err != nil {
165 return err
166 } else if n != len(raw) {
167 return io.ErrShortWrite
168 }
169 return historySync(f, path)
170}
171
172func historySync(f *os.File, path string) error {
173 if err := f.Sync(); err != nil {
174 return err
175 }
176 parent, err := safefile.OpenDir(filepath.Dir(path))
177 if err != nil {
178 return err
179 }
180 defer parent.Close()
181 current, err := safefile.OpenFile(path, os.O_RDONLY, 0)
182 if err != nil {
183 return err
184 }
185 defer current.Close()
186 a, err := f.Stat()
187 if err != nil {
188 return err
189 }
190 b, err := current.Stat()
191 if err != nil {
192 return err
193 }
194 if !os.SameFile(a, b) {
195 return os.ErrPermission
196 }
197 return parent.Sync()
198}
199