carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "crypto/sha256"
6 "encoding/hex"
7 "github.com/carlosframework/platform/internal/provision"
8 "io"
9 "os"
10 "path/filepath"
11 "strings"
12 "sync"
13 "time"
14
15 "github.com/carlosframework/platform/internal/hostclaim"
16 "github.com/carlosframework/platform/internal/hostwrite"
17 "github.com/carlosframework/platform/internal/registry"
18 "github.com/carlosframework/platform/internal/release"
19 "github.com/carlosframework/platform/internal/rootstate"
20)
21
22type proofBinding struct {
23 ExecPath string
24 Local bool
25 Route registry.Route
26 Source string
27 Network bool
28 Version string
29 Digest string
30 Historical bool
31 Signed bool `json:",omitempty"`
32 History *release.HealthyBuild
33 Invocation string
34 PID int
35 Start string
36 RecordKey string
37 RecordETag string
38 CandidateUnit string
39 Files []proofUnitFile
40}
41
42type proofUnitFile struct {
43 Persistent rootstate.FileIdentity `json:",omitzero"`
44 Path string
45 Identity rootstate.FileIdentity
46}
47
48type proofBindingResolver func(context.Context, registry.Route) (proofBinding, error)
49
50type proofBindingSource struct {
51 signedRecovery bool
52 persistent func(*os.File) (rootstate.FileIdentity, error)
53 indexMu sync.Mutex
54 index *provision.RecordIndex
55 indexErr error
56 cacheIndex bool
57 checkPointers func(string) error
58 now func() time.Time
59 namedPath string
60 attempts func() (rootstate.Store, error)
61 store release.Store
62 claim func(context.Context, string) (hostclaim.Observed, error)
63 box func(context.Context) (string, error)
64 sys unitSystem
65 optDir string
66 openFile func(string) (*os.File, error)
67 unitDirs []string
68 healthy func(string, string, string, string) *release.HealthyBuild
69}
70
71func proofRouteReady(rt registry.Route, timeout time.Duration) func() error {
72 if rt.Addrs == "" {
73 return routeReady(rt, timeout)
74 }
75 var probes []func() error
76 for _, addr := range strings.Split(rt.Addrs, ",") {
77 probe := rt
78 probe.Addr, probe.Addrs, probe.Socket = addr, "", ""
79 ready := routeReady(probe, timeout)
80 if ready == nil {
81 return func() error { return os.ErrPermission }
82 }
83 probes = append(probes, ready)
84 }
85 return func() error {
86 for _, probe := range probes {
87 if err := probe(); err != nil {
88 return err
89 }
90 }
91 return nil
92 }
93}
94
95func productionProofBindingSource(store release.Store, sys unitSystem, optDir string) *proofBindingSource {
96 source := &proofBindingSource{cacheIndex: true, checkPointers: func(dir string) error {
97 f, err := rootstate.Open(dir, false, true)
98 if err != nil {
99 return err
100 }
101 return f.Close()
102 }, namedPath: rootNamedUnits, attempts: func() (rootstate.Store, error) { return rootstate.Open(rootRestartState, false, false) }, store: store, sys: sys, optDir: optDir, openFile: rootstate.OpenTrustedFile,
103 unitDirs: []string{"/etc/systemd/system", "/run/systemd/system", "/usr/lib/systemd/system"},
104 healthy: func(acct, app, version, digest string) *release.HealthyBuild {
105 for _, b := range release.ReadHealthy(release.RootHealthyDir, acct, app) {
106 if b.SHA256 == digest {
107 return &b
108 }
109 }
110 return nil
111 }}
112 if legacy, err := legacyRootRestart(); err == nil && !legacy {
113 source.persistent = rootstate.PersistentIdentity
114 source.signedRecovery = true
115 }
116 if hostwrite.Enabled(os.Getenv) {
117 source.claim = func(ctx context.Context, host string) (hostclaim.Observed, error) {
118 return hostclaim.Read(ctx, store, host)
119 }
120 source.box = func(ctx context.Context) (string, error) {
121 r, err := readProductionRosterRow(ctx, store, os.Getenv)
122 if err != nil {
123 return "", err
124 }
125 return r.row.BoxID, nil
126 }
127 }
128 return source
129}
130
131func (s *proofBindingSource) resolve(ctx context.Context, candidate registry.Route) (proofBinding, error) {
132 b, err := s.forHost(ctx, candidate.Host)
133 if err != nil {
134 return proofBinding{}, err
135 }
136 if !b.Local && !sameProofCandidate(candidate, b.Route, b.CandidateUnit) {
137 return proofBinding{}, os.ErrPermission
138 }
139 return b, nil
140}
141
142func (s *proofBindingSource) unitBinding(ctx context.Context, host string) (*provision.Record, proofBinding, unitProps, error) {
143 if s.store == nil || s.sys == nil || s.openFile == nil {
144 return nil, proofBinding{}, unitProps{}, os.ErrPermission
145 }
146 evidence, err := s.record(ctx, host)
147 if err != nil {
148 return nil, proofBinding{}, unitProps{}, err
149 }
150 rec, etag := evidence.record, evidence.revision
151 rt, err := rootRecordRoute(rec)
152 if err != nil {
153 return nil, proofBinding{}, unitProps{}, err
154 }
155 if rt.Unit == "" {
156 return nil, proofBinding{}, unitProps{}, os.ErrPermission
157 }
158 p, err := s.sys.Show(rt.Unit)
159 if err != nil {
160 return nil, proofBinding{}, unitProps{}, err
161 }
162 execPath := evidence.execPath
163 if execPath == "" {
164 execPath = release.LiveLink(s.optDir, host)
165 }
166 if p.Name != rt.Unit || p.ExecPath != execPath {
167 return nil, proofBinding{}, unitProps{}, os.ErrPermission
168 }
169 if evidence.local && execPath != release.LiveLink(s.optDir, host) {
170 expected := filepath.Join(s.unitDirs[0], "console.service.d", "90-host-floor.conf")
171 found := false
172 for _, path := range p.DropInPaths {
173 if path == expected {
174 found = true
175 }
176 }
177 if !found {
178 return nil, proofBinding{}, unitProps{}, os.ErrPermission
179 }
180 f, err := s.openFile(execPath)
181 if err != nil {
182 return nil, proofBinding{}, unitProps{}, err
183 }
184 f.Close()
185 }
186 definition := sha256.New()
187 files := append([]proofUnitFile(nil), evidence.files...)
188 for _, path := range append([]string{p.FragmentPath}, p.DropInPaths...) {
189 allowed := false
190 for _, dir := range s.unitDirs {
191 if strings.HasPrefix(path, dir+"/") {
192 allowed = true
193 }
194 }
195 if !allowed {
196 return nil, proofBinding{}, unitProps{}, os.ErrPermission
197 }
198 f, err := s.openFile(path)
199 if err != nil {
200 return nil, proofBinding{}, unitProps{}, err
201 }
202 identity, err := rootstate.Identity(f)
203 if err != nil {
204 f.Close()
205 return nil, proofBinding{}, unitProps{}, err
206 }
207 file := proofUnitFile{Path: path, Identity: identity}
208 if s.persistent != nil {
209 file.Persistent, err = s.persistent(f)
210 if err != nil {
211 f.Close()
212 return nil, proofBinding{}, unitProps{}, err
213 }
214 }
215 files = append(files, file)
216 encoded := proofFileSource(file)
217 definition.Write(encoded)
218 definition.Write([]byte(path + "\x00"))
219 n, copyErr := io.Copy(definition, io.LimitReader(f, rootstate.MaxRecord+1))
220 f.Close()
221 if copyErr != nil {
222 return nil, proofBinding{}, unitProps{}, copyErr
223 }
224 if n > rootstate.MaxRecord {
225 return nil, proofBinding{}, unitProps{}, os.ErrPermission
226 }
227 }
228 claimID := ""
229 if !evidence.local {
230 claimID, err = s.claimIdentity(ctx, rec)
231 }
232 if err != nil {
233 return nil, proofBinding{}, unitProps{}, err
234 }
235 source := claimID + evidence.key + "@" + etag + "@" + hex.EncodeToString(definition.Sum(nil))
236 return rec, proofBinding{ExecPath: execPath, Local: evidence.local, Route: rt, Source: source, Network: true, RecordKey: evidence.key, RecordETag: etag, CandidateUnit: rec.Unit, Files: files}, p, nil
237}
238
239func (s *proofBindingSource) binding(ctx context.Context, host string, rollback *proofBinding, prepare ...bool) (proofBinding, error) {
240 rec, unit, p, err := s.unitBinding(ctx, host)
241 if err != nil {
242 return proofBinding{}, err
243 }
244 rt, source, etag, files := unit.Route, unit.Source, unit.RecordETag, unit.Files
245 running := rollback == nil || (len(prepare) == 2 && prepare[1])
246 if rollback != nil && (rollback.Source != source || !sameProofFiles(rollback.Files, files)) {
247 return proofBinding{}, os.ErrPermission
248 }
249 var exe string
250 if !running {
251 exe, err = filepath.EvalSymlinks(release.LiveLink(s.optDir, host))
252 if len(prepare) == 1 && prepare[0] {
253 exe, err = release.VersionPath(s.optDir, rec.Account, rollback.Version, rec.App), nil
254 }
255 } else {
256 if pointerBoundUnit(p) {
257 exe, err = filepath.EvalSymlinks(release.LiveLink(s.optDir, host))
258 } else {
259 if p.MainPID <= 0 || p.InvocationID == "" {
260 return proofBinding{}, os.ErrPermission
261 }
262 exe, err = s.sys.Exe(p.MainPID)
263 }
264 }
265 if err != nil {
266 return proofBinding{}, err
267 }
268 if s.checkPointers != nil {
269 if err := s.checkPointers(filepath.Join(s.optDir, "live")); err != nil {
270 return proofBinding{}, err
271 }
272 }
273 current, currentErr := filepath.EvalSymlinks(release.LiveLink(s.optDir, host))
274 if currentErr != nil {
275 return proofBinding{}, currentErr
276 }
277 if exe != current {
278 if s.checkPointers != nil {
279 if err := s.checkPointers(filepath.Join(s.optDir, "previous")); err != nil {
280 return proofBinding{}, err
281 }
282 }
283 previous, err := filepath.EvalSymlinks(filepath.Join(s.optDir, "previous", host))
284 if err != nil || previous != exe {
285 return proofBinding{}, os.ErrPermission
286 }
287 }
288 rel, err := filepath.Rel(filepath.Join(s.optDir, "versions"), exe)
289 if err != nil {
290 return proofBinding{}, err
291 }
292 version := strings.Split(rel, string(filepath.Separator))[0]
293 if !release.ValidVersion(version) || exe != release.VersionPath(s.optDir, rec.Account, version, rec.App) {
294 return proofBinding{}, os.ErrPermission
295 }
296 f, err := s.openFile(exe)
297 if err != nil {
298 return proofBinding{}, err
299 }
300 defer f.Close()
301 hash := sha256.New()
302 if _, err := io.Copy(hash, f); err != nil {
303 return proofBinding{}, err
304 }
305 digest := hex.EncodeToString(hash.Sum(nil))
306 var history *release.HealthyBuild
307 if rollback == nil && s.healthy != nil {
308 history = s.healthy(rec.Account, rec.App, version, digest)
309 }
310 historical := history != nil
311 signed := false
312 if rollback != nil {
313 if rollback.Version != version || rollback.Digest != digest {
314 return proofBinding{}, os.ErrPermission
315 }
316 historical, history, signed = rollback.Historical, rollback.History, rollback.Signed
317 } else if !historical && s.signedRecovery {
318 pub, keyErr := release.ParsePublicKey(os.Getenv("CARLOS_RELEASE_PUBKEY"), "CARLOS_RELEASE_PUBKEY")
319 if keyErr == nil {
320 m, authErr := release.AuthorizedManifest(ctx, s.store, pub, rec.Account, rec.App, version)
321 signed = authErr == nil && m.Kind == "binary" && m.Artifacts[release.AdoptPlatform()].SHA256 == digest
322 }
323 }
324 start := ""
325 if running && p.MainPID > 0 {
326 start, err = s.sys.StartTime(p.MainPID)
327 if err != nil || start == "" {
328 return proofBinding{}, os.ErrPermission
329 }
330 again, err := s.sys.Show(rt.Unit)
331 if err != nil || again.MainPID != p.MainPID || again.InvocationID != p.InvocationID || again.FragmentPath != p.FragmentPath {
332 return proofBinding{}, os.ErrPermission
333 }
334 againExe, err := s.sys.Exe(again.MainPID)
335 if err != nil || againExe != exe {
336 return proofBinding{}, os.ErrPermission
337 }
338 }
339 return proofBinding{ExecPath: unit.ExecPath, Local: unit.Local, CandidateUnit: rec.Unit, Signed: signed, History: history, Invocation: p.InvocationID, PID: p.MainPID, Start: start, Route: rt, Source: source, Network: true, Version: version, Digest: digest, Historical: historical, RecordKey: unit.RecordKey, RecordETag: etag, Files: files}, nil
340}
341
342func (b proofBinding) authorizedRecovery() bool {
343 return b.Signed || (b.Historical && b.History != nil && b.History.Version == b.Version && b.History.SHA256 == b.Digest)
344}
345
346func (s *proofBindingSource) forHost(ctx context.Context, host string) (proofBinding, error) {
347 return s.binding(ctx, host, nil)
348}
349func (s *proofBindingSource) rollback(ctx context.Context, b proofBinding) (proofBinding, error) {
350 return s.binding(ctx, b.Route.Host, &b)
351}
352
353func (s *proofBindingSource) prepareRollback(ctx context.Context, b proofBinding) (proofBinding, error) {
354 return s.binding(ctx, b.Route.Host, &b, true)
355}
356
357func (s *proofBindingSource) recovered(ctx context.Context, b proofBinding) (proofBinding, error) {
358 return s.binding(ctx, b.Route.Host, &b, false, true)
359}
360
361func pointerBoundUnit(p unitProps) bool {
362 return p.MainPID == 0 && (p.ActiveState == "failed" || p.ActiveState == "inactive" || p.ActiveState == "activating" || p.SubState == "auto-restart")
363}
364