-
Notifications
You must be signed in to change notification settings - Fork 278
/
kms.go
354 lines (312 loc) · 11.7 KB
/
kms.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
343
344
345
346
347
348
349
350
351
352
353
354
package kms
import (
"context"
"fmt"
"sync"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/hashicorp/go-hclog"
wrapping "github.com/hashicorp/go-kms-wrapping"
"github.com/hashicorp/go-kms-wrapping/wrappers/aead"
"github.com/hashicorp/go-kms-wrapping/wrappers/multiwrapper"
)
// ExternalWrappers holds wrappers defined outside of Boundary, e.g. in its
// configuration file.
type ExternalWrappers struct {
m sync.RWMutex
root wrapping.Wrapper
workerAuth wrapping.Wrapper
recovery wrapping.Wrapper
}
// Root returns the wrapper for root keys
func (e *ExternalWrappers) Root() wrapping.Wrapper {
e.m.RLock()
defer e.m.RUnlock()
return e.root
}
// WorkerAuth returns the wrapper for worker authentication
func (e *ExternalWrappers) WorkerAuth() wrapping.Wrapper {
e.m.RLock()
defer e.m.RUnlock()
return e.workerAuth
}
// Recovery returns the wrapper for recovery operations
func (e *ExternalWrappers) Recovery() wrapping.Wrapper {
e.m.RLock()
defer e.m.RUnlock()
return e.recovery
}
// Kms is a way to access wrappers for a given scope and purpose. Since keys can
// never change, only be added or (eventually) removed, it opportunistically
// caches, going to the database as needed.
type Kms struct {
logger hclog.Logger
// scopePurposeCache holds a per-scope-purpose multiwrapper containing the
// current encrypting key and all previous key versions, for decryption
scopePurposeCache sync.Map
externalScopeCache map[string]*ExternalWrappers
externalScopeCacheMutex sync.RWMutex
repo *Repository
}
// NewKms takes in a repo and returns a Kms. Supported options: WithLogger.
func NewKms(repo *Repository, opt ...Option) (*Kms, error) {
const op = "kms.NewKms"
if repo == nil {
return nil, errors.New(errors.InvalidParameter, op, "missing underlying repo")
}
opts := getOpts(opt...)
return &Kms{
logger: opts.withLogger,
externalScopeCache: make(map[string]*ExternalWrappers),
repo: repo,
}, nil
}
// GetScopePurposeCache is used in test functions for validation. Since the
// tests need to be in a different package to avoid circular dependencies, this
// is exported.
func (k *Kms) GetScopePurposeCache() *sync.Map {
return &k.scopePurposeCache
}
// AddExternalWrappers allows setting the external keys.
//
// TODO: If we support more than one, e.g. for encrypting against many in case
// of a key loss, there will need to be some refactoring here to have the values
// being stored in the struct be a multiwrapper, but that's for a later project.
func (k *Kms) AddExternalWrappers(opt ...Option) error {
const op = "kms.AddExternalWrappers"
k.externalScopeCacheMutex.Lock()
defer k.externalScopeCacheMutex.Unlock()
ext := k.externalScopeCache[scope.Global.String()]
if ext == nil {
ext = &ExternalWrappers{}
}
ext.m.Lock()
defer ext.m.Unlock()
opts := getOpts(opt...)
if opts.withRootWrapper != nil {
ext.root = opts.withRootWrapper
if ext.root.KeyID() == "" {
return errors.New(errors.InvalidParameter, op, "root wrapper has no key ID")
}
}
if opts.withWorkerAuthWrapper != nil {
ext.workerAuth = opts.withWorkerAuthWrapper
if ext.workerAuth.KeyID() == "" {
return errors.New(errors.InvalidParameter, op, "worker auth wrapper has no key ID")
}
}
if opts.withRecoveryWrapper != nil {
ext.recovery = opts.withRecoveryWrapper
if ext.recovery.KeyID() == "" {
return errors.New(errors.InvalidParameter, op, "recovery wrapper has no key ID")
}
}
k.externalScopeCache[scope.Global.String()] = ext
return nil
}
func (k *Kms) GetExternalWrappers() *ExternalWrappers {
k.externalScopeCacheMutex.RLock()
defer k.externalScopeCacheMutex.RUnlock()
ext := k.externalScopeCache[scope.Global.String()]
ext.m.RLock()
defer ext.m.RUnlock()
ret := &ExternalWrappers{
root: ext.root,
workerAuth: ext.workerAuth,
recovery: ext.recovery,
}
return ret
}
func generateKeyId(scopeId string, purpose KeyPurpose, version uint32) string {
return fmt.Sprintf("%s_%s_%d", scopeId, purpose, version)
}
// GetWrapper returns a wrapper for the given scope and purpose. When a keyId is
// passed, it will ensure that the returning wrapper has that key ID in the
// multiwrapper. This is not necesary for encryption but should be supplied for
// decryption.
func (k *Kms) GetWrapper(ctx context.Context, scopeId string, purpose KeyPurpose, opt ...Option) (wrapping.Wrapper, error) {
const op = "kms.GetWrapper"
if scopeId == "" {
return nil, errors.New(errors.InvalidParameter, op, "missing scope id")
}
switch purpose {
case KeyPurposeOplog, KeyPurposeDatabase, KeyPurposeTokens, KeyPurposeSessions:
case KeyPurposeUnknown:
return nil, errors.New(errors.InvalidParameter, op, "missing key purpose")
default:
return nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("unsupported purpose %q", purpose))
}
opts := getOpts(opt...)
// Fast-path: we have a valid key at the scope/purpose. Verify the key with
// that ID is in the multiwrapper; if not, fall through to reload from the
// DB.
val, ok := k.scopePurposeCache.Load(scopeId + purpose.String())
if ok {
wrapper := val.(*multiwrapper.MultiWrapper)
if opts.withKeyId == "" || wrapper.WrapperForKeyID(opts.withKeyId) != nil {
return wrapper, nil
}
// Fall through to refresh our multiwrapper for this scope/purpose from the DB
}
// We don't have it cached, so we'll need to read from the database. Get the
// root for the scope as we'll need it to decrypt the value coming from the
// DB. We don't cache the roots as we expect that after a few calls the
// scope-purpose cache will catch everything in steady-state.
rootWrapper, rootKeyId, err := k.loadRoot(ctx, scopeId, opt...)
if err != nil {
return nil, errors.Wrap(err, op, errors.WithMsg(fmt.Sprintf("error loading root key for scope %s", scopeId)))
}
if rootWrapper == nil {
return nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("got nil root wrapper for scope %s", scopeId))
}
wrapper, err := k.loadDek(ctx, scopeId, purpose, rootWrapper, rootKeyId, opt...)
if err != nil {
return nil, errors.Wrap(err, op, errors.WithMsg(fmt.Sprintf("error loading %s for scope %s", purpose.String(), scopeId)))
}
k.scopePurposeCache.Store(scopeId+purpose.String(), wrapper)
return wrapper, nil
}
func (k *Kms) loadRoot(ctx context.Context, scopeId string, opt ...Option) (*multiwrapper.MultiWrapper, string, error) {
const op = "kms.loadRoot"
opts := getOpts(opt...)
repo := opts.withRepository
if repo == nil {
repo = k.repo
}
rootKeys, err := repo.ListRootKeys(ctx)
if err != nil {
return nil, "", errors.Wrap(err, op)
}
var rootKeyId string
for _, k := range rootKeys {
if k.GetScopeId() == scopeId {
rootKeyId = k.GetPrivateId()
break
}
}
if rootKeyId == "" {
return nil, "", errors.New(errors.KeyNotFound, op, fmt.Sprintf("missing root key for scope %s", scopeId))
}
// Now: find the external KMS that can be used to decrypt the root values
// from the DB.
k.externalScopeCacheMutex.Lock()
externalWrappers := k.externalScopeCache[scope.Global.String()]
k.externalScopeCacheMutex.Unlock()
if externalWrappers == nil {
return nil, "", errors.New(errors.KeyNotFound, op, "could not find kms information at either the needed scope or global fallback")
}
externalWrappers.m.RLock()
defer externalWrappers.m.RUnlock()
if externalWrappers.root == nil {
return nil, "", errors.New(errors.InvalidParameter, op, fmt.Sprintf("root key wrapper for scope %s is nil", scopeId))
}
rootKeyVersions, err := repo.ListRootKeyVersions(ctx, externalWrappers.root, rootKeyId, WithOrder("version desc"))
if err != nil {
return nil, "", errors.Wrap(err, op, errors.WithMsg(fmt.Sprintf("error looking up root key versions for scope %s with key ID %s", scopeId, externalWrappers.root.KeyID())))
}
if len(rootKeyVersions) == 0 {
return nil, "", errors.New(errors.KeyNotFound, op, fmt.Sprintf("no root key versions found for scope %s", scopeId))
}
var multi *multiwrapper.MultiWrapper
for i, key := range rootKeyVersions {
wrapper := aead.NewWrapper(nil)
if _, err := wrapper.SetConfig(map[string]string{
"key_id": key.GetPrivateId(),
}); err != nil {
return nil, "", errors.Wrap(err, op, errors.WithMsg(fmt.Sprintf("error setting config on aead root wrapper in scope %s", scopeId)))
}
if err := wrapper.SetAESGCMKeyBytes(key.GetKey()); err != nil {
return nil, "", errors.Wrap(err, op, errors.WithMsg(fmt.Sprintf("error setting key bytes on aead root wrapper in scope %s", scopeId)))
}
if i == 0 {
multi = multiwrapper.NewMultiWrapper(wrapper)
} else {
multi.AddWrapper(wrapper)
}
}
return multi, rootKeyId, nil
}
// Dek is an interface wrapping dek types to allow a lot less switching in loadDek
type Dek interface {
GetRootKeyId() string
GetPrivateId() string
}
// DekVersion is an interface wrapping versioned dek types to allow a lot less switching in loadDek
type DekVersion interface {
GetPrivateId() string
GetKey() []byte
}
func (k *Kms) loadDek(ctx context.Context, scopeId string, purpose KeyPurpose, rootWrapper wrapping.Wrapper, rootKeyId string, opt ...Option) (*multiwrapper.MultiWrapper, error) {
const op = "kms.loadDek"
if rootWrapper == nil {
return nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("nil root wrapper for scope %s", scopeId))
}
if rootKeyId == "" {
return nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("missing root key ID for scope %s", scopeId))
}
opts := getOpts(opt...)
repo := opts.withRepository
if repo == nil {
repo = k.repo
}
var keys []Dek
var err error
switch purpose {
case KeyPurposeDatabase:
keys, err = repo.ListDatabaseKeys(ctx)
case KeyPurposeOplog:
keys, err = repo.ListOplogKeys(ctx)
case KeyPurposeTokens:
keys, err = repo.ListTokenKeys(ctx)
case KeyPurposeSessions:
keys, err = repo.ListSessionKeys(ctx)
}
if err != nil {
return nil, errors.Wrap(err, op, errors.WithMsg("error listing root keys"))
}
var keyId string
for _, k := range keys {
if k.GetRootKeyId() == rootKeyId {
keyId = k.GetPrivateId()
break
}
}
if keyId == "" {
return nil, errors.New(errors.KeyNotFound, op, fmt.Sprintf("error finding %s key for scope %s", purpose.String(), scopeId))
}
var keyVersions []DekVersion
switch purpose {
case KeyPurposeDatabase:
keyVersions, err = repo.ListDatabaseKeyVersions(ctx, rootWrapper, keyId, WithOrder("version desc"))
case KeyPurposeOplog:
keyVersions, err = repo.ListOplogKeyVersions(ctx, rootWrapper, keyId, WithOrder("version desc"))
case KeyPurposeTokens:
keyVersions, err = repo.ListTokenKeyVersions(ctx, rootWrapper, keyId, WithOrder("version desc"))
case KeyPurposeSessions:
keyVersions, err = repo.ListSessionKeyVersions(ctx, rootWrapper, keyId, WithOrder("version desc"))
}
if err != nil {
return nil, errors.Wrap(err, op, errors.WithMsg(fmt.Sprintf("error looking up %s key versions for scope %s with key ID %s", purpose.String(), scopeId, rootWrapper.KeyID())))
}
if len(keyVersions) == 0 {
return nil, errors.New(errors.KeyNotFound, op, fmt.Sprintf("no %s key versions found for scope %s", purpose.String(), scopeId))
}
var multi *multiwrapper.MultiWrapper
for i, keyVersion := range keyVersions {
wrapper := aead.NewWrapper(nil)
if _, err := wrapper.SetConfig(map[string]string{
"key_id": keyVersion.GetPrivateId(),
}); err != nil {
return nil, errors.Wrap(err, op, errors.WithMsg(fmt.Sprintf("error setting config on aead %s wrapper in scope %s", purpose.String(), scopeId)))
}
if err := wrapper.SetAESGCMKeyBytes(keyVersion.GetKey()); err != nil {
return nil, errors.Wrap(err, op, errors.WithMsg(fmt.Sprintf("error setting key bytes on aead %s wrapper in scope %s", purpose.String(), scopeId)))
}
if i == 0 {
multi = multiwrapper.NewMultiWrapper(wrapper)
} else {
multi.AddWrapper(wrapper)
}
}
return multi, nil
}