| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/ed25519" |
| 6 | "crypto/sha256" |
| 7 | "encoding/hex" |
| 8 | "io" |
| 9 | "os" |
| 10 | "path/filepath" |
| 11 | "strings" |
| 12 | |
| 13 | "github.com/carlosframework/platform/internal/hostbox" |
| 14 | "github.com/carlosframework/platform/internal/hostclaim" |
| 15 | "github.com/carlosframework/platform/internal/hostguard" |
| 16 | "github.com/carlosframework/platform/internal/instancectl" |
| 17 | "github.com/carlosframework/platform/internal/provision" |
| 18 | "github.com/carlosframework/platform/internal/release" |
| 19 | "github.com/carlosframework/platform/internal/rootstate" |
| 20 | ) |
| 21 | |
| 22 | type rootGenerationSource struct { |
| 23 | beginUpdate func(context.Context, *rootGenerationSource, release.GenerationBinding) (func(string, error, func() error) error, func() error, error) |
| 24 | store release.Store |
| 25 | optDir string |
| 26 | pub func() (ed25519.PublicKey, error) |
| 27 | open func() (rootstate.Store, error) |
| 28 | openFile func(string) (*os.File, error) |
| 29 | persistent func(*os.File) (rootstate.FileIdentity, error) |
| 30 | } |
| 31 | |
| 32 | func productionRootGenerations(store release.Store, optDir string) *rootGenerationSource { |
| 33 | return &rootGenerationSource{beginUpdate: beginRootGenerationUpdate, store: store, optDir: optDir, |
| 34 | pub: func() (ed25519.PublicKey, error) { |
| 35 | return release.ParsePublicKey(os.Getenv("CARLOS_RELEASE_PUBKEY"), "CARLOS_RELEASE_PUBKEY") |
| 36 | }, |
| 37 | open: func() (rootstate.Store, error) { return rootstate.Open(release.RootGenerationsDir, true, false) }, |
| 38 | openFile: rootstate.OpenTrustedFile, persistent: rootstate.PersistentIdentity} |
| 39 | } |
| 40 | |
| 41 | // resolve runs under release and host exclusion. The request nominates a build; |
| 42 | // root binds it to its installation, the deployment record and signed manifest. |
| 43 | func (s *rootGenerationSource) resolve(ctx context.Context, h *ClaimLaunchRoot, peer StopPeer, q hostbox.RoleLaunchRequest) (release.GenerationBinding, error) { |
| 44 | var b release.GenerationBinding |
| 45 | if s == nil || s.store == nil || s.pub == nil || s.openFile == nil || h.Guard.Root == nil || q.Reservation == nil || q.Spec == nil { |
| 46 | return b, os.ErrPermission |
| 47 | } |
| 48 | t := q.Reservation.Target |
| 49 | in, err := h.Guard.Root.Installation(ctx, t.Host, hostguard.Captured{Incarnation: t.Incarnation, Stamp: t.Stamp}) |
| 50 | if err != nil { |
| 51 | return b, err |
| 52 | } |
| 53 | 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) { |
| 54 | return b, os.ErrPermission |
| 55 | } |
| 56 | rec, etag, err := provision.GetRecordETag(ctx, s.store, in.Account, in.App, in.Host) |
| 57 | if err != nil { |
| 58 | return b, err |
| 59 | } |
| 60 | inc, err := hostclaim.Incarnation(rec.Account, rec.App, rec.Host, rec.CreatedAt) |
| 61 | if err != nil || inc != in.Incarnation || rec.Account != in.Account || rec.App != in.App || rec.Host != in.Host || etag == "" { |
| 62 | return b, os.ErrPermission |
| 63 | } |
| 64 | rel, err := filepath.Rel(filepath.Join(s.optDir, "versions"), q.Executable) |
| 65 | if err != nil || !filepath.IsLocal(rel) { |
| 66 | return b, os.ErrPermission |
| 67 | } |
| 68 | version, _, ok := strings.Cut(rel, string(filepath.Separator)) |
| 69 | if !ok || !release.ValidVersion(version) || q.Executable != release.VersionPath(s.optDir, in.Account, version, in.App) { |
| 70 | return b, os.ErrPermission |
| 71 | } |
| 72 | pub, err := s.pub() |
| 73 | if err != nil { |
| 74 | return b, err |
| 75 | } |
| 76 | m, err := release.AuthorizedManifest(ctx, s.store, pub, in.Account, in.App, version) |
| 77 | if err != nil { |
| 78 | return b, err |
| 79 | } |
| 80 | if m.Kind != "binary" || m.Artifacts[release.AdoptPlatform()].SHA256 != q.Spec.Digest { |
| 81 | return b, os.ErrPermission |
| 82 | } |
| 83 | f, err := s.openFile(q.Executable) |
| 84 | if err != nil { |
| 85 | return b, err |
| 86 | } |
| 87 | defer f.Close() |
| 88 | id, err := rootstate.Identity(f) |
| 89 | if err != nil { |
| 90 | return b, err |
| 91 | } |
| 92 | if id.Device != q.Reservation.Executable.Device || id.Inode != q.Reservation.Executable.Inode { |
| 93 | return b, os.ErrPermission |
| 94 | } |
| 95 | hash := sha256.New() |
| 96 | if _, err := io.Copy(hash, f); err != nil { |
| 97 | return b, err |
| 98 | } |
| 99 | if hex.EncodeToString(hash.Sum(nil)) != q.Spec.Digest { |
| 100 | return b, os.ErrPermission |
| 101 | } |
| 102 | // OpenTrustedFile already checked this ancestry; release exclusion prevents |
| 103 | // another root release writer replacing it while the identities are captured. |
| 104 | tree := filepath.Dir(q.Executable) |
| 105 | d, err := os.Open(tree) |
| 106 | if err != nil { |
| 107 | return b, err |
| 108 | } |
| 109 | defer d.Close() |
| 110 | treeID, err := rootstate.Identity(d) |
| 111 | if err != nil { |
| 112 | return b, err |
| 113 | } |
| 114 | b = release.GenerationBinding{Binding: release.HistoryBinding{Host: in.Host, Account: in.Account, App: in.App}, |
| 115 | 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}, |
| 116 | Installation: rootHistoryInstallation(in), Record: in.Record, RecordETag: etag, |
| 117 | Attempt: q.Reservation.ID, Inputs: q.Reservation.Inputs, Invocation: q.Reservation.InvocationID, ControllerPID: peer.PID, ControllerStart: peer.ProcessStart, |
| 118 | Version: version, Digest: q.Spec.Digest, Path: q.Executable, Tree: tree, Executable: id, TreeIdentity: treeID} |
| 119 | if s.persistent != nil { |
| 120 | b.ExecutableProof, err = s.persistent(f) |
| 121 | if err != nil { |
| 122 | return release.GenerationBinding{}, err |
| 123 | } |
| 124 | b.TreeProof, err = s.persistent(d) |
| 125 | if err != nil { |
| 126 | return release.GenerationBinding{}, err |
| 127 | } |
| 128 | if b.ExecutableProof.Boot != peer.BootID || b.TreeProof.Boot != peer.BootID { |
| 129 | return release.GenerationBinding{}, os.ErrPermission |
| 130 | } |
| 131 | } |
| 132 | unit, group, err := claimLaunchPlacement(h.Units, t.Host) |
| 133 | if err != nil || group != q.Reservation.Group.RelativePath { |
| 134 | return release.GenerationBinding{}, os.ErrPermission |
| 135 | } |
| 136 | if unit != instancectl.UnitName(instancectl.RoleRun, t.Host) { |
| 137 | b.Unit = unit |
| 138 | } |
| 139 | return b, nil |
| 140 | } |
| 141 | |
| 142 | type generationLaunchPermit struct { |
| 143 | finishUpdate func(string, error, func() error) error |
| 144 | closeUpdate func() error |
| 145 | ledger *release.RootGenerations |
| 146 | reservation release.GenerationReservation |
| 147 | checkBinding func() (release.GenerationBinding, error) |
| 148 | } |
| 149 | |
| 150 | func (s *rootGenerationSource) reserve(ctx context.Context, h *ClaimLaunchRoot, peer StopPeer, q hostbox.RoleLaunchRequest) (*generationLaunchPermit, error) { |
| 151 | if s == nil || s.open == nil { |
| 152 | return nil, os.ErrPermission |
| 153 | } |
| 154 | b, err := s.resolve(ctx, h, peer, q) |
| 155 | if err != nil { |
| 156 | return nil, err |
| 157 | } |
| 158 | store, err := s.open() |
| 159 | if err != nil { |
| 160 | return nil, err |
| 161 | } |
| 162 | g := &release.RootGenerations{Store: store} |
| 163 | r, err := g.Reserve(b) |
| 164 | if err != nil { |
| 165 | store.Close() |
| 166 | return nil, err |
| 167 | } |
| 168 | p := &generationLaunchPermit{ledger: g, reservation: r, checkBinding: func() (release.GenerationBinding, error) { return s.resolve(ctx, h, peer, q) }} |
| 169 | if s.beginUpdate != nil { |
| 170 | p.finishUpdate, p.closeUpdate, err = s.beginUpdate(ctx, s, b) |
| 171 | if err != nil { |
| 172 | store.Close() |
| 173 | return nil, err |
| 174 | } |
| 175 | } |
| 176 | return p, nil |
| 177 | } |
| 178 | |
| 179 | func (p *generationLaunchPermit) check() error { |
| 180 | if p == nil { |
| 181 | return nil |
| 182 | } |
| 183 | b, err := p.checkBinding() |
| 184 | if err != nil { |
| 185 | return err |
| 186 | } |
| 187 | if b != p.reservation.Binding { |
| 188 | return os.ErrPermission |
| 189 | } |
| 190 | return p.ledger.Check(p.reservation) |
| 191 | } |
| 192 | |
| 193 | func (p *generationLaunchPermit) started(grant hostbox.RoleLaunchGrant, observed ClaimLaunchObserved) error { |
| 194 | if p == nil { |
| 195 | return nil |
| 196 | } |
| 197 | containment := uint8(0) |
| 198 | if observed.Launcher.Device != 0 && observed.Launcher.Inode != 0 { |
| 199 | containment = 1 |
| 200 | } |
| 201 | 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, |
| 202 | Group: grant.Group.RelativePath, Invocation: grant.UnitInvocationID, GroupIdentity: rootstate.FileIdentity{Device: observed.Group.Device, Inode: observed.Group.Inode}}) |
| 203 | } |
| 204 | |