package release import ( "crypto/ed25519" "encoding/base64" "encoding/json" "reflect" "sort" "strings" "testing" "time" ) // The grant-signed wire shape, pinned beside the root-signed one. Captured // with deterministic seeds (root = bytes 1..32, scoped = bytes 101..132) and // frozen timestamps: a grant minted 2026-08-03T09:00Z for canary+edge, // 90 days, over an edge pointer promoted half an hour later. // // The root public key is goldenRootPub — the SAME anchor as the root-signed // fixture, which is the point of the whole scheme: one CARLOS_RELEASE_PUBKEY // verifies both shapes. const goldenGrantSignedWrapper = `{ "payload": "ewogICJzY2hlbWEiOiAxLAogICJhcHAiOiAia2FzcyIsCiAgImNoYW5uZWwiOiAiZWRnZSIsCiAgInZlcnNpb24iOiAiOWYzYzFhZCIsCiAgInByb21vdGVkX2F0IjogIjIwMjYtMDgtMDNUMDk6MzA6MDBaIiwKICAiYXVkaXQiOiBbCiAgICB7CiAgICAgICJ2ZXJzaW9uIjogIjlmM2MxYWQiLAogICAgICAidG8iOiAiZWRnZSIsCiAgICAgICJhY3RvciI6ICJidWlsZGVyQGNpIiwKICAgICAgImF0IjogIjIwMjYtMDgtMDNUMDk6MzA6MDBaIgogICAgfQogIF0KfQ==", "sig": "oXZdFZDT5ul4RlKG3/VgSmhJjKBshdontRLT5ytm6IsB8MbAnoDicG4T2cVIN0pML2DsJUNhYEIcjg9eZvW2Dw==", "grant": { "payload": "ewogICJzY2hlbWEiOiAxLAogICJwdWIiOiAiMmlucFd3TGdEL29WWkZkMSt4MHJvaUtobERPVjdxQnJsT0xBVjdlK2FkQT0iLAogICJydW5ncyI6IFsKICAgICJjYW5hcnkiLAogICAgImVkZ2UiCiAgXSwKICAibmFtZSI6ICJlZGdlLWJ1aWxkZXIiLAogICJtaW50ZWRfYXQiOiAiMjAyNi0wOC0wM1QwOTowMDowMFoiLAogICJleHBpcmVzX2F0IjogIjIwMjYtMTEtMDFUMDk6MDA6MDBaIgp9", "sig": "MlawPu9BYwUcYr2VhAfPqgj2SRWzfDSIeSGxbW+3noo+xB36gSiPo3Aqa4ZNZ5TZvOx+Vjv+mlg/+Rsb1wYZAw==" } }` // rootPubKey is the fixture's root anchor, parsed. func rootPubKey(t *testing.T) ed25519.PublicKey { t.Helper() pub, err := ParsePublicKey(goldenRootPub, "golden") if err != nil { t.Fatal(err) } return pub } // scopedFixture returns a fresh root key, its public half, a scoped key // covering rungs, and a Signer built from them — the setup nearly every // scoped test needs. func scopedFixture(t *testing.T, rungs []string, ttl time.Duration) (root ed25519.PrivateKey, rootPub ed25519.PublicKey, sg Signer) { t.Helper() root, rootPub = testKeys(t) key, blob := mintGrant(t, root, "edge-builder", rungs, ttl) sg, err := ScopedSigner(key, blob, rootPub) if err != nil { t.Fatalf("ScopedSigner: %v", err) } return root, rootPub, sg } // signAs wraps a pointer with a given signer, without a store. The pointer's // promoted_at is signPointer's own stamp, so these are pointers as they would // really be written. func signAs(t *testing.T, sg Signer, app, channel, version string) []byte { t.Helper() w, err := signPointer(sg, &ChannelPointer{Account: "bab", App: app, Channel: channel, Version: version}) if err != nil { t.Fatalf("signPointer(%s): %v", channel, err) } return w } // The grant-signed shape, pinned: three top-level keys, the grant a two-key // sub-object, and the whole thing verifying against the SAME root public key // the fleet already has configured. // // Mutation-mindedness: rename or restructure any wrapper/grant field and this // fails on the key list before the signature check even runs — which is what // stops a "harmless" JSON tag edit from orphaning pointers already in // production buckets. func TestGoldenGrantSignedWrapperShapeAndVerification(t *testing.T) { var top map[string]json.RawMessage if err := json.Unmarshal([]byte(goldenGrantSignedWrapper), &top); err != nil { t.Fatal(err) } got := make([]string, 0, len(top)) for k := range top { got = append(got, k) } sort.Strings(got) if want := []string{"grant", "payload", "sig"}; !reflect.DeepEqual(got, want) { t.Fatalf("grant-signed wrapper keys = %v, want %v", got, want) } var sub map[string]json.RawMessage if err := json.Unmarshal(top["grant"], &sub); err != nil { t.Fatal(err) } got = got[:0] for k := range sub { got = append(got, k) } sort.Strings(got) if want := []string{"payload", "sig"}; !reflect.DeepEqual(got, want) { t.Fatalf("grant sub-object keys = %v, want %v", got, want) } p, err := verifyPointer(rootPubKey(t), []byte(goldenGrantSignedWrapper), "bab", "kass", "edge") if err != nil { t.Fatalf("golden grant-signed pointer does not verify: %v", err) } if p.Version != "9f3c1ad" || p.Channel != "edge" { t.Fatalf("pointer = %+v", p) } // The grant payload's own field names, pinned: they are wire, and a // verifier that silently read zero values out of a renamed field would // treat an expiring grant as one that never expires. var sgw signedWrapper if err := json.Unmarshal([]byte(goldenGrantSignedWrapper), &sgw); err != nil { t.Fatal(err) } raw, err := base64.StdEncoding.DecodeString(sgw.Grant.Payload) if err != nil { t.Fatal(err) } var gk map[string]json.RawMessage if err := json.Unmarshal(raw, &gk); err != nil { t.Fatal(err) } got = got[:0] for k := range gk { got = append(got, k) } sort.Strings(got) if want := []string{"expires_at", "minted_at", "name", "pub", "rungs", "schema"}; !reflect.DeepEqual(got, want) { t.Fatalf("grant payload keys = %v, want %v", got, want) } } // In scope verifies; out of scope does not — at the verify end, where it has // to hold even if the promoting binary was compromised, patched, or simply // older than this one. // // Mutation-mindedness: delete the scope.allows call in verifyPointer and the // beta case passes, which is a builder key placing builds on beta. func TestVerifyEnforcesGrantScope(t *testing.T) { _, rootPub, sg := scopedFixture(t, []string{RungCanary, RungEdge}, 90*24*time.Hour) for _, ch := range []string{"canary/keymail", "edge"} { w := signAs(t, sg, "kass", ch, "9f3c1ad") if _, err := verifyPointer(rootPub, w, "bab", "kass", ch); err != nil { t.Errorf("in-scope channel %q did not verify: %v", ch, err) } } // Out of scope: sign it anyway, bypassing signPointer's own refusal, and // prove the VERIFIER refuses it. The promote-side guard and this one are // independent — an attacker holding the scoped key does not run our // promote path. for _, ch := range []string{"beta", "stable"} { w := forgePointerAs(t, sg, "kass", ch, "9f3c1ad", time.Now().UTC().Truncate(time.Second)) _, err := verifyPointer(rootPub, w, "bab", "kass", ch) if err == nil { t.Fatalf("out-of-scope channel %q verified", ch) } if !strings.Contains(err.Error(), "key grant") { t.Errorf("refusal for %q = %q, want it to name the key grant", ch, err) } } } // forgePointerAs signs a pointer with the signer's key and grant while // skipping signPointer's scope check, and stamps an arbitrary promoted_at. // This is the hostile promoter: it holds the scoped key and will not ask // permission before signing. func forgePointerAs(t *testing.T, sg Signer, app, channel, version string, at time.Time) []byte { t.Helper() p := &ChannelPointer{Schema: 1, App: app, Channel: channel, Version: version, PromotedAt: at} payload, err := json.MarshalIndent(p, "", " ") if err != nil { t.Fatal(err) } b64 := base64.StdEncoding.EncodeToString(payload) w, err := json.MarshalIndent(signedWrapper{ Payload: b64, Sig: base64.StdEncoding.EncodeToString(ed25519.Sign(sg.key, []byte(b64))), Grant: sg.grant, }, "", " ") if err != nil { t.Fatal(err) } return w } // Expiry is a promotion-time gate and nothing else. A pointer promoted while // the grant was valid verifies forever — adoption re-verifies every pass, and // a pointer that stopped verifying at midnight would take a running route's // channel resolution dark for a reason that has nothing to do with the build. // A pointer promoted AFTER expiry never verifies at all. // // Mutation-mindedness: replace p.PromotedAt with time.Now() in verifyPointer's // scope.allows call and the first case fails. The grant used here expired in // 2025, long before this test can ever run, and its pointer must still // resolve — that is only true if expiry is read from the pointer. func TestExpiryGatesPromotionNotAdoption(t *testing.T) { root, rootPub := testKeys(t) key, _ := mintGrant(t, root, "edge-builder", []string{RungEdge}, 30*24*time.Hour) pub, _ := key.Public().(ed25519.PublicKey) // A grant that lived for the first quarter of 2025 and is long dead now. g := Grant{ Schema: 1, Pub: base64.StdEncoding.EncodeToString(pub), Rungs: []string{RungEdge}, Name: "edge-builder", MintedAt: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), ExpiresAt: time.Date(2025, 4, 1, 0, 0, 0, 0, time.UTC), } sg := Signer{key: key, grant: forgeGrantSub(t, root, g), scope: &g, root: rootPub} // Promoted while the grant was valid, read today: still resolves. This is // the property adoption depends on — it re-verifies every pass, and a // pointer that went dark on the grant's expiry date would take a running // route's channel resolution with it. w := forgePointerAs(t, sg, "kass", "edge", "9f3c1ad", time.Date(2025, 2, 1, 0, 0, 0, 0, time.UTC)) if _, err := verifyPointer(rootPub, w, "bab", "kass", "edge"); err != nil { t.Fatalf("a pointer promoted while the grant was valid stopped verifying: %v", err) } // Promoted one second after expiry: dead, and it says why. late := forgePointerAs(t, sg, "kass", "edge", "9f3c1ad", g.ExpiresAt.Add(time.Second)) _, err := verifyPointer(rootPub, late, "bab", "kass", "edge") if err == nil { t.Fatal("a pointer promoted after the grant expired verified") } if !strings.Contains(err.Error(), "expired") { t.Errorf("refusal %q does not say expired", err) } // And the promote side refuses to sign with it at all, now. if _, err := signPointer(sg, &ChannelPointer{Account: "bab", App: "kass", Channel: "edge", Version: "9f3c1ad"}); err == nil { t.Fatal("an expired grant signed a new pointer") } } // The three ways a grant-bearing wrapper can be malformed, each failing // closed, and each naming the link of the chain that broke: a grant the root // did not sign, a grant whose payload was swapped after signing, and a grant // that is perfectly valid but authorises a DIFFERENT key than the one that // signed the pointer. // // The first two share a message on purpose — Ed25519 cannot distinguish a // tampered payload from a wrong signer, and pretending otherwise would be a // lie in an error string. The third is a genuinely different failure and says // so: the grant is fine, the pointer is not. // // Mutation-mindedness: drop the grant signature check and the first two pass; // verify the pointer against `pub` instead of the granted key and the third // passes, which would let any grant launder any signature. func TestGrantChainFailuresAreDistinct(t *testing.T) { root, rootPub := testKeys(t) notRoot, _ := testKeys(t) key, blob := mintGrant(t, root, "edge-builder", []string{RungEdge}, 30*24*time.Hour) sg, err := ScopedSigner(key, blob, rootPub) if err != nil { t.Fatal(err) } good := signAs(t, sg, "kass", "edge", "9f3c1ad") // (1) grant signed by a key that is not the root. forged := withGrant(t, good, forgeGrantSub(t, notRoot, *sg.scope)) if _, err := verifyPointer(rootPub, forged, "bab", "kass", "edge"); err == nil { t.Error("a grant signed by a non-root key was honoured") } else if !strings.Contains(err.Error(), "key grant signature") { t.Errorf("non-root grant refusal = %q", err) } // (2) grant payload swapped for a wider scope after the root signed it. wider := *sg.scope wider.Rungs = []string{RungCanary, RungEdge, RungBeta} sub := *forgeGrantSub(t, root, *sg.scope) widerPayload, err := json.MarshalIndent(wider, "", " ") if err != nil { t.Fatal(err) } sub.Payload = base64.StdEncoding.EncodeToString(widerPayload) tampered := withGrant(t, good, &sub) if _, err := verifyPointer(rootPub, tampered, "bab", "kass", "edge"); err == nil { t.Error("a tampered grant payload was honoured") } else if !strings.Contains(err.Error(), "key grant signature") { t.Errorf("tampered grant refusal = %q", err) } // (3) a valid grant for someone ELSE's key, attached to a pointer this // key signed. The grant verifies; the pointer signature must not. otherKey, otherBlob := mintGrant(t, root, "other-builder", []string{RungEdge}, 30*24*time.Hour) _ = otherKey otherSub, err := parseGrantBlob(otherBlob, "test") if err != nil { t.Fatal(err) } mismatched := withGrant(t, good, otherSub) if _, err := verifyPointer(rootPub, mismatched, "bab", "kass", "edge"); err == nil { t.Error("a grant for a different key was honoured") } else if !strings.Contains(err.Error(), "channel pointer signature") { t.Errorf("mismatched-key refusal = %q, want it to name the POINTER signature", err) } } // forgeGrantSub signs an arbitrary grant with signer and returns the wrapper // sub-object. func forgeGrantSub(t *testing.T, signer ed25519.PrivateKey, g Grant) *signedGrant { t.Helper() payload, err := json.MarshalIndent(g, "", " ") if err != nil { t.Fatal(err) } b64 := base64.StdEncoding.EncodeToString(payload) return &signedGrant{ Payload: b64, Sig: base64.StdEncoding.EncodeToString(ed25519.Sign(signer, []byte(b64))), } } // withGrant swaps the grant sub-object of a wrapper, leaving payload and sig // untouched. func withGrant(t *testing.T, wrapper []byte, sub *signedGrant) []byte { t.Helper() var sw signedWrapper if err := json.Unmarshal(wrapper, &sw); err != nil { t.Fatal(err) } sw.Grant = sub return mustJSON(t, sw) } func mustJSON(t *testing.T, v any) []byte { t.Helper() b, err := json.MarshalIndent(v, "", " ") if err != nil { t.Fatal(err) } return b } // A pointer signed by a scoped key but stripped of its grant is refused: the // signature is not the root's. This is also what an OLD binary does with a // new grant-bearing wrapper — it ignores the field it does not know and // checks the sig against the root key — so the fail-closed deploy order // (binaries first, scoped keys second) is a property, not a hope. // // Mutation-mindedness: default `signer` to anything but the root pubkey when // there is no grant and this passes, which would accept unsigned-by-root // pointers on the entire live fleet. func TestScopedPointerWithoutItsGrantIsRefused(t *testing.T) { _, rootPub, sg := scopedFixture(t, []string{RungEdge}, 30*24*time.Hour) w := signAs(t, sg, "kass", "edge", "9f3c1ad") var sw signedWrapper if err := json.Unmarshal(w, &sw); err != nil { t.Fatal(err) } sw.Grant = nil if _, err := verifyPointer(rootPub, mustJSON(t, sw), "bab", "kass", "edge"); err == nil { t.Fatal("a scoped signature was accepted as a root signature") } } // A scoped signer refuses to SIGN out of scope, independently of anything the // promote verbs check. signPointer is the one place a pointer becomes signed // bytes, so this is the floor under every caller present and future. // // Mutation-mindedness: remove the allows() call from signPointer and this // fails; it is what makes "a caller forgot to check" not a vulnerability. func TestSignPointerRefusesOutOfScope(t *testing.T) { _, _, sg := scopedFixture(t, []string{RungCanary, RungEdge}, 30*24*time.Hour) if _, err := signPointer(sg, &ChannelPointer{Account: "bab", App: "kass", Channel: "stable", Version: "9f3c1ad"}); err == nil { t.Fatal("a canary/edge signer signed stable") } if _, err := signPointer(sg, &ChannelPointer{Account: "bab", App: "kass", Channel: "beta", Version: "9f3c1ad"}); err == nil { t.Fatal("a canary/edge signer signed beta") } if _, err := signPointer(sg, &ChannelPointer{Account: "bab", App: "kass", Channel: "edge", Version: "9f3c1ad"}); err != nil { t.Fatalf("a canary/edge signer refused edge: %v", err) } } // ScopedSigner refuses a grant that does not authorise the key it is handed — // the operator pasted one builder's grant beside another builder's seed. It // fails when the Signer is BUILT, not at the first promote, because a signer // that cannot prove its own authority should not exist. // // Mutation-mindedness: drop the g.Pub comparison and this passes; the signer // would then produce pointers that every verifier rejects, turning a clear // local error into a mystery in production. func TestScopedSignerRefusesAGrantForAnotherKey(t *testing.T) { root, rootPub := testKeys(t) key, _ := mintGrant(t, root, "edge-builder", []string{RungEdge}, 30*24*time.Hour) _, otherBlob := mintGrant(t, root, "other-builder", []string{RungEdge}, 30*24*time.Hour) if _, err := ScopedSigner(key, otherBlob, rootPub); err == nil { t.Fatal("ScopedSigner accepted a grant for a different key") } // And it refuses a grant that does not chain to THIS deployment's root. elsewhere, _ := testKeys(t) otherKey, otherRootBlob := mintGrant(t, elsewhere, "edge-builder", []string{RungEdge}, 30*24*time.Hour) if _, err := ScopedSigner(otherKey, otherRootBlob, rootPub); err == nil { t.Fatal("ScopedSigner accepted a grant signed by another deployment's root key") } }