carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "context"
5 "crypto/ed25519"
6 "math"
7
8 "github.com/carlosframework/platform/internal/maintenanceguard"
9)
10
11const maxWorkerControlETagBytes = 1024
12
13func validWorkerControlETag(etag string) bool {
14 return len(etag) > 0 && len(etag) <= maxWorkerControlETagBytes
15}
16
17// WorkerAssignmentSnapshot binds verified bytes to the store version read.
18// It proves neither worker authentication nor the evidence behind a receipt.
19// Callers must check current enrollment, registration, expiry and the complete
20// maintenance protocol before attempting a publication.
21type WorkerAssignmentSnapshot struct {
22 raw []byte
23 etag string
24 identity WorkerAssignmentIdentity
25}
26
27func (s WorkerAssignmentSnapshot) State(pub ed25519.PublicKey) (WorkerAssignmentState, error) {
28 state, err := VerifyWorkerAssignment(pub, s.raw)
29 if err != nil || state.Identity != s.identity || !validWorkerControlETag(s.etag) {
30 return WorkerAssignmentState{}, ErrWorkerControl
31 }
32 return state, nil
33}
34
35type WorkerControlStore interface {
36 Get(context.Context, string) ([]byte, string, error)
37 Put(context.Context, string, []byte, string) (string, error)
38}
39
40func workerAssignmentKey(identity WorkerAssignmentIdentity) (string, error) {
41 if !identity.Valid() {
42 return "", ErrWorkerControl
43 }
44 return "control/maintenance/assignments/" + identity.Assignment.ID + "/state.json", nil
45}
46
47func ReadWorkerAssignment(ctx context.Context, store WorkerControlStore, pub ed25519.PublicKey, identity WorkerAssignmentIdentity) (WorkerAssignmentSnapshot, error) {
48 key, err := workerAssignmentKey(identity)
49 if err != nil || store == nil || ctx == nil || ctx.Err() != nil {
50 return WorkerAssignmentSnapshot{}, ErrWorkerControl
51 }
52 raw, etag, err := store.Get(ctx, key)
53 if err != nil {
54 return WorkerAssignmentSnapshot{}, err
55 }
56 snapshot := WorkerAssignmentSnapshot{raw: append([]byte(nil), raw...), etag: etag, identity: identity}
57 if _, err := snapshot.State(pub); err != nil {
58 return WorkerAssignmentSnapshot{}, err
59 }
60 return snapshot, nil
61}
62
63// CreateWorkerAssignment writes only an unregistered initial state. The caller
64// must register its immutable assignment reference on an active worker head
65// before it can authorize work. A conflict requires reading existing state;
66// creating again never resets accepted receipts or revives a disabled state.
67// An invalid returned ETag is an uncertain write: re-read the same identity.
68func CreateWorkerAssignment(ctx context.Context, store WorkerControlStore, sg Signer, state WorkerAssignmentState) (WorkerAssignmentSnapshot, error) {
69 key, err := workerAssignmentKey(state.Identity)
70 if err != nil || store == nil || ctx == nil || ctx.Err() != nil || state.Revision != 1 || state.Disabled || state.Preflight != nil || state.Compatibility != nil || state.Execution != nil || state.Resume != nil {
71 return WorkerAssignmentSnapshot{}, ErrWorkerControl
72 }
73 raw, err := SignWorkerAssignment(sg, state)
74 if err != nil {
75 return WorkerAssignmentSnapshot{}, err
76 }
77 etag, err := store.Put(ctx, key, raw, "")
78 if err != nil {
79 return WorkerAssignmentSnapshot{}, err
80 }
81 if !validWorkerControlETag(etag) {
82 return WorkerAssignmentSnapshot{}, ErrWorkerControl
83 }
84 return WorkerAssignmentSnapshot{raw: raw, etag: etag, identity: state.Identity}, nil
85}
86
87// AdvanceWorkerAssignment is a single conditional write, with no retry. A
88// publication and revocation therefore compete on the same storage version.
89// The storage principal must prevent rollback of these objects: signatures
90// alone do not stop a writer replaying an older signed enabled state.
91// An invalid returned ETag is an uncertain write, never evidence of rollback.
92func AdvanceWorkerAssignment(ctx context.Context, store WorkerControlStore, sg Signer, before WorkerAssignmentSnapshot, after WorkerAssignmentState) (WorkerAssignmentSnapshot, error) {
93 if store == nil || ctx == nil || ctx.Err() != nil || sg.usable() != nil || sg.scope != nil || sg.grant != nil {
94 return WorkerAssignmentSnapshot{}, ErrWorkerControl
95 }
96 previous, err := before.State(sg.verifyPub())
97 if err != nil || !validWorkerAssignmentAdvance(previous, after) {
98 return WorkerAssignmentSnapshot{}, ErrWorkerControl
99 }
100 key, err := workerAssignmentKey(previous.Identity)
101 if err != nil {
102 return WorkerAssignmentSnapshot{}, err
103 }
104 raw, err := SignWorkerAssignment(sg, after)
105 if err != nil {
106 return WorkerAssignmentSnapshot{}, err
107 }
108 etag, err := store.Put(ctx, key, raw, before.etag)
109 if err != nil {
110 return WorkerAssignmentSnapshot{}, err
111 }
112 if !validWorkerControlETag(etag) {
113 return WorkerAssignmentSnapshot{}, ErrWorkerControl
114 }
115 return WorkerAssignmentSnapshot{raw: raw, etag: etag, identity: after.Identity}, nil
116}
117
118func sameWorkerReference(a, b *maintenanceguard.Reference) bool {
119 if a == nil || b == nil {
120 return a == nil && b == nil
121 }
122 return *a == *b
123}
124
125func validWorkerAssignmentAdvance(before, after WorkerAssignmentState) bool {
126 if !before.Valid() || !after.Valid() || before.Disabled || before.Identity != after.Identity || before.Expires != after.Expires || before.Revision == math.MaxInt64 || after.Revision != before.Revision+1 {
127 return false
128 }
129 oldRefs := []*maintenanceguard.Reference{before.Preflight, before.Compatibility, before.Execution, before.Resume}
130 newRefs := []*maintenanceguard.Reference{after.Preflight, after.Compatibility, after.Execution, after.Resume}
131 added := -1
132 for i, old := range oldRefs {
133 if sameWorkerReference(old, newRefs[i]) {
134 continue
135 }
136 if after.Disabled || old != nil || newRefs[i] == nil || added != -1 {
137 return false
138 }
139 added = i
140 }
141 if after.Disabled {
142 return added == -1
143 }
144 switch added {
145 case 0:
146 return before.Compatibility == nil && before.Execution == nil && before.Resume == nil
147 case 1:
148 return before.Preflight != nil && before.Execution == nil && before.Resume == nil
149 case 2:
150 return before.Preflight != nil && before.Resume == nil
151 case 3:
152 return before.Execution != nil
153 default:
154 return false
155 }
156}
157