package main import ( "os" "os/exec" "path/filepath" "strings" "testing" "github.com/carlosframework/platform/internal/release" ) // carlosTestEnv is os.Environ() with the operator's real login and console // state masked: CARLOS_CREDENTIALS points into the test's own tempdir where // no credential exists, HOME is a scratch dir (the binary reads // ~/.carlos/config for the default console), and CARLOS_CONSOLE is cleared. // Every subprocess that runs the carlos binary MUST build its env through // this AND set cmd.Dir to a scratch dir — the binary walks cwd ancestors // for .carlos/config, so a subprocess running from the real checkout reads // the developer's real config the moment one exists. A test that inherits // the real ~/.carlos is one `go test ./...` on a logged-in laptop away from // writing to a production console — which is exactly how 40+ test accounts // got minted on the flagship (2026-08-04). // // Duplicated from e2e_test.go rather than shared: that file is package // main_test (an external test package with no access to this package's // unexported symbols), this one is package main, and the two cannot share // an unexported helper across the package boundary. func carlosTestEnv(t *testing.T, extra ...string) []string { t.Helper() scratch := t.TempDir() // An inherited CARLOS_SERVICE_TOKEN (a CI shell that exports one) would // make any child that also sets CARLOS_DEPLOYMENT_DIR fail resolveMode's // token-plus-store refusal, so it is dropped here. It is filtered rather // than blanked: a test that passes its own token in extra still wins. var env []string for _, kv := range os.Environ() { if !strings.HasPrefix(kv, "CARLOS_SERVICE_TOKEN=") { env = append(env, kv) } } env = append(env, "CARLOS_CREDENTIALS="+filepath.Join(scratch, "credentials"), "HOME="+scratch, "CARLOS_CONSOLE=", "CARLOS_DESTINATION=", ) return append(env, extra...) } // The scoped signing key as an operator meets it: two modes of one verb, and // a promote that signs with whatever CARLOS_RELEASE_KEY holds. Driven through // the real binary, because the whole feature is about which secret is sitting // in which variable on which machine — a thing the functions underneath // cannot be wrong about on their own. func TestScopedKeysCLI(t *testing.T) { bin := buildCarlos(t) bucket := t.TempDir() scratch := t.TempDir() run := func(env []string, args ...string) (string, error) { t.Helper() cmd := exec.Command(bin, args...) cmd.Env = carlosTestEnv(t, append([]string{ "CARLOS_DEPLOYMENT_DIR=" + bucket, "CARLOS_DATA_DIR=" + t.TempDir(), }, env...)...) // pick() walks cwd ancestors for .carlos/config, so the subprocess must not run from the real checkout. cmd.Dir = scratch out, err := cmd.CombinedOutput() return string(out), err } // -- root keygen, unchanged ------------------------------------------ // // Mutation-mindedness: any change to the no-flag output breaks every // deployment's bootstrap instructions, so it is asserted key by key. out, err := run(nil, "release-keygen") if err != nil { t.Fatalf("release-keygen: %v\n%s", err, out) } rootKey := envValue(t, out, "CARLOS_RELEASE_KEY") rootPub := envValue(t, out, "CARLOS_RELEASE_PUBKEY") if release.IsScopedSeed(rootKey) { t.Fatal("the root key came out tagged as scoped") } rootEnv := []string{"CARLOS_RELEASE_KEY=" + rootKey, "CARLOS_RELEASE_PUBKEY=" + rootPub} // -name/-expires without -scope are refused rather than silently // ignored: an operator who typed them meant to mint a scoped key, and // walking away with a ROOT key they think is scoped is the worst // possible outcome of this verb. if out, err := run(rootEnv, "release-keygen", "-name", "edge-builder"); err == nil { t.Fatalf("release-keygen --name without --scope minted a root key:\n%s", out) } // -- scoped keygen --------------------------------------------------- out, err = run(rootEnv, "release-keygen", "-scope", "canary,edge", "-name", "edge-builder") if err != nil { t.Fatalf("release-keygen --scope: %v\n%s", err, out) } scopedKey := envValue(t, out, "CARLOS_RELEASE_KEY") grant := envValue(t, out, "CARLOS_RELEASE_GRANT") if !release.IsScopedSeed(scopedKey) { t.Fatalf("scoped keygen printed an untagged key %q", scopedKey) } // The fleet's anchor is not reprinted as an assignment. A second // copy-pasteable CARLOS_RELEASE_PUBKEY= line is an invitation to install // it on a box, which would give the deployment two trust anchors and // defeat the entire scheme. (The prose guidance names the variable to say // it does NOT change; that is the opposite of a footgun.) for _, line := range strings.Split(out, "\n") { if strings.HasPrefix(strings.TrimSpace(line), "CARLOS_RELEASE_PUBKEY=") { t.Fatalf("scoped keygen printed a public key assignment:\n%s", out) } } if !strings.Contains(out, "edge-builder") { t.Fatalf("scoped keygen does not name the key it minted:\n%s", out) } // stable is refused at mint, by the verb, naming stable. if out, err := run(rootEnv, "release-keygen", "-scope", "edge,stable", "-name", "bad"); err == nil { t.Fatalf("release-keygen --scope edge,stable succeeded:\n%s", out) } else if !strings.Contains(out, "stable") { t.Fatalf("refusal does not name stable:\n%s", out) } // Minting needs the ROOT key: a scoped key cannot widen itself. scopedOnly := []string{"CARLOS_RELEASE_KEY=" + scopedKey, "CARLOS_RELEASE_PUBKEY=" + rootPub} if out, err := run(scopedOnly, "release-keygen", "-scope", "beta", "-name", "wider"); err == nil { t.Fatalf("a scoped key minted a grant:\n%s", out) } // -- promoting with the scoped key ----------------------------------- art := filepath.Join(t.TempDir(), "kass") if err := os.WriteFile(art, []byte("build one"), 0o755); err != nil { t.Fatal(err) } if out, err := run(rootEnv, "ship", "-app", "kass", "-version", "9f3c1ad", art); err != nil { t.Fatalf("ship: %v\n%s", err, out) } scopedEnv := append(append([]string{}, scopedOnly...), "CARLOS_RELEASE_GRANT="+grant) if out, err := run(scopedEnv, "promote", "-app", "kass", "9f3c1ad", "edge"); err != nil { t.Fatalf("scoped promote to edge: %v\n%s", err, out) } // It verifies for a reader holding only the ROOT public key — which is // every box in the fleet. out, err = run(rootEnv, "channels", "-app", "kass") if err != nil { t.Fatalf("channels: %v\n%s", err, out) } if !strings.Contains(out, "edge") || !strings.Contains(out, "9f3c1ad") { t.Fatalf("channels does not show the scope-signed pointer:\n%s", out) } // -- what the scoped key may not do ---------------------------------- // // stable is refused locally and nothing is written. The bucket check is // the load-bearing half: an error message is easy, a channel pointer that // never appeared is the property. out, err = run(scopedEnv, "promote", "-app", "kass", "9f3c1ad", "stable") if err == nil { t.Fatalf("the edge key promoted to stable:\n%s", out) } for _, want := range []string{"edge-builder", "stable"} { if !strings.Contains(out, want) { t.Errorf("refusal does not name %q:\n%s", want, out) } } if _, err := os.Stat(filepath.Join(bucket, "apps", release.OpsAccount, "kass", "channels", "stable.json")); !os.IsNotExist(err) { t.Fatalf("a stable pointer exists after a refused promote (stat err = %v)", err) } // -hotfix bypasses the ladder, never the grant. if out, err := run(scopedEnv, "promote", "-app", "kass", "-hotfix", "9f3c1ad", "stable"); err == nil { t.Fatalf("--hotfix let the edge key reach stable:\n%s", out) } // A scoped key with no grant beside it fails HERE, naming the grant, // rather than signing something the fleet will silently refuse later. out, err = run(scopedOnly, "promote", "-app", "kass", "9f3c1ad", "edge") if err == nil { t.Fatalf("a scoped key promoted with no grant:\n%s", out) } if !strings.Contains(out, "CARLOS_RELEASE_GRANT") { t.Fatalf("refusal does not name the missing grant:\n%s", out) } // -- and the root key is still unrestricted -------------------------- if out, err := run(rootEnv, "promote", "-app", "kass", "9f3c1ad", "beta"); err != nil { t.Fatalf("root promote to beta: %v\n%s", err, out) } if out, err := run(rootEnv, "promote", "-app", "kass", "9f3c1ad", "stable"); err != nil { t.Fatalf("root promote to stable: %v\n%s", err, out) } } // envValue pulls VAR=value out of a verb's stdout. func envValue(t *testing.T, out, key string) string { t.Helper() for _, line := range strings.Split(out, "\n") { if v, ok := strings.CutPrefix(strings.TrimSpace(line), key+"="); ok { return v } } t.Fatalf("no %s in output:\n%s", key, out) return "" }