| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "crypto/ed25519" |
| 7 | |
| 8 | "github.com/carlosframework/platform/internal/hostactors" |
| 9 | "github.com/carlosframework/platform/internal/hostbox" |
| 10 | ) |
| 11 | |
| 12 | // This projection can bootstrap root's cleanup service after quarantine. Its |
| 13 | // actor explicitly lacks execution eligibility; it must never replace Identity |
| 14 | // in a work selector. Root's exact claim/fence, journal and peer checks still |
| 15 | // authorize each stop, retirement and settlement independently. |
| 16 | type claimCleanupEnrollment struct { |
| 17 | actor hostbox.Actor |
| 18 | directory hostactors.Directory |
| 19 | } |
| 20 | type cleanupCloudRoster struct{ actor hostbox.Actor } |
| 21 | |
| 22 | func (p cleanupCloudRoster) Actor(context.Context, string) (hostbox.Actor, error) { |
| 23 | a := p.actor |
| 24 | // IdentityProvider verifies cloud evidence against this immutable row. These |
| 25 | // values exist only in this private verification copy and are cleared below. |
| 26 | a.Eligible, a.Quarantined = true, false |
| 27 | return a, nil |
| 28 | } |
| 29 | func verifyClaimCleanupEnrollment(ctx context.Context, identity hostbox.IdentityProvider, directory hostactors.Directory) (*claimCleanupEnrollment, error) { |
| 30 | roster, err := directory.Read(ctx) |
| 31 | if err != nil { |
| 32 | return nil, err |
| 33 | } |
| 34 | row, ok := roster.Roster.Boxes[identity.Box] |
| 35 | if !ok || roster.Roster.RequestLimitNS != int64(hostbox.RequestLimit) { |
| 36 | return nil, ErrClaimStopIdentity |
| 37 | } |
| 38 | // Binding is the row's binding in this startup read. It stamps only what the |
| 39 | // cleanup identity itself constructs; fresh authority is never this actor. |
| 40 | binding, _ := hostactors.BoxBinding(roster.Roster, identity.Box) |
| 41 | actor := hostbox.Actor{FleetHomeRegion: roster.Roster.HomeRegion, ControlBucket: directory.Bucket, Box: row.BoxID, Enrollment: row.Enrollment, Revision: roster.Roster.Revision, Binding: binding, Region: row.Region, Bucket: identity.Bucket, Namespace: row.Namespace, Cloud: hostbox.CloudIdentity{Provider: row.Cloud.Provider, Account: row.Cloud.Account, Region: row.Cloud.Region, InstanceID: row.Cloud.InstanceID}} |
| 42 | identity.Roster = cleanupCloudRoster{actor} |
| 43 | if _, err := identity.Verify(ctx); err != nil { |
| 44 | return nil, err |
| 45 | } |
| 46 | actor.HomeRegion = identity.HomeRegion |
| 47 | actor.Eligible, actor.Quarantined = false, true |
| 48 | return &claimCleanupEnrollment{actor: actor, directory: directory}, nil |
| 49 | } |
| 50 | func (p *claimCleanupEnrollment) configureSigner(ctx context.Context, c *ClaimStopCoordinator, private ed25519.PrivateKey) error { |
| 51 | if p == nil || c == nil || c.Now == nil || len(private) != ed25519.PrivateKeySize { |
| 52 | return ErrClaimStopUnavailable |
| 53 | } |
| 54 | roster, err := p.directory.Read(ctx) |
| 55 | if err != nil { |
| 56 | return err |
| 57 | } |
| 58 | row, ok := roster.Roster.Boxes[p.actor.Box] |
| 59 | if !ok || row.Enrollment != p.actor.Enrollment || roster.Roster.RequestLimitNS != int64(hostbox.RequestLimit) || !bytes.Equal(row.CensusPublicKey, private.Public().(ed25519.PublicKey)) { |
| 60 | return ErrClaimStopIdentity |
| 61 | } |
| 62 | c.EvidenceSigner = append(ed25519.PrivateKey(nil), private...) |
| 63 | c.RequestLimit = hostbox.RequestLimit |
| 64 | return nil |
| 65 | } |
| 66 | |
| 67 | // rosterRevision is root's own verified roster read at the moment of the |
| 68 | // call (directory.Read checks the deployment's signature): the write |
| 69 | // counter of a roster that still names this box under its enrollment. It |
| 70 | // holds while the box is quarantined, so historical cleanup keeps working; |
| 71 | // only the bound it gives a stop request's numeral matters (fix round 1, |
| 72 | // finding 3). It is never the startup read. |
| 73 | func (p *claimCleanupEnrollment) rosterRevision(ctx context.Context) (uint64, error) { |
| 74 | if p == nil { |
| 75 | return 0, ErrClaimStopUnavailable |
| 76 | } |
| 77 | roster, err := p.directory.Read(ctx) |
| 78 | if err != nil { |
| 79 | return 0, err |
| 80 | } |
| 81 | row, ok := roster.Roster.Boxes[p.actor.Box] |
| 82 | if !ok || row.Enrollment != p.actor.Enrollment || roster.Roster.Revision == 0 { |
| 83 | return 0, ErrClaimStopIdentity |
| 84 | } |
| 85 | return roster.Roster.Revision, nil |
| 86 | } |
| 87 | |
| 88 | func (o ClaimOwnerBindings) actorForConstruction(ctx context.Context, mode string) (hostbox.Actor, error) { |
| 89 | if mode == "instance-control" && o.cleanupEnrollment != nil { |
| 90 | return o.cleanupEnrollment.actor, nil |
| 91 | } |
| 92 | return o.Identity.Verify(ctx) |
| 93 | } |
| 94 | func (a ClaimAssembly) actorForRoot(ctx context.Context) (hostbox.Actor, error) { |
| 95 | if a.Mode == "instance-control" && a.cleanupEnrollment != nil { |
| 96 | return a.cleanupEnrollment.actor, nil |
| 97 | } |
| 98 | return a.Identity.Verify(ctx) |
| 99 | } |
| 100 | |