package host import ( "crypto/ed25519" "crypto/sha256" "encoding/hex" "encoding/json" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostevidence" "github.com/carlosframework/platform/internal/release" "strings" "testing" "time" ) func TestPlan13RecoveryHomeReceipt(t *testing.T) { t.Parallel() for _, bad := range []string{"", "session", "stamp", "enrollment", "signature", "operation", "region", "digest", "unbound", "roster-name"} { t.Run(bad, func(t *testing.T) { f, o, h := plan12Handover(t, hostclaim.KindRestoring, "stop") o.Claim.Op.Intent["to"] = "@home" o.Claim.Op.Intent["home_region"] = "uk" if bad == "unbound" { delete(o.Claim.Op.Intent, "home_region") } key := ed25519.NewKeyFromSeed([]byte(strings.Repeat("b", 32))) en := strings.Repeat("a", 32) d := hostactors.Directory{Store: f.claims, PublicKey: f.signer.Public().(ed25519.PublicKey), Bucket: "bucket"} roster := hostactors.Roster{HomeRegion: "uk", V: 1, Revision: 1, DeploymentBucket: "bucket", Boxes: map[string]hostactors.Box{"box-2": {Enrollment: en, BoxID: "box-2", Label: "box-2", Namespace: "platform", Region: "uk", Pool: "default", State: "active", Cloud: hostactors.Cloud{Provider: "aws", Account: "123", Region: "eu-west-2", InstanceID: "i-target"}, EvidenceSHA256: strings.Repeat("d", 64), CensusPublicKey: key.Public().(ed25519.PublicKey)}}, Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}} if bad == "roster-name" { b := roster.Boxes["box-2"] b.Region = "ie" roster.Boxes["box-2"] = b } wire, _ := release.SignRootEnvelope(release.RootSigner(f.signer), roster) if _, err := f.claims.Put(t.Context(), hostactors.Key, wire, ""); err != nil { t.Fatal(err) } r := StopReceipt{V: 1, ID: "stop", Kind: "stopped", Host: o.Host, Incarnation: o.Claim.Holder.Incarnation, InstallationStamp: 2, WriterSession: 2, OperationID: "m1", Phase: "releasing", ObligationID: "writer-session-2", Box: "box-2", Region: "uk", Enrollment: en, EvidenceAt: time.Now(), RequestLimitNS: int64(2 * time.Minute), ClaimGen: 1, FenceID: "fence", InventorySHA256: strings.Repeat("e", 64), LocalJournalSHA256: strings.Repeat("f", 64), ProducerDrainDigest: strings.Repeat("a", 64), CgroupIdentities: []string{"root-role"}} switch bad { case "session": r.WriterSession++ case "stamp": r.InstallationStamp++ case "enrollment": r.Enrollment = strings.Repeat("e", 32) case "operation": r.OperationID = "other" case "region": r.Region = "ie" } body, _ := json.Marshal(r) receiptKey := stopEvidenceKey(o.Host, "m1", "box-2", "stop") sum := sha256.Sum256(body) signer := key if bad == "signature" { signer = f.signer } if err := hostevidence.Publish(t.Context(), f.claims, signer, receiptKey, body); err != nil { t.Fatal(err) } h.Directory = d o.Claim.Op.Journal["target_stop_receipt"] = receiptKey o.Claim.Op.Journal["target_stop_sha256"] = hex.EncodeToString(sum[:]) o.Claim.Op.Journal[hostclaim.JournalTargetStamp] = "2" // Remove the unsigned report path: the receipt alone must suffice. delete(o.Claim.Op.Journal, hostclaim.JournalEvidenceTarget) if bad == "digest" { o.Claim.Op.Journal["target_stop_sha256"] = strings.Repeat("0", 64) } err := h.recoveryEvidence(t.Context(), o) stored, _, readErr := f.claims.Get(t.Context(), receiptKey) if readErr != nil || string(stored) != string(body) { t.Fatal("receipt bytes changed") } if (err == nil) != (bad == "") { t.Fatal(bad, err) } }) } } func TestPlan13HomeRecoveryFenceBinding(t *testing.T) { t.Parallel() for _, home := range []string{"uk", "", "ie"} { _, o, h := plan12Handover(t, hostclaim.KindRestoring, "fence") o.Claim.Op.Intent["to"] = "@home" o.Claim.Op.Intent["home_region"] = home h.FenceAuthority = plan12FenceAuthority{home: "uk"} err := h.recoveryEvidence(t.Context(), o) if (err == nil) != (home == "uk") { t.Fatal(home, err) } } }