carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// carlos proof — D3 of the console release defences (spec
2// 2026-10-06-console-release-defences-design.md §4.2-§4.4).
3//
4// keygen runs as root on the proving console box. get runs against the
5// proving deployment's bucket and prints the proof; put runs against the
6// requiring deployment's bucket. Direct mode only (CARLOS_DEPLOYMENT_BUCKET or
7// _DIR): these are operator verbs, and a proof lives only in a bucket. put
8// checks shape, not the signature: only the requiring box holds the prover's
9// public key, and it checks.
10//
11// put is safe to repeat: the same proof again is a no-op success (a re-run
12// roll script), a different proof under the same key is refused.
13package main
14
15import (
16 "context"
17 "encoding/json"
18 "errors"
19 "flag"
20 "fmt"
21 "io"
22 "log"
23 "os"
24 "path"
25 "strings"
26 "time"
27
28 "github.com/carlosframework/platform/internal/release"
29)
30
31const proofUsage = "usage: carlos proof history <status|reconstruct|retire|resume> [flags] | keygen [--out <file>] | get --app <a> --sha256 <hex> [--prover <name>] | put --app <a> <file>"
32
33func runProof(args []string) {
34 if len(args) == 0 {
35 log.Fatal(proofUsage)
36 }
37 ctx := context.Background()
38 switch args[0] {
39 case "history":
40 runHistory(args[1:])
41 case "keygen":
42 fs := flag.NewFlagSet("proof keygen", flag.ExitOnError)
43 out := fs.String("out", "/etc/carlos/proving.key", "where to write the private key (root only, mode 0600)")
44 fs.Usage = flagUsage(fs, "")
45 fs.Parse(args[1:])
46 if fs.NArg() != 0 {
47 log.Fatal(proofUsage)
48 }
49 if err := proofKeygen(*out, stdout); err != nil {
50 log.Fatal(err)
51 }
52 case "get":
53 fs := flag.NewFlagSet("proof get", flag.ExitOnError)
54 app := appFlag(fs, "")
55 sha := fs.String("sha256", "", "the artifact's sha256, hex")
56 prover := fs.String("prover", "", "whose proof (default: the only one)")
57 _, _, acctOf := accountFlags(fs)
58 fs.Usage = flagUsage(fs, "")
59 fs.Parse(args[1:])
60 if *app == "" || *sha == "" || fs.NArg() != 0 {
61 log.Fatal(proofUsage)
62 }
63 if err := proofGet(ctx, openStore(), acctOf(), *app, *sha, *prover, stdout); err != nil {
64 log.Fatal(err)
65 }
66 case "put":
67 fs := flag.NewFlagSet("proof put", flag.ExitOnError)
68 app := appFlag(fs, "")
69 _, _, acctOf := accountFlags(fs)
70 fs.Usage = flagUsage(fs, "")
71 fs.Parse(args[1:])
72 if *app == "" || fs.NArg() != 1 {
73 log.Fatal(proofUsage)
74 }
75 raw, err := os.ReadFile(fs.Arg(0))
76 if err != nil {
77 log.Fatal(err)
78 }
79 if err := proofPut(ctx, openStore(), acctOf(), *app, raw, stdout); err != nil {
80 log.Fatal(err)
81 }
82 default:
83 log.Fatal(proofUsage)
84 }
85}
86
87func proofKeygen(file string, w io.Writer) error {
88 seed, pub, err := release.GenerateKey()
89 if err != nil {
90 return err
91 }
92 f, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
93 if errors.Is(err, os.ErrExist) {
94 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)
95 }
96 if err != nil {
97 return err
98 }
99 if _, err := f.WriteString(seed + "\n"); err != nil {
100 f.Close()
101 return err
102 }
103 if err := f.Close(); err != nil {
104 return err
105 }
106 fmt.Fprintf(w, "wrote the proving key to %s (root only, mode 0600)\n", file)
107 fmt.Fprintf(w, "public key: %s\n", pub)
108 fmt.Fprintf(w, "a requiring box names it in its proof policy: {\"app\":\"<app>\",\"prover\":\"<prover>\",\"key\":%q}\n", pub)
109 return nil
110}
111
112func proofGet(ctx context.Context, s release.Store, acct, app, sha, prover string, w io.Writer) error {
113 if prover == "" {
114 keys, err := s.List(ctx, release.ProofsPrefix(acct, app, sha))
115 if err != nil {
116 return err
117 }
118 var names []string
119 for _, k := range keys {
120 base := path.Base(k)
121 if strings.HasSuffix(base, ".pending.json") {
122 continue
123 }
124 if n, ok := strings.CutSuffix(base, ".json"); ok {
125 names = append(names, n)
126 }
127 }
128 switch len(names) {
129 case 0:
130 return proofNotYet(ctx, s, acct, app, sha, keys)
131 case 1:
132 prover = names[0]
133 default:
134 return fmt.Errorf("%s has proofs from %s; choose one with --prover", sha, strings.Join(names, ", "))
135 }
136 }
137 p, err := release.GetProof(ctx, s, acct, app, sha, prover)
138 if errors.Is(err, os.ErrNotExist) {
139 return proofNotYet(ctx, s, acct, app, sha, []string{release.ProofPendingKey(acct, app, sha, prover)})
140 }
141 if err != nil {
142 return err
143 }
144 b, err := json.MarshalIndent(p, "", " ")
145 if err != nil {
146 return err
147 }
148 fmt.Fprintf(w, "%s\n", b)
149 return nil
150}
151
152// proofNotYet explains a missing proof from the proving box's pending note
153// when there is one among keys.
154func proofNotYet(ctx context.Context, s release.Store, acct, app, sha string, keys []string) error {
155 for _, k := range keys {
156 prover, ok := strings.CutSuffix(path.Base(k), ".pending.json")
157 if !ok {
158 continue
159 }
160 pend, err := release.GetProofPending(ctx, s, acct, app, sha, prover)
161 if err != nil {
162 continue
163 }
164 return fmt.Errorf("not proven yet: %s has been live on %s since %s, proven after %s",
165 sha, pend.Prover, pend.LiveSince.UTC().Format(time.RFC3339), pend.After)
166 }
167 return fmt.Errorf("not proven yet: %s has not been seen live on the proving deployment", sha)
168}
169
170func proofPut(ctx context.Context, s release.Store, acct, app string, raw []byte, w io.Writer) error {
171 p, err := release.ParseProof(raw)
172 if err != nil {
173 return err
174 }
175 if p.App != app {
176 return fmt.Errorf("this proof is for %s, not %s", p.App, app)
177 }
178 if err := release.PutProof(ctx, s, acct, p); err != nil {
179 return err
180 }
181 fmt.Fprintf(w, "put %s's proof of %s %s (sha256 %s) at %s\n", p.Prover, p.App, p.Version, p.SHA256,
182 release.ProofKey(acct, p.App, p.SHA256, p.Prover))
183 return nil
184}
185