-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathseed.go
342 lines (301 loc) · 9.85 KB
/
seed.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
// Package fleet provides a distributed peer-to-peer communication framework.
package fleet
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"errors"
"fmt"
"io"
"log/slog"
"os"
"time"
"golang.org/x/crypto/sha3"
"github.com/google/uuid"
)
// seedData represents a cluster-wide shared secret.
// The seed is used to create a unique cluster identity, authenticate peers,
// and provide encryption for secure communication.
//
// The seed is synchronized across all peers in the fleet using the newest-wins
// policy, where newer timestamps take precedence.
type seedData struct {
seed []byte // Raw seed data (128 bytes of cryptographic randomness)
Id uuid.UUID // UUID derived from the seed for identification
ts time.Time // Timestamp when the seed was created
}
var (
// UUID namespace for seed IDs
uuidSeedidSpace = uuid.Must(uuid.Parse(UUID_SEEDID_SPACE))
)
// UUID namespace for generating deterministic seed IDs
const UUID_SEEDID_SPACE = "da736663-83ec-46ef-9c29-3f9102c5c519"
// makeSeed creates a new seedData instance from raw seed bytes and a timestamp.
// It generates a deterministic UUID from the seed for identification purposes.
//
// Parameters:
// - s: Raw seed data (128 bytes)
// - t: Timestamp for the seed
//
// Returns:
// - A new seedData instance
func makeSeed(s []byte, t time.Time) *seedData {
// Generate a deterministic UUID from the seed using SHA3-256
// UUID v6 is used (not in UUID spec, but provides deterministic output)
seedId := uuid.NewHash(sha3.New256(), uuidSeedidSpace, s, 6)
return &seedData{
seed: s, // Raw seed data
Id: seedId, // UUID derived from seed
ts: t, // Timestamp
}
}
// initSeed initializes the agent's seed data.
// The seed is loaded from the database if available, or from a legacy file,
// or generated fresh if neither exists.
//
// The seed serves as a cluster-wide shared secret that all peers in the
// same fleet will synchronize to the newest one.
func (a *Agent) initSeed() {
// Try to load seed from database
// The seed is stored in the "fleet" bucket for internal data
if d, err := a.dbSimpleGet([]byte("fleet"), []byte("seed")); d != nil && err == nil && len(d) > 128 {
// Found seed data in database
t := time.Time{}
if t.UnmarshalBinary(d[128:]) == nil {
// Successfully parsed the timestamp
a.seed = makeSeed(d[:128], t)
slog.Debug(fmt.Sprintf("[fleet] Initialized with saved cluster seed ID = %s", a.SeedId()),
"event", "fleet:seed:init")
return
}
}
// Prepare buffer for seed (128 bytes of randomness)
s := make([]byte, 128)
// Legacy: Try to load from file (older versions stored the seed in a file)
if f, err := os.Open("fleet_seed.bin"); err == nil {
defer f.Close()
// Try to read the seed data
n, err := f.Read(s)
if n == 128 && err == nil {
// Read the timestamp that follows the seed
tsBin, err := io.ReadAll(f)
if err == nil {
t := time.Time{}
if t.UnmarshalBinary(tsBin) == nil {
// Successfully loaded from file
a.seed = makeSeed(s, t)
slog.Debug(fmt.Sprintf("[fleet] Initialized with saved cluster seed ID = %s", a.SeedId()),
"event", "fleet:seed:init")
// Migrate to database storage and remove the file
if a.seed.WriteToDisk(a) == nil {
os.Remove("fleet_seed.bin")
}
return
}
}
}
}
// No existing seed found, generate a new one
_, err := io.ReadFull(rand.Reader, s)
if err != nil {
panic(fmt.Sprintf("failed to initialize fleet seed: %s", err))
}
// Create and save the new seed
a.seed = makeSeed(s, time.Now())
a.seed.WriteToDisk(a)
slog.Debug(fmt.Sprintf("[fleet] Initialized with cluster seed ID = %s", a.SeedId()),
"event", "fleet:seed:new")
}
// SeedId returns the UUID identifier for the cluster seed.
// This is a deterministic UUID generated from the seed data.
//
// Returns:
// - UUID derived from the seed
func (a *Agent) SeedId() uuid.UUID {
return a.seed.Id
}
// WriteToDisk persists the seed data to the database.
// This saves both the raw seed and its timestamp for later retrieval.
//
// Parameters:
// - a: The agent to use for database access
//
// Returns:
// - An error if the operation fails, nil otherwise
func (s *seedData) WriteToDisk(a *Agent) error {
// Marshal the timestamp to binary form
ts, err := s.ts.MarshalBinary()
if err != nil {
return err
}
// Store seed + timestamp in the fleet bucket
err = a.dbSimpleSet([]byte("fleet"), []byte("seed"), append(s.seed, ts...))
if err != nil {
return err
}
return nil
}
// SeedTlsConfig configures a TLS config object with session ticket keys
// derived from the seed. This ensures all nodes in the fleet use the same
// ticket keys, allowing session resumption between different nodes.
//
// Parameters:
// - c: The TLS config to modify
func (a *Agent) SeedTlsConfig(c *tls.Config) {
// Generate a key from part of the seed
k := sha256.Sum256(a.seed.seed[32:64])
// TODO: use hmac for better security
// Set the session ticket keys
c.SetSessionTicketKeys([][32]byte{k})
}
// SeedShake128 creates a new cSHAKE-128 hash instance customized with the seed.
// This provides a deterministic pseudo-random function that's consistent
// across all peers with the same seed.
//
// Parameters:
// - N: The function name/customization string
//
// Returns:
// - A ShakeHash instance for generating deterministic output
func (a *Agent) SeedShake128(N []byte) sha3.ShakeHash {
return sha3.NewCShake128(N, a.seed.seed)
}
// SeedShake256 creates a new cSHAKE-256 hash instance customized with the seed.
// This provides a deterministic pseudo-random function that's consistent
// across all peers with the same seed, with higher security than SeedShake128.
//
// Parameters:
// - N: The function name/customization string
//
// Returns:
// - A ShakeHash instance for generating deterministic output
func (a *Agent) SeedShake256(N []byte) sha3.ShakeHash {
return sha3.NewCShake256(N, a.seed.seed)
}
// SeedSign creates an HMAC signature for the input data using SHA3-256.
// This is used to authenticate messages between peers.
//
// Parameters:
// - in: The data to sign
//
// Returns:
// - The HMAC signature
func (a *Agent) SeedSign(in []byte) []byte {
h := hmac.New(sha3.New256, a.seed.seed)
h.Write(in)
return h.Sum([]byte{})
}
// SeedCrypt encrypts data using AES-GCM with a key derived from the seed.
// This provides authenticated encryption for sensitive data.
//
// Parameters:
// - in: The plaintext data to encrypt
//
// Returns:
// - The encrypted data (nonce + ciphertext)
// - An error if encryption fails
func (a *Agent) SeedCrypt(in []byte) ([]byte, error) {
// Create a new AES cipher using the first 32 bytes of the seed as the key
block, err := aes.NewCipher(a.seed.seed[:32])
if err != nil {
return nil, err
}
// Create a GCM (Galois/Counter Mode) instance
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
// Generate a random nonce
nonce := make([]byte, aesgcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
// Encrypt and authenticate the plaintext
ciphertext := aesgcm.Seal(nil, nonce, in, nil)
// Return nonce + ciphertext
return append(nonce, ciphertext...), nil
}
// SeedDecrypt decrypts data that was encrypted with SeedCrypt.
// This verifies and decrypts data encrypted by any peer with the same seed.
//
// Parameters:
// - in: The encrypted data (nonce + ciphertext)
//
// Returns:
// - The decrypted plaintext
// - An error if decryption fails
func (a *Agent) SeedDecrypt(in []byte) ([]byte, error) {
// Create a new AES cipher using the first 32 bytes of the seed as the key
block, err := aes.NewCipher(a.seed.seed[:32])
if err != nil {
return nil, err
}
// Create a GCM (Galois/Counter Mode) instance
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
// Check if the input is long enough to contain a nonce
if len(in) <= aesgcm.NonceSize() {
return nil, errors.New("decrypt: not enough data to decrypt input")
}
// Extract nonce and decrypt+verify the ciphertext
plaintext, err := aesgcm.Open(nil, in[:aesgcm.NonceSize()], in[aesgcm.NonceSize():], nil)
if err != nil {
return nil, err
}
return plaintext, nil
}
// seedData returns a binary representation of the seed with its timestamp.
// This is used for network transmission between peers.
//
// Returns:
// - A byte slice containing the seed timestamp and raw seed data
func (a *Agent) seedData() []byte {
// Convert the timestamp to binary form
ts := DbStamp(a.seed.ts).Bytes()
// Return timestamp + seed
return append(ts, a.seed.seed...)
}
// handleNewSeed processes a seed received from another peer.
// The fleet uses an oldest-wins policy for seed synchronization:
// - If the received seed is newer than our current seed, we keep our seed
// - If the received seed is older than our current seed, we adopt the received seed
// - If they have the same timestamp, the seed with the larger binary value wins
//
// Parameters:
// - s: The raw seed data received from a peer
// - t: The timestamp of the received seed
//
// Returns:
// - An error if the operation fails, nil otherwise
func (a *Agent) handleNewSeed(s []byte, t time.Time) error {
cur := a.seed
// Check if the received seed is newer than our current seed
if t.After(cur.ts) {
// Our seed is older, so we keep our seed
return nil
}
// Check if it's the same seed
if bytes.Equal(s, cur.seed) {
return nil // Already have this seed
}
// If timestamps are identical, compare the raw seed values
if t == cur.ts {
// Tie-breaker: compare the raw seed values
if bytes.Compare(s, cur.seed) > 0 {
// Received seed is larger, keep our seed
return nil
}
}
// The received seed takes precedence, adopt it
a.seed = makeSeed(s, t)
a.seed.WriteToDisk(a)
slog.Info(fmt.Sprintf("[fleet] Updated seed from peer, new seed ID = %s", a.SeedId()),
"event", "fleet:seed:update")
return nil
}