| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "crypto/ed25519" |
| 5 | "errors" |
| 6 | "os" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | "time" |
| 10 | |
| 11 | "github.com/carlosframework/platform/internal/historyreadback" |
| 12 | "github.com/carlosframework/platform/internal/hostactors" |
| 13 | "github.com/carlosframework/platform/internal/hostfloor" |
| 14 | "github.com/carlosframework/platform/internal/release" |
| 15 | "github.com/carlosframework/platform/internal/rootstate" |
| 16 | ) |
| 17 | |
| 18 | func TestPlan14HistoryReadbackProduction(t *testing.T) { |
| 19 | for _, fault := range []string{"valid", "projection unavailable", "history unsynced", "source changed", "key unavailable", "wrong instance", "pending original"} { |
| 20 | t.Run(fault, func(t *testing.T) { |
| 21 | e, units, checks, source, signedPub := signedRecoveryFixture(t) |
| 22 | rt, err := e.root.reg.Get(observedHost) |
| 23 | if err != nil { |
| 24 | t.Fatal(err) |
| 25 | } |
| 26 | h := seedHostRootHistory(t, *rt) |
| 27 | pub, key, err := ed25519.GenerateKey(nil) |
| 28 | if err != nil { |
| 29 | t.Fatal(err) |
| 30 | } |
| 31 | _, evidenceKey, err := ed25519.GenerateKey(nil) |
| 32 | if err != nil { |
| 33 | t.Fatal(err) |
| 34 | } |
| 35 | now := time.Now().UTC() |
| 36 | enrollment := hostfloor.Enrollment{Box: "box", Enrollment: strings.Repeat("e", 32), InstanceID: "i-box"} |
| 37 | directory := hostactors.Directory{Store: e.store, PublicKey: pub, Bucket: "bucket"} |
| 38 | roster := hostactors.Roster{V: 1, DeploymentBucket: "bucket", Revision: 1, Boxes: map[string]hostactors.Box{"box": {Enrollment: enrollment.Enrollment, Namespace: "platform", BoxID: "box", Label: "box", Region: "ie", Pool: "default", Cloud: hostactors.Cloud{Provider: "aws", Account: "123", Region: "eu-west-1", InstanceID: "i-box"}, EvidenceSHA256: strings.Repeat("b", 64), State: "active", CensusPublicKey: evidenceKey.Public().(ed25519.PublicKey)}}, Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}} |
| 39 | wire, err := release.SignRootEnvelope(release.RootSigner(key), roster) |
| 40 | if err != nil { |
| 41 | t.Fatal(err) |
| 42 | } |
| 43 | if _, err := e.store.Put(t.Context(), hostactors.Key, wire, ""); err != nil { |
| 44 | t.Fatal(err) |
| 45 | } |
| 46 | b := release.HistoryBinding{Account: rt.Account, App: rt.App, Host: rt.Host} |
| 47 | scope := historyreadback.Scope{Bucket: directory.Bucket, Box: enrollment.Box, Enrollment: enrollment.Enrollment, Binding: b} |
| 48 | q, err := historyreadback.RequestStatus(t.Context(), directory, release.RootSigner(key), scope, now) |
| 49 | if err != nil { |
| 50 | t.Fatal(err) |
| 51 | } |
| 52 | projection := &rollbackMemory{files: map[string][]byte{}} |
| 53 | // A public projection can neither supply the signed snapshot nor repair it. |
| 54 | projection.files[release.HistoryStatusKey(b.Account, b.App)] = []byte(`{"app_complete":true,"identity":"forged"}`) |
| 55 | responder := historyReadbackPublisher{directory: directory, enrollment: enrollment, key: func() (ed25519.PrivateKey, error) { |
| 56 | if fault == "key unavailable" { |
| 57 | return nil, os.ErrPermission |
| 58 | } |
| 59 | return evidenceKey, nil |
| 60 | }, now: func() time.Time { return now }} |
| 61 | if fault == "wrong instance" { |
| 62 | responder.enrollment.InstanceID = "i-other" |
| 63 | } |
| 64 | if fault == "history unsynced" { |
| 65 | h.Store = &readbackFailStore{Store: h.Store} |
| 66 | } |
| 67 | if fault == "source changed" { |
| 68 | source.openFile = func(string) (*os.File, error) { return nil, os.ErrPermission } |
| 69 | } |
| 70 | if fault == "pending original" { |
| 71 | n := namedUnitRecovery{source: source, pub: signedPub, readLive: os.Readlink} |
| 72 | if _, err := n.recover(t.Context(), rt.Host, checks, func(string) string { return "5s" }, e.root.reg); err != nil { |
| 73 | t.Fatal(err) |
| 74 | } |
| 75 | disk, err := checks.attempts() |
| 76 | if err != nil { |
| 77 | t.Fatal(err) |
| 78 | } |
| 79 | pending, err := readRollbackAttempt(disk, rt.Host) |
| 80 | if err != nil { |
| 81 | t.Fatal(err) |
| 82 | } |
| 83 | pending.Settled = false |
| 84 | if err := writeRollbackState(disk, recoveryResultKey(pending), pending); err != nil { |
| 85 | t.Fatal(err) |
| 86 | } |
| 87 | } |
| 88 | m := rootHistoryMaintenance{history: h, store: e.store, source: source, checks: checks, pub: func() (ed25519.PublicKey, error) { return pub, nil }, now: func() time.Time { return now }, projection: func() (rootstate.Store, error) { |
| 89 | if fault == "projection unavailable" { |
| 90 | return nil, os.ErrPermission |
| 91 | } |
| 92 | return projection, nil |
| 93 | }, readback: responder.publish} |
| 94 | before := len(units.effects) |
| 95 | _ = m.run(t.Context(), b) |
| 96 | receipt, err := historyreadback.ReadStatus(t.Context(), directory, q, now) |
| 97 | switch fault { |
| 98 | case "valid", "projection unavailable": |
| 99 | if err != nil { |
| 100 | t.Fatal(err) |
| 101 | } |
| 102 | if receipt.Status.Identity == "forged" || len(receipt.Status.Hosts) != 1 || receipt.Status.Hosts[0].Source == "" || receipt.Status.RecoverySources == nil { |
| 103 | t.Fatalf("bad private readback: %+v", receipt) |
| 104 | } |
| 105 | case "pending original": |
| 106 | if err != nil { |
| 107 | t.Fatal(err) |
| 108 | } |
| 109 | if receipt.Status.RecoverySources == nil || len(receipt.Status.RecoverySources.Unresolved) != 1 { |
| 110 | t.Fatal("pending original hidden") |
| 111 | } |
| 112 | default: |
| 113 | if err == nil { |
| 114 | t.Fatal("unavailable evidence returned authenticated status") |
| 115 | } |
| 116 | if _, _, err := e.store.Get(t.Context(), historyreadback.ResponseKey(q)); !errors.Is(err, os.ErrNotExist) { |
| 117 | t.Fatalf("response on failure: %v", err) |
| 118 | } |
| 119 | } |
| 120 | if len(units.effects) != before { |
| 121 | t.Fatal("readback changed service effects") |
| 122 | } |
| 123 | }) |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | type readbackFailStore struct{ rootstate.Store } |
| 128 | |
| 129 | func (s *readbackFailStore) Write(string, []byte) error { return os.ErrPermission } |
| 130 | |