package host import ( "context" "crypto/ed25519" "crypto/sha256" "encoding/hex" "io" "os" "path/filepath" "strings" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/instancectl" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootstate" ) type rootGenerationSource struct { beginUpdate func(context.Context, *rootGenerationSource, release.GenerationBinding) (func(string, error, func() error) error, func() error, error) store release.Store optDir string pub func() (ed25519.PublicKey, error) open func() (rootstate.Store, error) openFile func(string) (*os.File, error) persistent func(*os.File) (rootstate.FileIdentity, error) } func productionRootGenerations(store release.Store, optDir string) *rootGenerationSource { return &rootGenerationSource{beginUpdate: beginRootGenerationUpdate, store: store, optDir: optDir, pub: func() (ed25519.PublicKey, error) { return release.ParsePublicKey(os.Getenv("CARLOS_RELEASE_PUBKEY"), "CARLOS_RELEASE_PUBKEY") }, open: func() (rootstate.Store, error) { return rootstate.Open(release.RootGenerationsDir, true, false) }, openFile: rootstate.OpenTrustedFile, persistent: rootstate.PersistentIdentity} } // resolve runs under release and host exclusion. The request nominates a build; // root binds it to its installation, the deployment record and signed manifest. func (s *rootGenerationSource) resolve(ctx context.Context, h *ClaimLaunchRoot, peer StopPeer, q hostbox.RoleLaunchRequest) (release.GenerationBinding, error) { var b release.GenerationBinding if s == nil || s.store == nil || s.pub == nil || s.openFile == nil || h.Guard.Root == nil || q.Reservation == nil || q.Spec == nil { return b, os.ErrPermission } t := q.Reservation.Target in, err := h.Guard.Root.Installation(ctx, t.Host, hostguard.Captured{Incarnation: t.Incarnation, Stamp: t.Stamp}) if err != nil { return b, err } if in.Host != t.Host || in.Incarnation != t.Incarnation || in.Stamp != t.Stamp || in.Box != t.Box || in.Enrollment != t.Enrollment || in.InventoryRevision == 0 || in.Record != provision.RecordKey(in.Account, in.App, in.Host) { return b, os.ErrPermission } rec, etag, err := provision.GetRecordETag(ctx, s.store, in.Account, in.App, in.Host) if err != nil { return b, err } inc, err := hostclaim.Incarnation(rec.Account, rec.App, rec.Host, rec.CreatedAt) if err != nil || inc != in.Incarnation || rec.Account != in.Account || rec.App != in.App || rec.Host != in.Host || etag == "" { return b, os.ErrPermission } rel, err := filepath.Rel(filepath.Join(s.optDir, "versions"), q.Executable) if err != nil || !filepath.IsLocal(rel) { return b, os.ErrPermission } version, _, ok := strings.Cut(rel, string(filepath.Separator)) if !ok || !release.ValidVersion(version) || q.Executable != release.VersionPath(s.optDir, in.Account, version, in.App) { return b, os.ErrPermission } pub, err := s.pub() if err != nil { return b, err } m, err := release.AuthorizedManifest(ctx, s.store, pub, in.Account, in.App, version) if err != nil { return b, err } if m.Kind != "binary" || m.Artifacts[release.AdoptPlatform()].SHA256 != q.Spec.Digest { return b, os.ErrPermission } f, err := s.openFile(q.Executable) if err != nil { return b, err } defer f.Close() id, err := rootstate.Identity(f) if err != nil { return b, err } if id.Device != q.Reservation.Executable.Device || id.Inode != q.Reservation.Executable.Inode { return b, os.ErrPermission } hash := sha256.New() if _, err := io.Copy(hash, f); err != nil { return b, err } if hex.EncodeToString(hash.Sum(nil)) != q.Spec.Digest { return b, os.ErrPermission } // OpenTrustedFile already checked this ancestry; release exclusion prevents // another root release writer replacing it while the identities are captured. tree := filepath.Dir(q.Executable) d, err := os.Open(tree) if err != nil { return b, err } defer d.Close() treeID, err := rootstate.Identity(d) if err != nil { return b, err } b = release.GenerationBinding{Binding: release.HistoryBinding{Host: in.Host, Account: in.Account, App: in.App}, Target: release.GenerationOwner{Host: t.Host, Incarnation: t.Incarnation, Stamp: t.Stamp, Session: t.Session, Box: t.Box, Enrollment: t.Enrollment, BootID: peer.BootID, ControllerID: peer.ControllerID}, Installation: rootHistoryInstallation(in), Record: in.Record, RecordETag: etag, Attempt: q.Reservation.ID, Inputs: q.Reservation.Inputs, Invocation: q.Reservation.InvocationID, ControllerPID: peer.PID, ControllerStart: peer.ProcessStart, Version: version, Digest: q.Spec.Digest, Path: q.Executable, Tree: tree, Executable: id, TreeIdentity: treeID} if s.persistent != nil { b.ExecutableProof, err = s.persistent(f) if err != nil { return release.GenerationBinding{}, err } b.TreeProof, err = s.persistent(d) if err != nil { return release.GenerationBinding{}, err } if b.ExecutableProof.Boot != peer.BootID || b.TreeProof.Boot != peer.BootID { return release.GenerationBinding{}, os.ErrPermission } } unit, group, err := claimLaunchPlacement(h.Units, t.Host) if err != nil || group != q.Reservation.Group.RelativePath { return release.GenerationBinding{}, os.ErrPermission } if unit != instancectl.UnitName(instancectl.RoleRun, t.Host) { b.Unit = unit } return b, nil } type generationLaunchPermit struct { finishUpdate func(string, error, func() error) error closeUpdate func() error ledger *release.RootGenerations reservation release.GenerationReservation checkBinding func() (release.GenerationBinding, error) } func (s *rootGenerationSource) reserve(ctx context.Context, h *ClaimLaunchRoot, peer StopPeer, q hostbox.RoleLaunchRequest) (*generationLaunchPermit, error) { if s == nil || s.open == nil { return nil, os.ErrPermission } b, err := s.resolve(ctx, h, peer, q) if err != nil { return nil, err } store, err := s.open() if err != nil { return nil, err } g := &release.RootGenerations{Store: store} r, err := g.Reserve(b) if err != nil { store.Close() return nil, err } p := &generationLaunchPermit{ledger: g, reservation: r, checkBinding: func() (release.GenerationBinding, error) { return s.resolve(ctx, h, peer, q) }} if s.beginUpdate != nil { p.finishUpdate, p.closeUpdate, err = s.beginUpdate(ctx, s, b) if err != nil { store.Close() return nil, err } } return p, nil } func (p *generationLaunchPermit) check() error { if p == nil { return nil } b, err := p.checkBinding() if err != nil { return err } if b != p.reservation.Binding { return os.ErrPermission } return p.ledger.Check(p.reservation) } func (p *generationLaunchPermit) started(grant hostbox.RoleLaunchGrant, observed ClaimLaunchObserved) error { if p == nil { return nil } containment := uint8(0) if observed.Launcher.Device != 0 && observed.Launcher.Inode != 0 { containment = 1 } return p.ledger.Started(p.reservation, release.GenerationProcess{Containment: containment, LauncherIdentity: rootstate.FileIdentity{Device: observed.Launcher.Device, Inode: observed.Launcher.Inode}, PID: grant.PID, Start: grant.ProcessStart, TenantPID: observed.TenantPID, TenantStart: observed.TenantStart, Group: grant.Group.RelativePath, Invocation: grant.UnitInvocationID, GroupIdentity: rootstate.FileIdentity{Device: observed.Group.Device, Inode: observed.Group.Inode}}) }