carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1// The proof policy file — where D3 is switched on (spec
2// 2026-10-06-console-release-defences-design.md §4.5, codex 6).
3//
4// It is not host.env. host.env is root 0600 and reaches the agent only when
5// carlos-edge.service starts, while every root pass starts fresh, so an edit
6// there would reach one process and not the other. This file is root-owned
7// 0644 and holds no secrets: app names, the prover name, public keys and a
8// duration. The agent and root both read it at the start of every pass.
9//
10// Absent: D3 is off. Absent means nothing at all is at the path (Lstat says
11// it does not exist). Anything else that cannot be read or does not parse,
12// including a dangling symlink, a directory, an unknown key or data after the
13// policy object, is an error, and the gate reads an error as "on and waiting
14// for every named-unit route": the box cannot tell which apps were listed,
15// and a typo or a broken link must not open the gate.
16package release
17
18import (
19 "bytes"
20 "crypto/ed25519"
21 "encoding/json"
22 "errors"
23 "fmt"
24 "io"
25 "os"
26 "path/filepath"
27 "time"
28
29 "github.com/carlosframework/platform/internal/safefile"
30)
31
32const (
33 ProofPolicyPath = "/etc/carlos/proof-policy.json"
34 DefaultProveAfter = 15 * time.Minute
35)
36
37type ProofPolicy struct {
38 Schema int `json:"schema"`
39 Prove *ProveBlock `json:"prove,omitempty"`
40 Require []RequireEntry `json:"require,omitempty"`
41}
42
43// ProveBlock makes this box a prover: it mints proofs for Apps once a build
44// has run healthily for After, signed with the root-only key at KeyFile.
45type ProveBlock struct {
46 Apps []string `json:"apps"`
47 Prover string `json:"prover"`
48 After string `json:"after"`
49 KeyFile string `json:"key_file"`
50 after time.Duration
51}
52
53// RequireEntry makes App on this box adopt only builds Prover has proven.
54type RequireEntry struct {
55 App string `json:"app"`
56 Prover string `json:"prover"`
57 Key string `json:"key"`
58 pub ed25519.PublicKey
59}
60
61func (e RequireEntry) PublicKey() ed25519.PublicKey { return e.pub }
62
63func LoadProofPolicy(path string) (*ProofPolicy, error) {
64 // Lstat, not the read's error, decides absence: a symlink whose target
65 // has gone reads as ENOENT, and reading that as "D3 off" would let an
66 // unproven build in because of a broken config.
67 if _, err := os.Lstat(path); errors.Is(err, os.ErrNotExist) {
68 return nil, nil
69 }
70 b, err := safefile.ReadFile(path)
71 if err != nil {
72 return nil, fmt.Errorf("proof policy %s: %w", path, err)
73 }
74 p, err := ParseProofPolicy(b)
75 if err != nil {
76 return nil, fmt.Errorf("proof policy %s: %w", path, err)
77 }
78 return p, nil
79}
80
81func ParseProofPolicy(b []byte) (*ProofPolicy, error) {
82 dec := json.NewDecoder(bytes.NewReader(b))
83 dec.DisallowUnknownFields()
84 var p ProofPolicy
85 if err := dec.Decode(&p); err != nil {
86 return nil, fmt.Errorf("does not parse: %v", err)
87 }
88 // More() is false before a stray '}' or ']', so it cannot prove the end
89 // of input; only a second Decode returning io.EOF does.
90 if err := dec.Decode(&json.RawMessage{}); err != io.EOF {
91 return nil, fmt.Errorf("does not parse: data after the policy object")
92 }
93 if p.Schema != 1 {
94 return nil, fmt.Errorf("schema %d, want 1", p.Schema)
95 }
96 if pr := p.Prove; pr != nil {
97 if len(pr.Apps) == 0 {
98 return nil, fmt.Errorf("prove: apps is empty")
99 }
100 for _, a := range pr.Apps {
101 if !ValidApp(a) {
102 return nil, fmt.Errorf("prove: invalid app %q", a)
103 }
104 }
105 if !ValidProver(pr.Prover) {
106 return nil, fmt.Errorf("prove: invalid prover %q", pr.Prover)
107 }
108 d, err := time.ParseDuration(pr.After)
109 if err != nil || d <= 0 {
110 return nil, fmt.Errorf("prove: after %q is not a positive duration", pr.After)
111 }
112 pr.after = d
113 if !filepath.IsAbs(pr.KeyFile) {
114 return nil, fmt.Errorf("prove: key_file %q is not an absolute path", pr.KeyFile)
115 }
116 }
117 for i := range p.Require {
118 e := &p.Require[i]
119 if !ValidApp(e.App) {
120 return nil, fmt.Errorf("require[%d]: invalid app %q", i, e.App)
121 }
122 if !ValidProver(e.Prover) {
123 return nil, fmt.Errorf("require[%d]: invalid prover %q", i, e.Prover)
124 }
125 k, err := ParsePublicKey(e.Key, fmt.Sprintf("require[%d].key", i))
126 if err != nil {
127 return nil, err
128 }
129 e.pub = k
130 }
131 return &p, nil
132}
133
134func (p *ProofPolicy) Requires(app string) []RequireEntry {
135 if p == nil {
136 return nil
137 }
138 var out []RequireEntry
139 for _, e := range p.Require {
140 if e.App == app {
141 out = append(out, e)
142 }
143 }
144 return out
145}
146
147func (p *ProofPolicy) Proves(app string) bool {
148 if p == nil || p.Prove == nil {
149 return false
150 }
151 for _, a := range p.Prove.Apps {
152 if a == app {
153 return true
154 }
155 }
156 return false
157}
158
159// ProveAfter is how long a build must run before it is proven, and before a
160// root pass records it as healthy (spec §4.5): the prove block's after, else
161// DefaultProveAfter.
162func (p *ProofPolicy) ProveAfter() time.Duration {
163 if p == nil || p.Prove == nil {
164 return DefaultProveAfter
165 }
166 return p.Prove.after
167}
168