| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/ed25519" |
| 6 | "net" |
| 7 | "net/http" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | "time" |
| 11 | |
| 12 | "github.com/carlosframework/platform/internal/hostactors" |
| 13 | "github.com/carlosframework/platform/internal/hostbox" |
| 14 | "github.com/carlosframework/platform/internal/hostops" |
| 15 | "github.com/carlosframework/platform/internal/hostwrite" |
| 16 | "github.com/carlosframework/platform/internal/release" |
| 17 | ) |
| 18 | |
| 19 | func TestPlan9CleanupEnrollmentDoesNotGrantExecution(t *testing.T) { |
| 20 | t.Parallel() |
| 21 | root := ed25519.NewKeyFromSeed(make([]byte, 32)) |
| 22 | evidence := ed25519.NewKeyFromSeed([]byte(strings.Repeat("e", 32))) |
| 23 | actor := hostbox.Actor{Box: "box", Enrollment: strings.Repeat("a", 32), Revision: 1, Binding: "1", Region: "ie"} |
| 24 | identity := handoverRecoveryIdentity(t, actor) |
| 25 | cloudActor, err := identity.Roster.Actor(t.Context(), actor.Box) |
| 26 | if err != nil { |
| 27 | t.Fatal(err) |
| 28 | } |
| 29 | store := release.NewDirStore(t.TempDir()) |
| 30 | row := hostactors.Box{BoxID: actor.Box, Enrollment: actor.Enrollment, Namespace: identity.Namespace, Label: actor.Box, Region: actor.Region, Pool: "default", Cloud: hostactors.Cloud{Provider: "aws", Account: cloudActor.Cloud.Account, Region: cloudActor.Cloud.Region, InstanceID: cloudActor.Cloud.InstanceID}, EvidenceSHA256: strings.Repeat("c", 64), CensusPublicKey: evidence.Public().(ed25519.PublicKey), State: "quarantined", QuarantineRevision: 1, LossIDs: []string{"loss"}} |
| 31 | roster := hostactors.Roster{V: 1, DeploymentBucket: "deployment", Revision: 1, RequestLimitNS: int64(hostbox.RequestLimit), Boxes: map[string]hostactors.Box{actor.Box: row}, Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}} |
| 32 | write := func() { |
| 33 | raw, err := release.SignRootEnvelope(release.RootSigner(root), roster) |
| 34 | if err != nil { |
| 35 | t.Fatal(err) |
| 36 | } |
| 37 | _, etag, _ := store.Get(t.Context(), hostactors.Key) |
| 38 | if _, err = store.Put(t.Context(), hostactors.Key, raw, etag); err != nil { |
| 39 | t.Fatal(err) |
| 40 | } |
| 41 | } |
| 42 | write() |
| 43 | directory := hostactors.Directory{Store: store, PublicKey: root.Public().(ed25519.PublicKey), Bucket: "deployment"} |
| 44 | identity.Roster = productionClaimRoster{Directory: directory, DataBucket: identity.Bucket, FloorDir: t.TempDir()} |
| 45 | if _, err = identity.Verify(t.Context()); err == nil { |
| 46 | t.Fatal("quarantine granted work") |
| 47 | } |
| 48 | cleanup, err := verifyClaimCleanupEnrollment(t.Context(), identity, directory) |
| 49 | if err != nil { |
| 50 | t.Fatal(err) |
| 51 | } |
| 52 | if cleanup.actor.Eligible || !cleanup.actor.Quarantined { |
| 53 | t.Fatal("cleanup projection can grant execution") |
| 54 | } |
| 55 | authority := &hostops.ProductionAuthority{Store: store, Directory: directory, Now: time.Now} |
| 56 | owners := ClaimOwnerBindings{Identity: identity, cleanupEnrollment: cleanup, RetirementAuthority: authority, EvidenceKey: evidence} |
| 57 | if _, err = owners.actorForConstruction(t.Context(), "agent"); err == nil { |
| 58 | t.Fatal("agent used cleanup projection") |
| 59 | } |
| 60 | if _, err = owners.actorForConstruction(t.Context(), "adopt"); err == nil { |
| 61 | t.Fatal("adopt used cleanup projection") |
| 62 | } |
| 63 | if _, err = owners.actorForConstruction(t.Context(), "instance-control"); err != nil { |
| 64 | t.Fatal(err) |
| 65 | } |
| 66 | stop, err := owners.claimEvidenceCoordinator(t.Context(), store, "instance-control", cleanup.actor) |
| 67 | if err != nil || len(stop.EvidenceSigner) == 0 { |
| 68 | t.Fatalf("cleanup signer: %v", err) |
| 69 | } |
| 70 | owners.EvidenceKey = root |
| 71 | if _, err = owners.claimEvidenceCoordinator(t.Context(), store, "instance-control", cleanup.actor); err == nil { |
| 72 | t.Fatal("unenrolled key accepted") |
| 73 | } |
| 74 | row.Enrollment = strings.Repeat("b", 32) |
| 75 | roster.Boxes[actor.Box] = row |
| 76 | write() |
| 77 | if err = cleanup.configureSigner(t.Context(), stop, evidence); err == nil { |
| 78 | t.Fatal("old enrollment signed after replacement") |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | func TestPlan9RootWithdrawalRefusesNewHandover(t *testing.T) { |
| 83 | f := newRecoveryFixture(t) |
| 84 | controller := f.controller("boot-1", "controller-1", 4101, "start-1") |
| 85 | _ = controller |
| 86 | root := ClaimRootHTTP{RequireNewWork: func(context.Context) error { return hostwrite.ErrUnavailable }, Stop: ClaimStopHTTP{Coordinator: f.root(recoveryStopDrain{}), Authenticate: func(context.Context, net.Conn) (StopPeer, error) { return f.peer, nil }}} |
| 87 | socket := startRetirementRecoveryTransport(t, root) |
| 88 | if got := postRecoveryHandover(t, socket, coldHandoverRequest{}); got != http.StatusForbidden { |
| 89 | t.Fatalf("withdrawn handover=%d", got) |
| 90 | } |
| 91 | } |
| 92 | |