carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostfloor
2
3import (
4 "context"
5 "crypto/ed25519"
6 "encoding/json"
7 "errors"
8 "github.com/carlosframework/platform/internal/release"
9 "github.com/carlosframework/platform/internal/rootparticipation"
10 "io"
11 "os"
12 "path/filepath"
13)
14
15type Disk struct {
16 Participation *rootparticipation.Ledger
17 Dir, Bucket string
18 PublicKey ed25519.PublicKey
19}
20
21func (d Disk) Load() (Policy, Verified, error) {
22 raw, err := os.ReadFile(filepath.Join(d.Dir, "policy.json"))
23 if err != nil {
24 return Policy{}, Verified{}, err
25 }
26 var p Policy
27 if json.Unmarshal(raw, &p) != nil {
28 return Policy{}, Verified{}, ErrFloor
29 }
30 f, err := p.Verify(d.PublicKey, d.Bucket)
31 return p, f, err
32}
33func Durable(path string, raw []byte, mode os.FileMode) error {
34 dir := filepath.Dir(path)
35 f, err := os.CreateTemp(dir, ".guard-*")
36 if err != nil {
37 return err
38 }
39 name := f.Name()
40 defer os.Remove(name)
41 if err = f.Chmod(mode); err == nil {
42 _, err = f.Write(raw)
43 }
44 if err == nil {
45 err = f.Sync()
46 }
47 err = errors.Join(err, f.Close())
48 if err != nil {
49 return err
50 }
51 if err = os.Rename(name, path); err != nil {
52 return err
53 }
54 fd, err := os.Open(dir)
55 if err != nil {
56 return err
57 }
58 defer fd.Close()
59 return fd.Sync()
60}
61
62// Persist never creates unpublished state implicitly. Bootstrap creates that
63// explicit root-owned file before installing the guarded unit drop-ins.
64func (d Disk) Persist(next Policy, identities map[string][]byte) error {
65 _, nf, err := func() (Policy, Verified, error) { f, e := next.Verify(d.PublicKey, d.Bucket); return next, f, e }()
66 if err != nil {
67 return err
68 }
69 old, of, err := d.Load()
70 if err != nil {
71 return err
72 }
73 if old.Mode != "unpublished" {
74 if next.Mode == "unpublished" || nf.Digest != of.Digest {
75 return ErrFloor
76 }
77 if old.Mode == "enforced" && next.Mode != "enforced" {
78 return ErrFloor
79 }
80 }
81 for tag, raw := range identities {
82 p, err := release.VerifySystemIdentity(d.PublicKey, raw)
83 if err != nil || p.Current.ReleaseTag != tag {
84 return ErrFloor
85 }
86 if err = Durable(filepath.Join(d.Dir, "identities", tag+".json"), raw, 0644); err != nil {
87 return err
88 }
89 }
90 raw, err := json.Marshal(next)
91 if err != nil {
92 return err
93 }
94 return Durable(filepath.Join(d.Dir, "policy.json"), raw, 0644)
95}
96func (d Disk) VerifyFile(f *os.File, platform string, roles ...string) (string, error) {
97 component := "router"
98 if len(roles) == 1 {
99 component = roles[0]
100 } else if len(roles) > 1 {
101 return "", ErrFloor
102 }
103 p, floor, err := d.Load()
104 if err != nil {
105 return "", err
106 }
107 if p.Mode == "unpublished" {
108 return "", nil
109 }
110 if _, err = f.Seek(0, io.SeekStart); err != nil {
111 return "", err
112 }
113 hash, err := DigestReader(io.LimitReader(f, 512<<20))
114 if err != nil {
115 return "", err
116 }
117 entries, err := os.ReadDir(filepath.Join(d.Dir, "identities"))
118 if err != nil {
119 return "", err
120 }
121 for _, entry := range entries {
122 if entry.IsDir() {
123 continue
124 }
125 wire, err := os.ReadFile(filepath.Join(d.Dir, "identities", entry.Name()))
126 if err != nil {
127 return "", err
128 }
129 identity, err := release.VerifySystemIdentity(d.PublicKey, wire)
130 if err != nil {
131 return "", err
132 }
133 if CheckComponent(d.PublicKey, floor, wire, identity.Current.Version, platform, hash, component) == nil {
134 return hash, nil
135 }
136 }
137 return "", ErrFloor
138}
139func (d Disk) Begin(ctx context.Context, id, dataDir string) error {
140 if id == "" {
141 return ErrFloor
142 }
143 marker := filepath.Join(d.Dir, "update-begun.json")
144 if _, err := os.Stat(marker); err == nil {
145 return ErrFloor
146 } else if !errors.Is(err, os.ErrNotExist) {
147 return err
148 }
149 raw, _ := json.Marshal(struct {
150 ID string `json:"id"`
151 }{id})
152 return d.WithParticipation(ctx, func(ctx context.Context, l rootparticipation.Ledger) error {
153 if err := checkLegacyParticipation(dataDir, false); err != nil {
154 return err
155 }
156 return l.BeginUpdateWith(ctx, id, func() error { return Durable(marker, raw, 0644) })
157 }, func() error {
158 if err := Durable(marker, raw, 0644); err != nil {
159 return err
160 }
161 if err := checkLegacyParticipation(dataDir, true); err != nil {
162 return errors.Join(err, RemoveDurable(marker))
163 }
164 return nil
165 })
166}
167
168// CheckControllersIdle refuses while any controller state is present. The
169// directory must also be readable: a missing or unreadable directory cannot
170// prove that no controller is active.
171func CheckControllersIdle(ctx context.Context, dataDir string) error {
172 return (Disk{}).ControllersIdle(ctx, dataDir)
173}
174
175func RemoveDurable(path string) error {
176 if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
177 return err
178 }
179 dir, err := os.Open(filepath.Dir(path))
180 if err != nil {
181 return err
182 }
183 defer dir.Close()
184 return dir.Sync()
185}
186func UpdateIdle(guardDir, journal string) error {
187 for _, p := range []string{filepath.Join(guardDir, "update-begun.json"), journal} {
188 if _, err := os.Lstat(p); err == nil {
189 return ErrFloor
190 } else if !errors.Is(err, os.ErrNotExist) {
191 return err
192 }
193 }
194 return nil
195}
196
197func (d Disk) WithParticipation(ctx context.Context, fn func(context.Context, rootparticipation.Ledger) error, legacy func() error) error {
198 return d.withParticipation(ctx, fn, legacy, true)
199}
200
201func (d Disk) withParticipation(ctx context.Context, fn func(context.Context, rootparticipation.Ledger) error, legacy func() error, exclude bool) error {
202 l := d.Participation
203 if held, ok, err := rootparticipation.TransactionLedger(ctx); ok || err != nil {
204 if err != nil {
205 return err
206 }
207 if l != nil && l.State != held.State {
208 return rootparticipation.ErrUnavailable
209 }
210 l = &held
211 }
212 var required bool
213 var err error
214 if l != nil {
215 required, err = l.UpdateRequired()
216 } else {
217 var opened rootparticipation.Ledger
218 opened, required, err = rootparticipation.OpenForUpdate()
219 l = &opened
220 if l.State != nil {
221 defer l.State.Close()
222 }
223 }
224 if err != nil {
225 return err
226 }
227 if !required {
228 return legacy()
229 }
230 if !exclude {
231 return fn(ctx, *l)
232 }
233 return l.WithLaunchExclusion(ctx, func(ctx context.Context) error { return fn(ctx, *l) })
234}
235
236func (d Disk) ParticipationRequired(ctx context.Context) (bool, error) {
237 required := false
238 err := d.withParticipation(ctx, func(context.Context, rootparticipation.Ledger) error { required = true; return nil }, func() error { return nil }, false)
239 return required, err
240}
241
242func (d Disk) ControllersIdle(ctx context.Context, dataDir ...string) error {
243 return d.WithParticipation(ctx, func(ctx context.Context, l rootparticipation.Ledger) error {
244 if err := l.Idle(ctx); err != nil {
245 return err
246 }
247 if len(dataDir) == 1 {
248 return checkLegacyParticipation(dataDir[0], false)
249 }
250 return nil
251 }, func() error {
252 if len(dataDir) != 1 {
253 return ErrFloor
254 }
255 return checkLegacyParticipation(dataDir[0], true)
256 })
257}
258
259func (d Disk) BeginParticipation(ctx context.Context, id, dataDir string, commit, legacy func() error) error {
260 return d.WithParticipation(ctx, func(ctx context.Context, l rootparticipation.Ledger) error {
261 if err := checkLegacyParticipation(dataDir, false); err != nil {
262 return err
263 }
264 return l.BeginUpdateWith(ctx, id, commit)
265 }, legacy)
266}
267
268func (d Disk) EndParticipation(ctx context.Context, id string, commit func() error) error {
269 return d.WithParticipation(ctx, func(ctx context.Context, l rootparticipation.Ledger) error {
270 if _, err := l.UpdateID(); errors.Is(err, os.ErrNotExist) {
271 if err := l.BeginUpdate(ctx, id); err != nil {
272 return err
273 }
274 } else if err != nil {
275 return err
276 }
277 return l.EndUpdateWith(ctx, id, commit)
278 }, func() error {
279 if commit != nil {
280 return commit()
281 }
282 return nil
283 })
284}
285