carlos / platform Public

carlos proof: keygen on the proving box, get and put to carry a proof

put is idempotent for a byte-identical proof and refuses a different one under the same key.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 7b2cad1b605d3064989a8810bf71a20df379b2d7 parent 1b6339e
3 files changed, +335 −1
  • main.go +4 −1
  • proof_cli.go +182 −0
  • proof_cli_test.go +149 −0

CI failed — run details

diff --git a/main.go b/main.go
index 16f2ccd5b..9de7b8687 100644
--- a/main.go
+++ b/main.go
@@ -132,6 +132,8 @@ func main() {
RunSecrets(args)
case "release-keygen":
runKeygen(args)
+ case "proof":
+ runProof(args)
case "ops":
ops.Run(args)
case "system":
@@ -263,7 +265,8 @@ Your deployment — stand up and administer boxes, routes and accounts
revoke a capability, hibernate it, change its backing
instances with no sub-verb: list THIS box's registry routes
release-keygen mint the root signing key, or a scoped signing key
- economics bill: record the month's AWS bill; backhaul: what region
+ proof dev proofs a box requires before adopting: keygen | get | put
+ economics bill: record the month's AWS bill; backhaul: what region
edges cost us this period
status --live: what every box last reported; --dry-run: leak check
diff --git a/proof_cli.go b/proof_cli.go
new file mode 100644
index 000000000..5c0108c48
--- /dev/null
+++ b/proof_cli.go
@@ -0,0 +1,182 @@
+// carlos proof — D3 of the console release defences (spec
+// 2026-10-06-console-release-defences-design.md §4.2-§4.4).
+//
+// keygen runs as root on the proving console box. get runs against the
+// proving deployment's bucket and prints the proof; put runs against the
+// requiring deployment's bucket. Direct mode only (CARLOS_DEPLOYMENT_BUCKET or
+// _DIR): these are operator verbs, and a proof lives only in a bucket. put
+// checks shape, not the signature: only the requiring box holds the prover's
+// public key, and it checks.
+//
+// put is safe to repeat: the same proof again is a no-op success (a re-run
+// roll script), a different proof under the same key is refused.
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "path"
+ "strings"
+ "time"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+const proofUsage = "usage: carlos proof keygen [--out <file>] | get --app <a> --sha256 <hex> [--prover <name>] | put --app <a> <file>"
+
+func runProof(args []string) {
+ if len(args) == 0 {
+ log.Fatal(proofUsage)
+ }
+ ctx := context.Background()
+ switch args[0] {
+ case "keygen":
+ fs := flag.NewFlagSet("proof keygen", flag.ExitOnError)
+ out := fs.String("out", "/etc/carlos/proving.key", "where to write the private key (root only, mode 0600)")
+ fs.Usage = flagUsage(fs, "")
+ fs.Parse(args[1:])
+ if fs.NArg() != 0 {
+ log.Fatal(proofUsage)
+ }
+ if err := proofKeygen(*out, stdout); err != nil {
+ log.Fatal(err)
+ }
+ case "get":
+ fs := flag.NewFlagSet("proof get", flag.ExitOnError)
+ app := appFlag(fs, "")
+ sha := fs.String("sha256", "", "the artifact's sha256, hex")
+ prover := fs.String("prover", "", "whose proof (default: the only one)")
+ _, _, acctOf := accountFlags(fs)
+ fs.Usage = flagUsage(fs, "")
+ fs.Parse(args[1:])
+ if *app == "" || *sha == "" || fs.NArg() != 0 {
+ log.Fatal(proofUsage)
+ }
+ if err := proofGet(ctx, openStore(), acctOf(), *app, *sha, *prover, stdout); err != nil {
+ log.Fatal(err)
+ }
+ case "put":
+ fs := flag.NewFlagSet("proof put", flag.ExitOnError)
+ app := appFlag(fs, "")
+ _, _, acctOf := accountFlags(fs)
+ fs.Usage = flagUsage(fs, "")
+ fs.Parse(args[1:])
+ if *app == "" || fs.NArg() != 1 {
+ log.Fatal(proofUsage)
+ }
+ raw, err := os.ReadFile(fs.Arg(0))
+ if err != nil {
+ log.Fatal(err)
+ }
+ if err := proofPut(ctx, openStore(), acctOf(), *app, raw, stdout); err != nil {
+ log.Fatal(err)
+ }
+ default:
+ log.Fatal(proofUsage)
+ }
+}
+
+func proofKeygen(file string, w io.Writer) error {
+ seed, pub, err := release.GenerateKey()
+ if err != nil {
+ return err
+ }
+ f, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
+ if errors.Is(err, os.ErrExist) {
+ return fmt.Errorf("%s already exists. A proving key is never overwritten: delete it first if you mean to replace it, then update every require entry that names its public key", file)
+ }
+ if err != nil {
+ return err
+ }
+ if _, err := f.WriteString(seed + "\n"); err != nil {
+ f.Close()
+ return err
+ }
+ if err := f.Close(); err != nil {
+ return err
+ }
+ fmt.Fprintf(w, "wrote the proving key to %s (root only, mode 0600)\n", file)
+ fmt.Fprintf(w, "public key: %s\n", pub)
+ fmt.Fprintf(w, "a requiring box names it in its proof policy: {\"app\":\"<app>\",\"prover\":\"<prover>\",\"key\":%q}\n", pub)
+ return nil
+}
+
+func proofGet(ctx context.Context, s release.Store, acct, app, sha, prover string, w io.Writer) error {
+ if prover == "" {
+ keys, err := s.List(ctx, release.ProofsPrefix(acct, app, sha))
+ if err != nil {
+ return err
+ }
+ var names []string
+ for _, k := range keys {
+ base := path.Base(k)
+ if strings.HasSuffix(base, ".pending.json") {
+ continue
+ }
+ if n, ok := strings.CutSuffix(base, ".json"); ok {
+ names = append(names, n)
+ }
+ }
+ switch len(names) {
+ case 0:
+ return proofNotYet(ctx, s, acct, app, sha, keys)
+ case 1:
+ prover = names[0]
+ default:
+ return fmt.Errorf("%s has proofs from %s; choose one with --prover", sha, strings.Join(names, ", "))
+ }
+ }
+ p, err := release.GetProof(ctx, s, acct, app, sha, prover)
+ if errors.Is(err, os.ErrNotExist) {
+ return proofNotYet(ctx, s, acct, app, sha, []string{release.ProofPendingKey(acct, app, sha, prover)})
+ }
+ if err != nil {
+ return err
+ }
+ b, err := json.MarshalIndent(p, "", " ")
+ if err != nil {
+ return err
+ }
+ fmt.Fprintf(w, "%s\n", b)
+ return nil
+}
+
+// proofNotYet explains a missing proof from the proving box's pending note
+// when there is one among keys.
+func proofNotYet(ctx context.Context, s release.Store, acct, app, sha string, keys []string) error {
+ for _, k := range keys {
+ prover, ok := strings.CutSuffix(path.Base(k), ".pending.json")
+ if !ok {
+ continue
+ }
+ pend, err := release.GetProofPending(ctx, s, acct, app, sha, prover)
+ if err != nil {
+ continue
+ }
+ return fmt.Errorf("not proven yet: %s has been live on %s since %s, proven after %s",
+ sha, pend.Prover, pend.LiveSince.UTC().Format(time.RFC3339), pend.After)
+ }
+ return fmt.Errorf("not proven yet: %s has not been seen live on the proving deployment", sha)
+}
+
+func proofPut(ctx context.Context, s release.Store, acct, app string, raw []byte, w io.Writer) error {
+ p, err := release.ParseProof(raw)
+ if err != nil {
+ return err
+ }
+ if p.App != app {
+ return fmt.Errorf("this proof is for %s, not %s", p.App, app)
+ }
+ if err := release.PutProof(ctx, s, acct, p); err != nil {
+ return err
+ }
+ fmt.Fprintf(w, "put %s's proof of %s %s (sha256 %s) at %s\n", p.Prover, p.App, p.Version, p.SHA256,
+ release.ProofKey(acct, p.App, p.SHA256, p.Prover))
+ return nil
+}
diff --git a/proof_cli_test.go b/proof_cli_test.go
new file mode 100644
index 000000000..c04a3ac30
--- /dev/null
+++ b/proof_cli_test.go
@@ -0,0 +1,149 @@
+package main
+
+import (
+ "bytes"
+ "context"
+ "crypto/ed25519"
+ "encoding/base64"
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+const cliSHA = "e47fcc1e0123456789abcdef0123456789abcdef0123456789abcdef01234567"
+
+func TestProofKeygenWritesARootOnlyKey(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "proving.key")
+ var out bytes.Buffer
+ if err := proofKeygen(path, &out); err != nil {
+ t.Fatal(err)
+ }
+ fi, err := os.Stat(path)
+ if err != nil || fi.Mode().Perm() != 0o600 {
+ t.Fatalf("key file mode = %v, %v; want 0600", fi.Mode().Perm(), err)
+ }
+ seed, _ := os.ReadFile(path)
+ key, err := release.ParseSigningKey(string(seed), path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ pub := base64.StdEncoding.EncodeToString(key.Public().(ed25519.PublicKey))
+ if !strings.Contains(out.String(), "public key: "+pub+"\n") {
+ t.Fatalf("printed %q, want the key file's public half %s", out.String(), pub)
+ }
+ if err := proofKeygen(path, &out); err == nil || !strings.Contains(err.Error(), "already exists") {
+ t.Fatalf("second keygen = %v, want a refusal to overwrite", err)
+ }
+}
+
+func signedCLIProof(t *testing.T) *release.Proof {
+ t.Helper()
+ seed, _, err := release.GenerateKey()
+ if err != nil {
+ t.Fatal(err)
+ }
+ key, err := release.ParseSigningKey(seed, "test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ p, err := release.SignProof(key, release.Proof{App: "console", Platform: "linux-arm64", SHA256: cliSHA,
+ Version: "f4478b15", Prover: "dev", ActiveSince: time.Date(2026, 10, 6, 9, 0, 0, 0, time.UTC),
+ ProvenAt: time.Date(2026, 10, 6, 9, 15, 0, 0, time.UTC)})
+ if err != nil {
+ t.Fatal(err)
+ }
+ return p
+}
+
+// get against dev, put against flagship, byte for byte.
+func TestProofGetThenPutCarriesIt(t *testing.T) {
+ ctx := context.Background()
+ dev, flagship := release.NewDirStore(t.TempDir()), release.NewDirStore(t.TempDir())
+ p := signedCLIProof(t)
+ if err := release.PutProof(ctx, dev, "bab", p); err != nil {
+ t.Fatal(err)
+ }
+ var got bytes.Buffer
+ if err := proofGet(ctx, dev, "bab", "console", cliSHA, "", &got); err != nil {
+ t.Fatalf("proof get: %v", err)
+ }
+ var out bytes.Buffer
+ if err := proofPut(ctx, flagship, "bab", "console", got.Bytes(), &out); err != nil {
+ t.Fatalf("proof put: %v", err)
+ }
+ carried, err := release.GetProof(ctx, flagship, "bab", "console", cliSHA, "dev")
+ if err != nil || carried.Sig != p.Sig {
+ t.Fatalf("carried = %+v, %v", carried, err)
+ }
+}
+
+func TestProofGetNotYetNamesTheBake(t *testing.T) {
+ ctx := context.Background()
+ dev := release.NewDirStore(t.TempDir())
+ if err := release.PutProofPending(ctx, dev, "bab", release.ProofPending{App: "console", Platform: "linux-arm64",
+ SHA256: cliSHA, Version: "f4478b15", Prover: "dev",
+ LiveSince: time.Date(2026, 10, 6, 9, 30, 30, 0, time.UTC), After: "15m"}); err != nil {
+ t.Fatal(err)
+ }
+ err := proofGet(ctx, dev, "bab", "console", cliSHA, "", &bytes.Buffer{})
+ want := "not proven yet: " + cliSHA + " has been live on dev since 2026-10-06T09:30:30Z, proven after 15m"
+ if err == nil || err.Error() != want {
+ t.Fatalf("err = %v, want %q", err, want)
+ }
+ err = proofGet(ctx, release.NewDirStore(t.TempDir()), "bab", "console", cliSHA, "", &bytes.Buffer{})
+ if err == nil || !strings.Contains(err.Error(), "has not been seen live") {
+ t.Fatalf("err = %v, want the never-seen text", err)
+ }
+}
+
+func TestProofPutRefusesAnotherApp(t *testing.T) {
+ var b bytes.Buffer
+ p := signedCLIProof(t)
+ raw, _ := json.MarshalIndent(p, "", " ")
+ if err := proofPut(context.Background(), release.NewDirStore(t.TempDir()), "bab", "other", raw, &b); err == nil {
+ t.Fatal("put a console proof under another app")
+ }
+ if err := proofPut(context.Background(), release.NewDirStore(t.TempDir()), "bab", "console", []byte(`{"schema":1}`), &b); err == nil {
+ t.Fatal("put a malformed proof")
+ }
+}
+
+// Re-running the roll script puts the same proof again: that must succeed.
+func TestProofPutTwiceIsIdempotent(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ p := signedCLIProof(t)
+ raw, _ := json.MarshalIndent(p, "", " ")
+ for i := 0; i < 2; i++ {
+ if err := proofPut(ctx, s, "bab", "console", raw, &bytes.Buffer{}); err != nil {
+ t.Fatalf("put #%d: %v", i+1, err)
+ }
+ }
+}
+
+// A different proof under the same key is refused, and the first one stands.
+func TestProofPutDifferentProofSameKeyFails(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ p := signedCLIProof(t)
+ raw, _ := json.MarshalIndent(p, "", " ")
+ if err := proofPut(ctx, s, "bab", "console", raw, &bytes.Buffer{}); err != nil {
+ t.Fatal(err)
+ }
+ other := *p
+ other.ProvenAt = other.ProvenAt.Add(time.Minute)
+ raw2, _ := json.MarshalIndent(&other, "", " ")
+ err := proofPut(ctx, s, "bab", "console", raw2, &bytes.Buffer{})
+ if err == nil || !strings.Contains(err.Error(), "already exists with different content") {
+ t.Fatalf("err = %v, want a refusal naming different content", err)
+ }
+ got, gerr := release.GetProof(ctx, s, "bab", "console", cliSHA, "dev")
+ if gerr != nil || got.Sig != p.Sig {
+ t.Fatalf("stored proof changed: %+v, %v", got, gerr)
+ }
+}