// Discussion #216: a member must not be able to take over an SES identity // this account's claim did not create, and a sending-domain claim that was // never verified must not hold a domain forever. package mail import ( "context" "errors" "testing" "time" "github.com/carlosframework/platform/internal/dnsrec" "github.com/carlosframework/platform/internal/release" ) // seedForeignIdentity stands in an identity verified in SES outside CARLOS: // tito.io in spec 2026-09-15 §1, an operator's own sign-in domain, or one // created before the claim gate existed. func seedForeignIdentity(p *MemProvisioner, region, domain string) *MemIdentity { id := &MemIdentity{ ARN: "arn:aws:ses:" + region + ":shared:identity/" + domain, DKIMTokens: []string{"classic1", "classic2", "classic3"}, MailFrom: "bounce." + domain, ConfigSet: "classic-events", Status: Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"}, } p.Identities[identityKey(region, domain)] = id return id } // The HIGH finding as it stood: a member's `domains add` on a domain someone // else verified in SES adopted the identity, rewrote its MAIL FROM, and // would have bound and later deleted it. func TestEnsureDomainRefusesAnIdentityItDidNotCreate(t *testing.T) { ctx := context.Background() s, w := release.NewDirStore(t.TempDir()), dnsrec.NewMemWriter() p := NewMemProvisioner("shared") now := time.Date(2026, 10, 3, 12, 0, 0, 0, time.UTC) d := testDeployment() foreign := seedForeignIdentity(p, d.Region, "tito.io") _, _, err := EnsureDomain(ctx, s, p, w, d, "aaa", "jelly", "tito.io", now) if !errors.Is(err, ErrIdentityNotOwned) { t.Fatalf("EnsureDomain on a foreign identity = %v, want ErrIdentityNotOwned", err) } if foreign.MailFrom != "bounce.tito.io" || foreign.ConfigSet != "classic-events" { t.Fatalf("the foreign identity was mutated: MailFrom=%q ConfigSet=%q", foreign.MailFrom, foreign.ConfigSet) } if rec, _, err := GetDomain(ctx, s, "aaa", "tito.io"); err != nil || rec != nil { t.Fatalf("a domain record was written for a refused domain: %+v %v", rec, err) } // The refusal leaves nothing holding the domain: no claim to block the // real owner, and nothing a later Teardown could delete. if c, err := getDomainClaim(ctx, s, "tito.io"); err != nil || c != nil { t.Fatalf("a refused domain kept its claim: %+v %v", c, err) } if err := Teardown(ctx, s, p, w, d, "aaa", "jelly"); err != nil { t.Fatalf("teardown: %v", err) } if _, ok := p.Identities[identityKey(d.Region, "tito.io")]; !ok { t.Fatal("teardown deleted an identity this account never created") } } // fakeTXT is the injectable resolver: name -> TXT values. type fakeTXT map[string][]string func (f fakeTXT) LookupTXT(_ context.Context, name string) ([]string, error) { if v, ok := f[name]; ok { return v, nil } return nil, errors.New("no such host") } // Proof of DNS control is the one way in: the claimant publishes a TXT // record naming its own account, and only then is the existing identity // adopted. Teardown still never deletes it, because CARLOS did not create // it. func TestEnsureDomainAdoptsAForeignIdentityOnlyWithDNSProof(t *testing.T) { ctx := context.Background() s, w := release.NewDirStore(t.TempDir()), dnsrec.NewMemWriter() p := NewMemProvisioner("shared") now := time.Date(2026, 10, 3, 12, 0, 0, 0, time.UTC) d := testDeployment() seedForeignIdentity(p, d.Region, "tito.io") // A proof naming a DIFFERENT account proves nothing for this one. d.Resolver = fakeTXT{ProofRecordName("tito.io"): {ProofRecordValue("bbb")}} if _, _, err := EnsureDomain(ctx, s, p, w, d, "aaa", "jelly", "tito.io", now); !errors.Is(err, ErrIdentityNotOwned) { t.Fatalf("another account's proof was accepted: %v", err) } d.Resolver = fakeTXT{ProofRecordName("tito.io"): {"v=spf1 -all", ProofRecordValue("aaa")}} rec, _, err := EnsureDomain(ctx, s, p, w, d, "aaa", "jelly", "tito.io", now) if err != nil { t.Fatalf("EnsureDomain with a valid proof: %v", err) } st := rec.Regions[d.Region] if !st.Adopted { t.Fatal("an adopted identity was not recorded as adopted") } if st.DKIMTokens[0] != "classic1" { t.Fatalf("adoption did not keep the identity's published DKIM tokens: %v", st.DKIMTokens) } // A re-run needs no proof any more: the record is this account's. d.Resolver = nil again, _, err := EnsureDomain(ctx, s, p, w, d, "aaa", "jelly", "tito.io", now) if err != nil { t.Fatalf("re-run after adoption: %v", err) } if got := again.Regions[d.Region]; !got.Adopted { t.Fatal("re-run lost the adopted flag") } if err := Teardown(ctx, s, p, w, d, "aaa", "jelly"); err != nil { t.Fatalf("teardown: %v", err) } if _, ok := p.Identities[identityKey(d.Region, "tito.io")]; !ok { t.Fatal("teardown deleted an adopted identity") } if c, _ := getDomainClaim(ctx, s, "tito.io"); c != nil { t.Fatalf("teardown left the claim behind: %+v", c) } } // Identities CARLOS itself created for this account keep working with no // proof: a re-run, a sibling app, and the crash between EnsureIdentity and // the record (the claim remembers the create). func TestEnsureDomainKeepsItsOwnIdentitiesWithoutProof(t *testing.T) { ctx := context.Background() s, w := release.NewDirStore(t.TempDir()), dnsrec.NewMemWriter() p := NewMemProvisioner("shared") now := time.Date(2026, 10, 3, 12, 0, 0, 0, time.UTC) d := testDeployment() p.FailNext["EnsureConfigSet"] = errors.New("throttle") if _, _, err := EnsureDomain(ctx, s, p, w, d, "aaa", "app1", "mine.com", now); err == nil { t.Fatal("expected the injected failure") } rec, _, err := EnsureDomain(ctx, s, p, w, d, "aaa", "app1", "mine.com", now) if err != nil { t.Fatalf("re-run after a crash past EnsureIdentity: %v", err) } if rec.Regions[d.Region].Adopted { t.Fatal("an identity CARLOS created was recorded as adopted") } if _, _, err := EnsureDomain(ctx, s, p, w, d, "aaa", "app2", "mine.com", now); err != nil { t.Fatalf("sibling app: %v", err) } } // A record written before this change (no claim stamp, no Adopted field) // still re-ensures and verifies. Teardown no longer deletes its identity: // nothing says CARLOS created it, so it is left in SES. func TestEnsureDomainHonoursALegacyRecord(t *testing.T) { ctx := context.Background() s, w := release.NewDirStore(t.TempDir()), dnsrec.NewMemWriter() p := NewMemProvisioner("shared") now := time.Date(2026, 10, 3, 12, 0, 0, 0, time.UTC) d := testDeployment() id := seedForeignIdentity(p, d.Region, "old.com") // created by a pre-gate walk legacy := &DomainRecord{Domain: "old.com", Kind: KindCustom, Apps: []string{"app1"}, CreatedAt: now, Regions: map[string]*RegionState{d.Region: {IdentityARN: id.ARN, DKIMTokens: id.DKIMTokens, Status: StatusVerified}}} if _, err := PutDomain(ctx, s, "aaa", legacy, ""); err != nil { t.Fatal(err) } if _, _, err := EnsureDomain(ctx, s, p, w, d, "aaa", "app1", "old.com", now); err != nil { t.Fatalf("legacy re-ensure: %v", err) } if _, err := VerifyDomain(ctx, s, p, d, "aaa", "old.com", d.Region, now); err != nil { t.Fatalf("legacy verify: %v", err) } if err := Teardown(ctx, s, p, w, d, "aaa", "app1"); err != nil { t.Fatalf("teardown: %v", err) } if _, ok := p.Identities[identityKey(d.Region, "old.com")]; !ok { t.Fatal("teardown deleted a legacy record's identity that nothing says CARLOS created") } if c, _ := getDomainClaim(ctx, s, "old.com"); c != nil { t.Fatalf("teardown left the claim behind: %+v", c) } } // The reviewer's case: on main, the #216 attack itself left a record that // adopted a foreign identity, with a claim and no stamp. `domains rm` after // this change must not delete the real owner's identity. func TestRemoveDomainSparesALegacyAdoptedIdentity(t *testing.T) { ctx := context.Background() s, w := release.NewDirStore(t.TempDir()), dnsrec.NewMemWriter() p := NewMemProvisioner("shared") now := time.Date(2026, 10, 3, 12, 0, 0, 0, time.UTC) d := testDeployment() id := seedForeignIdentity(p, d.Region, "tito.io") if err := claimDomain(ctx, s, "tito.io", "aaa", "jelly", now); err != nil { t.Fatal(err) } legacy := &DomainRecord{Domain: "tito.io", Kind: KindCustom, Apps: []string{"jelly"}, CreatedAt: now, Regions: map[string]*RegionState{d.Region: {IdentityARN: id.ARN, DKIMTokens: id.DKIMTokens, Status: StatusVerified}}} if _, err := PutDomain(ctx, s, "aaa", legacy, ""); err != nil { t.Fatal(err) } if _, err := RemoveDomain(ctx, s, p, w, d, "aaa", "jelly", "tito.io"); err != nil { t.Fatalf("RemoveDomain: %v", err) } if _, ok := p.Identities[identityKey(d.Region, "tito.io")]; !ok { t.Fatal("domains rm deleted an identity a pre-fix record had adopted") } if c, _ := getDomainClaim(ctx, s, "tito.io"); c != nil { t.Fatalf("the claim was not released: %+v", c) } } // A holder that verifies between the lapse check and the takeover keeps // its claim and its identity: the verified_at stamp moves the etag. func TestLapsedTakeoverLosesToAHolderThatVerifies(t *testing.T) { ctx := context.Background() s, w := release.NewDirStore(t.TempDir()), dnsrec.NewMemWriter() p := NewMemProvisioner("shared") now := time.Date(2026, 10, 3, 12, 0, 0, 0, time.UTC) d := testDeployment() if _, _, err := EnsureDomain(ctx, s, p, w, d, "aaa", "app", "race.com", now); err != nil { t.Fatal(err) } later := now.Add(ClaimLapse + time.Hour) // Judge the lapse on the current etag, then let the holder verify. cur, etag, err := getDomainClaimETag(ctx, s, "race.com") if err != nil { t.Fatal(err) } if lapsed, err := claimLapsed(ctx, s, p, cur, d.Region, later); err != nil || !lapsed { t.Fatalf("setup: claim should read lapsed: %v %v", lapsed, err) } p.SetIdentityStatus(d.Region, "race.com", Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"}) if _, err := VerifyDomain(ctx, s, p, d, "aaa", "race.com", d.Region, later); err != nil { t.Fatal(err) } body := []byte(`{"schema":1,"domain":"race.com","account":"bbb","created_at":"2026-10-11T13:00:00Z"}`) if _, err := s.Put(ctx, domainClaimKey("race.com"), body, etag); !errors.Is(err, release.ErrCASConflict) { t.Fatalf("a takeover on the pre-verify etag = %v, want a CAS conflict", err) } if _, _, err := EnsureDomain(ctx, s, p, w, d, "bbb", "app", "race.com", later); !errors.Is(err, ErrDomainClaimed) { t.Fatalf("takeover after the holder verified = %v, want ErrDomainClaimed", err) } if _, ok := p.Identities[identityKey(d.Region, "race.com")]; !ok { t.Fatal("the verified holder lost its identity") } } // Finding 2: an unverified claim lapses after ClaimLapse and another // account may take the domain. The squatter's half-made identity goes with // it, so the new claimant starts fresh, with DKIM tokens of its own. func TestUnverifiedClaimLapses(t *testing.T) { ctx := context.Background() s, w := release.NewDirStore(t.TempDir()), dnsrec.NewMemWriter() p := NewMemProvisioner("shared") now := time.Date(2026, 10, 3, 12, 0, 0, 0, time.UTC) d := testDeployment() first, _, err := EnsureDomain(ctx, s, p, w, d, "aaa", "squat", "victim.com", now) if err != nil { t.Fatal(err) } squatTokens := first.Regions[d.Region].DKIMTokens if _, _, err := EnsureDomain(ctx, s, p, w, d, "bbb", "real", "victim.com", now.Add(ClaimLapse-time.Hour)); !errors.Is(err, ErrDomainClaimed) { t.Fatalf("a claim inside its lapse window was taken: %v", err) } rec, _, err := EnsureDomain(ctx, s, p, w, d, "bbb", "real", "victim.com", now.Add(ClaimLapse+time.Hour)) if err != nil { t.Fatalf("a lapsed claim was not released to another account: %v", err) } if got := rec.Regions[d.Region].DKIMTokens; got[0] == squatTokens[0] { t.Fatalf("the new holder inherited the lapsed holder's identity: %v", got) } if c, _ := getDomainClaim(ctx, s, "victim.com"); c == nil || c.Account != "bbb" { t.Fatalf("claim after lapse = %+v, want bbb's", c) } // The lapsed holder can neither verify its stale record into a grant // nor tear down the identity that is now bbb's. p.SetIdentityStatus(d.Region, "victim.com", Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"}) if _, err := VerifyDomain(ctx, s, p, d, "aaa", "victim.com", d.Region, now.Add(ClaimLapse+2*time.Hour)); !errors.Is(err, ErrDomainClaimed) { t.Fatalf("lapsed holder's verify = %v, want ErrDomainClaimed", err) } if old, _, _ := GetDomain(ctx, s, "aaa", "victim.com"); old == nil || old.Regions[d.Region].Status == StatusVerified { t.Fatalf("lapsed holder's record reads verified: %+v", old) } if err := Teardown(ctx, s, p, w, d, "aaa", "squat"); err != nil { t.Fatalf("lapsed holder's teardown: %v", err) } if _, ok := p.Identities[identityKey(d.Region, "victim.com")]; !ok { t.Fatal("the lapsed holder's teardown deleted the new holder's identity") } if c, _ := getDomainClaim(ctx, s, "victim.com"); c == nil || c.Account != "bbb" { t.Fatalf("the lapsed holder's teardown disturbed bbb's claim: %+v", c) } } // A verified claim never lapses, even if its record later reads failed — // and neither does one SES has verified while nobody ran verify. func TestVerifiedClaimDoesNotLapse(t *testing.T) { ctx := context.Background() s, w := release.NewDirStore(t.TempDir()), dnsrec.NewMemWriter() p := NewMemProvisioner("shared") now := time.Date(2026, 10, 3, 12, 0, 0, 0, time.UTC) d := testDeployment() if _, _, err := EnsureDomain(ctx, s, p, w, d, "aaa", "app", "kept.com", now); err != nil { t.Fatal(err) } p.SetIdentityStatus(d.Region, "kept.com", Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"}) if _, err := VerifyDomain(ctx, s, p, d, "aaa", "kept.com", d.Region, now); err != nil { t.Fatal(err) } p.SetIdentityStatus(d.Region, "kept.com", Status{DKIM: "FAILED", MailFrom: "SUCCESS", Sending: "SUCCESS"}) if _, err := VerifyDomain(ctx, s, p, d, "aaa", "kept.com", d.Region, now); err != nil { t.Fatal(err) } if _, _, err := EnsureDomain(ctx, s, p, w, d, "bbb", "app", "kept.com", now.Add(10*ClaimLapse)); !errors.Is(err, ErrDomainClaimed) { t.Fatalf("a once-verified claim lapsed: %v", err) } // Verified at SES, never verified through CARLOS. if _, _, err := EnsureDomain(ctx, s, p, w, d, "aaa", "app", "quiet.com", now); err != nil { t.Fatal(err) } p.SetIdentityStatus(d.Region, "quiet.com", Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"}) if _, _, err := EnsureDomain(ctx, s, p, w, d, "bbb", "app", "quiet.com", now.Add(10*ClaimLapse)); !errors.Is(err, ErrDomainClaimed) { t.Fatalf("a claim SES had verified lapsed: %v", err) } }