package domains // Proof of control for hostname claims (#215). Before this, the first account // to attach a hostname held it outright: a tenant could take over a name // whose DNS already pointed at the edge (and be served on it, with a // platform-issued certificate), or squat any name forever. A claim is now // pending until its writer proves control, and a pending claim is inert and // yields. import ( "context" "encoding/json" "errors" "testing" "time" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) var proofNow = time.Unix(1_800_000_000, 0).UTC() func proofs(answer bool, calls *int) func() bool { return func() bool { *calls++ return answer } } // An unproven attach writes a reservation, and the reconcile pass derives // nothing from it — no alias, no prefix rows, no wildcard parent, no // catch-all — so the name is neither served nor issued a certificate. This // is the takeover: on main the claim was live the moment it was written. func TestPendingClaimIsNeitherRoutedNorCertified(t *testing.T) { s := release.NewDirStore(t.TempDir()) reg := aliasRegWith(t, registry.Route{ Host: "web.bes.oncarlos.com", App: "web", Account: "bes", Kind: "instance", Status: "ready", }) calls := 0 created, changed, pending, err := AttachClaimProven(context.Background(), s, Claim{ Host: "victim.example.com", Account: "bes", App: "web", Route: "web.bes.oncarlos.com", }, proofNow, proofs(false, &calls)) if err != nil || !created || !changed || !pending { t.Fatalf("unproven attach = created %v changed %v pending %v err %v, want a new pending claim", created, changed, pending, err) } c, err := GetClaim(context.Background(), s, "victim.example.com") if err != nil { t.Fatal(err) } if !c.Pending || c.PendingUntil != proofNow.Add(ClaimPendingTTL).Format(time.RFC3339) { t.Fatalf("stored claim = %+v, want pending until now+%s", *c, ClaimPendingTTL) } // Capabilities and a prefix map set on a pending claim must stay inert too. if _, err := SetClaimCapabilities(context.Background(), s, c.Host, "bes", "web", true, true); err != nil { t.Fatal(err) } if _, err := SetClaimPaths(context.Background(), s, c.Host, "bes", "web", map[string]string{"x": "web.bes.oncarlos.com"}); err != nil { t.Fatal(err) } if err := ReconcileAliases(context.Background(), s, reg, aliasNow(), nil, nil); err != nil { t.Fatal(err) } if as, _ := reg.ListAliases(); len(as) != 0 { t.Fatalf("a pending claim materialized aliases %+v", as) } if ws, _ := reg.ListWildcardDomains(); len(ws) != 0 { t.Fatalf("a pending claim derived wildcard parents %+v", ws) } if ps, _ := reg.ListPaths(); len(ps) != 0 { t.Fatalf("a pending claim derived path rows %+v", ps) } // Proving it later is a re-attach by the same owner: the claim turns // verified in place, and the next pass serves it. created, changed, pending, err = AttachClaimProven(context.Background(), s, Claim{ Host: "victim.example.com", Account: "bes", App: "web", Route: "web.bes.oncarlos.com", }, proofNow, proofs(true, &calls)) if err != nil || created || !changed || pending { t.Fatalf("proven re-attach = created %v changed %v pending %v err %v, want a verifying write", created, changed, pending, err) } if c, _ := GetClaim(context.Background(), s, "victim.example.com"); c.Pending || c.PendingUntil != "" || !c.Wildcard { t.Fatalf("verified claim = %+v, want pending cleared and capabilities kept", *c) } if err := ReconcileAliases(context.Background(), s, reg, aliasNow(), nil, nil); err != nil { t.Fatal(err) } if as, _ := reg.ListAliases(); len(as) == 0 { t.Fatal("the verified claim materialized nothing") } // Once verified, a re-attach never asks again: a router that re-attaches // every hostname on every pass must not cost a DNS lookup per hostname. calls = 0 if _, changed, pending, err := AttachClaimProven(context.Background(), s, Claim{ Host: "victim.example.com", Account: "bes", App: "web", Route: "web.bes.oncarlos.com", }, proofNow, proofs(false, &calls)); err != nil || changed || pending || calls != 0 { t.Fatalf("re-attach of a verified claim = changed %v pending %v err %v after %d proofs, want a silent no-op", changed, pending, err, calls) } } // A pending claim does not block the hostname's real owner: a caller who can // prove control supersedes it at any time, and an unproven caller can take // it only once it has run out. A VERIFIED claim yields to nobody, and the // refusal is the same ErrHostUnavailable either way. func TestPendingClaimYieldsToProofAndToExpiry(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) calls := 0 squat := Claim{Host: "shop.example.com", Account: "aaa", App: "squat", Route: "x.aaa.oncarlos.com"} if _, _, _, err := AttachClaimProven(ctx, s, squat, proofNow, proofs(false, &calls)); err != nil { t.Fatal(err) } if _, err := SetClaimCapabilities(ctx, s, squat.Host, "aaa", "squat", true, false); err != nil { t.Fatal(err) } owner := Claim{Host: "shop.example.com", Account: "bbb", App: "shop", Route: "shop.bbb.oncarlos.com"} if _, _, _, err := AttachClaimProven(ctx, s, owner, proofNow.Add(time.Hour), proofs(false, &calls)); !errors.Is(err, ErrHostUnavailable) { t.Fatalf("unproven attach over a live reservation: err = %v, want ErrHostUnavailable", err) } created, changed, pending, err := AttachClaimProven(ctx, s, owner, proofNow.Add(time.Hour), proofs(true, &calls)) if err != nil || !created || !changed || pending { t.Fatalf("proven attach over a reservation = created %v changed %v pending %v err %v, want a new verified claim", created, changed, pending, err) } c, _ := GetClaim(ctx, s, owner.Host) if c.Account != "bbb" || c.App != "shop" || c.Pending || c.Wildcard { t.Fatalf("superseding claim = %+v, want bbb/shop verified with nothing of the squatter's left", *c) } // The squatter, now facing a verified claim, gets the ordinary refusal, // and no lookup is spent finding that out. calls = 0 if _, _, _, err := AttachClaimProven(ctx, s, squat, proofNow.Add(2*time.Hour), proofs(true, &calls)); !errors.Is(err, ErrHostUnavailable) || calls != 0 { t.Fatalf("attach over a verified claim: err = %v after %d proofs, want ErrHostUnavailable after none", err, calls) } // Expiry: an unproven reservation that has run out is free to anyone, // even another unproven caller, whose own reservation starts fresh. expired := Claim{Host: "old.example.com", Account: "aaa", App: "squat"} if _, _, _, err := AttachClaimProven(ctx, s, expired, proofNow, proofs(false, &calls)); err != nil { t.Fatal(err) } later := proofNow.Add(ClaimPendingTTL + time.Minute) created, _, pending, err = AttachClaimProven(ctx, s, Claim{Host: "old.example.com", Account: "bbb", App: "shop"}, later, proofs(false, &calls)) if err != nil || !created || !pending { t.Fatalf("unproven attach over an expired reservation = created %v pending %v err %v, want a fresh reservation", created, pending, err) } if c, _ := GetClaim(ctx, s, "old.example.com"); c.Account != "bbb" || c.PendingUntil != later.Add(ClaimPendingTTL).Format(time.RFC3339) { t.Fatalf("claim after expiry = %+v, want bbb's reservation from now", *c) } } // Claims written before #215 have no pending key. They are verified — that // is what they have been all along — so they keep serving with no rewrite. func TestClaimWithoutPendingKeyIsVerified(t *testing.T) { s := release.NewDirStore(t.TempDir()) legacy := `{"schema":1,"host":"tickets.example.com","account":"bab","app":"kass","route":"canary/live","created_at":"2026-08-12T00:00:00Z","created_by":"paul@keymail.dev"}` if _, err := s.Put(context.Background(), ClaimKey("tickets.example.com"), []byte(legacy), ""); err != nil { t.Fatal(err) } reg := aliasRegWith(t, registry.Route{ Host: "canary/live", App: "kass", Account: "bab", Kind: "instance", Status: "ready", }) if err := ReconcileAliases(context.Background(), s, reg, aliasNow(), nil, nil); err != nil { t.Fatal(err) } if as, _ := reg.ListAliases(); len(as) != 1 { t.Fatalf("legacy claim materialized %+v, want its alias", as) } // And a verified claim never grows the keys on the wire. body, _ := json.Marshal(Claim{Schema: 1, Host: "a.example.com"}) var m map[string]any _ = json.Unmarshal(body, &m) if _, ok := m["pending"]; ok { t.Fatalf("verified claim marshals a pending key: %s", body) } if _, ok := m["pending_until"]; ok { t.Fatalf("verified claim marshals a pending_until key: %s", body) } } // The token is account- and deployment-bound: a record published for one // account proves nothing for another, or for the same sqid on another // deployment. func TestChallengeTokenIsBound(t *testing.T) { base := ChallengeToken("console.carloku.com", "bes", "shop.example.com") for name, other := range map[string]string{ "another account": ChallengeToken("console.carloku.com", "bab", "shop.example.com"), "another deployment": ChallengeToken("carlos.tito.io", "bes", "shop.example.com"), "another host": ChallengeToken("console.carloku.com", "bes", "www.example.com"), } { if other == base { t.Errorf("%s produces the same token %q", name, base) } } if base != ChallengeToken("console.carloku.com", "bes", "shop.example.com") { t.Error("token is not deterministic") } if got := ChallengeName("shop.example.com"); got != "_carlos-challenge.shop.example.com" { t.Errorf("ChallengeName = %q", got) } } // A pending parent is a reservation, not a holding: it cannot lend an // instance catch-all its one-label names, and a catch-all over it reads as // not live. func TestPendingParentHoldsNoInstanceCatchall(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) calls := 0 if _, _, _, err := AttachClaimProven(ctx, s, Claim{Host: "teams.example.com", Account: "bes", App: "web"}, time.Now(), proofs(false, &calls)); err != nil { t.Fatal(err) } if _, err := AttachInstanceCatchall(ctx, s, "teams.example.com", "bes", "web", "", ""); !errors.Is(err, ErrParentNotHeld) { t.Fatalf("catch-all over a pending parent: err = %v, want ErrParentNotHeld", err) } } // A reservation is never renewed by its own account re-attaching (#215 // review): `carlos deploy` re-attaches on every run, and renewal would let a // squatter hold a name forever without proving it. func TestPendingReservationIsNotRenewed(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) calls := 0 squat := Claim{Host: "shop.example.com", Account: "aaa", App: "squat"} if _, _, _, err := AttachClaimProven(ctx, s, squat, proofNow, proofs(false, &calls)); err != nil { t.Fatal(err) } first, _ := GetClaim(ctx, s, squat.Host) for _, at := range []time.Duration{time.Hour, ClaimPendingTTL - time.Minute, ClaimPendingTTL + time.Hour} { if _, _, _, err := AttachClaimProven(ctx, s, squat, proofNow.Add(at), proofs(false, &calls)); err != nil { t.Fatal(err) } if c, _ := GetClaim(ctx, s, squat.Host); c.PendingUntil != first.PendingUntil { t.Fatalf("re-attach at +%s moved pending_until %s -> %s", at, first.PendingUntil, c.PendingUntil) } } // Past it, anyone may take the name. if created, _, _, err := AttachClaimProven(ctx, s, Claim{Host: squat.Host, Account: "bbb", App: "shop"}, proofNow.Add(ClaimPendingTTL+2*time.Hour), proofs(false, &calls)); err != nil || !created { t.Fatalf("attach after the reservation lapsed: created %v err %v", created, err) } }