package host import ( "context" "crypto/sha256" "encoding/hex" "github.com/carlosframework/platform/internal/provision" "io" "os" "path/filepath" "strings" "sync" "time" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootstate" ) type proofBinding struct { ExecPath string Local bool Route registry.Route Source string Network bool Version string Digest string Historical bool Signed bool `json:",omitempty"` History *release.HealthyBuild Invocation string PID int Start string RecordKey string RecordETag string CandidateUnit string Files []proofUnitFile } type proofUnitFile struct { Persistent rootstate.FileIdentity `json:",omitzero"` Path string Identity rootstate.FileIdentity } type proofBindingResolver func(context.Context, registry.Route) (proofBinding, error) type proofBindingSource struct { signedRecovery bool persistent func(*os.File) (rootstate.FileIdentity, error) indexMu sync.Mutex index *provision.RecordIndex indexErr error cacheIndex bool checkPointers func(string) error now func() time.Time namedPath string attempts func() (rootstate.Store, error) store release.Store claim func(context.Context, string) (hostclaim.Observed, error) box func(context.Context) (string, error) sys unitSystem optDir string openFile func(string) (*os.File, error) unitDirs []string healthy func(string, string, string, string) *release.HealthyBuild } func proofRouteReady(rt registry.Route, timeout time.Duration) func() error { if rt.Addrs == "" { return routeReady(rt, timeout) } var probes []func() error for _, addr := range strings.Split(rt.Addrs, ",") { probe := rt probe.Addr, probe.Addrs, probe.Socket = addr, "", "" ready := routeReady(probe, timeout) if ready == nil { return func() error { return os.ErrPermission } } probes = append(probes, ready) } return func() error { for _, probe := range probes { if err := probe(); err != nil { return err } } return nil } } func productionProofBindingSource(store release.Store, sys unitSystem, optDir string) *proofBindingSource { source := &proofBindingSource{cacheIndex: true, checkPointers: func(dir string) error { f, err := rootstate.Open(dir, false, true) if err != nil { return err } return f.Close() }, namedPath: rootNamedUnits, attempts: func() (rootstate.Store, error) { return rootstate.Open(rootRestartState, false, false) }, store: store, sys: sys, optDir: optDir, openFile: rootstate.OpenTrustedFile, unitDirs: []string{"/etc/systemd/system", "/run/systemd/system", "/usr/lib/systemd/system"}, healthy: func(acct, app, version, digest string) *release.HealthyBuild { for _, b := range release.ReadHealthy(release.RootHealthyDir, acct, app) { if b.SHA256 == digest { return &b } } return nil }} if legacy, err := legacyRootRestart(); err == nil && !legacy { source.persistent = rootstate.PersistentIdentity source.signedRecovery = true } if hostwrite.Enabled(os.Getenv) { source.claim = func(ctx context.Context, host string) (hostclaim.Observed, error) { return hostclaim.Read(ctx, store, host) } source.box = func(ctx context.Context) (string, error) { r, err := readProductionRosterRow(ctx, store, os.Getenv) if err != nil { return "", err } return r.row.BoxID, nil } } return source } func (s *proofBindingSource) resolve(ctx context.Context, candidate registry.Route) (proofBinding, error) { b, err := s.forHost(ctx, candidate.Host) if err != nil { return proofBinding{}, err } if !b.Local && !sameProofCandidate(candidate, b.Route, b.CandidateUnit) { return proofBinding{}, os.ErrPermission } return b, nil } func (s *proofBindingSource) unitBinding(ctx context.Context, host string) (*provision.Record, proofBinding, unitProps, error) { if s.store == nil || s.sys == nil || s.openFile == nil { return nil, proofBinding{}, unitProps{}, os.ErrPermission } evidence, err := s.record(ctx, host) if err != nil { return nil, proofBinding{}, unitProps{}, err } rec, etag := evidence.record, evidence.revision rt, err := rootRecordRoute(rec) if err != nil { return nil, proofBinding{}, unitProps{}, err } if rt.Unit == "" { return nil, proofBinding{}, unitProps{}, os.ErrPermission } p, err := s.sys.Show(rt.Unit) if err != nil { return nil, proofBinding{}, unitProps{}, err } execPath := evidence.execPath if execPath == "" { execPath = release.LiveLink(s.optDir, host) } if p.Name != rt.Unit || p.ExecPath != execPath { return nil, proofBinding{}, unitProps{}, os.ErrPermission } if evidence.local && execPath != release.LiveLink(s.optDir, host) { expected := filepath.Join(s.unitDirs[0], "console.service.d", "90-host-floor.conf") found := false for _, path := range p.DropInPaths { if path == expected { found = true } } if !found { return nil, proofBinding{}, unitProps{}, os.ErrPermission } f, err := s.openFile(execPath) if err != nil { return nil, proofBinding{}, unitProps{}, err } f.Close() } definition := sha256.New() files := append([]proofUnitFile(nil), evidence.files...) for _, path := range append([]string{p.FragmentPath}, p.DropInPaths...) { allowed := false for _, dir := range s.unitDirs { if strings.HasPrefix(path, dir+"/") { allowed = true } } if !allowed { return nil, proofBinding{}, unitProps{}, os.ErrPermission } f, err := s.openFile(path) if err != nil { return nil, proofBinding{}, unitProps{}, err } identity, err := rootstate.Identity(f) if err != nil { f.Close() return nil, proofBinding{}, unitProps{}, err } file := proofUnitFile{Path: path, Identity: identity} if s.persistent != nil { file.Persistent, err = s.persistent(f) if err != nil { f.Close() return nil, proofBinding{}, unitProps{}, err } } files = append(files, file) encoded := proofFileSource(file) definition.Write(encoded) definition.Write([]byte(path + "\x00")) n, copyErr := io.Copy(definition, io.LimitReader(f, rootstate.MaxRecord+1)) f.Close() if copyErr != nil { return nil, proofBinding{}, unitProps{}, copyErr } if n > rootstate.MaxRecord { return nil, proofBinding{}, unitProps{}, os.ErrPermission } } claimID := "" if !evidence.local { claimID, err = s.claimIdentity(ctx, rec) } if err != nil { return nil, proofBinding{}, unitProps{}, err } source := claimID + evidence.key + "@" + etag + "@" + hex.EncodeToString(definition.Sum(nil)) 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 } func (s *proofBindingSource) binding(ctx context.Context, host string, rollback *proofBinding, prepare ...bool) (proofBinding, error) { rec, unit, p, err := s.unitBinding(ctx, host) if err != nil { return proofBinding{}, err } rt, source, etag, files := unit.Route, unit.Source, unit.RecordETag, unit.Files running := rollback == nil || (len(prepare) == 2 && prepare[1]) if rollback != nil && (rollback.Source != source || !sameProofFiles(rollback.Files, files)) { return proofBinding{}, os.ErrPermission } var exe string if !running { exe, err = filepath.EvalSymlinks(release.LiveLink(s.optDir, host)) if len(prepare) == 1 && prepare[0] { exe, err = release.VersionPath(s.optDir, rec.Account, rollback.Version, rec.App), nil } } else { if pointerBoundUnit(p) { exe, err = filepath.EvalSymlinks(release.LiveLink(s.optDir, host)) } else { if p.MainPID <= 0 || p.InvocationID == "" { return proofBinding{}, os.ErrPermission } exe, err = s.sys.Exe(p.MainPID) } } if err != nil { return proofBinding{}, err } if s.checkPointers != nil { if err := s.checkPointers(filepath.Join(s.optDir, "live")); err != nil { return proofBinding{}, err } } current, currentErr := filepath.EvalSymlinks(release.LiveLink(s.optDir, host)) if currentErr != nil { return proofBinding{}, currentErr } if exe != current { if s.checkPointers != nil { if err := s.checkPointers(filepath.Join(s.optDir, "previous")); err != nil { return proofBinding{}, err } } previous, err := filepath.EvalSymlinks(filepath.Join(s.optDir, "previous", host)) if err != nil || previous != exe { return proofBinding{}, os.ErrPermission } } rel, err := filepath.Rel(filepath.Join(s.optDir, "versions"), exe) if err != nil { return proofBinding{}, err } version := strings.Split(rel, string(filepath.Separator))[0] if !release.ValidVersion(version) || exe != release.VersionPath(s.optDir, rec.Account, version, rec.App) { return proofBinding{}, os.ErrPermission } f, err := s.openFile(exe) if err != nil { return proofBinding{}, err } defer f.Close() hash := sha256.New() if _, err := io.Copy(hash, f); err != nil { return proofBinding{}, err } digest := hex.EncodeToString(hash.Sum(nil)) var history *release.HealthyBuild if rollback == nil && s.healthy != nil { history = s.healthy(rec.Account, rec.App, version, digest) } historical := history != nil signed := false if rollback != nil { if rollback.Version != version || rollback.Digest != digest { return proofBinding{}, os.ErrPermission } historical, history, signed = rollback.Historical, rollback.History, rollback.Signed } else if !historical && s.signedRecovery { pub, keyErr := release.ParsePublicKey(os.Getenv("CARLOS_RELEASE_PUBKEY"), "CARLOS_RELEASE_PUBKEY") if keyErr == nil { m, authErr := release.AuthorizedManifest(ctx, s.store, pub, rec.Account, rec.App, version) signed = authErr == nil && m.Kind == "binary" && m.Artifacts[release.AdoptPlatform()].SHA256 == digest } } start := "" if running && p.MainPID > 0 { start, err = s.sys.StartTime(p.MainPID) if err != nil || start == "" { return proofBinding{}, os.ErrPermission } again, err := s.sys.Show(rt.Unit) if err != nil || again.MainPID != p.MainPID || again.InvocationID != p.InvocationID || again.FragmentPath != p.FragmentPath { return proofBinding{}, os.ErrPermission } againExe, err := s.sys.Exe(again.MainPID) if err != nil || againExe != exe { return proofBinding{}, os.ErrPermission } } 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 } func (b proofBinding) authorizedRecovery() bool { return b.Signed || (b.Historical && b.History != nil && b.History.Version == b.Version && b.History.SHA256 == b.Digest) } func (s *proofBindingSource) forHost(ctx context.Context, host string) (proofBinding, error) { return s.binding(ctx, host, nil) } func (s *proofBindingSource) rollback(ctx context.Context, b proofBinding) (proofBinding, error) { return s.binding(ctx, b.Route.Host, &b) } func (s *proofBindingSource) prepareRollback(ctx context.Context, b proofBinding) (proofBinding, error) { return s.binding(ctx, b.Route.Host, &b, true) } func (s *proofBindingSource) recovered(ctx context.Context, b proofBinding) (proofBinding, error) { return s.binding(ctx, b.Route.Host, &b, false, true) } func pointerBoundUnit(p unitProps) bool { return p.MainPID == 0 && (p.ActiveState == "failed" || p.ActiveState == "inactive" || p.ActiveState == "activating" || p.SubState == "auto-restart") }