carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Dev proofs — D3 of the console release defences (spec
2// 2026-10-06-console-release-defences-design.md §4.2-§4.4).
3//
4// A proving deployment (dev) signs a Proof once a build has run healthily on
5// its console for a bake period. An operator carries it to a requiring
6// deployment's bucket, and a requiring box adopts a listed app's build only
7// with a proof for those exact bytes from a prover it trusts (proofgate.go).
8// A proof binds app, platform and the artifact's sha256. version is
9// informational, because version names get reused after pruning.
10//
11// The signature covers the canonical JSON of every field except sig:
12// json.Marshal of proofBody, whose field order the struct fixes and whose
13// times are UTC. Verify re-marshals the parsed fields, so a proof whose bytes
14// were reformatted in transit still verifies and one whose fields were edited
15// does not. The proving key is its own ed25519 key, never a release key.
16package release
17
18import (
19 "context"
20 "crypto/ed25519"
21 "crypto/sha256"
22 "encoding/base64"
23 "encoding/hex"
24 "encoding/json"
25 "errors"
26 "fmt"
27 "io"
28 "os"
29 "regexp"
30 "time"
31)
32
33const proofSchema = 1
34
35type Proof struct {
36 Schema int `json:"schema"`
37 App string `json:"app"`
38 Platform string `json:"platform"`
39 SHA256 string `json:"sha256"`
40 Version string `json:"version"`
41 Prover string `json:"prover"`
42 ActiveSince time.Time `json:"active_since"`
43 ProvenAt time.Time `json:"proven_at"`
44 Sig string `json:"sig"`
45}
46
47// proofBody is the signed form: every field of Proof except Sig, in order.
48type proofBody struct {
49 Schema int `json:"schema"`
50 App string `json:"app"`
51 Platform string `json:"platform"`
52 SHA256 string `json:"sha256"`
53 Version string `json:"version"`
54 Prover string `json:"prover"`
55 ActiveSince time.Time `json:"active_since"`
56 ProvenAt time.Time `json:"proven_at"`
57}
58
59func (p *Proof) canonical() ([]byte, error) {
60 return json.Marshal(proofBody{Schema: p.Schema, App: p.App, Platform: p.Platform, SHA256: p.SHA256,
61 Version: p.Version, Prover: p.Prover, ActiveSince: p.ActiveSince.UTC(), ProvenAt: p.ProvenAt.UTC()})
62}
63
64var (
65 proverRe = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,31}$`)
66 sha256Re = regexp.MustCompile(`^[0-9a-f]{64}$`)
67 platformRe = regexp.MustCompile(`^[a-z0-9]+-[a-z0-9]+$`)
68)
69
70// ValidProver is a prover name: lower case, digits and dashes, at most 32.
71func ValidProver(s string) bool { return proverRe.MatchString(s) }
72
73// AdoptPlatform is the artifact key this box installs (adoptPlatform), for
74// callers outside the package that mint or describe proofs.
75func AdoptPlatform() string { return adoptPlatform }
76
77func (p *Proof) shape() error {
78 switch {
79 case p.Schema != proofSchema:
80 return fmt.Errorf("proof: schema %d, want %d", p.Schema, proofSchema)
81 case !ValidApp(p.App):
82 return fmt.Errorf("proof: invalid app %q", p.App)
83 case !platformRe.MatchString(p.Platform):
84 return fmt.Errorf("proof: invalid platform %q", p.Platform)
85 case !sha256Re.MatchString(p.SHA256):
86 return fmt.Errorf("proof: sha256 %q is not 64 lower-case hex digits", p.SHA256)
87 case !ValidVersion(p.Version):
88 return fmt.Errorf("proof: invalid version %q", p.Version)
89 case !ValidProver(p.Prover):
90 return fmt.Errorf("proof: invalid prover %q", p.Prover)
91 case p.ActiveSince.IsZero() || p.ProvenAt.IsZero():
92 return fmt.Errorf("proof: active_since and proven_at are both required")
93 }
94 return nil
95}
96
97// SignProof stamps the schema and signs p with the proving key.
98func SignProof(priv ed25519.PrivateKey, p Proof) (*Proof, error) {
99 if len(priv) != ed25519.PrivateKeySize {
100 return nil, fmt.Errorf("proof: no usable proving key")
101 }
102 p.Schema, p.Sig = proofSchema, ""
103 if err := p.shape(); err != nil {
104 return nil, err
105 }
106 body, err := p.canonical()
107 if err != nil {
108 return nil, err
109 }
110 p.Sig = base64.StdEncoding.EncodeToString(ed25519.Sign(priv, body))
111 return &p, nil
112}
113
114// ParseProof checks shape only. Whether it verifies is the requiring box's
115// question (Check), because only the box holds the prover's public key.
116func ParseProof(b []byte) (*Proof, error) {
117 var p Proof
118 if err := json.Unmarshal(b, &p); err != nil {
119 return nil, fmt.Errorf("proof: %v", err)
120 }
121 if err := p.shape(); err != nil {
122 return nil, err
123 }
124 if sig, err := base64.StdEncoding.DecodeString(p.Sig); err != nil || len(sig) != ed25519.SignatureSize {
125 return nil, fmt.Errorf("proof: sig is not a base64 ed25519 signature")
126 }
127 return &p, nil
128}
129
130func (p *Proof) Verify(pub ed25519.PublicKey) error {
131 if len(pub) != ed25519.PublicKeySize {
132 return fmt.Errorf("proof: no usable prover public key")
133 }
134 sig, err := base64.StdEncoding.DecodeString(p.Sig)
135 if err != nil {
136 return fmt.Errorf("proof: sig: %v", err)
137 }
138 body, err := p.canonical()
139 if err != nil {
140 return err
141 }
142 if !ed25519.Verify(pub, body, sig) {
143 return fmt.Errorf("proof signature does not verify against the configured key")
144 }
145 return nil
146}
147
148// Check is Verify plus the binding: this proof is for exactly app, platform
149// and sha, from prover.
150func (p *Proof) Check(pub ed25519.PublicKey, app, platform, sha, prover string) error {
151 if err := p.Verify(pub); err != nil {
152 return err
153 }
154 switch {
155 case p.App != app:
156 return fmt.Errorf("proof is for app %s, not %s", p.App, app)
157 case p.Platform != platform:
158 return fmt.Errorf("proof is for platform %s, not %s", p.Platform, platform)
159 case p.SHA256 != sha:
160 return fmt.Errorf("proof is for sha256 %s, not %s", p.SHA256, sha)
161 case p.Prover != prover:
162 return fmt.Errorf("proof names prover %s, not %s", p.Prover, prover)
163 }
164 return nil
165}
166
167// FileSHA256 hashes the bytes at path (following a symlink), the one way D3
168// learns a sha256 (spec §4.2, codex 3).
169func FileSHA256(path string) (string, error) {
170 f, err := os.Open(path)
171 if err != nil {
172 return "", err
173 }
174 defer f.Close()
175 h := sha256.New()
176 if _, err := io.Copy(h, f); err != nil {
177 return "", err
178 }
179 return hex.EncodeToString(h.Sum(nil)), nil
180}
181
182func ProofsPrefix(acct, app, sha string) string { return appPrefix(acct, app) + "proofs/" + sha + "/" }
183func ProofKey(acct, app, sha, prover string) string {
184 return ProofsPrefix(acct, app, sha) + prover + ".json"
185}
186func ProofPendingKey(acct, app, sha, prover string) string {
187 return ProofsPrefix(acct, app, sha) + prover + ".pending.json"
188}
189
190func GetProof(ctx context.Context, s Store, acct, app, sha, prover string) (*Proof, error) {
191 if !sha256Re.MatchString(sha) || !ValidProver(prover) || !ValidApp(app) {
192 return nil, fmt.Errorf("proof: invalid key %s/%s/%s", app, sha, prover)
193 }
194 raw, _, err := s.Get(ctx, ProofKey(acct, app, sha, prover))
195 if err != nil {
196 return nil, err
197 }
198 return ParseProof(raw)
199}
200
201func PutProof(ctx context.Context, s Store, acct string, p *Proof) error {
202 if err := p.shape(); err != nil {
203 return err
204 }
205 b, err := json.MarshalIndent(p, "", " ")
206 if err != nil {
207 return err
208 }
209 return putImmutable(ctx, s, ProofKey(acct, p.App, p.SHA256, p.Prover), append(b, '\n'))
210}
211
212// ProofPending is the proving box's note that a listed build is live and
213// baking, so `carlos proof get` can say how long it has to go. Advisory: no
214// box reads it.
215type ProofPending struct {
216 Schema int `json:"schema"`
217 App string `json:"app"`
218 Platform string `json:"platform"`
219 SHA256 string `json:"sha256"`
220 Version string `json:"version"`
221 Prover string `json:"prover"`
222 LiveSince time.Time `json:"live_since"`
223 After string `json:"after"`
224}
225
226func PutProofPending(ctx context.Context, s Store, acct string, p ProofPending) error {
227 if !ValidApp(p.App) || !sha256Re.MatchString(p.SHA256) || !ValidProver(p.Prover) {
228 return fmt.Errorf("proof pending: invalid record %s/%s/%s", p.App, p.SHA256, p.Prover)
229 }
230 p.Schema = proofSchema
231 b, err := json.MarshalIndent(p, "", " ")
232 if err != nil {
233 return err
234 }
235 _, err = s.Put(ctx, ProofPendingKey(acct, p.App, p.SHA256, p.Prover), append(b, '\n'), "")
236 if errors.Is(err, ErrCASConflict) {
237 return nil // the first sighting stands
238 }
239 return err
240}
241
242func GetProofPending(ctx context.Context, s Store, acct, app, sha, prover string) (*ProofPending, error) {
243 if !sha256Re.MatchString(sha) || !ValidProver(prover) || !ValidApp(app) {
244 return nil, fmt.Errorf("proof pending: invalid key %s/%s/%s", app, sha, prover)
245 }
246 raw, _, err := s.Get(ctx, ProofPendingKey(acct, app, sha, prover))
247 if err != nil {
248 return nil, err
249 }
250 var p ProofPending
251 if err := json.Unmarshal(raw, &p); err != nil {
252 return nil, fmt.Errorf("proof pending: %v", err)
253 }
254 return &p, nil
255}
256