// 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 history [flags] | keygen [--out ] | get --app --sha256 [--prover ] | put --app " func runProof(args []string) { if len(args) == 0 { log.Fatal(proofUsage) } ctx := context.Background() switch args[0] { case "history": runHistory(args[1:]) 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\":\"\",\"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 }