package host import ( "context" "crypto/ed25519" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "io" "os" "path/filepath" "runtime" "time" "github.com/carlosframework/platform/internal/moves" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootparticipation" "github.com/carlosframework/platform/internal/rootstate" ) type rootFloorBinary struct { Version, Digest string Protocol, SeedsProtocol int File rootstate.FileIdentity } type rootFloorVerdict struct { Identity rootFloorIdentity Generation uint64 Current, Prev rootFloorBinary CurrentOK, PrevAbsent, PrevOK bool } type rootFloorAdvertisement struct { State, MinRelease, PrevRelease, PrevAllowedFor string Generation uint64 } type rootFloorJudge struct { floors rootMovesFloors remote func(context.Context) (*moves.Floor, error) legacy func() ([]*moves.Floor, error) binary func(string) (rootFloorBinary, error) idle func(context.Context) error current, prev string } func (b rootFloorBinary) supports(fl *moves.Floor) bool { if b.Digest == "" || !validFloorRelease(b.Version) { return false } if fl == nil { return true } at, err := moves.ReleaseAtOrAbove(b.Version, fl.MinRelease) return err == nil && at && b.Protocol >= fl.Min } func (j rootFloorJudge) refresh(ctx context.Context) error { remote, rerr := j.remote(ctx) legacy, lerr := j.legacy() if err := j.floors.refresh(ctx, remote, legacy, errors.Join(rerr, lerr)); err != nil { return err } return j.judge(ctx) } func (j rootFloorJudge) judge(ctx context.Context) error { // Release exclusion is held by the caller. Observe participation before // taking a floor lock; the verifier must never run underneath that lock. idleErr := errRootFloorHistory if j.idle != nil { idleErr = j.idle(ctx) } f := j.floors unlock, err := f.private.Lock(ctx, "floor.lock") if err != nil { return err } defer unlock() p, err := f.projection() if err != nil { bound, err := f.localBound() if err != nil { return err } p = rootFloorProjection{rootFloorRecord: rootFloorRecord{Identity: f.identity, Floor: bound}} } // Revoke a same-generation verdict before any observation that can fail. if err := f.public.Write("prev.json", []byte("{}")); err != nil { return err } v := rootFloorVerdict{Identity: f.identity, Generation: p.Generation} v.Current, err = j.binary(j.current) if err != nil { return err } v.CurrentOK = v.Current.supports(p.Floor) if !v.CurrentOK { rootFloorAlert() } v.Prev, err = j.binary(j.prev) if errors.Is(err, os.ErrNotExist) { v.PrevAbsent = true } else if err != nil { return err } else if v.Prev.supports(p.Floor) { if idleErr != nil { // Retain a negative verdict without blocking the repair executor. v.PrevOK = false } else { v.PrevOK = true } } raw, _ := json.Marshal(v) if err := f.public.Write("prev.json", raw); err != nil { return errors.Join(err, f.public.Write("prev.json", []byte("{}"))) } return nil } func (f rootMovesFloors) advertisement() (rootFloorAdvertisement, error) { p, err := f.projection() if err != nil { return rootFloorAdvertisement{}, err } return rootFloorAdvertisementFor(f.public, p) } func rootFloorAdvertisementFor(public rootstate.Store, p rootFloorProjection) (rootFloorAdvertisement, error) { ad := rootFloorAdvertisement{State: "quarantined", Generation: p.Generation} raw, err := public.Read("prev.json") if err != nil { return ad, err } var v rootFloorVerdict if len(raw) > movesFloorMaxBytes || rootparticipation.Decode(raw, &v) != nil || v.Identity != p.Identity || v.Generation != p.Generation || !v.CurrentOK || !v.Current.supports(p.Floor) { return ad, errRootFloorHistory } ad.State = "accepted" if p.Floor != nil { ad.MinRelease = p.Floor.MinRelease } if v.PrevAbsent { ad.PrevRelease = "none" } else { ad.PrevRelease = v.Prev.Version if v.PrevOK && v.Prev.supports(p.Floor) && p.Floor != nil { ad.PrevAllowedFor = p.Floor.MinRelease } } return ad, nil } // A binary's signed identity, not its mutable version sidecar, supplies its // release and protocol. The cache is root-owned; each envelope is reverified. func readRootFloorBinary(opt, path string) (rootFloorBinary, error) { return readRootFloorBinaryContext(context.Background(), opt, path) } // readRootFloorBinaryContext is readRootFloorBinary under ctx: the hash of // the image stops between reads, and the scan of the identity cache before // each identity, once ctx ends. The generation census's service lookup // proves signed images with it under the census deadline. func readRootFloorBinaryContext(ctx context.Context, opt, path string) (rootFloorBinary, error) { var b rootFloorBinary f, err := rootstate.OpenAuthorityFile(path) if err != nil { return b, err } defer f.Close() b.File, err = rootstate.Identity(f) if err != nil { return b, err } st, err := f.Stat() if err != nil { return b, err } if st.Size() > 512<<20 || st.Mode()&0111 == 0 { return b, os.ErrPermission } sum, err := rootFloorDigest(ctx, f) if err != nil { return b, err } // One single-valued hex write, as before: the moves copy gate derives the // Digest that reaches a seal's Reason from here (approved_moves_copy_test). b.Digest = hex.EncodeToString(sum) pub, err := release.ParsePublicKey(os.Getenv("CARLOS_RELEASE_PUBKEY"), "CARLOS_RELEASE_PUBKEY") if err != nil { return b, err } identities, err := rootstate.Open(filepath.Join(opt, "system", "guard", "identities"), false, true) if err != nil { return b, err } defer identities.Close() names, err := identities.Names() if err != nil { return b, err } return rootFloorSignedIdentity(ctx, pub, b, names, identities.Read) } // rootFloorDigest is the sha256 of an image, read through ctx: the raw sum, // which the caller hex-encodes in place. func rootFloorDigest(ctx context.Context, image io.Reader) ([]byte, error) { sum := sha256.New() if _, err := io.Copy(sum, generationContextReader{ctx: ctx, r: image}); err != nil { return nil, err } return sum.Sum(nil), nil } // rootFloorSignedIdentity finds the cached signed identity whose router // artifact for this platform is b's digest, verifying each envelope, and // stops before the next identity once ctx ends. func rootFloorSignedIdentity(ctx context.Context, pub ed25519.PublicKey, b rootFloorBinary, names []string, read func(string) ([]byte, error)) (rootFloorBinary, error) { for _, name := range names { if err := ctx.Err(); err != nil { return b, err } raw, err := read(name) if err != nil { return b, err } p, err := release.VerifySystemIdentity(pub, raw) if err != nil { return b, err } if p.Current.Artifacts[runtime.GOOS+"-"+runtime.GOARCH].SHA256 == b.Digest { b.Version, b.Protocol, b.SeedsProtocol = p.Current.ReleaseTag, p.Current.MovesProtocol, p.Current.SeedsProtocol return b, nil } } return b, errRootFloorHistory } // generationContextReader stops a long read, an image hash, once its // context ends. type generationContextReader struct { ctx context.Context r io.Reader } func (c generationContextReader) Read(p []byte) (int, error) { if err := c.ctx.Err(); err != nil { return 0, err } return c.r.Read(p) } func rootFloorDowngradeIdle(ctx context.Context) error { l, held, err := rootparticipation.TransactionLedger(ctx) if err != nil { return err } if !held { l, err = rootparticipation.Open() if err != nil { return err } defer l.State.Close() } l.VerifyLaunches = func(ctx context.Context, revision uint64) error { read := func(ctx context.Context) (generationInventory, error) { snapshot, err := productionLocalGenerationInventory(ctx) if err == nil && snapshot.Census.LaunchRevision != revision { return snapshot, errRootFloorHistory } return snapshot, err } state, err := rootstate.Open(release.RootGenerationsDir, false, false) if err != nil { return err } defer state.Close() source := release.RootGenerationPins{Ledger: &release.RootGenerations{Store: state}, Reconcile: (generationReconciler{read: read, kernel: productionGenerationKernelCensus}).reconcile} pins, err := source.StagedPins(ctx, release.OptDir()) if err != nil { return err } if len(pins) > 0 { return errRootFloorHistory } return nil } return l.Idle(ctx) } func rootRefreshAndJudgeMovesPrev(id rootFloorIdentity) error { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() opt, dd := release.OptDir(), dataDir() // The adopt pass already owns release exclusion. Acquire no release lock here. return withPrevLock(opt, func() error { f, err := openRootMovesFloors(opt, id, true) if err != nil { return err } defer f.close() j := rootFloorJudge{floors: f, current: systemBinaryPath(opt), prev: systemPrevPath(opt), idle: rootFloorDowngradeIdle, binary: func(path string) (rootFloorBinary, error) { return readRootFloorBinary(opt, path) }, legacy: func() ([]*moves.Floor, error) { return rootFloorLegacy(dd) }, remote: func(ctx context.Context) (*moves.Floor, error) { s, err := openDeployStore(ctx) if err != nil { return nil, err } return moves.ReadFloor(ctx, s) }} return j.refresh(ctx) }) } func withRootMovesFields(f edgesFile, opt string) edgesFile { f.MovesProtocol = moves.ProtocolVersion f.MovesFloorState = "quarantined" f.MovesFloorMinRelease, f.PrevRelease, f.PrevAllowedFor = "", "", "" p, err := readRootFloorProjection(opt) if err != nil { return f } if p.Identity.Deployment != os.Getenv("CARLOS_DEPLOYMENT_BUCKET") || p.Identity.Box != f.Box { return f } public, err := rootstate.Open(rootFloorDir(opt), false, true) if err != nil { return f } defer public.Close() ad, err := rootFloorAdvertisementFor(public, p) if err != nil { return f } if rootstate.Sync(public) != nil { return f } f.MovesFloorState, f.MovesFloorGeneration = ad.State, ad.Generation f.MovesFloorMinRelease, f.PrevRelease, f.PrevAllowedFor = ad.MinRelease, ad.PrevRelease, ad.PrevAllowedFor return f }