| 1 | package release |
| 2 | |
| 3 | import ( |
| 4 | "crypto/ed25519" |
| 5 | "encoding/base64" |
| 6 | "encoding/json" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | "time" |
| 10 | ) |
| 11 | |
| 12 | // mintGrant mints a scoped key + grant with the root key, failing the test on |
| 13 | // error — the happy path most cases start from. |
| 14 | func mintGrant(t *testing.T, root ed25519.PrivateKey, name string, rungs []string, ttl time.Duration) (ed25519.PrivateKey, string) { |
| 15 | t.Helper() |
| 16 | seed, blob, err := MintKeyGrant(root, name, rungs, ttl) |
| 17 | if err != nil { |
| 18 | t.Fatalf("MintKeyGrant(%v): %v", rungs, err) |
| 19 | } |
| 20 | key, err := ParseSigningKey(seed, "test") |
| 21 | if err != nil { |
| 22 | t.Fatalf("ParseSigningKey: %v", err) |
| 23 | } |
| 24 | return key, blob |
| 25 | } |
| 26 | |
| 27 | // forgeGrant signs an arbitrary grant payload with signer — the hand-crafted |
| 28 | // grant an attacker (or a root-key holder trying to delegate stable) would |
| 29 | // produce. This is how the verify-side guards are exercised without going |
| 30 | // through MintKeyGrant, whose own refusals would otherwise hide them. |
| 31 | func forgeGrant(t *testing.T, signer ed25519.PrivateKey, g Grant) string { |
| 32 | t.Helper() |
| 33 | payload, err := json.MarshalIndent(g, "", " ") |
| 34 | if err != nil { |
| 35 | t.Fatal(err) |
| 36 | } |
| 37 | b64 := base64.StdEncoding.EncodeToString(payload) |
| 38 | blob, err := json.MarshalIndent(signedGrant{ |
| 39 | Payload: b64, |
| 40 | Sig: base64.StdEncoding.EncodeToString(ed25519.Sign(signer, []byte(b64))), |
| 41 | }, "", " ") |
| 42 | if err != nil { |
| 43 | t.Fatal(err) |
| 44 | } |
| 45 | return base64.StdEncoding.EncodeToString(blob) |
| 46 | } |
| 47 | |
| 48 | // goodGrant is a grant that passes every check, as the base for one-field |
| 49 | // mutations. |
| 50 | func goodGrant(pub ed25519.PublicKey) Grant { |
| 51 | now := time.Date(2026, 8, 3, 12, 0, 0, 0, time.UTC) |
| 52 | return Grant{ |
| 53 | Schema: 1, |
| 54 | Pub: base64.StdEncoding.EncodeToString(pub), |
| 55 | Rungs: []string{RungCanary, RungEdge}, |
| 56 | Name: "edge-builder", |
| 57 | MintedAt: now, |
| 58 | ExpiresAt: now.Add(90 * 24 * time.Hour), |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | // The property the whole feature exists for, direction one: not even the |
| 63 | // holder of the root key can mint a grant that reaches stable. |
| 64 | // |
| 65 | // Mutation-mindedness: delete the RungStable check in MintKeyGrant and this |
| 66 | // fails on the first two cases — including the one where stable is smuggled |
| 67 | // in beside a legitimate rung, which a check that only looked at rungs[0] |
| 68 | // would miss. |
| 69 | func TestMintRefusesStable(t *testing.T) { |
| 70 | root, _ := testKeys(t) |
| 71 | for _, rungs := range [][]string{ |
| 72 | {RungStable}, |
| 73 | {RungEdge, RungStable}, |
| 74 | {RungCanary, RungStable, RungBeta}, |
| 75 | } { |
| 76 | _, _, err := MintKeyGrant(root, "edge-builder", rungs, 30*24*time.Hour) |
| 77 | if err == nil { |
| 78 | t.Fatalf("MintKeyGrant(%v) succeeded; stable must never be delegable", rungs) |
| 79 | } |
| 80 | if !strings.Contains(err.Error(), "stable") { |
| 81 | t.Errorf("MintKeyGrant(%v) refusal %q does not name stable", rungs, err) |
| 82 | } |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | // Direction two, enforced separately: a grant that names stable and carries a |
| 87 | // VALID root signature is still refused at verification. Mint and verify are |
| 88 | // two different programs in production — the minting binary may be older than |
| 89 | // the verifying one, or may not have been ours at all. |
| 90 | // |
| 91 | // Mutation-mindedness: delete the RungStable check in validateGrant and this |
| 92 | // goes green while TestMintRefusesStable stays green, which is exactly the |
| 93 | // hole the spec asks to be closed in both directions. |
| 94 | func TestVerifyRefusesAHandCraftedStableGrant(t *testing.T) { |
| 95 | root, rootPub := testKeys(t) |
| 96 | _, scopedPub := testKeys(t) |
| 97 | g := goodGrant(scopedPub) |
| 98 | g.Rungs = []string{RungEdge, RungStable} |
| 99 | blob := forgeGrant(t, root, g) |
| 100 | |
| 101 | sg, err := parseGrantBlob(blob, "test") |
| 102 | if err != nil { |
| 103 | t.Fatalf("parseGrantBlob: %v", err) |
| 104 | } |
| 105 | got, err := verifyGrant(rootPub, sg) |
| 106 | if err == nil { |
| 107 | t.Fatalf("verifyGrant honoured a stable grant: %+v", got) |
| 108 | } |
| 109 | if !strings.Contains(err.Error(), "stable") { |
| 110 | t.Errorf("refusal %q does not name stable", err) |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | // The second stable guard, in the scope decision itself: even if a grant |
| 115 | // naming stable were somehow honoured, the channel check refuses stable. |
| 116 | // |
| 117 | // Mutation-mindedness: this is the belt to validateGrant's braces. Remove |
| 118 | // either one alone and one of these two tests fails; that is the point of |
| 119 | // having both. |
| 120 | func TestGrantAllowsRefusesStableChannel(t *testing.T) { |
| 121 | _, scopedPub := testKeys(t) |
| 122 | g := goodGrant(scopedPub) |
| 123 | g.Rungs = []string{RungEdge, RungStable} // as if validateGrant had let it by |
| 124 | err := g.allows("stable", g.MintedAt) |
| 125 | if err == nil { |
| 126 | t.Fatal("grant.allows permitted stable") |
| 127 | } |
| 128 | if !strings.Contains(err.Error(), "stable") { |
| 129 | t.Errorf("refusal %q does not name stable", err) |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | // canary in a grant covers every canary/<branch> channel — the fleet runs on |
| 134 | // canary/* names, so a grant that only matched the literal string "canary" |
| 135 | // would be useless. beta is reachable when granted; a rung outside the grant |
| 136 | // is not; a channel that is not a rung at all is not. |
| 137 | // |
| 138 | // Mutation-mindedness: replace Rung(channel) with a plain string compare and |
| 139 | // the canary/* cases fail; drop the covers() check and the out-of-scope cases |
| 140 | // pass when they must not. |
| 141 | func TestGrantScopeMatchesTheLadderVocabulary(t *testing.T) { |
| 142 | _, scopedPub := testKeys(t) |
| 143 | g := goodGrant(scopedPub) // canary, edge |
| 144 | at := g.MintedAt |
| 145 | for _, ch := range []string{"canary", "canary/platform-adoption", "canary/keymail", "canary/console", "canary/rehearsal", "edge"} { |
| 146 | if err := g.allows(ch, at); err != nil { |
| 147 | t.Errorf("grant covering %v refused %q: %v", g.Rungs, ch, err) |
| 148 | } |
| 149 | } |
| 150 | for _, ch := range []string{"beta", "stable", "wip"} { |
| 151 | if err := g.allows(ch, at); err == nil { |
| 152 | t.Errorf("grant covering %v allowed %q", g.Rungs, ch) |
| 153 | } |
| 154 | } |
| 155 | // A grant that names beta reaches beta and nothing above it. |
| 156 | b := goodGrant(scopedPub) |
| 157 | b.Rungs = []string{RungBeta} |
| 158 | if err := b.allows("beta", at); err != nil { |
| 159 | t.Errorf("beta grant refused beta: %v", err) |
| 160 | } |
| 161 | if err := b.allows("edge", at); err == nil { |
| 162 | t.Error("beta-only grant allowed edge") |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | // Expiry is compared against the moment passed in — the pointer's own |
| 167 | // promoted_at at verification, wall clock at promotion — never against |
| 168 | // time.Now() inside the check. |
| 169 | // |
| 170 | // Mutation-mindedness: swap `at` for time.Now() in allows() and the |
| 171 | // "promoted while valid" case fails, because its `at` is a 2026 date and the |
| 172 | // grant it uses expired 90 days after that. |
| 173 | func TestGrantExpiryIsComparedAgainstTheGivenMoment(t *testing.T) { |
| 174 | _, scopedPub := testKeys(t) |
| 175 | g := goodGrant(scopedPub) |
| 176 | if err := g.allows("edge", g.ExpiresAt.Add(-time.Second)); err != nil { |
| 177 | t.Errorf("grant refused a moment inside its window: %v", err) |
| 178 | } |
| 179 | if err := g.allows("edge", g.ExpiresAt); err != nil { |
| 180 | t.Errorf("grant refused its exact expiry instant: %v", err) |
| 181 | } |
| 182 | err := g.allows("edge", g.ExpiresAt.Add(time.Second)) |
| 183 | if err == nil { |
| 184 | t.Fatal("grant allowed a moment past its expiry") |
| 185 | } |
| 186 | if !strings.Contains(err.Error(), "expired") { |
| 187 | t.Errorf("refusal %q does not say expired", err) |
| 188 | } |
| 189 | } |
| 190 | |
| 191 | // A grant's lifetime is capped at a year, at mint and at verification. A |
| 192 | // root-key holder hand-crafting a ten-year edge key is not an attack the |
| 193 | // scheme can prevent, but it is one it declines to honour: the bound on |
| 194 | // delegation lives in the same place as the bound on what may be delegated. |
| 195 | // |
| 196 | // Mutation-mindedness: raise or delete maxGrantTTL in either place and the |
| 197 | // corresponding half of this test goes green when it must not. |
| 198 | func TestGrantLifetimeIsCappedAtOneYear(t *testing.T) { |
| 199 | root, rootPub := testKeys(t) |
| 200 | if _, _, err := MintKeyGrant(root, "edge-builder", []string{RungEdge}, maxGrantTTL+time.Hour); err == nil { |
| 201 | t.Error("MintKeyGrant accepted a lifetime over the cap") |
| 202 | } |
| 203 | if _, _, err := MintKeyGrant(root, "edge-builder", []string{RungEdge}, maxGrantTTL); err != nil { |
| 204 | t.Errorf("MintKeyGrant refused exactly the cap: %v", err) |
| 205 | } |
| 206 | if _, _, err := MintKeyGrant(root, "edge-builder", []string{RungEdge}, 0); err == nil { |
| 207 | t.Error("MintKeyGrant accepted a zero lifetime") |
| 208 | } |
| 209 | if _, _, err := MintKeyGrant(root, "edge-builder", []string{RungEdge}, -time.Hour); err == nil { |
| 210 | t.Error("MintKeyGrant accepted a negative lifetime") |
| 211 | } |
| 212 | |
| 213 | _, scopedPub := testKeys(t) |
| 214 | g := goodGrant(scopedPub) |
| 215 | g.ExpiresAt = g.MintedAt.Add(maxGrantTTL + time.Hour) |
| 216 | sg, err := parseGrantBlob(forgeGrant(t, root, g), "test") |
| 217 | if err != nil { |
| 218 | t.Fatal(err) |
| 219 | } |
| 220 | if _, err := verifyGrant(rootPub, sg); err == nil { |
| 221 | t.Error("verifyGrant honoured a grant running longer than the cap") |
| 222 | } |
| 223 | } |
| 224 | |
| 225 | // A grant naming a rung that is not in the frozen vocabulary fails closed — |
| 226 | // a typo must not silently widen or narrow a scope. |
| 227 | // |
| 228 | // Mutation-mindedness: drop the RungIndex check and "prod" is honoured as a |
| 229 | // rung nothing matches, which is a scope nobody can reason about. |
| 230 | func TestGrantRejectsUnknownRungs(t *testing.T) { |
| 231 | root, rootPub := testKeys(t) |
| 232 | if _, _, err := MintKeyGrant(root, "edge-builder", []string{"prod"}, time.Hour); err == nil { |
| 233 | t.Error("MintKeyGrant accepted an unknown rung") |
| 234 | } |
| 235 | if _, _, err := MintKeyGrant(root, "edge-builder", nil, time.Hour); err == nil { |
| 236 | t.Error("MintKeyGrant accepted an empty rung list") |
| 237 | } |
| 238 | _, scopedPub := testKeys(t) |
| 239 | g := goodGrant(scopedPub) |
| 240 | g.Rungs = []string{"prod"} |
| 241 | sg, err := parseGrantBlob(forgeGrant(t, root, g), "test") |
| 242 | if err != nil { |
| 243 | t.Fatal(err) |
| 244 | } |
| 245 | if _, err := verifyGrant(rootPub, sg); err == nil { |
| 246 | t.Error("verifyGrant honoured an unknown rung") |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | // A grant signed by anything other than the root key the verifier is pinned |
| 251 | // to fails, and says so as a GRANT failure — distinct from a pointer |
| 252 | // signature failure, so an operator can tell which link of the chain broke. |
| 253 | // |
| 254 | // A tampered payload and a wrong signer are cryptographically the same event |
| 255 | // ("this signature does not check out") and deliberately share one message; |
| 256 | // what must never happen is either of them being honoured. |
| 257 | // |
| 258 | // Mutation-mindedness: remove the ed25519.Verify in verifyGrant and both |
| 259 | // cases go green, and anyone able to write a bucket object could mint trust. |
| 260 | func TestGrantSignatureMustBeByTheRootKey(t *testing.T) { |
| 261 | root, rootPub := testKeys(t) |
| 262 | notRoot, _ := testKeys(t) |
| 263 | _, scopedPub := testKeys(t) |
| 264 | |
| 265 | // Signed by a key that is not the root. |
| 266 | sg, err := parseGrantBlob(forgeGrant(t, notRoot, goodGrant(scopedPub)), "test") |
| 267 | if err != nil { |
| 268 | t.Fatal(err) |
| 269 | } |
| 270 | if _, err := verifyGrant(rootPub, sg); err == nil { |
| 271 | t.Fatal("verifyGrant honoured a grant signed by a non-root key") |
| 272 | } else if !strings.Contains(err.Error(), "key grant signature") { |
| 273 | t.Errorf("refusal %q does not name the grant signature", err) |
| 274 | } |
| 275 | |
| 276 | // Signed by the root, then the payload swapped for a wider scope. |
| 277 | good := goodGrant(scopedPub) |
| 278 | wider := good |
| 279 | wider.Rungs = []string{RungCanary, RungEdge, RungBeta} |
| 280 | tampered, err := parseGrantBlob(forgeGrant(t, root, good), "test") |
| 281 | if err != nil { |
| 282 | t.Fatal(err) |
| 283 | } |
| 284 | widerPayload, err := json.MarshalIndent(wider, "", " ") |
| 285 | if err != nil { |
| 286 | t.Fatal(err) |
| 287 | } |
| 288 | tampered.Payload = base64.StdEncoding.EncodeToString(widerPayload) |
| 289 | if _, err := verifyGrant(rootPub, tampered); err == nil { |
| 290 | t.Fatal("verifyGrant honoured a tampered grant payload") |
| 291 | } else if !strings.Contains(err.Error(), "key grant signature") { |
| 292 | t.Errorf("refusal %q does not name the grant signature", err) |
| 293 | } |
| 294 | } |
| 295 | |
| 296 | // A minted grant round-trips: the blob parses, verifies against the root |
| 297 | // public key, and describes the key that was minted with it. |
| 298 | func TestMintedGrantRoundTrips(t *testing.T) { |
| 299 | root, rootPub := testKeys(t) |
| 300 | key, blob := mintGrant(t, root, "edge-builder", []string{RungCanary, RungEdge}, 30*24*time.Hour) |
| 301 | |
| 302 | sg, err := parseGrantBlob(blob, "test") |
| 303 | if err != nil { |
| 304 | t.Fatalf("parseGrantBlob: %v", err) |
| 305 | } |
| 306 | g, err := verifyGrant(rootPub, sg) |
| 307 | if err != nil { |
| 308 | t.Fatalf("verifyGrant: %v", err) |
| 309 | } |
| 310 | if g.Schema != 1 || g.Name != "edge-builder" { |
| 311 | t.Fatalf("grant = %+v", g) |
| 312 | } |
| 313 | want := base64.StdEncoding.EncodeToString(key.Public().(ed25519.PublicKey)) |
| 314 | if g.Pub != want { |
| 315 | t.Fatalf("grant pub = %q, want the minted key's public half %q", g.Pub, want) |
| 316 | } |
| 317 | if got := g.ExpiresAt.Sub(g.MintedAt); got != 30*24*time.Hour { |
| 318 | t.Fatalf("grant lifetime = %v, want 720h", got) |
| 319 | } |
| 320 | if err := g.allows("edge", g.MintedAt); err != nil { |
| 321 | t.Fatalf("minted edge grant refuses edge: %v", err) |
| 322 | } |
| 323 | } |
| 324 | |
| 325 | // The scoped seed is TAGGED. A scoped key and a root key are both 32 random |
| 326 | // bytes, so nothing downstream could otherwise tell them apart — and the one |
| 327 | // mistake that must never pass silently is a scoped seed dropped into the |
| 328 | // root key's slot with no grant beside it. ParseSigningKey accepts the tag |
| 329 | // and an untagged seed alike, so every existing root key keeps parsing. |
| 330 | // |
| 331 | // Mutation-mindedness: drop the prefix from MintKeyGrant's seed and the |
| 332 | // promote-side "grant is missing" refusal loses the only signal it has. |
| 333 | func TestScopedSeedIsTaggedAndStillParses(t *testing.T) { |
| 334 | root, _ := testKeys(t) |
| 335 | seed, _, err := MintKeyGrant(root, "edge-builder", []string{RungEdge}, time.Hour) |
| 336 | if err != nil { |
| 337 | t.Fatal(err) |
| 338 | } |
| 339 | if !strings.HasPrefix(seed, ScopedKeyPrefix) { |
| 340 | t.Fatalf("minted seed %q is not tagged with %q", seed, ScopedKeyPrefix) |
| 341 | } |
| 342 | if !IsScopedSeed(seed) { |
| 343 | t.Fatalf("IsScopedSeed(%q) = false", seed) |
| 344 | } |
| 345 | key, err := ParseSigningKey(seed, "test") |
| 346 | if err != nil { |
| 347 | t.Fatalf("ParseSigningKey on a tagged seed: %v", err) |
| 348 | } |
| 349 | // The same 32 bytes without the tag parse to the same key, and are not |
| 350 | // mistaken for a scoped seed. |
| 351 | plain := strings.TrimPrefix(seed, ScopedKeyPrefix) |
| 352 | if IsScopedSeed(plain) { |
| 353 | t.Error("an untagged seed was reported scoped") |
| 354 | } |
| 355 | same, err := ParseSigningKey(plain, "test") |
| 356 | if err != nil { |
| 357 | t.Fatalf("ParseSigningKey on an untagged seed: %v", err) |
| 358 | } |
| 359 | if !key.Equal(same) { |
| 360 | t.Error("tagged and untagged forms of one seed parsed to different keys") |
| 361 | } |
| 362 | } |
| 363 | |
| 364 | // A grant blob that is not base64, not JSON, or not a grant at all fails with |
| 365 | // a message naming where it came from — the operator pasted the wrong secret. |
| 366 | func TestGrantBlobParseErrorsNameTheSource(t *testing.T) { |
| 367 | for _, blob := range []string{"", "not base64!!", base64.StdEncoding.EncodeToString([]byte("{not json"))} { |
| 368 | _, err := parseGrantBlob(blob, "CARLOS_RELEASE_GRANT") |
| 369 | if err == nil { |
| 370 | t.Fatalf("parseGrantBlob(%q) succeeded", blob) |
| 371 | } |
| 372 | if !strings.Contains(err.Error(), "CARLOS_RELEASE_GRANT") { |
| 373 | t.Errorf("error %q does not name the source", err) |
| 374 | } |
| 375 | } |
| 376 | } |
| 377 | |
| 378 | // A pointer dated before the grant existed is forged history: no real |
| 379 | // promotion by a key can predate the key, so the floor refuses it outright |
| 380 | // (2026-08-03 review, IMPORTANT 1 — before the floor, a scoped key could |
| 381 | // sign a pointer dated years before its own mint and it verified). |
| 382 | // |
| 383 | // Mutation-mindedness: delete the at.Before(g.MintedAt) refusal in allows() |
| 384 | // and the first half of this test goes green when it must not; the second |
| 385 | // half pins that the exact mint instant — the earliest honest promotion — |
| 386 | // stays allowed, so the floor cannot drift into refusing real history. |
| 387 | func TestGrantRefusesAPointerPredatingTheGrant(t *testing.T) { |
| 388 | _, scopedPub := testKeys(t) |
| 389 | g := goodGrant(scopedPub) |
| 390 | err := g.allows("edge", g.MintedAt.Add(-time.Second)) |
| 391 | if err == nil { |
| 392 | t.Fatal("grant honoured a pointer dated before the grant was minted") |
| 393 | } |
| 394 | if !strings.Contains(err.Error(), "predates") { |
| 395 | t.Errorf("refusal %q does not say the pointer predates the key", err) |
| 396 | } |
| 397 | if err := g.allows("edge", g.MintedAt); err != nil { |
| 398 | t.Errorf("grant refused its own mint instant: %v", err) |
| 399 | } |
| 400 | } |
| 401 | |
| 402 | // Duplicate rungs are collapsed at mint (2026-08-03 review, MINOR 6): |
| 403 | // harmless to verify, noise in every listing of the grant forever. |
| 404 | func TestMintDeduplicatesRungs(t *testing.T) { |
| 405 | root, _ := testKeys(t) |
| 406 | rootPub := root.Public().(ed25519.PublicKey) |
| 407 | _, blob := mintGrant(t, root, "dupes", []string{"edge", "edge", "canary", "edge"}, time.Hour) |
| 408 | sg, err := parseGrantBlob(blob, "test") |
| 409 | if err != nil { |
| 410 | t.Fatal(err) |
| 411 | } |
| 412 | g, err := verifyGrant(rootPub, sg) |
| 413 | if err != nil { |
| 414 | t.Fatal(err) |
| 415 | } |
| 416 | if len(g.Rungs) != 2 { |
| 417 | t.Fatalf("rungs = %v, want the two distinct ones", g.Rungs) |
| 418 | } |
| 419 | } |
| 420 | |